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. |
| 11 | import { open, shot, errors, APP } from './harness.mjs'; |
| 12 | |
| 13 | const ok = [], bad = []; |
| 14 | const check = (name, pass, detail) => { |
| 15 | (pass ? ok : bad).push(name + (detail ? ' — ' + detail : '')); |
| 16 | console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : '')); |
| 17 | }; |
| 18 | const 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 | |
| 27 | const s = await open({ name: 'qr', signIn: true, connect: true }); |
| 28 | const { page } = s; |
| 29 | |
| 30 | await page.waitForFunction( |
| 31 | () => !!window.DaimondQR && !!window.DaimondPairing && window.DaimondGateway |
| 32 | && DaimondGateway.state().authed, |
| 33 | null, { timeout: 12000 }).catch(() => {}); |
| 34 | |
| 35 | try { |
| 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 | |
| 102 | console.log(`\n${ok.length} passed, ${bad.length} failed`); |
| 103 | await s.close(); |
| 104 | process.exit(bad.length ? 1 : 0); |