Files
EasyCLIProxyAPI/tests/api-access-layout-ui.cjs
T
lzt404 5a5eab96e2 fix(ui): improve responsive layouts and refine API access page
- Fix overflow, clipped controls, and toolbar overlap in narrow windows
- Improve responsive layouts across usage, OAuth, agents, settings, and dialogs
- Unify API category navigation and compact provider rows and actions
- Preserve inherited provider priorities instead of displaying zero
- Add responsive layout regression tests and dense provider fixtures
- Bump proxy core version from 8.0.6 to 8.0.10
2026-10-04 17:31:52 +08:00

139 lines
8.8 KiB
JavaScript

const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
const assert = require('node:assert/strict');
const path = require('node:path');
const widths = [1440, 980, 854, 640];
const locales = ['zh-CN', 'en'];
(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-groups.html'] },
logLevel: 'error',
server: { host: '127.0.0.1', port: 0, watch: null },
});
let browser;
try {
await server.listen();
const base = `http://127.0.0.1:${server.httpServer.address().port}`;
const channel = process.env.PLAYWRIGHT_CHANNEL || (process.platform === 'win32' ? 'msedge' : undefined);
browser = await chromium.launch({ channel, headless: true, args: ['--no-proxy-server'] });
const errors = [];
for (const locale of locales) for (const width of widths) {
const label = `${locale} ${width}x600`;
const page = await browser.newPage({ viewport: { width, height: 600 } });
page.setDefaultTimeout(10000);
page.on('pageerror', error => errors.push(`${label}: ${error}`));
await page.route('**/*', route => route.request().url().startsWith(`${base}/`) ? route.continue() : route.abort());
await page.goto(`${base}/tests/fixtures/provider-groups.html?layout=dense&locale=${locale}`, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => document.querySelectorAll('.real-provider-row').length === 4
&& document.querySelectorAll('.provider-row-title > span:not(.state-pill)').length === 4);
await page.evaluate(async () => {
await document.fonts.ready;
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
});
const navigation = await page.locator('.provider-category-panel').evaluate(panel => {
const rect = panel.getBoundingClientRect();
const buttons = [...panel.querySelectorAll('button')];
return {
count: buttons.length,
rows: new Set(buttons.map(button => Math.round(button.getBoundingClientRect().top))).size,
allVisible: buttons.every(button => {
const item = button.getBoundingClientRect();
return item.width > 0 && item.height > 0 && item.left >= rect.left - 1 && item.right <= rect.right + 1
&& item.top >= rect.top - 1 && item.bottom <= rect.bottom + 1 && item.right <= innerWidth + 1;
}),
labelsFit: buttons.every(button => {
const label = button.querySelector('span');
const range = document.createRange();
range.selectNodeContents(label);
const text = range.getBoundingClientRect();
const bounds = label.getBoundingClientRect();
return text.left >= bounds.left - 1 && text.right <= bounds.right + 1
&& text.top >= bounds.top - 1 && text.bottom <= bounds.bottom + 1;
}),
bottom: rect.bottom,
listTop: document.querySelector('.provider-resource-panel').getBoundingClientRect().top,
};
});
assert.equal(navigation.count, 5, `${label}: every provider category remains available`);
assert.equal(navigation.rows, 1, `${label}: categories retain a single top row`);
assert.equal(navigation.allVisible, true, `${label}: all category controls fit without hidden columns`);
assert.equal(navigation.labelsFit, true, `${label}: full category names stay readable`);
assert.ok(navigation.bottom <= navigation.listTop + 1, `${label}: category navigation stays above the resource list`);
const overflow = await page.evaluate(() => [document.documentElement, document.body,
document.querySelector('.content'), document.querySelector('.api-access-page')]
.filter(element => element.scrollWidth > element.clientWidth + 1)
.map(element => element.className || element.tagName));
assert.deepEqual(overflow, [], `${label}: dense API content must not overflow the page horizontally`);
const groups = await page.evaluate(() => window.groupFixture.groups);
const rows = page.locator('.real-provider-row');
for (const group of groups) {
const row = rows.filter({ has: page.locator('.provider-row-title strong', { hasText: group.name }) });
const meta = row.locator('.provider-row-meta');
assert.equal(await row.count(), 1, `${label}: each group has exactly one row`);
assert.equal(await meta.isVisible(), true, `${label} ${group.name}: metadata stays visible`);
const metaText = await meta.innerText();
const expectedModels = locale === 'en' ? `${group.models.length} models` : `模型 ${group.models.length} 个`;
if (group.models.length) assert.ok(metaText.includes(expectedModels), `${label} ${group.name}: model count is preserved`);
const priorityLabel = locale === 'en' ? 'Priority' : '优先级';
if (group.priority === null) assert.ok(!metaText.includes(priorityLabel), `${label}: inherited priority is not displayed as zero`);
else assert.ok(metaText.includes(`${priorityLabel} ${group.priority}`), `${label} ${group.name}: priority, including zero, is preserved`);
if (group.keys.length > 1) {
const keyCount = locale === 'en' ? `${group.keys.length} keys total` : `共 ${group.keys.length} 个密钥`;
assert.ok(metaText.includes(keyCount), `${label} ${group.name}: multiple keys stay grouped and counted`);
}
assert.equal(await row.locator('.provider-row-url').getAttribute('title'), group['base-url'], `${label}: full URL remains available`);
const remark = row.locator('.provider-row-title > span:not(.state-pill)');
assert.equal(await remark.getAttribute('title'), await remark.textContent(), `${label}: the full remark remains available when shortened`);
const layout = await row.evaluate(element => {
const bounds = element.getBoundingClientRect();
const main = element.querySelector('.provider-row-main').getBoundingClientRect();
const actions = element.querySelector('.provider-row-actions').getBoundingClientRect();
const controls = [...element.querySelectorAll('.provider-row-actions > button, .provider-enabled-control')];
const rects = controls.map(control => control.getBoundingClientRect());
const overlap = (a, b) => Math.min(a.right, b.right) - Math.max(a.left, b.left) > 1
&& Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top) > 1;
return {
mainOverlapsActions: overlap(main, actions),
controlsContained: rects.every(rect => rect.width > 0 && rect.height > 0 && rect.left >= bounds.left - 1
&& rect.right <= bounds.right + 1 && rect.top >= bounds.top - 1 && rect.bottom <= bounds.bottom + 1),
controlsOverlap: rects.some((rect, index) => rects.slice(index + 1).some(next => overlap(rect, next))),
};
});
assert.equal(layout.mainOverlapsActions, false, `${label} ${group.name}: details and actions do not overlap`);
assert.equal(layout.controlsContained, true, `${label} ${group.name}: every action fits its own row`);
assert.equal(layout.controlsOverlap, false, `${label} ${group.name}: action targets remain separate`);
for (const control of await row.locator('.provider-row-actions > button, .provider-enabled-control, .provider-drag-handle').all()) {
await control.scrollIntoViewIfNeeded();
assert.equal(await control.evaluate(element => {
const rect = element.getBoundingClientRect();
const hit = document.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2);
return rect.top >= -1 && rect.bottom <= innerHeight + 1 && !!hit && element.contains(hit);
}), true, `${label} ${group.name}: actions and drag handle remain reachable by scrolling`);
}
}
const partial = rows.filter({ hasText: 'Mock Borealis partial' });
assert.equal(await partial.locator('.state-pill').textContent(), locale === 'en' ? 'Some keys disabled' : '部分密钥已停用');
assert.equal(await rows.filter({ hasText: 'Mock Dusk disabled' }).getByRole('checkbox').isChecked(), false);
assert.deepEqual(await page.evaluate(() => window.groupFixture.writes), [], 'Layout checks must not change provider records');
await page.close();
}
assert.deepEqual(errors, [], 'Dense API layouts must not produce runtime errors');
console.log('PASS: dense API groups retain navigation, metadata and reachable actions at four widths in Chinese and English.');
} finally {
if (browser) await browser.close();
await server.close();
}
})().catch(error => { console.error(error); process.exitCode = 1; });