From 86f893ec36b11c51758a2e6a7d42627f3ab597c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=AD=E5=90=89=E6=B5=A9?= <1625567290@qq.com> Date: Sun, 30 Aug 2026 00:41:56 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B5=8B=E8=AF=95=EF=BC=9A=E6=89=A9=E5=B1=95?= =?UTF-8?q?=E6=A1=8C=E9=9D=A2=E7=AB=AF=E6=97=A0=E9=9A=9C=E7=A2=8D=E8=A1=8C?= =?UTF-8?q?=E4=B8=BA=E8=A6=86=E7=9B=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../e2e/accessibility-coverage.spec.ts | 141 +++++++++++++++++- apps/desktop/e2e/fixtures.ts | 13 ++ apps/desktop/e2e/workhub-layout.spec.ts | 2 +- .../e2e/workhub-reconstruction.spec.ts | 5 +- apps/desktop/src/main/e2e-fixture.ts | 42 ++++++ apps/desktop/src/renderer/workhub-surface.tsx | 8 +- 6 files changed, 201 insertions(+), 10 deletions(-) diff --git a/apps/desktop/e2e/accessibility-coverage.spec.ts b/apps/desktop/e2e/accessibility-coverage.spec.ts index fe4e97fe80..82cc6e3a89 100644 --- a/apps/desktop/e2e/accessibility-coverage.spec.ts +++ b/apps/desktop/e2e/accessibility-coverage.spec.ts @@ -17,7 +17,8 @@ * under the License. */ -import type { CDPSession, Page } from '@playwright/test'; +import { FAKE_HOLD_OPEN_PROMPT } from '@maka/runtime/test-only/fake-backend'; +import type { CDPSession, Locator, Page } from '@playwright/test'; import { expect, test, COMPOSER_INPUT } from './fixtures'; import { auditAxTree } from '../../../scripts/ax-tree-audit.mjs'; import { groupedNav } from '../src/renderer/settings/settings-nav'; @@ -40,6 +41,29 @@ async function openSettings(page: Page): Promise { await expect(page.getByRole('main', { name: '设置内容' })).toBeVisible(); } +async function tabTo(page: Page, target: Locator, label: string, limit = 30): Promise { + for (let index = 0; index < limit; index += 1) { + if (await target.evaluate((element) => element === document.activeElement)) return; + await page.keyboard.press('Tab'); + } + expect( + await target.evaluate((element) => element === document.activeElement), + `${label} is not reachable within ${limit} Tab presses`, + ).toBe(true); +} + +async function enterMainFromSkipLink(page: Page): Promise { + await page.evaluate(() => { + document.body.tabIndex = -1; + document.body.focus(); + }); + const skipLink = page.getByRole('link', { name: '跳到主要内容' }); + await tabTo(page, skipLink, 'skip link', 10); + await page.keyboard.press('Enter'); + await expect(page.getByRole('main')).toBeFocused(); + await page.evaluate(() => document.body.removeAttribute('tabindex')); +} + test('every settings page exposes named actionable controls', async ({ window: page }) => { await openSettings(page); const cdp = await page.context().newCDPSession(page); @@ -118,6 +142,104 @@ test('module pages and global overlays expose named actionable controls', async await page.keyboard.press('Escape'); }); +test('data-backed conversation supports keyboard access to tools, models, tasks, and Graph', async ({ + accessibilityNarrativeWindow: page, +}) => { + const cdp = await page.context().newCDPSession(page); + await expect(page.getByRole('region', { name: /对话:/ })).toBeVisible(); + await expect(page.getByRole('region', { name: '任务待办' })).toBeVisible(); + await expect(page.getByText('补齐桌面端无障碍覆盖', { exact: true })).toBeVisible(); + await assertAxHealth(cdp, 'conversation/data-backed'); + + await expect(page.getByRole('main')).toHaveCount(1); + await enterMainFromSkipLink(page); + + const toolCall = page.getByRole('button', { name: /^检查测试状态/ }); + await tabTo(page, toolCall, 'tool result'); + await page.keyboard.press('Enter'); + await expect(toolCall).toHaveAttribute('aria-expanded', 'true'); + await expect(page.locator('[data-slot="tool-output"]')).toContainText('core 41 passing'); + await assertAxHealth(cdp, 'conversation/tool-result-expanded'); + await page.keyboard.press('Enter'); + await expect(toolCall).toHaveAttribute('aria-expanded', 'false'); + + const modelSwitcher = page.getByRole('button', { name: '切换当前任务模型' }); + await tabTo(page, modelSwitcher, 'model picker'); + await page.keyboard.press('Enter'); + await expect(page.getByRole('menuitem', { name: /glm-5\.1/ })).toBeVisible(); + await assertAxHealth(cdp, 'conversation/model-picker'); + const availableModel = page.getByRole('menuitem', { name: 'glm-4.5', exact: true }); + await expect(availableModel).toBeFocused(); + await page.keyboard.press('Enter'); + await expect(modelSwitcher).toContainText('glm-4.5'); + + const composer = page.locator(COMPOSER_INPUT); + await tabTo(page, composer, 'composer', 60); + await page.keyboard.insertText('/graph on'); + const send = page.getByRole('button', { name: '发送' }); + await tabTo(page, send, 'Send button', 20); + await page.keyboard.press('Enter'); + await expect(page.getByText('Graph Mode 已开启', { exact: true })).toBeVisible(); + await assertAxHealth(cdp, 'overlay/graph-mode-toast'); + + const graphPanel = page.getByRole('region', { name: 'Agent Graph' }); + await expect(graphPanel).toBeVisible(); + await expect(graphPanel).toContainText('等待主 Agent 创建 operator…'); + const collapseGraph = graphPanel.getByRole('button', { name: '收起 Agent Graph' }); + await tabTo(page, collapseGraph, 'Graph collapse', 60); + await page.keyboard.press('Enter'); + await expect( + graphPanel.getByRole('button', { name: '展开 Agent Graph' }), + ).toHaveAttribute('aria-expanded', 'false'); + await assertAxHealth(cdp, 'conversation/agent-graph-empty'); + + const recentTasks = page.getByRole('button', { name: /最近结束/ }); + await tabTo(page, recentTasks, 'recent tasks', 80); + await page.keyboard.press('Enter'); + await expect(page.getByText('确认工具结果可以展开阅读', { exact: true })).toBeVisible(); +}); + +test('toast and error states expose healthy live regions', async ({ window: page }) => { + const cdp = await page.context().newCDPSession(page); + const composer = page.locator(COMPOSER_INPUT); + await composer.fill('/graph history'); + await composer.press('Enter'); + await expect(page.getByText('Graph 历史', { exact: true })).toBeVisible(); + await assertAxHealth(cdp, 'overlay/graph-history-toast'); + + await page.evaluate(async () => { + await window.maka.connections.setDefaultModel(null); + await window.maka.settings.updateClient({ workHub: { enabled: true } }); + }); + const failure = page.getByRole('alert'); + await expect(failure).toContainText('WorkHub 暂时无法启动'); + await expect(failure).toContainText('请检查当前 Runtime Host 的默认模型配置'); + await assertAxHealth(cdp, 'workhub/startup-error'); +}); + +test('a streaming answer exposes a healthy live conversation state', async ({ window: page }) => { + const cdp = await page.context().newCDPSession(page); + await enterMainFromSkipLink(page); + const composer = page.locator(COMPOSER_INPUT); + await tabTo(page, composer, 'streaming composer', 60); + await page.keyboard.insertText(FAKE_HOLD_OPEN_PROMPT); + const send = page.getByRole('button', { name: '发送' }); + await tabTo(page, send, 'streaming Send button', 20); + await page.keyboard.press('Enter'); + + await expect(page.locator('.maka-bubble-streaming')).toContainText('Fake backend waiting'); + await expect(page.getByRole('button', { name: '停止' })).toBeEnabled(); + await assertAxHealth(cdp, 'conversation/streaming'); + + const stop = page.getByRole('button', { name: '停止' }); + await tabTo(page, stop, 'streaming Stop button', 20); + await page.keyboard.press('Enter'); + await expect(page.getByRole('button', { name: '重新生成' })).toHaveCount(1, { + timeout: 20_000, + }); + await assertAxHealth(cdp, 'conversation/stopped'); +}); + test('composer and workbar entry points expose named actionable controls', async ({ window: page, }) => { @@ -126,11 +248,15 @@ test('composer and workbar entry points expose named actionable controls', async await expect(page.getByRole('region', { name: '新任务对话' })).toBeVisible(); await assertAxHealth(cdp, 'conversation/new-task'); + await enterMainFromSkipLink(page); const composer = page.locator(COMPOSER_INPUT); const prompt = 'create a session for accessibility coverage'; - await composer.fill(prompt); - await expect(page.getByRole('button', { name: '发送' })).toBeEnabled(); - await composer.press('Enter'); + await tabTo(page, composer, 'new-task composer', 60); + await page.keyboard.insertText(prompt); + const send = page.getByRole('button', { name: '发送' }); + await expect(send).toBeEnabled(); + await tabTo(page, send, 'new-task Send button', 20); + await page.keyboard.press('Enter'); await expect(page.getByText(`Fake backend received: ${prompt}`)).toBeVisible({ timeout: 30_000, }); @@ -159,6 +285,13 @@ test('composer and workbar entry points expose named actionable controls', async const activeTab = page.getByRole('tab', { name: new RegExp(panel) }); await expect(activeTab).toBeVisible(); await expect(activeTab).toHaveAttribute('aria-selected', 'true'); + if (panel === '终端') { + await expect(page.getByRole('region', { name: '任务终端' })).toBeVisible(); + } else if (panel === '浏览器') { + await expect(page.getByRole('region', { name: '嵌入式浏览器' })).toBeVisible(); + } else if (panel === '待办') { + await expect(page.getByRole('region', { name: '任务待办' })).toBeVisible(); + } await assertAxHealth(cdp, `workbar/${panel}`); if (panel !== workbarPanels.at(-1)) { await page.getByRole('button', { name: '打开工作栏标签' }).first().click(); diff --git a/apps/desktop/e2e/fixtures.ts b/apps/desktop/e2e/fixtures.ts index 2a725c5029..892c41ad98 100644 --- a/apps/desktop/e2e/fixtures.ts +++ b/apps/desktop/e2e/fixtures.ts @@ -501,6 +501,7 @@ export const test = base.extend<{ promptRailMotionWindow: Page; requestHeaderRowWindow: Page; newTaskTargetWindow: Page; + accessibilityNarrativeWindow: Page; }>({ // Seeded: a pre-staged connection clears onboarding so the composer is ready. window: async ({}, use) => { @@ -641,6 +642,18 @@ export const test = base.extend<{ showWindow: true, }, use); }, + // A data-backed conversation with settled tool evidence and a populated + // task ledger. Shown because the accessibility journey follows real native + // focus order through the transcript into the composer controls. + accessibilityNarrativeWindow: async ({}, use) => { + await withE2eWindow({ + seed: false, + readinessSelector: '[data-turn-id]', + e2eFixtureScenario: 'turn-narrative', + locale: 'zh', + showWindow: true, + }, use); + }, }); export { expect }; diff --git a/apps/desktop/e2e/workhub-layout.spec.ts b/apps/desktop/e2e/workhub-layout.spec.ts index 1e25de1205..53c5dc39db 100644 --- a/apps/desktop/e2e/workhub-layout.spec.ts +++ b/apps/desktop/e2e/workhub-layout.spec.ts @@ -77,7 +77,7 @@ test('WorkHub explains Coordination startup failure and recovers after a default }); }); - await expect(page.getByRole('main', { name: 'WorkHub' })).toBeVisible(); + await expect(page.getByRole('region', { name: 'WorkHub' })).toBeVisible(); await expect(page.locator('.workhub-empty')).toContainText('从这里继续所有工作'); await expect(page.locator('.workhub-surface .maka-composer-editor')).toBeVisible(); }); diff --git a/apps/desktop/e2e/workhub-reconstruction.spec.ts b/apps/desktop/e2e/workhub-reconstruction.spec.ts index 4586b37964..f4fcde9100 100644 --- a/apps/desktop/e2e/workhub-reconstruction.spec.ts +++ b/apps/desktop/e2e/workhub-reconstruction.spec.ts @@ -43,6 +43,7 @@ test('WorkHub rebuilds delegated execution feedback after navigating away and ba await page.evaluate(async () => { await window.maka.settings.updateClient({ workHub: { enabled: true } }); }); + await expect(page.getByRole('region', { name: 'WorkHub' })).toBeVisible(); // The conversation is the Coordination Session transcript. An ordinary // Session is a routing target and a status row, never a turn in WorkHub. await waitForWorkHubReady(page, 1); @@ -58,11 +59,12 @@ test('WorkHub rebuilds delegated execution feedback after navigating away and ba const routedTurn = page.locator('.workhub-turn', { hasText: routedPrompt }); await expect(routedTurn.locator('.workhub-submitted')).toBeVisible(); await routedTurn.locator('.workhub-submitted > button').click(); - await expect(page.getByRole('main', { name: 'WorkHub' })).toBeHidden(); + await expect(page.getByRole('region', { name: 'WorkHub' })).toBeHidden(); await ensureSidebarExpanded(page); await page.getByRole('button', { name: 'WorkHub', exact: true }).click(); await waitForWorkHubReady(page, 1); + await expect(page.getByRole('region', { name: 'WorkHub' })).toBeVisible(); await expect( page.locator('.workhub-projected-turn .workhub-user-bubble > p', { hasText: routedPrompt, @@ -92,6 +94,7 @@ test('WorkHub defers destructive correction until linked delegation exists', asy await page.evaluate(async () => { await window.maka.settings.updateClient({ workHub: { enabled: true } }); }); + await expect(page.getByRole('region', { name: 'WorkHub' })).toBeVisible(); await page.evaluate(async () => { await window.maka.sessions.create({ name: '登录稳定性' }); }); diff --git a/apps/desktop/src/main/e2e-fixture.ts b/apps/desktop/src/main/e2e-fixture.ts index dff4e6aab5..4f06653bb2 100644 --- a/apps/desktop/src/main/e2e-fixture.ts +++ b/apps/desktop/src/main/e2e-fixture.ts @@ -28,6 +28,7 @@ import { resolveStorageRoot, tryAcquireInteractiveRootOwner, } from '@maka/storage/root-authority'; +import { openInteractiveTaskLedgerStoreForWrite } from '@maka/storage/task-ledger-authority'; import { openInteractiveUsageStoresForWrite } from '@maka/storage/usage-stores'; import { E2E_FIXTURE_NOW, @@ -228,6 +229,47 @@ export async function seedE2eFixture(input: { await writeConnections(input.workspaceRoot, now, scenario); await writeSession(input.workspaceRoot, turnSession(now), turnMessages(now)); + if (scenario === 'turn-narrative' || scenario === 'turn-narrative-browser') { + const owner = await tryAcquireInteractiveRootOwner(storageRoot); + if (!owner) throw new Error('Unable to acquire the E2E fixture task-ledger root'); + try { + const tasks = await openInteractiveTaskLedgerStoreForWrite(owner.lease); + try { + const created = await tasks.create( + TURN_SESSION_ID, + [ + { subject: '补齐桌面端无障碍覆盖' }, + { subject: '核对模型选择器的键盘路径' }, + { subject: '确认工具结果可以展开阅读' }, + ], + { source: 'import', actor: 'system' }, + ); + await tasks.update( + TURN_SESSION_ID, + created.created[0]!.id, + { status: 'in_progress' }, + { source: 'import', actor: 'system' }, + ); + await tasks.update( + TURN_SESSION_ID, + created.created[2]!.id, + { status: 'in_progress' }, + { source: 'import', actor: 'system' }, + ); + await tasks.update( + TURN_SESSION_ID, + created.created[2]!.id, + { status: 'completed', completionEvidence: '工具输出已成功显示。' }, + { source: 'import', actor: 'system' }, + ); + } finally { + tasks.close(); + } + } finally { + await owner.close(); + } + } + if (scenario === 'chat-prompt-rail') { await writeSession(input.workspaceRoot, promptRailSession(now), promptRailMessages(now)); } diff --git a/apps/desktop/src/renderer/workhub-surface.tsx b/apps/desktop/src/renderer/workhub-surface.tsx index 99f78c1948..0e64ac3fd6 100644 --- a/apps/desktop/src/renderer/workhub-surface.tsx +++ b/apps/desktop/src/renderer/workhub-surface.tsx @@ -374,7 +374,7 @@ export function WorkHubSurface(props: { /> )} > -
+

WorkHub

@@ -448,7 +448,7 @@ export function WorkHubSurface(props: { )}
-
+ ); } @@ -484,7 +484,7 @@ export function WorkHubCoordinationStatus(props: { /> )} > -
@@ -518,7 +518,7 @@ export function WorkHubCoordinationStatus(props: { )} -
+ ); }