Skip to content

Add Playwright E2E tests with sandbox isolation#488

Open
mveytsman wants to merge 12 commits intomainfrom
claude/fix-pr-429-tests-TvcIN
Open

Add Playwright E2E tests with sandbox isolation#488
mveytsman wants to merge 12 commits intomainfrom
claude/fix-pr-429-tests-TvcIN

Conversation

@mveytsman
Copy link
Copy Markdown
Member

@mveytsman mveytsman commented Apr 3, 2026

Describe your changes

This PR adds a comprehensive end-to-end testing suite using Playwright with database sandbox isolation. The test suite covers critical user workflows including authentication, program management, campaign creation, and navigation.

Key additions:

  1. E2E Test Suite (test/e2e/all.spec.ts):

    • Login/logout flows with validation
    • Program creation and editing
    • Campaign creation for different dates
    • Rider search functionality
    • Navigation and authentication redirects
  2. Sandbox Test Helper (test/e2e/helpers/sandbox.ts):

    • Implements Ecto sandbox isolation for test database
    • Automatically sets up and tears down test sessions
    • Injects session IDs into HTTP headers and LiveView connections
  3. Playwright Configuration:

    • Added playwright.config.ts with test configuration
    • Added package.json with Playwright and Faker dependencies
    • Tests run against test database on port 4002
  4. Minor fixes:

    • Fixed typo: "Photo Descriotion" → "Photo Description"
    • Fixed campaign form to properly display program names in dropdown
    • Updated .gitignore to exclude test artifacts

Test execution:

Tests run against the test database (not dev), ensuring development data safety. The sandbox approach provides complete test isolation without manual cleanup.

Checklist before requesting a review

  • I have performed a self-review of my code
  • If it is a core feature, I have added tests (comprehensive E2E test suite added)
  • Are there other PRs or Issues that I should link to here?
  • Will this be part of a product update? If yes, please write one phrase about this update in the description above.

https://claude.ai/code/session_01L8gaYbunRb7cRLN13P8Xqf


Summary by cubic

Add Playwright end-to-end tests with per-test Ecto sandbox isolation, running against the test DB on port 4002. Covers core flows (auth, programs, campaigns, riders, navigation) and adds a mix alias to run the test server.

  • New Features

    • E2E suite under test/e2e using @playwright/test and @faker-js/faker.
    • Sandbox helper creates a session via /sandbox and injects x-session-id for HTTP and LiveView.
    • Configured baseURL to 4002, single worker, Chromium only; artifacts ignored in git.
    • Tests cover login/logout, program create/edit, campaign creation (today/next week), rider search, and sidebar links + auth redirects.
    • Minor fixes for testability: program select now uses labels; “Photo Description” typo fixed.
  • Migration

    • From test/e2e: run npm install.
    • In repo root: MIX_ENV=test mix test.e2e to start the server on 4002.
    • In test/e2e: run npm run test:ui or npm test.
    • Runs against the test database; dev data is safe.

Written for commit 8cb6724. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Added comprehensive end-to-end testing framework with Playwright
  • Documentation

    • Added E2E testing workflow guide including installation, server startup, and troubleshooting steps
  • Bug Fixes

    • Fixed typo in program form label
  • Tests

    • Added E2E test suite covering authentication, program management, campaigns, riders, and navigation
  • Chores

    • Updated configuration and dependencies to support E2E testing infrastructure

teesloane and others added 12 commits August 11, 2025 18:46
- Use MIX_ENV=test so e2e tests run against the test database (port 4002)
  instead of wiping the dev database, addressing reviewer feedback
- Remove duplicate sandbox plug and debug logging from endpoint.ex
- Revert dev.exs changes (sandbox pool, commented logger line)
- Update mix alias test.e2e to seed the test DB instead of dropping dev
- Use Playwright baseURL config instead of hardcoded localhost:4000
- Fix Campaigns tests to use per-test sandbox (was bypassing via beforeAll)
- Only run Chromium (remove Firefox/Safari as PR author suggested)
- Remove committed test-results/.last-run.json, add to gitignore
- Add Riders and Navigation test suites
- Clean up test code: proper types, remove unnecessary clicks, semicolons

https://claude.ai/code/session_01L8gaYbunRb7cRLN13P8Xqf
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 3, 2026

📝 Walkthrough

Walkthrough

This PR introduces end-to-end testing infrastructure using Playwright, adding test configuration files, a comprehensive test suite covering authentication and core workflows, sandbox helpers for database isolation, and supporting documentation and configuration updates.

Changes

Cohort / File(s) Summary
Playwright E2E Testing Configuration
test/e2e/playwright.config.ts, test/e2e/package.json, test/e2e/.gitignore
Adds Playwright configuration with single-worker execution, HTML reporting, and trace collection; defines npm scripts for test execution (test, test:ui, test:withreport); ignores test artifacts and dependencies.
Test Fixture & Helper
test/e2e/helpers/sandbox.ts
Introduces Playwright test helper that provisions sandbox sessions via HTTP POST/DELETE to /sandbox, injects session IDs into request headers, and implements per-test isolation and cleanup.
Test Suite Implementation
test/e2e/all.spec.ts
Comprehensive Playwright test suite covering authentication (login, logout, phone validation), program creation and editing, campaign creation with date selection, rider search, and navigation/authorization flows; includes local helper functions for login, date computation, and form interactions.
Build Configuration
mix.exs
Adds new Mix alias test.e2e that executes database setup (ecto.create, ecto.migrate, seeds) followed by starting the Phoenix test server.
Documentation & Root Gitignore
README.md, .gitignore
Documents the E2E testing workflow (test database protection, dependency installation, test server startup on port 4002, Ecto sandbox isolation, troubleshooting for network-dependent calls); adds /test-results/ to gitignore.
UI & Code Refinements
lib/bike_brigade_web/live/campaign_live/form_component.ex, lib/bike_brigade_web/live/program_live/form_component.html.heex, config/dev.exs
Refactors program options from 2-tuples to keyword entries with explicit key, value, and label fields; corrects "Photo Descriotion" typo to "Photo Description"; removes blank line in dev config.

Sequence Diagram(s)

sequenceDiagram
    participant Test as Playwright Test
    participant Phoenix as Phoenix Server
    participant Sandbox as Ecto Sandbox
    participant Context as Request Context

    Test->>Phoenix: POST /sandbox (setup)
    Phoenix->>Sandbox: Create isolated session
    Sandbox-->>Phoenix: Return sessionId
    Phoenix-->>Test: sessionId in response

    Test->>Context: Register route handler
    Context->>Context: Store sessionId for header injection

    Test->>Test: Run test with injected headers
    Test->>Context: Make requests (headers include x-session-id)
    Context->>Phoenix: Route with x-session-id header
    Phoenix->>Sandbox: Execute in isolated context
    Sandbox-->>Phoenix: Return results
    Phoenix-->>Test: Response

    Test->>Phoenix: DELETE /sandbox (teardown)
    Phoenix->>Sandbox: Cleanup isolated session
    Sandbox-->>Phoenix: Session destroyed
    Phoenix-->>Test: Confirmed
Loading
sequenceDiagram
    participant Test as E2E Test
    participant UI as Browser/UI
    participant Phoenix as Phoenix Server
    participant DB as Database

    Test->>UI: Navigate to /login
    UI->>Phoenix: GET /login
    Phoenix->>UI: Render login form
    
    Test->>UI: Fill phone number & submit
    UI->>Phoenix: POST login credentials
    Phoenix->>DB: Verify credentials
    DB-->>Phoenix: Success
    Phoenix->>UI: Redirect with flash message
    
    Test->>UI: Assert flash message present
    Test->>UI: Verify page navigated
    UI-->>Test: Assertion passes
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Poem

🐰✨ Hops with glee at tests so tight,
Playwright scripts run morning and night,
Sandbox gardens grow fresh and clean,
E2E flows like never before seen,
Quality blooms in every test line! 🌱

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the primary change: adding Playwright end-to-end tests with sandbox isolation for database test isolation.
Description check ✅ Passed The description comprehensively covers the changes with key additions, test execution details, and follows the template structure with completed checklist items.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/fix-pr-429-tests-TvcIN

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/bike_brigade_web/live/campaign_live/form_component.ex (1)

75-79: ⚠️ Potential issue | 🟠 Major

Heterogeneous options format will cause issues with options_for_select/2.

The code mixes a tuple {"", nil} with keyword list entries [key: p.name, value: p.id, label: p.name]. According to the Phoenix HTML docs, options_for_select/2 expects a homogeneous list. Additionally, the keyword list format here uses non-standard keys—:label is not a valid option key. The standard formats are either 2-tuples {label, value} (as used in messaging_form_component.ex) or keyword lists with :key and :value keys only.

Use consistent tuple format instead:

Proposed fix
   def mount(socket) do
-    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}

     {:ok,
      socket
      |> assign(:program_options, [{"", nil} | programs])}
   end

Run scripts/format before committing.

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

In `@lib/bike_brigade_web/live/campaign_live/form_component.ex` around lines 75 -
79, The program options are built heterogeneously (a tuple {"", nil} mixed with
keyword lists [key: ..., value: ..., label: ...]) which breaks
options_for_select/2; change the construction in the block that builds programs
(where Delivery.list_programs() is mapped and assigned to :program_options) to
produce a homogeneous list of 2-tuples like {label, value} (e.g., {p.name,
p.id}) and prepend {"", nil} to that list instead of using keyword lists, then
run scripts/format before committing.
🧹 Nitpick comments (3)
test/e2e/.gitignore (1)

1-1: Remove the leading blank line.

Starting the file with a blank line is unconventional for .gitignore files.

✨ Proposed fix
-
 # Playwright
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/e2e/.gitignore` at line 1, Remove the leading blank line at the top of
the .gitignore file so the file starts immediately with the first ignore
pattern; open the .gitignore, delete the initial empty line (the "leading blank
line" at the top of the file), and ensure the first non-empty line is the first
ignore pattern.
test/e2e/helpers/sandbox.ts (1)

5-5: Consider using proper Playwright types instead of any.

Using any for context, route, and request parameters loses TypeScript's type-checking benefits. Playwright provides proper types for these.

♻️ Suggested type improvements
+import { request, test as base, BrowserContext, Route, Request } from '@playwright/test';
-import { request, test as base } from '@playwright/test';

-async function setupSandbox(context: any) {
+async function setupSandbox(context: BrowserContext) {
   // ...
-  await context.route('**/*', async (route: any, request: any) => {
+  await context.route('**/*', async (route: Route, req: Request) => {
-    const headers = request.headers();
+    const headers = req.headers();

Also applies to: 16-16

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

In `@test/e2e/helpers/sandbox.ts` at line 5, Replace the use of any by importing
and using Playwright types: change the function signature of setupSandbox to
accept a BrowserContext (or BrowserContext from '@playwright/test' /
'playwright'), and update any handler parameter types from any to Route and
Request (e.g., route: Route, request: Request) where they appear (including the
usages flagged at line ~16). Ensure you add the corresponding import(s) for
BrowserContext, Route, and Request at the top of the file so TypeScript can
type-check the context, route, and request parameters.
test/e2e/all.spec.ts (1)

72-75: Pagination navigation by index is fragile.

Using .nth(2) to navigate pagination assumes a fixed structure. If pagination changes (e.g., different number of page links), this test could break.

♻️ Consider a more resilient selector
-    await page.getByRole('navigation', { name: 'Pagination' }).getByRole('link').nth(2).click();
+    // Navigate to next page using a more descriptive selector if available
+    await page.getByRole('navigation', { name: 'Pagination' }).getByRole('link', { name: /next|2|→/i }).click();

Or if there's a "Next" button/link in the pagination, prefer that.

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

In `@test/e2e/all.spec.ts` around lines 72 - 75, The test uses a fragile
index-based selector page.getByRole('navigation', { name: 'Pagination'
}).getByRole('link').nth(2) to click a pagination link; replace it with a
resilient selector (e.g., click the "Next" pagination control via
page.getByRole('link', { name: /next/i }) or a stable aria-label/button text)
and ensure you await the navigation/update before asserting programName
visibility; update the click target and ensure the expectation still uses
page.getByText(programName).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- 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).

In `@test/e2e/helpers/sandbox.ts`:
- Around line 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.
- Around line 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.

---

Outside diff comments:
In `@lib/bike_brigade_web/live/campaign_live/form_component.ex`:
- Around line 75-79: The program options are built heterogeneously (a tuple {"",
nil} mixed with keyword lists [key: ..., value: ..., label: ...]) which breaks
options_for_select/2; change the construction in the block that builds programs
(where Delivery.list_programs() is mapped and assigned to :program_options) to
produce a homogeneous list of 2-tuples like {label, value} (e.g., {p.name,
p.id}) and prepend {"", nil} to that list instead of using keyword lists, then
run scripts/format before committing.

---

Nitpick comments:
In `@test/e2e/.gitignore`:
- Line 1: Remove the leading blank line at the top of the .gitignore file so the
file starts immediately with the first ignore pattern; open the .gitignore,
delete the initial empty line (the "leading blank line" at the top of the file),
and ensure the first non-empty line is the first ignore pattern.

In `@test/e2e/all.spec.ts`:
- Around line 72-75: The test uses a fragile index-based selector
page.getByRole('navigation', { name: 'Pagination' }).getByRole('link').nth(2) to
click a pagination link; replace it with a resilient selector (e.g., click the
"Next" pagination control via page.getByRole('link', { name: /next/i }) or a
stable aria-label/button text) and ensure you await the navigation/update before
asserting programName visibility; update the click target and ensure the
expectation still uses page.getByText(programName).

In `@test/e2e/helpers/sandbox.ts`:
- Line 5: Replace the use of any by importing and using Playwright types: change
the function signature of setupSandbox to accept a BrowserContext (or
BrowserContext from '@playwright/test' / 'playwright'), and update any handler
parameter types from any to Route and Request (e.g., route: Route, request:
Request) where they appear (including the usages flagged at line ~16). Ensure
you add the corresponding import(s) for BrowserContext, Route, and Request at
the top of the file so TypeScript can type-check the context, route, and request
parameters.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7623c374-3be0-4cdc-815d-cf3b81f41836

📥 Commits

Reviewing files that changed from the base of the PR and between a27f40f and 8cb6724.

⛔ Files ignored due to path filters (1)
  • test/e2e/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (11)
  • .gitignore
  • README.md
  • config/dev.exs
  • lib/bike_brigade_web/live/campaign_live/form_component.ex
  • lib/bike_brigade_web/live/program_live/form_component.html.heex
  • mix.exs
  • test/e2e/.gitignore
  • test/e2e/all.spec.ts
  • test/e2e/helpers/sandbox.ts
  • test/e2e/package.json
  • test/e2e/playwright.config.ts
💤 Files with no reviewable changes (1)
  • config/dev.exs

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).

Comment on lines +5 to +28
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;
}
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.

Comment on lines +30 to +37
async function teardownSandbox(sessionId: string) {
const requestContext = await request.newContext();
await requestContext.delete(`${BASE_URL}/sandbox`, {
headers: {
'x-session-id': sessionId
}
});
}
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.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

6 issues found across 12 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="lib/bike_brigade_web/live/campaign_live/form_component.ex">

<violation number="1" location="lib/bike_brigade_web/live/campaign_live/form_component.ex:75">
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.</violation>
</file>

<file name="test/e2e/helpers/sandbox.ts">

<violation number="1" location="test/e2e/helpers/sandbox.ts:6">
P2: The `APIRequestContext` created in `setupSandbox` is never disposed. Call `await requestContext.dispose()` after extracting the session ID to avoid leaking connections.</violation>

<violation number="2" location="test/e2e/helpers/sandbox.ts:13">
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.</violation>

<violation number="3" location="test/e2e/helpers/sandbox.ts:32">
P3: Same resource leak here — the `requestContext` is never disposed. Wrap in `try/finally` and call `await requestContext.dispose()` to clean up.</violation>
</file>

<file name="test/e2e/all.spec.ts">

<violation number="1" location="test/e2e/all.spec.ts:44">
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.</violation>

<violation number="2" location="test/e2e/all.spec.ts:49">
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.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

@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

}
});

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

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


// 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

// 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


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

@teesloane
Copy link
Copy Markdown
Contributor

Hype 💃

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants