- 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
372 lines
27 KiB
JavaScript
372 lines
27 KiB
JavaScript
const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const base = process.env.HOME_DASHBOARD_TEST_BASE_URL || 'http://127.0.0.1:1421';
|
|
|
|
(async () => {
|
|
let server;
|
|
let browser;
|
|
try {
|
|
try { await fetch(base, { signal: AbortSignal.timeout(5000) }); } catch {
|
|
const { createServer } = await import('vite');
|
|
const react = (await import('@vitejs/plugin-react')).default;
|
|
server = await createServer({ configFile: false, cacheDir: 'node_modules/.vite-home-dashboard', plugins: [react()], logLevel: 'error', optimizeDeps: { noDiscovery: true, include: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime', '@tauri-apps/api/core', '@tauri-apps/api/mocks', '@tauri-apps/api/event', 'lucide-react', 'react-markdown', '@dnd-kit/core', '@dnd-kit/sortable', '@dnd-kit/utilities'] }, server: { host: '127.0.0.1', port: Number(new URL(base).port), strictPort: true, watch: null } });
|
|
await server.listen();
|
|
}
|
|
browser = await chromium.launch({ channel: 'msedge', headless: true, args: ['--no-proxy-server'] });
|
|
const errors = [];
|
|
const makePage = async (viewport = { width: 1280, height: 1050 }) => {
|
|
const page = await browser.newPage({ viewport });
|
|
page.setDefaultTimeout(10000);
|
|
page.setDefaultNavigationTimeout(60000);
|
|
page.on('pageerror', error => errors.push(String(error)));
|
|
await page.route('**/*', route => route.request().url().startsWith(base + '/') ? route.continue() : route.abort());
|
|
return page;
|
|
};
|
|
const page = await makePage();
|
|
const showHealth = async (target = page, query = '') => {
|
|
await target.locator('.core-health-open').click();
|
|
await target.getByRole('dialog').waitFor();
|
|
if (!query.includes('loading') && !query.includes('modelError') && !query.includes('offline')) {
|
|
await target.locator('.core-health-table tbody tr').first().waitFor();
|
|
}
|
|
};
|
|
const closeHealth = async (target = page) => {
|
|
await target.getByRole('dialog').getByRole('button', { name: 'Close', exact: true }).click();
|
|
await target.getByRole('dialog').waitFor({ state: 'detached' });
|
|
assert.equal(await target.locator('.core-health-open').evaluate(node => node === document.activeElement), true);
|
|
};
|
|
const open = async (query = '', target = page, showDialog = true) => {
|
|
const options = new URLSearchParams(query);
|
|
if (!options.has('locale')) options.set('locale', 'en');
|
|
await target.goto(`${base}/tests/fixtures/home-dashboard.html?${options}`, { waitUntil: 'domcontentloaded' });
|
|
await target.locator('.core-health-entry').waitFor();
|
|
if (showDialog) await showHealth(target, query);
|
|
};
|
|
const probes = target => target.evaluate(() => window.homeFixture.calls.filter(call => call.cmd === 'core_health_probe'));
|
|
const checkAll = target => target.locator('.core-health-actions').getByRole('button', { name: 'Check All', exact: true });
|
|
const awaitIdle = target => target.waitForFunction(() => !document.querySelector('.core-health-progress'));
|
|
const value = id => page.locator(`[data-stat="${id}"] .home-stat-value`);
|
|
const runtimeValue = label => page.locator('.control-panel .panel-detail-row').filter({ has: page.getByText(label, { exact: true }) }).locator('dd');
|
|
const meter = id => page.locator(`[data-stat="${id}"] .home-stat-track[role="meter"]`);
|
|
|
|
await open('', page, false);
|
|
await page.locator('.home-overview[aria-busy="false"]').waitFor();
|
|
fs.mkdirSync('misc', { recursive: true });
|
|
await page.screenshot({ path: 'misc/home-dashboard-initial.png', fullPage: true, animations: 'disabled' });
|
|
console.log('Initial homepage rendered: misc/home-dashboard-initial.png');
|
|
assert.equal(await page.locator('.home-stat-card').count(), 4);
|
|
assert.equal(await runtimeValue('Installation Status').innerText(), 'Installed');
|
|
assert.equal(await runtimeValue('Runtime Status').innerText(), 'Running');
|
|
assert.equal(await page.locator('.home-runtime-details .panel-detail-row').count(), 6, 'runtime details retain both statuses, both versions, PID, and port');
|
|
for (const [id, expected] of [['usage', '97.8%'], ['credentials', '11'], ['providerKeys', '2'], ['models', '6']]) {
|
|
assert.equal(await value(id).innerText(), expected, `incorrect ${id} aggregate`);
|
|
}
|
|
assert.match(await page.locator('[data-stat="credentials"] p').innerText(), /5 available · 6 not in rotation/);
|
|
assert.match(await page.locator('[data-stat="usage"] p').innerText(), /503 requests/);
|
|
assert.ok(Math.abs(Number(await meter('usage').getAttribute('aria-valuenow')) - 492 / 503 * 100) < 0.000001, 'success meter must use the measured success rate');
|
|
assert.ok(Math.abs(Number(await meter('credentials').getAttribute('aria-valuenow')) - 5 / 11 * 100) < 0.000001, 'credential meter must use the available share');
|
|
assert.equal(await page.getByRole('meter', { name: 'Available credential share', exact: true }).count(), 1);
|
|
assert.equal(await page.locator('[data-stat="providerKeys"] [role="meter"], [data-stat="models"] [role="meter"]').count(), 0, 'counts without a denominator must not display a ratio');
|
|
const usageQuery = await page.evaluate(() => window.homeFixture.calls.find(call => call.cmd === 'get_usage_overview').args.query);
|
|
assert.equal(Date.parse(usageQuery.end) - Date.parse(usageQuery.start), 86_400_000);
|
|
assert.equal((await probes(page)).length, 0, 'opening the dashboard must not perform inference');
|
|
assert.equal(await page.locator('.core-health-panel, .core-health-table, .core-health-dialog').count(), 0, 'the dashboard must show only the compact entry');
|
|
assert.ok((await page.locator('.core-health-entry').boundingBox()).height < 150, 'the collapsed entry must not take up a full panel');
|
|
assert.equal(await page.locator('.home-overview-heading .core-health-entry').count(), 1, 'health monitoring belongs in the overview header');
|
|
assert.equal(await page.locator('.home-dashboard > .core-health-entry').count(), 0, 'health monitoring must not leave an orphan entry below the overview');
|
|
const healthSummaryBounds = await page.locator('.core-health-entry-status').boundingBox();
|
|
assert.ok(healthSummaryBounds.width > 1 && healthSummaryBounds.height > 1, 'the health summary must stay visible beside its entry');
|
|
|
|
const access = page.locator('.home-access-panel');
|
|
assert.equal(await access.locator('.client-api-card').count(), 1, 'only the selected connection details should be shown');
|
|
assert.equal(await access.locator('.home-key-value').innerText(), '••••••••••••••••••••');
|
|
assert.equal(await access.getByText('test-client-access-key-never-real', { exact: true }).count(), 0);
|
|
await access.getByRole('button', { name: 'Show key', exact: true }).click();
|
|
assert.equal(await access.locator('.home-key-value').innerText(), 'test-client-access-key-never-real');
|
|
await access.getByRole('button', { name: 'Hide key', exact: true }).click();
|
|
await access.getByRole('button', { name: 'Copy authentication key', exact: true }).click();
|
|
await page.waitForFunction(() => window.homeFixture.copied.includes('test-client-access-key-never-real'));
|
|
const openaiTab = page.locator('#home-protocol-openai');
|
|
await openaiTab.focus();
|
|
await page.keyboard.press('ArrowRight');
|
|
assert.equal(await page.locator('#home-protocol-claude').getAttribute('aria-selected'), 'true');
|
|
assert.equal(await page.locator('#home-protocol-claude').evaluate(node => node === document.activeElement), true);
|
|
assert.equal(await page.getByRole('tabpanel').locator('code').innerText(), 'http://127.0.0.1:8317');
|
|
await page.getByRole('tabpanel').getByRole('button').click();
|
|
await page.waitForFunction(() => window.homeFixture.copied.includes('http://127.0.0.1:8317'));
|
|
await page.locator('#home-protocol-claude').focus();
|
|
await page.keyboard.press('End');
|
|
assert.equal(await page.locator('#home-protocol-gemini').getAttribute('aria-selected'), 'true');
|
|
await page.keyboard.press('Home');
|
|
assert.equal(await openaiTab.getAttribute('aria-selected'), 'true');
|
|
assert.equal(await page.getByRole('tabpanel').locator('code').innerText(), 'http://127.0.0.1:8317/v1');
|
|
|
|
await showHealth();
|
|
const dialog = page.getByRole('dialog', { name: 'Model Health', exact: true });
|
|
assert.equal(await dialog.getAttribute('aria-modal'), 'true');
|
|
assert.equal(await dialog.evaluate(node => node.contains(document.activeElement)), true, 'opening the modal must focus a control inside it');
|
|
assert.equal(await dialog.getByRole('checkbox').count(), 0, 'health checks must not offer automatic mode');
|
|
assert.equal(await dialog.getByText(/automatically|automatic check/i).count(), 0);
|
|
assert.equal((await probes(page)).length, 0, 'opening the modal must not perform inference');
|
|
const focusable = dialog.locator('button:not(:disabled), input:not(:disabled), summary, [tabindex="0"]');
|
|
await focusable.last().focus();
|
|
await page.keyboard.press('Tab');
|
|
assert.equal(await focusable.first().evaluate(node => node === document.activeElement), true, 'Tab must wrap inside the modal');
|
|
await page.keyboard.press('Shift+Tab');
|
|
assert.equal(await focusable.last().evaluate(node => node === document.activeElement), true, 'Shift+Tab must wrap inside the modal');
|
|
await page.getByRole('button', { name: 'Check gpt-5.2-codex', exact: true }).click();
|
|
await page.locator('.core-health-status.healthy').waitFor();
|
|
assert.deepEqual((await probes(page)).map(call => call.args), [{ model: 'gpt-5.2-codex', timeoutMs: 15000 }]);
|
|
assert.match(await page.locator('.core-health-table tbody tr').first().locator('.core-health-latency').innerText(), /184 ms.*642 ms/);
|
|
await page.keyboard.press('Escape');
|
|
await dialog.waitFor({ state: 'detached' });
|
|
assert.equal(await page.locator('.core-health-open').evaluate(node => node === document.activeElement), true, 'Escape must restore focus to the entry');
|
|
assert.notEqual(await page.evaluate(() => document.body.style.overflow), 'hidden', 'closing must release the page scroll lock');
|
|
await showHealth();
|
|
assert.equal(await page.locator('.core-health-status.healthy').count(), 1, 'completed results must survive closing and reopening');
|
|
assert.equal((await probes(page)).length, 1, 'reopening must not launch a check');
|
|
await page.locator('.core-health-backdrop').click({ position: { x: 2, y: 2 } });
|
|
await dialog.waitFor({ state: 'detached' });
|
|
assert.equal(await page.locator('.core-health-open').evaluate(node => node === document.activeElement), true, 'backdrop dismissal must restore focus');
|
|
await showHealth();
|
|
await page.locator('.core-health-search input').fill('custom/team');
|
|
assert.equal(await page.locator('.core-health-table tbody tr').count(), 1);
|
|
await page.locator('.core-health-search input').fill('not-a-real-model');
|
|
assert.equal(await page.locator('.core-health-table tbody tr').count(), 0);
|
|
await page.locator('.core-health-search input').clear();
|
|
await page.evaluate(() => {
|
|
window.homeFixture.probeFailures['gemini-3-pro'] = 'HTTP 503: fixture upstream unavailable';
|
|
window.homeFixture.probeFailures['deepseek-chat'] = 'deadline has elapsed';
|
|
});
|
|
await checkAll(page).click();
|
|
await page.waitForFunction(() => document.querySelectorAll('.core-health-status.failed').length === 2);
|
|
assert.equal((await probes(page)).length, 7);
|
|
assert.equal(await page.locator('.core-health-status.healthy').count(), 4);
|
|
const gemini = page.locator('.core-health-table tbody tr').filter({ hasText: 'gemini-3-pro' });
|
|
await gemini.locator('summary').click();
|
|
assert.match(await gemini.locator('details p').innerText(), /HTTP 503/);
|
|
console.log('Home data, connection tabs, clipboard, single/batch/error checks passed.');
|
|
|
|
await open();
|
|
await page.evaluate(() => { window.homeFixture.holdProbes = true; });
|
|
await checkAll(page).click();
|
|
await page.waitForFunction(() => window.homeFixture.probeReleases.length === 4);
|
|
assert.equal(await page.locator('.core-health-status.queued').count(), 2);
|
|
await page.getByRole('button', { name: 'Stop Checking', exact: true }).click();
|
|
await page.evaluate(() => window.homeFixture.releaseProbes());
|
|
await awaitIdle(page);
|
|
assert.equal((await probes(page)).length, 4, 'stop must not launch queued model requests');
|
|
assert.equal(await page.locator('.core-health-status.healthy').count(), 0, 'late probe results must not populate stopped checks');
|
|
|
|
await open();
|
|
await page.getByRole('button', { name: 'Check gpt-5.2-codex', exact: true }).click();
|
|
await page.locator('.core-health-status.healthy').waitFor();
|
|
await page.evaluate(() => { window.homeFixture.holdProbes = true; });
|
|
await checkAll(page).click();
|
|
await page.waitForFunction(() => window.homeFixture.probeReleases.length === 4);
|
|
assert.equal(await page.locator('.core-health-status.queued').count(), 2);
|
|
await closeHealth();
|
|
await page.evaluate(() => window.homeFixture.releaseProbes());
|
|
await showHealth();
|
|
await awaitIdle(page);
|
|
assert.equal((await probes(page)).length, 5, 'closing must not launch queued model requests');
|
|
assert.equal(await page.locator('.core-health-status.healthy').count(), 1, 'closing must keep completed results and discard late results');
|
|
|
|
await open();
|
|
await page.evaluate(() => { window.homeFixture.holdProbes = true; });
|
|
await checkAll(page).click();
|
|
await page.waitForFunction(() => window.homeFixture.probeReleases.length === 4);
|
|
await page.evaluate(() => window.homeFixture.changeConfig());
|
|
await page.waitForFunction(() => document.querySelector('#home-protocol-panel code')?.textContent.includes(':8318/'));
|
|
await page.evaluate(() => window.homeFixture.releaseProbes());
|
|
await awaitIdle(page);
|
|
assert.equal(await page.locator('.core-health-status.healthy').count(), 0, 'config replacement must discard pending results');
|
|
assert.equal((await probes(page)).length, 4);
|
|
|
|
await open('loading=1');
|
|
await page.waitForFunction(() => window.homeFixture.modelReleases.length > 0);
|
|
assert.equal(await checkAll(page).isDisabled(), true);
|
|
assert.equal(await page.locator('.core-health-empty .spin').count(), 1);
|
|
await page.evaluate(() => window.homeFixture.releaseModels());
|
|
await page.locator('.core-health-table tbody tr').first().waitFor();
|
|
await open('modelError=1');
|
|
await page.locator('.core-health-error').waitFor();
|
|
assert.equal(await checkAll(page).isDisabled(), true);
|
|
assert.equal(await value('models').innerText(), '—');
|
|
await page.evaluate(() => { window.homeFixture.modelError = ''; });
|
|
await page.getByRole('button', { name: 'Refresh Models', exact: true }).click();
|
|
await page.locator('.core-health-table tbody tr').first().waitFor();
|
|
await open('offline=1');
|
|
await page.getByRole('dialog').getByText('Start the core to check model health', { exact: true }).waitFor();
|
|
await page.locator('.home-overview[aria-busy="false"]').waitFor();
|
|
assert.equal(await value('usage').innerText(), '97.8%');
|
|
assert.equal(await value('models').innerText(), '—');
|
|
assert.equal(await runtimeValue('Installation Status').innerText(), 'Installed');
|
|
assert.equal(await runtimeValue('Runtime Status').innerText(), 'Stopped');
|
|
assert.equal(await meter('usage').count(), 1, 'local usage stays measured while the core is stopped');
|
|
assert.equal(await meter('credentials').count(), 0, 'unavailable credential data must not display a ratio');
|
|
assert.equal(await checkAll(page).isDisabled(), true);
|
|
assert.equal(await page.evaluate(() => window.homeFixture.calls.filter(call => call.cmd === 'get_core_models').length), 0);
|
|
await open('noKey=1', page, false);
|
|
assert.equal(await access.locator('.home-key-value').count(), 0);
|
|
await open('keyError=1', page, false);
|
|
assert.equal(await access.locator('.home-field-error').count(), 1);
|
|
await open('tls=1', page, false);
|
|
assert.equal(await page.getByRole('tabpanel').locator('code').innerText(), 'https://127.0.0.1:8317/v1');
|
|
await open('noRequests=1&unknownCredentials=1', page, false);
|
|
await page.locator('.home-overview[aria-busy="false"]').waitFor();
|
|
assert.equal(await value('usage').innerText(), '—', 'an empty request window has no measured success rate');
|
|
assert.equal(await page.locator('[data-stat="usage"] p').innerText(), 'No completed requests yet');
|
|
assert.equal(await value('credentials').innerText(), '11');
|
|
assert.match(await page.locator('[data-stat="credentials"] p').innerText(), /0 available · 0 not in rotation · 11 with unknown status/);
|
|
for (const id of ['usage', 'credentials']) {
|
|
assert.equal(await meter(id).count(), 0, `unmeasured ${id} must not display a zero-percent meter`);
|
|
assert.equal(await page.locator(`[data-stat="${id}"] .home-stat-track[aria-hidden="true"]`).count(), 1);
|
|
}
|
|
await open('noCredentials=1&usageError=1', page, false);
|
|
await page.locator('.home-overview[aria-busy="false"]').waitFor();
|
|
assert.equal(await value('usage').innerText(), '—');
|
|
assert.equal(await value('credentials').innerText(), '0');
|
|
assert.equal(await meter('usage').count(), 0, 'a failed usage read must not display a ratio');
|
|
assert.equal(await meter('credentials').count(), 0, 'an empty credential list has no denominator');
|
|
console.log('Modal dismissal/focus/retention/cancellation, stop, stale configuration, loading/offline/retry/key/TLS checks passed.');
|
|
|
|
// Overview refreshes and elapsed five-minute periods must never trigger inference.
|
|
const timed = await makePage();
|
|
await timed.clock.install({ time: new Date('2026-10-03T04:00:00Z') });
|
|
await timed.clock.pauseAt(new Date('2026-10-03T04:00:01Z'));
|
|
await open('', timed);
|
|
assert.equal(await timed.getByRole('dialog').getByRole('checkbox').count(), 0);
|
|
for (let cycle = 1; cycle <= 2; cycle += 1) {
|
|
await timed.clock.runFor(301000);
|
|
assert.equal((await probes(timed)).length, 0, `the open dialog must stay idle after ${cycle} five-minute periods`);
|
|
}
|
|
assert.ok(await timed.evaluate(() => window.homeFixture.calls.filter(call => call.cmd === 'get_core_models').length) >= 6);
|
|
await checkAll(timed).click();
|
|
await timed.waitForFunction(() => document.querySelectorAll('.core-health-status.healthy').length === 6);
|
|
assert.equal((await probes(timed)).length, 6, 'manual Check All must probe every model once');
|
|
for (let cycle = 1; cycle <= 2; cycle += 1) {
|
|
await timed.clock.runFor(301000);
|
|
assert.equal((await probes(timed)).length, 6, `manual checks must not repeat after ${cycle} five-minute periods`);
|
|
}
|
|
await closeHealth(timed);
|
|
await timed.clock.runFor(301000);
|
|
assert.equal((await probes(timed)).length, 6, 'the closed dialog must not start another check');
|
|
await showHealth(timed);
|
|
assert.equal(await timed.locator('.core-health-status.healthy').count(), 6);
|
|
await timed.clock.runFor(301000);
|
|
assert.equal((await probes(timed)).length, 6, 'reopening must not schedule another check');
|
|
|
|
// Also stress loading=true and fresh-but-equal arrays on every refresh.
|
|
await open('view=panel', timed);
|
|
await checkAll(timed).click();
|
|
await timed.waitForFunction(() => document.querySelectorAll('.core-health-status.healthy').length === 6);
|
|
for (let cycle = 1; cycle <= 2; cycle += 1) {
|
|
await timed.evaluate(() => window.homeFixture.refreshPanel());
|
|
await timed.clock.runFor(301000);
|
|
assert.equal((await probes(timed)).length, 6, 'loading model lists must not schedule checks');
|
|
await timed.evaluate(() => window.homeFixture.resolvePanel());
|
|
await timed.clock.runFor(301000);
|
|
assert.equal((await probes(timed)).length, 6, 'refreshed model arrays must not schedule checks');
|
|
}
|
|
await checkAll(timed).click();
|
|
await timed.waitForFunction(() => window.homeFixture.calls.filter(call => call.cmd === 'core_health_probe').length === 12);
|
|
await awaitIdle(timed);
|
|
await timed.close();
|
|
console.log('Fake-clock manual-only checks passed: no inference while idle or across repeated five-minute periods, close/reopen, and model refreshes.');
|
|
|
|
fs.mkdirSync('misc', { recursive: true });
|
|
for (const [theme, width] of [['light', 1280], ['dark', 1280], ['light', 640], ['dark', 640], ['light', 390], ['dark', 390]]) {
|
|
const height = width < 640 ? 844 : 1050;
|
|
await page.setViewportSize({ width, height });
|
|
await open(`locale=zh-CN&theme=${theme}`, page, false);
|
|
await page.locator('.home-overview[aria-busy="false"]').waitFor();
|
|
assert.equal(await page.locator('.core-health-table').count(), 0);
|
|
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), `${theme}/${width} collapsed homepage overflows horizontally`);
|
|
await page.screenshot({ path: `misc/home-dashboard-${theme}-${width}.png`, fullPage: true, animations: 'disabled' });
|
|
await showHealth();
|
|
await page.locator('.core-health-actions .primary-button').click();
|
|
await page.waitForFunction(() => document.querySelectorAll('.core-health-status.healthy').length === 6);
|
|
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), `${theme}/${width} overflows horizontally`);
|
|
const bounds = await page.locator('.core-health-dialog').boundingBox();
|
|
assert.ok(bounds.x >= 0 && bounds.y >= 0 && bounds.x + bounds.width <= width + 1 && bounds.y + bounds.height <= height + 1, `${theme}/${width} modal leaves the viewport`);
|
|
await page.screenshot({ path: `misc/home-health-dialog-${theme}-${width}.png`, animations: 'disabled' });
|
|
}
|
|
for (const [theme, width, height] of [['light', 1280, 900], ['dark', 1280, 900], ['light', 640, 600], ['dark', 640, 600]]) {
|
|
await page.setViewportSize({ width, height });
|
|
await page.evaluate(theme => {
|
|
localStorage.setItem('easy-cli-proxy-api.locale', 'zh-CN');
|
|
localStorage.setItem('easy-cli-proxy-api.theme', theme);
|
|
}, theme);
|
|
await page.goto(`${base}/?mock=running`, { waitUntil: 'domcontentloaded' });
|
|
await page.locator('.home-overview[aria-busy="false"]').waitFor();
|
|
const toolbar = page.locator('#browser-mock-toolbar');
|
|
if (await toolbar.count()) await toolbar.evaluate(node => { node.style.display = 'none'; });
|
|
assert.equal(await page.locator('.home-stat-card').count(), 4);
|
|
assert.equal(await page.locator('.home-overview-heading .core-health-open').count(), 1);
|
|
assert.equal(await page.locator('.home-dashboard > .core-health-entry').count(), 0);
|
|
assert.equal(await page.locator('.core-health-table').count(), 0);
|
|
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), `${theme}/${width} app homepage overflows horizontally`);
|
|
await page.screenshot({ path: `misc/home-dashboard-app-${theme}-${width}.png`, animations: 'disabled' });
|
|
if (theme === 'light' && width === 1280) await page.screenshot({ path: 'misc/home-dashboard-app.png', animations: 'disabled' });
|
|
await showHealth();
|
|
const bounds = await page.locator('.core-health-dialog').boundingBox();
|
|
assert.ok(bounds.x >= 0 && bounds.y >= 0 && bounds.x + bounds.width <= width + 1 && bounds.y + bounds.height <= height + 1, `${theme}/${width} app modal leaves the viewport`);
|
|
await page.screenshot({ path: `misc/home-health-dialog-app-${theme}-${width}.png`, animations: 'disabled' });
|
|
if (theme === 'light' && width === 1280) await page.screenshot({ path: 'misc/home-health-dialog-app.png', animations: 'disabled' });
|
|
}
|
|
// The normal desktop dashboard must fit the usable viewport, including the
|
|
// shorter CSS viewport produced by desktop display scaling.
|
|
const desktopViewports = [
|
|
{ width: 1464, height: 845 },
|
|
{ width: 1280, height: 800 },
|
|
{ width: 1440, height: 900 },
|
|
{ width: 1280, height: 720 },
|
|
];
|
|
for (const locale of ['zh-CN', 'en']) {
|
|
for (const theme of ['light', 'dark']) {
|
|
await page.setViewportSize(desktopViewports[0]);
|
|
await page.evaluate(({ locale, theme }) => {
|
|
localStorage.setItem('easy-cli-proxy-api.locale', locale);
|
|
localStorage.setItem('easy-cli-proxy-api.theme', theme);
|
|
}, { locale, theme });
|
|
await page.goto(`${base}/?mock=running`, { waitUntil: 'domcontentloaded' });
|
|
await page.locator('.home-overview[aria-busy="false"]').waitFor();
|
|
const toolbar = page.locator('#browser-mock-toolbar');
|
|
if (await toolbar.count()) await toolbar.evaluate(node => { node.style.display = 'none'; });
|
|
assert.equal(await page.locator('html').getAttribute('lang'), locale);
|
|
assert.equal(await page.locator('html').getAttribute('data-theme'), theme);
|
|
for (const viewport of desktopViewports) {
|
|
await page.setViewportSize(viewport);
|
|
const layout = await page.evaluate(async () => {
|
|
await document.fonts.ready;
|
|
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
window.scrollTo(0, 0);
|
|
const root = document.scrollingElement;
|
|
const cards = [...document.querySelectorAll('.home-top-grid > .panel, .home-stat-card')].map(node => {
|
|
const bounds = node.getBoundingClientRect();
|
|
return { name: node.getAttribute('data-stat') || node.className, top: bounds.top, bottom: bounds.bottom, height: bounds.height };
|
|
});
|
|
return { scrollHeight: root.scrollHeight, clientHeight: root.clientHeight, viewportHeight: innerHeight, cards };
|
|
});
|
|
const context = `${locale}/${theme}/${viewport.width}x${viewport.height}`;
|
|
assert.equal(layout.cards.length, 6, `${context} must show both top panels and all four overview cards`);
|
|
assert.ok(layout.scrollHeight <= layout.clientHeight + 1,
|
|
`${context} homepage causes a vertical page scrollbar: ${layout.scrollHeight}px content in ${layout.clientHeight}px viewport`);
|
|
for (const card of layout.cards) {
|
|
assert.ok(card.height > 0 && card.top >= -1 && card.bottom <= layout.viewportHeight + 1,
|
|
`${context} ${card.name} must be fully visible without scrolling: ${JSON.stringify(card)}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
console.log('Desktop homepage fits all four viewport sizes in Chinese/English and light/dark themes without vertical page overflow.');
|
|
assert.deepEqual(errors, []);
|
|
console.log('Home dashboard browser checks passed. Screenshots: misc/home-{dashboard,health-dialog}-{light,dark}-{1280,640,390}.png and app.png');
|
|
} finally {
|
|
if (browser) await browser.close();
|
|
if (server) await server.close();
|
|
}
|
|
})().catch(error => { console.error(error); process.exitCode = 1; });
|