Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,7 @@ config/google-service-account.json
/priv/static/images/media_bucket/
/screenshots/

/tmp/
/tmp/

# Playwright e2e test artifacts
/test-results/
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,25 @@ Copyright 2021 The Bike Brigade Inc.
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

## End to End testing with Playwright
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor grammar: "End to End" should be hyphenated.

Per standard English grammar, compound adjectives before a noun should be hyphenated: "End-to-End testing".

📝 Suggested fix
-## End to End testing with Playwright
+## End-to-End Testing with Playwright
🧰 Tools
🪛 LanguageTool

[grammar] ~117-~117: Use a hyphen to join words.
Context: ... limitations under the License. ## End to End testing with Playwright E2E tests r...

(QB_NEW_EN_HYPHEN)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 117, Update the README heading string "End to End testing
with Playwright" to use hyphens as a compound adjective: change it to
"End-to-End testing with Playwright" wherever the heading text appears (match
the exact heading text to find it).


E2E tests run against the **test** database (not dev), so your development data is safe.

To run e2e tests:

1. Navigate to `/test/e2e` and run `npm install`
2. From the root directory, start the test server: `MIX_ENV=test mix test.e2e`
3. In a new terminal, navigate to `/test/e2e` and run `npm run test:ui`

The test server runs on port 4002 using the Ecto sandbox for database isolation.

### Troubleshooting E2E tests

Sometimes e2e tests will fail due to network calls being made (e.g. Google Maps address fetching). Re-running individual tests often helps. If needed, adjust delays:

```js
await page
.locator('#location-form-location-input-open')
.pressSequentially("200 Yonge", { delay: 200 })
```
1 change: 0 additions & 1 deletion config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ config :bike_brigade, BikeBrigadeWeb.Endpoint,

# Do not include metadata nor timestamps in development logs
config :logger, :console, format: "[$level] $message\n"

# Disable the extremely annoying debug logging for the spreadsheet library
config :logger,
compile_time_purge_matching: [
Expand Down
2 changes: 1 addition & 1 deletion lib/bike_brigade_web/live/campaign_live/form_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ defmodule BikeBrigadeWeb.CampaignLive.FormComponent do

@impl true
def mount(socket) do
programs = for p <- Delivery.list_programs(), do: {p.name, p.id}
programs = for p <- Delivery.list_programs(), do: [key: p.name, value: p.id, label: p.name]
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: This keyword list format is not valid for Phoenix.HTML.Form.options_for_select/2. Each [key: name, value: id, label: name] keyword list will be interpreted as three separate options (:key, :value, :label) rather than a single option. The original {p.name, p.id} tuple format was correct.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/bike_brigade_web/live/campaign_live/form_component.ex, line 75:

<comment>This keyword list format is not valid for `Phoenix.HTML.Form.options_for_select/2`. Each `[key: name, value: id, label: name]` keyword list will be interpreted as three separate options (`:key`, `:value`, `:label`) rather than a single option. The original `{p.name, p.id}` tuple format was correct.</comment>

<file context>
@@ -72,7 +72,7 @@ defmodule BikeBrigadeWeb.CampaignLive.FormComponent do
   @impl true
   def mount(socket) do
-    programs = for p <- Delivery.list_programs(), do: {p.name, p.id}
+    programs = for p <- Delivery.list_programs(), do: [key: p.name, value: p.id, label: p.name]
 
     {:ok,
</file context>
Suggested change
programs = for p <- Delivery.list_programs(), do: [key: p.name, value: p.id, label: p.name]
programs = for p <- Delivery.list_programs(), do: {p.name, p.id}
Fix with Cubic


{:ok,
socket
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@
<.input
type="text"
field={f[:photo_description]}
label="Photo Descriotion"
label="Photo Description"
placeholder="Typical delivery size"
/>
<div class="grid grid-cols-3 gap-4 mt-2">
Expand Down
3 changes: 2 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ defmodule BikeBrigade.MixProject do
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
"ecto.reset": ["ecto.drop", "ecto.setup"],
test: ["ecto.create --quiet", "ecto.migrate", "test"],
"assets.deploy": ["esbuild default --minify", "tailwind default --minify", "phx.digest"]
"assets.deploy": ["esbuild default --minify", "tailwind default --minify", "phx.digest"],
"test.e2e": ["ecto.create --quiet", "ecto.migrate", "run priv/repo/seeds.exs", "phx.server"]
]
end

Expand Down
7 changes: 7 additions & 0 deletions test/e2e/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

# Playwright
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
178 changes: 178 additions & 0 deletions test/e2e/all.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { Page } from '@playwright/test';
import { test, expect } from "./helpers/sandbox";
import { faker } from '@faker-js/faker';

test.describe('Login and Logout', () => {
test('Can Login', async ({ page }) => {
await doLogin(page);
await expect(page.locator('#flash')).toContainText('Success! Welcome!');
});

test('Validates phone number', async ({ page }) => {
await page.goto('/login');
await page.getByRole('textbox', { name: 'Phone Number' }).fill('647555');
await page.getByRole('button', { name: 'Get Login Code' }).click();
await expect(page.locator('#login-form')).toContainText('phone number is not valid for Canada');
});

test('Cancel button returns to login page', async ({ page }) => {
await page.goto('/login');
await page.getByRole('textbox', { name: 'Phone Number' }).fill('6475555555');
await page.getByRole('button', { name: 'Get Login Code' }).click();
await page.getByRole('link', { name: 'Cancel' }).click();
await expect(page.getByRole('button')).toContainText('Get Login Code');
});

test('Can Logout', async ({ page }) => {
await doLogin(page);
await page.getByRole('link', { name: 'Log out' }).click();
await expect(page.locator('#flash')).toContainText('Success! Goodbye');
});
});

test.describe('Programs', () => {
test('Can create and edit program', async ({ page }) => {
const programName = faker.company.name();
await doLogin(page);

await createProgram(page, programName);
await expect(page.getByRole('link', { name: programName, exact: true })).toBeVisible();

// Edit the program
await page.getByRole('link', { name: `Edit , ${programName}` }).click();
await page.getByRole('textbox', { name: 'Campaign Blurb (please keep' }).fill('This is a test program that was updated');
await page.getByRole('button', { name: 'Save' }).click();
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Race condition: no wait between typing the address and clicking Save. The LiveLocation component geocodes the address asynchronously via phx-change, but the test clicks Save immediately after pressSequentially returns. Add a wait for the geocoded address to resolve (e.g., wait for the expanded location fields or map marker to appear) to avoid flaky failures.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At test/e2e/all.spec.ts, line 44:

<comment>Race condition: no wait between typing the address and clicking Save. The `LiveLocation` component geocodes the address asynchronously via `phx-change`, but the test clicks Save immediately after `pressSequentially` returns. Add a wait for the geocoded address to resolve (e.g., wait for the expanded location fields or map marker to appear) to avoid flaky failures.</comment>

<file context>
@@ -0,0 +1,178 @@
+    // Edit the program
+    await page.getByRole('link', { name: `Edit , ${programName}` }).click();
+    await page.getByRole('textbox', { name: 'Campaign Blurb (please keep' }).fill('This is a test program that was updated');
+    await page.getByRole('button', { name: 'Save' }).click();
+    await expect(page.getByText('Success! program updated')).toBeVisible();
+
</file context>
Fix with Cubic

await expect(page.getByText('Success! program updated')).toBeVisible();

// Verify the edit persisted
await page.getByRole('link', { name: `Edit , ${programName}` }).click();
await expect(page.getByLabel('Campaign Blurb (please keep')).toContainText('This is a test program that was updated');
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Use toHaveValue instead of toContainText to assert textarea field values. toContainText checks textContent, not the form field's value property, and is not the idiomatic Playwright assertion for input/textarea elements.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At test/e2e/all.spec.ts, line 49:

<comment>Use `toHaveValue` instead of `toContainText` to assert textarea field values. `toContainText` checks `textContent`, not the form field's `value` property, and is not the idiomatic Playwright assertion for input/textarea elements.</comment>

<file context>
@@ -0,0 +1,178 @@
+
+    // Verify the edit persisted
+    await page.getByRole('link', { name: `Edit , ${programName}` }).click();
+    await expect(page.getByLabel('Campaign Blurb (please keep')).toContainText('This is a test program that was updated');
+  });
+});
</file context>
Fix with Cubic

});
});

test.describe('Campaigns', () => {
test('Can create a campaign for today', async ({ page }) => {
const programName = faker.company.name();
await doLogin(page);
await createProgram(page, programName);

await createCampaign({ page, programName, numDays: 0 });
await expect(page.locator('#flash')).toContainText('Success! Campaign created successfully');
await expect(page.getByText(programName)).toBeVisible();
});

test('Can create a campaign for next week', async ({ page }) => {
const programName = faker.company.name();
await doLogin(page);
await createProgram(page, programName);

await createCampaign({ page, programName, numDays: 8 });
await expect(page.locator('#flash')).toContainText('Success! Campaign created successfully');

// Verify campaign shows up on next week's view
await page.getByRole('link', { name: 'Campaigns' }).click();
await page.getByRole('navigation', { name: 'Pagination' }).getByRole('link').nth(2).click();
await expect(page.getByText(programName)).toBeVisible();
});
});

test.describe('Riders', () => {
test('Can view the riders list', async ({ page }) => {
await doLogin(page);
await page.goto('/riders');
await expect(page.getByRole('heading', { name: 'Riders' })).toBeVisible();
});

test('Can search for a rider by name', async ({ page }) => {
await doLogin(page);
await page.goto('/riders');
const searchBox = page.getByRole('textbox', { name: /search/i });
await searchBox.fill('Dispatcher');
await expect(page.getByText('Dispatcher')).toBeVisible();
});
});

test.describe('Navigation', () => {
test('Sidebar navigation links work', async ({ page }) => {
await doLogin(page);

// Navigate to Campaigns
await page.getByRole('link', { name: 'Campaigns' }).click();
await expect(page).toHaveURL(/\/campaigns/);

// Navigate to Programs
await page.getByRole('link', { name: 'Programs' }).click();
await expect(page).toHaveURL(/\/programs/);

// Navigate to Riders
await page.getByRole('link', { name: 'Riders' }).click();
await expect(page).toHaveURL(/\/riders/);
});

test('Unauthenticated user is redirected to login', async ({ page }) => {
await page.goto('/campaigns');
await expect(page).toHaveURL(/\/login/);
});
});

// --- Helper functions ---

async function doLogin(page: Page) {
await page.goto('/login');
await page.getByRole('textbox', { name: 'Phone Number' }).fill('6475555555');
await page.getByRole('button', { name: 'Get Login Code' }).click();
await page.getByRole('textbox', { name: 'Authentication Code' }).fill('123456');
await page.getByRole('button', { name: 'Sign in' }).click();
}

function getDatePlusDays(daysToAdd: number): string {
const today = new Date();
const futureDate = new Date(today);
futureDate.setDate(today.getDate() + daysToAdd);

const year = futureDate.getFullYear();
const month = String(futureDate.getMonth() + 1).padStart(2, '0');
const day = String(futureDate.getDate()).padStart(2, '0');

return `${year}-${month}-${day}`;
}

async function createProgram(page: Page, programName: string) {
await page.goto('/programs');
await page.getByRole('link', { name: 'New Program' }).click();
await page.getByRole('textbox', { name: 'Name', exact: true }).fill(programName);
await page.getByRole('textbox', { name: 'Campaign Blurb (please keep' }).fill('This is a test program');
await page.getByRole('textbox', { name: 'About (internal description)' }).fill('This is an internal description');
await page.getByRole('textbox', { name: 'Start Date' }).fill('2025-02-12');
await page.getByRole('checkbox', { name: 'Public' }).check();
await page.getByRole('checkbox', { name: 'Hide Pickup Address' }).check();
await page.getByRole('button', { name: 'Add Schedule' }).click();

await page.getByRole('textbox', { name: 'Photo Description' }).fill('1 Large Box');
await page.getByRole('textbox', { name: 'Contact Name' }).fill('Joe Cool');
await page.getByRole('textbox', { name: 'Contact Email' }).fill('joecool@gmail.com');
await page.getByRole('textbox', { name: 'Contact Phone' }).fill('6475555554');
await page.getByRole('button', { name: 'Save' }).click();

// Edit the program to add items (can't add items during creation)
await page.getByRole('link', { name: programName, exact: true }).click();
await page.getByRole('link', { name: 'Edit', exact: true }).click();
await page.getByRole('link', { name: 'New Item' }).click();
await page.locator('#program-form_program_0_items_0_name').fill('An item');
await page.locator('#program-form_program_0_items_0_description').fill('5 lbs');
await page.getByRole('cell', { name: 'Foodshare Box' }).getByLabel('').selectOption('Food Hamper');
await page.getByRole('button', { name: 'Save' }).click();
}

async function createCampaign({ page, programName, numDays }: { page: Page; programName: string; numDays: number }) {
await page.goto('/campaigns/new');
await page.waitForSelector("body > .phx-connected");
await page.getByRole('textbox', { name: 'Delivery Date' }).fill(getDatePlusDays(numDays));

const programSelector = page.locator('#user-form_program_id');
await programSelector.selectOption({ label: programName });

await page.locator('#location-form-location-input-open').click();
await page.locator('#location-form-location-input-open').pressSequentially("200 Yonge", { delay: 200 });
await page.getByRole('button', { name: 'Save' }).click();
}
49 changes: 49 additions & 0 deletions test/e2e/helpers/sandbox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { request, test as base } from '@playwright/test';

const BASE_URL = 'http://localhost:4002';

async function setupSandbox(context: any) {
const requestContext = await request.newContext();
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The APIRequestContext created in setupSandbox is never disposed. Call await requestContext.dispose() after extracting the session ID to avoid leaking connections.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At test/e2e/helpers/sandbox.ts, line 6:

<comment>The `APIRequestContext` created in `setupSandbox` is never disposed. Call `await requestContext.dispose()` after extracting the session ID to avoid leaking connections.</comment>

<file context>
@@ -0,0 +1,49 @@
+const BASE_URL = 'http://localhost:4002';
+
+async function setupSandbox(context: any) {
+  const requestContext = await request.newContext();
+  const response = await requestContext.post(`${BASE_URL}/sandbox`, {
+    headers: {
</file context>
Fix with Cubic

const response = await requestContext.post(`${BASE_URL}/sandbox`, {
headers: {
'Cache-Control': 'no-store'
}
});

const sessionId = await response.text();
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The response from the /sandbox POST is used without checking its status. If the endpoint is unreachable or errors, sessionId will contain an error body, and every test will fail with misleading errors. Check response.ok() before proceeding.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At test/e2e/helpers/sandbox.ts, line 13:

<comment>The response from the `/sandbox` POST is used without checking its status. If the endpoint is unreachable or errors, `sessionId` will contain an error body, and every test will fail with misleading errors. Check `response.ok()` before proceeding.</comment>

<file context>
@@ -0,0 +1,49 @@
+    }
+  });
+
+  const sessionId = await response.text();
+
+  // Add sessionId header to all requests for sandbox isolation
</file context>
Fix with Cubic


// Add sessionId header to all requests for sandbox isolation
await context.route('**/*', async (route: any, request: any) => {
const headers = request.headers();
headers['x-session-id'] = sessionId;
await route.continue({ headers });
});

// Store sessionId for LiveView WebSocket connections
await context.addInitScript(({ sessionId }: { sessionId: string }) => {
(window as any).sessionId = sessionId;
}, { sessionId });

return sessionId;
}
Comment on lines +5 to +28
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Request context should be disposed after use to prevent resource leaks.

The requestContext created in setupSandbox is never disposed. Playwright's APIRequestContext holds resources that should be cleaned up.

🛡️ Proposed fix to dispose request context
 async function setupSandbox(context: any) {
   const requestContext = await request.newContext();
-  const response = await requestContext.post(`${BASE_URL}/sandbox`, {
-    headers: {
-      'Cache-Control': 'no-store'
-    }
-  });
-
-  const sessionId = await response.text();
+  try {
+    const response = await requestContext.post(`${BASE_URL}/sandbox`, {
+      headers: {
+        'Cache-Control': 'no-store'
+      }
+    });
+    const sessionId = await response.text();
+
+    // Add sessionId header to all requests for sandbox isolation
+    await context.route('**/*', async (route: any, request: any) => {
+      const headers = request.headers();
+      headers['x-session-id'] = sessionId;
+      await route.continue({ headers });
+    });
+
+    // Store sessionId for LiveView WebSocket connections
+    await context.addInitScript(({ sessionId }: { sessionId: string }) => {
+      (window as any).sessionId = sessionId;
+    }, { sessionId });
+
+    return sessionId;
+  } finally {
+    await requestContext.dispose();
+  }
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function setupSandbox(context: any) {
const requestContext = await request.newContext();
const response = await requestContext.post(`${BASE_URL}/sandbox`, {
headers: {
'Cache-Control': 'no-store'
}
});
const sessionId = await response.text();
// Add sessionId header to all requests for sandbox isolation
await context.route('**/*', async (route: any, request: any) => {
const headers = request.headers();
headers['x-session-id'] = sessionId;
await route.continue({ headers });
});
// Store sessionId for LiveView WebSocket connections
await context.addInitScript(({ sessionId }: { sessionId: string }) => {
(window as any).sessionId = sessionId;
}, { sessionId });
return sessionId;
}
async function setupSandbox(context: any) {
const requestContext = await request.newContext();
try {
const response = await requestContext.post(`${BASE_URL}/sandbox`, {
headers: {
'Cache-Control': 'no-store'
}
});
const sessionId = await response.text();
// Add sessionId header to all requests for sandbox isolation
await context.route('**/*', async (route: any, request: any) => {
const headers = request.headers();
headers['x-session-id'] = sessionId;
await route.continue({ headers });
});
// Store sessionId for LiveView WebSocket connections
await context.addInitScript(({ sessionId }: { sessionId: string }) => {
(window as any).sessionId = sessionId;
}, { sessionId });
return sessionId;
} finally {
await requestContext.dispose();
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/e2e/helpers/sandbox.ts` around lines 5 - 28, The requestContext created
in setupSandbox is never disposed, leaking Playwright resources; wrap the usage
of the requestContext (the variable requestContext in function setupSandbox) in
a try/finally (or ensure final cleanup) and call await requestContext.dispose()
in the finally block after you have obtained the response and sessionId but
before returning; keep the rest of the logic (the context.route handler and
context.addInitScript for sessionId) unchanged so sandbox isolation continues to
work.


async function teardownSandbox(sessionId: string) {
const requestContext = await request.newContext();
await requestContext.delete(`${BASE_URL}/sandbox`, {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Same resource leak here — the requestContext is never disposed. Wrap in try/finally and call await requestContext.dispose() to clean up.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At test/e2e/helpers/sandbox.ts, line 32:

<comment>Same resource leak here — the `requestContext` is never disposed. Wrap in `try/finally` and call `await requestContext.dispose()` to clean up.</comment>

<file context>
@@ -0,0 +1,49 @@
+
+async function teardownSandbox(sessionId: string) {
+  const requestContext = await request.newContext();
+  await requestContext.delete(`${BASE_URL}/sandbox`, {
+    headers: {
+      'x-session-id': sessionId
</file context>
Fix with Cubic

headers: {
'x-session-id': sessionId
}
});
}
Comment on lines +30 to +37
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Teardown request context also needs disposal.

Same resource leak concern as in setupSandbox.

🛡️ Proposed fix
 async function teardownSandbox(sessionId: string) {
   const requestContext = await request.newContext();
-  await requestContext.delete(`${BASE_URL}/sandbox`, {
-    headers: {
-      'x-session-id': sessionId
-    }
-  });
+  try {
+    await requestContext.delete(`${BASE_URL}/sandbox`, {
+      headers: {
+        'x-session-id': sessionId
+      }
+    });
+  } finally {
+    await requestContext.dispose();
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function teardownSandbox(sessionId: string) {
const requestContext = await request.newContext();
await requestContext.delete(`${BASE_URL}/sandbox`, {
headers: {
'x-session-id': sessionId
}
});
}
async function teardownSandbox(sessionId: string) {
const requestContext = await request.newContext();
try {
await requestContext.delete(`${BASE_URL}/sandbox`, {
headers: {
'x-session-id': sessionId
}
});
} finally {
await requestContext.dispose();
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/e2e/helpers/sandbox.ts` around lines 30 - 37, The teardownSandbox
function creates an API request context via request.newContext() but never
disposes it; after performing the DELETE call include explicit cleanup by
awaiting requestContext.dispose() (i.e., call await requestContext.dispose()
after the delete) to mirror setupSandbox and prevent the resource leak from
requestContext.


const test = base.extend({
context: async ({ context }, use) => {
const sessionId = await setupSandbox(context);
await use(context);
await teardownSandbox(sessionId);
}
});

const expect = base.expect;

export { test, expect };
Loading
Loading