import { expect, test, type Page } from '@playwright/test' import type { Artifact, Conversation, SurfaceMeta, SurfaceSnapshot } from '../src/contracts' test.skip(!process.env.WORK_COMPANION_BROWSER, 'Run explicitly against the task-owned UI preview') const stamp = '2026-10-12T10:00:00Z' const conversation: Conversation = { id: 'navigation-conversation', projectId: 'navigation-project', title: '继续阅读材料', createdAt: stamp, updatedAt: stamp, } const artifact: Artifact = { id: 'navigation-document', projectId: conversation.projectId, name: '材料摘要.md', kind: 'markdown', size: 64, createdAt: stamp, updatedAt: stamp, } const snapshot: SurfaceSnapshot = { identity: { surfaceId: 'saved-navigation-page', owner: 'reader', pluginId: 'generative-ui', conversationId: conversation.id }, compat: { schemaVersion: 1, catalogVersion: 'ui-v1' }, revision: 1, activationEpoch: 1, spec: { title: '已保存的材料页面', nodes: [ { id: 'summary', type: 'markdown', markdown: '已保存的材料范围' }, { id: 'source', type: 'doc-ref', docId: artifact.id }, ] }, } const metadata: SurfaceMeta = { surfaceId: snapshot.identity.surfaceId, title: snapshot.spec.title, pluginId: snapshot.identity.pluginId, revision: snapshot.revision, conversationId: conversation.id, updatedAt: stamp, } // Delay the external list contract, while running the real store, App and panes. // The saved selection is restored by openConversation rather than test-only state injection. async function fixture(page: Page) { const errors: string[] = [] page.on('pageerror', error => errors.push(error.message)) let release!: () => void const hydration = new Promise(resolve => { release = resolve }) await page.addInitScript(({ conversationId }) => { localStorage.setItem('as.conv', conversationId) localStorage.setItem('as.ui', JSON.stringify({ theme: 'light', rightTab: 'files', filesView: 'preview', rightCollapsed: true, })) }, { conversationId: conversation.id }) await page.route('**/api/**', async route => { const request = route.request() const path = new URL(request.url()).pathname if (request.method() !== 'GET') return route.fulfill({ status: 405, body: 'read-only navigation fixture' }) if (path === '/api/events') return route.fulfill({ contentType: 'text/event-stream', body: ': connected\n\n' }) let body: unknown = [] if (path === '/api/health') body = { ok: true } else if (path === '/api/me') body = { owner: 'reader', name: '材料阅读者', auth: true, authVia: 'session', isAdmin: false } else if (path === '/api/projects') body = [{ id: conversation.projectId, name: '材料项目', createdAt: stamp, updatedAt: stamp }] else if (path === '/api/conversations') body = [conversation] else if (path === `/api/conversations/${conversation.id}`) body = { conversation, messages: [], runs: [], activeHitl: [], lastSeq: 0 } else if (path === `/api/conversations/${conversation.id}/surfaces`) { await hydration; body = [metadata] } else if (path === `/api/surfaces/${snapshot.identity.surfaceId}`) body = snapshot else if (path === `/api/conversations/${conversation.id}/cards`) body = { cards: [] } else if (path === `/api/projects/${conversation.projectId}/artifacts`) body = [artifact] else if (path === `/api/artifacts/${artifact.id}/content`) { return route.fulfill({ contentType: 'text/markdown', body: '# 材料摘要\n\n原始材料的阅读范围' }) } else if (path === '/api/settings') body = { user: {}, project: {}, effective: {}, schema: [] } else if (path === '/api/workspaces') body = [{ id: 'navigation-materials', title: '材料工作区', body: [{ type: 'doc-ref', docId: artifact.id }] }] else if (path.endsWith('/plan') || path.endsWith('/todos')) body = null await route.fulfill({ contentType: 'application/json', body: JSON.stringify(body) }) }) const listRequest = page.waitForRequest(request => new URL(request.url()).pathname === `/api/conversations/${conversation.id}/surfaces`) await page.goto('/') await listRequest await expect(page.locator('.chat-title')).toHaveText(conversation.title) return { release, errors } } for (const width of [736, 390]) { test(`keeps context and focus when saved Surface hydration arrives at ${width}px`, async ({ page }) => { await page.setViewportSize({ width, height: 900 }) const state = await fixture(page) await page.getByRole('button', { name: '打开上下文', exact: true }).click() const search = page.getByRole('textbox', { name: '搜索本项目会话', exact: true }) await search.fill('材料') await expect(search).toBeFocused() state.release() // The real hidden preview proves both restored tab selection and snapshot hydration finished. await expect(page.locator('.preview-pane .surface-view-title')).toHaveText(snapshot.spec.title!) await expect(page.locator('.app-body')).toHaveAttribute('data-active-pane', 'context') await expect(page.locator('.left-nav')).toBeVisible() await expect(page.locator('.main')).toBeHidden() await expect(page.locator('.right-panel')).toBeHidden() await expect(search).toHaveValue('材料') await expect(search).toBeFocused() expect(state.errors).toEqual([]) }) test(`preserves explicit work, Surface, document and return navigation at ${width}px`, async ({ page }) => { await page.setViewportSize({ width, height: 900 }) const state = await fixture(page) state.release() await expect(page.locator('.preview-pane .surface-view-title')).toHaveText(snapshot.spec.title!) await page.getByRole('button', { name: '打开上下文', exact: true }).click() await page.getByRole('button', { name: '打开工作', exact: true }).click() await expect(page.locator('.app-body')).toHaveAttribute('data-active-pane', 'work') await expect(page.locator('.right-panel')).toBeVisible() await expect(page.locator('.left-nav')).toBeHidden() await page.getByRole('button', { name: '打开上下文', exact: true }).click() await page.getByRole('button', { name: '打开文件', exact: true }).click() await expect(page.locator('.app-body')).toHaveAttribute('data-active-pane', 'files') await page.getByRole('button', { name: '返回文件列表', exact: true }).click() await page.locator(`[data-surface-row="${snapshot.identity.surfaceId}"]`).click() await expect(page.locator('.preview-pane .surface-view-title')).toHaveText(snapshot.spec.title!) // Surface doc-ref uses the existing document bridge, which opens the same preview. await page.locator('.preview-pane').getByRole('button', { name: artifact.id, exact: true }).click() await expect(page.locator('.preview-pane-name')).toHaveText(artifact.name) await expect(page.locator('.preview-pane-body')).toContainText('原始材料的阅读范围') await page.getByRole('button', { name: '展开预览', exact: true }).click() await expect(page.locator('.right-panel')).toHaveClass(/expanded/) await page.getByRole('button', { name: '返回对话', exact: true }).click() await expect(page.locator('.app-body')).toHaveAttribute('data-active-pane', 'conversation') await expect(page.locator('.main')).toBeVisible() await page.getByRole('button', { name: '打开上下文', exact: true }).click() await page.setViewportSize({ width: 1440, height: 900 }) await expect(page.locator('.main')).toBeVisible() await page.setViewportSize({ width, height: 900 }) await expect(page.locator('.app-body')).toHaveAttribute('data-active-pane', 'conversation') await expect(page.locator('.left-nav')).toBeHidden() expect(state.errors).toEqual([]) }) test(`opens a workspace document while context overlays the selected files pane at ${width}px`, async ({ page }) => { await page.setViewportSize({ width, height: 900 }) const state = await fixture(page) state.release() await expect(page.locator('.preview-pane .surface-view-title')).toHaveText(snapshot.spec.title!) await page.getByRole('button', { name: '打开文件', exact: true }).click() await page.getByRole('button', { name: '打开上下文', exact: true }).click() await page.locator('.left-nav').getByRole('button', { name: /材料工作区/ }).click() const workspace = page.getByRole('dialog', { name: '材料工作区', exact: true }) await workspace.getByRole('button', { name: artifact.name, exact: true }).click() await page.keyboard.press('Escape') await expect(page.locator('.app-body')).toHaveAttribute('data-active-pane', 'files') await expect(page.locator('.preview-pane-name')).toHaveText(artifact.name) await expect(page.locator('.preview-pane-body')).toContainText('原始材料的阅读范围') expect(state.errors).toEqual([]) }) }