Files
EasyCLIProxyAPI/tests/auth-file-cooldown-ui.cjs
lzt404 fab5159cd4 fix(ui): compact credential cards and stop overlapping content
Replace the wide credential table with a quota-style card grid so account, plan, status, requests, usage, and quota stay readable without colliding.
2026-10-08 18:36:58 +08:00

216 lines
16 KiB
JavaScript

const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
const assert = require('node:assert/strict');
const path = require('node:path');
const fs = require('node:fs/promises');
(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/auth-file-cooldown.html'] }, logLevel: 'error',
server: { host: '127.0.0.1', port: 0, watch: null } });
let browser;
const screenshotDir = path.resolve('bin-work/auth-file-cooldown-qa');
try {
await server.listen();
const base = `http://127.0.0.1:${server.httpServer.address().port}`;
browser = await chromium.launch({ channel: 'msedge', headless: true, args: ['--no-proxy-server'] });
const page = await browser.newPage({ viewport: { width: 1280, height: 900 } });
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.clock.install({ time: new Date('2026-10-03T04:00:00Z') });
const card = name => page.locator('.auth-file-card').filter({ has: page.locator('.auth-card-identity strong', { hasText: name }) });
const primary = () => card('01-primary.json');
const other = () => card('02-other.json');
const clear = target => target.getByRole('button', { name: '清除冷却', exact: true });
const confirmation = () => page.getByRole('alertdialog', { name: '清除凭证冷却?', exact: true });
const requests = () => page.evaluate(() => window.cooldownFixture.resetRequests);
const expandMetadata = async target => {
const button = target.locator('.auth-list-details-button');
if (await button.getAttribute('aria-expanded') !== 'true') await button.click();
};
const expandCooldown = async target => {
const details = target.locator('.auth-file-health-compact > details');
if (await details.getAttribute('open') === null) await details.locator('.auth-health-summary').click();
};
const assertMainRowHeight = async label => {
const heights = await page.locator('.auth-credential-row').evaluateAll(nodes => nodes.map(node => node.getBoundingClientRect().height));
assert.ok(heights.every(height => height >= 140 && height <= 420), `${label}: cooldown content does not stretch the credential card: ${JSON.stringify(heights)}`);
};
const open = async query => {
await page.goto(`${base}/tests/fixtures/auth-file-cooldown.html?${query || ''}`);
await primary().waitFor();
};
const releaseReset = () => page.evaluate(() => window.cooldownFixture.releaseReset());
const confirmReset = async target => {
await clear(target).click();
await confirmation().waitFor();
await confirmation().getByRole('button', { name: '清除冷却', exact: true }).click();
await page.waitForFunction(() => Boolean(window.cooldownFixture.releaseReset));
};
await fs.mkdir(screenshotDir, { recursive: true });
await open();
assert.equal(await primary().locator('.auth-file-health details').getAttribute('open'), null);
assert.equal(await primary().locator('.auth-list-details').isVisible(), false, 'File metadata starts independently collapsed');
assert.equal(await primary().locator('.auth-file-health-compact').isVisible(), true, 'Cooldown summary is visible before opening any details');
assert.equal(await primary().locator('.auth-health-body').isVisible(), false, 'Individual cooldown records start collapsed');
assert.ok((await primary().locator('.auth-health-heading').innerText()).includes('凭证及 2 个模型冷却'));
assert.equal(await primary().locator('.auth-credential-row > .auth-list-cell').count(), 7, 'Cooldown cards retain the complete seven-section overview');
assert.equal(await primary().locator('.auth-list-recent').isVisible(), true, 'Recent requests stay visible while diagnostics are collapsed');
assert.equal(await primary().locator('.auth-list-usage').isVisible(), true, 'Runtime counters stay visible while diagnostics are collapsed');
assert.equal(await primary().locator('.auth-list-icon-actions > button:visible').count(), 5, 'Credential actions stay directly available');
assert.equal(await clear(primary()).isVisible(), true, 'Reset is directly available beside the collapsed cooldown summary');
await assertMainRowHeight('Collapsed cooldown');
await primary().screenshot({ path: path.join(screenshotDir, 'cooldown-collapsed-light-desktop.png') });
for (const name of ['05-empty.json', '06-null.json', '07-absent.json', '08-no-index.json']) {
assert.equal(await clear(card(name)).count(), 0, `${name} must not offer an unsafe or inapplicable reset`);
}
assert.equal(await card('05-empty.json').locator('.auth-file-health-compact').count(), 0, 'An empty snapshot does not display a cooldown strip');
assert.equal(await card('06-null.json').locator('.auth-file-health-compact').isVisible(), true);
assert.ok((await card('06-null.json').locator('.auth-file-health-compact').innerText()).includes('冷却状态未知'), 'Null remains an explicit unknown state');
assert.equal(await card('07-absent.json').locator('.auth-file-health-compact').count(), 0, 'A missing snapshot never claims to know cooldown state');
await expandMetadata(card('07-absent.json'));
assert.ok((await card('07-absent.json').locator('.auth-list-details').innerText()).includes('当前内核未提供冷却详情'));
await primary().locator('.auth-health-summary').focus();
await page.keyboard.press('Enter');
assert.equal(await primary().locator('.auth-health-body').isVisible(), true, 'Enter opens cooldown records without opening metadata');
await page.keyboard.press('Space');
assert.equal(await primary().locator('.auth-health-body').isVisible(), false, 'Space closes native cooldown disclosure');
await expandCooldown(primary());
assert.equal(await primary().locator('.auth-list-details').isVisible(), false, 'Cooldown and metadata disclosures are independent');
await assertMainRowHeight('Expanded cooldown');
const details = primary().locator('.auth-health-body');
for (const text of ['整个凭证', 'gpt-fictional', 'claude-fictional', '额度或速率限制', '上游服务暂时异常', 'HTTP 429', 'HTTP 503', '退避等级 0']) {
assert.ok((await details.innerText()).includes(text), `Cooldown detail contains ${text}`);
}
assert.equal(await details.locator('time').count(), 4, 'Every cooldown and its observation timestamp are available');
const beforeTick = await primary().locator('.auth-health-countdown').innerText();
await page.clock.runFor(3100);
assert.notEqual(await primary().locator('.auth-health-countdown').innerText(), beforeTick, 'Countdown advances without a list reload');
assert.equal(await page.evaluate(() => window.cooldownFixture.reads), 1);
assert.ok((await card('04-expired-soon.json').innerText()).includes('冷却计时已到期'));
assert.equal(await clear(card('04-expired-soon.json')).isEnabled(), true, 'Elapsed records remain explicitly resettable');
await expandCooldown(card('03-disabled-runtime.json'));
assert.ok((await card('03-disabled-runtime.json').innerText()).includes('运行时'));
assert.ok((await card('03-disabled-runtime.json').locator('.auth-file-health').innerText()).includes('已停用'));
assert.equal(await clear(card('03-disabled-runtime.json')).isEnabled(), true, 'Runtime and disabled credentials can clear existing cooldown records');
await page.screenshot({ path: path.join(screenshotDir, 'details-light-desktop.png'), fullPage: true });
await clear(primary()).click();
await confirmation().waitFor();
assert.ok((await confirmation().innerText()).includes('01-primary.json'));
assert.deepEqual(await requests(), [], 'Opening confirmation does not submit a reset');
await confirmation().getByRole('button', { name: '取消', exact: true }).click();
assert.deepEqual(await requests(), [], 'Cancel leaves the backend untouched');
assert.equal(await primary().locator('.auth-health-records li').count(), 3);
await expandCooldown(other());
const otherDeadline = await other().locator('.auth-health-records time').getAttribute('datetime');
const otherBefore = await other().locator('.auth-health-countdown').innerText();
await confirmReset(primary());
const pending = primary().locator('.auth-file-health button');
assert.equal(await pending.isDisabled(), true, 'Pending reset prevents duplicate submission');
await pending.evaluate(button => button.click());
const sent = await requests();
assert.equal(sent.length, 1);
assert.equal(sent[0].method, 'POST');
assert.equal(sent[0].path, '/routing/cooldown/reset');
assert.deepEqual(sent[0].body, { auth_index: 'auth-primary-123' }, 'Only the selected auth_index is sent; no all/model scope');
assert.equal(await primary().locator('.auth-health-records li').count(), 3, 'Pending request retains existing cooldown details');
await page.clock.runFor(3000);
await releaseReset();
await page.waitForFunction(() => window.cooldownFixture.reads === 2);
await primary().locator('.auth-file-health').waitFor({ state: 'detached' });
assert.equal(await other().locator('.auth-health-records li').count(), 1);
assert.equal(await other().locator('.auth-health-records time').getAttribute('datetime'), otherDeadline);
assert.notEqual(await other().locator('.auth-health-countdown').innerText(), otherBefore, 'A sibling timer continues instead of restarting or clearing');
assert.ok((await page.locator('.app-notice-stack').innerText()).includes('01-primary.json'), 'Success identifies the cleared credential');
await open('resetError=1');
await expandCooldown(primary());
await confirmReset(primary());
await releaseReset();
await page.getByText(/Fixture cooldown reset failed/).waitFor();
assert.equal(await primary().locator('.auth-health-records li').count(), 3, 'A rejected reset retains the displayed records');
assert.equal(await clear(primary()).isEnabled(), true, 'Failure allows a deliberate retry');
assert.equal(await page.evaluate(() => window.cooldownFixture.reads), 1, 'Failure does not pretend a refreshed successful state');
await page.screenshot({ path: path.join(screenshotDir, 'reset-failure.png'), fullPage: true });
await open();
await confirmReset(card('03-disabled-runtime.json'));
await releaseReset();
await page.waitForFunction(() => window.cooldownFixture.reads === 2);
await clear(card('03-disabled-runtime.json')).waitFor({ state: 'detached' });
assert.ok((await card('03-disabled-runtime.json').innerText()).includes('运行时'));
assert.ok((await card('03-disabled-runtime.json').locator('.auth-list-status').innerText()).includes('已停用'), 'Clearing cooldown preserves disabled state in the main row');
assert.equal(await card('03-disabled-runtime.json').locator('.auth-file-health-compact').count(), 0, 'Successful reset removes only the cooldown strip');
await open();
await confirmReset(primary());
await page.evaluate(() => { window.cooldownFixture.holdNextRead = true; });
await page.locator('.auth-files-heading').getByRole('button', { name: '刷新列表', exact: true }).click();
await page.waitForFunction(() => Boolean(window.cooldownFixture.releaseRead));
await releaseReset();
await page.waitForFunction(() => window.cooldownFixture.reads === 3 && window.cooldownFixture.completedReads === 2);
await primary().waitFor();
assert.equal(await primary().locator('.auth-file-health').count(), 0);
await page.evaluate(() => window.cooldownFixture.releaseRead());
await page.waitForFunction(() => window.cooldownFixture.completedReads === 3);
await page.clock.runFor(100);
assert.equal(await primary().locator('.auth-file-health').count(), 0, 'An older list response cannot resurrect cleared cooldowns');
for (const theme of ['light', 'dark']) {
for (const width of [1280, 390]) {
await page.setViewportSize({ width, height: 900 });
await open(`theme=${theme}`);
assert.equal(await clear(primary()).isVisible(), true);
assert.equal(await primary().locator('.auth-list-details').isVisible(), false);
if (width >= 1280) await assertMainRowHeight(`${theme} ${width} collapsed`);
await primary().screenshot({ path: path.join(screenshotDir, `cooldown-collapsed-${theme}-${width}.png`) });
await expandCooldown(primary());
await expandCooldown(other());
const sizes = await page.locator('.auth-file-card, .auth-file-health:visible, .auth-file-table-scroll').evaluateAll(nodes => nodes.map(node => ({
className: node.className, width: node.clientWidth, scroll: node.scrollWidth,
})));
assert.ok(sizes.every(size => size.scroll <= size.width + 1), `${theme} ${width}: ${JSON.stringify(sizes)}`);
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1), true, `No page overflow at ${theme} ${width}`);
await page.screenshot({ path: path.join(screenshotDir, `details-${theme}-${width}.png`), fullPage: true });
await primary().screenshot({ path: path.join(screenshotDir, `card-${theme}-${width}.png`) });
await expandMetadata(primary());
const summary = primary().locator('.auth-file-requests-summary');
assert.equal(await summary.isVisible(), true, 'File details expose the separate request summary');
assert.equal(await summary.locator('.auth-file-request-block:visible').count(), 20);
assert.equal(await primary().locator('.auth-list-recent .auth-file-request-block:visible').count(), 20, 'The main request timeline remains intact');
assert.ok((await summary.locator('.auth-file-requests-heading').innerText()).includes('成功 20'));
assert.ok((await summary.locator('.auth-file-requests-heading').innerText()).includes('失败 2'));
assert.equal(await summary.locator('.auth-file-requests-rate').innerText(), '95.2%', 'Summary rate uses the recent window while counts retain the runtime totals');
const block = summary.locator('.auth-file-request-block').nth(18);
await block.focus();
assert.ok((await summary.getByRole('tooltip').innerText()).includes('失败 1'));
await page.keyboard.press('ArrowRight');
assert.equal(await summary.locator('.auth-file-request-block').nth(19).evaluate(node => node === document.activeElement), true);
await page.keyboard.press('ArrowLeft');
assert.equal(await block.evaluate(node => node === document.activeElement), true);
await page.keyboard.press('Escape');
assert.equal(await summary.getByRole('tooltip').count(), 0);
await block.blur();
assert.equal(await summary.evaluate(node => node.scrollWidth <= node.clientWidth + 1), true, 'Request summary fits desktop and mobile');
if (width >= 1280) await assertMainRowHeight(`${theme} ${width} expanded`);
await primary().screenshot({ path: path.join(screenshotDir, `cooldown-and-requests-${theme}-${width}.png`) });
await clear(primary()).click();
await confirmation().waitFor();
assert.equal(await confirmation().evaluate(node => node.scrollWidth <= node.clientWidth + 1), true, 'Confirmation fits viewport');
await page.screenshot({ path: path.join(screenshotDir, `confirmation-${theme}-${width}.png`), fullPage: true });
await confirmation().screenshot({ path: path.join(screenshotDir, `dialog-${theme}-${width}.png`) });
await confirmation().getByRole('button', { name: '取消', exact: true }).click();
}
}
assert.deepEqual(errors, []);
console.log('PASS: directly visible cooldown summary/reset, independent keyboard disclosure, compact card height, cooldown models/reasons/countdowns, request summary counts/rate and keyboard tooltips, confirmation and cancel, single auth_index POST, pending duplicate protection, isolated success refresh, failure preservation, stale response protection, unknown/empty/missing-index guards, elapsed records, runtime/disabled state, light/dark desktop/mobile layout.');
console.log(`Screenshots: ${screenshotDir}`);
} finally { await browser?.close(); await server.close(); }
})().catch(error => { console.error(error); process.exitCode = 1; });