Files
EasyCLIProxyAPI/tests/deepseek-harness-catalog-ui.cjs
T
lzt404 18a9bfbaa9 fix(ui): unify bottom-right notifications and improve dialog interactions
- Move operation feedback into a shared floating stack to prevent layout shifts
- Allow identical errors to reappear after dismissal
- Support keyboard navigation, scrolling, and focus restoration in dialogs
- Add notification interaction regression tests
- Remove code comments throughout the project
2026-09-15 02:39:03 +08:00

144 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
(async () => {
const browser = await chromium.launch({ channel: 'msedge', headless: true });
const page = await browser.newPage();
await page.route('**/*', route => route.request().url().startsWith('http://127.0.0.1:1421/') ? route.continue() : route.abort());
const errors = [];
page.on('pageerror', e => errors.push(e.message));
const open = async (query = '') => {
await page.goto(`http://127.0.0.1:1421/tests/fixtures/agent-backups.html?client=deepseek-harness&reset-selections&${query}`);
await page.getByRole('button', { name: '配置模型', exact: true }).click();
await page.getByRole('option', { name: /gpt-one/ }).waitFor();
};
const dialog = () => page.getByRole('dialog', { name: 'DeepSeek Harness 模型配置' });
const save = async () => {
await dialog().getByRole('button', { name: '保存', exact: true }).click();
await page.locator('.app-notice-stack').getByRole('status').filter({ hasText: /已保存/ }).waitFor();
};
try {
for (const mode of ['', 'embedded&']) {
await open(mode);
const context = () => dialog().getByRole('spinbutton', { name: '上下文窗口(tokens)' });
assert.equal(await context().inputValue(), '128000');
await dialog().getByText('来源:内核模型目录 / 模型 API', { exact: true }).waitFor();
await context().focus();
await page.keyboard.press('Tab');
assert.equal(await dialog().getByRole('button', { name: '保存', exact: true }).isDisabled(), true);
await context().fill('192000');
await dialog().getByRole('button', { name: '恢复默认上下文', exact: true }).click();
assert.equal(await context().inputValue(), '128000');
assert.equal(await dialog().getByRole('button', { name: '保存', exact: true }).isDisabled(), true);
assert.equal(await dialog().getByRole('combobox', { name: '输入类型', exact: true }).inputValue(), '');
await dialog().getByRole('option', { name: /unknown-model/ }).click();
await dialog().getByText(/未提供(不代表不支持)/).waitFor();
assert.equal(await context().inputValue(), '262144');
await dialog().getByText('来源:DSH 默认值', { exact: true }).waitFor();
await dialog().getByRole('combobox', { name: '输入类型', exact: true }).selectOption('["text","image"]');
await dialog().getByRole('option', { name: /gpt-two/ }).click();
await dialog().getByRole('spinbutton', { name: '输出上限(tokens)' }).fill('4096');
await dialog().getByRole('combobox', { name: '推理等级', exact: true }).selectOption('custom');
await dialog().getByRole('textbox', { name: 'high reasoning_effort' }).fill('custom-high');
await dialog().getByRole('button', { name: '供应商设置', exact: true }).click();
const providerContext = dialog().getByRole('spinbutton', { name: '默认上下文(tokens)', exact: true });
assert.equal(await providerContext.inputValue(), '262144');
await providerContext.fill('512000');
await dialog().getByRole('option', { name: /unknown-model/ }).click();
assert.equal(await context().inputValue(), '512000');
await dialog().getByText('来源:供应商默认上下文', { exact: true }).waitFor();
await context().fill('256000');
await context().fill('');
await page.keyboard.press('Tab');
assert.equal(await context().inputValue(), '512000');
await dialog().getByRole('option', { name: /gpt-one/ }).click();
assert.equal(await context().inputValue(), '128000');
await dialog().getByRole('button', { name: '供应商设置', exact: true }).click();
await dialog().getByRole('combobox', { name: '默认输入类型', exact: true }).selectOption('["text","image"]');
await dialog().getByRole('spinbutton', { name: '请求超时(ms)' }).fill('60000');
await save();
const request = await page.evaluate(() => window.fixtureCalls.filter(c => c.cmd === 'save_deepseek_harness_model_catalog_editor').at(-1).args.request);
assert.deepEqual(request.models.find(m => m.id === 'unknown-model').configuration.input, ['text','image']);
assert.equal(request.models.find(m => m.id === 'gpt-two').configuration.maxTokens, 4096);
assert.equal(request.models.find(m => m.id === 'gpt-two').configuration.reasoningEfforts.high, 'custom-high');
assert.equal(request.provider.timeoutMs, 60000);
assert.equal(request.provider.defaultContextWindow, 512000);
assert(request.models.every(m => m.configuration.contextWindow === undefined));
assert.equal(await page.evaluate(() => window.fixtureCalls.some(c => c.cmd === 'update_agent_config')), false);
await dialog().getByRole('button', { name: '关闭', exact: true }).click();
assert.match(await page.locator('.agent-model-trigger').innerText(), /gpt-one/);
await page.evaluate(() => window.fixtureRemount());
await page.getByRole('button', { name: '配置模型', exact: true }).click();
await dialog().getByRole('option', { name: /unknown-model/ }).click();
assert.equal(await dialog().getByRole('combobox', { name: '输入类型', exact: true }).inputValue(), '["text","image"]');
await dialog().getByRole('button', { name: '恢复自动', exact: true }).click();
await save();
assert.equal(await dialog().getByRole('combobox', { name: '输入类型', exact: true }).inputValue(), '');
await dialog().getByRole('spinbutton', { name: '上下文窗口(tokens)' }).fill('0');
await dialog().getByRole('button', { name: '保存', exact: true }).click();
const validation = page.locator('.app-notice-stack').getByRole('alert').filter({ hasText: /contextWindow/ });
await validation.waitFor();
await dialog().getByRole('button', { name: '保存', exact: true }).focus();
await page.keyboard.press('Tab');
assert.equal(await validation.locator('.action-feedback-text').evaluate(el => el === document.activeElement), true);
await page.keyboard.press('Tab');
assert.equal(await validation.getByRole('button').evaluate(el => el === document.activeElement), true);
await page.keyboard.press('Enter');
await validation.waitFor({ state: 'detached' });
assert.equal(await dialog().getByRole('button', { name: '保存', exact: true }).evaluate(el => el === document.activeElement), true);
const invalidWrites = await page.evaluate(() => window.fixtureCalls.filter(c => c.cmd === 'save_deepseek_harness_model_catalog_editor').length);
await page.keyboard.press('Enter');
await validation.waitFor();
assert.equal(await page.evaluate(() => window.fixtureCalls.filter(c => c.cmd === 'save_deepseek_harness_model_catalog_editor').length), invalidWrites);
await dialog().getByRole('spinbutton', { name: '上下文窗口(tokens)' }).fill('64000');
await page.evaluate(() => window.fixtureFailHarnessSave = true);
await dialog().getByRole('button', { name: '保存', exact: true }).click();
await page.locator('.app-notice-stack').getByRole('alert').filter({ hasText: /模拟模型配置保存失败/ }).waitFor();
assert.equal(await dialog().getByRole('spinbutton', { name: '上下文窗口(tokens)' }).inputValue(), '64000');
await page.evaluate(() => { window.fixtureFailHarnessSave = false; window.fixtureStaleHarnessSave = true; });
await dialog().getByRole('button', { name: '保存', exact: true }).click();
await page.locator('.app-notice-stack').getByRole('alert').filter({ hasText: /模型列表或配置已变化/ }).waitFor();
await page.evaluate(() => window.fixtureStaleHarnessSave = false);
await page.keyboard.press('Escape');
const confirmation = page.getByRole('alertdialog');
await confirmation.waitFor();
const nestedNotice = confirmation.locator('.app-notice-stack');
await nestedNotice.waitFor();
const nestedBounds = await nestedNotice.boundingBox();
const viewport = page.viewportSize();
assert.ok(Math.abs(viewport.width - nestedBounds.x - nestedBounds.width - 24) < 1, 'Nested modal notices stay at the viewport right edge');
assert.ok(nestedBounds.y >= 0 && nestedBounds.y + nestedBounds.height <= viewport.height, 'Nested notices fit the viewport');
const discard = confirmation.getByRole('button', { name: '放弃修改', exact: true });
await discard.focus();
await page.keyboard.press('Tab');
assert.equal(await nestedNotice.locator('.action-feedback-text').evaluate(el => el === document.activeElement), true);
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
assert.equal(await confirmation.getByRole('button', { name: '继续编辑', exact: true }).evaluate(el => el === document.activeElement), true, 'Notification remains within the nested focus trap');
await page.getByRole('button', { name: '继续编辑', exact: true }).click();
await save();
assert.equal(await context().inputValue(), '64000');
await dialog().getByRole('button', { name: '恢复默认上下文', exact: true }).click();
assert.equal(await context().inputValue(), '512000');
await save();
}
const screenshots = path.join(os.tmpdir(), 'cpa-harness-catalog-ui');
fs.mkdirSync(screenshots, { recursive: true });
for (const width of [1280, 540]) for (const theme of ['light', 'dark']) {
await page.setViewportSize({ width, height: 900 });
await open(`theme=${theme}`);
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false);
await page.screenshot({ path: path.join(screenshots, `${width}-${theme}-model.png`) });
await dialog().getByRole('button', { name: '供应商设置', exact: true }).click();
assert.equal(await page.evaluate(() => document.querySelector('.codex-catalog-editor').scrollWidth > document.querySelector('.codex-catalog-editor').clientWidth), false);
await page.screenshot({ path: path.join(screenshots, `${width}-${theme}-provider.png`) });
}
assert.deepEqual(errors, []);
console.log('PASS: default model preserved, catalog overrides, API reset, reasoning, validation, retry, stale save, responsive themes');
console.log(screenshots);
} finally { await browser.close(); }
})().catch(e => { console.error(e); process.exit(1); });