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
118 changes: 117 additions & 1 deletion 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,17 @@ 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);
}

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 +130,103 @@ 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();
const recentTasks = page.getByRole('button', { name: /最近结束/ });
await recentTasks.focus();
await page.keyboard.press('Enter');
await expect(page.getByText('确认工具结果可以展开阅读', { exact: true })).toBeVisible();
await assertAxHealth(cdp, 'conversation/data-backed');

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'));

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 availableModel.focus();
await page.keyboard.press('Enter');
await expect(modelSwitcher).toContainText('glm-4.5');

const composer = page.locator(COMPOSER_INPUT);
await composer.fill('/graph on');
await composer.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 collapseGraph.focus();
await page.keyboard.press('Enter');
await expect(
graphPanel.getByRole('button', { name: '展开 Agent Graph' }),
).toHaveAttribute('aria-expanded', 'false');
await assertAxHealth(cdp, 'conversation/agent-graph-empty');
});

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);
const composer = page.locator(COMPOSER_INPUT);
await composer.fill(FAKE_HOLD_OPEN_PROMPT);
await composer.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 stop.focus();
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 Down Expand Up @@ -159,6 +268,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 @@ -486,6 +486,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 @@ -626,6 +627,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 @@ -82,7 +82,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();
});
8 changes: 4 additions & 4 deletions apps/desktop/e2e/workhub-reconstruction.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,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('main', { name: 'WorkHub' })).toBeVisible();
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 expect(page.getByText('1 项工作', { exact: true })).toBeVisible();
Expand All @@ -54,11 +54,11 @@ test('WorkHub rebuilds delegated execution feedback after navigating away and ba
await page.locator('.workhub-turn', { hasText: routedPrompt })
.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 expect(page.getByRole('main', { name: 'WorkHub' })).toBeVisible();
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 @@ -88,7 +88,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('main', { name: 'WorkHub' })).toBeVisible();
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