Download source/scripts/verify_playground_layout.cjs from andyshu/opensysone: direct link, hf CLI and curl.
- Browser
- Download file 13.3 kB
-
https://huggingface.co/andyshu/opensysone/resolve/main/source/scripts/verify_playground_layout.cjs
- Command line
-
hf download hf://andyshu/opensysone/source/scripts/verify_playground_layout.cjs
-
curl -L -o verify_playground_layout.cjs https://huggingface.co/andyshu/opensysone/resolve/main/source/scripts/verify_playground_layout.cjs
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; }); | |