Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 137 additions & 4 deletions apps/desktop/e2e/accessibility-coverage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -40,6 +41,29 @@ async function openSettings(page: Page): Promise<void> {
await expect(page.getByRole('main', { name: '设置内容' })).toBeVisible();
}

async function tabTo(page: Page, target: Locator, label: string, limit = 30): Promise<void> {
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<void> {
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);
Expand Down Expand Up @@ -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,
}) => {
Expand All @@ -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,
});
Expand Down Expand Up @@ -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();
Expand Down
13 changes: 13 additions & 0 deletions apps/desktop/e2e/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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 };
2 changes: 1 addition & 1 deletion apps/desktop/e2e/workhub-layout.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
5 changes: 4 additions & 1 deletion apps/desktop/e2e/workhub-reconstruction.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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,
Expand Down Expand Up @@ -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: '登录稳定性' });
});
Expand Down
42 changes: 42 additions & 0 deletions apps/desktop/src/main/e2e-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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));
}
Expand Down
8 changes: 4 additions & 4 deletions apps/desktop/src/renderer/workhub-surface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ export function WorkHubSurface(props: {
/>
)}
>
<main className="maka-main agents-chat-panel agents-chat-view-root workhub-timeline" aria-label="WorkHub">
<section className="maka-main agents-chat-panel agents-chat-view-root workhub-timeline" aria-label="WorkHub">
<header className="workhub-header">
<div>
<h1>WorkHub</h1>
Expand Down Expand Up @@ -448,7 +448,7 @@ export function WorkHubSurface(props: {
)}
</ChatMessageList>
</div>
</main>
</section>
</ChatSurfaceLayout>
);
}
Expand Down Expand Up @@ -484,7 +484,7 @@ export function WorkHubCoordinationStatus(props: {
/>
)}
>
<main
<section
className="maka-main agents-chat-panel agents-chat-view-root workhub-timeline"
aria-label="WorkHub"
>
Expand Down Expand Up @@ -518,7 +518,7 @@ export function WorkHubCoordinationStatus(props: {
)}
</ChatMessageList>
</div>
</main>
</section>
</ChatSurfaceLayout>
);
}
Expand Down