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; });