gitoriaLog in with ident

components

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Main branchmain3bc905c5components #15 (mission 003) 2/2: docs (README Look, STATUS, LOG), reportmremain/tests/same-output.mjs

7.7 KB

  1. // tests/same-output.mjs (components mission 002): same-output baseline of a CODE-ONLY COPY of a components tree.
  2. // node tests/same-output.mjs <tree copy> <outdir> (README "Test" → "Same output"; refuses a tree with .git: it wipes <tree>/storage)
  3. // Server on 8750 (cwd = tree, its own storage/), Chrome 8751-8759. Saves every HTTP answer (pages, READMEs, icons, css,
  4. // manifest, worker, every browser module a page loads), the hydrated DOM of every page, and a file upload (drop) on
  5. // /file-upload + a md-editor drop: the component's DOM afterwards and the sha256 of every stored file. Stops all by PID.
  6. import { spawn } from 'node:child_process';
  7. import { writeFileSync, mkdirSync, rmSync, readdirSync, readFileSync, existsSync } from 'node:fs';
  8. import { join, resolve } from 'node:path';
  9. import { createHash } from 'node:crypto';
  10. import { launchBrowser } from './cdp.mjs';
  11. if (!process.env.HL_CHROME && existsSync('/opt/google/chrome/chrome')) process.env.HL_CHROME = '/opt/google/chrome/chrome';
  12. const TREE = resolve(process.argv[2]), OUT = resolve(process.argv[3]);
  13. 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); }
  14. const PORT = 8750, BASE = `http://127.0.0.1:${PORT}`;
  15. rmSync(OUT, { recursive: true, force: true }); mkdirSync(OUT, { recursive: true });
  16. rmSync(join(TREE, 'storage'), { recursive: true, force: true });
  17. const SES = join(OUT, '..', 'sessions-' + process.pid); rmSync(SES, { recursive: true, force: true }); mkdirSync(SES, { recursive: true });
  18. const sleep = (ms) => new Promise(r => setTimeout(r, ms));
  19. let log = '';
  20. 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'] });
  21. server.stdout.on('data', d => log += d); server.stderr.on('data', d => log += d);
  22. let browser = null;
  23. const n = { http: 0, dom: 0 };
  24. const save = (kind, key, text) => { writeFileSync(join(OUT, kind + '__' + key.replace(/\?v=\w+/, '').replace(/[^\w.-]+/g, '_')), text); n[kind === 'http' ? 'http' : 'dom']++; };
  25. try {
  26. 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); }
  27. const names = ['month-view', 'date-picker', 'range-picker', 'pagination', 'md-editor', 'code-editor', 'file-upload', 'modal'];
  28. const pages = ['/', ...names.map(c => '/' + c)];
  29. 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'];
  30. const seen = new Set();
  31. const get = async (u) => {
  32. if (seen.has(u)) return; seen.add(u);
  33. const r = await fetch(BASE + u); const b = Buffer.from(await r.arrayBuffer());
  34. const ct = r.headers.get('content-type') || '';
  35. const body = /text|json|javascript|svg|markdown|css|manifest/.test(ct) ? b.toString('utf8') : 'sha256 ' + createHash('sha256').update(b).digest('hex');
  36. save('http', u, `${r.status} ${ct} ${r.headers.get('cache-control') || ''}\n${body}`);
  37. if (/text\/html|javascript/.test(ct)) for (const m of body.matchAll(/["'(](\/(?:__hl\/[\w./-]+|components\/[\w./-]+\.hl)(?:\?v=\w+)?)/g)) await get(m[1]);
  38. };
  39. for (const u of urls) await get(u);
  40. browser = await launchBrowser({ debugPortRange: [8751, 8759] });
  41. const page = await browser.newPage();
  42. await page.send('Emulation.setDeviceMetricsOverride', { width: 1280, height: 900, deviceScaleFactor: 1, mobile: false });
  43. const ev = (e) => page.evaluate(e);
  44. const hydrated = () => page.waitFor('!!window.__hl && window.__hl.socket && window.__hl.socket.readyState === 1', { timeout: 30000, label: 'hydrated' });
  45. const dom = `(() => { const c = document.body.cloneNode(true); c.querySelectorAll('script').forEach(s => s.remove()); return c.outerHTML; })()`;
  46. for (const p of pages) { await page.goto(BASE + p, { timeout: 45000 }); await hydrated(); await sleep(700); save('dom', p, await ev(dom)); }
  47. // file upload: drop a 300 KB file, then choose one through the input
  48. await page.goto(BASE + '/file-upload', { timeout: 45000 }); await hydrated(); await sleep(600);
  49. 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 })); })()`);
  50. await page.waitFor(`document.querySelector('file-upload .state').textContent === 'Uploaded'`, { timeout: 30000, label: 'uploaded' });
  51. await sleep(300); save('dom', 'upload-drop', await ev(`document.querySelector('file-upload').outerHTML`));
  52. writeFileSync(join(OUT, '..', 'pick.txt'), 'hello m002');
  53. const { root } = await page.send('DOM.getDocument');
  54. const { nodeId } = await page.send('DOM.querySelector', { nodeId: root.nodeId, selector: 'file-upload input[type=file]' });
  55. await page.send('DOM.setFileInputFiles', { files: [join(OUT, '..', 'pick.txt')], nodeId });
  56. await page.waitFor(`document.querySelectorAll('file-upload ul.uploaded li').length === 2`, { timeout: 30000, label: 'chosen' });
  57. await sleep(300); save('dom', 'upload-choose', await ev(`document.querySelector('file-upload').outerHTML`));
  58. // md-editor: drop a png and a .exe-named file (stored as .bin) into the second editor
  59. await page.goto(BASE + '/md-editor', { timeout: 45000 }); await hydrated(); await sleep(600);
  60. const PNG = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
  61. 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 })); })()`);
  62. await page.waitFor(`document.querySelectorAll('md-editor')[1].querySelector('input.value').value.includes('run.exe')`, { timeout: 30000, label: 'md uploaded' });
  63. await sleep(500);
  64. save('dom', 'md-upload', await ev(`document.querySelector('md-editor-demo').outerHTML`));
  65. const fileUrl = await ev(`document.querySelectorAll('md-editor')[1].querySelector('input.value').value.match(/\\((\\/md-editor\\/files\\/[^)]+\\.png)\\)/)[1]`);
  66. const fr = await fetch(BASE + fileUrl);
  67. 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')}`);
  68. const st = [];
  69. 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')}`);
  70. save('http', 'storage-listing', st.sort().join('\n'));
  71. const probs = page.problems().filter(m => !/favicon|Failed to load resource/.test(m.text));
  72. save('dom', 'console-problems', probs.map(m => m.text).join('\n'));
  73. console.log(`saved http ${n.http}, dom ${n.dom} -> ${OUT}`);
  74. } catch (e) { console.log('BASELINE FAILED', e.stack || e, '\n', log.slice(-2000)); process.exitCode = 1; }
  75. finally {
  76. if (browser) { try { await browser.close(); } catch {} }
  77. server.kill('SIGTERM'); await new Promise(r => { if (server.exitCode !== null) return r(); server.once('exit', r); setTimeout(r, 3000); });
  78. rmSync(SES, { recursive: true, force: true });
  79. }

Branches

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