opensysone / source /scripts /verify_playground_layout.cjs
andyshu's picture
Back up verified OpenSysOne training snapshot and pinned source
e60eab9 verified
Raw History Blame Contribute Delete
13.3 kB
/* Browser layout checks with an isolated fixture API. Never calls a model. */
const assert = require('node:assert/strict');
const fs = require('node:fs/promises');
const http = require('node:http');
const path = require('node:path');
const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright-core');
async function main() {
const output = process.argv[2];
if (!output || !process.env.CHROMIUM_PATH) throw new Error('Provide a new output directory and CHROMIUM_PATH.');
await fs.mkdir(output, { recursive: false });
const root = path.resolve(__dirname, '../web');
const catalog = [
{ id: 'fixture-main', label: 'Qwen3 4B · UI fixture', description: 'Browser fixture only; no model inference.', calibrated: false, max_tokens: 1024 },
{ id: 'fixture-small', label: 'Qwen3.5 2B · UI fixture', description: 'A second browser fixture.', calibrated: true, max_tokens: 1024 },
];
let failure = null;
let modelFailure = false;
let scoreCount = 0;
let lastRequest;
const server = http.createServer(async (request, response) => {
response.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'");
response.setHeader('Cache-Control', 'no-store');
const json = (status, body) => {
response.writeHead(status, { 'Content-Type': 'application/json' });
response.end(JSON.stringify(body));
};
if (request.url === '/api/models') return modelFailure
? json(503, { error: 'Fixture model connection unavailable. Please retry.' })
: json(200, { models: catalog, default_model: catalog[0].id });
if (request.url === '/api/status') return json(200, { status: 'ready', loaded_model_id: catalog[0].id });
if (request.url === '/api/score') {
let raw = '';
for await (const chunk of request) raw += chunk;
lastRequest = JSON.parse(raw);
scoreCount += 1;
await new Promise(resolve => setTimeout(resolve, 220));
if (failure) return json(failure.status, { error: failure.message });
return json(200, {
model: catalog.find(model => model.id === lastRequest.model),
probabilities: lastRequest.options.map((option, index) => ({ option, probability: index ? .3 / (lastRequest.options.length - 1) : .7 })),
elapsed_seconds: .22, input_tokens: 391,
});
}
if (request.url === '/favicon.ico') { response.writeHead(204); return response.end(); }
const name = request.url === '/' ? 'playground.html' : request.url.slice(1);
if (!['playground.html', 'playground.css', 'playground.js'].includes(name)) { response.writeHead(404); return response.end(); }
response.writeHead(200, { 'Content-Type': name.endsWith('.js') ? 'text/javascript' : name.endsWith('.css') ? 'text/css' : 'text/html' });
response.end(await fs.readFile(path.join(root, name)));
});
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
const url = `http://127.0.0.1:${server.address().port}`;
const browser = await chromium.launch({ executablePath: process.env.CHROMIUM_PATH, headless: true, args: ['--disable-gpu', '--no-sandbox'] });
const report = { fixture_only: true, scope: 'Responsive layout and interaction checks; no model inference or training changes.', started_utc: new Date().toISOString(), viewports: [], checks: [], errors: [] };
try {
const context = await browser.newContext({ permissions: ['clipboard-read', 'clipboard-write'], reducedMotion: 'reduce' });
const page = await context.newPage();
page.on('pageerror', error => report.errors.push(error.message));
page.on('console', message => {
if (/Content Security Policy|Refused to|violates.*directive/i.test(message.text())) report.errors.push(message.text());
});
const ready = () => page.waitForFunction(() => !document.getElementById('submit').disabled);
const tabs = () => page.locator('#workspace-tabs').isVisible();
const view = async name => { if (await tabs()) await page.locator(`#tab-${name}`).click(); };
const geometry = async label => {
const measured = await page.evaluate(() => {
const root = document.documentElement;
return { width: innerWidth, height: innerHeight, scrollWidth: root.scrollWidth, scrollHeight: root.scrollHeight, bodyWidth: document.body.scrollWidth, bodyHeight: document.body.scrollHeight };
});
assert(measured.scrollWidth <= measured.width + 1 && measured.bodyWidth <= measured.width + 1, `${label}: horizontal overflow ${JSON.stringify(measured)}`);
assert(measured.scrollHeight <= measured.height + 1 && measured.bodyHeight <= measured.height + 1, `${label}: vertical page overflow ${JSON.stringify(measured)}`);
const scroll = await page.evaluate(() => { window.scrollTo(200, 200); return { x: scrollX, y: scrollY }; });
assert.deepEqual(scroll, { x: 0, y: 0 }, `${label}: page can scroll`);
return measured;
};
const inView = async (selector, label) => {
const result = await page.locator(selector).evaluate(element => {
const box = element.getBoundingClientRect();
let clip = { left: 0, top: 0, right: innerWidth, bottom: innerHeight };
for (let parent = element.parentElement; parent; parent = parent.parentElement) {
const style = getComputedStyle(parent), parentBox = parent.getBoundingClientRect();
if (/(auto|scroll|hidden|clip)/.test(style.overflowX)) { clip.left = Math.max(clip.left, parentBox.left); clip.right = Math.min(clip.right, parentBox.right); }
if (/(auto|scroll|hidden|clip)/.test(style.overflowY)) { clip.top = Math.max(clip.top, parentBox.top); clip.bottom = Math.min(clip.bottom, parentBox.bottom); }
}
const style = getComputedStyle(element);
const minimumHeight = element.tagName === 'TEXTAREA' ? ['lineHeight', 'paddingTop', 'paddingBottom', 'borderTopWidth', 'borderBottomWidth'].reduce((sum, property) => sum + parseFloat(style[property]), 0) : 0;
return { x: box.x, y: box.y, width: box.width, height: box.height, minimumHeight, visible: box.width > 0 && box.height > 0 && box.left >= clip.left - 1 && box.right <= clip.right + 1 && box.top >= clip.top - 1 && box.bottom <= clip.bottom + 1 };
});
assert(result.visible, `${label}: ${selector} clipped or hidden ${JSON.stringify(result)}`);
assert(result.height >= result.minimumHeight - 1, `${label}: ${selector} has less than one readable line ${JSON.stringify(result)}`);
return result;
};
const score = async () => {
await ready();
await inView('#submit', 'Submit available');
await page.locator('#submit').click();
await page.locator('#loading-state').waitFor({ state: 'visible' });
await geometry('Loading');
await page.locator('#results-content').waitFor({ state: 'visible' });
await ready();
};
for (const [width, height] of [[1440, 900], [1366, 768], [1280, 720], [1024, 600], [768, 1024], [390, 844], [375, 667], [320, 568], [844, 390], [667, 375], [1024, 420], [390, 360], [320, 420]]) {
const label = `${width}x${height}`;
await page.setViewportSize({ width, height });
await page.goto(url);
await ready();
const compact = await tabs();
const measurements = await geometry(`${label} initial`);
await inView('#model', label);
await inView('#calibration-badge', label);
await inView('#context', label);
await inView('#submit', label);
await page.locator('#context').fill('An invented context. '.repeat(90));
await view('choices');
await inView('#question', label);
await inView('#options', label);
await inView('#submit', label);
await page.locator('#question').fill('Which option fits?');
await page.locator('#options').fill('First option\nSecond option\nThird option');
await geometry(`${label} choices`);
if (['1366x768', '320x568', '844x390'].includes(label)) {
await page.screenshot({ path: path.join(output, `${label}-choices.png`) });
await view('context');
await page.screenshot({ path: path.join(output, `${label}-context.png`) });
await view('choices');
}
await score();
await geometry(`${label} results`);
await inView('#copy-results', label);
assert.equal(await page.locator('.probability-item').count(), 3);
if (height >= 568) {
for (let index = 1; index <= 3; index += 1) await inView(`.probability-item:nth-child(${index})`, `${label} short answer list`);
}
assert.equal(lastRequest.options.length, 3);
if (compact) assert.equal(await page.locator('#tab-results').getAttribute('aria-selected'), 'true');
if (['1366x768', '390x844', '320x568', '844x390'].includes(label)) await page.screenshot({ path: path.join(output, `${label}-results.png`) });
await view('choices');
assert.equal(await page.locator('#question').inputValue(), 'Which option fits?');
await page.locator('#options').fill('Duplicate\nDuplicate');
const before = scoreCount;
await page.locator('#submit').click();
await inView('#error-message', label);
await inView('#submit', label);
await inView('#options', `${label} invalid options remain editable`);
await geometry(`${label} validation`);
assert.equal(scoreCount, before, 'Invalid input must not send a score request');
report.viewports.push({ ...measurements, compact, verified: ['context', 'choices', 'loading', 'results', 'validation', 'input preservation', 'visible controls'] });
}
await page.setViewportSize({ width: 390, height: 844 });
await page.reload(); await ready();
if (await tabs()) {
await page.locator('#tab-context').focus();
await page.keyboard.press('ArrowRight');
assert.equal(await page.locator('#tab-choices').getAttribute('aria-selected'), 'true');
await page.keyboard.press('End');
assert.equal(await page.locator('#tab-results').getAttribute('aria-selected'), 'true');
await page.keyboard.press('Home');
assert.equal(await page.locator('#tab-context').getAttribute('aria-selected'), 'true');
report.checks.push('Tab arrow/Home/End keyboard navigation');
}
await view('choices');
const answers = Array.from({ length: 24 }, (_, i) => `${i + 1}. A longer invented answer ${'with enough detail to wrap on a small screen. '.repeat(3)}`);
await page.locator('#options').fill(answers.join('\n'));
await score(); await geometry('Long results');
await inView('#copy-results', 'Long results');
await page.locator('#copy-results').click();
await page.waitForFunction(() => document.getElementById('copy-label').textContent === 'Copied');
const copied = JSON.parse(await page.evaluate(() => navigator.clipboard.readText()));
assert.equal(copied.probabilities.length, 24);
await page.locator('.probability-item').last().scrollIntoViewIfNeeded();
await geometry('Last result is reachable without page scrolling');
await inView('#copy-results', 'After internal result scrolling');
report.checks.push('Long answers remain reachable inside results; copy stays visible and exports all 24 options');
await view('context');
await page.locator('#context').fill('Changed context');
await view('results');
await inView('#stale-notice', 'Stale notice');
await geometry('Stale results');
await page.setViewportSize({ width: 1366, height: 768 });
await inView('#context', 'Resize to desktop');
await inView('#options', 'Resize to desktop');
await inView('#copy-results', 'Resize to desktop');
await geometry('Desktop resize');
await page.setViewportSize({ width: 320, height: 568 });
await view('choices');
failure = { status: 422, message: 'The complete prompt exceeds the supported token limit. Shorten the context or options before retrying.' };
await page.locator('#submit').click();
await page.waitForFunction(() => !document.getElementById('error-message').hidden);
await geometry('Server error'); await inView('#submit', 'Server error'); await inView('#error-message', 'Server error');
failure = null;
modelFailure = true;
await page.reload();
await page.locator('#retry-models').waitFor({ state: 'visible' });
await geometry('Connection error'); await inView('#retry-models', 'Connection retry');
modelFailure = false;
await page.locator('#retry-models').click(); await ready();
report.checks.push('Stale results, desktop/mobile resize, server error and model connection retry');
assert.deepEqual(report.errors, []);
report.status = 'passed';
} catch (error) {
report.status = 'failed';
report.failure = error.stack;
process.exitCode = 1;
} finally {
report.finished_utc = new Date().toISOString();
await fs.writeFile(path.join(output, 'layout-verification.json'), JSON.stringify(report, null, 2) + '\n');
await browser.close();
await new Promise(resolve => server.close(resolve));
}
console.log(JSON.stringify({ status: report.status, viewports: report.viewports.length, failure: report.failure, output }));
}
main().catch(error => { console.error(error); process.exitCode = 1; });