blob: fa961dcf5a1ed75a9085a4942acf1d327f61c1fc [file]
import type { Page } from '@playwright/test';
import { test, expect } from './fixtures';
function settingsNavigation(page: Page) {
return page.getByRole('navigation', { name: /^(设置分组|Settings sections)$/ });
}
/**
* Settings take effect: open settings, switch the theme to dark, and confirm
* the <html> root picks up the `dark` class (theme.ts applies it via
* classList.toggle). This exercises the settings open → navigate → mutate →
* apply path without depending on pixel colors.
*/
test('changing the theme in settings applies to the UI', async ({ window: page }) => {
await page.getByRole('button', { name: '展开侧边栏' }).click();
await page.getByRole('button', { name: '设置' }).click();
await expect(page.getByLabel('设置内容')).toBeVisible();
await settingsNavigation(page).getByRole('button', { name: '外观', exact: true }).click();
const lightTheme = page.getByRole('checkbox', { name: '浅色' });
const darkTheme = page.getByRole('checkbox', { name: '深色' });
await darkTheme.locator('..').click();
await expect(darkTheme).toBeChecked();
await expect(lightTheme).not.toBeChecked();
await expect.poll(
async () => page.evaluate(() => document.documentElement.classList.contains('dark')),
).toBe(true);
});
test('settings back icon shares the navigation icon rail', async ({ window: page }) => {
await page.getByRole('button', { name: '展开侧边栏' }).click();
await page.getByRole('button', { name: '设置' }).click();
const iconCenterX = async (locator: ReturnType<Page['locator']>) => {
const box = await locator.boundingBox();
expect(box).not.toBeNull();
return box ? box.x + box.width / 2 : Number.NaN;
};
const backIcon = page.getByRole('button', { name: '返回应用', exact: true }).locator('svg').first();
const navIcon = settingsNavigation(page).getByRole('button', { name: '通用', exact: true }).locator('svg').first();
expect(Math.abs(await iconCenterX(backIcon) - await iconCenterX(navIcon))).toBeLessThanOrEqual(0.5);
});
test('subagent presets can be reviewed and edited in desktop settings', async ({ window: page }) => {
await page.evaluate(async () => {
const connections = await window.maka.connections.list();
const connection = connections[0];
if (!connection) throw new Error('E2E subagent settings requires a seeded connection');
await window.maka.settings.update({
subagents: {
presets: [{
id: 'e2e-fast-reader',
name: 'E2E 快速阅读',
description: '快速阅读大型代码仓库。',
profile: 'local_read',
connectionSlug: connection.slug,
model: connection.enabledModelIds?.[0] ?? connection.defaultModel,
// Seeded DISABLED on purpose: the editor's own switch reads from the
// preset, so this run is what proves saving an unrelated field makes
// the round trip without quietly re-enabling a preset the user
// turned off.
enabled: false,
}],
},
});
});
await page.getByRole('button', { name: '展开侧边栏' }).click();
await page.getByRole('button', { name: '设置' }).click();
const navItem = settingsNavigation(page).getByRole('button', { name: '子 Agent', exact: true });
await navItem.click();
const settings = page.getByRole('main', { name: '设置内容' });
await expect(settings.getByRole('heading', { name: '子 Agent', exact: true })).toBeVisible();
await expect(settings.getByText('E2E 快速阅读', { exact: true })).toBeVisible();
// The row's switch states the disabled preset; a badge beside it would be the
// same fact twice.
const rowSwitch = settings.getByRole('switch', { name: '启用: E2E 快速阅读' });
await expect(rowSwitch).not.toBeChecked();
// Arriving is not navigating: focus stays on the settings nav item the user
// just clicked. Only a level change moves it.
await expect(navItem).toBeFocused();
// The editor is a route level, not a dialog: the list is replaced in place
// and the back affordance is the only way out.
await settings.getByRole('button', { name: '配置“E2E 快速阅读”' }).click();
await expect(settings.getByRole('heading', { name: 'E2E 快速阅读', exact: true })).toBeVisible();
await expect(settings.getByRole('button', { name: '添加子 Agent' })).toBeHidden();
// A level change moves focus to the level itself; without it the chevron
// that had focus unmounts and a keyboard user restarts from document.body.
await expect(settings.locator('[data-maka-contract="subagent-detail"]')).toBeFocused();
// The level owns the whole preset, so it carries the two things the list row
// deliberately does not: the settled id, and deletion.
await expect(settings.getByText('e2e-fast-reader', { exact: true })).toBeVisible();
await expect(settings.getByRole('button', { name: '删除', exact: true })).toBeVisible();
// Renaming is the one edit that could re-key the preset: the id derives from
// the name while creating, and an existing preset must never follow it.
await settings.getByRole('textbox', { name: '显示名称' }).fill('E2E 快速阅读 v2');
await settings.getByRole('textbox', { name: '适用场景' }).fill('快速阅读代码,并总结关键调用链。');
await settings.getByRole('button', { name: '保存', exact: true }).click();
await expect(settings.getByRole('button', { name: '添加子 Agent' })).toBeVisible();
await expect(settings.getByText('快速阅读代码,并总结关键调用链。', { exact: true })).toBeVisible();
// Returning to the list puts focus back on the row the user left from.
await expect(settings.locator('[data-subagent-preset="e2e-fast-reader"]')).toBeFocused();
await expect.poll(async () => page.evaluate(async () => {
const current = await window.maka.settings.get();
const preset = current.subagents.presets[0];
return { id: preset?.id, name: preset?.name, description: preset?.description, enabled: preset?.enabled };
})).toEqual({
id: 'e2e-fast-reader',
name: 'E2E 快速阅读 v2',
description: '快速阅读代码,并总结关键调用链。',
enabled: false,
});
});
test('deleting a subagent preset is reversible until the confirm is accepted', async ({ window: page }) => {
await page.evaluate(async () => {
const connections = await window.maka.connections.list();
const connection = connections[0];
if (!connection) throw new Error('E2E subagent settings requires a seeded connection');
await window.maka.settings.update({
subagents: {
presets: [{
id: 'e2e-doomed',
name: 'E2E 待删除',
description: '这个配置会在本次测试里被删除。',
profile: 'local_read',
connectionSlug: connection.slug,
model: connection.enabledModelIds?.[0] ?? connection.defaultModel,
enabled: true,
}],
},
});
});
await page.getByRole('button', { name: '展开侧边栏' }).click();
await page.getByRole('button', { name: '设置' }).click();
await settingsNavigation(page).getByRole('button', { name: '子 Agent', exact: true }).click();
const settings = page.getByRole('main', { name: '设置内容' });
await settings.getByRole('button', { name: '配置“E2E 待删除”' }).click();
const deleteButton = settings.getByRole('button', { name: '删除', exact: true });
// Cancelling the confirm has to leave the preset alone — the destructive path
// is the one place where "it did nothing" cannot be checked by eye.
await deleteButton.click();
const confirm = page.getByRole('alertdialog');
await expect(confirm).toBeVisible();
await confirm.getByRole('button', { name: '取消', exact: true }).click();
await expect(confirm).toBeHidden();
await expect(settings.getByText('e2e-doomed', { exact: true })).toBeVisible();
await deleteButton.click();
await expect(confirm).toBeVisible();
await confirm.getByRole('button', { name: '删除', exact: true }).click();
await expect(confirm).toBeHidden();
// Deletion is the only way the row a user came from can be missing, so it is
// the only thing that exercises the focus fallback.
await expect(settings.getByText('E2E 待删除', { exact: true })).toBeHidden();
await expect(settings.getByRole('button', { name: '添加子 Agent' })).toBeFocused();
await expect.poll(async () => page.evaluate(async () => {
const current = await window.maka.settings.get();
return current.subagents.presets.length;
})).toBe(0);
});
test('a subagent preset can be created disabled and then enabled from its row', async ({ window: page }) => {
await page.evaluate(async () => {
await window.maka.settings.update({ subagents: { presets: [] } });
});
await page.getByRole('button', { name: '展开侧边栏' }).click();
await page.getByRole('button', { name: '设置' }).click();
await settingsNavigation(page).getByRole('button', { name: '子 Agent', exact: true }).click();
const settings = page.getByRole('main', { name: '设置内容' });
// The create branch is a structurally different tree from the edit branch —
// a typed id instead of a settled one, and no delete section — so it needs
// its own journey rather than riding on the edit one.
await settings.getByRole('button', { name: '添加子 Agent' }).click();
await settings.getByRole('textbox', { name: '显示名称' }).fill('E2E Web Research');
// The id derives from the name until the user takes it over.
await expect(settings.getByRole('textbox', { name: 'subagent_id' })).toHaveValue('e2e-web-research');
// Taking the id over stops the derivation for good: a later name edit must
// not walk over what the user typed.
await settings.getByRole('textbox', { name: 'subagent_id' }).fill('web-research-owned');
await settings.getByRole('textbox', { name: '显示名称' }).fill('E2E Web Research 2');
await expect(settings.getByRole('textbox', { name: 'subagent_id' })).toHaveValue('web-research-owned');
await settings.getByRole('textbox', { name: '适用场景' }).fill('查找外部资料。');
await settings.getByRole('switch', { name: '启用', exact: true }).click();
await settings.getByRole('button', { name: '创建', exact: true }).click();
await expect(settings.getByText('E2E Web Research 2', { exact: true })).toBeVisible();
await expect.poll(async () => page.evaluate(async () => {
const current = await window.maka.settings.get();
const preset = current.subagents.presets[0];
return { id: preset?.id, enabled: preset?.enabled };
})).toEqual({ id: 'web-research-owned', enabled: false });
await settings.getByRole('switch', { name: '启用: E2E Web Research 2' }).click();
await expect.poll(async () => page.evaluate(async () => {
const current = await window.maka.settings.get();
return current.subagents.presets[0]?.enabled;
})).toBe(true);
});
test('remote access prioritizes a configured channel that needs attention', async ({ window: page }) => {
const runtimeError = 'runtime-diagnostic-'.repeat(10);
await page.evaluate(async (lastError) => {
await window.maka.settings.update({
botChat: {
channels: {
telegram: {
connected: true,
readiness: 'operational',
token: 'e2e-telegram-placeholder',
},
discord: {
connected: true,
readiness: 'degraded',
token: 'e2e-discord-placeholder',
lastError,
},
},
},
});
}, runtimeError);
await page.getByRole('button', { name: '展开侧边栏' }).click();
await page.getByRole('button', { name: '设置' }).click();
const settings = page.getByRole('main', { name: '设置内容' });
await settingsNavigation(page).getByRole('button', { name: '远程接入' }).click();
const activeChannels = page.getByRole('region', { name: '正在使用' }).getByRole('button');
await expect(activeChannels).toHaveCount(2);
await expect(activeChannels.nth(0)).toHaveAccessibleName(/管理 Discord/);
await expect(activeChannels.nth(0)).toHaveAccessibleName(new RegExp(runtimeError));
await expect(settings.getByText(runtimeError, { exact: true })).toBeVisible();
await expect(activeChannels.nth(1)).toHaveAccessibleName(/管理 Telegram/);
await activeChannels.nth(0).click();
const enabledSwitch = settings.getByRole('switch', { name: '启用Discord渠道' });
const configDocs = settings.getByRole('link', { name: '查看配置文档' });
const connectButton = settings.getByRole('button', { name: '测试并连接' });
await expect(enabledSwitch).toBeEnabled();
await settings.getByRole('button', { name: '返回远程接入' }).focus();
await page.keyboard.press('Tab');
await expect(enabledSwitch).toBeFocused();
await page.keyboard.press('Tab');
await expect(configDocs).toBeFocused();
await page.keyboard.press('Tab');
await expect(connectButton).toBeFocused();
const recentFailure = settings.getByRole('alert').filter({ hasText: '最近一次失败' });
await expect(recentFailure).toContainText(runtimeError);
});