gitoriaLog in with ident

gitoria

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Branchmain4f47843egate: ticket links use the tickets short URL (/<slug>/<n>, tickets#25)mremain/tests/firefox.mjs

5.2 KB

  1. // tests/firefox.mjs — a REAL Firefox for the gate (mission 033: the creator uses Firefox and saw full page loads).
  2. // Headless Firefox driven over WebDriver BiDi (Firefox's own protocol, `--remote-debugging-port`, ws …/session), no
  3. // driver or npm package. Host names are mapped to 127.0.0.1 with the pref `network.dns.localDomains` (exact names).
  4. // Clicks are REAL pointer input (input.performActions), not element.click().
  5. // const ff = await launchFirefox({ port: 8730, hosts: ['gitoria.test', 'alpha.gitoria.test'] });
  6. // await ff.goto(url); await ff.click('#navcode'); await ff.waitFor('expr'); await ff.evaluate('expr'); await ff.close();
  7. import { spawn } from 'node:child_process';
  8. import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
  9. import { tmpdir } from 'node:os';
  10. import { join } from 'node:path';
  11. const sleep = (ms) => new Promise(r => setTimeout(r, ms));
  12. export async function launchFirefox({ port, hosts = [], bin = process.env.HL_FIREFOX || '/usr/bin/firefox' }) {
  13. const profile = mkdtempSync(join(tmpdir(), 'gitoria-ff-'));
  14. writeFileSync(join(profile, 'user.js'), [
  15. `user_pref("network.dns.localDomains", ${JSON.stringify(hosts.join(','))});`,
  16. 'user_pref("browser.shell.checkDefaultBrowser", false);',
  17. 'user_pref("datareporting.policy.dataSubmissionEnabled", false);',
  18. 'user_pref("toolkit.telemetry.reportingpolicy.firstRun", false);',
  19. 'user_pref("browser.aboutwelcome.enabled", false);',
  20. 'user_pref("network.proxy.type", 0);',
  21. ].join('\n') + '\n');
  22. const proc = spawn(bin, ['--headless', '--no-remote', '--profile', profile, `--remote-debugging-port=${port}`, 'about:blank'], { stdio: ['ignore', 'pipe', 'pipe'] });
  23. let out = '';
  24. proc.stdout.on('data', d => out += d); proc.stderr.on('data', d => out += d);
  25. let ws = null;
  26. for (let i = 0; i < 80 && !ws; i++) {
  27. await sleep(250);
  28. if (!/WebDriver BiDi listening/.test(out)) continue;
  29. ws = await new Promise((res) => { const w = new WebSocket(`ws://127.0.0.1:${port}/session`); w.onopen = () => res(w); w.onerror = () => res(null); });
  30. }
  31. if (!ws) { try { proc.kill('SIGKILL'); } catch {} rmSync(profile, { recursive: true, force: true }); throw new Error('firefox did not open BiDi\n' + out.slice(-800)); }
  32. let next = 0; const pending = new Map();
  33. const errors = [];
  34. ws.onmessage = (ev) => {
  35. const m = JSON.parse(ev.data);
  36. if (m.id != null && pending.has(m.id)) { const p = pending.get(m.id); pending.delete(m.id); m.type === 'error' ? p.rej(new Error(m.error + ': ' + m.message)) : p.res(m.result); return; }
  37. if (m.method === 'log.entryAdded' && m.params && m.params.level === 'error') errors.push(m.params.text || '');
  38. };
  39. const send = (method, params = {}) => new Promise((res, rej) => { const id = ++next; pending.set(id, { res, rej }); ws.send(JSON.stringify({ id, method, params })); });
  40. await send('session.new', { capabilities: {} });
  41. await send('session.subscribe', { events: ['log.entryAdded'] });
  42. const tree = await send('browsingContext.getTree', {});
  43. const context = tree.contexts[0].context;
  44. const evaluate = async (expression) => {
  45. const r = await send('script.evaluate', { expression, target: { context }, awaitPromise: true, resultOwnership: 'none' });
  46. if (r.type === 'exception') throw new Error('evaluate: ' + (r.exceptionDetails && r.exceptionDetails.text));
  47. // primitives only (string, number, boolean, null): an expression that wants to hand back more returns JSON.stringify(…)
  48. const v = r.result; return v && 'value' in v && typeof v.value !== 'object' ? v.value : null;
  49. };
  50. const waitFor = async (expression, { timeout = 15000, label = expression } = {}) => {
  51. const end = Date.now() + timeout;
  52. while (Date.now() < end) { try { const v = await evaluate(expression); if (v) return v; } catch {} await sleep(100); }
  53. throw new Error('firefox waitFor timed out: ' + label);
  54. };
  55. return {
  56. context, errors,
  57. evaluate,
  58. waitFor,
  59. goto: (url) => send('browsingContext.navigate', { context, url, wait: 'complete' }),
  60. setCookie: (name, value, domain) => send('storage.setCookie', { cookie: { name, value: { type: 'string', value }, domain, path: '/' } }),
  61. async click(selector, { timeout = 15000 } = {}) {
  62. const box = await waitFor(`(() => { const el = document.querySelector(${JSON.stringify(selector)}); if (!el) return null; el.scrollIntoView({ block: 'center' }); const b = el.getBoundingClientRect(); return b.width ? JSON.stringify({ x: Math.round(b.left + b.width / 2), y: Math.round(b.top + b.height / 2) }) : null; })()`, { timeout, label: 'click ' + selector });
  63. const { x, y } = JSON.parse(box);
  64. await send('input.performActions', { context, actions: [{ type: 'pointer', id: 'mouse', parameters: { pointerType: 'mouse' }, actions: [{ type: 'pointerMove', x, y }, { type: 'pointerDown', button: 0 }, { type: 'pointerUp', button: 0 }] }] });
  65. },
  66. async close() {
  67. try { await send('session.end', {}); } catch {}
  68. try { ws.close(); } catch {}
  69. try { proc.kill('SIGTERM'); } catch {}
  70. await new Promise(r => { if (proc.exitCode !== null || proc.signalCode !== null) return r(); proc.once('exit', r); setTimeout(() => { try { proc.kill('SIGKILL'); } catch {} r(); }, 4000); });
  71. rmSync(profile, { recursive: true, force: true });
  72. },
  73. };
  74. }

Branches

  • mainmain branch

Latest commits

  • 4f47843egate: ticket links use the tickets short URL (/<slug>/<n>, tickets#25)mre
  • 86605446mission 002 (code order) 4/4: README file map + import order + 'Same output' test + gate run with a tickets HEAD copy, STATUS (entry, lessons), LOG, report; tests/realdata-baseline.mjs + realdata-compare.py (a cleanup answers the same on live data: pages, modules, API, git over HTTPS and SSH, faces), tests/letcount.pymre
  • cc7bf7bamission 002 (code order) 3/4: let only where a variable is reassigned or re-bound in a loop body (289 lets → plain declarations; 213 left: 125 reassigned, 88 loop-bound; no member/import/param clash). gates 200/0, 46/0, 44/0; real-data reads + writes identical (browser modules: var → const only)mre
  • 090a20c6mission 002 (code order) 2/4: one lib/ file per topic — git.hl split into git (calls, branches, init, temp folder) / homepage / code / pulls / releases (+ git-helpers: paths, ids, |||PR/|||RL markers); repos-helpers, tickets-helpers, transport-helpers; util.hl = localtime + env, storage dir, addresses, lists, text checks, one newest-first sort (was 3 copies); the function routes out of project.hl into lib/api.hl (thin; plumbing in api-helpers.hl), sshgate.hl folded into api.hl + sshkeys.hl keyLine + repos.hl mayPush; 'Make main' and the merge answer out of the faces (code.hl makeMain, pulls.hl pullsView), one login helper (users.hl userOfLoginCode); project.hl is the map. Session-writing routes get &req + &server.sessions. gates 200/0, 46/0, 44/0; real-data identical except /login/failed now shows the parked reason for a browser that already had a session (the old copy-of-req lost it)mre
  • 110c2799mission 002 (code order) 1/4: .hl files out of the root — lib/ (api, git, localtime, markdown, repos, sshgate, sshkeys, tickets, tokens, transport, users), components/styles.hl; jsoncheck.hl removed (imported nowhere); import paths only. gates 200/0, 46/0, 44/0; real-data reads + writes identicalmre
  • fdfb4b1bgitoria: Hybriel master 06617221 (plugin allocators 3a781359 + 413f60e4, mpackdb 2cb7ae5e, http1 773de63e); gates 200/0, 46/0, 44/0mre
  • 5b46ac84antcolony#40: LOG.md — missions 069/072 are antcolony missions (report paths on Byrodin)mre
  • 5602ff41gitoria: Hybriel master 190aa11d (fc838894 GC correctness, #127 mountKids by reference, #126, #48) — tracker README flat; gates 200/0, 46/0, 44/0mre
  • e85eaf01gitoria: 069 round 2 — hybriel 1a096ad3 not adopted (Markdown SSR still grows); browser gate waits for the server-side logout before restartmre
  • 09ce4f3fgitoria: mission 069 re-vendor hybriel 8efba065 stopped (big SSR pages grow + slow down); lambda audit clean; old vendor keptmre
  • 3dc43108antcolony#40: mission references point to the moved missionsmre
  • 8d9450fdantcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
  • 205d5fe4gitoria: Hybriel master ff51cf46; ssh keys/tokens no double rows (session sync); gates follow #20mre
  • 9b27cb26gitoria#21: installable app (manifest, service worker, offline start page), own iconmre
  • 68dcb603deploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
  • e2deed6dgitoria#20: "Add code" only on the Code page of an empty repository, no collapsiblemre
  • 8bb97ffddeploy.sh: never send .git or .gitignore to Byrodinmre
  • fd981932State of 2026-09-27; bin/ no longer tracked (Hybriel commit is in README)mre
  • 4a2d7125initial commitmre