fix: stabilize public demo and align sorter twins
Document arhipovdan.ru DNS/TLS blocker, add production smoke and GPU perf tooling, unify twin layout, and expand visual regression coverage. Co-authored-by: Cursor <cursoragent@cursor.com>
44
e2e/production.smoke.spec.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Production smoke — run manually:
|
||||
* PLAYWRIGHT_BASE_URL=https://arhipovdan.ru/ npm run test:e2e:production
|
||||
* or against the working tunnel / local prod:
|
||||
* PLAYWRIGHT_BASE_URL=http://127.0.0.1:3100 npm run test:e2e:production
|
||||
*/
|
||||
test.describe('production smoke @production', () => {
|
||||
test('serves current app shell and demo controls', async ({ page }) => {
|
||||
const pageErrors: string[] = [];
|
||||
const failed: string[] = [];
|
||||
page.on('pageerror', (e) => pageErrors.push(String(e)));
|
||||
page.on('response', (r) => {
|
||||
if (r.status() >= 400 && r.url().includes(new URL(page.url()).host)) {
|
||||
failed.push(`${r.status()} ${r.url()}`);
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
const html = await page.content();
|
||||
const bundle = html.match(/index-[A-Za-z0-9_-]+\.js/)?.[0] ?? '';
|
||||
expect(bundle).toMatch(/^index-/);
|
||||
expect(bundle).not.toBe('index-CCdZzxPJ.js');
|
||||
|
||||
await expect(page.locator('canvas')).toBeVisible({ timeout: 60_000 });
|
||||
await expect(page.getByTestId('demo-play')).toBeVisible();
|
||||
await expect(page.getByTestId('demo-case-9')).toBeVisible();
|
||||
|
||||
await page.getByTestId('demo-play').click();
|
||||
await page.waitForTimeout(800);
|
||||
await page.getByTestId('demo-case-8').click();
|
||||
await expect(page.getByTestId('demo-case-label')).toHaveText('9/10');
|
||||
|
||||
await page.goto('/details');
|
||||
await expect(page.locator('#root')).toBeVisible();
|
||||
await page.reload();
|
||||
await expect(page.locator('#root')).toBeVisible();
|
||||
|
||||
expect(pageErrors, pageErrors.join('\n')).toEqual([]);
|
||||
const critical = failed.filter((f) => !f.includes('favicon'));
|
||||
expect(critical, critical.join('\n')).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -1,20 +1,174 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
|
||||
test.setTimeout(90_000);
|
||||
|
||||
async function waitScene(page: Page) {
|
||||
await page.goto('/');
|
||||
await expect(page.locator('canvas')).toBeVisible({ timeout: 60_000 });
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
await expect(page.getByTestId('demo-hud')).toBeVisible();
|
||||
// Demo auto-starts as running — pin speed early
|
||||
await page.getByTestId('demo-speed-2').click();
|
||||
}
|
||||
|
||||
async function dismissFinishedIfNeeded(page: Page) {
|
||||
const finished = page.getByTestId('demo-finished');
|
||||
if (await finished.isVisible().catch(() => false)) {
|
||||
await finished.getByRole('button', { name: /Replay/i }).click();
|
||||
await expect(finished).toHaveCount(0);
|
||||
}
|
||||
}
|
||||
|
||||
async function jumpCase(page: Page, index: number) {
|
||||
await dismissFinishedIfNeeded(page);
|
||||
await page.getByTestId(`demo-case-${index}`).click();
|
||||
await expect(page.getByTestId('demo-case-label')).toHaveText(`${index + 1}/10`, {
|
||||
timeout: 10_000,
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureRunning(page: Page) {
|
||||
await dismissFinishedIfNeeded(page);
|
||||
const pause = page.getByTestId('demo-pause');
|
||||
if (await pause.isVisible().catch(() => false)) return;
|
||||
await page.getByTestId('demo-play').click();
|
||||
}
|
||||
|
||||
async function snapHud(page: Page, name: string) {
|
||||
await dismissFinishedIfNeeded(page);
|
||||
const hud = page.getByTestId('demo-hud');
|
||||
await expect(hud).toBeVisible();
|
||||
await expect(hud).toHaveScreenshot(name, {
|
||||
threshold: 0.35,
|
||||
maxDiffPixelRatio: 0.08,
|
||||
});
|
||||
}
|
||||
|
||||
test.describe('visual regression', () => {
|
||||
test('home idle HUD panel', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.locator('canvas')).toBeVisible({ timeout: 60_000 });
|
||||
|
||||
const hud = page.getByTestId('demo-hud');
|
||||
await expect(hud).toBeVisible();
|
||||
// Wait for idle classification proof row / stable HUD text
|
||||
await expect(page.getByTestId('demo-command')).toHaveText('IDLE');
|
||||
test('01 home idle HUD', async ({ page }) => {
|
||||
await waitScene(page);
|
||||
// Pause for a stable idle-ish HUD
|
||||
const pause = page.getByTestId('demo-pause');
|
||||
if (await pause.isVisible().catch(() => false)) {
|
||||
await pause.click();
|
||||
}
|
||||
await expect(page.getByTestId('demo-case-label')).toHaveText('1/10');
|
||||
await snapHud(page, '01-home-idle-hud.png');
|
||||
});
|
||||
|
||||
// Snapshot HUD only — avoids flaky WebGL canvas pixels
|
||||
await expect(hud).toHaveScreenshot('home-idle-hud.png', {
|
||||
threshold: 0.3,
|
||||
maxDiffPixelRatio: 0.05,
|
||||
test('02 class B after seek case 0', async ({ page }) => {
|
||||
await waitScene(page);
|
||||
await jumpCase(page, 0);
|
||||
await ensureRunning(page);
|
||||
await expect(page.getByTestId('demo-category')).toHaveText('B', { timeout: 25_000 });
|
||||
await page.getByTestId('demo-pause').click().catch(() => undefined);
|
||||
await snapHud(page, '02-class-b-hud.png');
|
||||
});
|
||||
|
||||
test('03 class C oversized case 2', async ({ page }) => {
|
||||
await waitScene(page);
|
||||
await jumpCase(page, 2);
|
||||
await ensureRunning(page);
|
||||
await expect(page.getByTestId('demo-category')).toHaveText('C', { timeout: 25_000 });
|
||||
await page.getByTestId('demo-pause').click().catch(() => undefined);
|
||||
await snapHud(page, '03-class-c-hud.png');
|
||||
});
|
||||
|
||||
test('04 class D plate case 4', async ({ page }) => {
|
||||
await waitScene(page);
|
||||
await jumpCase(page, 4);
|
||||
await ensureRunning(page);
|
||||
await expect(page.getByTestId('demo-category')).toHaveText('D', { timeout: 25_000 });
|
||||
await page.getByTestId('demo-pause').click().catch(() => undefined);
|
||||
await snapHud(page, '04-class-d-hud.png');
|
||||
});
|
||||
|
||||
test('05 jam fault HUD', async ({ page }) => {
|
||||
await waitScene(page);
|
||||
await jumpCase(page, 8);
|
||||
await ensureRunning(page);
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const status = await page.getByTestId('demo-status').innerText();
|
||||
const cmd = await page.getByTestId('demo-command').innerText();
|
||||
return `${status} ${cmd}`;
|
||||
}, { timeout: 30_000 })
|
||||
.toMatch(/FAULT|JAM/i);
|
||||
await page.getByTestId('demo-pause').click().catch(() => undefined);
|
||||
await snapHud(page, '05-jam-fault-hud.png');
|
||||
});
|
||||
|
||||
test('06 emergency stop HUD', async ({ page }) => {
|
||||
await waitScene(page);
|
||||
await jumpCase(page, 9);
|
||||
await ensureRunning(page);
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const status = await page.getByTestId('demo-status').innerText();
|
||||
const cmd = await page.getByTestId('demo-command').innerText();
|
||||
return `${status} ${cmd}`;
|
||||
}, { timeout: 30_000 })
|
||||
.toMatch(/EMERGENCY/i);
|
||||
await page.getByTestId('demo-pause').click().catch(() => undefined);
|
||||
await snapHud(page, '06-emergency-hud.png');
|
||||
});
|
||||
|
||||
test('07 presentation mode hides HUD', async ({ page }) => {
|
||||
await waitScene(page);
|
||||
await dismissFinishedIfNeeded(page);
|
||||
await page.getByTestId('demo-presentation').click();
|
||||
await expect(page.getByTestId('demo-hud')).toHaveCount(0);
|
||||
await expect(page.locator('.main-page')).toHaveScreenshot('07-presentation-mode.png', {
|
||||
threshold: 0.4,
|
||||
maxDiffPixelRatio: 0.12,
|
||||
animations: 'disabled',
|
||||
});
|
||||
});
|
||||
|
||||
test('08 details engineering page', async ({ page }) => {
|
||||
await page.goto('/details');
|
||||
await expect(page.locator('#root')).toBeVisible();
|
||||
await page.waitForTimeout(1500);
|
||||
await expect(page).toHaveScreenshot('08-details-page.png', {
|
||||
threshold: 0.4,
|
||||
maxDiffPixelRatio: 0.12,
|
||||
fullPage: true,
|
||||
animations: 'disabled',
|
||||
});
|
||||
});
|
||||
|
||||
test('09 recovery after jam stop', async ({ page }) => {
|
||||
await waitScene(page);
|
||||
await jumpCase(page, 8);
|
||||
await ensureRunning(page);
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const status = await page.getByTestId('demo-status').innerText();
|
||||
const cmd = await page.getByTestId('demo-command').innerText();
|
||||
return `${status} ${cmd}`;
|
||||
}, { timeout: 30_000 })
|
||||
.toMatch(/FAULT|JAM/i);
|
||||
// Stop resets toward recovery
|
||||
const stop = page.getByTestId('demo-stop');
|
||||
if (await stop.isVisible().catch(() => false)) {
|
||||
await stop.click({ force: true });
|
||||
} else {
|
||||
await dismissFinishedIfNeeded(page);
|
||||
await page.getByTestId('demo-case-0').click();
|
||||
}
|
||||
await expect(page.getByTestId('demo-case-label')).toBeVisible();
|
||||
await snapHud(page, '09-recovery-hud.png');
|
||||
});
|
||||
|
||||
test('10 perf overlay engineering snapshot', async ({ page }) => {
|
||||
await page.goto('/?perf=1&quality=demo');
|
||||
await expect(page.locator('canvas')).toBeVisible({ timeout: 60_000 });
|
||||
await page.waitForTimeout(2000);
|
||||
const overlay = page.locator('.perf-overlay');
|
||||
await expect(overlay).toBeVisible({ timeout: 15_000 });
|
||||
await expect(overlay).toHaveScreenshot('10-perf-overlay.png', {
|
||||
threshold: 0.4,
|
||||
maxDiffPixelRatio: 0.15,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
BIN
e2e/visual.spec.ts-snapshots/01-home-idle-hud-chromium-linux.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
e2e/visual.spec.ts-snapshots/02-class-b-hud-chromium-linux.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
e2e/visual.spec.ts-snapshots/03-class-c-hud-chromium-linux.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
e2e/visual.spec.ts-snapshots/04-class-d-hud-chromium-linux.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
e2e/visual.spec.ts-snapshots/05-jam-fault-hud-chromium-linux.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
e2e/visual.spec.ts-snapshots/06-emergency-hud-chromium-linux.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 120 KiB |
BIN
e2e/visual.spec.ts-snapshots/08-details-page-chromium-linux.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
e2e/visual.spec.ts-snapshots/09-recovery-hud-chromium-linux.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
e2e/visual.spec.ts-snapshots/10-perf-overlay-chromium-linux.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 12 KiB |