From 13c0f487303bc1f2d264fedf52640e9fa8bebfb7 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Thu, 9 Jul 2026 10:06:16 -0700 Subject: [PATCH] test(cockpit): promote render examples to e2e --- apps/cockpit/scripts/serve-example.spec.ts | 9 ++++++-- apps/cockpit/scripts/serve-example.ts | 6 +++++- .../angular/e2e/fixtures/.gitkeep | 1 + .../angular/e2e/global-setup-impl.ts | 14 +++++++++++++ .../angular/e2e/playwright.config.ts | 21 +++++++++++++++++++ .../angular/e2e/r-computed-functions.spec.ts | 18 ++++++++++++++++ .../computed-functions/angular/project.json | 6 ++++++ .../angular/e2e/fixtures/.gitkeep | 1 + .../angular/e2e/global-setup-impl.ts | 14 +++++++++++++ .../angular/e2e/playwright.config.ts | 21 +++++++++++++++++++ .../angular/e2e/r-element-rendering.spec.ts | 18 ++++++++++++++++ .../element-rendering/angular/project.json | 6 ++++++ .../registry/angular/e2e/fixtures/.gitkeep | 1 + .../registry/angular/e2e/global-setup-impl.ts | 14 +++++++++++++ .../registry/angular/e2e/playwright.config.ts | 21 +++++++++++++++++++ .../registry/angular/e2e/r-registry.spec.ts | 18 ++++++++++++++++ cockpit/render/registry/angular/project.json | 6 ++++++ .../angular/e2e/fixtures/.gitkeep | 1 + .../angular/e2e/global-setup-impl.ts | 14 +++++++++++++ .../angular/e2e/playwright.config.ts | 21 +++++++++++++++++++ .../angular/e2e/r-repeat-loops.spec.ts | 18 ++++++++++++++++ .../render/repeat-loops/angular/project.json | 6 ++++++ .../angular/e2e/fixtures/.gitkeep | 1 + .../angular/e2e/global-setup-impl.ts | 14 +++++++++++++ .../angular/e2e/playwright.config.ts | 21 +++++++++++++++++++ .../angular/e2e/r-spec-rendering.spec.ts | 18 ++++++++++++++++ .../spec-rendering/angular/project.json | 6 ++++++ .../angular/e2e/fixtures/.gitkeep | 1 + .../angular/e2e/global-setup-impl.ts | 14 +++++++++++++ .../angular/e2e/playwright.config.ts | 21 +++++++++++++++++++ .../angular/e2e/r-state-management.spec.ts | 18 ++++++++++++++++ .../state-management/angular/project.json | 6 ++++++ scripts/cockpit-ports.spec.mjs | 17 +++++++++++++++ 33 files changed, 389 insertions(+), 3 deletions(-) create mode 100644 cockpit/render/computed-functions/angular/e2e/fixtures/.gitkeep create mode 100644 cockpit/render/computed-functions/angular/e2e/global-setup-impl.ts create mode 100644 cockpit/render/computed-functions/angular/e2e/playwright.config.ts create mode 100644 cockpit/render/computed-functions/angular/e2e/r-computed-functions.spec.ts create mode 100644 cockpit/render/element-rendering/angular/e2e/fixtures/.gitkeep create mode 100644 cockpit/render/element-rendering/angular/e2e/global-setup-impl.ts create mode 100644 cockpit/render/element-rendering/angular/e2e/playwright.config.ts create mode 100644 cockpit/render/element-rendering/angular/e2e/r-element-rendering.spec.ts create mode 100644 cockpit/render/registry/angular/e2e/fixtures/.gitkeep create mode 100644 cockpit/render/registry/angular/e2e/global-setup-impl.ts create mode 100644 cockpit/render/registry/angular/e2e/playwright.config.ts create mode 100644 cockpit/render/registry/angular/e2e/r-registry.spec.ts create mode 100644 cockpit/render/repeat-loops/angular/e2e/fixtures/.gitkeep create mode 100644 cockpit/render/repeat-loops/angular/e2e/global-setup-impl.ts create mode 100644 cockpit/render/repeat-loops/angular/e2e/playwright.config.ts create mode 100644 cockpit/render/repeat-loops/angular/e2e/r-repeat-loops.spec.ts create mode 100644 cockpit/render/spec-rendering/angular/e2e/fixtures/.gitkeep create mode 100644 cockpit/render/spec-rendering/angular/e2e/global-setup-impl.ts create mode 100644 cockpit/render/spec-rendering/angular/e2e/playwright.config.ts create mode 100644 cockpit/render/spec-rendering/angular/e2e/r-spec-rendering.spec.ts create mode 100644 cockpit/render/state-management/angular/e2e/fixtures/.gitkeep create mode 100644 cockpit/render/state-management/angular/e2e/global-setup-impl.ts create mode 100644 cockpit/render/state-management/angular/e2e/playwright.config.ts create mode 100644 cockpit/render/state-management/angular/e2e/r-state-management.spec.ts diff --git a/apps/cockpit/scripts/serve-example.spec.ts b/apps/cockpit/scripts/serve-example.spec.ts index 6e634d1bc..12cdf37bc 100644 --- a/apps/cockpit/scripts/serve-example.spec.ts +++ b/apps/cockpit/scripts/serve-example.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { backendCommand, COCKPIT_RUNTIME_ENV } from './serve-example'; -import { findCapability, type Capability } from './capability-registry'; +import { backendCommand, COCKPIT_RUNTIME_ENV, formatAllModeSummary } from './serve-example'; +import { capabilities, findCapability, type Capability } from './capability-registry'; describe('backendCommand', () => { it('uses uvicorn on the registry pythonPort for ag-ui caps', () => { @@ -36,4 +36,9 @@ describe('backendCommand', () => { it('exposes an empty runtime base URL so the cockpit iframe targets localhost', () => { expect(COCKPIT_RUNTIME_ENV).toEqual({ NEXT_PUBLIC_COCKPIT_RUNTIME_BASE_URL: '' }); }); + + it('formats the all-mode startup summary from the registry count', () => { + expect(formatAllModeSummary()).toBe(`\nStarting cockpit + all ${capabilities.length} examples\n`); + expect(formatAllModeSummary()).not.toContain('14 examples'); + }); }); diff --git a/apps/cockpit/scripts/serve-example.ts b/apps/cockpit/scripts/serve-example.ts index 4014addc3..4e1a81005 100644 --- a/apps/cockpit/scripts/serve-example.ts +++ b/apps/cockpit/scripts/serve-example.ts @@ -15,6 +15,10 @@ export function backendCommand(cap: Capability): string | null { return `${enter} && uv run langgraph dev --port ${cap.pythonPort} --no-browser`; } +export function formatAllModeSummary(): string { + return `\nStarting cockpit + all ${capabilities.length} examples\n`; +} + // Guard: only execute CLI/orchestration logic when run directly (not imported by tests). if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { const args = process.argv.slice(2); @@ -47,7 +51,7 @@ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { if (allMode) { capabilities.forEach((c) => run(c.id, `npx nx run ${c.angularProject}:serve:cockpit --port ${c.port}`, '33')); - console.log('\nšŸš€ Starting cockpit + all 14 examples\n'); + console.log(formatAllModeSummary()); } else { const cap = findCapability(capabilityArg!); if (!cap) { console.error(`Unknown: ${capabilityArg}`); process.exit(1); } diff --git a/cockpit/render/computed-functions/angular/e2e/fixtures/.gitkeep b/cockpit/render/computed-functions/angular/e2e/fixtures/.gitkeep new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/cockpit/render/computed-functions/angular/e2e/fixtures/.gitkeep @@ -0,0 +1 @@ + diff --git a/cockpit/render/computed-functions/angular/e2e/global-setup-impl.ts b/cockpit/render/computed-functions/angular/e2e/global-setup-impl.ts new file mode 100644 index 000000000..ac4138193 --- /dev/null +++ b/cockpit/render/computed-functions/angular/e2e/global-setup-impl.ts @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +import { resolve } from 'node:path'; +import { portsFor } from '../../../../../cockpit/ports.mjs'; +import { createGlobalSetup } from '@threadplane-internal/e2e-harness'; + +const ports = portsFor('cockpit-render-computed-functions-angular'); + +export default createGlobalSetup({ + langgraphCwd: 'cockpit/render/computed-functions/python', + langgraphPort: ports.langgraph, + angularProject: 'cockpit-render-computed-functions-angular', + angularPort: ports.angular, + fixturesDir: resolve(__dirname, 'fixtures'), +}); diff --git a/cockpit/render/computed-functions/angular/e2e/playwright.config.ts b/cockpit/render/computed-functions/angular/e2e/playwright.config.ts new file mode 100644 index 000000000..0f7540fd2 --- /dev/null +++ b/cockpit/render/computed-functions/angular/e2e/playwright.config.ts @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +import { defineConfig, devices } from '@playwright/test'; +import { portsFor } from '../../../../../cockpit/ports.mjs'; + +const { angular: angularPort } = portsFor('cockpit-render-computed-functions-angular'); + +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:${angularPort}`, + 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/render/computed-functions/angular/e2e/r-computed-functions.spec.ts b/cockpit/render/computed-functions/angular/e2e/r-computed-functions.spec.ts new file mode 100644 index 000000000..605b1709b --- /dev/null +++ b/cockpit/render/computed-functions/angular/e2e/r-computed-functions.spec.ts @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +import { expect, test } from '@playwright/test'; + +test.describe('Render Computed Functions Example', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + await page.waitForSelector('app-computed-functions', { state: 'attached' }); + }); + + test('renders spec picker and timeline', async ({ page }) => { + await expect(page.locator('button', { hasText: 'Text Transforms' })).toBeVisible(); + await expect(page.locator('streaming-timeline')).toBeVisible(); + }); + + test('shows streaming JSON pane', async ({ page }) => { + await expect(page.locator('pre')).toBeVisible(); + }); +}); diff --git a/cockpit/render/computed-functions/angular/project.json b/cockpit/render/computed-functions/angular/project.json index 31877d42a..4902281a8 100644 --- a/cockpit/render/computed-functions/angular/project.json +++ b/cockpit/render/computed-functions/angular/project.json @@ -87,6 +87,12 @@ "cwd": "cockpit/render/computed-functions/angular", "command": "npx tsx -e \"import { renderComputedFunctionsAngularModule } from './src/index.ts'; const module = renderComputedFunctionsAngularModule; if (module.id !== 'render-computed-functions-angular' || module.title !== 'Render Computed Functions (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" } + }, + "e2e": { + "executor": "@nx/playwright:playwright", + "options": { + "config": "cockpit/render/computed-functions/angular/e2e/playwright.config.ts" + } } }, "tags": [ diff --git a/cockpit/render/element-rendering/angular/e2e/fixtures/.gitkeep b/cockpit/render/element-rendering/angular/e2e/fixtures/.gitkeep new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/cockpit/render/element-rendering/angular/e2e/fixtures/.gitkeep @@ -0,0 +1 @@ + diff --git a/cockpit/render/element-rendering/angular/e2e/global-setup-impl.ts b/cockpit/render/element-rendering/angular/e2e/global-setup-impl.ts new file mode 100644 index 000000000..4cec342d5 --- /dev/null +++ b/cockpit/render/element-rendering/angular/e2e/global-setup-impl.ts @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +import { resolve } from 'node:path'; +import { portsFor } from '../../../../../cockpit/ports.mjs'; +import { createGlobalSetup } from '@threadplane-internal/e2e-harness'; + +const ports = portsFor('cockpit-render-element-rendering-angular'); + +export default createGlobalSetup({ + langgraphCwd: 'cockpit/render/element-rendering/python', + langgraphPort: ports.langgraph, + angularProject: 'cockpit-render-element-rendering-angular', + angularPort: ports.angular, + fixturesDir: resolve(__dirname, 'fixtures'), +}); diff --git a/cockpit/render/element-rendering/angular/e2e/playwright.config.ts b/cockpit/render/element-rendering/angular/e2e/playwright.config.ts new file mode 100644 index 000000000..1104ddb78 --- /dev/null +++ b/cockpit/render/element-rendering/angular/e2e/playwright.config.ts @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +import { defineConfig, devices } from '@playwright/test'; +import { portsFor } from '../../../../../cockpit/ports.mjs'; + +const { angular: angularPort } = portsFor('cockpit-render-element-rendering-angular'); + +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:${angularPort}`, + 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/render/element-rendering/angular/e2e/r-element-rendering.spec.ts b/cockpit/render/element-rendering/angular/e2e/r-element-rendering.spec.ts new file mode 100644 index 000000000..e4824c0f3 --- /dev/null +++ b/cockpit/render/element-rendering/angular/e2e/r-element-rendering.spec.ts @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +import { expect, test } from '@playwright/test'; + +test.describe('Render Element Rendering Example', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + await page.waitForSelector('app-element-rendering', { state: 'attached' }); + }); + + test('renders spec picker and timeline', async ({ page }) => { + await expect(page.locator('button', { hasText: 'Parent + Children' })).toBeVisible(); + await expect(page.locator('streaming-timeline')).toBeVisible(); + }); + + test('shows streaming JSON pane', async ({ page }) => { + await expect(page.locator('pre')).toBeVisible(); + }); +}); diff --git a/cockpit/render/element-rendering/angular/project.json b/cockpit/render/element-rendering/angular/project.json index 40378d21d..14791781e 100644 --- a/cockpit/render/element-rendering/angular/project.json +++ b/cockpit/render/element-rendering/angular/project.json @@ -87,6 +87,12 @@ "cwd": "cockpit/render/element-rendering/angular", "command": "npx tsx -e \"import { renderElementRenderingAngularModule } from './src/index.ts'; const module = renderElementRenderingAngularModule; if (module.id !== 'render-element-rendering-angular' || module.title !== 'Render Element Rendering (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" } + }, + "e2e": { + "executor": "@nx/playwright:playwright", + "options": { + "config": "cockpit/render/element-rendering/angular/e2e/playwright.config.ts" + } } }, "tags": [ diff --git a/cockpit/render/registry/angular/e2e/fixtures/.gitkeep b/cockpit/render/registry/angular/e2e/fixtures/.gitkeep new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/cockpit/render/registry/angular/e2e/fixtures/.gitkeep @@ -0,0 +1 @@ + diff --git a/cockpit/render/registry/angular/e2e/global-setup-impl.ts b/cockpit/render/registry/angular/e2e/global-setup-impl.ts new file mode 100644 index 000000000..844aae8b2 --- /dev/null +++ b/cockpit/render/registry/angular/e2e/global-setup-impl.ts @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +import { resolve } from 'node:path'; +import { portsFor } from '../../../../../cockpit/ports.mjs'; +import { createGlobalSetup } from '@threadplane-internal/e2e-harness'; + +const ports = portsFor('cockpit-render-registry-angular'); + +export default createGlobalSetup({ + langgraphCwd: 'cockpit/render/registry/python', + langgraphPort: ports.langgraph, + angularProject: 'cockpit-render-registry-angular', + angularPort: ports.angular, + fixturesDir: resolve(__dirname, 'fixtures'), +}); diff --git a/cockpit/render/registry/angular/e2e/playwright.config.ts b/cockpit/render/registry/angular/e2e/playwright.config.ts new file mode 100644 index 000000000..744f130e5 --- /dev/null +++ b/cockpit/render/registry/angular/e2e/playwright.config.ts @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +import { defineConfig, devices } from '@playwright/test'; +import { portsFor } from '../../../../../cockpit/ports.mjs'; + +const { angular: angularPort } = portsFor('cockpit-render-registry-angular'); + +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:${angularPort}`, + 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/render/registry/angular/e2e/r-registry.spec.ts b/cockpit/render/registry/angular/e2e/r-registry.spec.ts new file mode 100644 index 000000000..05e206d93 --- /dev/null +++ b/cockpit/render/registry/angular/e2e/r-registry.spec.ts @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +import { expect, test } from '@playwright/test'; + +test.describe('Render Registry Example', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + await page.waitForSelector('app-registry', { state: 'attached' }); + }); + + test('renders spec picker and timeline', async ({ page }) => { + await expect(page.locator('button', { hasText: 'Basic Types' })).toBeVisible(); + await expect(page.locator('streaming-timeline')).toBeVisible(); + }); + + test('shows streaming JSON pane', async ({ page }) => { + await expect(page.locator('pre')).toBeVisible(); + }); +}); diff --git a/cockpit/render/registry/angular/project.json b/cockpit/render/registry/angular/project.json index 0d6d48570..17fc63838 100644 --- a/cockpit/render/registry/angular/project.json +++ b/cockpit/render/registry/angular/project.json @@ -87,6 +87,12 @@ "cwd": "cockpit/render/registry/angular", "command": "npx tsx -e \"import { renderRegistryAngularModule } from './src/index.ts'; const module = renderRegistryAngularModule; if (module.id !== 'render-registry-angular' || module.title !== 'Render Registry (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" } + }, + "e2e": { + "executor": "@nx/playwright:playwright", + "options": { + "config": "cockpit/render/registry/angular/e2e/playwright.config.ts" + } } }, "tags": [ diff --git a/cockpit/render/repeat-loops/angular/e2e/fixtures/.gitkeep b/cockpit/render/repeat-loops/angular/e2e/fixtures/.gitkeep new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/cockpit/render/repeat-loops/angular/e2e/fixtures/.gitkeep @@ -0,0 +1 @@ + diff --git a/cockpit/render/repeat-loops/angular/e2e/global-setup-impl.ts b/cockpit/render/repeat-loops/angular/e2e/global-setup-impl.ts new file mode 100644 index 000000000..c11731dfb --- /dev/null +++ b/cockpit/render/repeat-loops/angular/e2e/global-setup-impl.ts @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +import { resolve } from 'node:path'; +import { portsFor } from '../../../../../cockpit/ports.mjs'; +import { createGlobalSetup } from '@threadplane-internal/e2e-harness'; + +const ports = portsFor('cockpit-render-repeat-loops-angular'); + +export default createGlobalSetup({ + langgraphCwd: 'cockpit/render/repeat-loops/python', + langgraphPort: ports.langgraph, + angularProject: 'cockpit-render-repeat-loops-angular', + angularPort: ports.angular, + fixturesDir: resolve(__dirname, 'fixtures'), +}); diff --git a/cockpit/render/repeat-loops/angular/e2e/playwright.config.ts b/cockpit/render/repeat-loops/angular/e2e/playwright.config.ts new file mode 100644 index 000000000..a627b3269 --- /dev/null +++ b/cockpit/render/repeat-loops/angular/e2e/playwright.config.ts @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +import { defineConfig, devices } from '@playwright/test'; +import { portsFor } from '../../../../../cockpit/ports.mjs'; + +const { angular: angularPort } = portsFor('cockpit-render-repeat-loops-angular'); + +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:${angularPort}`, + 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/render/repeat-loops/angular/e2e/r-repeat-loops.spec.ts b/cockpit/render/repeat-loops/angular/e2e/r-repeat-loops.spec.ts new file mode 100644 index 000000000..6792c186b --- /dev/null +++ b/cockpit/render/repeat-loops/angular/e2e/r-repeat-loops.spec.ts @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +import { expect, test } from '@playwright/test'; + +test.describe('Render Repeat Loops Example', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + await page.waitForSelector('app-repeat-loops', { state: 'attached' }); + }); + + test('renders spec picker and timeline', async ({ page }) => { + await expect(page.locator('button', { hasText: 'Simple List' })).toBeVisible(); + await expect(page.locator('streaming-timeline')).toBeVisible(); + }); + + test('shows streaming JSON pane', async ({ page }) => { + await expect(page.locator('pre')).toBeVisible(); + }); +}); diff --git a/cockpit/render/repeat-loops/angular/project.json b/cockpit/render/repeat-loops/angular/project.json index 3745a40bc..3ecc515ab 100644 --- a/cockpit/render/repeat-loops/angular/project.json +++ b/cockpit/render/repeat-loops/angular/project.json @@ -87,6 +87,12 @@ "cwd": "cockpit/render/repeat-loops/angular", "command": "npx tsx -e \"import { renderRepeatLoopsAngularModule } from './src/index.ts'; const module = renderRepeatLoopsAngularModule; if (module.id !== 'render-repeat-loops-angular' || module.title !== 'Render Repeat Loops (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" } + }, + "e2e": { + "executor": "@nx/playwright:playwright", + "options": { + "config": "cockpit/render/repeat-loops/angular/e2e/playwright.config.ts" + } } }, "tags": [ diff --git a/cockpit/render/spec-rendering/angular/e2e/fixtures/.gitkeep b/cockpit/render/spec-rendering/angular/e2e/fixtures/.gitkeep new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/cockpit/render/spec-rendering/angular/e2e/fixtures/.gitkeep @@ -0,0 +1 @@ + diff --git a/cockpit/render/spec-rendering/angular/e2e/global-setup-impl.ts b/cockpit/render/spec-rendering/angular/e2e/global-setup-impl.ts new file mode 100644 index 000000000..8b08b8a69 --- /dev/null +++ b/cockpit/render/spec-rendering/angular/e2e/global-setup-impl.ts @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +import { resolve } from 'node:path'; +import { portsFor } from '../../../../../cockpit/ports.mjs'; +import { createGlobalSetup } from '@threadplane-internal/e2e-harness'; + +const ports = portsFor('cockpit-render-spec-rendering-angular'); + +export default createGlobalSetup({ + langgraphCwd: 'cockpit/render/spec-rendering/python', + langgraphPort: ports.langgraph, + angularProject: 'cockpit-render-spec-rendering-angular', + angularPort: ports.angular, + fixturesDir: resolve(__dirname, 'fixtures'), +}); diff --git a/cockpit/render/spec-rendering/angular/e2e/playwright.config.ts b/cockpit/render/spec-rendering/angular/e2e/playwright.config.ts new file mode 100644 index 000000000..4c950a5f8 --- /dev/null +++ b/cockpit/render/spec-rendering/angular/e2e/playwright.config.ts @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +import { defineConfig, devices } from '@playwright/test'; +import { portsFor } from '../../../../../cockpit/ports.mjs'; + +const { angular: angularPort } = portsFor('cockpit-render-spec-rendering-angular'); + +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:${angularPort}`, + 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/render/spec-rendering/angular/e2e/r-spec-rendering.spec.ts b/cockpit/render/spec-rendering/angular/e2e/r-spec-rendering.spec.ts new file mode 100644 index 000000000..f53c7685b --- /dev/null +++ b/cockpit/render/spec-rendering/angular/e2e/r-spec-rendering.spec.ts @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +import { expect, test } from '@playwright/test'; + +test.describe('Render Spec Rendering Example', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + await page.waitForSelector('app-spec-rendering', { state: 'attached' }); + }); + + test('renders spec picker and timeline', async ({ page }) => { + await expect(page.locator('button', { hasText: 'Heading + Text' })).toBeVisible(); + await expect(page.locator('streaming-timeline')).toBeVisible(); + }); + + test('shows streaming JSON pane', async ({ page }) => { + await expect(page.locator('pre')).toBeVisible(); + }); +}); diff --git a/cockpit/render/spec-rendering/angular/project.json b/cockpit/render/spec-rendering/angular/project.json index a58092be8..737e4df28 100644 --- a/cockpit/render/spec-rendering/angular/project.json +++ b/cockpit/render/spec-rendering/angular/project.json @@ -87,6 +87,12 @@ "cwd": "cockpit/render/spec-rendering/angular", "command": "npx tsx -e \"import { renderSpecRenderingAngularModule } from './src/index.ts'; const module = renderSpecRenderingAngularModule; if (module.id !== 'render-spec-rendering-angular' || module.title !== 'Render Spec Rendering (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" } + }, + "e2e": { + "executor": "@nx/playwright:playwright", + "options": { + "config": "cockpit/render/spec-rendering/angular/e2e/playwright.config.ts" + } } }, "tags": [ diff --git a/cockpit/render/state-management/angular/e2e/fixtures/.gitkeep b/cockpit/render/state-management/angular/e2e/fixtures/.gitkeep new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/cockpit/render/state-management/angular/e2e/fixtures/.gitkeep @@ -0,0 +1 @@ + diff --git a/cockpit/render/state-management/angular/e2e/global-setup-impl.ts b/cockpit/render/state-management/angular/e2e/global-setup-impl.ts new file mode 100644 index 000000000..951a43768 --- /dev/null +++ b/cockpit/render/state-management/angular/e2e/global-setup-impl.ts @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +import { resolve } from 'node:path'; +import { portsFor } from '../../../../../cockpit/ports.mjs'; +import { createGlobalSetup } from '@threadplane-internal/e2e-harness'; + +const ports = portsFor('cockpit-render-state-management-angular'); + +export default createGlobalSetup({ + langgraphCwd: 'cockpit/render/state-management/python', + langgraphPort: ports.langgraph, + angularProject: 'cockpit-render-state-management-angular', + angularPort: ports.angular, + fixturesDir: resolve(__dirname, 'fixtures'), +}); diff --git a/cockpit/render/state-management/angular/e2e/playwright.config.ts b/cockpit/render/state-management/angular/e2e/playwright.config.ts new file mode 100644 index 000000000..a179c876f --- /dev/null +++ b/cockpit/render/state-management/angular/e2e/playwright.config.ts @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +import { defineConfig, devices } from '@playwright/test'; +import { portsFor } from '../../../../../cockpit/ports.mjs'; + +const { angular: angularPort } = portsFor('cockpit-render-state-management-angular'); + +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:${angularPort}`, + 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/render/state-management/angular/e2e/r-state-management.spec.ts b/cockpit/render/state-management/angular/e2e/r-state-management.spec.ts new file mode 100644 index 000000000..72de5b62f --- /dev/null +++ b/cockpit/render/state-management/angular/e2e/r-state-management.spec.ts @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +import { expect, test } from '@playwright/test'; + +test.describe('Render State Management Example', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + await page.waitForSelector('app-state-management', { state: 'attached' }); + }); + + test('renders spec picker and timeline', async ({ page }) => { + await expect(page.locator('button', { hasText: 'User Profile' })).toBeVisible(); + await expect(page.locator('streaming-timeline')).toBeVisible(); + }); + + test('shows streaming JSON pane', async ({ page }) => { + await expect(page.locator('pre')).toBeVisible(); + }); +}); diff --git a/cockpit/render/state-management/angular/project.json b/cockpit/render/state-management/angular/project.json index 3d65502ec..4c8e220f6 100644 --- a/cockpit/render/state-management/angular/project.json +++ b/cockpit/render/state-management/angular/project.json @@ -87,6 +87,12 @@ "cwd": "cockpit/render/state-management/angular", "command": "npx tsx -e \"import { renderStateManagementAngularModule } from './src/index.ts'; const module = renderStateManagementAngularModule; if (module.id !== 'render-state-management-angular' || module.title !== 'Render State Management (Angular)') { throw new Error('Unexpected module shape for ' + module.id); }\"" } + }, + "e2e": { + "executor": "@nx/playwright:playwright", + "options": { + "config": "cockpit/render/state-management/angular/e2e/playwright.config.ts" + } } }, "tags": [ diff --git a/scripts/cockpit-ports.spec.mjs b/scripts/cockpit-ports.spec.mjs index da1b041c2..60b4cfced 100644 --- a/scripts/cockpit-ports.spec.mjs +++ b/scripts/cockpit-ports.spec.mjs @@ -113,4 +113,21 @@ describe('cockpit/ports.mjs registry', () => { } assert.deepEqual(mismatches, []); }); + + test('render cockpit examples have target-backed e2e suites', () => { + const missing = []; + for (const { name, dir, meta } of projects.filter((p) => p.name.startsWith('cockpit-render-'))) { + const e2eDir = join(dir, 'e2e'); + const config = join(e2eDir, 'playwright.config.ts'); + const setup = join(e2eDir, 'global-setup-impl.ts'); + const spec = join(e2eDir, `${name.replace(/^cockpit-render-/, 'r-').replace(/-angular$/, '')}.spec.ts`); + + if (!meta.targets?.e2e) missing.push(`${name}: missing e2e target`); + if (!existsSync(config)) missing.push(`${name}: missing e2e/playwright.config.ts`); + if (!existsSync(setup)) missing.push(`${name}: missing e2e/global-setup-impl.ts`); + if (!existsSync(spec)) missing.push(`${name}: missing ${spec}`); + } + + assert.deepEqual(missing, []); + }); });