Files
EasyCLIProxyAPI/tests/model-alias-editor-ui.cjs
T
lzt404 36d3181475 fix(aliases): prevent stale edits and preserve payload rules
- Validate configuration revisions and reject ambiguous model mappings
- Preserve raw overrides, conditions, and unchanged settings
- Serialize saves and restore configuration after write failures
- Add regression coverage and remove comments from affected files
2026-09-11 16:25:14 +08:00

92 lines
5.8 KiB
JavaScript

const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
const assert = require('node:assert/strict');
const { mkdirSync } = require('node:fs');
(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('http://127.0.0.1:1423/')
? route.continue() : route.abort());
const errors = [];
page.on('pageerror', error => errors.push(String(error)));
await page.goto('http://127.0.0.1:1423/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 dialog.getByRole('alert').waitFor();
assert.match(await dialog.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();
await dialog.getByRole('alert').waitFor();
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, inline errors, retry, rename, source switch, create, narrow dialog');
} finally {
await browser.close();
}
})().catch(error => { console.error(error); process.exitCode = 1; });