Download tests/coverage.mjs from SharpMeow/unroll: direct link, hf CLI and curl.
- Browser
- Download file 7.08 kB
-
https://huggingface.co/spaces/SharpMeow/unroll/resolve/main/tests/coverage.mjs
- Command line
-
hf download hf://spaces/SharpMeow/unroll/tests/coverage.mjs
-
curl -L -o coverage.mjs https://huggingface.co/spaces/SharpMeow/unroll/resolve/main/tests/coverage.mjs
7.08 kB
| // API coverage test: every interactive element in every UI state must carry a unique data-testid | |
| // and a data-api naming a window.unroll method that exists. Exit code 1 on any miss. | |
| // Usage: node tests/coverage.mjs [baseURL] (needs playwright-core and a Chrome; CHROME=/path) | |
| import { chromium } from 'playwright-core'; | |
| const BASE = process.argv[2] || 'http://localhost:8080/'; | |
| const SEL = 'button, a[href], input, select, textarea, summary, [role=button], [role=tab], [role=slider], [tabindex]:not([tabindex="-1"]), canvas, label:has(input)'; | |
| const browser = await chromium.launch({ executablePath: process.env.CHROME || '/usr/bin/google-chrome', args: ['--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] }); | |
| const page = await browser.newPage({ viewport: { width: 1280, height: 800 } }); | |
| page.setDefaultTimeout(180000); // shared CI boxes can be slow; real browsers are far faster | |
| const pageErrors = []; | |
| page.on('pageerror', (e) => pageErrors.push(String(e))); | |
| const seen = new Map(); const misses = []; | |
| async function audit(state, scope = 'body') { | |
| const els = await page.evaluate(([SEL, scope]) => { | |
| const resolve = (path) => path.split('.').reduce((o, k) => (o == null ? undefined : o[k]), window.unroll); | |
| return [...document.querySelector(scope).querySelectorAll(SEL)].map((el) => { | |
| const api = el.dataset.api || ''; | |
| const path = api.split('(')[0].trim(); | |
| return { tag: el.tagName.toLowerCase(), id: el.id, text: (el.getAttribute('aria-label') || el.textContent || '').trim().slice(0, 40), testid: el.dataset.testid || '', api, ok: !!path && typeof resolve(path) === 'function', named: !!(el.getAttribute('aria-label') || el.getAttribute('aria-labelledby') || (el.textContent || '').trim() || el.closest('label')) }; | |
| }); | |
| }, [SEL, scope]); | |
| for (const e of els) { | |
| const key = e.testid || `${state}:${e.tag}#${e.id}:${e.text}`; | |
| if (!e.testid) misses.push(`[${state}] ${e.tag}#${e.id} "${e.text}": no data-testid`); | |
| if (!e.ok) misses.push(`[${state}] ${e.testid || e.tag}: data-api "${e.api}" does not resolve to a window.unroll method`); | |
| if (!e.named) misses.push(`[${state}] ${e.testid || e.tag}: no accessible name`); | |
| if (!seen.has(key)) seen.set(key, { ...e, state }); | |
| } | |
| const dup = await page.evaluate(() => { const c = {}; for (const el of document.querySelectorAll('[data-testid]')) c[el.dataset.testid] = (c[el.dataset.testid] || 0) + 1; return Object.keys(c).filter((k) => c[k] > 1); }); | |
| for (const d of dup) misses.push(`[${state}] duplicate data-testid "${d}"`); | |
| } | |
| const u = (code) => page.evaluate(code); | |
| await page.goto(BASE, { waitUntil: 'load' }); | |
| await page.waitForFunction(() => window.unroll); | |
| await u(() => window.unroll.waitFor('ready', { timeout: 60000 })); | |
| await audit('initial'); | |
| await u(() => window.unroll.panel('about')); await audit('about'); | |
| await u(() => window.unroll.panel('help')); await audit('help'); | |
| await u(() => window.unroll.panel('embed')); await audit('embed-panel'); | |
| await u(() => window.unroll.panel('open')); await audit('open-own'); | |
| await u(() => window.unroll.panel('open', false)); | |
| await u(() => window.unroll.destroy.mode(true)); await audit('destroy'); | |
| await u(() => window.unroll.destroy.sandbox(true)); await audit('destroy-sandbox'); | |
| await u(() => window.unroll.destroy.sandbox(false)); | |
| await u(() => window.unroll.destroy.mode(false)); | |
| await u(async () => { const s = await window.unroll.getState(); const ov = s.layers.available.find((l) => l.id === 'overlay'); const r = await window.unroll.focus(ov ? ov.windings[0] : undefined, ov ? { view: 'compare' } : {}); if (!r.ok) throw new Error(r.error); }); | |
| await audit('closeup'); | |
| await u(() => window.unroll.focus(null)); | |
| // schema and implementation agree, including add-ons such as bench | |
| const api = await u(async () => { | |
| const sch = (await window.unroll.schema()).methods; | |
| const resolve = (path) => path.split('.').reduce((o, k) => (o == null ? undefined : o[k]), window.unroll); | |
| return { missing: Object.keys(sch).filter((k) => typeof resolve(k) !== 'function'), count: Object.keys(sch).length, extensions: (await window.unroll.schema()).extensions, bench: !!window.unroll.bench, benchMethods: window.unroll.bench ? Object.keys(window.unroll.bench) : [] }; | |
| }); | |
| for (const m of api.missing) misses.push(`schema lists ${m} but window.unroll has no such function`); | |
| for (const m of api.benchMethods) if (typeof (await u((m) => typeof window.unroll.bench[m], m)) !== 'string') misses.push(`bench.${m} unreachable`); | |
| // a scroll opened from file bytes: its saved-list buttons and the mesh scroll UI | |
| await u(async () => { const ex = await window.unroll.open.example(); window.unroll.open.fromFiles([{ name: ex.name, data: ex.base64 }]); }); | |
| await page.waitForURL(/s=local-/, { timeout: 60000 }); | |
| await page.waitForFunction(() => window.unroll); | |
| await u(() => window.unroll.waitFor('ready', { timeout: 60000 })); | |
| await audit('local-scroll'); | |
| await u(() => window.unroll.panel('open')); | |
| await page.waitForSelector('[data-testid^="open-saved-open-"]', { timeout: 10000 }).catch(() => {}); | |
| await audit('open-own-saved'); | |
| // load-error state: the recovery buttons | |
| const errorsBefore = pageErrors.length; | |
| await page.goto(BASE + '?scroll=local-missing', { waitUntil: 'load' }); | |
| await page.waitForSelector('[data-testid="load-back"]', { state: 'attached', timeout: 60000 }); | |
| await audit('load-error', '#loader'); // the rest of the page sits behind the error screen | |
| // the load failure itself is expected here | |
| pageErrors.splice(errorsBefore).filter((e) => !/removed from this one/.test(e)).forEach((e) => pageErrors.push(e)); | |
| // clean up the stored example | |
| await page.goto(BASE, { waitUntil: 'load' }); | |
| await page.waitForFunction(() => window.unroll && window.unroll.open); | |
| await u(async () => { for (const s of (await window.unroll.open.listLocal()).scrolls) await window.unroll.open.removeLocal(s.key); }); | |
| const MUST = ['destroy-tool-burn', 'destroy-tool-water', 'destroy-sandbox', 'roll-up', 'embed', 'embed-copy', 'embed-code', 'fullscreen', 'embed-activate', 'embed-open-full', 'view-3d', 'layer-ink', 'unroll-slider', 'closeup-tab-compare', 'closeup-tab-overlay', 'compare-slider', 'load-back', 'load-retry', 'help', 'help-close']; | |
| for (const m of MUST) if (![...seen.values()].some((e) => e.testid === m)) misses.push(`expected control ${m} was never seen`); | |
| if (![...seen.values()].some((e) => e.testid.startsWith('open-saved-open-'))) misses.push('saved scroll buttons were never seen'); | |
| await browser.close(); | |
| const n = seen.size; | |
| console.log(JSON.stringify({ elements: n, states: ['initial', 'about', 'help', 'embed-panel', 'open-own', 'destroy', 'destroy-sandbox', 'closeup', 'local-scroll', 'open-own-saved', 'load-error'], apiMethods: api.count, extensions: api.extensions, bench: api.bench ? api.benchMethods : 'not present (hook ready)', covered: `${[...seen.values()].filter((e) => e.testid && e.ok).length}/${n}`, testids: [...seen.values()].map((e) => `${e.testid} -> ${e.api}`), misses, pageErrors }, null, 1)); | |
| process.exit(misses.length || pageErrors.length ? 1 : 0); | |