Files
EasyCLIProxyAPI/tests/api-provider-dialog-layout-ui.cjs
T
lzt404 d29a1af69b feat: add plugin management and redesign dashboard and usage analytics
- Add plugin store, version selection, configuration, and OAuth login
- Support Muse (Meta) OAuth device authorization
- Redesign home dashboard and add batch model health checks
- Support per-credential provider editing, removal, and reordering
- Improve usage charts and track actual response models
- Preserve installed plugins and runtime data during portable rebuilds
- Update translations, documentation, and related tests
2026-10-04 17:31:52 +08:00

140 lines
8.3 KiB
JavaScript

const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
const assert = require('node:assert/strict');
const path = require('node:path');
(async () => {
const { createServer } = await import('vite');
const react = (await import('@vitejs/plugin-react')).default;
const server = await createServer({
configFile: false,
root: path.resolve(__dirname, '..'),
plugins: [react()],
optimizeDeps: { entries: ['tests/fixtures/provider-duplicates.html'] },
logLevel: 'error',
server: { host: '127.0.0.1', port: 1421, strictPort: false, watch: null },
});
let browser;
try {
await server.listen();
const base = `http://127.0.0.1:${server.httpServer.address().port}`;
await (await fetch(base)).text();
const channel = process.env.PLAYWRIGHT_CHANNEL || (process.platform === 'win32' ? 'msedge' : undefined);
browser = await chromium.launch({ channel, headless: true, args: ['--no-proxy-server'] });
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
page.setDefaultTimeout(10000);
const errors = [];
page.on('pageerror', (error) => errors.push(String(error)));
await page.route('**/*', (route) => route.request().url().startsWith(`${base}/`) ? route.continue() : route.abort());
await page.goto(`${base}/tests/fixtures/provider-duplicates.html`, { waitUntil: 'domcontentloaded' });
await page.locator('.provider-category-panel button').filter({ hasText: 'Claude' }).click();
await page.locator('.real-provider-row').waitFor();
await page.evaluate(() => {
window.providerFixture.records[0].models = Array.from({ length: 30 }, (_, index) => ({ name: `model-${index}` }));
});
await page.getByRole('button', { name: 'Refresh', exact: true }).click();
await page.locator('.real-provider-row').getByRole('button', { name: 'Edit', exact: true }).click();
const form = page.getByRole('dialog', { name: 'Edit API Key', exact: true });
assert.equal(await form.locator('.model-config-entry').count(), 30);
assert.equal(await form.getByLabel('API Key', { exact: true }).count(), 1);
assert.equal(await form.locator('.provider-group-key').count(), 0);
assert.equal(await form.getByText(/shared settings|inherit shared|One group connects/i).count(), 0);
assert.equal(await form.getByLabel('Group Name', { exact: true }).count(), 0);
assert.equal(await form.getByLabel('Key for Model Discovery', { exact: true }).count(), 0);
// Older WebKit makes an inline-size query container a fixed-position containing block.
// Explicit layout containment reproduces that behavior on current browser engines.
await page.addStyleTag({ content: '.content { contain: layout; min-height: 1400px; }' });
const assertViewportOverlay = async (selector) => {
const bounds = await page.locator(selector).evaluate((element) => {
const rect = element.getBoundingClientRect();
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
});
const viewport = page.viewportSize();
assert.deepEqual(bounds, { x: 0, y: 0, ...viewport }, `${selector} must cover the viewport independently of page containment`);
};
const assertDialogControls = async (dialog) => {
const bounds = await dialog.boundingBox();
const viewport = page.viewportSize();
assert.ok(bounds.x >= 0 && bounds.y >= 0 && bounds.x + bounds.width <= viewport.width + 1
&& bounds.y + bounds.height <= viewport.height + 1, 'Dialog must fit inside the window');
for (const button of ['Close', 'Cancel', 'Save']) {
const control = dialog.getByRole('button', { name: button, exact: true });
const visible = await control.evaluate((element) => {
const rect = element.getBoundingClientRect();
return rect.top >= 0 && rect.bottom <= innerHeight
&& element.contains(document.elementFromPoint(rect.x + rect.width / 2, rect.y + rect.height / 2));
});
assert.equal(visible, true, `${button} must remain visible and unobstructed`);
}
const dimensions = await dialog.evaluate((element) => ({ client: element.clientWidth, scroll: element.scrollWidth }));
assert.ok(dimensions.scroll <= dimensions.client + 1, 'Dialog must not overflow horizontally');
};
for (const viewport of [{ width: 1280, height: 800 }, { width: 1024, height: 600 }, { width: 640, height: 600 }]) {
await page.setViewportSize(viewport);
await assertViewportOverlay('.config-dialog-backdrop');
await assertDialogControls(form);
await form.getByLabel('Remark', { exact: true }).fill('Small-window edit');
await form.locator('.provider-advanced-settings > summary').click();
const headers = form.getByRole('textbox', { name: /Custom Headers/ });
await headers.fill('X-Layout-Test: reachable');
await form.getByRole('combobox', { name: 'Disable Cooldown', exact: true }).selectOption('false');
await form.getByRole('combobox', { name: 'Cache User ID', exact: true }).selectOption('false');
await assertDialogControls(form);
const scroll = await form.evaluate((element) => ({ top: element.scrollTop, max: element.scrollHeight - element.clientHeight }));
assert.ok(scroll.top > 0 && scroll.max > 0, 'Advanced settings must be reachable by scrolling inside the dialog');
await form.locator('.provider-advanced-settings > summary').click();
}
await form.getByRole('button', { name: 'Fetch Models', exact: true }).click();
const models = page.getByRole('dialog', { name: 'Select Models', exact: true });
await models.waitFor();
await assertViewportOverlay('.model-discovery-backdrop');
for (let index = 0; index < 5; index += 1) {
await page.keyboard.press('Tab');
assert.equal(await models.evaluate((element) => element.contains(document.activeElement)), true);
}
await page.keyboard.press('Escape');
await models.waitFor({ state: 'detached' });
assert.equal(await form.getByRole('button', { name: 'Fetch Models', exact: true }).evaluate((element) => element === document.activeElement), true);
await assertDialogControls(form);
await form.getByRole('button', { name: 'Save', exact: true }).click();
await form.waitFor({ state: 'detached' });
const record = await page.evaluate(() => window.providerFixture.records[0]);
assert.equal(record.keys[0].headers['X-Layout-Test'], 'reachable');
assert.equal(record.models.length, 30);
assert.equal(record.keys[0]['disable-cooling'], false);
assert.equal(record.keys[0].cloak['cache-user-id'], false);
assert.equal(await page.evaluate(() => document.body.style.overflow), '');
await page.locator('.real-provider-row').getByRole('button', { name: 'Edit', exact: true }).click();
await form.locator('.provider-advanced-settings > summary').click();
for (const name of ['Disable Cooldown', 'Cache User ID']) {
const control = form.getByRole('combobox', { name, exact: true });
assert.equal(await control.inputValue(), 'false');
await control.selectOption('');
}
await form.getByRole('button', { name: 'Save', exact: true }).click();
await form.waitFor({ state: 'detached' });
const inherited = await page.evaluate(() => window.providerFixture.records[0]);
assert.equal(inherited.keys[0]['disable-cooling'], undefined);
assert.equal(inherited.keys[0].cloak?.['cache-user-id'], undefined);
await page.locator('.provider-category-panel button').filter({ hasText: 'OpenAI' }).click();
await page.getByRole('button', { name: 'Add Service', exact: true }).click();
const add = page.getByRole('dialog', { name: 'Add Service', exact: true });
await assertViewportOverlay('.config-dialog-backdrop');
await assertDialogControls(add);
await add.locator('.provider-advanced-settings > summary').click();
assert.equal(await add.getByLabel('Test Model', { exact: true }).count(), 0);
await add.getByRole('button', { name: 'Cancel', exact: true }).click();
await add.waitFor({ state: 'detached' });
assert.deepEqual(errors, []);
console.log('PASS: #321 provider dialogs fit small windows, escape page containment, scroll long forms, preserve nested focus, and save edits.');
} finally {
if (browser) await browser.close();
await server.close();
}
})().catch((error) => { console.error(error); process.exitCode = 1; });