diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index ed19afe07a..1940a89890 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -158,13 +158,18 @@ jobs: with: path: ~/.cache/ms-playwright key: playwright-${{ runner.os }}-${{ hashFiles('packages/e2e/package.json') }} + # On a key rotation (any packages/e2e dep bump) the fallback restores + # the previous browsers and `playwright install` downloads only the + # missing version instead of a full cold install. + restore-keys: | + playwright-${{ runner.os }}- - - name: Install Playwright browsers + - name: Install Playwright chromium if: steps.cache-playwright.outputs.cache-hit != 'true' working-directory: packages/e2e run: npx playwright install chromium --with-deps - - name: Install Playwright system deps + - name: Install Playwright chromium system deps if: steps.cache-playwright.outputs.cache-hit == 'true' working-directory: packages/e2e run: npx playwright install-deps chromium diff --git a/.github/workflows/storage-integration.yml b/.github/workflows/storage-integration.yml index 4a061c3b50..1a8750da7b 100644 --- a/.github/workflows/storage-integration.yml +++ b/.github/workflows/storage-integration.yml @@ -135,4 +135,4 @@ jobs: echo "::group::$f" bun test "$f" echo "::endgroup::" - done < <(find apps/api -name '*.integration.test.ts' | sort) + done < <(find apps/api packages -name '*.integration.test.ts' -not -path '*/node_modules/*' | sort) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f288dc0cc6..76d62ea433 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,6 +38,7 @@ jobs: outputs: relevant: ${{ steps.filter.outputs.relevant }} sandbox: ${{ steps.filter.outputs.sandbox }} + web: ${{ steps.filter.outputs.web }} steps: - name: Detect relevant changes id: filter @@ -53,10 +54,12 @@ jobs: '[release]:'*) echo "relevant=false" >> "$GITHUB_OUTPUT" echo "sandbox=false" >> "$GITHUB_OUTPUT" + echo "web=false" >> "$GITHUB_OUTPUT" ;; *) echo "relevant=true" >> "$GITHUB_OUTPUT" echo "sandbox=true" >> "$GITHUB_OUTPUT" + echo "web=true" >> "$GITHUB_OUTPUT" ;; esac exit 0 @@ -68,6 +71,7 @@ jobs: echo "Could not list PR files — running to be safe." echo "relevant=true" >> "$GITHUB_OUTPUT" echo "sandbox=true" >> "$GITHUB_OUTPUT" + echo "web=true" >> "$GITHUB_OUTPUT" exit 0 fi echo "Changed files:"; echo "$FILES" @@ -76,6 +80,7 @@ jobs: # change (the daemon bundle pulls in typegen, shared, and harness). relevant=false sandbox=false + web=false while IFS= read -r f; do [ -z "$f" ] && continue case "$f" in @@ -85,9 +90,13 @@ jobs: case "$f" in packages/sandbox/*|packages/typegen/*|packages/shared/*|packages/harness/*) sandbox=true ;; esac + case "$f" in + apps/web/*|packages/ui/*) web=true ;; + esac done <<< "$FILES" echo "relevant=$relevant" >> "$GITHUB_OUTPUT" echo "sandbox=$sandbox" >> "$GITHUB_OUTPUT" + echo "web=$web" >> "$GITHUB_OUTPUT" format: needs: changes @@ -233,3 +242,43 @@ jobs: - name: Tear down if: always() run: docker rm -f sandbox-smoke || true + + # Playwright component tests for the sections-editor field widgets + # (apps/web/ct). Self-contained — real chromium, no server/DB — so it lives + # here rather than e2e.yml, gated to web changes only. + component-tests: + needs: changes + if: needs.changes.outputs.web == 'true' + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Bun and install dependencies + uses: ./.github/actions/setup-bun + + - name: Cache Playwright browsers + id: cache-playwright + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-ct-${{ runner.os }}-${{ hashFiles('apps/web/package.json') }} + # On a key rotation (playwright version bump) the fallback restores + # the previous browsers and `playwright install` downloads only the + # missing version instead of everything. + restore-keys: | + playwright-ct-${{ runner.os }}- + + - name: Install Playwright chromium + if: steps.cache-playwright.outputs.cache-hit != 'true' + working-directory: apps/web + run: npx playwright install chromium --with-deps + + - name: Install Playwright chromium system deps + if: steps.cache-playwright.outputs.cache-hit == 'true' + working-directory: apps/web + run: npx playwright install-deps chromium + + - name: Run component tests + run: bun run --cwd=apps/web test:ct diff --git a/.gitignore b/.gitignore index 790f74d581..e8cb09f66f 100644 --- a/.gitignore +++ b/.gitignore @@ -90,6 +90,8 @@ studio-demo/ # Playwright apps/web/test-results/ apps/web/playwright-report/ +# Playwright component-testing's vite build cache (test:ct) +apps/web/ct/playwright/.cache/ apps/web/screenshots/ packages/e2e/test-results/ packages/e2e/playwright-report/ diff --git a/apps/api/src/harnesses/decopilot/built-in-tools/user-ask.e2e.test.ts b/apps/api/src/harnesses/decopilot/built-in-tools/user-ask.test.ts similarity index 98% rename from apps/api/src/harnesses/decopilot/built-in-tools/user-ask.e2e.test.ts rename to apps/api/src/harnesses/decopilot/built-in-tools/user-ask.test.ts index f2a7ca71bd..b8f20347f7 100644 --- a/apps/api/src/harnesses/decopilot/built-in-tools/user-ask.e2e.test.ts +++ b/apps/api/src/harnesses/decopilot/built-in-tools/user-ask.test.ts @@ -1,14 +1,13 @@ /** - * user_ask Built-in Tool E2E Tests + * user_ask built-in tool registration/schema tests * - * End-to-end tests that verify the complete integration of user_ask tool: + * Verifies the user_ask tool wiring in getBuiltInTools(): * - Tool is registered in getBuiltInTools() * - Tool has correct metadata (description, schemas) * - Tool has no execute function (client-side only) * - Input schema validates correctly * - Output schema is defined correctly * - * Run with: bun test user-ask.e2e.test.ts */ import { describe, expect, test } from "bun:test"; diff --git a/apps/web/ct/tests/array-object-drilldown.ct.tsx b/apps/web/ct/tests/array-object-drilldown.ct.tsx index b240dee586..64943fd6f7 100644 --- a/apps/web/ct/tests/array-object-drilldown.ct.tsx +++ b/apps/web/ct/tests/array-object-drilldown.ct.tsx @@ -1,8 +1,19 @@ import { expect, test } from "@playwright/experimental-ct-react"; +import type { Locator } from "@playwright/test"; import { SchemaFormHarness } from "../harness/schema-form-harness"; import { sectionWithProps, TEST_RESOLVE_TYPE } from "../harness/fixtures"; import { readBreadcrumb, readFormValue } from "../harness/ct-utils"; +/** + * The sortable row wrapper is a role=button whose accessible NAME concatenates + * the label with its inline action buttons ("Alpha Hide item Open actions for + * Alpha"), so an exact-name query no longer matches. Match rows by the visible + * label text instead (the inline action buttons are icon-only, and the array + * header label lives outside any button). + */ +const itemRow = (component: Locator, label: string) => + component.getByRole("button").filter({ hasText: label }); + /** * ArrayField with OBJECT items + breadcrumb drill-down, exercised through the * full SchemaFormHarness (raw JSON Schema -> resolveSchema -> SchemaForm). @@ -100,12 +111,8 @@ test("item rows are labelled by titleBy with a count badge", async ({ // Empty breadcrumb -> list view with both rows. await expect.poll(() => readBreadcrumb(component)).toEqual([]); - await expect( - component.getByRole("button", { name: "Alpha", exact: true }), - ).toBeVisible(); - await expect( - component.getByRole("button", { name: "Beta", exact: true }), - ).toBeVisible(); + await expect(itemRow(component, "Alpha")).toBeVisible(); + await expect(itemRow(component, "Beta")).toBeVisible(); // Count badge reflects the number of items. await expect(component.getByText("2", { exact: true })).toBeVisible(); @@ -123,7 +130,7 @@ test("clicking a row drills into that item and shows its Name value", async ({ />, ); - await component.getByRole("button", { name: "Alpha", exact: true }).click(); + await itemRow(component, "Alpha").click(); // Breadcrumb drilled into the Alpha row. await expect.poll(() => readBreadcrumb(component)).toContain("Alpha"); @@ -148,7 +155,7 @@ test("editing a drilled-in item updates the correct index", async ({ ); // Drill into the second row (index 1). - await component.getByRole("button", { name: "Beta", exact: true }).click(); + await itemRow(component, "Beta").click(); await expect.poll(() => readBreadcrumb(component)).toContain("Beta"); const nameInput = component.getByLabel("Name"); @@ -187,7 +194,7 @@ test("editing a label field whose value equals the array label keeps the editor />, ); - await component.getByRole("button", { name: "Banner", exact: true }).click(); + await itemRow(component, "Banner").click(); await expect .poll(() => readBreadcrumb(component)) .toEqual(["Banner", "Banner"]); @@ -225,7 +232,7 @@ test("editing a label to collide with an earlier sibling keeps the opened row", />, ); - await component.getByRole("button", { name: "Beta", exact: true }).click(); + await itemRow(component, "Beta").click(); const nameInput = component.getByLabel("Name"); await expect(nameInput).toHaveAttribute("id", "cards.1.name"); @@ -254,9 +261,11 @@ test("deleting a row removes the right item from the array", async ({ />, ); - // Open the actions menu for the Alpha row (button is in the DOM though hidden). + // Open the actions menu for the Alpha row (button is in the DOM though + // hidden). exact: true — the row wrapper is also a role=button whose + // accessible name CONTAINS "Open actions for Alpha". await component - .getByRole("button", { name: "Open actions for Alpha" }) + .getByRole("button", { name: "Open actions for Alpha", exact: true }) .click(); // DropdownMenu content is portaled onto document.body -> query via page. await page.getByRole("menuitem", { name: "Delete" }).click(); @@ -292,7 +301,5 @@ test("fallback label 'Item 1' when itemSchema has no titleBy/title and item has ); await expect.poll(() => readBreadcrumb(component)).toEqual([]); - await expect( - component.getByRole("button", { name: "Item 1", exact: true }), - ).toBeVisible(); + await expect(itemRow(component, "Item 1")).toBeVisible(); }); diff --git a/apps/web/ct/tests/string-field.ct.tsx b/apps/web/ct/tests/string-field.ct.tsx index b6fdf0ab4d..efb2e122aa 100644 --- a/apps/web/ct/tests/string-field.ct.tsx +++ b/apps/web/ct/tests/string-field.ct.tsx @@ -74,7 +74,9 @@ test("textarea format renders a