import { expect, test } from '@/playwright/suite'; import { openNewProjectModal as openNewProjectModalFromProjects } from '@/playwright/rail'; import { routeAgents } from '@/playwright/mock-factory'; import { clickDeckNextSlide, clickDeckPreviousSlide, openAllProjectFiles } from '@/playwright/workspace'; import type { Dialog, Locator, Page, Request, Response } from '@playwright/test'; import { mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { T } from '@/timeouts'; import { automatedUiScenarios } from '@/playwright/resources'; import type { UiScenario } from '@/playwright/resources'; const STORAGE_KEY = 'open-design:config'; const ACTIVE_ARTIFACT_PREVIEW_SELECTOR = '[data-testid="artifact-preview-frame"]:visible, [data-testid="artifact-preview-frame-url-load"]:visible, [data-testid="artifact-preview-frame-srcdoc"]:visible, [data-testid="live-artifact-preview-frame"]:visible'; const APP_OWNED_SCENARIO_FLOWS = new Set([ 'design-files-upload', 'design-files-delete', 'design-files-tab-persistence', 'uploaded-image-renders-in-preview', 'python-source-preview', 'example-use-prompt', 'comment-attachment-flow', ]); const CRITICAL_SCENARIO_IDS = new Set([ 'prototype-basic', 'deck-basic', 'hyperframes-basic', 'image-basic', 'video-basic', 'live-artifact-basic', 'conversation-persistence', 'file-mention', 'deep-link-preview', 'file-upload-send', 'conversation-delete-recovery', ]); test.describe.configure({ timeout: 45_000 }); function artifactPreview(page: Page) { return page.locator(ACTIVE_ARTIFACT_PREVIEW_SELECTOR).first(); } function artifactPreviewFrame(page: Page) { return page.frameLocator(ACTIVE_ARTIFACT_PREVIEW_SELECTOR); } function stagedAttachmentName(page: Page, name: string): Locator { return page .locator('[data-testid="staged-attachments"], [data-testid="staged-contexts"]') .getByText(name, { exact: true }); } test.beforeEach(async ({ page }) => { await page.addInitScript((key) => { window.localStorage.setItem( key, JSON.stringify({ mode: 'daemon', apiKey: '', baseUrl: 'https://api.anthropic.com', model: 'claude-sonnet-4-5', agentId: 'mock', skillId: null, designSystemId: null, onboardingCompleted: true, agentModels: {}, privacyDecisionAt: 1, telemetry: { metrics: false, content: false, artifactManifest: false }, }), ); }, STORAGE_KEY); await page.route('**/api/app-config', async (route) => { if (route.request().method() !== 'GET') { await route.continue(); return; } await route.fulfill({ json: { config: { onboardingCompleted: true, agentId: 'mock', skillId: null, designSystemId: null, agentModels: {}, privacyDecisionAt: 1, telemetry: { metrics: false, content: false, artifactManifest: false }, }, }, }); }); }); for (const entry of automatedUiScenarios().filter( (scenario) => !APP_OWNED_SCENARIO_FLOWS.has(scenario.flow ?? ''), )) { test(`[${scenarioPriority(entry)}]${criticalScenarioTag(entry)} ${entry.id}: ${entry.title}`, async ({ page }) => { await routeMockAgents(page); if (entry.flow === 'example-use-prompt') { const exampleSummary = { id: 'warm-utility-example', name: 'Warm Utility Example', description: 'A warm utility prototype example.', triggers: [], mode: 'prototype', platform: 'desktop', scenario: 'product', previewType: 'html', designSystemRequired: false, defaultFor: ['prototype'], upstream: null, featured: 1, fidelity: 'high-fidelity', speakerNotes: null, animations: null, hasBody: true, examplePrompt: entry.prompt, }; await page.route('**/api/skills', async (route) => { await route.fulfill({ json: { skills: [exampleSummary] } }); }); // The skills/design-templates split (see specs/current/ // skills-and-design-templates.md) moved the EntryView Templates // tab onto its own daemon registry. The fixture skill above now // also has to be served on the design-templates surface so the // gallery card the test clicks actually renders. await page.route('**/api/design-templates', async (route) => { await route.fulfill({ json: { designTemplates: [exampleSummary] } }); }); } if (entry.flow === 'hyperframes-project-routing') { await page.route('**/api/skills', async (route) => { await route.fulfill({ json: { skills: [ { id: 'video-shortform', name: 'Video shortform', description: 'Shortform video skill', mode: 'video', surface: 'video', previewType: 'video', designSystemRequired: false, defaultFor: [], triggers: [], upstream: null, hasBody: true, examplePrompt: '', aggregatesExamples: false, }, { id: 'hyperframes', name: 'HyperFrames', description: 'HTML-in-canvas video', mode: 'video', surface: 'video', previewType: 'video', designSystemRequired: false, defaultFor: [], triggers: [], upstream: null, hasBody: true, examplePrompt: '', aggregatesExamples: false, }, ], }, }); }); } if (entry.mockArtifact) { await page.route('**/api/runs', async (route) => { await route.fulfill({ status: 202, contentType: 'application/json', body: '{"runId":"mock-run"}' }); }); await page.route('**/api/runs/*/events', async (route) => { const artifact = `` + entry.mockArtifact!.html + ''; const body = [ 'event: start', 'data: {"bin":"mock-agent"}', '', 'event: stdout', `data: ${JSON.stringify({ chunk: artifact })}`, '', 'event: end', 'data: {"code":0}', '', '', ].join('\n'); await route.fulfill({ status: 200, headers: { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', }, body, }); }); } if (entry.flow === 'question-form-selection-limit') { await page.route('**/api/runs', async (route) => { await route.fulfill({ status: 202, contentType: 'application/json', body: '{"runId":"mock-run"}' }); }); await page.route('**/api/runs/*/events', async (route) => { const form = [ '', JSON.stringify( { description: "I'll lock these in before building.", questions: [ { id: 'tone', label: 'Visual tone (pick up to two)', type: 'checkbox', maxSelections: 2, options: ['Editorial / magazine', 'Modern minimal', 'Soft / warm'], required: true, }, ], }, null, 2, ), '', ].join('\n'); const body = [ 'event: start', 'data: {"bin":"mock-agent"}', '', 'event: stdout', `data: ${JSON.stringify({ chunk: form })}`, '', 'event: end', 'data: {"code":0}', '', '', ].join('\n'); await route.fulfill({ status: 200, headers: { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', }, body, }); }); } if (entry.flow === 'question-form-submit-persistence') { let requestCount = 0; await page.route('**/api/runs', async (route) => { await route.fulfill({ status: 202, contentType: 'application/json', body: '{"runId":"mock-run"}' }); }); await page.route('**/api/runs/*/events', async (route) => { requestCount += 1; const chunk = requestCount === 1 ? [ '', JSON.stringify( { description: "I'll lock these in before building.", questions: [ { id: 'tone', label: 'Visual tone (pick up to two)', type: 'checkbox', maxSelections: 2, options: ['Editorial / magazine', 'Modern minimal', 'Soft / warm'], required: true, }, ], }, null, 2, ), '', ].join('\n') : 'Thanks — I will use these answers for the next draft.'; const body = [ 'event: start', 'data: {"bin":"mock-agent"}', '', 'event: stdout', `data: ${JSON.stringify({ chunk })}`, '', 'event: end', 'data: {"code":0,"status":"succeeded"}', '', '', ].join('\n'); await route.fulfill({ status: 200, headers: { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', }, body, }); }); } if (entry.flow === 'file-mention') { await routeMockSuccessfulRun(page, 'file-mention-run'); } await gotoEntryHome(page); if (entry.flow === 'example-use-prompt') { await runExampleUsePromptFlow(page, entry); return; } if (entry.flow === 'hyperframes-project-routing') { await runHyperframesProjectRoutingFlow(page, entry); return; } if (entry.flow === 'image-project-routing') { await runImageProjectRoutingFlow(page, entry); return; } if (entry.flow === 'video-project-routing') { await runVideoProjectRoutingFlow(page, entry); return; } if (entry.flow === 'audio-project-routing') { await runAudioProjectRoutingFlow(page, entry); return; } if (entry.flow === 'live-artifact-project-routing') { await runLiveArtifactProjectRoutingFlow(page, entry); return; } await createProject(page, entry); await expectWorkspaceReady(page); if (entry.flow === 'conversation-persistence') { await runConversationPersistenceFlow(page, entry); return; } if (entry.flow === 'file-mention') { await runFileMentionFlow(page, entry); return; } if (entry.flow === 'deep-link-preview') { await runDeepLinkPreviewFlow(page, entry); return; } if (entry.flow === 'file-upload-send') { await runFileUploadSendFlow(page, entry); return; } if (entry.flow === 'conversation-delete-recovery') { await runConversationDeleteRecoveryFlow(page, entry); return; } if (entry.flow === 'question-form-selection-limit') { await runQuestionFormSelectionLimitFlow(page, entry); return; } if (entry.flow === 'question-form-submit-persistence') { await runQuestionFormSubmitPersistenceFlow(page, entry); return; } if (entry.flow === 'generation-does-not-create-extra-file') { await runGenerationDoesNotCreateExtraFileFlow(page, entry); return; } if (entry.flow === 'comment-attachment-flow') { await runCommentAttachmentFlow(page, entry); return; } if (entry.flow === 'deck-pagination-next-prev-correctness') { await runDeckPaginationNextPrevCorrectnessFlow(page); return; } if (entry.flow === 'deck-pagination-per-file-isolated') { await runDeckPaginationPerFileIsolatedFlow(page); return; } await sendPrompt(page, entry.prompt); if (entry.mockArtifact) { await expectArtifactVisible(page, entry); } const { projectId } = await getCurrentProjectContext(page); await expectScenarioProjectState(page, entry, projectId); }); } test('[P0] @critical comment attachment flow attaches preview comments to the next run as structured context', async ({ page }) => { test.setTimeout(75_000); const entry = automatedUiScenarios().find((scenario) => scenario.id === 'comment-attachment-flow'); if (!entry?.mockArtifact) { throw new Error('comment-attachment-flow scenario fixture is missing'); } await routeMockAgents(page); await page.route('**/api/runs', async (route) => { await route.fulfill({ status: 202, contentType: 'application/json', body: JSON.stringify({ runId: 'comment-attachment-run' }), }); }); await page.route('**/api/runs/*/events', async (route) => { const body = [ 'event: start', 'data: {"bin":"mock-agent"}', '', 'event: end', 'data: {"code":0,"status":"succeeded"}', '', '', ].join('\n'); await route.fulfill({ status: 200, headers: { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', }, body, }); }); const projectId = await createEmptyProject(page, 'Comment attachment flow'); await expectWorkspaceReady(page); await seedHtmlArtifact(page, projectId, entry.mockArtifact.fileName, entry.mockArtifact.html); await page.reload(); await expectWorkspaceReady(page); await page.goto(`/projects/${projectId}/files/${entry.mockArtifact.fileName}`, { waitUntil: 'domcontentloaded' }); await waitForLoadingToClear(page); await expect(artifactPreview(page)).toBeVisible(); await runCommentAttachmentFlow(page, entry); }); test('[P0] sending preview comments opens the refreshed follow-up artifact', async ({ page }) => { test.setTimeout(75_000); const entry = automatedUiScenarios().find((scenario) => scenario.id === 'comment-attachment-flow'); if (!entry?.mockArtifact) { throw new Error('comment-attachment-flow scenario fixture is missing'); } const revisedHtml = '
' + '

Revised headline

' + '

Preview copy refreshed after comment send.

' + '
'; await routeMockAgents(page); let requestCount = 0; await page.route('**/api/runs', async (route) => { requestCount += 1; await route.fulfill({ status: 202, contentType: 'application/json', body: JSON.stringify({ runId: `mock-run-${requestCount}` }), }); }); await page.route('**/api/runs/*/events', async (route) => { const artifactTitle = entry.mockArtifact!.title; const artifactHtml = revisedHtml; const body = [ 'event: start', 'data: {"bin":"mock-agent"}', '', 'event: stdout', `data: ${JSON.stringify({ chunk: `` + artifactHtml + '', })}`, '', 'event: end', 'data: {"code":0,"status":"succeeded"}', '', '', ].join('\n'); await route.fulfill({ status: 200, headers: { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', }, body, }); }); const projectId = await createEmptyProject(page, 'Comment preview follow-up'); await expectWorkspaceReady(page); await seedHtmlArtifact(page, projectId, entry.mockArtifact.fileName, entry.mockArtifact.html); await page.reload(); await expectWorkspaceReady(page); await page.goto(`/projects/${projectId}/files/${entry.mockArtifact.fileName}`, { waitUntil: 'domcontentloaded' }); await waitForLoadingToClear(page); await expect(artifactPreview(page)).toBeVisible(); await page.getByTestId('board-mode-toggle').click(); await page.getByTestId('comment-panel-toggle').click(); const frame = artifactPreviewFrame(page); await frame.locator('[data-od-id="hero-title"]').click(); await expect(page.getByTestId('comment-popover')).toBeVisible(); await page.getByTestId('comment-popover-input').fill('Make the headline more specific.'); await page.getByTestId('comment-popover-save').click(); await expect(page.getByTestId('comment-saved-marker-hero-title')).toBeVisible(); const sidePanel = page.getByTestId('comment-side-panel'); await expect(sidePanel).toBeVisible(); await expect(sidePanel.getByTestId('comment-side-item').filter({ hasText: 'Make the headline more specific.' }).first()).toBeVisible(); await expect .poll(async () => { const selectAll = sidePanel.getByRole('button', { name: /select all/i }).first(); if ((await selectAll.count()) === 0) return false; await selectAll.evaluate((element: HTMLButtonElement) => element.click()); return (await page.getByTestId('comment-side-send-claude').count()) > 0; }) .toBe(true); await expect(page.getByTestId('comment-side-send-claude')).toBeVisible(); const runRequest = page.waitForRequest(isCreateRunRequest); const runEvents = page.waitForResponse((response) => { const url = new URL(response.url()); return url.pathname.startsWith('/api/runs/') && url.pathname.endsWith('/events'); }); await page.getByTestId('comment-side-send-claude').click(); const body = (await runRequest).postDataJSON() as { message?: string; commentAttachments?: Array<{ elementId?: string; comment?: string; commentContext?: string; filePath?: string; }>; }; expect(body.message).toContain('Make the headline more specific.'); expect(body.commentAttachments).toEqual([ expect.objectContaining({ elementId: 'hero-title', comment: '', commentContext: 'query', filePath: 'commentable-artifact.html', }), ]); await runEvents; const revisedFileName = await findProjectFileContaining(page, projectId, 'Revised headline'); expect(revisedFileName).not.toBe(''); await page.goto(`/projects/${projectId}/files/${revisedFileName}`, { waitUntil: 'domcontentloaded' }); await waitForLoadingToClear(page); const escapedRevisedFileName = revisedFileName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); await expect(page).toHaveURL( new RegExp(`/projects/${projectId}(?:/conversations/[^/]+)?/files/${escapedRevisedFileName}$`), ); await expect(artifactPreview(page)).toBeVisible(); await expectProjectFileToContain(page, projectId, revisedFileName, 'Revised headline'); await expectProjectFileToContain(page, projectId, revisedFileName, 'Preview copy refreshed after comment send.'); }); async function routeMockAgents(page: Page) { await routeAgents(page, [ { id: 'mock', name: 'Mock Agent', bin: 'mock-agent', available: true, version: 'test', models: [{ id: 'default', label: 'Default' }], }, ]); } function scenarioPriority(entry: UiScenario): 'P0' | 'P1' | 'P2' { switch (entry.flow) { case 'example-use-prompt': case 'hyperframes-project-routing': case 'image-project-routing': case 'video-project-routing': case 'audio-project-routing': case 'live-artifact-project-routing': case 'conversation-persistence': case 'file-upload-send': case 'conversation-delete-recovery': case 'comment-attachment-flow': return 'P0'; case 'deep-link-preview': case 'question-form-submit-persistence': case 'generation-does-not-create-extra-file': case 'file-mention': case 'deck-pagination-next-prev-correctness': case 'deck-pagination-per-file-isolated': return 'P1'; case 'question-form-selection-limit': return 'P2'; default: return 'P1'; } } function criticalScenarioTag(entry: UiScenario): string { return CRITICAL_SCENARIO_IDS.has(entry.id) ? ' @critical' : ''; } async function routeMockSuccessfulRun(page: Page, runId: string) { await page.route('**/api/runs', async (route) => { await route.fulfill({ status: 202, contentType: 'application/json', body: JSON.stringify({ runId }), }); }); await page.route('**/api/runs/*/events', async (route) => { const body = [ 'event: start', 'data: {"bin":"mock-agent"}', '', 'event: stdout', 'data: {"chunk":"Plugin flow completed."}', '', 'event: end', 'data: {"code":0,"status":"succeeded"}', '', '', ].join('\n'); await route.fulfill({ status: 200, headers: { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', }, body, }); }); } async function createEmptyProject(page: Page, name: string): Promise { await gotoEntryHome(page); await openNewProjectModal(page); await page.getByTestId('new-project-name').fill(name); await page.getByTestId('create-project').click(); await expect(page).toHaveURL(/\/projects\//); const current = new URL(page.url()); const [, projects, projectId] = current.pathname.split('/'); if (projects !== 'projects' || !projectId) throw new Error(`unexpected project route: ${current.pathname}`); return projectId; } async function seedHtmlArtifact( page: Page, projectId: string, fileName: string, content: string, ) { const resp = await page.request.post(`/api/projects/${projectId}/files`, { data: { name: fileName, content, artifactManifest: { version: 1, kind: 'html', title: fileName, entry: fileName, renderer: 'html', exports: ['html'], }, }, }); expect(resp.ok()).toBeTruthy(); } async function openDesignFile(page: Page, fileName: string) { await openAllProjectFiles(page); const fileRow = page.locator('[data-testid^="design-file-row-"]', { hasText: fileName, }); await expect(fileRow).toBeVisible(); const mainButton = fileRow.getByRole('button').first(); await mainButton.click(); const openButton = page.getByTestId('design-file-preview').getByRole('button', { name: 'Open' }); if (await openButton.isVisible().catch(() => false)) { await openButton.click(); return; } await mainButton.dblclick(); } async function expectFileSource( page: Page, projectId: string, fileName: string, snippets: string[], ) { await expect .poll(async () => { const resp = await page.request.get(`/api/projects/${projectId}/files/${fileName}`); if (!resp.ok()) return false; const source = await resp.text(); return snippets.every((snippet) => source.includes(snippet)); }) .toBe(true); } function manualEditHtml(): string { return ` Manual Edit

Original Hero

Start now Hero
`; } function deckHtml(): string { return `

Slide One

`; } async function createProject( page: Page, entry: UiScenario, ) { await createProjectNameOnly(page, entry); await page.getByTestId('create-project').click(); } async function expectWorkspaceReady(page: Page) { await waitForLoadingToClear(page); await expect(page).toHaveURL(/\/projects\//); await expect(page.getByTestId('chat-composer')).toBeVisible(); await expect(page.getByTestId('chat-composer-input')).toBeVisible(); await expect(page.locator('.chat-loading-state')).toHaveCount(0, { timeout: T.medium }); await expect(page.getByTestId('file-workspace')).toBeVisible(); } async function expectProjectShellReady(page: Page) { await waitForLoadingToClear(page); await expect(page).toHaveURL(/\/projects\//); await expect(page.getByTestId('chat-composer')).toBeVisible(); await expect(page.getByTestId('file-workspace')).toBeVisible(); } async function sendPrompt(page: Page, prompt: string) { const input = page.getByTestId('chat-composer-input'); const sendButton = page.getByTestId('chat-send'); await expect(input).toBeVisible({ timeout: T.short }); await input.click(); await input.fill(prompt); await expect(input).toHaveText(prompt, { timeout: T.short }); await expect(sendButton).toBeEnabled({ timeout: T.medium }); await Promise.all([ page.waitForResponse(isCreateRunResponse, { timeout: 5_000 }), sendButton.evaluate((button: HTMLButtonElement) => button.click()), ]); } async function startNewConversation(page: Page) { await page.getByTestId('conversation-history-trigger').click(); await expect(page.getByTestId('conversation-list')).toBeVisible(); await page.getByTestId('conversation-history-new').click(); await expect(page.getByTestId('conversation-list')).toHaveCount(0); } function tabBySuffix(page: Page, name: string): Locator { return page.getByRole('tab', { name: new RegExp(`${escapeRegExp(name)}(?:\\s+Close tab)?$`, 'i') }); } function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function isCreateRunResponse(resp: Response): boolean { const url = new URL(resp.url()); return url.pathname === '/api/runs' && resp.request().method() === 'POST'; } function isCreateProjectResponse(resp: Response): boolean { const url = new URL(resp.url()); return url.pathname === '/api/projects' && resp.request().method() === 'POST'; } function isCreateRunRequest(request: Request): boolean { const url = new URL(request.url()); return url.pathname === '/api/runs' && request.method() === 'POST'; } function isCreateProjectRequest(request: Request): boolean { const url = new URL(request.url()); return url.pathname === '/api/projects' && request.method() === 'POST'; } async function runExampleUsePromptFlow( page: Page, entry: UiScenario, ) { const exampleCard = page.getByTestId('example-card-warm-utility-example'); if ((await exampleCard.count()) === 0) { const examplesTab = page.getByTestId('entry-tab-examples'); if ((await examplesTab.count()) > 0) { await examplesTab.click(); } } await expect(exampleCard).toBeVisible(); await page.getByTestId('example-use-prompt-warm-utility-example').click(); await expect(page).toHaveURL(/\/projects\//); await expect(page.getByTestId('chat-composer')).toBeVisible(); await expect(page.getByTestId('chat-composer-input')).toHaveText(entry.prompt); await expect(page.getByTestId('project-title')).toContainText('Warm Utility Example'); await expect(page.getByTestId('project-meta')).toContainText('Warm Utility Example'); } async function runHyperframesProjectRoutingFlow( page: Page, entry: UiScenario, ) { await createProjectNameOnly(page, entry); const createProjectRequest = page.waitForRequest(isCreateProjectRequest); const createProjectResponse = page.waitForResponse(isCreateProjectResponse); await page.getByTestId('create-project').click(); const request = await createProjectRequest; const body = request.postDataJSON() as { skillId?: string; metadata?: { kind?: string; videoModel?: string; }; }; expect(body.skillId).toBe('hyperframes'); expect(body.metadata?.kind).toBe('video'); expect(body.metadata?.videoModel).toBe('hyperframes-html'); const response = await createProjectResponse; expect(response.ok(), `${response.status()} ${await response.text()}`).toBeTruthy(); await expectWorkspaceReady(page); await sendPrompt(page, entry.prompt); const { projectId } = await getCurrentProjectContext(page); await expect(tabBySuffix(page, entry.mockArtifact!.fileName)).toBeVisible(); await expectProjectFileToContain(page, projectId, entry.mockArtifact!.fileName, entry.mockArtifact!.heading); await expectScenarioProjectState(page, entry, projectId); } async function runImageProjectRoutingFlow( page: Page, entry: UiScenario, ) { await createProjectNameOnly(page, entry); const createProjectRequest = page.waitForRequest(isCreateProjectRequest); const createProjectResponse = page.waitForResponse(isCreateProjectResponse); await page.getByTestId('create-project').click(); const request = await createProjectRequest; const body = request.postDataJSON() as { metadata?: { kind?: string; }; }; expect(body.metadata?.kind).toBe('image'); const response = await createProjectResponse; expect(response.ok(), `${response.status()} ${await response.text()}`).toBeTruthy(); await expectWorkspaceReady(page); const { projectId } = await getCurrentProjectContext(page); await expectScenarioProjectState(page, entry, projectId); } async function runVideoProjectRoutingFlow( page: Page, entry: UiScenario, ) { await createProjectNameOnly(page, entry); const createProjectRequest = page.waitForRequest(isCreateProjectRequest); const createProjectResponse = page.waitForResponse(isCreateProjectResponse); await page.getByTestId('create-project').click(); const request = await createProjectRequest; const body = request.postDataJSON() as { metadata?: { kind?: string; videoModel?: string; videoAspect?: string; videoLength?: number; }; }; expect(body.metadata?.kind).toBe('video'); expect(body.metadata?.videoModel).toBeTruthy(); expect(body.metadata?.videoAspect).toBe('16:9'); expect(body.metadata?.videoLength).toBe(5); const response = await createProjectResponse; expect(response.ok()).toBeTruthy(); await expectWorkspaceReady(page); const { projectId } = await getCurrentProjectContext(page); await expectScenarioProjectState(page, entry, projectId); } async function runAudioProjectRoutingFlow( page: Page, entry: UiScenario, ) { await createProjectNameOnly(page, entry); const createProjectRequest = page.waitForRequest(isCreateProjectRequest); const createProjectResponse = page.waitForResponse(isCreateProjectResponse); await page.getByTestId('create-project').click(); const request = await createProjectRequest; const body = request.postDataJSON() as { metadata?: { kind?: string; audioKind?: string; audioDuration?: number; }; }; expect(body.metadata?.kind).toBe('audio'); expect(body.metadata?.audioKind).toBe('sfx'); expect(typeof body.metadata?.audioDuration).toBe('number'); expect((body.metadata?.audioDuration ?? 0)).toBeGreaterThan(0); const response = await createProjectResponse; expect(response.ok()).toBeTruthy(); await expectWorkspaceReady(page); const { projectId } = await getCurrentProjectContext(page); const project = await fetchProjectFromApi(page, projectId); await expectScenarioProjectState(page, entry, projectId); const metadata = project.metadata as Record | undefined; expect(metadata?.audioDuration).toBe(body.metadata?.audioDuration); } async function runLiveArtifactProjectRoutingFlow( page: Page, entry: UiScenario, ) { await createProjectNameOnly(page, entry); const createProjectRequest = page.waitForRequest(isCreateProjectRequest); const createProjectResponse = page.waitForResponse(isCreateProjectResponse); await page.getByTestId('create-project').click(); const request = await createProjectRequest; const body = request.postDataJSON() as { metadata?: { kind?: string; intent?: string; fidelity?: string; }; }; expect(body.metadata?.kind).toBe('prototype'); expect(body.metadata?.intent).toBe('live-artifact'); expect(body.metadata?.fidelity).toBe('high-fidelity'); const response = await createProjectResponse; expect(response.ok()).toBeTruthy(); await expectWorkspaceReady(page); const { projectId } = await getCurrentProjectContext(page); await expectScenarioProjectState(page, entry, projectId); } async function runQuestionFormSelectionLimitFlow( page: Page, entry: UiScenario, ) { await sendPrompt(page, entry.prompt); const toneQuestion = page.locator('.qf-field', { has: page.getByText('Visual tone (pick up to two)'), }); await expect(toneQuestion).toBeVisible(); const editorialChip = toneQuestion.locator('label.qf-chip', { has: page.getByText('Editorial / magazine'), }); const modernChip = toneQuestion.locator('label.qf-chip', { has: page.getByText('Modern minimal'), }); const softChip = toneQuestion.locator('label.qf-chip', { has: page.getByText('Soft / warm'), }); const editorial = editorialChip.locator('input[type="checkbox"]'); const modern = modernChip.locator('input[type="checkbox"]'); const soft = softChip.locator('input[type="checkbox"]'); await editorialChip.click(); await modernChip.click(); await expect(editorial).toBeChecked(); await expect(modern).toBeChecked(); await expect(soft).toBeDisabled(); const checkedOptions = toneQuestion.locator('input[type="checkbox"]:checked'); await expect(checkedOptions).toHaveCount(2); await expect(soft).not.toBeChecked(); await expect(checkedOptions).toHaveCount(2); } async function runQuestionFormSubmitPersistenceFlow( page: Page, entry: UiScenario, ) { const firstRunRequestPromise = page.waitForRequest(isCreateRunRequest); await sendPrompt(page, entry.prompt); const firstRunBody = (await firstRunRequestPromise).postDataJSON() as Record; expectScenarioRunRequest(firstRunBody, entry); const form = page.locator('.question-form').first(); await expect(form).toBeVisible(); const toneQuestion = form.locator('.qf-field', { has: page.getByText('Visual tone (pick up to two)'), }); await toneQuestion.locator('label.qf-chip', { has: page.getByText('Editorial / magazine') }).click(); await toneQuestion.locator('label.qf-chip', { has: page.getByText('Modern minimal') }).click(); await form.getByRole('button', { name: 'Send answers' }).click(); await expect(page.getByText('[form answers — discovery]', { exact: false })).toBeVisible(); await expect(form.getByText('answered', { exact: true })).toBeVisible(); await expect(form.getByText('Answers sent — agent is using these for the rest of the session.')).toBeVisible(); const { projectId, conversationId } = await getCurrentProjectContext(page); const messagesResponse = await page.request.get( `/api/projects/${projectId}/conversations/${conversationId}/messages`, ); expect(messagesResponse.ok()).toBeTruthy(); const { messages } = (await messagesResponse.json()) as { messages: Array<{ role: string; content: string }> }; const formAnswerMessage = messages.find((message) => message.role === 'user' && message.content.includes('[form answers — discovery]')); expect(formAnswerMessage).toBeTruthy(); await page.reload(); const restoredForm = page.locator('.question-form').first(); await expect(restoredForm).toBeVisible(); await expect(restoredForm.getByText('answered', { exact: true })).toBeVisible(); await expect(restoredForm.locator('input[type="checkbox"]:checked')).toHaveCount(2); await expect(restoredForm.getByRole('button', { name: 'Send answers' })).toHaveCount(0); } async function runGenerationDoesNotCreateExtraFileFlow( page: Page, entry: UiScenario, ) { await sendPrompt(page, entry.prompt); await expectArtifactVisible(page, entry); const { projectId } = await getCurrentProjectContext(page); const initialFiles = await listProjectFilesFromApi(page, projectId); expect(initialFiles.map((file) => file.name)).toContain(entry.mockArtifact!.fileName); await page.reload(); await expect(page.getByTestId('file-workspace')).toBeVisible(); const reloadedFiles = await listProjectFilesFromApi(page, projectId); expect(reloadedFiles.map((file) => file.name)).toEqual(initialFiles.map((file) => file.name)); await expect(page.getByText(entry.mockArtifact!.fileName, { exact: true }).first()).toBeVisible(); await expectScenarioProjectState(page, entry, projectId); } async function runCommentAttachmentFlow( page: Page, entry: UiScenario, ) { await page.getByTestId('board-mode-toggle').click(); await page.getByTestId('comment-panel-toggle').click(); const frame = artifactPreviewFrame(page); await frame.locator('[data-od-id="hero-title"]').click(); await expect(page.getByTestId('comment-popover')).toBeVisible(); await page.getByTestId('comment-popover-input').fill('Make the headline more specific.'); await page.getByTestId('comment-popover-save').click(); await expect(page.getByTestId('comment-saved-marker-hero-title')).toBeVisible(); await expect(page.getByTestId('staged-comment-attachments')).toHaveCount(0); await expect(page.getByTestId('comment-popover')).toHaveCount(0); const sidePanel = page.getByTestId('comment-side-panel'); await expect(sidePanel).toBeVisible(); await expect(sidePanel).toContainText('Make the headline more specific.'); await expect(sidePanel.getByTestId('comment-side-item').filter({ hasText: 'Make the headline more specific.' }).first()).toBeVisible(); await expect .poll(async () => { const selectAll = sidePanel.getByRole('button', { name: /select all/i }).first(); if ((await selectAll.count()) === 0) return false; await selectAll.evaluate((element: HTMLButtonElement) => element.click()); return (await page.getByTestId('comment-side-send-claude').count()) > 0; }) .toBe(true); await expect(page.getByTestId('comment-side-send-claude')).toBeVisible(); const runRequest = page.waitForRequest( isCreateRunRequest, ); await page.getByTestId('comment-side-send-claude').click(); const request = await runRequest; const body = request.postDataJSON() as { message?: string; commentAttachments?: Array<{ elementId?: string; comment?: string; commentContext?: string; filePath?: string; }>; }; expect(body.message ?? '').not.toContain('Apply selected preview comments'); expect(body.message).toContain('Make the headline more specific.'); expect(body.commentAttachments).toEqual([ expect.objectContaining({ elementId: 'hero-title', comment: '', commentContext: 'query', filePath: 'commentable-artifact.html', }), ]); } async function runDeckPaginationNextPrevCorrectnessFlow(page: Page) { const { projectId } = await getCurrentProjectContext(page); await seedDeckArtifact(page, projectId, 'pagination.html', 'Pagination Deck', ['Slide One', 'Slide Two', 'Slide Three']); await page.reload(); await openDesignFile(page, 'pagination.html'); const frame = artifactPreviewFrame(page); await expect(frame.getByText('Slide One')).toBeVisible(); await clickDeckNextSlide(page); await expect(frame.getByText('Slide Two')).toBeVisible(); await clickDeckNextSlide(page); await expect(frame.getByText('Slide Three')).toBeVisible(); await clickDeckPreviousSlide(page); await expect(frame.getByText('Slide Two')).toBeVisible(); } async function runDeckPaginationPerFileIsolatedFlow(page: Page) { const { projectId } = await getCurrentProjectContext(page); await seedDeckArtifact(page, projectId, 'deck-alpha.html', 'Deck Alpha', ['Alpha One', 'Alpha Two']); await seedDeckArtifact(page, projectId, 'deck-beta.html', 'Deck Beta', ['Beta One', 'Beta Two']); await page.reload(); await openDesignFile(page, 'deck-alpha.html'); const frame = artifactPreviewFrame(page); await expect(frame.getByText('Alpha One')).toBeVisible(); await clickDeckNextSlide(page); await expect(frame.getByText('Alpha Two')).toBeVisible(); await openAllProjectFiles(page); await openDesignFile(page, 'deck-beta.html'); await expect(frame.getByText('Beta One')).toBeVisible(); await clickDeckNextSlide(page); await expect(frame.getByText('Beta Two')).toBeVisible(); await page.getByRole('tab', { name: /deck-alpha\.html/i }).click(); await expect(frame.getByText('Alpha Two')).toBeVisible(); await page.getByRole('tab', { name: /deck-beta\.html/i }).click(); await expect(frame.getByText('Beta Two')).toBeVisible(); } async function seedDeckArtifact( page: Page, projectId: string, fileName: string, title: string, slides: string[], ) { const slideHtml = slides .map((slide, index) => `

${slide}

`) .join('\n'); await seedProjectFile( page, projectId, fileName, `${slideHtml}`, undefined, { version: 1, kind: 'deck', title, entry: fileName, renderer: 'deck-html', exports: ['html', 'pdf'], }, ); } async function seedProjectFile( page: Page, projectId: string, name: string, content: string, encoding?: 'base64', artifactManifest?: Record, ) { const response = await page.request.post(`/api/projects/${projectId}/files`, { data: { name, content, ...(encoding ? { encoding } : {}), ...(artifactManifest ? { artifactManifest } : {}), }, }); expect(response.ok()).toBeTruthy(); } async function createProjectNameOnly( page: Page, entry: UiScenario, ) { await openNewProjectModal(page); await expect(page.getByTestId('new-project-panel')).toBeVisible(); if (entry.create.tab) { await clickVisible(page.getByTestId(`new-project-tab-${entry.create.tab}`)); await expect(page.getByTestId(`new-project-tab-${entry.create.tab}`)).toHaveAttribute('aria-selected', 'true'); } if (entry.create.tab === 'media' && entry.create.mediaSurface) { await clickVisible(page.getByTestId(`new-project-media-surface-${entry.create.mediaSurface}`)); await expect(page.getByTestId(`new-project-media-surface-${entry.create.mediaSurface}`)).toHaveAttribute('aria-selected', 'true'); } if (entry.create.tab === 'media' && entry.create.mediaSurface === 'video' && entry.create.videoModel) { await page.getByTestId('model-picker-trigger').click(); await page.getByTestId(`model-picker-option-${entry.create.videoModel}`).click(); } if (entry.create.tab === 'media' && entry.create.mediaSurface === 'audio' && entry.create.audioKind === 'sfx') { await page.getByRole('button', { name: 'SFX' }).click(); } await page.getByTestId('new-project-name').fill(entry.create.projectName); } async function clickVisible(locator: Locator) { await expect(locator).toBeVisible({ timeout: T.medium }); await locator.evaluate((element: HTMLElement) => element.click()); } async function gotoEntryHome(page: Page) { await page.goto('/', { waitUntil: 'domcontentloaded' }); await waitForLoadingToClear(page); const privacyDialog = page.getByRole('dialog').filter({ hasText: 'Help us improve Open Design' }); if (await privacyDialog.isVisible()) { await privacyDialog.getByRole('button', { name: /I get it|not now|got it|don't share/i }).click(); await expect(privacyDialog).toHaveCount(0); } await expect(page.getByTestId('home-hero')).toBeVisible(); await expect(page.getByTestId('home-hero-input')).toBeVisible(); } async function openNewProjectModal(page: Page) { await openNewProjectModalFromProjects(page); } async function waitForLoadingToClear(page: Page) { await page.getByText('Loading Open Design…').waitFor({ state: 'hidden', timeout: T.long }); } async function getCurrentProjectContext( page: Page, ): Promise<{ projectId: string; conversationId: string }> { const current = new URL(page.url()); const [, projects, projectId, maybeConversations, conversationId] = current.pathname.split('/'); if (projects !== 'projects' || !projectId) { throw new Error(`unexpected project route: ${current.pathname}`); } if (maybeConversations === 'conversations' && conversationId) { return { projectId, conversationId }; } const response = await page.request.get(`/api/projects/${projectId}/conversations`); expect(response.ok()).toBeTruthy(); const { conversations } = (await response.json()) as { conversations: Array<{ id: string; updatedAt: number }>; }; const active = [...conversations].sort((a, b) => b.updatedAt - a.updatedAt)[0]; if (!active) throw new Error(`no conversations found for project ${projectId}`); return { projectId, conversationId: active.id }; } async function fetchProjectFromApi( page: Page, projectId: string, ): Promise<{ metadata?: { kind?: string }; appliedPluginSnapshotId?: string; }> { const response = await page.request.get(`/api/projects/${projectId}`); expect(response.ok()).toBeTruthy(); const { project } = (await response.json()) as { project: { metadata?: { kind?: string }; appliedPluginSnapshotId?: string; }; }; return project; } async function listProjectFilesFromApi( page: Page, projectId: string, ): Promise> { const response = await page.request.get(`/api/projects/${projectId}/files`); expect(response.ok()).toBeTruthy(); const { files } = (await response.json()) as { files: Array<{ name: string; kind: string }> }; return files; } async function expectScenarioProjectState( page: Page, entry: UiScenario, projectId: string, ) { await expectScenarioProjectMetadata(page, entry, projectId); await expectScenarioFiles(page, entry, projectId); await expectScenarioPreviewText(page, entry); } async function expectScenarioProjectMetadata( page: Page, entry: UiScenario, projectId: string, ) { if (!entry.expectedProjectMetadata) return; const project = await fetchProjectFromApi(page, projectId); const metadata = project.metadata as Record | undefined; expect(metadata).toBeDefined(); expectObjectContaining(metadata ?? {}, entry.expectedProjectMetadata); } async function expectScenarioFiles( page: Page, entry: UiScenario, projectId: string, ) { if (!entry.expectedFiles?.length) return; const files = await listProjectFilesFromApi(page, projectId); for (const expectedFile of entry.expectedFiles) { const actual = files.find((file) => file.name === expectedFile.name); expect(actual, `missing expected file ${expectedFile.name}`).toBeDefined(); if (expectedFile.kind) { expect(actual?.kind).toBe(expectedFile.kind); } if (expectedFile.previewText) { await expectProjectFileToContain(page, projectId, expectedFile.name, expectedFile.previewText); } } } async function expectScenarioPreviewText( page: Page, entry: UiScenario, ) { if (!entry.expectedPreviewText) return; if ((await artifactPreview(page).count()) === 0) return; const frame = artifactPreviewFrame(page); await expect(frame.getByText(entry.expectedPreviewText, { exact: false })).toBeVisible(); } function expectScenarioRunRequest( requestBody: Record, entry: UiScenario, ) { if (!entry.expectedRunRequest) return; const normalizedActual = { ...requestBody, attachments: Array.isArray(requestBody.attachments) ? requestBody.attachments : [], }; expectObjectContaining(normalizedActual, entry.expectedRunRequest); } function expectObjectContaining( actual: Record, expected: Record, ) { for (const [key, value] of Object.entries(expected)) { const actualValue = actual[key]; if (Array.isArray(value)) { expect(actualValue).toEqual(expect.arrayContaining(value)); continue; } if (value && typeof value === 'object') { expect(actualValue).toBeTruthy(); expectObjectContaining(actualValue as Record, value as Record); continue; } if (typeof value === 'string' && typeof actualValue === 'string') { expect(actualValue).toContain(value); continue; } expect(actualValue).toBe(value); } } async function expectProjectFileToContain( page: Page, projectId: string, fileName: string, expected: string, ) { await expect .poll(async () => { const response = await page.request.get(`/api/projects/${projectId}/files/${fileName}`); if (!response.ok()) return ''; return response.text(); }, { timeout: 15_000 }) .toContain(expected); } async function findProjectFileContaining( page: Page, projectId: string, expected: string, ): Promise { const deadline = Date.now() + 15_000; while (Date.now() < deadline) { const files = await listProjectFilesFromApi(page, projectId); for (const file of files) { const response = await page.request.get(`/api/projects/${projectId}/files/${file.name}`); if (!response.ok()) continue; const source = await response.text(); if (source.includes(expected)) return file.name; } await page.waitForTimeout(250); } return ''; } async function expectArtifactVisible( page: Page, entry: UiScenario, ) { const artifact = entry.mockArtifact!; await expect(tabBySuffix(page, artifact.fileName)).toBeVisible(); if ((await artifactPreview(page).count()) === 0) { const turnCard = page.locator('.msg.assistant').filter({ hasText: artifact.fileName }).last(); if ((await turnCard.count()) > 0) { const openButton = turnCard.getByRole('button', { name: 'Open', exact: true }); if ((await openButton.count()) > 0) { await openButton.click(); } } } if ((await artifactPreview(page).count()) === 0) { const { projectId } = await getCurrentProjectContext(page); await expectProjectFileToContain(page, projectId, artifact.fileName, artifact.heading); return; } await expect(artifactPreview(page)).toBeVisible(); if (entry.kind === 'deck') { await expect(page.getByLabel('Previous slide')).toBeVisible(); await expect(page.getByLabel('Next slide')).toBeVisible(); const { projectId } = await getCurrentProjectContext(page); await expectProjectFileToContain(page, projectId, artifact.fileName, artifact.heading); return; } const frame = artifactPreviewFrame(page); await expect(frame.getByRole('heading', { name: artifact.heading })).toBeVisible(); } async function runConversationPersistenceFlow( page: Page, entry: UiScenario, ) { await sendPrompt(page, entry.prompt); await expect(page.locator('.msg.user').getByText(entry.prompt, { exact: true })).toBeVisible(); const firstContext = await getCurrentProjectContext(page); await expect(tabBySuffix(page, entry.mockArtifact!.fileName)).toBeVisible(); await expectProjectFileToContain(page, firstContext.projectId, entry.mockArtifact!.fileName, entry.mockArtifact!.heading); const firstConversationId = firstContext.conversationId; await startNewConversation(page); await expect(page.getByTestId('chat-composer-input')).toBeVisible(); await expect(page.getByTestId('chat-composer-input')).toHaveText(''); const nextPrompt = entry.secondaryPrompt!; await sendPrompt(page, nextPrompt); await expect(page.locator('.msg.user').getByText(nextPrompt, { exact: true })).toBeVisible(); const secondContext = await getCurrentProjectContext(page); const secondConversationId = secondContext.conversationId; expect(secondConversationId).not.toBe(firstConversationId); await page.reload(); await expect(page.getByTestId('chat-composer')).toBeVisible(); await expect(page.locator('.msg.user').getByText(nextPrompt, { exact: true })).toBeVisible(); await page.getByTestId('conversation-history-trigger').click(); const historyList = page.getByTestId('conversation-list'); await expect(historyList).toBeVisible(); await expect(historyList.locator('.chat-conv-item')).toHaveCount(2); await historyList.getByTestId(`conversation-select-${firstConversationId}`).click(); await expect(page.locator('.msg.user').getByText(entry.prompt, { exact: true })).toBeVisible(); await expect(page.locator('.msg.user').getByText(nextPrompt, { exact: true })).toHaveCount(0); const { projectId } = await getCurrentProjectContext(page); const conversationsResponse = await page.request.get(`/api/projects/${projectId}/conversations`); expect(conversationsResponse.ok()).toBeTruthy(); const { conversations } = (await conversationsResponse.json()) as { conversations: Array<{ id: string }> }; expect(conversations.map((conversation) => conversation.id)).toEqual( expect.arrayContaining([firstConversationId, secondConversationId]), ); await expectScenarioProjectState(page, entry, projectId); } async function runFileMentionFlow( page: Page, entry: UiScenario, ) { const current = new URL(page.url()); const [, projects, projectId] = current.pathname.split('/'); if (projects !== 'projects' || !projectId) { throw new Error(`unexpected project route: ${current.pathname}`); } const resp = await page.request.post(`/api/projects/${projectId}/files`, { data: { name: 'reference.txt', content: 'Reference content for mention flow.\n', }, }); expect(resp.ok()).toBeTruthy(); await page.reload(); await expect(page.getByTestId('chat-composer')).toBeVisible(); await expect(page.getByText('reference.txt', { exact: true })).toBeVisible(); await page.getByTestId('chat-composer-input').click(); await page.getByTestId('chat-composer-input').pressSequentially('Review @ref'); await expect(page.getByTestId('mention-popover')).toBeVisible(); await page.getByTestId('mention-popover').getByRole('option', { name: /reference\.txt/i }).click(); await expect(page.getByTestId('chat-composer-input')).toHaveText('Review @reference.txt '); await expect(stagedAttachmentName(page, 'reference.txt')).toBeVisible(); await expect(page.getByTestId('chat-send')).toBeEnabled(); const runRequestPromise = page.waitForRequest(isCreateRunRequest); await page.getByTestId('chat-send').click(); const runBody = (await runRequestPromise).postDataJSON() as Record; expectScenarioRunRequest(runBody, entry); await expect(page.locator('.msg.user').filter({ hasText: 'Review @reference.txt' }).first()).toBeVisible(); await expect(page.locator('.user-attachments').getByText('reference.txt', { exact: true })).toBeVisible(); await expectScenarioProjectState(page, entry, projectId); } async function runDeepLinkPreviewFlow( page: Page, entry: UiScenario, ) { await sendPrompt(page, entry.prompt); await expectArtifactVisible(page, entry); const fileName = entry.mockArtifact!.fileName; await expect(page).toHaveURL( new RegExp(`/projects/[^/]+(?:/conversations/[^/]+)?/files/${fileName.replace('.', '\\.')}$`), ); const current = new URL(page.url()); const [, projects, projectId, maybeConversations, conversationId] = current.pathname.split('/'); if (projects !== 'projects' || !projectId) { throw new Error(`unexpected project route: ${current.pathname}`); } await page.goto(`/projects/${projectId}`, { waitUntil: 'domcontentloaded' }); await waitForLoadingToClear(page); await expect(page.getByTestId('file-workspace')).toBeVisible(); await page.goto(`/projects/${projectId}/files/${fileName}`, { waitUntil: 'domcontentloaded' }); await waitForLoadingToClear(page); const artifactTab = tabBySuffix(page, fileName); await expect(artifactTab).toBeVisible(); await expect(artifactTab).toHaveAttribute('aria-selected', 'true'); await expectProjectFileToContain(page, projectId, fileName, entry.mockArtifact!.heading); await expectScenarioProjectState(page, entry, projectId); } async function runFileUploadSendFlow( page: Page, entry: UiScenario, ) { const { projectId } = await getCurrentProjectContext(page); const uploadResponse = page.waitForResponse( (resp: Response) => resp.url().includes('/upload') && resp.request().method() === 'POST', { timeout: 5000 }, ); await page.getByTestId('chat-file-input').setInputFiles({ name: 'reference.txt', mimeType: 'text/plain', buffer: Buffer.from('Reference content for upload flow.\n', 'utf8'), }); await expect((await uploadResponse).ok()).toBeTruthy(); await expect(stagedAttachmentName(page, 'reference.txt')).toBeVisible(); await expect(page.getByText('reference.txt', { exact: true })).toBeVisible(); await sendPrompt(page, entry.prompt); await expect(page.locator('.msg.user').getByText(entry.prompt, { exact: true })).toBeVisible(); await expect(page.locator('.user-attachments').getByText('reference.txt', { exact: true })).toBeVisible(); await expectScenarioProjectState(page, entry, projectId); } async function runConversationDeleteRecoveryFlow( page: Page, entry: UiScenario, ) { page.on('dialog', async (dialog: Dialog) => { await dialog.accept(); }); await sendPrompt(page, entry.prompt); await expect( page.locator('.msg.user .user-text').filter({ hasText: entry.prompt }).first(), ).toBeVisible(); await startNewConversation(page); await expect(page.getByTestId('chat-composer-input')).toBeVisible(); await expect(page.getByTestId('chat-composer-input')).toHaveText(''); const nextPrompt = entry.secondaryPrompt!; await sendPrompt(page, nextPrompt); await expect( page.locator('.msg.user .user-text').filter({ hasText: nextPrompt }).first(), ).toBeVisible(); await page.getByTestId('conversation-history-trigger').click(); await expect(page.getByTestId('conversation-list')).toBeVisible(); const activeRow = page .getByTestId('conversation-list') .locator('.chat-conv-item.active') .first(); await expect(activeRow).toBeVisible(); await activeRow.getByTestId(/conversation-delete-/).click(); await expect( page.locator('.msg.user .user-text').filter({ hasText: entry.prompt }).first(), ).toBeVisible(); await expect(page.locator('.msg.user .user-text').filter({ hasText: nextPrompt })).toHaveCount(0); await page.getByTestId('conversation-history-trigger').click(); await expect(page.getByTestId('conversation-list').locator('.chat-conv-item')).toHaveCount(1); }