Skip to content

Commit 0833bcb

Browse files
bloveclaude
andauthored
test(cockpit-chat): aimock e2e for c-generative-ui + c-a2ui (Task #4 final chat slice) (#479)
* test(cockpit-chat): add aimock e2e for c-generative-ui + c-a2ui 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) <noreply@anthropic.com> * fix(c-generative-ui-e2e): use .first() on chat-generative-ui locator The dashboard spec renders a TREE of <chat-generative-ui> 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent fafbc94 commit 0833bcb

13 files changed

Lines changed: 209 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,8 @@ jobs:
275275
- { angular: cockpit-chat-threads-angular, python: cockpit/chat/threads/python }
276276
- { angular: cockpit-chat-timeline-angular, python: cockpit/chat/timeline/python }
277277
- { angular: cockpit-chat-theming-angular, python: cockpit/chat/theming/python }
278+
- { angular: cockpit-chat-generative-ui-angular, python: cockpit/chat/generative-ui/python }
279+
- { angular: cockpit-chat-a2ui-angular, python: cockpit/chat/a2ui/python }
278280
steps:
279281
- uses: actions/checkout@v6.0.2
280282
- uses: actions/setup-node@v6.3.0
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// SPDX-License-Identifier: MIT
2+
import { test, expect } from '@playwright/test';
3+
import { submitAndWaitForResponse } from '../../../../../libs/e2e-harness/src';
4+
5+
test('c-a2ui: LAX-JFK prompt renders a2ui-surface with form spec', async ({ page }) => {
6+
await submitAndWaitForResponse(page, 'I want to fly LAX to JFK');
7+
8+
// The BookingFormSpec tool call returns an A2UI form spec which the
9+
// chat-lib content-classifier mounts as a <a2ui-surface> with the
10+
// booking surface_id. Presence proves the envelope parsed and the
11+
// A2UI host wired up against the cap's catalog views.
12+
await expect(page.locator('a2ui-surface')).toBeVisible();
13+
});
14+
15+
test('c-a2ui: SFO-SEA prompt also renders a2ui-surface', async ({ page }) => {
16+
await submitAndWaitForResponse(page, 'I want to fly SFO to SEA');
17+
await expect(page.locator('a2ui-surface')).toBeVisible();
18+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"fixtures": [
3+
{
4+
"match": {
5+
"userMessage": "I want to fly LAX to JFK"
6+
},
7+
"response": {
8+
"toolCalls": [
9+
{
10+
"name": "BookingFormSpec",
11+
"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\"}}]}}}}]}",
12+
"id": "call_DiKu9KXnOKtbRcNBtDlXwTzx"
13+
}
14+
]
15+
}
16+
},
17+
{
18+
"match": {
19+
"userMessage": "I want to fly SFO to SEA"
20+
},
21+
"response": {
22+
"toolCalls": [
23+
{
24+
"name": "BookingFormSpec",
25+
"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\"}}}]}",
26+
"id": "call_Miv5QrUKT95KeUOO7xzPGSEl"
27+
}
28+
]
29+
}
30+
}
31+
]
32+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// SPDX-License-Identifier: MIT
2+
import { resolve } from 'node:path';
3+
import { createGlobalSetup } from '../../../../../libs/e2e-harness/src';
4+
5+
export default createGlobalSetup({
6+
langgraphCwd: 'cockpit/chat/a2ui/python',
7+
langgraphPort: 5511,
8+
angularProject: 'cockpit-chat-a2ui-angular',
9+
angularPort: 4511,
10+
fixturesDir: resolve(__dirname, 'fixtures'),
11+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// SPDX-License-Identifier: MIT
2+
import { defineConfig, devices } from '@playwright/test';
3+
4+
export default defineConfig({
5+
testDir: '.',
6+
testMatch: '**/*.spec.ts',
7+
fullyParallel: false,
8+
workers: 1,
9+
retries: process.env.CI ? 2 : 0,
10+
reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : 'list',
11+
use: {
12+
baseURL: 'http://localhost:4511',
13+
trace: 'retain-on-failure',
14+
},
15+
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
16+
globalSetup: './global-setup-impl.ts',
17+
globalTeardown: require.resolve('../../../../../libs/e2e-harness/src/global-teardown'),
18+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2022",
4+
"module": "ES2022",
5+
"moduleResolution": "Bundler",
6+
"esModuleInterop": true,
7+
"strict": true,
8+
"skipLibCheck": true,
9+
"noEmit": true,
10+
"types": ["node"]
11+
},
12+
"include": ["**/*.ts"],
13+
"exclude": ["node_modules", "test-results", "playwright-report"]
14+
}

cockpit/chat/a2ui/angular/project.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@
8787
"cwd": "cockpit/chat/a2ui/angular",
8888
"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); }\""
8989
}
90+
},
91+
"e2e": {
92+
"executor": "@nx/playwright:playwright",
93+
"options": {
94+
"config": "cockpit/chat/a2ui/angular/e2e/playwright.config.ts"
95+
}
9096
}
9197
}
9298
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// SPDX-License-Identifier: MIT
2+
import { test, expect } from '@playwright/test';
3+
import { submitAndWaitForResponse } from '../../../../../libs/e2e-harness/src';
4+
5+
test('c-generative-ui: dashboard prompt renders chat-generative-ui surface', async ({ page }) => {
6+
await submitAndWaitForResponse(page, 'Show me a dashboard of airline operations.');
7+
8+
// The render_spec tool call returns a dashboard JSON spec which the
9+
// content-classifier in @ngaf/chat mounts as a tree of
10+
// <chat-generative-ui> hosts (one per node in the dashboard view).
11+
// Multiple matches expected (≥5 for the standard dashboard layout);
12+
// assert the count proves the GenUI tree wired up. .first() unblocks
13+
// toBeVisible's strict-mode requirement.
14+
await expect(page.locator('chat-generative-ui').first()).toBeVisible();
15+
await expect(page.locator('chat-generative-ui')).not.toHaveCount(0);
16+
});
17+
18+
test('c-generative-ui: filter prompt produces assistant turn', async ({ page }) => {
19+
await submitAndWaitForResponse(page, 'Filter to only the cancelled flights.');
20+
21+
// The query_recent_disruptions tool returns data the assistant uses to
22+
// narrow the dashboard. Distinctive surface here is just that the
23+
// assistant turn finalized — the dashboard view update is internal state.
24+
await expect(
25+
page.locator('chat-message[data-role="assistant"]').last(),
26+
).toBeVisible();
27+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"fixtures": [
3+
{
4+
"match": {
5+
"userMessage": "Show me a dashboard of airline operations."
6+
},
7+
"response": {
8+
"toolCalls": [
9+
{
10+
"name": "render_spec",
11+
"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\"}",
12+
"id": "call_offZhaDPm8tbblqv1sNHarnc"
13+
}
14+
]
15+
}
16+
},
17+
{
18+
"match": {
19+
"userMessage": "Filter to only the cancelled flights."
20+
},
21+
"response": {
22+
"toolCalls": [
23+
{
24+
"name": "query_recent_disruptions",
25+
"arguments": "{\"limit\":5,\"type\":\"cancelled\"}",
26+
"id": "call_ivGTFO6CX8sl4bkOprs633gH"
27+
}
28+
]
29+
}
30+
}
31+
]
32+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// SPDX-License-Identifier: MIT
2+
import { resolve } from 'node:path';
3+
import { createGlobalSetup } from '../../../../../libs/e2e-harness/src';
4+
5+
export default createGlobalSetup({
6+
langgraphCwd: 'cockpit/chat/generative-ui/python',
7+
langgraphPort: 5508,
8+
angularProject: 'cockpit-chat-generative-ui-angular',
9+
angularPort: 4508,
10+
fixturesDir: resolve(__dirname, 'fixtures'),
11+
});

0 commit comments

Comments
 (0)