- Adopt a compact seven-column credential layout with visible quotas, request stats, and actions - Display cooldown scope, countdowns, and model details with accessible reset controls - Fix cooldown misassignment when merging same-named credentials from different accounts - Add client visibility management with search, auto-detection, and persisted preferences - Integrate plugin OAuth providers with deduplication and change synchronization - Refine dashboard typography and spacing, and streamline usage history controls - Add translations, unit tests, and responsive interaction coverage
226 lines
18 KiB
JavaScript
226 lines
18 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-list.html'] }, logLevel: 'error',
|
|
server: { host: '127.0.0.1', port: 0, watch: null } });
|
|
let browser;
|
|
const screenshotDir = path.resolve('bin-work/auth-file-list-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: 1800, height: 1000 }, timezoneId: 'Asia/Shanghai' });
|
|
page.setDefaultTimeout(10000);
|
|
const errors = [], externalRequests = [];
|
|
page.on('pageerror', error => errors.push(String(error)));
|
|
await page.route('**/*', route => {
|
|
if (route.request().url().startsWith(`${base}/`)) return route.continue();
|
|
externalRequests.push(route.request().url());
|
|
return route.abort();
|
|
});
|
|
await page.clock.install({ time: new Date('2026-10-03T04:00:00Z') });
|
|
const cards = () => page.locator('.auth-file-card');
|
|
const card = number => cards().filter({ has: page.locator(`.auth-card-filename[title="${String(number).padStart(2, '0')}-account.json"]`) });
|
|
const pagination = () => page.locator('.auth-list-pagination');
|
|
const next = () => pagination().getByRole('button', { name: '下一页', exact: true });
|
|
const previous = () => pagination().getByRole('button', { name: '上一页', exact: true });
|
|
const expand = async target => {
|
|
const button = target.locator('.auth-list-details-button');
|
|
if (await button.getAttribute('aria-expanded') !== 'true') await button.click();
|
|
};
|
|
const open = async (query = '') => {
|
|
await page.goto(`${base}/tests/fixtures/auth-file-list.html?${query}`);
|
|
await card(1).waitFor();
|
|
await page.waitForFunction(() => window.authFileListFixture.reads === 1);
|
|
};
|
|
const assertFits = async label => {
|
|
const dimensions = await page.locator('.auth-file-card, .auth-file-table-scroll').evaluateAll(nodes => nodes.map(node => ({
|
|
className: node.className, width: node.clientWidth, scroll: node.scrollWidth, overflowX: getComputedStyle(node).overflowX,
|
|
})));
|
|
assert.ok(dimensions.every(size => size.scroll <= size.width + 1 || (
|
|
size.className === 'auth-file-table-scroll' && size.width > 700 && size.width < 906 && size.overflowX === 'auto'
|
|
)), `${label}: only intermediate widths may scroll inside the table; cards must fit ${JSON.stringify(dimensions)}`);
|
|
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1), true, `${label}: page overflow`);
|
|
};
|
|
const assertMainInformation = async (label, width) => {
|
|
for (const selector of ['.auth-card-filename', '.auth-list-plan', '.auth-list-status', '.auth-list-recent', '.auth-list-usage', '.auth-list-quota', '.auth-card-actions']) {
|
|
assert.equal(await card(1).locator(selector).isVisible(), true, `${label}: ${selector} is visible without opening details`);
|
|
}
|
|
assert.equal(await card(1).locator('.auth-list-recent .auth-file-request-block:visible').count(), 20, `${label}: the request timeline remains visible`);
|
|
assert.equal(await card(1).locator('.auth-file-usage-metric:visible').count(), 4, `${label}: all four runtime counters remain visible`);
|
|
assert.equal(await card(1).locator('.auth-list-icon-actions > button:visible').count(), 5, `${label}: all five actions are directly available`);
|
|
assert.equal(await card(1).getByRole('switch').isVisible(), true);
|
|
assert.equal(await card(1).locator('.auth-list-details-button').isVisible(), true);
|
|
assert.equal(await card(1).locator('.auth-list-details').isVisible(), false, `${label}: file metadata and the expanded request summary start collapsed`);
|
|
if (width >= 1280) {
|
|
const heights = await page.locator('.auth-credential-row').evaluateAll(nodes => nodes.map(node => node.getBoundingClientRect().height));
|
|
assert.ok(heights.every(height => height >= 88 && height <= 120), `${label}: dense desktop rows stay near 104px, including quota footnotes: ${JSON.stringify(heights)}`);
|
|
const cells = await card(1).locator('.auth-credential-row > .auth-list-cell').evaluateAll(nodes => nodes.map(node => {
|
|
const bounds = node.getBoundingClientRect(); return { left: bounds.left, right: bounds.right };
|
|
}));
|
|
assert.equal(cells.length, 7, `${label}: desktop has seven information columns`);
|
|
assert.ok(cells.slice(1).every((cell, index) => cell.left >= cells[index].right - 1), `${label}: all seven columns remain in one row`);
|
|
}
|
|
};
|
|
await fs.mkdir(screenshotDir, { recursive: true });
|
|
await open();
|
|
assert.equal(await cards().count(), 10, 'Default page contains ten of twelve credentials');
|
|
assert.equal(await previous().isDisabled(), true);
|
|
assert.ok((await pagination().innerText()).includes('1 / 2'));
|
|
assert.equal(await page.locator('.auth-file-list-head > span').count(), 7);
|
|
await assertMainInformation('initial desktop', 1800);
|
|
assert.equal(await card(2).locator('.auth-file-health-compact').isVisible(), true, 'Cooldown summary is directly visible below the main row');
|
|
assert.equal(await card(2).locator('.auth-health-body').isVisible(), false, 'Individual cooldown records start collapsed');
|
|
assert.equal(await card(2).getByRole('button', { name: '清除冷却', exact: true }).isVisible(), true, 'Clear cooldown is available without opening file details');
|
|
assert.equal(await card(1).getByRole('button', { name: '删除', exact: true }).isVisible(), true, 'Delete is directly available alongside the other actions');
|
|
assert.equal(await card(4).locator('.credential-quota-row:visible').count(), 2, 'The first two quota windows are always shown');
|
|
assert.equal(await card(2).locator('.credential-quota-row.low .credential-quota-label strong').first().innerText(), '0%', 'Exhausted quota stays explicitly zero');
|
|
assert.equal(await card(2).getByRole('progressbar').first().getAttribute('aria-valuenow'), '0');
|
|
assert.equal(await card(1).getByRole('progressbar').first().getAttribute('aria-valuenow'), '72');
|
|
assert.equal(await card(1).getByRole('progressbar').nth(1).getAttribute('aria-valuenow'), '83');
|
|
assert.equal(await card(5).locator('.credential-quota-row.unknown [role="progressbar"]').count(), 0, 'Unknown quota is not reported as a zero-percent progress bar');
|
|
assert.equal(await card(5).locator('.credential-quota-label strong').innerText(), '—', 'Unknown quota retains its separate presentation');
|
|
const resetTimes = card(1).locator('.credential-quota-reset-time');
|
|
assert.equal(await resetTimes.count(), 2, 'Both quota reset times stay visible');
|
|
assert.ok((await resetTimes.first().innerText()).includes('10/03 15:00'), 'Five-hour window preserves its exact reset timestamp');
|
|
assert.ok((await resetTimes.nth(1).innerText()).includes('10/08 12:00'), 'Weekly window preserves its exact reset timestamp');
|
|
const resetTitle = await resetTimes.first().getAttribute('title');
|
|
assert.ok(resetTitle.startsWith(await resetTimes.first().innerText()) && resetTitle.includes('小时后'), 'Compact timestamps retain their exact time and relative countdown on hover');
|
|
await next().click();
|
|
await card(11).waitFor();
|
|
assert.equal(await cards().count(), 2);
|
|
assert.equal(await next().isDisabled(), true);
|
|
assert.ok((await pagination().innerText()).includes('2 / 2'));
|
|
|
|
await page.locator('.auth-files-toolbar input').fill('account');
|
|
await card(1).waitFor();
|
|
assert.equal(await cards().count(), 10, 'Search resets pagination even when all credentials match');
|
|
assert.equal(await previous().isDisabled(), true);
|
|
await next().click();
|
|
await page.locator('.auth-files-toolbar select').first().selectOption({ label: 'Codex' });
|
|
await card(1).waitFor();
|
|
assert.equal(await previous().isDisabled(), true, 'Provider filter resets to the first page');
|
|
await page.locator('.auth-files-toolbar select').first().selectOption('all');
|
|
await next().click();
|
|
await page.locator('.auth-files-toolbar select').last().selectOption('disabled');
|
|
await card(3).waitFor();
|
|
assert.equal(await cards().count(), 2, 'Disabled filter retains matching rows');
|
|
assert.equal(await previous().isDisabled(), true);
|
|
await page.locator('.auth-files-toolbar select').last().selectOption('all');
|
|
await pagination().locator('select').selectOption('20');
|
|
await card(12).waitFor();
|
|
assert.equal(await cards().count(), 12, 'Page size twenty shows all twelve credentials');
|
|
assert.equal(await next().isDisabled(), true);
|
|
|
|
const detailsButton = card(1).locator('.auth-list-details-button');
|
|
assert.equal(await card(1).locator('.auth-list-details').isVisible(), false);
|
|
await detailsButton.click();
|
|
assert.equal(await detailsButton.getAttribute('aria-expanded'), 'true');
|
|
assert.equal(await card(1).locator('.auth-list-details').isVisible(), true);
|
|
assert.ok((await card(1).locator('.auth-list-details').innerText()).includes('Fictional account note'));
|
|
const requestSummary = card(1).locator('.auth-file-requests-summary');
|
|
assert.equal(await requestSummary.isVisible(), true, 'File details include the additional compact request summary');
|
|
assert.equal(await requestSummary.locator('.auth-file-request-block:visible').count(), 20);
|
|
assert.ok((await requestSummary.locator('.auth-file-requests-heading').innerText()).includes('成功 1,200'));
|
|
assert.ok((await requestSummary.locator('.auth-file-requests-heading').innerText()).includes('失败 3'));
|
|
await requestSummary.locator('.auth-file-request-block').nth(18).focus();
|
|
assert.equal(await requestSummary.getByRole('tooltip').isVisible(), true, 'Expanded request summary retains keyboard tooltips');
|
|
await page.keyboard.press('Escape');
|
|
await requestSummary.locator('.auth-file-request-block').nth(18).blur();
|
|
await detailsButton.click();
|
|
assert.equal(await card(1).locator('.auth-list-details').isVisible(), false);
|
|
|
|
const extraQuota = card(4).locator('.auth-list-quota .credential-quota');
|
|
assert.equal(await extraQuota.getByText('Additional model B', { exact: true }).isVisible(), false);
|
|
await extraQuota.locator('.credential-quota-more > summary').click();
|
|
assert.equal(await extraQuota.getByText('Additional model B', { exact: true }).isVisible(), true, 'All additional quotas remain accessible');
|
|
assert.ok((await extraQuota.innerText()).includes('No quota limit supplied'));
|
|
assert.equal(await extraQuota.locator('.credential-quota-row:visible').count(), 4, 'Expanding quota details shows all four rows');
|
|
assert.equal(await card(4).locator('.auth-list-details').isVisible(), false, 'Additional quota rows open independently from health and metadata');
|
|
await extraQuota.locator('.credential-quota-more > summary').click();
|
|
assert.equal(await card(1).locator('.auth-list-recent .auth-file-request-block').count(), 20);
|
|
await card(1).locator('.auth-list-recent .auth-file-request-block').nth(18).focus();
|
|
assert.equal(await card(1).locator('.auth-list-recent').getByRole('tooltip').isVisible(), true, 'Recent request details are keyboard accessible');
|
|
await page.keyboard.press('Escape');
|
|
await card(1).locator('.auth-list-recent .auth-file-request-block').nth(18).blur();
|
|
assert.equal(await card(2).getByRole('button', { name: '清除冷却', exact: true }).isVisible(), true);
|
|
await card(2).locator('.auth-health-summary').click();
|
|
assert.ok((await card(2).locator('.auth-health-body').innerText()).includes('gpt-fictional-layout'), 'Model cooldown details remain available');
|
|
assert.equal(await card(2).locator('.auth-list-details').isVisible(), false, 'Cooldown expansion is independent of file metadata');
|
|
|
|
await card(1).getByRole('switch').click();
|
|
await page.waitForFunction(() => window.authFileListFixture.reads === 2);
|
|
assert.equal(await card(1).getByRole('switch').getAttribute('aria-checked'), 'false');
|
|
let updates = await page.evaluate(() => window.authFileListFixture.requests.filter(request => request.method === 'PATCH'));
|
|
assert.equal(updates.length, 1);
|
|
assert.equal(updates[0].path, '/credentials/status');
|
|
assert.deepEqual(updates[0].body, { name: '01-account.json', disabled: true });
|
|
await card(1).getByRole('switch').click();
|
|
await page.waitForFunction(() => window.authFileListFixture.reads === 3);
|
|
updates = await page.evaluate(() => window.authFileListFixture.requests.filter(request => request.method === 'PATCH'));
|
|
assert.deepEqual(updates[1].body, { name: '01-account.json', disabled: false });
|
|
assert.equal(await card(1).getByRole('switch').getAttribute('aria-checked'), 'true');
|
|
assert.equal(await card(3).getByRole('switch').getAttribute('aria-checked'), 'false', 'Sibling status stays unchanged');
|
|
|
|
await card(12).getByRole('button', { name: '删除', exact: true }).click();
|
|
const deleteDialog = page.getByRole('alertdialog', { name: '删除', exact: true });
|
|
await deleteDialog.waitFor();
|
|
assert.equal(await page.evaluate(() => window.authFileListFixture.requests.filter(request => request.method === 'DELETE').length), 0, 'The direct delete action still requires confirmation');
|
|
await deleteDialog.getByRole('button', { name: '取消', exact: true }).click();
|
|
assert.equal(await card(12).count(), 1, 'Cancel preserves the credential');
|
|
await card(12).getByRole('button', { name: '删除', exact: true }).click();
|
|
await deleteDialog.waitFor();
|
|
await deleteDialog.getByRole('button', { name: '删除', exact: true }).click();
|
|
await card(12).waitFor({ state: 'detached' });
|
|
assert.equal(await cards().count(), 11);
|
|
assert.deepEqual(await page.evaluate(() => window.authFileListFixture.requests.find(request => request.method === 'DELETE')?.query), { name: '12-account.json' });
|
|
assert.deepEqual(await page.evaluate(() => window.authFileListFixture.unhandled), []);
|
|
|
|
const cases = ['light', 'dark'].flatMap(theme => [1800, 1280, 1024, 390].flatMap(width => ['zh-CN', 'en'].map(locale => ({ theme, width, locale }))));
|
|
for (const { theme, width, locale } of cases) {
|
|
await page.setViewportSize({ width, height: 1000 });
|
|
await open(`theme=${theme}&locale=${locale}`);
|
|
await assertMainInformation(`${locale} ${theme} ${width}`, width);
|
|
await assertFits(`${locale} ${theme} ${width}`);
|
|
await page.screenshot({ path: path.join(screenshotDir, `list-${locale}-${theme}-${width}.png`), fullPage: true });
|
|
await expand(card(1));
|
|
await card(2).locator('.auth-health-summary').click();
|
|
await card(4).locator('.credential-quota-more > summary').click();
|
|
await assertFits(`${locale} ${theme} ${width} expanded`);
|
|
await card(1).screenshot({ path: path.join(screenshotDir, `request-summary-${locale}-${theme}-${width}.png`) });
|
|
await card(2).screenshot({ path: path.join(screenshotDir, `cooldown-${locale}-${theme}-${width}.png`) });
|
|
await card(4).screenshot({ path: path.join(screenshotDir, `expanded-quota-${locale}-${theme}-${width}.png`) });
|
|
await open(`theme=${theme}&locale=${locale}&small=1`);
|
|
assert.equal(await cards().count(), 3, 'Screenshot-like idle state shows three accounts');
|
|
assert.equal(await pagination().count(), 0, 'Small lists omit unnecessary pagination');
|
|
assert.equal(await page.locator('.auth-list-details:visible, .auth-file-health:visible').count(), 0, 'Small idle list starts with diagnostics collapsed');
|
|
await assertMainInformation(`${locale} ${theme} ${width} small idle`, width);
|
|
assert.equal(await page.locator('.auth-file-usage-metric:visible').count(), 12, 'Idle counters stay visible on all three rows');
|
|
assert.equal(await page.locator('.auth-list-quota:visible').count(), 3, 'Disabled and idle quota states remain visible');
|
|
await assertFits(`${locale} ${theme} ${width} small idle`);
|
|
await page.screenshot({ path: path.join(screenshotDir, `small-idle-${locale}-${theme}-${width}.png`), fullPage: true });
|
|
assert.deepEqual(await page.evaluate(() => window.authFileListFixture.unhandled), []);
|
|
}
|
|
await page.setViewportSize({ width: 1800, height: 1200 });
|
|
await open('theme=light&locale=zh-CN');
|
|
const tableBounds = await page.locator('.auth-file-table-scroll').boundingBox();
|
|
const seventhBounds = await card(7).boundingBox();
|
|
await page.screenshot({ path: path.join(screenshotDir, 'reference-dense-preview.png'), clip: {
|
|
x: tableBounds.x, y: tableBounds.y, width: tableBounds.width,
|
|
height: seventhBounds.y + seventhBounds.height - tableBounds.y,
|
|
} });
|
|
await open('theme=light&locale=zh-CN&small=1');
|
|
await page.locator('.auth-file-table-scroll').screenshot({ path: path.join(screenshotDir, 'reference-small-idle-preview.png') });
|
|
assert.deepEqual(errors, []);
|
|
assert.deepEqual(externalRequests, []);
|
|
console.log('PASS: offline seven-column dense credential list with visible request/runtime/quota/action data, compact row height, exact quota percentages/reset timestamps, unknown quota, ten/two pagination, search/provider/status reset, page size twenty, additional quota disclosure, main-row and detail-summary request keyboard tooltips, directly visible cooldown controls, exact status PATCH, direct delete confirmation/cancel, small idle accounts without pagination, light/dark Chinese/English 1800/1280/1024/390 without page or card overflow, contained table scrolling only at intermediate widths.');
|
|
console.log(`Screenshots: ${screenshotDir}`);
|
|
} finally { await browser?.close(); await server.close(); }
|
|
})().catch(error => { console.error(error); process.exitCode = 1; });
|