Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s
302 lines
14 KiB
TypeScript
302 lines
14 KiB
TypeScript
/*
|
|
* Licensed to the Apache Software Foundation (ASF) under one
|
|
* or more contributor license agreements. See the NOTICE file
|
|
* distributed with this work for additional information
|
|
* regarding copyright ownership. The ASF licenses this file
|
|
* to you under the Apache License, Version 2.0 (the
|
|
* "License"); you may not use this file except in compliance
|
|
* with the License. You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing,
|
|
* software distributed under the License is distributed on an
|
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
* KIND, either express or implied. See the License for the
|
|
* specific language governing permissions and limitations
|
|
* under the License.
|
|
*/
|
|
|
|
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';
|
|
|
|
function exactNameWithOptionalBadge(label: string): RegExp {
|
|
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
return new RegExp(`^${escaped}(?: Beta)?$`);
|
|
}
|
|
|
|
async function assertAxHealth(cdp: CDPSession, surface: string): Promise<void> {
|
|
const result = await cdp.send('Accessibility.getFullAXTree');
|
|
const audit = auditAxTree(result.nodes);
|
|
expect(audit.problems, `${surface} exposes an unhealthy AX tree`).toEqual([]);
|
|
}
|
|
|
|
async function openSettings(page: Page): Promise<void> {
|
|
const sidebarToggle = page.getByRole('button', { name: '展开侧边栏' });
|
|
if (await sidebarToggle.isVisible()) await sidebarToggle.click();
|
|
await page.getByRole('button', { name: '设置', exact: true }).click();
|
|
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);
|
|
const navigation = page.getByRole('navigation', { name: '设置分组' });
|
|
await expect(page.getByRole('main')).toHaveCount(1);
|
|
const sectionLabels = (await navigation.getByRole('button').allTextContents())
|
|
.map((label) => label.trim().replace(/\s*Beta$/, ''))
|
|
.filter((label) => label.length > 0 && label !== '返回应用');
|
|
const expectedSectionLabels = groupedNav('zh')
|
|
.flatMap(({ items }) => items)
|
|
.filter(({ enabled }) => enabled)
|
|
.map(({ label }) => label);
|
|
expect(sectionLabels, 'settings navigation must expose every enabled page in source order').toEqual(
|
|
expectedSectionLabels,
|
|
);
|
|
|
|
for (const section of sectionLabels) {
|
|
const sectionButton = navigation.getByRole('button', {
|
|
name: exactNameWithOptionalBadge(section),
|
|
});
|
|
await sectionButton.click();
|
|
await expect(sectionButton).toHaveAttribute('aria-current', 'page');
|
|
await expect(page.getByRole('heading', { name: section, exact: true })).toBeVisible();
|
|
await assertAxHealth(cdp, `settings/${section}`);
|
|
}
|
|
});
|
|
|
|
test('module pages and global overlays expose named actionable controls', async ({
|
|
window: page,
|
|
}) => {
|
|
const cdp = await page.context().newCDPSession(page);
|
|
await expect(page.getByRole('link', { name: '跳到主要内容' })).toHaveCount(1);
|
|
await expect(page.getByRole('link', { name: 'Skip to content' })).toHaveCount(0);
|
|
const sidebarToggle = page.getByRole('button', { name: '展开侧边栏' });
|
|
if (await sidebarToggle.isVisible()) await sidebarToggle.click();
|
|
const navigation = page.getByRole('navigation', { name: '任务列表' });
|
|
|
|
await navigation.getByRole('button', { name: '扩展', exact: true }).click();
|
|
await expect(page.getByRole('main')).toHaveCount(1);
|
|
await expect(page.getByRole('region', { name: '扩展', exact: true })).toBeVisible();
|
|
const extensionsNavigation = page.getByRole('navigation', { name: /扩展内容/ });
|
|
await expect(
|
|
extensionsNavigation.getByRole('button', { name: '技能', exact: true }),
|
|
).toHaveAttribute('aria-current', 'true');
|
|
await assertAxHealth(cdp, 'extensions/skills');
|
|
const mcpButton = extensionsNavigation.getByRole('button', { name: 'MCP', exact: true });
|
|
await mcpButton.click();
|
|
await expect(mcpButton).toHaveAttribute('aria-current', 'true');
|
|
await assertAxHealth(cdp, 'extensions/mcp');
|
|
|
|
await navigation.getByRole('button', { name: /定时任务/ }).click();
|
|
const automationsNavigation = page.getByRole('navigation', { name: /定时任务内容/ });
|
|
await expect(
|
|
automationsNavigation.getByRole('button', { name: '定时任务', exact: true }),
|
|
).toHaveAttribute('aria-current', 'true');
|
|
await assertAxHealth(cdp, 'automations/scheduled-tasks');
|
|
const dailyReviewButton = automationsNavigation.getByRole('button', {
|
|
name: '每日回顾',
|
|
exact: true,
|
|
});
|
|
await dailyReviewButton.click();
|
|
await expect(dailyReviewButton).toHaveAttribute('aria-current', 'true');
|
|
await assertAxHealth(cdp, 'automations/daily-review');
|
|
|
|
await page.keyboard.press('Shift+Slash');
|
|
const keyboardHelpDialog = page.getByRole('dialog', { name: '键盘快捷键' });
|
|
await expect(keyboardHelpDialog).toBeVisible();
|
|
await assertAxHealth(cdp, 'overlay/keyboard-help');
|
|
await page.keyboard.press('Escape');
|
|
await expect(keyboardHelpDialog).toBeHidden();
|
|
|
|
await page.keyboard.press(process.platform === 'darwin' ? 'Meta+k' : 'Control+k');
|
|
const commandPaletteDialog = page.getByRole('dialog', { name: '命令面板' });
|
|
await expect(commandPaletteDialog).toBeVisible();
|
|
await assertAxHealth(cdp, 'overlay/command-palette');
|
|
await page.keyboard.press('Escape');
|
|
});
|
|
|
|
test('data-backed conversation exposes ordered todos and keyboard access to tools, models, and Graph', async ({
|
|
accessibilityNarrativeWindow: page,
|
|
}) => {
|
|
const cdp = await page.context().newCDPSession(page);
|
|
await expect(page.getByRole('region', { name: /对话:/ })).toBeVisible();
|
|
const todoRegion = page.getByRole('region', { name: '任务待办' });
|
|
await expect(todoRegion).toBeVisible();
|
|
await expect(todoRegion.getByRole('listitem')).toHaveText([
|
|
'补齐桌面端无障碍覆盖',
|
|
'核对模型选择器的键盘路径',
|
|
'确认工具结果可以展开阅读',
|
|
]);
|
|
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');
|
|
});
|
|
|
|
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,
|
|
}) => {
|
|
const cdp = await page.context().newCDPSession(page);
|
|
await expect(page.getByRole('main')).toHaveCount(1);
|
|
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 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,
|
|
});
|
|
await expect(page.getByRole('main')).toHaveCount(1);
|
|
await expect(page.getByRole('region', { name: /对话:/ })).toBeVisible();
|
|
await assertAxHealth(cdp, 'conversation/session');
|
|
|
|
await page.getByRole('button', { name: '展开任务工作栏' }).click();
|
|
await expect(page.getByRole('list', { name: '打开工具' })).toBeVisible();
|
|
await assertAxHealth(cdp, 'workbar/launcher');
|
|
|
|
const workbarPanels = [
|
|
'侧边对话',
|
|
'变更',
|
|
'终端',
|
|
'浏览器',
|
|
'生成文件',
|
|
'待办',
|
|
'追踪',
|
|
] as const;
|
|
for (const panel of workbarPanels) {
|
|
await page
|
|
.getByRole('list', { name: '打开工具' })
|
|
.getByRole('button', { name: new RegExp(`^${panel}(?: |$)`) })
|
|
.click();
|
|
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();
|
|
await expect(page.getByRole('list', { name: '打开工具' })).toBeVisible();
|
|
}
|
|
}
|
|
});
|