Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
cacc8bc
test(channel-colors): expect the 3px border-only row style; register …
dborup Oct 3, 2026
91c9bcf
test(channels): follow the #1648 M3 sprite copy in two UX tests; regi…
dborup Oct 3, 2026
0edea8d
test(drag-manager): make the panel mock's dataset track data-* attrib…
dborup Oct 3, 2026
f1dba54
test(fluid-scaffolding): read every top-level :root block; register (…
dborup Oct 3, 2026
19e27a7
test(hop-resolver): fix the affinity fixture so NodeA really is geo-c…
dborup Oct 3, 2026
b959616
test(css): strip style.css comments before parsing in the 1470 and 16…
dborup Oct 3, 2026
c64dd3c
test(packets): expect the #1648 M2 sprites in test-packets.js; keep t…
dborup Oct 3, 2026
dc52153
test(perf): rework test-perf-disk-io-1120.js for the sprite flag and …
dborup Oct 3, 2026
ffbeb55
test(table-sort): port test-table-sort.js from jsdom to vm; register …
dborup Oct 3, 2026
b25d177
test(live): port the pulse-ring weight spec to plain playwright; regi…
dborup Oct 3, 2026
4b7d7a8
test(channels): update test-channel-modal-e2e.js for the "+ Add" chip…
dborup Oct 3, 2026
b932b40
test(reach): fix the map selector in test-node-reach-e2e.js; register…
dborup Oct 3, 2026
5e9f91a
test(path-inspector): port the map-pane E2E to plain playwright; regi…
dborup Oct 3, 2026
ec3877c
test(traces): port the #1522 trace URL sync spec to plain playwright;…
dborup Oct 3, 2026
c9c085b
test(test-all): record why test-touch-targets.js stays unregistered (…
dborup Oct 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,12 @@ jobs:
CHROMIUM_REQUIRE=1 node test-a11y-1719-contrast-root-causes-e2e.js 2>&1 | tee -a e2e-output.txt
CHROMIUM_REQUIRE=1 node test-nodes-favorite-rerender-e2e.js 2>&1 | tee -a e2e-output.txt
CHROMIUM_REQUIRE=1 node test-packet-trace-alignment-e2e.js 2>&1 | tee -a e2e-output.txt
# Root E2Es repaired or ported to plain playwright in #189.
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-live-pulse-ring-weight-e2e.js 2>&1 | tee -a e2e-output.txt
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-channel-modal-e2e.js 2>&1 | tee -a e2e-output.txt
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-node-reach-e2e.js 2>&1 | tee -a e2e-output.txt
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-path-inspector-e2e.js 2>&1 | tee -a e2e-output.txt
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1522-trace-url-sync-e2e.js 2>&1 | tee -a e2e-output.txt

# #1616: slide-over focus-restore flake-gate. Runs the slide-over
# E2E 20 consecutive times against the SAME backend instance so
Expand Down
12 changes: 12 additions & 0 deletions test-all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,18 @@ run test-map-clustering.js
run test-panel-corner.js
run test-customizer-v2.js

# Repaired orphans (#189)
run test-channel-colors.js
run test-channel-ux-followup.js
run test-channel-ux-round2.js
run test-drag-manager.js
run test-fluid-scaffolding.js
run test-hop-resolver-affinity.js
run test-issue-1470-card-bg-contrast.js
run test-issue-1646-compare-polish.js
run test-perf-disk-io-1120.js
run test-table-sort.js

# test-all.sh self-test (#174)
run test-test-all.js

Expand Down
9 changes: 4 additions & 5 deletions test-channel-colors.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,20 +146,19 @@ test('getRowStyle returns empty string for null channel', function() {
assert.strictEqual(ctx.window.ChannelColors.getRowStyle('GRP_TXT', null), '');
});

test('getRowStyle returns border + background for assigned GRP_TXT channel', function() {
// #675 (#674): a 3px left border only, no background tint.
test('getRowStyle returns a 3px left border for assigned GRP_TXT channel', function() {
const ctx = makeSandbox();
ctx.window.ChannelColors.set('#sf', '#ef4444');
const style = ctx.window.ChannelColors.getRowStyle('GRP_TXT', '#sf');
assert.ok(style.includes('border-left:4px solid #ef4444'), 'should have left border');
assert.ok(style.includes('background:#ef44441a'), 'should have 10% opacity background');
assert.strictEqual(style, 'border-left:3px solid #ef4444;');
});

test('getRowStyle works with CHAN type (alias for GRP_TXT)', function() {
const ctx = makeSandbox();
ctx.window.ChannelColors.set('#mesh', '#3b82f6');
const style = ctx.window.ChannelColors.getRowStyle('CHAN', '#mesh');
assert.ok(style.includes('border-left:4px solid #3b82f6'), 'should have left border');
assert.ok(style.includes('background:#3b82f61a'), 'should have background tint');
assert.strictEqual(style, 'border-left:3px solid #3b82f6;');
});

test('getRowStyle returns empty when channel has no assigned color', function() {
Expand Down
31 changes: 22 additions & 9 deletions test-channel-modal-e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
* - sidebar [+ Add Channel] opens modal
* - modal renders three labeled sections + privacy footer + QR placeholders
* - close (✕) hides modal
* - sectioned sidebar renders My Channels / Network / Encrypted sections
* - sectioned sidebar renders Network / Encrypted sections, and My Channels
* only once this browser holds a key (#1111)
* - PSK add flow: invalid hex → error; valid hex → modal closes
*
* Usage: BASE_URL=http://localhost:38201 node test-channel-modal-e2e.js
Expand Down Expand Up @@ -42,8 +43,13 @@ function assert(c, m) { if (!c) throw new Error(m || 'assertion failed'); }
});

await step('Add Channel button is visible', async () => {
const text = await page.textContent('#chAddChannelBtn');
assert(/Add Channel/.test(text), 'button text: ' + text);
// #1227 (#1224) shortened the label to a "+ Add" chip; the accessible
// name is still "Add channel".
assert(await page.isVisible('#chAddChannelBtn'), '#chAddChannelBtn not visible');
const text = (await page.textContent('#chAddChannelBtn')).trim();
assert(/^\+\s*Add\b/.test(text), 'button text: ' + text);
const label = await page.getAttribute('#chAddChannelBtn', 'aria-label');
assert(/^Add channel$/i.test(label || ''), 'aria-label: ' + label);
});

await step('modal hidden on load', async () => {
Expand Down Expand Up @@ -104,20 +110,20 @@ function assert(c, m) { if (!c) throw new Error(m || 'assertion failed'); }
}, { timeout: 3000 });
});

await step('sidebar renders three sections (My Channels / Network / Encrypted)', async () => {
await step('sidebar renders Network + Encrypted, and no My Channels without a key (#1111)', async () => {
// Wait for channel list to populate from API (or render empty-state).
await page.waitForFunction(() => {
const el = document.getElementById('chList');
if (!el) return false;
return el.querySelector('.ch-section-mychannels') &&
el.querySelector('.ch-section-network') &&
return el.querySelector('.ch-section-network') &&
el.querySelector('.ch-section-encrypted');
}, { timeout: 8000 });
const headers = await page.$$eval('.ch-section-header', els => els.map(e => e.textContent.trim()));
const joined = headers.join(' | ');
assert(/My Channels/.test(joined), 'My Channels header missing: ' + joined);
assert(/Network/.test(joined), 'Network header missing');
assert(/Encrypted/.test(joined), 'Encrypted header missing');
assert(/Network/.test(joined), 'Network header missing: ' + joined);
assert(/Encrypted/.test(joined), 'Encrypted header missing: ' + joined);
// 12d96a9d (#1111): the My Channels section is hidden while empty.
assert(!(await page.$('.ch-section-mychannels')), 'My Channels must not render without a stored key: ' + joined);
});

await step('Encrypted section is collapsed by default', async () => {
Expand Down Expand Up @@ -154,6 +160,13 @@ function assert(c, m) { if (!c) throw new Error(m || 'assertion failed'); }
assert(/cafebabe/i.test(stored), 'expected stored key in localStorage corescope_channel_keys, got: ' + stored);
});

await step('sidebar now renders My Channels with the new channel', async () => {
await page.waitForFunction(() => {
const sec = document.querySelector('#chList .ch-section-mychannels');
return sec && /My Channels/.test(sec.textContent) && /E2E Test Channel/.test(sec.textContent);
}, { timeout: 5000 });
});

await browser.close();

console.log(`\n=== Results: passed ${passed} failed ${failed} ===`);
Expand Down
8 changes: 6 additions & 2 deletions test-channel-ux-followup.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,12 @@ assert(!/\$\{ch\.messageCount\s*\|\|\s*0\}\s*packets/.test(chSrc),
console.log('\n=== Fix 3: privacy footer wording ===');
assert(!/Clear browser data to remove stored keys/.test(chSrc),
'old "Clear browser data to remove stored keys" copy is gone');
assert(/Use\s+✕\s+to remove individual channels/.test(chSrc),
'new copy points at the ✕ button for individual key removal');
// b812a98a (#1648 M3) swapped the ✕ glyph for the #ph-x sprite and reworded
// the hint to "close button"; the remove button must still be that X icon.
assert(/Use the close button to remove individual channels/.test(chSrc),
'new copy points at the close (X) button for individual key removal');
assert(/iconBtn\(\s*'ch-remove-btn'[^)]*'<svg class="ph-icon" aria-hidden="true"><use href="\/icons\/phosphor-sprite\.svg#ph-x"\/><\/svg>'/.test(chSrc),
"the remove button the copy points at renders the #ph-x close icon");

console.log('\n=== Fix 4: Share/reshare affordance on user-added rows ===');
// Source-level: data attribute and helper exist. Behavior-level checks
Expand Down
13 changes: 7 additions & 6 deletions test-channel-ux-round2.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,13 @@ assert(/channelDisplayName\(ch\)/.test(chSrc),
console.log('\n=== Fix 2: share button has recognizable label ===');
assert(!/'⤴'/.test(chSrc) && !/"⤴"/.test(chSrc),
'bare ⤴ glyph no longer used as the share button content');
// Tighten: assert the literal '📤 Share' string is the glyph argument
// passed into the iconBtn(...) call for ch-share-btn — this catches the
// case where someone removes the icon from the button content but leaves
// "Share" in an aria-label or title.
assert(/iconBtn\(\s*'ch-share-btn'[^)]*'📤 Share'/.test(chSrc),
"iconBtn('ch-share-btn', ...) is called with '📤 Share' as the glyph");
// Tighten: assert the icon + " Share" string is the glyph argument passed
// into the iconBtn(...) call for ch-share-btn — this catches the case where
// someone removes the icon from the button content but leaves "Share" in an
// aria-label or title. b812a98a (#1648 M3) replaced 📤 with the
// #ph-share-network sprite.
assert(/iconBtn\(\s*'ch-share-btn'[^)]*'<svg class="ph-icon" aria-hidden="true"><use href="\/icons\/phosphor-sprite\.svg#ph-share-network"\/><\/svg> Share'/.test(chSrc),
"iconBtn('ch-share-btn', ...) is called with the #ph-share-network icon + ' Share' as the glyph");

console.log('\n=== Fix 3: ✕ delete button is a visibly red destructive button ===');
const removeRule = (cssSrc.match(/\.ch-remove-btn\s*\{[^}]*\}/) || [''])[0];
Expand Down
13 changes: 11 additions & 2 deletions test-drag-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,23 @@ const assert = require('assert');
function makePanel(id) {
const listeners = {};
const style = {};
const dataset = {};
let attrs = {};
// Like a real DOMStringMap, dataset is a live view of the data-*
// attributes: removeAttribute('data-dragged') clears dataset.dragged
// (#1567 switched clearPanel to removeAttribute).
const attrName = (prop) => 'data-' + String(prop).replace(/[A-Z]/g, (c) => '-' + c.toLowerCase());
const dataset = new Proxy({}, {
get(_, prop) { return typeof prop === 'string' ? attrs[attrName(prop)] : undefined; },
set(_, prop, v) { attrs[attrName(prop)] = String(v); return true; },
deleteProperty(_, prop) { delete attrs[attrName(prop)]; return true; },
has(_, prop) { return attrName(prop) in attrs; }
});
const classList = {
_set: new Set(),
add(c) { this._set.add(c); },
remove(c) { this._set.delete(c); },
contains(c) { return this._set.has(c); }
};
let attrs = {};
const header = {
_listeners: {},
addEventListener(ev, fn) {
Expand Down
27 changes: 18 additions & 9 deletions test-fluid-scaffolding.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,27 @@ function test(name, fn) {

// --- Helpers ---------------------------------------------------------------

// Extract the :root { ... } block (first occurrence — the light/default one).
function rootBlock() {
const m = css.match(/:root\s*\{([\s\S]*?)\}/);
if (!m) throw new Error(':root block not found in style.css');
return m[1];
// The bodies of every top-level, unconditional `:root { ... }` block.
// style.css has several (f0addfda / #1668 put the palette block first), so
// reading only the first one misses the fluid tokens. Comments are stripped
// first; a block preceded by `{` is nested (e.g. in @media) and skipped.
function rootBlocks() {
const code = css.replace(/\/\*[\s\S]*?\*\//g, '');
const blocks = [...code.matchAll(/(?<=(?:^|\})\s*):root\s*\{([^}]*)\}/g)].map((m) => m[1]);
if (!blocks.length) throw new Error(':root block not found in style.css');
return blocks;
}

// Find the value of a custom property declared in :root.
// Find the value of a custom property declared in :root (the last
// declaration wins, as in the cascade).
function rootVar(name) {
const re = new RegExp(`${name}\\s*:\\s*([^;]+);`);
const m = rootBlock().match(re);
return m ? m[1].trim() : null;
const re = new RegExp(`(?:^|[;{\\s])${name}\\s*:\\s*([^;]+);`);
let value = null;
for (const block of rootBlocks()) {
const m = block.match(re);
if (m) value = m[1].trim();
}
return value;
}

function assertClamp(name) {
Expand Down
11 changes: 9 additions & 2 deletions test-hop-resolver-affinity.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ function assert(condition, msg) {

// ── Test nodes ──
// Two nodes share the same 1-byte prefix "ab"
const nodeA = { public_key: 'ab1111', name: 'NodeA', role: 'repeater', lat: 37.0, lon: -122.0 };
// NodeA must be clearly geo-closer to NodeC than NodeB. At (37.0, -122.0)
// the two were mirror images in degrees, and with haversine (2b9f3056, #874)
// NodeB was the closer one (70.88 vs 71.06 km). At (37.2, -122.2) NodeA is
// ~43 km from NodeC and NodeB ~71 km, so Tests 1 and 5 prove that affinity
// overrides geography, and Test 2 proves the geo fallback.
const nodeA = { public_key: 'ab1111', name: 'NodeA', role: 'repeater', lat: 37.2, lon: -122.2 };
const nodeB = { public_key: 'ab2222', name: 'NodeB', role: 'repeater', lat: 38.0, lon: -123.0 };
const nodeC = { public_key: 'cd3333', name: 'NodeC', role: 'repeater', lat: 37.5, lon: -122.5 };

Expand All @@ -49,8 +54,10 @@ HopResolver.init([nodeA, nodeB, nodeC]);
HopResolver.setAffinity({}); // No edges

// With anchor at NodeC's position, NodeA is closer to NodeC than NodeB
assert(HopResolver.haversineKm(nodeC.lat, nodeC.lon, nodeA.lat, nodeA.lon) <
HopResolver.haversineKm(nodeC.lat, nodeC.lon, nodeB.lat, nodeB.lon),
'fixture: NodeA is geo-closer to NodeC than NodeB');
const result2 = HopResolver.resolve(['cd33', 'ab'], nodeC.lat, nodeC.lon, null, null, null);
// NodeA (37, -122) is closer to NodeC (37.5, -122.5) than NodeB (38, -123)
assert(result2['ab'].name === 'NodeA', 'Should pick NodeA (geo-closest) — got: ' + result2['ab'].name);

// Test 3: setAffinity with null/undefined doesn't crash
Expand Down
9 changes: 6 additions & 3 deletions test-issue-1470-card-bg-contrast.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ const assert = require('assert');

const ROOT = __dirname;
function read(rel) { return fs.readFileSync(path.join(ROOT, rel), 'utf8'); }
// style.css with comments removed: e2212f50 (#1627) added a comment that
// mentions [data-theme="dark"] above the real block, and indexOf landed on it.
function readCss() { return read('public/style.css').replace(/\/\*[\s\S]*?\*\//g, ''); }

let passed = 0, failed = 0;
function test(name, fn) {
Expand All @@ -34,7 +37,7 @@ function test(name, fn) {
console.log('\n── #1470 CSS: --card-bg dark-mode variable ──');

test('@media dark block sets --card-bg to var(--surface-2)', () => {
const css = read('public/style.css');
const css = readCss();
// Find the @media (prefers-color-scheme: dark) block
const mediaIdx = css.indexOf('@media (prefers-color-scheme: dark)');
assert.ok(mediaIdx !== -1, '@media dark block not found');
Expand All @@ -46,7 +49,7 @@ test('@media dark block sets --card-bg to var(--surface-2)', () => {
});

test('[data-theme="dark"] block sets --card-bg to var(--surface-2)', () => {
const css = read('public/style.css');
const css = readCss();
const themeIdx = css.indexOf('[data-theme="dark"]');
assert.ok(themeIdx !== -1, '[data-theme="dark"] block not found');
const themeBlock = css.slice(themeIdx, themeIdx + 2000);
Expand All @@ -57,7 +60,7 @@ test('[data-theme="dark"] block sets --card-bg to var(--surface-2)', () => {
});

test('light-mode block keeps --card-bg as var(--surface-1)', () => {
const css = read('public/style.css');
const css = readCss();
// Light mode block ends before the @media dark block
const mediaIdx = css.indexOf('@media (prefers-color-scheme: dark)');
const lightBlock = css.slice(0, mediaIdx);
Expand Down
Loading
Loading