components
All repositories: gitoria
7.7 KB
// tests/same-output.mjs (components mission 002): same-output baseline of a CODE-ONLY COPY of a components tree.// node tests/same-output.mjs <tree copy> <outdir> (README "Test" → "Same output"; refuses a tree with .git: it wipes <tree>/storage)// Server on 8750 (cwd = tree, its own storage/), Chrome 8751-8759. Saves every HTTP answer (pages, READMEs, icons, css,// manifest, worker, every browser module a page loads), the hydrated DOM of every page, and a file upload (drop) on// /file-upload + a md-editor drop: the component's DOM afterwards and the sha256 of every stored file. Stops all by PID.import { spawn } from 'node:child_process';import { writeFileSync, mkdirSync, rmSync, readdirSync, readFileSync, existsSync } from 'node:fs';import { join, resolve } from 'node:path';import { createHash } from 'node:crypto';import { launchBrowser } from './cdp.mjs';if (!process.env.HL_CHROME && existsSync('/opt/google/chrome/chrome')) process.env.HL_CHROME = '/opt/google/chrome/chrome';const TREE = resolve(process.argv[2]), OUT = resolve(process.argv[3]);if (existsSync(join(TREE, '.git'))) { console.log('refused: ' + TREE + ' has .git — run it on a code-only copy (its storage/ is wiped)'); process.exit(2); }const PORT = 8750, BASE = `http://127.0.0.1:${PORT}`;rmSync(OUT, { recursive: true, force: true }); mkdirSync(OUT, { recursive: true });rmSync(join(TREE, 'storage'), { recursive: true, force: true });const SES = join(OUT, '..', 'sessions-' + process.pid); rmSync(SES, { recursive: true, force: true }); mkdirSync(SES, { recursive: true });const sleep = (ms) => new Promise(r => setTimeout(r, ms));let log = '';const server = spawn(join(TREE, 'bin/hybriel'), ['project.hl'], { cwd: TREE, env: { ...process.env, COMPONENTS_PORT: String(PORT), COMPONENTS_SESSIONS: SES + '/', COMPONENTS_WATCH: '0' }, stdio: ['ignore', 'pipe', 'pipe'] });server.stdout.on('data', d => log += d); server.stderr.on('data', d => log += d);let browser = null;const n = { http: 0, dom: 0 };const save = (kind, key, text) => { writeFileSync(join(OUT, kind + '__' + key.replace(/\?v=\w+/, '').replace(/[^\w.-]+/g, '_')), text); n[kind === 'http' ? 'http' : 'dom']++; };try {for (let i = 0; ; i++) { try { if ((await fetch(BASE + '/')).ok) break; } catch {} if (i > 80) throw new Error('no server\n' + log); await sleep(250); }const names = ['month-view', 'date-picker', 'range-picker', 'pagination', 'md-editor', 'code-editor', 'file-upload', 'modal'];const pages = ['/', ...names.map(c => '/' + c)];const urls = [...pages, ...names.map(c => `/${c}/README.md`), '/favicon.ico', '/icons/icon-192.png', '/icons/icon-512.png', '/icons/apple-touch-icon.png', '/icons/favicon.svg', '/__hl/manifest.webmanifest', '/__hl/sw.js', '/nope', '/md-editor/files/nope.png'];const seen = new Set();const get = async (u) => {if (seen.has(u)) return; seen.add(u);const r = await fetch(BASE + u); const b = Buffer.from(await r.arrayBuffer());const ct = r.headers.get('content-type') || '';const body = /text|json|javascript|svg|markdown|css|manifest/.test(ct) ? b.toString('utf8') : 'sha256 ' + createHash('sha256').update(b).digest('hex');save('http', u, `${r.status} ${ct} ${r.headers.get('cache-control') || ''}\n${body}`);if (/text\/html|javascript/.test(ct)) for (const m of body.matchAll(/["'(](\/(?:__hl\/[\w./-]+|components\/[\w./-]+\.hl)(?:\?v=\w+)?)/g)) await get(m[1]);};for (const u of urls) await get(u);browser = await launchBrowser({ debugPortRange: [8751, 8759] });const page = await browser.newPage();await page.send('Emulation.setDeviceMetricsOverride', { width: 1280, height: 900, deviceScaleFactor: 1, mobile: false });const ev = (e) => page.evaluate(e);const hydrated = () => page.waitFor('!!window.__hl && window.__hl.socket && window.__hl.socket.readyState === 1', { timeout: 30000, label: 'hydrated' });const dom = `(() => { const c = document.body.cloneNode(true); c.querySelectorAll('script').forEach(s => s.remove()); return c.outerHTML; })()`;for (const p of pages) { await page.goto(BASE + p, { timeout: 45000 }); await hydrated(); await sleep(700); save('dom', p, await ev(dom)); }// file upload: drop a 300 KB file, then choose one through the inputawait page.goto(BASE + '/file-upload', { timeout: 45000 }); await hydrated(); await sleep(600);await ev(`(() => { const dt = new DataTransfer(); dt.items.add(new File([new Uint8Array(307200).map((_, i) => i % 251)], 'data.bin', { type: 'application/octet-stream' })); document.querySelector('file-upload drop-zone').dispatchEvent(new DragEvent('drop', { dataTransfer: dt, bubbles: true, cancelable: true })); })()`);await page.waitFor(`document.querySelector('file-upload .state').textContent === 'Uploaded'`, { timeout: 30000, label: 'uploaded' });await sleep(300); save('dom', 'upload-drop', await ev(`document.querySelector('file-upload').outerHTML`));writeFileSync(join(OUT, '..', 'pick.txt'), 'hello m002');const { root } = await page.send('DOM.getDocument');const { nodeId } = await page.send('DOM.querySelector', { nodeId: root.nodeId, selector: 'file-upload input[type=file]' });await page.send('DOM.setFileInputFiles', { files: [join(OUT, '..', 'pick.txt')], nodeId });await page.waitFor(`document.querySelectorAll('file-upload ul.uploaded li').length === 2`, { timeout: 30000, label: 'chosen' });await sleep(300); save('dom', 'upload-choose', await ev(`document.querySelector('file-upload').outerHTML`));// md-editor: drop a png and a .exe-named file (stored as .bin) into the second editorawait page.goto(BASE + '/md-editor', { timeout: 45000 }); await hydrated(); await sleep(600);const PNG = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';await ev(`(() => { const ed = document.querySelectorAll('md-editor')[1]; const a = ed.querySelector('article'); a.focus(); const png = Uint8Array.from(atob('${PNG}'), c => c.charCodeAt(0)); const dt = new DataTransfer(); dt.items.add(new File([png], 'one.png', { type: 'image/png' })); dt.items.add(new File(['MZ'], 'run.exe', { type: 'application/octet-stream' })); a.dispatchEvent(new DragEvent('drop', { dataTransfer: dt, bubbles: true, cancelable: true, clientX: 50, clientY: 50 })); })()`);await page.waitFor(`document.querySelectorAll('md-editor')[1].querySelector('input.value').value.includes('run.exe')`, { timeout: 30000, label: 'md uploaded' });await sleep(500);save('dom', 'md-upload', await ev(`document.querySelector('md-editor-demo').outerHTML`));const fileUrl = await ev(`document.querySelectorAll('md-editor')[1].querySelector('input.value').value.match(/\\((\\/md-editor\\/files\\/[^)]+\\.png)\\)/)[1]`);const fr = await fetch(BASE + fileUrl);save('http', 'stored-png-served', `${fr.status} ${fr.headers.get('content-type')} ${fr.headers.get('content-security-policy')} ${createHash('sha256').update(Buffer.from(await fr.arrayBuffer())).digest('hex')}`);const st = [];for (const d of ['uploads', 'md-files']) for (const f of readdirSync(join(TREE, 'storage', d)).sort()) st.push(`${d} ${f.replace(/^\d+-[0-9a-f]+/, 'T-R')} ${createHash('sha256').update(readFileSync(join(TREE, 'storage', d, f))).digest('hex')}`);save('http', 'storage-listing', st.sort().join('\n'));const probs = page.problems().filter(m => !/favicon|Failed to load resource/.test(m.text));save('dom', 'console-problems', probs.map(m => m.text).join('\n'));console.log(`saved http ${n.http}, dom ${n.dom} -> ${OUT}`);} catch (e) { console.log('BASELINE FAILED', e.stack || e, '\n', log.slice(-2000)); process.exitCode = 1; }finally {if (browser) { try { await browser.close(); } catch {} }server.kill('SIGTERM'); await new Promise(r => { if (server.exitCode !== null) return r(); server.once('exit', r); setTimeout(r, 3000); });rmSync(SES, { recursive: true, force: true });}
Branches
- mainmain branch
Latest commits
- 3bc905c5components #15 (mission 003) 2/2: docs (README Look, STATUS, LOG), reportmre
- 5a98d77ecomponents #15 (mission 003) 1/2: black/white base look — greys for nuances, no WorldAPI fallbacks, no decorative frames; site + icon black/white; gate 230/0mre
- 3f93d716components code order (mission 002) 4/4: docs, tests/letcount.py, tests/same-output*, reportmre
- e747cfa0components code order (mission 002) 3/4: let only where reassigned (257 dropped, 0 never-reassigned left); gate 220/0, layouts gate 73/0, same outputmre
- 172914a3components code order (mission 002) 2/4: lib/uploads.hl (demo upload storage), thin demo handlers, project.hl map; gate 220/0, same outputmre
- d29f35b1components code order (mission 002) 1/4: styles.hl -> components/styles.hl; gate 220/0, same outputmre
- 09945750components: Hybriel master 06617221 (plugin allocators 3a781359 + 413f60e4, http1 773de63e); gate 220/0mre
- ede4d19acomponents: Hybriel master 190aa11d (fc838894 GC correctness, #126 closure scopes, #127); gate 220/0mre
- 2bb9a935components: Hybriel master 8efba065 (re-vendor round 069: #126 memory, #48 lambda copy)mre
- e4f01768antcolony#40: mission references point to the moved missionsmre
- d32b44e2antcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
- 0fb80b57components: Hybriel master ff51cf46 (re-vendor round)mre
- 57408b6ecomponents#14: installable app (manifest, service worker, offline index), own iconmre
- 317bd09fdeploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
- becbd59bcomponents#13: modal dialog (<dialog> based, focus wrap, backdrop/Esc/x close with reason, scroll lock, footer buttons)mre
- 367dd19adeploy.sh: never send .git or .gitignore to Byrodinmre
- 4b4dabe5State of 2026-09-27, before the move to gitoriamre