Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_qr.mjs

5.0 KiB, 1 run

created by r2519314175:613, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1// verify_qr.mjs — device pairing shows a scannable QR, and a scanned #pair=
2// deep link lands on the redeem dialog with the code filled in.
3//
4// Drives the real wasm QR encoder (fe2o3_graphics::qr, exposed as window.DaimondQR) and
5// the real pairing UI against the live gateway, so it needs the dev stack up: the app
6// (DAIMOND_PORT, default 8777) and the gateway on :9002.
7//
8// 1. DaimondQR.matrix(url) returns a square module grid of a sane size.
9// 2. "Link another device" draws a QR canvas with real dark modules.
10// 3. Opening the app at /#pair=<code> opens the redeem dialog, code prefilled.
11import { open, shot, errors, APP } from './harness.mjs';
12
13const ok = [], bad = [];
14const check = (name, pass, detail) => {
15 (pass ? ok : bad).push(name + (detail ? ' — ' + detail : ''));
16 console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : ''));
17};
18const until = async (page, fn, arg, ms = 6000) => {
19 const t0 = Date.now();
20 for (;;) {
21 try { if (await page.evaluate(fn, arg)) return true; } catch (e) {}
22 if (Date.now() - t0 > ms) return false;
23 await new Promise(r => setTimeout(r, 60));
24 }
25};
26
27const s = await open({ name: 'qr', signIn: true, connect: true });
28const { page } = s;
29
30await page.waitForFunction(
31 () => !!window.DaimondQR && !!window.DaimondPairing && window.DaimondGateway
32 && DaimondGateway.state().authed,
33 null, { timeout: 12000 }).catch(() => {});
34
35try {
36 // ── 1. The wasm encoder. ──────────────────────────────────────────────────
37 const grid = await page.evaluate(() => {
38 const m = window.DaimondQR.matrix('https://daimond.oxedyne.com/#pair=ABCDEFGH2345');
39 const len = m ? m.length : 0;
40 const n = Math.round(Math.sqrt(len));
41 let dark = 0;
42 for (let i = 0; i < len; i++) if (m[i]) dark++;
43 return { len, n, square: n * n === len, dark };
44 });
45 check('DaimondQR.matrix returns a square grid', grid.square && grid.len > 0, `${grid.n}×${grid.n}`);
46 check('the QR is a valid version (side ≥ 21, odd)', grid.n >= 21 && grid.n % 2 === 1, `side ${grid.n}`);
47 check('the grid has both dark and light modules',
48 grid.dark > 0 && grid.dark < grid.len, `${grid.dark}/${grid.len} dark`);
49
50 // ── 2. The Link dialog draws a QR canvas. ─────────────────────────────────
51 await page.evaluate(() => window.DaimondPairing.showLink());
52 await until(page, () => document.querySelector('.pair-qr'));
53 const qr = await page.evaluate(() => {
54 const c = document.querySelector('canvas.pair-qr');
55 if (!c) return { present: false };
56 // Sample the canvas: a real QR has black pixels on a white ground.
57 const ctx = c.getContext('2d');
58 const data = ctx.getImageData(0, 0, c.width, c.height).data;
59 let black = 0, white = 0;
60 for (let i = 0; i < data.length; i += 4) {
61 if (data[i] < 40 && data[i + 1] < 40 && data[i + 2] < 40) black++;
62 else if (data[i] > 215 && data[i + 1] > 215 && data[i + 2] > 215) white++;
63 }
64 return { present: true, w: c.width, black, white };
65 });
66 check('the Link dialog renders a QR canvas', qr.present, qr.present ? `${qr.w}px` : 'missing');
67 check('the QR canvas has dark modules on a light ground',
68 qr.present && qr.black > 100 && qr.white > 100, `black=${qr.black}, white=${qr.white}`);
69 await shot(s, 'pair-qr');
70
71 // ── 3. A scanned #pair= deep link opens redeem, prefilled. ────────────────
72 // A scan opens the URL in a fresh tab, so test a brand-new page loaded at the
73 // deep link, exactly as the other phone's camera would.
74 const CODE = 'TESTCODE2345';
75 const page2 = await s.browser.newPage();
76 await page2.goto(APP + '/#pair=' + CODE, { waitUntil: 'domcontentloaded' });
77 const opened = await (async () => {
78 const t0 = Date.now();
79 for (;;) {
80 try { if (await page2.evaluate(() => !!document.querySelector('.pair-input'))) return true; } catch (e) {}
81 if (Date.now() - t0 > 8000) return false;
82 await new Promise(r => setTimeout(r, 80));
83 }
84 })();
85 const prefilled = opened ? await page2.evaluate(() =>
86 (document.querySelector('.pair-input') || {}).value || '') : '';
87 check('a #pair= link opens the redeem dialog prefilled', prefilled === CODE, `"${prefilled}"`);
88 const hashCleared = await page2.evaluate(() => location.hash);
89 check('the one-time code is stripped from the URL', !/pair=/.test(hashCleared), `hash="${hashCleared}"`);
90 await page2.close();
91
92 // A free account's idle sync push is refused with 402 -- cross-device sync is
93 // a Pro capability -- and the browser logs the failed request. That is the
94 // product working, not this feature failing, so it is ambient here too.
95 const errs = errors(s).filter(e => !/status of 401|status of 402/.test(e));
96 check('no console errors (other than ambient sign-in 401s and the free-tier sync 402)',
97 errs.length === 0, errs.slice(0, 3).join(' | '));
98} catch (e) {
99 check('run completed without throwing', false, e.message);
100}
101
102console.log(`\n${ok.length} passed, ${bad.length} failed`);
103await s.close();
104process.exit(bad.length ? 1 : 0);