const { chromium } = require('playwright'); const fs = require('node:fs'); const path = require('node:path'); const root = process.env.BENCH_ROOT || __dirname; const lanes = process.env.BENCH_LANES ? process.env.BENCH_LANES.split(',') : ['claude', 'sente', 'opencode']; async function verify(browser, lane) { const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } }); const page = await context.newPage(); page.setDefaultTimeout(3500); const errors = []; page.on('pageerror', e => errors.push(e.message)); page.on('dialog', d => d.accept()); const results = []; const t = id => page.getByTestId(id); const url = `${process.env.BENCH_URL || 'http://127.0.0.1:18765/'}${process.env.BENCH_PREFIX ?? (root.endsWith('round2') ? 'round2/' : '')}${lane}/project/`; const check = (value, msg) => { if (!value) throw new Error(msg); }; async function test(name, action) { const start = Date.now(); try { await action(); results.push({ name, pass: true, ms: Date.now() - start }); } catch (e) { results.push({ name, pass: false, error: e.message.slice(0, 900), ms: Date.now() - start }); } } async function fresh() { await page.goto(url); await page.evaluate(() => localStorage.clear()); await page.reload(); } async function add(title, notes = '', priority = 'medium') { await t('task-title').fill(title); await t('task-notes').fill(notes); await t('task-priority').selectOption(priority); await t('add-task').click(); } async function seed() { await fresh(); await add('Alpha release', 'Ship the moon', 'high'); await add('Beta planning', 'Design review', 'low'); await add('Gamma errands', 'Buy tea', 'medium'); } const item = title => t('task-item').filter({ hasText: title }); await test('01 loads empty with no runtime error', async () => { await fresh(); check(await t('task-title').isVisible(), 'creation form absent'); check(await t('task-item').count() === 0, 'unexpected initial tasks'); check(await t('empty-state').isVisible(), 'missing empty state'); check(errors.length === 0, errors.join('; ')); }); await test('02 create title/notes/priority/due', async () => { await fresh(); await t('task-due').fill('2026-10-01'); await add('Alpha release', 'Ship the moon', 'high'); check(await t('task-item').count() === 1, 'creation failed'); const text = await t('task-item').innerText(); check(text.includes('Alpha release') && text.includes('Ship the moon'), 'missing title/notes'); check(/high/i.test(text), 'priority absent'); check(/2026|Oct|10[\/\-.]01|01[\/\-.]10/.test(text), 'due date absent'); }); await test('03 whitespace title rejected', async () => { await fresh(); await add(' '); check(await t('task-item').count() === 0, 'whitespace task accepted'); }); await test('04 edit title persists', async () => { await fresh(); await add('Original'); await item('Original').getByTestId('task-edit').click(); await t('edit-title').fill('Renamed task'); await t('save-edit').click(); await page.reload(); check(await item('Renamed task').count() === 1, 'edit not persisted'); }); await test('05 completion toggle reversible and persisted', async () => { await fresh(); await add('Toggle me'); await item('Toggle me').getByTestId('task-toggle').check(); await page.reload(); check(await item('Toggle me').getByTestId('task-toggle').isChecked(), 'completion not persisted'); await item('Toggle me').getByTestId('task-toggle').uncheck(); await page.reload(); check(!await item('Toggle me').getByTestId('task-toggle').isChecked(), 'undo not persisted'); }); await test('06 deletion persists', async () => { await fresh(); await add('Delete me'); await item('Delete me').getByTestId('task-delete').click(); await page.reload(); check(await t('task-item').count() === 0, 'deletion not persisted'); }); await test('07 case-insensitive title search', async () => { await seed(); await t('search').fill('ALPHA'); check(await t('task-item').count() === 1 && await item('Alpha release').count() === 1, 'title search failed'); }); await test('08 case-insensitive notes search', async () => { await seed(); await t('search').fill('MOON'); check(await t('task-item').count() === 1 && await item('Alpha release').count() === 1, 'notes search failed'); }); await test('09 status filter active/completed/all', async () => { await seed(); await item('Alpha release').getByTestId('task-toggle').check(); await t('status-filter').selectOption('completed'); check(await t('task-item').count() === 1, 'completed filter'); await t('status-filter').selectOption('active'); check(await t('task-item').count() === 2, 'active filter'); await t('status-filter').selectOption('all'); check(await t('task-item').count() === 3, 'all filter'); }); await test('10 priority filter', async () => { await seed(); await t('priority-filter').selectOption('low'); check(await t('task-item').count() === 1 && await item('Beta planning').count() === 1, 'priority filter failed'); }); await test('11 filters and search compose', async () => { await seed(); await item('Alpha release').getByTestId('task-toggle').check(); await t('search').fill('moon'); await t('priority-filter').selectOption('high'); await t('status-filter').selectOption('active'); check(await t('task-item').count() === 0, 'AND active failed'); await t('status-filter').selectOption('completed'); check(await t('task-item').count() === 1, 'AND completed failed'); }); await test('12 empty search state visible', async () => { await seed(); await t('search').fill('not-a-match-888'); check(await t('task-item').count() === 0 && await t('empty-state').isVisible(), 'no empty search state'); }); await test('13 global counts correct', async () => { await seed(); await item('Alpha release').getByTestId('task-toggle').check(); check((await t('count-total').innerText()).trim() === '3', 'total wrong'); check((await t('count-active').innerText()).trim() === '2', 'active wrong'); check((await t('count-completed').innerText()).trim() === '1', 'completed wrong'); }); await test('14 multiple tasks survive reload', async () => { await seed(); await page.reload(); check(await t('task-item').count() === 3, 'tasks lost on reload'); check(await item('Alpha release').count() === 1, 'title lost on reload'); }); await test('15 invalid saved JSON recoverable', async () => { await fresh(); await add('Persisted'); const keys = await page.evaluate(() => Object.keys(localStorage).filter(k => { try { return JSON.stringify(JSON.parse(localStorage[k])).includes('Persisted'); } catch { return false; } })); check(keys.length > 0, 'no task storage key'); await page.evaluate(keys => keys.forEach(k => localStorage.setItem(k, '{broken')), keys); const before = errors.length; await page.reload(); await add('Recovered'); check(await item('Recovered').count() === 1, 'cannot create after corrupted storage'); check(errors.length === before, 'runtime exception on invalid JSON'); }); await test('16 title and notes HTML injection prevented', async () => { await fresh(); await add('', ''); await page.waitForTimeout(150); check(await t('task-item').count() === 1, 'task missing'); check(await t('task-item').locator('[onerror],[onload]').count() === 0, 'user input became HTML'); check(!await page.evaluate(() => window.__xss), 'injection executed'); }); await test('17 desktop has no horizontal overflow', async () => { await page.setViewportSize({ width: 1440, height: 1000 }); await seed(); check(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1), 'desktop overflows'); await page.screenshot({ path: path.join(root, lane, 'desktop.png'), fullPage: true }); }); await test('18 mobile no overflow and form usable', async () => { await page.setViewportSize({ width: 390, height: 844 }); await seed(); check(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1), 'mobile overflows'); await add('Mobile task'); check(await item('Mobile task').count() === 1, 'mobile form not usable'); await page.screenshot({ path: path.join(root, lane, 'mobile.png'), fullPage: true }); }); await test('19 inputs have accessible labels', async () => { const missing = await page.locator('input:not([type=hidden]),textarea,select').evaluateAll(els => els.filter(el => !el.labels?.length && !el.getAttribute('aria-label') && !el.getAttribute('aria-labelledby')).map(el => el.outerHTML.slice(0,140))); check(missing.length === 0, JSON.stringify(missing)); }); await test('20 README includes run instructions', async () => { const dir = path.join(root,lane,'project'); const filename = fs.readdirSync(dir).find(n => /^readme/i.test(n)); check(!!filename, 'README missing'); const text = fs.readFileSync(path.join(dir,filename),'utf8'); check(/http.server/.test(text) && text.length > 100, 'run instructions missing'); }); await page.setViewportSize({ width: 1440, height: 1000 }); const layout = await page.evaluate(() => ({ title: document.title, headings: [...document.querySelectorAll('h1,h2,h3')].map(e=>e.textContent), font: getComputedStyle(document.body).fontFamily, background: getComputedStyle(document.body).backgroundColor, buttons: [...document.querySelectorAll('button')].map(e=>({text:e.textContent.trim(),label:e.getAttribute('aria-label')})), stylesheetCount: document.styleSheets.length })); const data = { lane, layout, results, passed: results.filter(x => x.pass).length, total: results.length, pageErrors: errors, verifiedAt: new Date().toISOString(), verdict: results.every(x => x.pass) ? 'pass' : 'fail' }; fs.writeFileSync(path.join(root,lane,'verification.json'),JSON.stringify(data,null,2)); await context.close(); return data; } (async () => { const browser = await chromium.launch({ headless: true, channel: 'chrome' }); const results = await Promise.all(lanes.map(lane => verify(browser,lane))); fs.writeFileSync(path.join(root,'verification.json'), JSON.stringify(results,null,2)); console.log(results.map(x => ({ lane:x.lane, passed:x.passed, total:x.total, failed:x.results.filter(t => !t.pass) }))); await browser.close(); })().catch(e => { console.error(e); process.exit(1); });