Files
meshcore-analyzer/test-channel-issue-1087.js
T
Kpa-clawbot 4cd51a41e7 fix(channels): strip Share modal — remove redundant URL copy + duplicated key field (#1101) (#1103)
## Summary

Strips the Share Channel modal (shipped in #1090) down to its
essentials. Removes redundant affordances that the QR already provides.

## What changed

**Removed from the Share modal:**
- The URL text printed inside the QR box (the QR encodes the URL)
- The inline Copy Key button inside the QR box (overlapped the image)
- The `meshcore://` URL input field below the QR
- The Copy URL button next to the URL field

**Result — the modal now contains exactly:**
- Title `Share: <Channel Name>`
- QR code (just the QR `<img>`, nothing else in that box)
- Hex Key field with a single Copy button BELOW the QR
- Privacy warning
- ✕ close button (top right)

## Implementation

- `public/channels.js` — drop the `meshcore://` URL field-group from
share modal markup; `openShareModal()` no longer looks up `#chShareUrl`
or builds a URL into a field; pass `{ qrOnly: true }` when calling
`ChannelQR.generate` so the QR box renders ONLY the QR image.
- `public/channel-qr.js` — `generate(name, secret, target, opts)` now
accepts `opts.qrOnly` which short-circuits before appending the inline
URL line + Copy Key button. Default behaviour (no opts) unchanged, so
the Add-Channel "Generate & Show QR" flow is untouched.

## Tests (TDD: red → green)

- New: `test-channel-issue-1101.js` (static grep) — asserts the URL
field is gone from markup, `openShareModal` no longer references it, and
`ChannelQR.generate` honours `qrOnly`.
- Updated: `test-channel-issue-1087.js` and
`test-channel-issue-1087-e2e.js` — those previously asserted the URL
field's presence (which is exactly what #1101 removes); they now assert
ONLY the hex key field exists, AND that `#chShareQr` contains exactly
one `<img>` and no `.channel-qr-url` / `.channel-qr-copy` children.
- Wired into `.github/workflows/deploy.yml` `node-test` job.

Commit history shows red (test commit `c0c254a`) → green (fix commit
`6315a19`) per AGENTS.md TDD requirement.

E2E assertion added: test-channel-issue-1087-e2e.js:184

## Acceptance criteria

- [x] Share modal contains only: QR, "Copy Key" button, privacy warning
- [x] No "Copy URL" affordance anywhere in the modal
- [x] No duplicated hex key field below
- [x] E2E test asserts the absence of the removed elements

Fixes #1101

---------

Co-authored-by: meshcore-bot <bot@meshcore.local>
Co-authored-by: clawbot <clawbot@users.noreply.github.com>
2026-05-05 11:19:10 -07:00

139 lines
7.0 KiB
JavaScript

/**
* #1087 — Channel modal QR/share regression tests.
*
* Pure source-string + targeted DOM-string assertions covering all 4 bugs:
*
* 1. QR generator must use the vendored Kazuhiko Arase `qrcode()` API
* (lowercase). Old code checked `root.QRCode` which never existed,
* causing "[QR library not loaded]" on every Generate click.
* 2. The Share button must use the user's display label (not the
* internal `psk:<hex8>` lookup key) when building the QR/URL.
* 3. PSK channel persistence: the Add/Generate handlers must route
* writes through a single dedicated helper (`persistAddedChannel`)
* so storage happens synchronously inside the submit path — not as
* a side effect of subsequent UI events. The helper must also
* verify localStorage actually contains the key after the write.
* 4. The Share affordance must open a DEDICATED modal element
* (`chShareModal`) — not reuse the Add Channel modal
* (`chAddChannelModal`).
*
* Companion E2E coverage: test-channel-issue-1087-e2e.js
*/
'use strict';
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); }
}
const chSrc = fs.readFileSync(path.join(__dirname, 'public/channels.js'), 'utf8');
const qrSrc = fs.readFileSync(path.join(__dirname, 'public/channel-qr.js'), 'utf8');
const decSrc = fs.readFileSync(path.join(__dirname, 'public/channel-decrypt.js'), 'utf8');
const idxSrc = fs.readFileSync(path.join(__dirname, 'public/index.html'), 'utf8');
console.log('\n=== #1087 Bug 1: QR vendor library is wired correctly ===');
// The vendored library is Kazuhiko Arase's qrcode-generator (lowercase
// `qrcode` global). The generate() helper must call into that API —
// either via `root.qrcode(...)` / `window.qrcode(...)` / a direct
// `qrcode(` call producing an image with `createImgTag` /
// `createSvgTag` / `createDataURL`.
assert(/\bqrcode\s*\(\s*\d/.test(qrSrc) ||
/createImgTag|createSvgTag|createDataURL/.test(qrSrc),
'channel-qr.js generate() uses the vendored qrcode-generator API');
// The "[QR library not loaded]" fallback string must NOT be the only
// detection branch for the generator — the new code must support the
// lowercase qrcode global. We accept either (a) the old check is gone
// or (b) the new check is added alongside.
assert(/typeof\s+(root|window)\.qrcode\s*===\s*['"]function['"]/.test(qrSrc) ||
/typeof\s+qrcode\s*===\s*['"]function['"]/.test(qrSrc),
'channel-qr.js detects the lowercase `qrcode` global (not just `QRCode`)');
console.log('\n=== #1087 Bug 2: Share QR uses the user display label ===');
// The share-channel click handler must resolve a display label
// (via ChannelDecrypt.getLabel / .getLabels / userLabel lookup) and
// pass that human-readable name to ChannelQR.generate — NOT the raw
// `psk:<hex8>` key prefix.
var shareIdx = chSrc.indexOf("data-share-channel");
assert(shareIdx > 0, 'found share button DOM marker');
// Find a window of source covering the share button click handler.
var shareHandlerIdx = chSrc.indexOf("e.target.closest('[data-share-channel]')");
assert(shareHandlerIdx > 0, 'found share-channel click handler block');
var shareBlock = chSrc.substring(shareHandlerIdx, shareHandlerIdx + 2500);
assert(/getLabel\s*\(|getLabels\s*\(|userLabel|labels\s*\[/.test(shareBlock),
'share handler resolves the user display label before rendering QR');
// Belt-and-suspenders: the call to ChannelQR.generate() inside the
// share handler must NOT pass a value derived only from
// `shareHash.substring(5)` (which yields `psk:<hex8>`). Require an
// explicit label fallback chain.
assert(/ChannelQR\.generate\s*\(\s*[a-zA-Z_]*[Ll]abel/.test(shareBlock) ||
/ChannelQR\.generate\s*\(\s*displayLabel|displayName/.test(shareBlock),
'share handler passes a label-derived display name to ChannelQR.generate');
console.log('\n=== #1087 Bug 3: PSK channel persistence via dedicated helper ===');
// A single canonical helper must own the persistence path. Both the
// Generate and the PSK-Add submit handlers must route through it so
// storage cannot be skipped or deferred to a later UI event.
assert(/function\s+persistAddedChannel\s*\(/.test(chSrc),
'channels.js defines a persistAddedChannel(...) helper');
// Helper must call ChannelDecrypt.storeKey AND verify the write
// landed in localStorage by re-reading it.
var helperIdx = chSrc.indexOf('function persistAddedChannel');
assert(helperIdx > 0, 'helper definition located');
var helperBlock = helperIdx > 0 ? chSrc.substring(helperIdx, helperIdx + 1500) : '';
assert(/storeKey\s*\(/.test(helperBlock),
'persistAddedChannel calls ChannelDecrypt.storeKey()');
assert(/getStoredKeys\s*\(|getKeys\s*\(|localStorage\.getItem/.test(helperBlock),
'persistAddedChannel verifies the write by re-reading storage');
// Both submit paths must invoke the helper.
assert(/chGenerateBtn[\s\S]{0,2000}persistAddedChannel\s*\(/.test(chSrc),
'Generate (#chGenerateBtn) handler invokes persistAddedChannel');
assert(/chPskAddBtn[\s\S]{0,2500}persistAddedChannel\s*\(|addUserChannel[\s\S]{0,2500}persistAddedChannel\s*\(/.test(chSrc),
'PSK Add path invokes persistAddedChannel');
console.log('\n=== #1087 Bug 4: Dedicated Share modal (separate from Add) ===');
// A NEW DOM element distinct from #chAddChannelModal must exist for
// sharing. Title, hex key field, URL field, privacy warning.
assert(/id="chShareModal"/.test(chSrc),
'dedicated #chShareModal element exists in channels.js markup');
// Modal must NOT just be an alias for the Add modal — its internals
// must include share-specific affordances.
var shareModalIdx = chSrc.indexOf('id="chShareModal"');
assert(shareModalIdx > 0, 'share modal markup located');
var shareModalBlock = shareModalIdx > 0 ? chSrc.substring(shareModalIdx, shareModalIdx + 3000) : '';
assert(/id="chShareModalTitle"|class="ch-share-modal-title"|>Share[^<]*</.test(shareModalBlock),
'share modal has its own title element ("Share: <Channel Name>")');
assert(/id="chShareKey"|data-share-field="key"/.test(shareModalBlock),
'share modal exposes the hex key field with a copy affordance');
// #1101: meshcore:// URL field intentionally REMOVED — QR already
// encodes the URL, separate field/button was redundant.
assert(/trusted|privacy|do not share|only share/i.test(shareModalBlock),
'share modal includes a privacy warning');
// Share click handler must open #chShareModal — not openAddModal().
var shareClickIdx = chSrc.indexOf("e.target.closest('[data-share-channel]')");
var shareClickBlock = shareClickIdx > 0 ? chSrc.substring(shareClickIdx, shareClickIdx + 2500) : '';
assert(/openShareModal\s*\(|chShareModal/.test(shareClickBlock),
'share button click handler opens #chShareModal (not the Add modal)');
assert(!/openAddModal\s*\(\s*\)/.test(shareClickBlock),
'share button click handler does NOT call openAddModal()');
console.log('\n=== Results ===');
console.log('Passed: ' + passed + ', Failed: ' + failed);
process.exit(failed > 0 ? 1 : 0);