mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-06-05 01:41:18 +00:00
c1d0daf200
## 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>
87 lines
3.3 KiB
JavaScript
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);
|