'use strict'; // Browser acceptance for the read-only memory-checkpoint comparison view. // // These cases drive the real loopback service. They require two things the // fixture/backend must provide: // // 1. The fixture seeds two finished runs named `browser-checkpoint-probe` // whose frozen captures match (see // crates/ultimate-tool-server/tests/browser_support/checkpoint_cases.rs). // 2. The service registers the read-only routes // GET /api/v1/runs/checkpoint-comparison?baseline_id=..&candidate_id=.. // GET /api/v1/checkpoint-comparisons // GET /api/v1/checkpoint-comparisons/{comparison_id} // // Against a service without those routes the panel renders the route's error // instead of a comparison, so these are not a substitute for that backend work. // No case here authorizes hardware, probes a device, or starts a stream: the // whole point of the view is that comparing two immutable runs is a plain read. const { test, expect } = require('@playwright/test'); const { startFixtureServer } = require('./server.cjs'); let server; test.beforeAll(async () => { test.setTimeout(100_000); server = await startFixtureServer(); }); test.afterAll(async () => { if (server) await server.stop(); }); function recordRequests(page, server, predicate) { const hits = []; page.on('request', (request) => { if (request.url().startsWith(server.url) && predicate(request)) { hits.push(request.url()); } }); return hits; } // The two seeded capture runs, in listing order. They share one test name, so // they are distinguished by the run id on each history card. async function checkpointRunIds(page) { const cards = page.locator('#run-list .run-card').filter({ hasText: 'browser-checkpoint-probe' }); await expect(cards).toHaveCount(2); const first = await cards.nth(0).getAttribute('data-run-id'); const second = await cards.nth(1).getAttribute('data-run-id'); return [first, second]; } async function compare(page, baselineId, candidateId) { await page.selectOption('#comparison-baseline', baselineId); await page.selectOption('#comparison-candidate', candidateId); await page.locator('#comparison-run').click(); } test('comparing two seeded captures is one read-only request and reports matched', async ({ page }) => { const comparisonRequests = recordRequests( page, server, (request) => new URL(request.url()).pathname === '/api/v1/runs/checkpoint-comparison', ); const mutations = recordRequests(page, server, (request) => request.method() !== 'GET'); await page.goto(server.url); const [baseline, candidate] = await checkpointRunIds(page); await compare(page, baseline, candidate); await expect(page.locator('#comparison-badge')).toHaveText('MATCHED'); const result = page.locator('#comparison-result'); await expect(result).toContainText('Where do the captures first differ?'); await expect(result).toContainText('no differing checkpoint row recorded'); await expect(result).toContainText('health'); await expect(result).toContainText('score'); await expect(result).toContainText('both captures share the same target kind'); expect(comparisonRequests.length).toBe(1); expect(mutations).toEqual([]); }); test('a side without a capture reads unavailable and never diverged', async ({ page }) => { await page.goto(server.url); const [baseline] = await checkpointRunIds(page); const smoke = await page.locator('#run-list .run-card').filter({ hasText: 'smoke' }) .first().getAttribute('data-run-id'); await compare(page, baseline, smoke); await expect(page.locator('#comparison-badge')).toHaveText('UNAVAILABLE'); const result = page.locator('#comparison-result'); await expect(result).toContainText('capture_missing'); await expect(result).not.toContainText('DIVERGED'); }); test('the comparison controls offer no mutation and clear cleanly', async ({ page }) => { const mutations = recordRequests(page, server, (request) => request.method() !== 'GET'); await page.goto(server.url); const [baseline, candidate] = await checkpointRunIds(page); await compare(page, baseline, candidate); await expect(page.locator('#comparison-badge')).toHaveText('MATCHED'); await page.locator('#comparison-clear').click(); await expect(page.locator('#comparison-badge')).toHaveText('NOT COMPARED'); await expect(page.locator('#comparison-baseline')).toHaveValue(''); await expect(page.locator('#comparison-candidate')).toHaveValue(''); expect(mutations).toEqual([]); }); test('a stale comparison response cannot replace a newer selection', async ({ page }) => { await page.goto(server.url); const [baseline, candidate] = await checkpointRunIds(page); const smoke = await page.locator('#run-list .run-card').filter({ hasText: 'smoke' }) .first().getAttribute('data-run-id'); let release; let reached; const held = new Promise((resolve) => { release = resolve; }); const waiting = new Promise((resolve) => { reached = resolve; }); await page.route('**/api/v1/runs/checkpoint-comparison*', async (route) => { if (route.request().url().includes(smoke)) { await route.continue(); return; } reached(); await held; await route.continue(); }); // First selection: the matched pair, whose response is held open. await compare(page, baseline, candidate); await waiting; // Second selection: the unavailable pair, which answers immediately. await compare(page, baseline, smoke); await expect(page.locator('#comparison-badge')).toHaveText('UNAVAILABLE'); // The late matched response must not overwrite the newer unavailable result. release(); await expect(page.locator('#comparison-badge')).toHaveText('UNAVAILABLE'); }); test('the optional matrix checkbox is offered only for a VICE-plus-Ultimate single repetition', async ({ page }) => { // The fixture deliberately has no executable VICE and no hardware probe. // Override only capability presentation to exercise the selection controls; // this case never submits a job or authorizes any target operation. await page.route('**/api/v1/targets', async (route) => { const response = await route.fetch(); const body = await response.json(); for (const target of body.targets || []) { if (target.kind === 'vice' || target.kind === 'ultimate') target.available = true; } await route.fulfill({ response, json: body }); }); await page.goto(server.url); const label = page.locator('#compare-checkpoints-label'); await expect(label).toBeHidden(); const choice = (kind) => page.locator('#target-options label.target-choice').filter({ hasText: kind }); await choice('fake').click(); await choice('vice').click(); await choice('ultimate').click(); // The selected test must actually declare the frozen-RAM contract. await expect(label).toBeHidden(); await page.selectOption('#test-select', 'browser-checkpoint-probe'); await expect(label).toBeVisible(); await expect(page.locator('#compare-checkpoints-text')).toContainText('frozen memory checkpoints'); // Two repetitions no longer produce a single comparable pair. await page.fill('#repeat-input', '2'); await expect(label).toBeHidden(); await page.fill('#repeat-input', '1'); await expect(label).toBeVisible(); // Dropping one half of the pair withdraws the option again. await choice('ultimate').click(); await expect(label).toBeHidden(); });