Files
meshcore-analyzer/test-channel-qr.js
T
Kpa-clawbot c1d0daf200 feat(#1034): channel QR generate + scan module (PR 2/3) (#1035)
## PR #2 of channel UX redesign (#1034) — QR generation + scanning

Self-contained QR module for MeshCore channel sharing. Wirable but **not
wired** — PR #3 wires this into the modal placeholders shipped by PR #1.

### What's in
- **`public/channel-qr.js`** — new module exporting `window.ChannelQR`:
- `buildUrl(name, secretHex)` →
`meshcore://channel/add?name=<urlencoded>&secret=<32hex>`
- `parseChannelUrl(url)` → `{name, secret}` or `null` (strict: scheme,
path, hex32 secret)
- `generate(name, secretHex, target)` — renders QR (via vendored
qrcode.js) + the URL string + a "Copy Key" button into `target`
- `scan()` → `Promise<{name, secret} | null>` — opens a camera overlay,
decodes with jsQR, parses, auto-closes on first valid match. Graceful
no-camera/permission-denied fallback ("Camera not available — paste key
manually").
- **`public/vendor/jsqr.min.js`** — vendored jsQR 1.4.0
- **`public/index.html`** — loads `vendor/jsqr.min.js` + `channel-qr.js`
after `channel-decrypt.js`
- **`test-channel-qr.js`** + wired into `test-all.sh` — 16 assertions on
`buildUrl` / `parseChannelUrl` (DOM/camera paths covered by Playwright
in #3)

### TDD
- Red commit `d6ba89e` — stub module + failing assertions on `buildUrl`
/ `parseChannelUrl` (compiles, runs, fails on assertion)
- Green commit `25328ac` — real impl, 16/16 pass

### License note
Brief specified jsQR as MIT — it's actually **Apache-2.0**
(https://github.com/cozmo/jsQR/blob/master/package.json). Apache-2.0 is
permissive and compatible with the repo's ISC license; flagging here so
reviewers can confirm. Cited in the file header.

### Independence guarantees
- Does **not** touch `channels.js` or `channel-decrypt.js`
- Does not call any UI from `channels.js`; PR #3 will call
`ChannelQR.generate(...)` into `#qr-output` and wire `#scan-qr-btn` to
`ChannelQR.scan()`

Refs #1034

---------

Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-05-04 18:29:48 -07:00

87 lines
3.3 KiB
JavaScript

/**
* Tests for public/channel-qr.js — the QR generation/scanning module
* for the channel UX redesign (#1034, PR #2 of 3).
*
* Pure-JS assertions only: covers buildUrl, parseChannelUrl. The DOM
* (generate) and camera (scan) paths are exercised by Playwright E2E
* elsewhere in the redesign series.
*/
'use strict';
const vm = require('vm');
const fs = require('fs');
const path = require('path');
let passed = 0;
let failed = 0;
function assert(cond, msg) {
if (cond) { passed++; console.log(' ✓ ' + msg); }
else { failed++; console.error(' ✗ ' + msg); }
}
function loadChannelQR() {
const sandbox = {
window: {}, console, Date, JSON, parseInt, Math, String, Number,
Object, Array, RegExp, Error, Promise, setTimeout, encodeURIComponent,
decodeURIComponent, URL, URLSearchParams,
};
sandbox.window = sandbox;
sandbox.self = sandbox;
vm.createContext(sandbox);
const src = fs.readFileSync(path.join(__dirname, 'public/channel-qr.js'), 'utf8');
vm.runInContext(src, sandbox);
return sandbox.window.ChannelQR;
}
console.log('── ChannelQR — URL helpers ──');
const ChannelQR = loadChannelQR();
assert(ChannelQR && typeof ChannelQR.buildUrl === 'function',
'ChannelQR.buildUrl is exported');
assert(typeof ChannelQR.parseChannelUrl === 'function',
'ChannelQR.parseChannelUrl is exported');
assert(typeof ChannelQR.generate === 'function',
'ChannelQR.generate is exported');
assert(typeof ChannelQR.scan === 'function',
'ChannelQR.scan is exported');
// --- buildUrl ---
const SECRET = '8b3387e1c4be1bbf09c1a4cd5c0fa5a3';
const url1 = ChannelQR.buildUrl('Public', SECRET);
assert(url1 === 'meshcore://channel/add?name=Public&secret=' + SECRET,
'buildUrl produces canonical URL for plain name');
const url2 = ChannelQR.buildUrl('My Channel & Stuff', SECRET);
assert(url2 === 'meshcore://channel/add?name=My%20Channel%20%26%20Stuff&secret=' + SECRET,
'buildUrl URL-encodes spaces and ampersands in name');
// --- parseChannelUrl ---
const p1 = ChannelQR.parseChannelUrl(url1);
assert(p1 && p1.name === 'Public' && p1.secret === SECRET,
'parseChannelUrl extracts name + secret from canonical URL');
const p2 = ChannelQR.parseChannelUrl(url2);
assert(p2 && p2.name === 'My Channel & Stuff' && p2.secret === SECRET,
'parseChannelUrl URL-decodes name correctly');
assert(ChannelQR.parseChannelUrl(null) === null, 'parseChannelUrl(null) → null');
assert(ChannelQR.parseChannelUrl('') === null, 'parseChannelUrl("") → null');
assert(ChannelQR.parseChannelUrl('https://example.com') === null,
'parseChannelUrl rejects non-meshcore scheme');
assert(ChannelQR.parseChannelUrl('meshcore://channel/add?name=Foo') === null,
'parseChannelUrl rejects URL missing secret');
assert(ChannelQR.parseChannelUrl('meshcore://channel/add?secret=' + SECRET) === null,
'parseChannelUrl rejects URL missing name');
assert(ChannelQR.parseChannelUrl('meshcore://other/add?name=Foo&secret=' + SECRET) === null,
'parseChannelUrl rejects wrong host/path');
assert(ChannelQR.parseChannelUrl('meshcore://channel/add?name=Foo&secret=zz') === null,
'parseChannelUrl rejects non-hex secret');
assert(ChannelQR.parseChannelUrl('meshcore://channel/add?name=Foo&secret=' + SECRET.slice(0, 30)) === null,
'parseChannelUrl rejects short secret (must be 32 hex chars)');
console.log('');
console.log(` ${passed} passed, ${failed} failed`);
if (failed > 0) process.exit(1);