From d7116f86913045ff0e00c3db9ad6fba02b910292 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 19 May 2026 14:00:04 -0700 Subject: [PATCH 1/2] test(cockpit-chat): add aimock e2e for c-generative-ui + c-a2ui MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Final 2 deferred surface caps from Task #4. Fixtures recorded via the per-cap record scripts shipped in PR #478: - c-generative-ui: 2 entries (dashboard + filter prompts). render_spec + query_recent_disruptions tool calls captured. Spec asserts the chat-generative-ui primitive mounts post-dashboard prompt. - c-a2ui: 2 entries (LAX-JFK + SFO-SEA prompts). BookingFormSpec tool calls captured. Spec asserts a2ui-surface primitive mounts. Both caps now in the cockpit-e2e matrix. Brings chat-cap aimock coverage to 10 of 11 caps (c-debug remains documented-as-skipped per PR #478 — viewer-only demo). Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 2 ++ cockpit/chat/a2ui/angular/e2e/c-a2ui.spec.ts | 18 +++++++++++ .../a2ui/angular/e2e/fixtures/c-a2ui.json | 32 +++++++++++++++++++ .../a2ui/angular/e2e/global-setup-impl.ts | 11 +++++++ .../a2ui/angular/e2e/playwright.config.ts | 18 +++++++++++ cockpit/chat/a2ui/angular/e2e/tsconfig.json | 14 ++++++++ cockpit/chat/a2ui/angular/project.json | 6 ++++ .../angular/e2e/c-generative-ui.spec.ts | 24 ++++++++++++++ .../angular/e2e/fixtures/c-generative-ui.json | 32 +++++++++++++++++++ .../angular/e2e/global-setup-impl.ts | 11 +++++++ .../angular/e2e/playwright.config.ts | 18 +++++++++++ .../generative-ui/angular/e2e/tsconfig.json | 14 ++++++++ .../chat/generative-ui/angular/project.json | 6 ++++ 13 files changed, 206 insertions(+) create mode 100644 cockpit/chat/a2ui/angular/e2e/c-a2ui.spec.ts create mode 100644 cockpit/chat/a2ui/angular/e2e/fixtures/c-a2ui.json create mode 100644 cockpit/chat/a2ui/angular/e2e/global-setup-impl.ts create mode 100644 cockpit/chat/a2ui/angular/e2e/playwright.config.ts create mode 100644 cockpit/chat/a2ui/angular/e2e/tsconfig.json create mode 100644 cockpit/chat/generative-ui/angular/e2e/c-generative-ui.spec.ts create mode 100644 cockpit/chat/generative-ui/angular/e2e/fixtures/c-generative-ui.json create mode 100644 cockpit/chat/generative-ui/angular/e2e/global-setup-impl.ts create mode 100644 cockpit/chat/generative-ui/angular/e2e/playwright.config.ts create mode 100644 cockpit/chat/generative-ui/angular/e2e/tsconfig.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c4c97c1e..6783ac35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -275,6 +275,8 @@ jobs: - { angular: cockpit-chat-threads-angular, python: cockpit/chat/threads/python } - { angular: cockpit-chat-timeline-angular, python: cockpit/chat/timeline/python } - { angular: cockpit-chat-theming-angular, python: cockpit/chat/theming/python } + - { angular: cockpit-chat-generative-ui-angular, python: cockpit/chat/generative-ui/python } + - { angular: cockpit-chat-a2ui-angular, python: cockpit/chat/a2ui/python } steps: - uses: actions/checkout@v6.0.2 - uses: actions/setup-node@v6.3.0 diff --git a/cockpit/chat/a2ui/angular/e2e/c-a2ui.spec.ts b/cockpit/chat/a2ui/angular/e2e/c-a2ui.spec.ts new file mode 100644 index 00000000..21cf5ad1 --- /dev/null +++ b/cockpit/chat/a2ui/angular/e2e/c-a2ui.spec.ts @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +import { test, expect } from '@playwright/test'; +import { submitAndWaitForResponse } from '../../../../../libs/e2e-harness/src'; + +test('c-a2ui: LAX-JFK prompt renders a2ui-surface with form spec', async ({ page }) => { + await submitAndWaitForResponse(page, 'I want to fly LAX to JFK'); + + // The BookingFormSpec tool call returns an A2UI form spec which the + // chat-lib content-classifier mounts as a with the + // booking surface_id. Presence proves the envelope parsed and the + // A2UI host wired up against the cap's catalog views. + await expect(page.locator('a2ui-surface')).toBeVisible(); +}); + +test('c-a2ui: SFO-SEA prompt also renders a2ui-surface', async ({ page }) => { + await submitAndWaitForResponse(page, 'I want to fly SFO to SEA'); + await expect(page.locator('a2ui-surface')).toBeVisible(); +}); diff --git a/cockpit/chat/a2ui/angular/e2e/fixtures/c-a2ui.json b/cockpit/chat/a2ui/angular/e2e/fixtures/c-a2ui.json new file mode 100644 index 00000000..4fc7ce05 --- /dev/null +++ b/cockpit/chat/a2ui/angular/e2e/fixtures/c-a2ui.json @@ -0,0 +1,32 @@ +{ + "fixtures": [ + { + "match": { + "userMessage": "I want to fly LAX to JFK" + }, + "response": { + "toolCalls": [ + { + "name": "BookingFormSpec", + "arguments": "{\"surface_id\":\"booking\",\"data_model\":{\"origin\":\"LAX\",\"dest\":\"JFK\",\"date\":\"\",\"passengers\":1,\"fare_class\":\"Economy\"},\"components\":[{\"id\":\"root\",\"component\":{\"Column\":{\"children\":{\"explicitList\":[\"card\"]}}}},{\"id\":\"card\",\"component\":{\"Card\":{\"child\":\"card_col\"}}},{\"id\":\"card_col\",\"component\":{\"Column\":{\"children\":{\"explicitList\":[\"title\",\"origin\",\"dest\",\"date\",\"passengers\",\"fare\",\"submit\"]}}}},{\"id\":\"title\",\"component\":{\"Text\":{\"text\":\"Book a flight\",\"usageHint\":\"h2\"}}},{\"id\":\"origin\",\"component\":{\"MultipleChoice\":{\"label\":\"Origin\",\"options\":[{\"label\":\"LAX\",\"value\":\"LAX\"},{\"label\":\"JFK\",\"value\":\"JFK\"},{\"label\":\"SFO\",\"value\":\"SFO\"},{\"label\":\"ORD\",\"value\":\"ORD\"},{\"label\":\"BOS\",\"value\":\"BOS\"},{\"label\":\"ATL\",\"value\":\"ATL\"},{\"label\":\"DFW\",\"value\":\"DFW\"},{\"label\":\"SEA\",\"value\":\"SEA\"},{\"label\":\"MIA\",\"value\":\"MIA\"},{\"label\":\"DEN\",\"value\":\"DEN\"}],\"selections\":{\"path\":\"/origin\"},\"maxAllowedSelections\":1}}},{\"id\":\"dest\",\"component\":{\"MultipleChoice\":{\"label\":\"Destination\",\"options\":[{\"label\":\"LAX\",\"value\":\"LAX\"},{\"label\":\"JFK\",\"value\":\"JFK\"},{\"label\":\"SFO\",\"value\":\"SFO\"},{\"label\":\"ORD\",\"value\":\"ORD\"},{\"label\":\"BOS\",\"value\":\"BOS\"},{\"label\":\"ATL\",\"value\":\"ATL\"},{\"label\":\"DFW\",\"value\":\"DFW\"},{\"label\":\"SEA\",\"value\":\"SEA\"},{\"label\":\"MIA\",\"value\":\"MIA\"},{\"label\":\"DEN\",\"value\":\"DEN\"}],\"selections\":{\"path\":\"/dest\"},\"maxAllowedSelections\":1}}},{\"id\":\"date\",\"component\":{\"TextField\":{\"label\":\"Departure date (YYYY-MM-DD)\",\"text\":{\"path\":\"/date\"},\"textFieldType\":\"date\"}}},{\"id\":\"passengers\",\"component\":{\"TextField\":{\"label\":\"Passengers\",\"text\":{\"path\":\"/passengers\"},\"textFieldType\":\"number\"}}},{\"id\":\"fare\",\"component\":{\"MultipleChoice\":{\"label\":\"Fare class\",\"options\":[{\"label\":\"Economy\",\"value\":\"Economy\"},{\"label\":\"Premium\",\"value\":\"Premium\"},{\"label\":\"Business\",\"value\":\"Business\"},{\"label\":\"First\",\"value\":\"First\"}],\"selections\":{\"path\":\"/fare_class\"},\"maxAllowedSelections\":1}}},{\"id\":\"submit_label\",\"component\":{\"Text\":{\"text\":\"Search flights\"}}},{\"id\":\"submit\",\"component\":{\"Button\":{\"child\":\"submit_label\",\"primary\":true,\"action\":{\"name\":\"bookingSubmit\",\"context\":[{\"key\":\"formId\",\"value\":\"booking\"},{\"key\":\"origin\",\"value\":{\"path\":\"/origin\"}},{\"key\":\"dest\",\"value\":{\"path\":\"/dest\"}},{\"key\":\"date\",\"value\":{\"path\":\"/date\"}},{\"key\":\"passengers\",\"value\":{\"path\":\"/passengers\"}},{\"key\":\"fare_class\",\"value\":{\"path\":\"/fare_class\"}}]}}}}]}", + "id": "call_DiKu9KXnOKtbRcNBtDlXwTzx" + } + ] + } + }, + { + "match": { + "userMessage": "I want to fly SFO to SEA" + }, + "response": { + "toolCalls": [ + { + "name": "BookingFormSpec", + "arguments": "{\"surface_id\":\"booking\",\"data_model\":{\"origin\":\"SFO\",\"dest\":\"SEA\",\"date\":\"\",\"passengers\":1,\"fare_class\":\"Economy\"},\"components\":[{\"id\":\"root\",\"component\":{\"Column\":{\"children\":{\"explicitList\":[\"card\"]}}}},{\"id\":\"card\",\"component\":{\"Card\":{\"child\":\"card_col\"}}},{\"id\":\"card_col\",\"component\":{\"Column\":{\"children\":{\"explicitList\":[\"title\",\"origin\",\"dest\",\"date\",\"passengers\",\"fare\",\"submit\"]}}}},{\"id\":\"title\",\"component\":{\"Text\":{\"text\":\"Book a flight\",\"usageHint\":\"h2\"}}},{\"id\":\"origin\",\"component\":{\"MultipleChoice\":{\"label\":\"Origin\",\"options\":[{\"label\":\"LAX\",\"value\":\"LAX\"},{\"label\":\"JFK\",\"value\":\"JFK\"},{\"label\":\"SFO\",\"value\":\"SFO\"},{\"label\":\"ORD\",\"value\":\"ORD\"},{\"label\":\"BOS\",\"value\":\"BOS\"},{\"label\":\"ATL\",\"value\":\"ATL\"},{\"label\":\"DFW\",\"value\":\"DFW\"},{\"label\":\"SEA\",\"value\":\"SEA\"},{\"label\":\"MIA\",\"value\":\"MIA\"},{\"label\":\"DEN\",\"value\":\"DEN\"}],\"selections\":{\"path\":\"/origin\"},\"maxAllowedSelections\":1}}},{\"id\":\"dest\",\"component\":{\"MultipleChoice\":{\"label\":\"Destination\",\"options\":[{\"label\":\"LAX\",\"value\":\"LAX\"},{\"label\":\"JFK\",\"value\":\"JFK\"},{\"label\":\"SFO\",\"value\":\"SFO\"},{\"label\":\"ORD\",\"value\":\"ORD\"},{\"label\":\"BOS\",\"value\":\"BOS\"},{\"label\":\"ATL\",\"value\":\"ATL\"},{\"label\":\"DFW\",\"value\":\"DFW\"},{\"label\":\"SEA\",\"value\":\"SEA\"},{\"label\":\"MIA\",\"value\":\"MIA\"},{\"label\":\"DEN\",\"value\":\"DEN\"}],\"selections\":{\"path\":\"/dest\"},\"maxAllowedSelections\":1}}},{\"id\":\"date\",\"component\":{\"TextField\":{\"label\":\"Departure date (YYYY-MM-DD)\",\"text\":{\"path\":\"/date\"},\"textFieldType\":\"date\"}}},{\"id\":\"passengers\",\"component\":{\"TextField\":{\"label\":\"Passengers\",\"text\":{\"path\":\"/passengers\"},\"textFieldType\":\"number\"}}},{\"id\":\"fare\",\"component\":{\"MultipleChoice\":{\"label\":\"Fare class\",\"options\":[{\"label\":\"Economy\",\"value\":\"Economy\"},{\"label\":\"Premium\",\"value\":\"Premium\"},{\"label\":\"Business\",\"value\":\"Business\"},{\"label\":\"First\",\"value\":\"First\"}],\"selections\":{\"path\":\"/fare_class\"},\"maxAllowedSelections\":1}}},{\"id\":\"submit\",\"component\":{\"Button\":{\"child\":\"submit_label\",\"primary\":true,\"action\":{\"name\":\"bookingSubmit\",\"context\":[{\"key\":\"formId\",\"value\":\"booking\"},{\"key\":\"origin\",\"value\":{\"path\":\"/origin\"}},{\"key\":\"dest\",\"value\":{\"path\":\"/dest\"}},{\"key\":\"date\",\"value\":{\"path\":\"/date\"}},{\"key\":\"passengers\",\"value\":{\"path\":\"/passengers\"}},{\"key\":\"fare_class\",\"value\":{\"path\":\"/fare_class\"}}]}}}},{\"id\":\"submit_label\",\"component\":{\"Text\":{\"text\":\"Search flights\"}}}]}", + "id": "call_Miv5QrUKT95KeUOO7xzPGSEl" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/cockpit/chat/a2ui/angular/e2e/global-setup-impl.ts b/cockpit/chat/a2ui/angular/e2e/global-setup-impl.ts new file mode 100644 index 00000000..5f5635f3 --- /dev/null +++ b/cockpit/chat/a2ui/angular/e2e/global-setup-impl.ts @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +import { resolve } from 'node:path'; +import { createGlobalSetup } from '../../../../../libs/e2e-harness/src'; + +export default createGlobalSetup({ + langgraphCwd: 'cockpit/chat/a2ui/python', + langgraphPort: 5511, + angularProject: 'cockpit-chat-a2ui-angular', + angularPort: 4511, + fixturesDir: resolve(__dirname, 'fixtures'), +}); diff --git a/cockpit/chat/a2ui/angular/e2e/playwright.config.ts b/cockpit/chat/a2ui/angular/e2e/playwright.config.ts new file mode 100644 index 00000000..29f94703 --- /dev/null +++ b/cockpit/chat/a2ui/angular/e2e/playwright.config.ts @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: '.', + testMatch: '**/*.spec.ts', + fullyParallel: false, + workers: 1, + retries: process.env.CI ? 2 : 0, + reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : 'list', + use: { + baseURL: 'http://localhost:4511', + trace: 'retain-on-failure', + }, + projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }], + globalSetup: './global-setup-impl.ts', + globalTeardown: require.resolve('../../../../../libs/e2e-harness/src/global-teardown'), +}); diff --git a/cockpit/chat/a2ui/angular/e2e/tsconfig.json b/cockpit/chat/a2ui/angular/e2e/tsconfig.json new file mode 100644 index 00000000..0b5aeecb --- /dev/null +++ b/cockpit/chat/a2ui/angular/e2e/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "test-results", "playwright-report"] +} diff --git a/cockpit/chat/a2ui/angular/project.json b/cockpit/chat/a2ui/angular/project.json index 3ece1c9c..fff87a06 100644 --- a/cockpit/chat/a2ui/angular/project.json +++ b/cockpit/chat/a2ui/angular/project.json @@ -87,6 +87,12 @@ "cwd": "cockpit/chat/a2ui/angular", "command": "npx tsx -e \"import { chatA2uiAngularModule } from './src/index.ts'; const module = chatA2uiAngularModule; if (module.id !== 'chat-a2ui-angular' || module.title !== 'Chat A2UI (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" } + }, + "e2e": { + "executor": "@nx/playwright:playwright", + "options": { + "config": "cockpit/chat/a2ui/angular/e2e/playwright.config.ts" + } } } } diff --git a/cockpit/chat/generative-ui/angular/e2e/c-generative-ui.spec.ts b/cockpit/chat/generative-ui/angular/e2e/c-generative-ui.spec.ts new file mode 100644 index 00000000..2579823e --- /dev/null +++ b/cockpit/chat/generative-ui/angular/e2e/c-generative-ui.spec.ts @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +import { test, expect } from '@playwright/test'; +import { submitAndWaitForResponse } from '../../../../../libs/e2e-harness/src'; + +test('c-generative-ui: dashboard prompt renders chat-generative-ui surface', async ({ page }) => { + await submitAndWaitForResponse(page, 'Show me a dashboard of airline operations.'); + + // The render_spec tool call returns a dashboard JSON spec which the + // content-classifier in @ngaf/chat mounts as a + // primitive inside the assistant bubble. Presence of this element + // proves the spec parsed and the GenUI host wired up. + await expect(page.locator('chat-generative-ui')).toBeVisible(); +}); + +test('c-generative-ui: filter prompt produces assistant turn', async ({ page }) => { + await submitAndWaitForResponse(page, 'Filter to only the cancelled flights.'); + + // The query_recent_disruptions tool returns data the assistant uses to + // narrow the dashboard. Distinctive surface here is just that the + // assistant turn finalized — the dashboard view update is internal state. + await expect( + page.locator('chat-message[data-role="assistant"]').last(), + ).toBeVisible(); +}); diff --git a/cockpit/chat/generative-ui/angular/e2e/fixtures/c-generative-ui.json b/cockpit/chat/generative-ui/angular/e2e/fixtures/c-generative-ui.json new file mode 100644 index 00000000..bccea626 --- /dev/null +++ b/cockpit/chat/generative-ui/angular/e2e/fixtures/c-generative-ui.json @@ -0,0 +1,32 @@ +{ + "fixtures": [ + { + "match": { + "userMessage": "Show me a dashboard of airline operations." + }, + "response": { + "toolCalls": [ + { + "name": "render_spec", + "arguments": "{\"elements\":{\"root\":{\"type\":\"dashboard_grid\",\"children\":[\"stats_row\",\"charts_row\",\"table_section\"]},\"stats_row\":{\"type\":\"container\",\"props\":{\"direction\":\"row\"},\"children\":[\"on_time_card\",\"flights_card\",\"delay_card\",\"load_card\"]},\"on_time_card\":{\"type\":\"stat_card\",\"props\":{\"label\":\"On-time %\",\"value\":{\"$state\":\"/on_time/value\"},\"delta\":{\"$state\":\"/on_time/delta\"}}},\"flights_card\":{\"type\":\"stat_card\",\"props\":{\"label\":\"Flights Today\",\"value\":{\"$state\":\"/flights_today/value\"},\"delta\":{\"$state\":\"/flights_today/delta\"}}},\"delay_card\":{\"type\":\"stat_card\",\"props\":{\"label\":\"Avg Delay (min)\",\"value\":{\"$state\":\"/avg_delay/value\"},\"delta\":{\"$state\":\"/avg_delay/delta\"}}},\"load_card\":{\"type\":\"stat_card\",\"props\":{\"label\":\"Load Factor\",\"value\":{\"$state\":\"/load_factor/value\"},\"delta\":{\"$state\":\"/load_factor/delta\"}}},\"charts_row\":{\"type\":\"container\",\"props\":{\"direction\":\"row\"},\"children\":[\"trend_chart\",\"airline_chart\"]},\"trend_chart\":{\"type\":\"line_chart\",\"props\":{\"title\":\"On-time % Trend\",\"data\":{\"$state\":\"/on_time_trend\"},\"xKey\":\"month\",\"yKey\":\"on_time_pct\"}},\"airline_chart\":{\"type\":\"bar_chart\",\"props\":{\"title\":\"Flights by Airline (Daily)\",\"data\":{\"$state\":\"/flights_by_airline\"},\"labelKey\":\"airline\",\"valueKey\":\"count\"}},\"table_section\":{\"type\":\"data_grid\",\"props\":{\"title\":\"Recent Disruptions\",\"rows\":{\"$state\":\"/recent_disruptions\"},\"columns\":[\"flight_number\",\"type\",\"minutes\",\"route\",\"date\"]}}},\"root\":\"root\"}", + "id": "call_offZhaDPm8tbblqv1sNHarnc" + } + ] + } + }, + { + "match": { + "userMessage": "Filter to only the cancelled flights." + }, + "response": { + "toolCalls": [ + { + "name": "query_recent_disruptions", + "arguments": "{\"limit\":5,\"type\":\"cancelled\"}", + "id": "call_ivGTFO6CX8sl4bkOprs633gH" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/cockpit/chat/generative-ui/angular/e2e/global-setup-impl.ts b/cockpit/chat/generative-ui/angular/e2e/global-setup-impl.ts new file mode 100644 index 00000000..f71ce151 --- /dev/null +++ b/cockpit/chat/generative-ui/angular/e2e/global-setup-impl.ts @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +import { resolve } from 'node:path'; +import { createGlobalSetup } from '../../../../../libs/e2e-harness/src'; + +export default createGlobalSetup({ + langgraphCwd: 'cockpit/chat/generative-ui/python', + langgraphPort: 5508, + angularProject: 'cockpit-chat-generative-ui-angular', + angularPort: 4508, + fixturesDir: resolve(__dirname, 'fixtures'), +}); diff --git a/cockpit/chat/generative-ui/angular/e2e/playwright.config.ts b/cockpit/chat/generative-ui/angular/e2e/playwright.config.ts new file mode 100644 index 00000000..7c3a63dc --- /dev/null +++ b/cockpit/chat/generative-ui/angular/e2e/playwright.config.ts @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: '.', + testMatch: '**/*.spec.ts', + fullyParallel: false, + workers: 1, + retries: process.env.CI ? 2 : 0, + reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : 'list', + use: { + baseURL: 'http://localhost:4508', + trace: 'retain-on-failure', + }, + projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }], + globalSetup: './global-setup-impl.ts', + globalTeardown: require.resolve('../../../../../libs/e2e-harness/src/global-teardown'), +}); diff --git a/cockpit/chat/generative-ui/angular/e2e/tsconfig.json b/cockpit/chat/generative-ui/angular/e2e/tsconfig.json new file mode 100644 index 00000000..0b5aeecb --- /dev/null +++ b/cockpit/chat/generative-ui/angular/e2e/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "test-results", "playwright-report"] +} diff --git a/cockpit/chat/generative-ui/angular/project.json b/cockpit/chat/generative-ui/angular/project.json index fcb2f1f3..b66c6679 100644 --- a/cockpit/chat/generative-ui/angular/project.json +++ b/cockpit/chat/generative-ui/angular/project.json @@ -93,6 +93,12 @@ "cwd": "cockpit/chat/generative-ui/angular", "command": "npx tsx -e \"import { chatGenerativeUiAngularModule } from './src/index.ts'; const module = chatGenerativeUiAngularModule; if (module.id !== 'chat-generative-ui-angular' || module.title !== 'Chat Generative UI (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" } + }, + "e2e": { + "executor": "@nx/playwright:playwright", + "options": { + "config": "cockpit/chat/generative-ui/angular/e2e/playwright.config.ts" + } } } } From a02e3c9e08f5ce7246cbea1059a797a321e32169 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 19 May 2026 14:11:06 -0700 Subject: [PATCH 2/2] fix(c-generative-ui-e2e): use .first() on chat-generative-ui locator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dashboard spec renders a TREE of hosts (one per node in the dashboard layout — 5 for the airline-operations spec), so the bare `locator('chat-generative-ui')` hits Playwright's strict-mode requirement that toBeVisible() target a single element. Use .first().toBeVisible() to assert the root mounts; additionally assert count > 0 so the tree-mount assertion stays meaningful if the spec ever shrinks to a single-node dashboard. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../generative-ui/angular/e2e/c-generative-ui.spec.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/cockpit/chat/generative-ui/angular/e2e/c-generative-ui.spec.ts b/cockpit/chat/generative-ui/angular/e2e/c-generative-ui.spec.ts index 2579823e..cd1acd73 100644 --- a/cockpit/chat/generative-ui/angular/e2e/c-generative-ui.spec.ts +++ b/cockpit/chat/generative-ui/angular/e2e/c-generative-ui.spec.ts @@ -6,10 +6,13 @@ test('c-generative-ui: dashboard prompt renders chat-generative-ui surface', asy await submitAndWaitForResponse(page, 'Show me a dashboard of airline operations.'); // The render_spec tool call returns a dashboard JSON spec which the - // content-classifier in @ngaf/chat mounts as a - // primitive inside the assistant bubble. Presence of this element - // proves the spec parsed and the GenUI host wired up. - await expect(page.locator('chat-generative-ui')).toBeVisible(); + // content-classifier in @ngaf/chat mounts as a tree of + // hosts (one per node in the dashboard view). + // Multiple matches expected (≥5 for the standard dashboard layout); + // assert the count proves the GenUI tree wired up. .first() unblocks + // toBeVisible's strict-mode requirement. + await expect(page.locator('chat-generative-ui').first()).toBeVisible(); + await expect(page.locator('chat-generative-ui')).not.toHaveCount(0); }); test('c-generative-ui: filter prompt produces assistant turn', async ({ page }) => {