Files
EasyCLIProxyAPI/tests/model-alias-editor-ui.cjs
lzt404 245319be2d feat: redesign advanced settings and client navigation, fix config compatibility
- Reorganize settings cards and improve responsive layouts, save states, and navigation
- Move model aliases to a dedicated tab and improve search and keyboard interactions
- Add pagination to the agent client list and update localized management text
- Use public model IDs consistently for Codex selection and catalogs
- Fix v8 migration and preserve keys, providers, and aliases in mixed configurations
- Improve the stop-core button styling and expand UI and configuration regression tests
2026-10-04 17:31:52 +08:00

119 lines
7.8 KiB
JavaScript

const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
const assert = require('node:assert/strict');
const { mkdirSync } = require('node:fs');
const base = process.env.ALIAS_TEST_BASE_URL || 'http://127.0.0.1:1423';
(async () => {
const browser = await chromium.launch({ channel: 'msedge', headless: true });
try {
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
page.setDefaultTimeout(10000);
await page.route('**/*', route => route.request().url().startsWith(base + '/')
? route.continue() : route.abort());
const errors = [];
page.on('pageerror', error => errors.push(String(error)));
await page.goto(`${base}/tests/fixtures/model-alias-editor.html`, { waitUntil: 'domcontentloaded' });
const writes = () => page.evaluate(() => window.fixtureCalls.filter(call => call.cmd === 'create_thinking_alias'));
await page.getByRole('button', { name: 'Edit', exact: true }).click();
const dialog = page.getByRole('dialog');
await dialog.waitFor();
assert.equal(await dialog.locator('#thinking-model-search').inputValue(), 'upstream-gpt');
assert.equal(await dialog.locator('#thinking-alias-name').inputValue(), 'my-alias');
await page.keyboard.press('Escape');
assert.equal(await dialog.count(), 1);
assert.equal(await page.getByRole('listbox').count(), 0);
await dialog.getByRole('button', { name: 'Save', exact: true }).focus();
await page.keyboard.press('Tab');
assert.equal(await dialog.getByRole('button', { name: 'Close', exact: true }).evaluate(el => el === document.activeElement), true);
await page.keyboard.press('Shift+Tab');
assert.equal(await dialog.getByRole('button', { name: 'Save', exact: true }).evaluate(el => el === document.activeElement), true);
await dialog.getByRole('button', { name: 'Cancel', exact: true }).click();
assert.equal((await writes()).length, 0);
assert.equal(await page.getByRole('button', { name: 'Edit', exact: true }).evaluate(el => el === document.activeElement), true);
await page.getByRole('button', { name: 'Edit', exact: true }).click();
await dialog.locator('#thinking-alias-name').fill('renamed-alias');
await dialog.getByRole('button', { name: 'Low', exact: true }).click();
await page.evaluate(() => { window.fixtureFailSave = true; });
await dialog.getByRole('button', { name: 'Save', exact: true }).click();
await page.locator('.app-notice-stack').getByRole('alert').waitFor();
assert.match(await page.locator('.app-notice-stack').getByRole('alert').innerText(), /configuration changed/);
assert.equal(await dialog.locator('#thinking-alias-name').inputValue(), 'renamed-alias');
await page.evaluate(() => { window.fixtureFailSave = false; });
await dialog.getByRole('button', { name: 'Save', exact: true }).click();
await dialog.waitFor({ state: 'detached' });
const saved = (await writes()).at(-1).args;
assert.equal(saved.sourceId, 'alias-edit:my-alias');
assert.equal(saved.originalAlias, 'my-alias');
assert.equal(saved.expectedRevision, 'revision:my-alias');
assert.equal(saved.alias, 'renamed-alias');
assert.equal(saved.effort, 'low');
await page.getByText('Updated model alias renamed-alias', { exact: true }).waitFor();
await page.evaluate(() => { window.fixtureCurrentEffort = 'high'; });
await page.getByRole('button', { name: 'Edit', exact: true }).click();
assert.match(await dialog.getByRole('button', { name: 'High', exact: true }).getAttribute('class'), /active/);
await dialog.locator('#thinking-model-search').fill('public-gpt');
await page.getByRole('option').click();
assert.equal(await dialog.locator('#thinking-alias-name').inputValue(), 'renamed-alias');
await dialog.getByRole('button', { name: 'Save', exact: true }).click();
await dialog.waitFor({ state: 'detached' });
assert.equal((await writes()).at(-1).args.sourceId, 'openai-compatibility:0:0:provider-revision');
assert.equal((await writes()).at(-1).args.expectedRevision, 'revision:renamed-alias');
await page.setViewportSize({ width: 640, height: 600 });
await page.getByRole('button', { name: 'Create Alias', exact: true }).click();
await page.keyboard.press('Escape');
await dialog.getByRole('button', { name: 'Create Alias', exact: true }).click();
const validation = page.locator('.app-notice-stack').getByRole('alert');
await validation.waitFor();
assert.equal(await validation.evaluate(el => Boolean(el.closest('[role="dialog"]'))), true);
const create = dialog.getByRole('button', { name: 'Create Alias', exact: true });
const close = dialog.getByRole('button', { name: 'Close', exact: true });
const createCount = (await writes()).length;
await create.focus();
await page.keyboard.press('Tab');
assert.equal(await validation.locator('.action-feedback-text').evaluate(el => el === document.activeElement), true, 'Tab reaches validation text');
await page.keyboard.press('Tab');
assert.equal(await validation.getByRole('button').evaluate(el => el === document.activeElement), true, 'Tab reaches notification dismissal');
await page.keyboard.press('Tab');
assert.equal(await close.evaluate(el => el === document.activeElement), true, 'Tab wraps inside the dialog from the notification');
await page.keyboard.press('Shift+Tab');
assert.equal(await validation.getByRole('button').evaluate(el => el === document.activeElement), true, 'Reverse Tab reaches the notification');
await page.keyboard.press('Enter');
await validation.waitFor({ state: 'detached' });
assert.equal(await close.evaluate(el => el === document.activeElement), true, 'Keyboard dismissal restores focus in the dialog');
await create.click();
await validation.waitFor();
assert.match(await validation.innerText(), /Select an original model first/);
assert.equal((await writes()).length, createCount, 'Repeated invalid submission stays local and shows the same error again');
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Enter');
await validation.waitFor({ state: 'detached' });
assert.equal(await create.evaluate(el => el === document.activeElement), true, 'Dismissal returns to the submit button');
await dialog.locator('#thinking-model-search').fill('public-gpt');
await page.getByRole('option').click();
await dialog.locator('#thinking-alias-name').fill('new-alias');
const rect = await dialog.boundingBox();
assert.ok(rect.x >= 0 && rect.y >= 0 && rect.x + rect.width <= 640 && rect.y + rect.height <= 600);
const overlap = await dialog.locator('.thinking-alias-dialog-body').evaluate(body => {
const fields = Array.from(body.children);
return fields.some((field, index) => index > 0 && field.getBoundingClientRect().top < fields[index - 1].getBoundingClientRect().bottom - 1);
});
assert.equal(overlap, false, 'Narrow dialog fields must scroll instead of overlapping');
assert.equal(await dialog.locator('.thinking-fast-option').evaluate(el => getComputedStyle(el).display), 'flex');
mkdirSync('misc', { recursive: true });
await page.screenshot({ path: 'misc/model-alias-editor-narrow.png', fullPage: true });
await dialog.getByRole('button', { name: 'Create Alias', exact: true }).click();
await dialog.waitFor({ state: 'detached' });
assert.equal((await writes()).at(-1).args.originalAlias, undefined);
assert.equal((await writes()).at(-1).args.expectedRevision, undefined);
assert.equal((await writes()).at(-1).args.alias, 'new-alias');
assert.deepEqual(errors, []);
console.log('PASS: remapped source, cancel, focus trap, Escape, floating errors, retry, rename, source switch, create, narrow dialog');
} finally {
await browser.close();
}
})().catch(error => { console.error(error); process.exitCode = 1; });