Skip to content
Merged
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
6 changes: 6 additions & 0 deletions apps/website/content/docs/chat/api/api-docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -3249,6 +3249,12 @@
"description": "",
"optional": false
},
{
"name": "closeOnEscape",
"type": "InputSignal<boolean>",
"description": "Close the sidebar on Escape (default true).",
"optional": false
},
{
"name": "forkRequested",
"type": "OutputEmitterRef<string>",
Expand Down
27 changes: 26 additions & 1 deletion examples/chat/angular/e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,32 @@ Re-records each committed fixture against real OpenAI and reports byte-level dif
- `scripts/record.ts` — dev-only fixture-capture CLI.
- `scripts/drift.ts` — CI fixture-drift comparison.
- `playwright.config.ts` — Playwright config with globalSetup that boots aimock + LangGraph + Angular dev server.
- `smoke.spec.ts` — Phase 2a smoke test (one scenario: "hi").
- `initial-render.spec.ts` — checklist pre-flight browser hygiene and welcome-state render.
- `send-receive.spec.ts` — basic deterministic send/receive and stream completion.
- `stop-streaming.spec.ts` — skipped harness pilot for stop-button abort behavior.
- `markdown-surfaces.spec.ts` — final-state markdown matrix.
- `regenerate.spec.ts` — regenerate replacement and server-state invariants.
- `mode-routing.spec.ts` — embed/popup/sidebar routing and cross-mode persistence.
- `model-picker.spec.ts` — model picker persistence and backend state.
- `debug-devtools.spec.ts` — chat-debug accessibility plus sidebar coexistence.
- `control-palette.spec.ts` — palette default/collapsed state and route controls.
- `color-scheme.spec.ts` — light/dark persistence and A2UI theme sync.
- `keyboard-accessibility.spec.ts` — keyboard send/newline, Escape, and core button names.
- `error-handling.spec.ts` — network-failure alert and recovery.
- `lifecycle.spec.ts` — reload reconnect, new conversation, welcome suggestion submit.
- `browser-hygiene.spec.ts` — pilot automation for repeated mode-switch hygiene.
- `visual-polish.spec.ts` — responsive overflow checks at checklist widths.
- `a2ui-single-bubble.spec.ts`, `interrupt-approval.spec.ts`, `research-subagent.spec.ts` — capability smoke coverage for GenUI, HITL, and subagents.

## Checklist coverage

The suite mirrors `examples/chat/smoke/CHECKLIST.md` by section. Items that
need live-model semantics or visual judgment stay represented by deterministic
proxies here and by the smoke checklist for manual release validation. The
browser-hygiene coverage has graduated into CI-grade assertions using Chromium
performance metrics and repeated route churn. The remaining skipped pilot is
stop-streaming, which needs the harness to expose an in-flight stream state
reliably enough for deterministic abort assertions.

## Env vars

Expand Down
43 changes: 43 additions & 0 deletions examples/chat/angular/e2e/browser-hygiene.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: MIT
import { test, expect } from '@playwright/test';
import { attachBrowserHygiene, openDemo } from './test-helpers';

test('browser hygiene pilot: repeated mode switches do not leak visible chat DOM', async ({
page,
}) => {
await openDemo(page, '/embed');
const hygiene = attachBrowserHygiene(page);

const client = await page.context().newCDPSession(page);
await client.send('Performance.enable');
const before = await client.send('Performance.getMetrics');

for (let i = 0; i < 10; i++) {
await page
.locator('.demo-shell__segmented-button', { hasText: 'Popup' })
.click();
await expect(page).toHaveURL(/\/popup$/);
await page
.locator('.demo-shell__segmented-button', { hasText: 'Sidebar' })
.click();
await expect(page).toHaveURL(/\/sidebar$/);
await page
.locator('.demo-shell__segmented-button', { hasText: 'Embed' })
.click();
await expect(page).toHaveURL(/\/embed$/);
}

const after = await client.send('Performance.getMetrics');
const jsHeapBefore =
before.metrics.find((m) => m.name === 'JSHeapUsedSize')?.value ?? 0;
const jsHeapAfter =
after.metrics.find((m) => m.name === 'JSHeapUsedSize')?.value ?? 0;

await expect(page.locator('embed-mode')).toHaveCount(1);
await expect(page.locator('popup-mode')).toHaveCount(0);
await expect(page.locator('sidebar-mode')).toHaveCount(0);
await expect(page.locator('chat-message')).toHaveCount(0);
expect(jsHeapAfter).toBeLessThan(jsHeapBefore + 20_000_000);
expect(hygiene.consoleErrors).toEqual([]);
expect(hygiene.failedRequests).toEqual([]);
});
77 changes: 77 additions & 0 deletions examples/chat/angular/e2e/color-scheme.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// SPDX-License-Identifier: MIT
import { test, expect } from '@playwright/test';
import {
attachBrowserHygiene,
openDemo,
selectToolbarOption,
} from './test-helpers';

test('color scheme: dark default, light toggle persists and syncs default A2UI theme', async ({
page,
}) => {
await openDemo(page, '/embed');
const hygiene = attachBrowserHygiene(page);

await expect(page.locator('html')).toHaveAttribute(
'data-color-scheme',
'dark'
);
await expect(page.locator('html')).toHaveAttribute(
'data-ngaf-chat-theme',
'dark'
);
await expect(page.locator('html')).toHaveAttribute(
'data-theme',
'default-dark'
);

await page.getByRole('button', { name: 'Switch to light theme' }).click();

await expect(page.locator('html')).toHaveAttribute(
'data-color-scheme',
'light'
);
await expect(page.locator('html')).toHaveAttribute(
'data-ngaf-chat-theme',
'light'
);
await expect(page.locator('html')).toHaveAttribute(
'data-theme',
'default-light'
);

await page.reload();
await expect(page.locator('html')).toHaveAttribute(
'data-color-scheme',
'light'
);
await expect(page.locator('html')).toHaveAttribute(
'data-ngaf-chat-theme',
'light'
);

expect(hygiene.consoleErrors).toEqual([]);
});

test('color scheme: material A2UI theme override wins over scheme sync', async ({
page,
}) => {
await openDemo(page, '/embed');

await selectToolbarOption(page, 'Theme', 'Material dark');
await expect(page.locator('html')).toHaveAttribute(
'data-theme',
'material-dark'
);

await page.getByRole('button', { name: 'Switch to light theme' }).click();

await expect(page.locator('html')).toHaveAttribute(
'data-color-scheme',
'light'
);
await expect(page.locator('html')).toHaveAttribute(
'data-theme',
'material-dark'
);
});
71 changes: 71 additions & 0 deletions examples/chat/angular/e2e/control-palette.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// SPDX-License-Identifier: MIT
import { test, expect } from '@playwright/test';
import {
openChatDevtools,
openDemo,
selectToolbarOption,
toolbarSelect,
} from './test-helpers';

test('control palette: toolbar renders defaults and persists selected controls', async ({
page,
}) => {
await openDemo(page, '/embed');

await expect(
page.getByRole('toolbar', { name: 'Demo controls' })
).toBeVisible();
await expect(
page.locator('.demo-shell__segmented-button.is-active', {
hasText: 'Embed',
})
).toBeVisible();
await expect(toolbarSelect(page, 'Model')).toHaveValue('gpt-5-mini');
await expect(toolbarSelect(page, 'Effort')).toHaveValue('minimal');
await expect(toolbarSelect(page, 'Gen UI')).toHaveValue('a2ui');
await expect(toolbarSelect(page, 'Theme')).toHaveValue('default-dark');

await selectToolbarOption(page, 'Model', 'gpt-5-nano');
await selectToolbarOption(page, 'Effort', 'low');
await selectToolbarOption(page, 'Gen UI', 'json-render');
await selectToolbarOption(page, 'Theme', 'Material dark');

await page.reload();
await expect(toolbarSelect(page, 'Model')).toHaveValue('gpt-5-nano');
await expect(toolbarSelect(page, 'Effort')).toHaveValue('low');
await expect(toolbarSelect(page, 'Gen UI')).toHaveValue('json-render');
await expect(toolbarSelect(page, 'Theme')).toHaveValue('material-dark');
});

test('control palette: devtools opens on demand and closes back to launcher', async ({
page,
}) => {
await openDemo(page, '/embed');

await openChatDevtools(page);
await expect(
page.getByRole('region', { name: 'Chat devtools' })
).toBeVisible();
await expect(page.locator('.panel.panel--right')).toBeVisible();

await page.getByRole('button', { name: 'Close' }).click();
await expect(page.locator('chat-debug .panel')).toHaveCount(0);
await expect(
page.getByRole('button', { name: 'Open chat devtools' })
).toBeVisible();
});

test('control palette: mode segmented control changes routes', async ({
page,
}) => {
await openDemo(page, '/embed');
await page
.locator('.demo-shell__segmented-button', { hasText: 'Sidebar' })
.click();
await expect(page).toHaveURL(/\/sidebar$/);
await expect(
page.locator('.demo-shell__segmented-button.is-active', {
hasText: 'Sidebar',
})
).toBeVisible();
});
Original file line number Diff line number Diff line change
@@ -1,26 +1,52 @@
// SPDX-License-Identifier: MIT
import { test, expect } from '@playwright/test';
import { openChatDevtools, openDemo } from './test-helpers';

test('chat-debug devtools: opens from the sidenav with accessible controls and closes cleanly', async ({
page,
}) => {
await openDemo(page, '/embed');
await openChatDevtools(page);

const panel = page.getByRole('region', { name: 'Chat devtools' });
await expect(panel).toBeVisible();
await expect(panel.getByRole('tab', { name: 'Timeline' })).toBeVisible();
await expect(panel.getByRole('tab', { name: 'State' })).toBeVisible();

await page.getByRole('button', { name: 'Close' }).click();
await expect(page.locator('chat-debug .panel')).toHaveCount(0);
await expect(
page.getByRole('button', { name: 'Open chat devtools' })
).toBeVisible();
});

test.describe('chat-debug × chat-sidebar coexistence', () => {
test('sidebar launcher remains reachable while chat-debug is open', async ({ page }) => {
await page.goto('/sidebar');
test('sidebar launcher remains reachable while chat-debug is open', async ({
page,
}) => {
await openDemo(page, '/sidebar');
await expect(page.locator('chat-sidebar')).toBeAttached();

// Open chat-debug from the sidenav footer.
await page.locator('.chat-sidenav__debug').click();
await openChatDevtools(page);

// Debug auto-picks bottom dock because <chat-sidebar> is present.
const debugPanel = page.locator('.panel.panel--bottom');
await expect(debugPanel).toBeVisible();

// The edge-claim attribute on <html> reflects the dock.
await expect(page.locator('html')).toHaveAttribute('data-ngaf-chat-debug', 'bottom');
await expect(page.locator('html')).toHaveAttribute(
'data-ngaf-chat-debug',
'bottom'
);

// Sidebar launcher remains visible (the bottom dock did not cover it).
// Click the actual <button> inside <chat-launcher-button> rather than the
// wrapping div — avoids any hit-test ambiguity between the wrapper and
// the higher-z-index debug panel.
const sidebarLauncherButton = page.locator('.chat-sidebar__launcher button.chat-launcher-button');
const sidebarLauncherButton = page.locator(
'.chat-sidebar__launcher button.chat-launcher-button'
);
await expect(sidebarLauncherButton).toBeVisible();
await sidebarLauncherButton.click();

Expand All @@ -30,16 +56,25 @@ test.describe('chat-debug × chat-sidebar coexistence', () => {
await expect(sidebarPanel).toBeVisible();

// Once the sidebar is open, the edge-claim attribute reflects it too.
await expect(page.locator('html')).toHaveAttribute('data-ngaf-chat-sidebar', 'open');
await expect(page.locator('html')).toHaveAttribute(
'data-ngaf-chat-sidebar',
'open'
);
});

test('user override survives mode switch: explicit right-dock stays right', async ({ page }) => {
await page.goto('/embed');
await page.locator('.chat-sidenav__debug').click();
test('user override survives mode switch: explicit right-dock stays right', async ({
page,
}) => {
await openDemo(page, '/embed');
await openChatDevtools(page);
// Click right-dock explicitly — sets userDockOverride.
await page.locator('.panel__dock-btn').nth(2).click(); // 0=left, 1=bottom, 2=right
// Switch to sidebar mode via the demo-owned top toolbar.
await page.locator('.demo-shell__segmented-button', { hasText: 'Sidebar' }).click();
await page
.locator('.demo-shell__segmented-button', { hasText: 'Sidebar' })
.click();
await openChatDevtools(page);

// Debug stays right-docked despite chat-sidebar now being on the page.
await expect(page.locator('.panel.panel--right')).toBeVisible();
await expect(page.locator('.panel.panel--bottom')).not.toBeVisible();
Expand Down
24 changes: 24 additions & 0 deletions examples/chat/angular/e2e/error-handling.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: MIT
import { test, expect } from '@playwright/test';
import { messageInput, openDemo, sendButton, waitForFinalAssistant } from './test-helpers';

test('error handling: failed stream surfaces an alert and the next send recovers', async ({
page,
}) => {
await openDemo(page, '/embed');

await page.route('**/runs/stream', async (route) => {
await route.abort('failed');
});

await messageInput(page).fill('say hi briefly');
await sendButton(page).click();
await expect(page.getByRole('alert')).toContainText(/fail|error/i, { timeout: 15_000 });

await page.unroute('**/runs/stream');
await expect(messageInput(page)).toBeEnabled();
await messageInput(page).fill('say hi briefly');
await sendButton(page).click();
const bubble = await waitForFinalAssistant(page);
await expect(bubble).toContainText(/hi/i);
});
6 changes: 6 additions & 0 deletions examples/chat/angular/e2e/fixtures/markdown.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@
{
"match": { "userMessage": "respond with a bullet list" },
"response": { "content": "Three things:\n\n- alpha\n- beta\n- gamma" }
},
{
"match": { "userMessage": "respond with the markdown checklist kitchen sink" },
"response": {
"content": "# Heading One\n\n## Heading Two\n\n### Heading Three\n\nA paragraph with **bold text**, *italic text*, and `inline code`.\n\n- parent item\n - nested child\n- second parent\n\n1. first ordered\n2. second ordered\n\n- [ ] unchecked task\n- [x] checked task\n\n```typescript\nconst answer = 42;\n```\n\n| Name | Mental model | When to use |\n| --- | --- | --- |\n| Signals | value graph | local state |\n| RxJS | event stream | async events |\n\n> This is a blockquote.\n\n[Angular](https://angular.dev)\n\n---\n\n<script>alert('xss')</script>"
}
}
]
}
12 changes: 12 additions & 0 deletions examples/chat/angular/e2e/fixtures/streaming.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"fixtures": [
{
"match": { "userMessage": "stream a long deterministic answer" },
"chunkSize": 128,
"streamingProfile": { "ttft": 500, "tps": 6 },
"response": {
"content": "Streaming smoke response begins. This deterministic response is intentionally long enough to cross the aimock chunk boundary so Playwright can observe the loading state, stop button, and final idle state. Section one repeats useful words about Angular signals, chat rendering, markdown stability, scroll anchoring, deterministic replay, and user feedback. Section two repeats useful words about Angular signals, chat rendering, markdown stability, scroll anchoring, deterministic replay, and user feedback. Section three repeats useful words about Angular signals, chat rendering, markdown stability, scroll anchoring, deterministic replay, and user feedback. Section four repeats useful words about Angular signals, chat rendering, markdown stability, scroll anchoring, deterministic replay, and user feedback. Section five repeats useful words about Angular signals, chat rendering, markdown stability, scroll anchoring, deterministic replay, and user feedback. Section six repeats useful words about Angular signals, chat rendering, markdown stability, scroll anchoring, deterministic replay, and user feedback. Section seven repeats useful words about Angular signals, chat rendering, markdown stability, scroll anchoring, deterministic replay, and user feedback. Section eight repeats useful words about Angular signals, chat rendering, markdown stability, scroll anchoring, deterministic replay, and user feedback. Section nine repeats useful words about Angular signals, chat rendering, markdown stability, scroll anchoring, deterministic replay, and user feedback. Section ten repeats useful words about Angular signals, chat rendering, markdown stability, scroll anchoring, deterministic replay, and user feedback. Section eleven repeats useful words about Angular signals, chat rendering, markdown stability, scroll anchoring, deterministic replay, and user feedback. Section twelve repeats useful words about Angular signals, chat rendering, markdown stability, scroll anchoring, deterministic replay, and user feedback. Section thirteen repeats useful words about Angular signals, chat rendering, markdown stability, scroll anchoring, deterministic replay, and user feedback. Section fourteen repeats useful words about Angular signals, chat rendering, markdown stability, scroll anchoring, deterministic replay, and user feedback. Section fifteen repeats useful words about Angular signals, chat rendering, markdown stability, scroll anchoring, deterministic replay, and user feedback. Final marker: complete."
}
}
]
}
Loading
Loading