tracker
All repositories: gitoria
7.1 KB
// tests/browser.mjs — THE GATE of tracker.worldapi.org#1: this app's own server on its own storage, its OWN ident// (a copy of ident's code without .env, codes to a mail sink: tests/identkit.mjs, same as calendar's gate), a real// headless Chrome. Proves: signed out (header shows "Log in with ident") -> sign in -> the header selector shows// signed in, "Log out" appears, the page itself stays empty -> sign out -> signed out again (a reload confirms it).//// node tests/browser.mjs (TRACKER_GATE_PORT server, default 8700; TRACKER_GATE_IDENT_PORT ident, default 8701;// TRACKER_GATE_CHROME "8702-8709" = Chrome debug ports)import { spawn } from 'node:child_process';import { existsSync, mkdirSync, rmSync } from 'node:fs';import { dirname, join, resolve } from 'node:path';import { fileURLToPath } from 'node:url';import { launchBrowser } from './cdp.mjs';import { startIdent } from './identkit.mjs';if (!process.env.HL_CHROME && existsSync('/opt/google/chrome/chrome')) process.env.HL_CHROME = '/opt/google/chrome/chrome';const HERE = dirname(fileURLToPath(import.meta.url));const APP = resolve(HERE, '..');const PORT = Number(process.env.TRACKER_GATE_PORT || 8700);const [CH_FROM, CH_TO] = (process.env.TRACKER_GATE_CHROME || '8702-8709').split('-').map(Number);const IDENT_PORT = Number(process.env.TRACKER_GATE_IDENT_PORT || 8701);const BASE = `http://127.0.0.1:${PORT}`;const STORE = join(APP, '.scratch', 'gate-store');const IDENT_DIR = process.env.TRACKER_GATE_IDENT_DIR || [resolve(APP, '../ident.worldapi.org'), '/media/STORAGE/projects/ident.worldapi.org'].find(d => existsSync(join(d, 'project.hl')));const SCRATCH = join(APP, '.scratch');const SESSIONS = join(SCRATCH, 'gate-sessions');rmSync(SESSIONS, { recursive: true, force: true });rmSync(STORE, { recursive: true, force: true });mkdirSync(SESSIONS, { recursive: true });let failures = 0, passes = 0;function check(label, ok, detail = '') {console.log(`${ok ? 'ok ' : 'FAIL'} ${label}${ok ? '' : ' — ' + detail}`);if (ok) passes++; else failures++;}let ident = null, server = null, browser = null, log = '';async function stop() {if (ident) { try { await ident.stop(); } catch {} ident = null; }if (browser) { try { await browser.close(); } catch {} browser = null; }if (server) {try { server.kill('SIGTERM'); } catch {}await new Promise(r => { if (server.exitCode !== null || server.signalCode !== null) return r(); server.once('exit', r); setTimeout(r, 3000); });server = null;}}async function main() {ident = await startIdent({ identDir: IDENT_DIR, workDir: join(STORE, 'ident'), port: IDENT_PORT });const alice = await ident.signIn('[email protected]');const APPKEY = await ident.registerApp(alice, 'tracker (gate)', [BASE]);check('ident: our own ident runs (a copy without .env), tracker is registered', /^pk_[0-9a-f]{32}$/.test(APPKEY.key) && !existsSync(join(STORE, 'ident', 'ident-code', '.env')));server = spawn(join(APP, 'bin/hybriel'), ['project.hl'], { cwd: APP, env: { ...process.env, TRACKER_PORT: String(PORT), TRACKER_SESSIONS: SESSIONS + '/', TRACKER_STORAGE: join(STORE, 'mpackdb'), TRACKER_WATCH: '0',TRACKER_URL: BASE, IDENT_URL: ident.base, IDENT_EXCHANGE_URL: ident.base, TRACKER_KEY: APPKEY.key, TRACKER_SECRET: APPKEY.secret }, stdio: ['ignore', 'pipe', 'pipe'] });server.stdout.on('data', d => log += d); server.stderr.on('data', d => log += d);for (let i = 0; ; i++) {try { if ((await fetch(BASE + '/')).ok) break; } catch {}if (i > 80) throw new Error('tracker did not come up\n' + log);await new Promise(r => setTimeout(r, 250));}check('tracker is up', true);browser = await launchBrowser({ debugPortRange: [CH_FROM, CH_TO] });const page = await browser.newPage();page.captureNetwork();const ev = (e) => page.evaluate(e);const hydrated = () => page.waitFor('!!window.__hl && window.__hl.socket && window.__hl.socket.readyState === 1', { timeout: 15000, label: 'hydrated' });const loginAs = async (who) => {const code = await ident.selectorCode(who, APPKEY, BASE);await page.goto(BASE + '/login/callback?ident_code=' + code);await page.waitForSelector('#logout');await hydrated();};// ---- signed out: header shows the selector + "Log in with ident", the page itself is empty ----await page.goto(BASE + '/');await hydrated();check('signed out: the identity selector is in the header', await ev(`!!document.querySelector('#selector')`));check('signed out: "Log in with ident" shows, no "Log out"', (await ev(`!!document.querySelector('#loginbutton')`)) && !(await ev(`!!document.querySelector('#logout')`)));check('the page itself stays empty (no centered sign-in)', (await ev(`document.querySelector('main').textContent.trim()`)) === '');// ---- tracker.worldapi.org#3: no bottom border on the header, no border on filled buttons ----check('the header has no bottom border', (await ev(`getComputedStyle(document.querySelector('application-header')).borderBottomWidth`)) === '0px');check('"Log in with ident" has no border', (await ev(`getComputedStyle(document.querySelector('#loginbutton')).borderTopWidth`)) === '0px');check('the ident selector\'s "choose ident" button has no border', (await ev(`getComputedStyle(document.querySelector('#selector').shadowRoot.querySelector('[part~="button"]')).borderTopWidth`)) === '0px');let r = await fetch(BASE + '/login/callback', { redirect: 'manual' });check('login: /login/callback without a code → /login/failed', r.status === 302 && r.headers.get('location') === '/login/failed');// ---- sign in: the header selector flips to signed in, "Log out" appears, page stays empty ----await loginAs(alice);check('signed in: the selector is still in the header, now signed in', (await ev(`!!document.querySelector('#selector')`)) && (await ev(`document.querySelector('#selector').classList.contains('in')`)));check('signed in: "Log out" shows, no "Log in with ident"', (await ev(`!!document.querySelector('#logout')`)) && !(await ev(`!!document.querySelector('#loginbutton')`)));check('the page itself stays empty while signed in too', (await ev(`document.querySelector('main').textContent.trim()`)) === '');check('"Log out" (inverted) keeps its border', (await ev(`getComputedStyle(document.querySelector('#logout')).borderTopWidth`)) !== '0px');// ---- sign out: back to signed out, a reload confirms the app's own session (not just client state) ----await page.click('#logout');await page.waitForSelector('#loginbutton', { timeout: 15000 });check('sign out returns to the signed-out header', (await ev(`!!document.querySelector('#loginbutton')`)) && !(await ev(`!!document.querySelector('#logout')`)));await page.goto(BASE + '/');await hydrated();check('reload after sign-out stays signed out', (await ev(`!!document.querySelector('#loginbutton')`)) && !(await ev(`!!document.querySelector('#logout')`)));const problems = page.problems();check('no console errors/warnings', problems.length === 0, page.dumpConsole());}try {await main();} catch (e) {check('gate ran without throwing', false, e.stack || String(e));} finally {await stop();}console.log(`\n${passes} passed, ${failures} failed`);process.exit(failures === 0 ? 0 : 1);
Branches
- mainmain branch