From fc7e95a68a1e30b08d9157abd319885e17ac1d5b Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 19 May 2026 13:15:48 -0700 Subject: [PATCH] test(cockpit-chat): add aimock e2e for input + threads + timeline + theming Task #4 batch (4 of 7 deferred chat caps). Generated via scripts/generate-aimock-scaffold.ts; fixtures + spec assertions hand-authored. Each cap gets one chat-message rendering test plus one cap-distinctive surface test. Caps covered: - c-input (raw primitives, like c-messages): user+AI bubbles, count=2. - c-threads (composed ): AI text, chat-thread-list mounted. - c-timeline (composed ): AI text, chat-timeline-slider mounted. - c-theming (composed ): AI text, four theme picker buttons present. Deferred from this batch (separate follow-ups): - c-generative-ui + c-a2ui: require multi-turn / rich-content fixtures (tool_calls turn + continuation, or inline A2UI JSONL emission). Safer to author with backend-trace verification than hand-author. - c-debug: demo has no affordance (only viewer). Either add an input or wire chat-debug to expose one. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 4 ++++ .../chat/input/angular/e2e/c-input.spec.ts | 23 +++++++++++++++++++ .../input/angular/e2e/fixtures/c-input.json | 10 ++++++++ .../input/angular/e2e/global-setup-impl.ts | 11 +++++++++ .../input/angular/e2e/playwright.config.ts | 18 +++++++++++++++ cockpit/chat/input/angular/e2e/tsconfig.json | 14 +++++++++++ cockpit/chat/input/angular/project.json | 6 +++++ .../theming/angular/e2e/c-theming.spec.ts | 19 +++++++++++++++ .../angular/e2e/fixtures/c-theming.json | 10 ++++++++ .../theming/angular/e2e/global-setup-impl.ts | 11 +++++++++ .../theming/angular/e2e/playwright.config.ts | 18 +++++++++++++++ .../chat/theming/angular/e2e/tsconfig.json | 14 +++++++++++ cockpit/chat/theming/angular/project.json | 6 +++++ .../threads/angular/e2e/c-threads.spec.ts | 17 ++++++++++++++ .../angular/e2e/fixtures/c-threads.json | 10 ++++++++ .../threads/angular/e2e/global-setup-impl.ts | 11 +++++++++ .../threads/angular/e2e/playwright.config.ts | 18 +++++++++++++++ .../chat/threads/angular/e2e/tsconfig.json | 14 +++++++++++ cockpit/chat/threads/angular/project.json | 6 +++++ .../timeline/angular/e2e/c-timeline.spec.ts | 16 +++++++++++++ .../angular/e2e/fixtures/c-timeline.json | 10 ++++++++ .../timeline/angular/e2e/global-setup-impl.ts | 11 +++++++++ .../timeline/angular/e2e/playwright.config.ts | 18 +++++++++++++++ .../chat/timeline/angular/e2e/tsconfig.json | 14 +++++++++++ cockpit/chat/timeline/angular/project.json | 6 +++++ 25 files changed, 315 insertions(+) create mode 100644 cockpit/chat/input/angular/e2e/c-input.spec.ts create mode 100644 cockpit/chat/input/angular/e2e/fixtures/c-input.json create mode 100644 cockpit/chat/input/angular/e2e/global-setup-impl.ts create mode 100644 cockpit/chat/input/angular/e2e/playwright.config.ts create mode 100644 cockpit/chat/input/angular/e2e/tsconfig.json create mode 100644 cockpit/chat/theming/angular/e2e/c-theming.spec.ts create mode 100644 cockpit/chat/theming/angular/e2e/fixtures/c-theming.json create mode 100644 cockpit/chat/theming/angular/e2e/global-setup-impl.ts create mode 100644 cockpit/chat/theming/angular/e2e/playwright.config.ts create mode 100644 cockpit/chat/theming/angular/e2e/tsconfig.json create mode 100644 cockpit/chat/threads/angular/e2e/c-threads.spec.ts create mode 100644 cockpit/chat/threads/angular/e2e/fixtures/c-threads.json create mode 100644 cockpit/chat/threads/angular/e2e/global-setup-impl.ts create mode 100644 cockpit/chat/threads/angular/e2e/playwright.config.ts create mode 100644 cockpit/chat/threads/angular/e2e/tsconfig.json create mode 100644 cockpit/chat/timeline/angular/e2e/c-timeline.spec.ts create mode 100644 cockpit/chat/timeline/angular/e2e/fixtures/c-timeline.json create mode 100644 cockpit/chat/timeline/angular/e2e/global-setup-impl.ts create mode 100644 cockpit/chat/timeline/angular/e2e/playwright.config.ts create mode 100644 cockpit/chat/timeline/angular/e2e/tsconfig.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c40e76e6..c4c97c1e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -271,6 +271,10 @@ jobs: - { angular: cockpit-chat-subagents-angular, python: cockpit/chat/subagents/python } - { angular: cockpit-chat-interrupts-angular, python: cockpit/chat/interrupts/python } - { angular: cockpit-chat-messages-angular, python: cockpit/chat/messages/python } + - { angular: cockpit-chat-input-angular, python: cockpit/chat/input/python } + - { 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 } steps: - uses: actions/checkout@v6.0.2 - uses: actions/setup-node@v6.3.0 diff --git a/cockpit/chat/input/angular/e2e/c-input.spec.ts b/cockpit/chat/input/angular/e2e/c-input.spec.ts new file mode 100644 index 00000000..aad1402c --- /dev/null +++ b/cockpit/chat/input/angular/e2e/c-input.spec.ts @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +import { test, expect } from '@playwright/test'; +import { submitAndWaitForResponse } from '../../../../../libs/e2e-harness/src'; + +const PROMPT = 'Hello'; + +test('c-input: user message and AI response both render', async ({ page }) => { + const bubble = await submitAndWaitForResponse(page, PROMPT); + + await expect( + page.locator('chat-message[data-role="user"]').last(), + ).toContainText(PROMPT); + + await expect(bubble).toContainText('chat-input demo'); +}); + +test('c-input: chat-message-list renders both turns', async ({ page }) => { + await submitAndWaitForResponse(page, PROMPT); + + // c-input uses raw ChatMessageListComponent primitives with projected + // templates (per PR #466's fix). Regression coverage for that fix. + await expect(page.locator('chat-message-list chat-message')).toHaveCount(2); +}); diff --git a/cockpit/chat/input/angular/e2e/fixtures/c-input.json b/cockpit/chat/input/angular/e2e/fixtures/c-input.json new file mode 100644 index 00000000..2f2a7c77 --- /dev/null +++ b/cockpit/chat/input/angular/e2e/fixtures/c-input.json @@ -0,0 +1,10 @@ +{ + "fixtures": [ + { + "match": { "userMessage": "Hello" }, + "response": { + "content": "Hi! I'm the chat-input demo. Try Enter to send, Shift+Enter for newline, and watch the sidebar reflect the streaming status." + } + } + ] +} diff --git a/cockpit/chat/input/angular/e2e/global-setup-impl.ts b/cockpit/chat/input/angular/e2e/global-setup-impl.ts new file mode 100644 index 00000000..fb41c349 --- /dev/null +++ b/cockpit/chat/input/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/input/python', + langgraphPort: 5502, + angularProject: 'cockpit-chat-input-angular', + angularPort: 4502, + fixturesDir: resolve(__dirname, 'fixtures'), +}); diff --git a/cockpit/chat/input/angular/e2e/playwright.config.ts b/cockpit/chat/input/angular/e2e/playwright.config.ts new file mode 100644 index 00000000..63497def --- /dev/null +++ b/cockpit/chat/input/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:4502', + 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/input/angular/e2e/tsconfig.json b/cockpit/chat/input/angular/e2e/tsconfig.json new file mode 100644 index 00000000..0b5aeecb --- /dev/null +++ b/cockpit/chat/input/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/input/angular/project.json b/cockpit/chat/input/angular/project.json index 39d44842..c581f8f5 100644 --- a/cockpit/chat/input/angular/project.json +++ b/cockpit/chat/input/angular/project.json @@ -87,6 +87,12 @@ "cwd": "cockpit/chat/input/angular", "command": "npx tsx -e \"import { chatInputAngularModule } from './src/index.ts'; const module = chatInputAngularModule; if (module.id !== 'chat-input-angular' || module.title !== 'Chat Input (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" } + }, + "e2e": { + "executor": "@nx/playwright:playwright", + "options": { + "config": "cockpit/chat/input/angular/e2e/playwright.config.ts" + } } } } diff --git a/cockpit/chat/theming/angular/e2e/c-theming.spec.ts b/cockpit/chat/theming/angular/e2e/c-theming.spec.ts new file mode 100644 index 00000000..4cc5f725 --- /dev/null +++ b/cockpit/chat/theming/angular/e2e/c-theming.spec.ts @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +import { test, expect } from '@playwright/test'; +import { submitAndWaitForResponse } from '../../../../../libs/e2e-harness/src'; + +const PROMPT = 'Hello'; + +test('c-theming: AI response renders in composed chat surface', async ({ page }) => { + const bubble = await submitAndWaitForResponse(page, PROMPT); + await expect(bubble).toContainText('chat-theming demo'); +}); + +test('c-theming: theme picker buttons are present in sidebar', async ({ page }) => { + await page.goto('/'); + // Distinctive surface — four theme-picker buttons that set CSS custom + // properties on document.documentElement. + for (const label of ['Dark', 'Light', 'Ocean', 'Forest']) { + await expect(page.getByRole('button', { name: label })).toBeVisible(); + } +}); diff --git a/cockpit/chat/theming/angular/e2e/fixtures/c-theming.json b/cockpit/chat/theming/angular/e2e/fixtures/c-theming.json new file mode 100644 index 00000000..3da770b3 --- /dev/null +++ b/cockpit/chat/theming/angular/e2e/fixtures/c-theming.json @@ -0,0 +1,10 @@ +{ + "fixtures": [ + { + "match": { "userMessage": "Hello" }, + "response": { + "content": "Hi! I'm the chat-theming demo. Try the theme picker buttons in the sidebar to switch between dark, light, ocean, and forest schemes." + } + } + ] +} diff --git a/cockpit/chat/theming/angular/e2e/global-setup-impl.ts b/cockpit/chat/theming/angular/e2e/global-setup-impl.ts new file mode 100644 index 00000000..51b25427 --- /dev/null +++ b/cockpit/chat/theming/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/theming/python', + langgraphPort: 5510, + angularProject: 'cockpit-chat-theming-angular', + angularPort: 4510, + fixturesDir: resolve(__dirname, 'fixtures'), +}); diff --git a/cockpit/chat/theming/angular/e2e/playwright.config.ts b/cockpit/chat/theming/angular/e2e/playwright.config.ts new file mode 100644 index 00000000..34f2d3fd --- /dev/null +++ b/cockpit/chat/theming/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:4510', + 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/theming/angular/e2e/tsconfig.json b/cockpit/chat/theming/angular/e2e/tsconfig.json new file mode 100644 index 00000000..0b5aeecb --- /dev/null +++ b/cockpit/chat/theming/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/theming/angular/project.json b/cockpit/chat/theming/angular/project.json index a9779cb8..d258c921 100644 --- a/cockpit/chat/theming/angular/project.json +++ b/cockpit/chat/theming/angular/project.json @@ -87,6 +87,12 @@ "cwd": "cockpit/chat/theming/angular", "command": "npx tsx -e \"import { chatThemingAngularModule } from './src/index.ts'; const module = chatThemingAngularModule; if (module.id !== 'chat-theming-angular' || module.title !== 'Chat Theming (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" } + }, + "e2e": { + "executor": "@nx/playwright:playwright", + "options": { + "config": "cockpit/chat/theming/angular/e2e/playwright.config.ts" + } } } } diff --git a/cockpit/chat/threads/angular/e2e/c-threads.spec.ts b/cockpit/chat/threads/angular/e2e/c-threads.spec.ts new file mode 100644 index 00000000..1592909a --- /dev/null +++ b/cockpit/chat/threads/angular/e2e/c-threads.spec.ts @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +import { test, expect } from '@playwright/test'; +import { submitAndWaitForResponse } from '../../../../../libs/e2e-harness/src'; + +const PROMPT = 'Hello'; + +test('c-threads: AI response renders in composed chat surface', async ({ page }) => { + const bubble = await submitAndWaitForResponse(page, PROMPT); + await expect(bubble).toContainText('chat-threads demo'); +}); + +test('c-threads: chat-thread-list sidebar is mounted', async ({ page }) => { + await page.goto('/'); + // Sidebar's distinctive surface — thread switcher seeded with 3 hardcoded + // threads (thread-1/2/3). Existence + non-zero entry count proves wiring. + await expect(page.locator('chat-thread-list')).toBeVisible(); +}); diff --git a/cockpit/chat/threads/angular/e2e/fixtures/c-threads.json b/cockpit/chat/threads/angular/e2e/fixtures/c-threads.json new file mode 100644 index 00000000..8c430436 --- /dev/null +++ b/cockpit/chat/threads/angular/e2e/fixtures/c-threads.json @@ -0,0 +1,10 @@ +{ + "fixtures": [ + { + "match": { "userMessage": "Hello" }, + "response": { + "content": "Hi! I'm the chat-threads demo. Switch between threads using the sidebar list to see independent conversation contexts." + } + } + ] +} diff --git a/cockpit/chat/threads/angular/e2e/global-setup-impl.ts b/cockpit/chat/threads/angular/e2e/global-setup-impl.ts new file mode 100644 index 00000000..42d85661 --- /dev/null +++ b/cockpit/chat/threads/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/threads/python', + langgraphPort: 5506, + angularProject: 'cockpit-chat-threads-angular', + angularPort: 4506, + fixturesDir: resolve(__dirname, 'fixtures'), +}); diff --git a/cockpit/chat/threads/angular/e2e/playwright.config.ts b/cockpit/chat/threads/angular/e2e/playwright.config.ts new file mode 100644 index 00000000..085d8757 --- /dev/null +++ b/cockpit/chat/threads/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:4506', + 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/threads/angular/e2e/tsconfig.json b/cockpit/chat/threads/angular/e2e/tsconfig.json new file mode 100644 index 00000000..0b5aeecb --- /dev/null +++ b/cockpit/chat/threads/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/threads/angular/project.json b/cockpit/chat/threads/angular/project.json index f4ebf004..5e863b3b 100644 --- a/cockpit/chat/threads/angular/project.json +++ b/cockpit/chat/threads/angular/project.json @@ -87,6 +87,12 @@ "cwd": "cockpit/chat/threads/angular", "command": "npx tsx -e \"import { chatThreadsAngularModule } from './src/index.ts'; const module = chatThreadsAngularModule; if (module.id !== 'chat-threads-angular' || module.title !== 'Chat Threads (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" } + }, + "e2e": { + "executor": "@nx/playwright:playwright", + "options": { + "config": "cockpit/chat/threads/angular/e2e/playwright.config.ts" + } } } } diff --git a/cockpit/chat/timeline/angular/e2e/c-timeline.spec.ts b/cockpit/chat/timeline/angular/e2e/c-timeline.spec.ts new file mode 100644 index 00000000..8abcb2d6 --- /dev/null +++ b/cockpit/chat/timeline/angular/e2e/c-timeline.spec.ts @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +import { test, expect } from '@playwright/test'; +import { submitAndWaitForResponse } from '../../../../../libs/e2e-harness/src'; + +const PROMPT = 'Hello'; + +test('c-timeline: AI response renders in composed chat surface', async ({ page }) => { + const bubble = await submitAndWaitForResponse(page, PROMPT); + await expect(bubble).toContainText('chat-timeline demo'); +}); + +test('c-timeline: chat-timeline-slider sidebar is mounted', async ({ page }) => { + await page.goto('/'); + // Distinctive surface — slider reflects checkpoint state. + await expect(page.locator('chat-timeline-slider')).toBeVisible(); +}); diff --git a/cockpit/chat/timeline/angular/e2e/fixtures/c-timeline.json b/cockpit/chat/timeline/angular/e2e/fixtures/c-timeline.json new file mode 100644 index 00000000..23c796ef --- /dev/null +++ b/cockpit/chat/timeline/angular/e2e/fixtures/c-timeline.json @@ -0,0 +1,10 @@ +{ + "fixtures": [ + { + "match": { "userMessage": "Hello" }, + "response": { + "content": "Hi! I'm the chat-timeline demo. Each message creates a checkpoint you can scrub with the timeline slider in the sidebar." + } + } + ] +} diff --git a/cockpit/chat/timeline/angular/e2e/global-setup-impl.ts b/cockpit/chat/timeline/angular/e2e/global-setup-impl.ts new file mode 100644 index 00000000..09a9de54 --- /dev/null +++ b/cockpit/chat/timeline/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/timeline/python', + langgraphPort: 5507, + angularProject: 'cockpit-chat-timeline-angular', + angularPort: 4507, + fixturesDir: resolve(__dirname, 'fixtures'), +}); diff --git a/cockpit/chat/timeline/angular/e2e/playwright.config.ts b/cockpit/chat/timeline/angular/e2e/playwright.config.ts new file mode 100644 index 00000000..153e8626 --- /dev/null +++ b/cockpit/chat/timeline/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:4507', + 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/timeline/angular/e2e/tsconfig.json b/cockpit/chat/timeline/angular/e2e/tsconfig.json new file mode 100644 index 00000000..0b5aeecb --- /dev/null +++ b/cockpit/chat/timeline/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/timeline/angular/project.json b/cockpit/chat/timeline/angular/project.json index b3e9c34e..58d065d2 100644 --- a/cockpit/chat/timeline/angular/project.json +++ b/cockpit/chat/timeline/angular/project.json @@ -87,6 +87,12 @@ "cwd": "cockpit/chat/timeline/angular", "command": "npx tsx -e \"import { chatTimelineAngularModule } from './src/index.ts'; const module = chatTimelineAngularModule; if (module.id !== 'chat-timeline-angular' || module.title !== 'Chat Timeline (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" } + }, + "e2e": { + "executor": "@nx/playwright:playwright", + "options": { + "config": "cockpit/chat/timeline/angular/e2e/playwright.config.ts" + } } } }