diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index eb6b7db7..8b42e2d3 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -221,78 +221,8 @@ jobs:
node scripts/check-css-vars.js
node scripts/test-check-css-vars.js
- - name: Run JS unit tests (packet-filter)
- run: |
- set -e
- node test-packet-filter.js
- node test-packet-filter-time.js
- node test-confidence-indicator.js
- node test-1659-analytics-warmup.js
- node test-channels-merge-1498-unit.js
- node test-packets-local-channels.js
- node test-issue-1518-home-url.js
- node test-channel-decrypt-insecure-context.js
- node test-live-region-filter.js
- node test-issue-1136-observer-iata-map.js
- node test-channel-qr.js
- node test-channel-qr-wiring.js
- node test-channel-modal-ux.js
- node test-channel-issue-1087.js
- node test-issue-1409-no-encrypted-flood.js
- node test-channel-issue-1101.js
- node test-observer-iata-1188.js
- node test-pull-to-reconnect-1091.js
- node test-channel-fluid-layout.js
- node test-issue-1279-p2-code-filter.js
- node test-area-filter.js
- node test-issue-1293-marker-shapes.js
- node test-issue-1356-map-a11y.js
- node test-issue-1360-pill-letter-count.js
- node test-issue-1364-pill-no-clamp.js
- node test-issue-1375-scope-stats-fetch.js
- node test-scope-audit-styles-linked.js
- node test-issue-1361-cb-presets.js
- node test-issue-1380-cb-sim-overlay.js
- node test-issue-1380-cb-reset-button.js
- node test-issue-1407-cb-preset-propagation.js
- node test-issue-1412-customizer-no-override.js
- node test-issue-1418-raw-hex-extraction.js
- node test-issue-1418-edge-weights.js
- node test-issue-1418-cb-preset-ramp.js
- node test-issue-1418-spider-fan.js
- node test-issue-1418-deeplink-hops-channels.js
- node test-issue-1418-polish-review.js
- node test-issue-1420-tile-providers.js
- node test-issue-1614-tile-url-function.js
- node test-issue-1438-marker-css-vars.js
- node test-issue-2001-map-scope-state.js
- node test-issue-1846-observers-width.js
- node test-issue-1562-observers-summary.js
- node test-issue-1509-nav-active-bg.js
- node test-issue-1509-detect-preset.js
- node test-live.js
- node test-coverage-gate.js
- node test-node-reach-coverage.js
- node test-issue-1107-live-layout.js
- node test-issue-1532-live-fullscreen.js
- node test-issue-1619-feed-detail-card-draggable.js
- node test-xss-escape-sinks.js
- node test-preflight-xss-gate.js
- node test-traces.js
- node test-issue-1648-m4-emoji-scan.js
- node test-issue-1753-copy-url-slash.js
- node test-issue-1668-m3-typography.js
- node test-mqtt-status-panel.js
- node test-issue-1697-mqtt-mobile-e2e.js
- node test-warmup-banner.js
- node test-issue-1633-hide-1byte-hops.js
- node test-issue-1668-m4-per-route.js
- node test-a11y-axe-1668-selftest.js
- node test-a11y-1716-rf-range-btn-active.js
- node test-issue-1705-subpath-contrast.js
- node test-issue-1770-mobile-row-clamp.js
- node test-a11y-axe-routes-coverage.js
- node test-issue-1956-release-routing.js
+ - name: Run JS unit tests
+ run: sh test-all.sh
- name: ๐ก๏ธ Preflight XSS gate โ actual --diff check (PR only)
# The fixture self-test above (test-preflight-xss-gate.js) only
diff --git a/docs/frontend-testing.md b/docs/frontend-testing.md
new file mode 100644
index 00000000..ec2803bf
--- /dev/null
+++ b/docs/frontend-testing.md
@@ -0,0 +1,32 @@
+# Frontend tests
+
+`test-all.sh` is the authoritative list of standalone frontend suites. Both
+`npm run test:unit` and the CI **Run JS unit tests** step invoke it. `npm test`
+and `npm run test:coverage` wrap the same runner with coverage reporting.
+
+The inventory currently covers 280 root suites: 167 standalone suites in the
+runner and 113 suites classified separately by their prerequisites.
+
+Run `npm run test:unit` from a checkout with Node.js, a POSIX shell, Bash, and
+Python 3 available. The XSS gate's fixture test uses Bash and Python. On
+Windows, use Git Bash and ensure `python3` resolves to Python 3 rather than a
+Microsoft Store alias; set `PYTHONUTF8=1` for the gate's Unicode output.
+No server or browser is started by this runner.
+
+When adding a root `test-*.js` file:
+
+1. Add its `node test-name.js` command to `test-all.sh` if it runs standalone.
+2. Otherwise add it to `scripts/non-unit-tests.json`, with a prerequisite or
+ reason explaining why it belongs outside the unit runner. Browser suites
+ still need explicit selection in the E2E workflow to become CI gates.
+3. Run `node test-test-inventory.js`. The guard rejects unclassified files,
+ duplicate assignments, removed files left in a list, undocumented groups,
+ and local/CI entry points that bypass the unit runner.
+
+The separate inventory distinguishes plain Node Playwright scripts, suites
+requiring the `@playwright/test` runner, and the `jsdom` integration suite.
+Those last two dependencies are not currently declared in `package.json`;
+classification records that limitation rather than installing them implicitly.
+The existing E2E workflow retains its browser installation, local fixture
+server, coverage collection, and repeat checks. Only use local servers for
+browser tests; inventory membership alone does not mean a suite runs in CI.
diff --git a/package.json b/package.json
index b52d5d89..f9f5b5f6 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
"main": "index.js",
"scripts": {
"test": "npx c8 --reporter=text --reporter=text-summary sh test-all.sh",
- "test:unit": "node test-packet-filter.js && node test-aging.js && node test-frontend-helpers.js",
+ "test:unit": "sh test-all.sh",
"test:coverage": "npx c8 --reporter=text --reporter=html sh test-all.sh",
"test:full-coverage": "sh scripts/combined-coverage.sh",
"lint": "eslint public/*.js"
diff --git a/public/live.js b/public/live.js
index 3e6da628..f44fb054 100644
--- a/public/live.js
+++ b/public/live.js
@@ -2629,7 +2629,7 @@
| Last Seen | ${lastSeen} |
| Adverts | ${n.advert_count || 0} |
- ${'configured_scope' in n && n.configured_scope !== null ? `| Configured scope โ | ${n.configured_scope === '' ? 'none configured' : `${escapeHtml(n.configured_scope)}`} |
` : ''}
+ ${'configured_scope' in n && n.configured_scope !== null ? `| Configured scope | ${n.configured_scope === '' ? 'none configured' : `${escapeHtml(n.configured_scope)}`} |
` : ''}
${'default_scope' in n ? `| Scope | ${n.default_scope === null ? 'โ'
: n.default_scope === '' ? 'unknown scope'
: `${escapeHtml(n.default_scope)}`
diff --git a/public/nodes.js b/public/nodes.js
index f68b658f..5699bc95 100644
--- a/public/nodes.js
+++ b/public/nodes.js
@@ -735,7 +735,7 @@
return ` |
| Bridge score โ | ${bpct}% ${blabel} |
`;
})() : ''}
${(n.role === 'repeater' || n.role === 'room') && Array.isArray(n.transported_scopes) && n.transported_scopes.length ? `| Transported scopes | ${n.transported_scopes.map(sc => '' + escapeHtml(String(sc)) + '').join('')} |
` : ''}
- ${'configured_scope' in n && n.configured_scope !== null ? `| Configured scope โ | ${n.configured_scope === '' ? 'none configured' : `${escapeHtml(n.configured_scope)}`} |
` : ''}
+ ${'configured_scope' in n && n.configured_scope !== null ? `| Configured scope | ${n.configured_scope === '' ? 'none configured' : `${escapeHtml(n.configured_scope)}`} |
` : ''}
| First Seen | ${renderNodeTimestampHtml(n.first_seen)} |
| Total Packets | ${stats.totalTransmissions || stats.totalPackets || n.advert_count || 0}${stats.totalObservations && stats.totalObservations !== (stats.totalTransmissions || stats.totalPackets) ? ' (seen ' + stats.totalObservations + 'ร)' : ''} |
| Packets Today | ${stats.packetsToday || 0} |
diff --git a/scripts/non-unit-tests.json b/scripts/non-unit-tests.json
new file mode 100644
index 00000000..85770ed6
--- /dev/null
+++ b/scripts/non-unit-tests.json
@@ -0,0 +1,130 @@
+{
+ "browser": {
+ "reason": "Requires Playwright and Chromium; most suites also require a running local fixture server via BASE_URL. Existing E2E workflow steps retain their separate setup and selection. Never point these tests at production.",
+ "files": [
+ "test-1110-live-filter.js",
+ "test-a11y-axe-1668.js",
+ "test-analytics-fluid-charts.js",
+ "test-audio-lab-1297-e2e.js",
+ "test-audio-live-1297-e2e.js",
+ "test-bottom-nav-1061-e2e.js",
+ "test-channel-color-picker-e2e.js",
+ "test-channel-decrypt-e2e.js",
+ "test-channel-fluid-e2e.js",
+ "test-channel-issue-1087-e2e.js",
+ "test-channel-issue-1111-e2e.js",
+ "test-channel-modal-e2e.js",
+ "test-channel-qr-e2e.js",
+ "test-channels-add-modal-e2e.js",
+ "test-channels-list-render-e2e.js",
+ "test-channels-selection-flow-e2e.js",
+ "test-channels-share-color-e2e.js",
+ "test-channels-ws-batch-e2e.js",
+ "test-channels-ws-race-1498-e2e.js",
+ "test-charts-fluid-1058-e2e.js",
+ "test-customize-branding-e2e.js",
+ "test-customize-display-e2e.js",
+ "test-customize-export-e2e.js",
+ "test-customize-theme-e2e.js",
+ "test-drag-manager-e2e.js",
+ "test-e2e-1267-mobile-vcr.js",
+ "test-e2e-playwright.js",
+ "test-filter-ux-e2e.js",
+ "test-gesture-hints-1065-e2e.js",
+ "test-gestures-1062-e2e.js",
+ "test-gestures-1185-scroll-discriminator-e2e.js",
+ "test-home-coverage-e2e.js",
+ "test-issue-1109-hamburger-dropdown-visible-e2e.js",
+ "test-issue-1122-packets-filter-ux-e2e.js",
+ "test-issue-1128-multi-viewport-e2e.js",
+ "test-issue-1128-packets-layout-e2e.js",
+ "test-issue-1136-live-region-e2e.js",
+ "test-issue-1146-path-link-contrast-e2e.js",
+ "test-issue-1147-section-order-e2e.js",
+ "test-issue-1150-404-state-e2e.js",
+ "test-issue-1151-orphan-separators-e2e.js",
+ "test-issue-1204-live-panel-structure-e2e.js",
+ "test-issue-1205-live-controls-anchor-e2e.js",
+ "test-issue-1206-resize-observer-leak-e2e.js",
+ "test-issue-1206-vcr-overlap-e2e.js",
+ "test-issue-1224-channels-mobile-ux-e2e.js",
+ "test-issue-1234-live-chrome-pass2-e2e.js",
+ "test-issue-1236-map-mobile-e2e.js",
+ "test-issue-1244-live-vcr-row-hints-e2e.js",
+ "test-issue-1273-qr-overlay-height-e2e.js",
+ "test-issue-1274-legend-coverage-e2e.js",
+ "test-issue-1279-legend-p2-e2e.js",
+ "test-issue-1281-location-row-e2e.js",
+ "test-issue-1306-collisions-terminology-e2e.js",
+ "test-issue-1329-map-controls-accordion-e2e.js",
+ "test-issue-1367-channels-chat-app-e2e.js",
+ "test-issue-1374-route-map-a11y-e2e.js",
+ "test-issue-1400-nav-vertical-clip.js",
+ "test-issue-1413-nav-overlap-e2e.js",
+ "test-issue-1486-collapse-reopens-detail-e2e.js",
+ "test-issue-1487-byop-modal-layout-e2e.js",
+ "test-issue-1510-live-nav-pin-e2e.js",
+ "test-issue-1567-corner-clears-drag-e2e.js",
+ "test-issue-1599-replay-freeze-e2e.js",
+ "test-issue-1630-reach-mobile-e2e.js",
+ "test-issue-1639-observers-sort-e2e.js",
+ "test-issue-1640-compare-discovery-e2e.js",
+ "test-issue-1648-m1-icons-e2e.js",
+ "test-issue-1648-m2-icons-e2e.js",
+ "test-issue-1648-m3-icons-e2e.js",
+ "test-issue-1648-m4-icons-e2e.js",
+ "test-issue-1648-m5-icons-e2e.js",
+ "test-issue-1657-analytics-channels-group-sprites-e2e.js",
+ "test-issue-1692-packets-init-parallel-e2e.js",
+ "test-issue-1705-subpath-contrast-e2e.js",
+ "test-issue-1758-ng-filter-rerenders-e2e.js",
+ "test-issue-1799-label-vocab-e2e.js",
+ "test-live-dedup.js",
+ "test-live-fullscreen-1572-e2e.js",
+ "test-live-layout-1178-1179-e2e.js",
+ "test-live-mql-leak-1180-e2e.js",
+ "test-live-multibyte-only-e2e.js",
+ "test-logo-default-sage-teal-e2e.js",
+ "test-logo-pulse-1173-e2e.js",
+ "test-logo-rebrand-e2e.js",
+ "test-logo-theme-e2e.js",
+ "test-map-modal-fluid-e2e.js",
+ "test-map-nodes-pagination-e2e.js",
+ "test-nav-drawer-1064-e2e.js",
+ "test-nav-fluid-1055-e2e.js",
+ "test-nav-more-floor-1139-e2e.js",
+ "test-nav-priority-1102-e2e.js",
+ "test-nav-priority-1311-e2e.js",
+ "test-nav-priority-1391-e2e.js",
+ "test-neighbor-map-btn-clip-e2e.js",
+ "test-node-reach-coverage-e2e.js",
+ "test-node-reach-e2e.js",
+ "test-nodes-export-e2e.js",
+ "test-observer-iata-1188-e2e.js",
+ "test-packets-scope-column.js",
+ "test-path-inspector-coverage-e2e.js",
+ "test-rx-coverage-mobile-nav-e2e.js",
+ "test-show-neighbors.js",
+ "test-slideover-1056-e2e.js",
+ "test-slideover-1168-munger-e2e.js",
+ "test-table-fluid-e2e.js",
+ "test-touch-gestures-coverage-e2e.js",
+ "test-touch-targets.js"
+ ]
+ },
+ "playwrightRunner": {
+ "reason": "Requires the @playwright/test runner (not plain node), its configuration, Chromium, and a local server. @playwright/test is not currently declared in package.json; these suites are inventoried but not part of the standalone unit gate.",
+ "files": [
+ "test-issue-1522-trace-url-sync-e2e.js",
+ "test-marker-outline-weight.js",
+ "test-path-inspector-e2e.js",
+ "test-pr-1490-live-map-gpu-animations-e2e.js"
+ ]
+ },
+ "domIntegration": {
+ "reason": "Requires jsdom, which is not currently declared in package.json. This DOM integration suite is inventoried separately; no new dependency is installed by the unit gate.",
+ "files": [
+ "test-table-sort.js"
+ ]
+ }
+}
diff --git a/test-all.sh b/test-all.sh
index 7b65c293..2d59388b 100755
--- a/test-all.sh
+++ b/test-all.sh
@@ -1,82 +1,175 @@
#!/bin/sh
-# Run all tests with coverage
+# Authoritative standalone frontend test list for local runs and CI.
+# Every root test is listed here or classified in scripts/non-unit-tests.json.
set -e
+cd "$(dirname "$0")"
-echo "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ"
-echo " CoreScope โ Test Suite"
-echo "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ"
-echo ""
-
-# Unit tests (deterministic, fast)
-echo "โโ Unit Tests โโ"
-node test-packet-filter.js
-node test-packet-filter-ux.js
-node test-packets-local-channels.js
+node test-test-inventory.js
+node test-1108-region-hide-nodes.js
+node test-1659-analytics-warmup.js
+node test-a11y-1705-subpath-hop-prefix-e2e.js
+node test-a11y-1715-dark-role-swatches.js
+node test-a11y-1716-rf-range-btn-active.js
+node test-a11y-1719-contrast-root-causes-e2e.js
+node test-a11y-axe-1668-selftest.js
+node test-a11y-axe-routes-coverage.js
node test-aging.js
-node test-issue-1065-gesture-hints-gates.js
-node test-frontend-helpers.js
-node test-issue-2001-map-scope-state.js
-node test-scope-audit-styles-linked.js
-node test-fetch-all-nodes-pagination.js
-node test-my-repeaters-dashboard.js
-node test-repeater-metric-scatter.js
-node test-top-routes-overlay.js
-node test-url-state.js
-node test-perf-go-runtime.js
-node test-channel-psk-ux.js
-node test-channel-sidebar-layout.js
-node test-channel-fluid-layout.js
-node test-channel-modal-ux.js
-node test-channel-decrypt-insecure-context.js
-node test-channel-qr.js
-node test-channel-qr-wiring.js
-node test-channel-issue-1087.js
-node test-issue-1409-no-encrypted-flood.js
node test-analytics-channels-integration.js
-node test-observers-headings.js
-node test-issue-1789-observer-firmware-cols.js
-node test-issue-1648-m1-emoji-scan.js
-node test-issue-1648-m2-emoji-scan.js
-node test-issue-1648-m3-emoji-scan.js
-node test-issue-1648-m6-final-sweep.js
-node test-issue-1648-m6-lint-self.js
-node test-issue-1890-og-url.js
-node test-traces.js
-node test-live-multibyte-filter.js
-node test-issue-1956-release-routing.js
-
-# #1418 โ route-view v2 (Tufte) coverage
-node test-issue-1418-raw-hex-extraction.js
-node test-issue-1418-edge-weights.js
-node test-issue-1418-cb-preset-ramp.js
-node test-issue-1418-spider-fan.js
-node test-issue-1418-deeplink-hops-channels.js
-node test-issue-1418-polish-review.js
-node test-issue-1420-tile-providers.js
-node test-issue-1614-tile-url-function.js
-node test-issue-1438-marker-css-vars.js
-node test-issue-1438-customizer-mcrole.js
-node test-issue-1446-cb-preset-cascade.js
-node test-issue-1380-cb-sim-overlay.js
+node test-anl1-tooltip-render.js
+node test-area-filter.js
+node test-channel-color-picker.js
+node test-channel-colors.js
+node test-channel-decrypt-ecb.js
+node test-channel-decrypt-insecure-context.js
+node test-channel-decrypt-m345.js
+node test-channel-fluid-layout.js
+node test-channel-issue-1087.js
+node test-channel-issue-1101.js
+node test-channel-live-decrypt-userprefix.js
+node test-channel-live-decrypt.js
+node test-channel-modal-ux.js
+node test-channel-psk-ux.js
+node test-channel-qr-wiring.js
+node test-channel-qr.js
+node test-channel-sidebar-layout.js
+node test-channel-ux-followup.js
+node test-channel-ux-round2.js
+node test-channels-merge-1498-unit.js
+node test-clear-filters.js
+node test-color-picker-ux.js
+node test-compare-flood-filter.js
+node test-compare-overlap.js
+node test-confidence-indicator.js
+node test-coverage-gate.js
+node test-customizer-v2.js
+node test-drag-manager.js
+node test-embed-mode-1369.js
+node test-fetch-all-nodes-pagination.js
+node test-fluid-scaffolding.js
+node test-frontend-helpers.js
+node test-geofilter-draft.js
+node test-hash-color.js
+node test-hop-resolver-affinity.js
+node test-issue-1065-gesture-hints-gates.js
+node test-issue-1107-live-layout.js
+node test-issue-1136-observer-iata-map.js
+node test-issue-1166-first-seen-column.js
+node test-issue-1189-composed-cell.js
+node test-issue-1189-live-iata-badge.js
+node test-issue-1279-p2-code-filter.js
+node test-issue-1293-marker-shapes.js
+node test-issue-1356-map-a11y.js
+node test-issue-1360-pill-letter-count.js
+node test-issue-1361-cb-presets.js
+node test-issue-1364-pill-no-clamp.js
+node test-issue-1375-scope-stats-fetch.js
node test-issue-1380-cb-reset-button.js
+node test-issue-1380-cb-sim-overlay.js
+node test-issue-1407-cb-preset-propagation.js
+node test-issue-1409-no-encrypted-flood.js
+node test-issue-1412-customizer-no-override.js
+node test-issue-1415-packets-layout.js
+node test-issue-1418-cb-preset-ramp.js
+node test-issue-1418-deeplink-hops-channels.js
+node test-issue-1418-edge-weights.js
+node test-issue-1418-polish-review.js
+node test-issue-1418-raw-hex-extraction.js
+node test-issue-1418-spider-fan.js
+node test-issue-1420-tile-providers.js
+node test-issue-1438-customizer-mcrole.js
+node test-issue-1438-marker-css-vars.js
+node test-issue-1446-cb-preset-cascade.js
node test-issue-1450-logo-aspect.js
node test-issue-1454-channels-toggle.js
node test-issue-1456-score-labels.js
-
-# #1461 mobile UX overhaul + #1470 node-detail tile helper (#1468 covered by E2E)
node test-issue-1461-mobile-page-actions.js
+node test-issue-1470-card-bg-contrast.js
node test-issue-1470-node-tile-helper.js
-node test-issue-1485-live-anim-z.js
-node test-issue-1532-live-fullscreen.js
-node test-naive-banner-tone.js
-node test-issue-1473-reserved-prefixes.js
node test-issue-1473-prefix-generator.js
+node test-issue-1473-reserved-prefixes.js
+node test-issue-1485-live-anim-z.js
+node test-issue-1488-marker-stroke-vars.js
+node test-issue-1496-reset-all-complete.js
+node test-issue-1509-detect-preset.js
+node test-issue-1509-nav-active-bg.js
+node test-issue-1518-home-url.js
+node test-issue-1532-live-fullscreen.js
+node test-issue-1562-observers-summary.js
+node test-issue-1563-aggregate-and-inflight.js
+node test-issue-1574-live-map-max-nodes.js
+node test-issue-1606-pagination.js
+node test-issue-1614-tile-url-function.js
+node test-issue-1619-feed-detail-card-draggable.js
+node test-issue-1633-hide-1byte-hops.js
+node test-issue-1644-redesign.js
+node test-issue-1646-compare-polish.js
+node test-issue-1648-followup-phosphor-leaks.js
+node test-issue-1648-m1-emoji-scan.js
+node test-issue-1648-m2-emoji-scan.js
+node test-issue-1648-m3-emoji-scan.js
+node test-issue-1648-m4-emoji-scan.js
+node test-issue-1648-m5-emoji-scan.js
+node test-issue-1648-m6-final-sweep.js
+node test-issue-1648-m6-lint-self.js
+node test-issue-1668-m2-contrast.js
+node test-issue-1668-m3-typography.js
+node test-issue-1668-m4-per-route.js
+node test-issue-1697-mqtt-mobile-e2e.js
+node test-issue-1705-subpath-contrast.js
+node test-issue-1753-copy-url-slash.js
node test-issue-1770-mobile-row-clamp.js
+node test-issue-1789-observer-firmware-cols.js
+node test-issue-1825-observer-node-cross-links.js
+node test-issue-1833-legend-toggle-vcr-offset.js
+node test-issue-1836-crossnav-case-normalization.js
+node test-issue-1843-node-qr-quiet-zone.js
+node test-issue-1846-observers-width.js
node test-issue-1849-trace-hashbytes.js
-node test-nodes-export.js
+node test-issue-1890-og-url.js
+node test-issue-1956-release-routing.js
+node test-issue-2001-map-scope-state.js
+node test-live-anims.js
+node test-live-dt-cap-1524.js
+node test-live-legend-helper.js
+node test-live-multibyte-filter.js
+node test-live-region-filter.js
+node test-live.js
+node test-map-clustering.js
+node test-mqtt-status-panel.js
+node test-my-repeaters-dashboard.js
+node test-naive-banner-tone.js
+node test-node-reach-coverage-debounce.js
+node test-node-reach-coverage.js
node test-nodes-export-wiring.js
+node test-nodes-export.js
+node test-observer-iata-1188.js
+node test-observer-naive-clock-1478.js
+node test-observers-headings.js
+node test-packet-filter-time.js
+node test-packet-filter-ux.js
+node test-packet-filter.js
+node test-packets-local-channels.js
+node test-packets.js
+node test-panel-corner.js
+node test-path-inspector.js
+node test-payload-labels-content.js
+node test-payload-labels-namespace.js
+node test-perf-anomaly.js
+node test-perf-disk-io-1120.js
+node test-perf-go-runtime.js
+node test-perf-render-1258.js
+node test-preflight-xss-gate.js
+node test-pull-to-reconnect-1091.js
+node test-pull-to-reconnect.js
+node test-repeater-metric-scatter.js
+node test-rx-coverage-config-race.js
+node test-rx-coverage-escape.js
+node test-scope-audit-styles-linked.js
+node test-slideover-1056-rowsel-strict.js
+node test-top-routes-overlay.js
+node test-traces.js
+node test-url-state.js
+node test-warmup-banner.js
+node test-xss-escape-sinks.js
-echo ""
-echo "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ"
-echo " All tests passed"
-echo "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ"
+echo "All standalone frontend suites passed"
diff --git a/test-channel-colors.js b/test-channel-colors.js
index 06c0e249..a3e2426b 100644
--- a/test-channel-colors.js
+++ b/test-channel-colors.js
@@ -146,20 +146,20 @@ 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() {
+test('getRowStyle returns the channel accent border without replacing the row background', 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;');
+ assert.ok(!style.includes('background'), 'row state/theme retains control of the background');
});
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;');
+ assert.ok(!style.includes('background'), 'alias also preserves the row background');
});
test('getRowStyle returns empty when channel has no assigned color', function() {
diff --git a/test-channel-ux-followup.js b/test-channel-ux-followup.js
index 6e2f284a..c41a24ee 100644
--- a/test-channel-ux-followup.js
+++ b/test-channel-ux-followup.js
@@ -43,8 +43,8 @@ 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');
+assert(/Use the close button to remove individual channels/.test(chSrc),
+ 'copy names the close button for individual key removal');
console.log('\n=== Fix 4: Share/reshare affordance on user-added rows ===');
// Source-level: data attribute and helper exist. Behavior-level checks
diff --git a/test-channel-ux-round2.js b/test-channel-ux-round2.js
index 4d223b6f..e18db123 100644
--- a/test-channel-ux-round2.js
+++ b/test-channel-ux-round2.js
@@ -64,12 +64,12 @@ 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
+// Assert the Phosphor icon and visible Share text are passed together
+// 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");
+assert(/iconBtn\(\s*'ch-share-btn'[^)]*#ph-share-network[^)]*<\/svg> Share'/.test(chSrc),
+ "share button content includes the Phosphor share icon and visible Share label");
console.log('\n=== Fix 3: โ delete button is a visibly red destructive button ===');
const removeRule = (cssSrc.match(/\.ch-remove-btn\s*\{[^}]*\}/) || [''])[0];
diff --git a/test-clear-filters.js b/test-clear-filters.js
index 1eda5be4..b8c9184d 100644
--- a/test-clear-filters.js
+++ b/test-clear-filters.js
@@ -269,11 +269,11 @@ test('updatePacketsUrl shows clear button when time window != default', () => {
const DEFAULT_TIME_WINDOW = 15;
const fn = new Function(
'filters', 'savedTimeWindowMin', 'DEFAULT_TIME_WINDOW',
- 'document', 'history', 'RegionFilter', 'buildPacketsQuery',
+ 'document', 'history', 'RegionFilter', 'buildPacketsQuery', 'location',
updateUrlBody
);
fn(filters, savedTimeWindowMin, DEFAULT_TIME_WINDOW,
- ctx.document, ctx.history, ctx.RegionFilter, () => '');
+ ctx.document, ctx.history, ctx.RegionFilter, () => '', { hash: '#/packets' });
assert.strictEqual(elements['clearFiltersBtn'].style.display, '', 'clear button should be visible when time window != default');
});
@@ -283,10 +283,10 @@ test('updatePacketsUrl hides clear button when all filters default', () => {
const filters = {};
const fn = new Function(
'filters', 'savedTimeWindowMin', 'DEFAULT_TIME_WINDOW',
- 'document', 'history', 'RegionFilter', 'buildPacketsQuery',
+ 'document', 'history', 'RegionFilter', 'buildPacketsQuery', 'location',
updateUrlBody
);
- fn(filters, 15, 15, ctx.document, ctx.history, ctx.RegionFilter, () => '');
+ fn(filters, 15, 15, ctx.document, ctx.history, ctx.RegionFilter, () => '', { hash: '#/packets' });
assert.strictEqual(elements['clearFiltersBtn'].style.display, 'none', 'clear button should be hidden');
});
diff --git a/test-customizer-v2.js b/test-customizer-v2.js
index a7163039..954491ec 100644
--- a/test-customizer-v2.js
+++ b/test-customizer-v2.js
@@ -162,11 +162,14 @@ test('accepts valid opacity', () => {
// โโ computeEffective โโ
console.log('\ncomputeEffective:');
-test('returns server defaults when overrides is {}', () => {
+test('preserves all server sections when overrides is {} and supplies missing home defaults', () => {
const { api } = loadCustomizer();
const defaults = { theme: { accent: '#aaa', text: '#bbb' }, nodeColors: { repeater: '#ccc' } };
const result = api.computeEffective(defaults, {});
- assert.deepStrictEqual(result, defaults);
+ const { home, ...serverSections } = result;
+ assert.deepStrictEqual(serverSections, defaults);
+ assert.ok(home && typeof home === 'object', 'missing home config gets built-in defaults');
+ assert.strictEqual(defaults.home, undefined, 'server input is not mutated');
});
test('overrides a single key in a section', () => {
diff --git a/test-drag-manager.js b/test-drag-manager.js
index 1bec20ba..67285129 100644
--- a/test-drag-manager.js
+++ b/test-drag-manager.js
@@ -41,7 +41,11 @@ function makePanel(id) {
},
getAttribute(k) { return attrs[k] || null; },
setAttribute(k, v) { attrs[k] = v; },
- removeAttribute(k) { delete attrs[k]; },
+ removeAttribute(k) {
+ delete attrs[k];
+ // DOMStringMap reflects removal of data-* attributes in real browsers.
+ if (k.startsWith('data-')) delete dataset[k.slice(5).replace(/-([a-z])/g, (_, c) => c.toUpperCase())];
+ },
getBoundingClientRect() {
return {
left: parseFloat(style.left) || 0,
diff --git a/test-fluid-scaffolding.js b/test-fluid-scaffolding.js
index 3768f63b..04b2e77a 100644
--- a/test-fluid-scaffolding.js
+++ b/test-fluid-scaffolding.js
@@ -9,6 +9,7 @@ const path = require('path');
const assert = require('assert');
const css = fs.readFileSync(path.join(__dirname, 'public/style.css'), 'utf8');
+const rules = css.replace(/\/\*[\s\S]*?\*\//g, '');
let passed = 0, failed = 0;
function test(name, fn) {
@@ -18,18 +19,19 @@ function test(name, fn) {
// --- Helpers ---------------------------------------------------------------
-// Extract the :root { ... } block (first occurrence โ the light/default one).
+// Base tokens can be split across multiple :root blocks. Theme overrides use
+// different selectors; combine the base declarations in stylesheet order.
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];
+ const blocks = Array.from(rules.matchAll(/^:root\s*\{([^}]*)\}/gm), m => m[1]);
+ if (!blocks.length) throw new Error(':root block not found in style.css');
+ return blocks.join('\n');
}
// Find the value of a custom property declared in :root.
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(`${name}\\s*:\\s*([^;]+);`, 'g');
+ const values = Array.from(rootBlock().matchAll(re), m => m[1].trim());
+ return values.length ? values[values.length - 1] : null;
}
function assertClamp(name) {
diff --git a/test-hop-resolver-affinity.js b/test-hop-resolver-affinity.js
index a9ce2dda..14dfe51a 100644
--- a/test-hop-resolver-affinity.js
+++ b/test-hop-resolver-affinity.js
@@ -24,7 +24,9 @@ function assert(condition, msg) {
// Two nodes share the same 1-byte prefix "ab"
const nodeA = { public_key: 'ab1111', name: 'NodeA', role: 'repeater', lat: 37.0, lon: -122.0 };
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 };
+// Keep the anchor distinctly closer to A. Equal degree offsets are not equal
+// great-circle distances: longitude converges toward the poles.
+const nodeC = { public_key: 'cd3333', name: 'NodeC', role: 'repeater', lat: 37.25, lon: -122.25 };
console.log('\n=== HopResolver Affinity Tests ===\n');
@@ -50,7 +52,7 @@ HopResolver.setAffinity({}); // No edges
// With anchor at NodeC's position, NodeA is 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)
+// NodeA is 0.25 degrees away on each axis; NodeB is 0.75 degrees away.
assert(result2['ab'].name === 'NodeA', 'Should pick NodeA (geo-closest) โ got: ' + result2['ab'].name);
// Test 3: setAffinity with null/undefined doesn't crash
diff --git a/test-issue-1438-customizer-mcrole.js b/test-issue-1438-customizer-mcrole.js
index 6689d8ce..ff8989b3 100644
--- a/test-issue-1438-customizer-mcrole.js
+++ b/test-issue-1438-customizer-mcrole.js
@@ -40,6 +40,7 @@ function assert(cond, msg) {
const cv2Src = fs.readFileSync(path.join(__dirname, 'public', 'customize-v2.js'), 'utf8');
const rolesSrc = fs.readFileSync(path.join(__dirname, 'public', 'roles.js'), 'utf8');
const presetsSrc = fs.readFileSync(path.join(__dirname, 'public', 'cb-presets.js'), 'utf8');
+const styleSrc = fs.readFileSync(path.join(__dirname, 'public', 'style.css'), 'utf8');
// โโโ Extract the nodeColors-processing block from customize-v2.js. โโโ
function extractBlock(src, anchor) {
@@ -163,12 +164,15 @@ console.log('\n=== #1438 FINAL C: server-only key does NOT clobber --mc-role-* (
blockA + '\n';
vm.runInContext(setup, env.sandbox);
- // --mc-role-companion must remain the preset's value (no clobber from server).
+ // applyCSS clears inline preset values for server-only keys. The active
+ // body selector supplies the color; a vm does not load that stylesheet.
const got = env.root.style.getPropertyValue('--mc-role-companion').toLowerCase();
- assert(got !== '#2563eb',
- '--mc-role-companion is NOT the server-config legacy #2563eb (got ' + got + ')');
- assert(got === '#648fff',
- '--mc-role-companion still reflects the active preset #648FFF (got ' + got + ')');
+ assert(got === '' && env.body.style.getPropertyValue('--mc-role-companion') === '',
+ 'server-only companion leaves no inline role color overriding the preset');
+ const presetRule = styleSrc.match(/body\[data-cb-preset="deut"\]\s*\{([^}]*)\}/);
+ assert(env.body.getAttribute('data-cb-preset') === 'deut' && presetRule &&
+ /--mc-role-companion:\s*#648fff\s*;/i.test(presetRule[1]),
+ 'the selected body preset supplies companion #648FFF from style.css');
// --node-companion CAN take the server value (legacy compat is fine here).
assert(env.root.style.getPropertyValue('--node-companion').toLowerCase() === '#2563eb',
diff --git a/test-issue-1446-cb-preset-cascade.js b/test-issue-1446-cb-preset-cascade.js
index 033a0b30..20ec2733 100644
--- a/test-issue-1446-cb-preset-cascade.js
+++ b/test-issue-1446-cb-preset-cascade.js
@@ -186,10 +186,13 @@ console.log('\n=== #1446 Scenario 5: clear preset โ reverts to server config /
vm.runInContext(presetsSrc, env.sandbox);
vm.runInContext(cv2Src, env.sandbox);
env.sandbox.window._customizerV2.init({ nodeColors: { repeater: '#aaaaaa' } });
- // Confirm deut is active first.
+ // The customizer removes inline role colors when a preset is active;
+ // its body selector in the real stylesheet supplies the selected color.
const repWithPreset = env.root.style.getPropertyValue('--mc-role-repeater').toLowerCase();
- assert(repWithPreset === '#fe6100',
- 'precondition: deut active โ --mc-role-repeater = #FE6100 (got: ' + JSON.stringify(repWithPreset) + ')');
+ const presetRule = styleSrc.match(/body\[data-cb-preset="deut"\]\s*\{([^}]*)\}/);
+ assert(repWithPreset === '' && env.body.getAttribute('data-cb-preset') === 'deut' &&
+ presetRule && /--mc-role-repeater:\s*#fe6100\s*;/i.test(presetRule[1]),
+ 'precondition: deut selects the CSS repeater color #FE6100 without a root override');
// Now clear the preset.
const hasClear = typeof env.sandbox.window.MeshCorePresets.clearPreset === 'function';
assert(hasClear, 'MeshCorePresets.clearPreset() exists');
diff --git a/test-issue-1470-card-bg-contrast.js b/test-issue-1470-card-bg-contrast.js
index 9a1bf14c..14bc7a10 100644
--- a/test-issue-1470-card-bg-contrast.js
+++ b/test-issue-1470-card-bg-contrast.js
@@ -47,10 +47,10 @@ 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 themeIdx = css.indexOf('[data-theme="dark"]');
- assert.ok(themeIdx !== -1, '[data-theme="dark"] block not found');
- const themeBlock = css.slice(themeIdx, themeIdx + 2000);
- const m = themeBlock.match(/--card-bg\s*:\s*([^;]+);/);
+ const rules = css.replace(/\/\*[\s\S]*?\*\//g, '');
+ const blocks = Array.from(rules.matchAll(/\[data-theme="dark"\]\s*\{([^}]*)\}/g), m => m[1]);
+ assert.ok(blocks.length, '[data-theme="dark"] block not found');
+ const m = blocks.join('\n').match(/--card-bg\s*:\s*([^;]+);/);
assert.ok(m, '--card-bg not found in [data-theme="dark"] block');
assert.strictEqual(m[1].trim(), 'var(--surface-2)',
`Expected var(--surface-2), got "${m[1].trim()}"`);
diff --git a/test-issue-1646-compare-polish.js b/test-issue-1646-compare-polish.js
index 6490e5b9..d77d4449 100644
--- a/test-issue-1646-compare-polish.js
+++ b/test-issue-1646-compare-polish.js
@@ -31,7 +31,7 @@
const fs = require('fs');
const path = require('path');
-const CSS = fs.readFileSync(path.join(__dirname, 'public/style.css'), 'utf8');
+const CSS = fs.readFileSync(path.join(__dirname, 'public/style.css'), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '');
const COMPARE_JS = fs.readFileSync(path.join(__dirname, 'public/compare.js'), 'utf8');
// Token-rank used by font-size comparisons. Comments-only mirror of the
diff --git a/test-issue-1648-m2-icons-e2e.js b/test-issue-1648-m2-icons-e2e.js
index 301b041b..c31ce206 100644
--- a/test-issue-1648-m2-icons-e2e.js
+++ b/test-issue-1648-m2-icons-e2e.js
@@ -75,6 +75,53 @@ async function spriteRefsResolve(page, label) {
else pass(`${label}: ${r.count} sprite refs rendered (samples: ${r.refs.join(', ')})`);
}
+async function checkConfiguredScope(page) {
+ const key = 'c1'.repeat(32);
+ const node = { public_key: key, name: 'Configured scope fixture', role: 'repeater' };
+ await page.route('**/api/nodes/' + key, route => route.fulfill({
+ json: { node, recentAdverts: [] },
+ }));
+ await page.route('**/api/nodes/' + key + '/health', route => route.fulfill({ json: {} }));
+ const nodeListUrl = /\/api\/nodes(?:\?.*)?$/;
+ await page.route(nodeListUrl, route => route.fulfill({
+ json: { nodes: [{ ...node, lat: 1, lon: 1 }], total: 1 },
+ }));
+ try {
+ for (const live of [false, true]) {
+ for (const scope of ['#fixture', '', null]) {
+ node.configured_scope = scope;
+ // A fresh document also clears the application's node-detail cache.
+ await page.goto('about:blank');
+ const route = live ? '/live?lat=1&lon=1&zoom=10' : '/nodes/' + key;
+ await page.goto(`${BASE}/#${route}`, { waitUntil: 'domcontentloaded' });
+ if (live) await page.locator('#liveMap .live-node-marker').click();
+ await page.locator(live ? '#nodeDetailContent table' : '#node-stats').waitFor({ state: 'visible', timeout: 8000 });
+ const row = live
+ ? page.locator('#nodeDetailContent tr').filter({ hasText: 'Configured scope' })
+ : page.locator('#row-configured-scope');
+ const label = live ? 'live detail' : 'node detail';
+ if (scope === null) {
+ assert.strictEqual(await row.count(), 0, 'unconfirmed scope must not render a confirmation row');
+ pass(label + ' configured scope: unconfirmed evidence stays absent');
+ continue;
+ }
+ assert.ok(await row.isVisible(), 'confirmed scope row must be visible, including an empty scope');
+ const confirmation = row.getByRole('img', { name: 'confirmed', exact: true });
+ assert.strictEqual(await confirmation.count(), 1, 'confirmation must have an accessible image role and name');
+ assert.ok(await confirmation.isVisible(), 'confirmation icon must be visible');
+ assert.strictEqual(await confirmation.locator('use').getAttribute('href'), '/icons/phosphor-sprite.svg#ph-check');
+ assert.doesNotMatch(await row.textContent(), EMOJI_RE, 'confirmation must follow the Phosphor icon policy');
+ assert.strictEqual((await row.locator('td').nth(1).textContent()).trim(), scope || 'none configured');
+ pass(label + ' configured scope: accessible confirmation and value for ' + (scope || 'none configured'));
+ }
+ }
+ } finally {
+ await page.unroute('**/api/nodes/' + key);
+ await page.unroute('**/api/nodes/' + key + '/health');
+ await page.unroute(nodeListUrl);
+ }
+}
+
async function main() {
const requireChromium = process.env.CHROMIUM_REQUIRE === '1';
let browser;
@@ -96,6 +143,8 @@ async function main() {
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
const page = await ctx.newPage();
+ await checkConfiguredScope(page);
+
// (a) /analytics
await checkHeading(page, '/analytics', /Mesh Analytics/, '(a) /analytics');
await spriteRefsResolve(page, '(a) /analytics sprite');
diff --git a/test-live-anims.js b/test-live-anims.js
index 31b4f4e8..91add623 100644
--- a/test-live-anims.js
+++ b/test-live-anims.js
@@ -2,6 +2,7 @@
'use strict';
const fs = require('fs');
const assert = require('assert');
+const vm = require('vm');
const src = fs.readFileSync('public/live.js', 'utf8');
@@ -117,16 +118,22 @@ test2('drawAnimatedLine renderFades() has null guard', () => {
assert.ok(fadeBody.includes('!pathsLayer'), 'renderFades() missing pathsLayer null guard');
});
-test2('pulseNode animatePulse() has null guard', () => {
- const pulseStart = src2.indexOf('function animatePulse(now)');
- const pulseBody = src2.substring(pulseStart, pulseStart + 200);
- assert.ok(pulseBody.includes('!animLayer'), 'animatePulse() missing animLayer null guard');
+function functionSource(name) {
+ const start = src2.indexOf(' function ' + name + '(');
+ assert.ok(start >= 0, name + ' exists');
+ const next = src2.indexOf('\n function ', start + 1);
+ assert.ok(next > start, name + ' source boundary exists');
+ return src2.slice(start, next);
+}
+
+test2('pulseNode returns safely after either layer is destroyed', () => {
+ for (const layers of [{ animLayer: null, nodesLayer: {} }, { animLayer: {}, nodesLayer: null }]) {
+ assert.doesNotThrow(() => vm.runInNewContext(functionSource('pulseNode') + '\npulseNode("test", [0, 0], "ADVERT");', layers));
+ }
});
-test2('ghostPulse has null guard', () => {
- const ghostStart = src2.indexOf('function ghostPulse(now)');
- const ghostBody = src2.substring(ghostStart, ghostStart + 200);
- assert.ok(ghostBody.includes('!animLayer'), 'ghostPulse() missing animLayer null guard');
+test2('shared pulse/ghost canvas callback returns safely after the context is destroyed', () => {
+ assert.doesNotThrow(() => vm.runInNewContext(functionSource('renderAnimations') + '\nrenderAnimations(0);', { animCtx: null }));
});
console.log(`\n${p2} passed, ${f2} failed\n`);
diff --git a/test-map-clustering.js b/test-map-clustering.js
index 25df326b..b2e5989f 100644
--- a/test-map-clustering.js
+++ b/test-map-clustering.js
@@ -115,10 +115,10 @@ console.log('\n=== map.js: clustering ===');
assert.ok(icon && icon._isDivIcon, 'expected an L.divIcon');
const html = icon.html || '';
assert.ok(/>66<') >= 0, `total count 6 not in html: ${html}`);
- // Role pill counts should appear
- assert.ok(html.indexOf('>3<') >= 0, `repeater pill (3) not in html: ${html}`);
- assert.ok(html.indexOf('>2<') >= 0, `companion pill (2) not in html: ${html}`);
- assert.ok(html.indexOf('>1<') >= 0, `room pill (1) not in html: ${html}`);
+ // Role letters disambiguate counts without relying on color alone.
+ assert.ok(/class="mc-pill role-repeater"[^>]*>R3]*>C2]*>M1= 0, `expected mc-cluster class, got: ${icon.className}`);
});
diff --git a/test-packets.js b/test-packets.js
index ee8a9100..589a406e 100644
--- a/test-packets.js
+++ b/test-packets.js
@@ -211,7 +211,7 @@ console.log('\n=== packets.js: getDetailPreview ===');
test('getDetailPreview handles CHAN type', () => {
const result = api.getDetailPreview({ type: 'CHAN', text: 'hello world', channel: 'general' });
- assert(result.includes('๐ฌ'));
+ assert(result.includes('href="/icons/phosphor-sprite.svg#ph-chat-circle"'));
assert(result.includes('hello world'));
assert(result.includes('chan-tag'));
assert(result.includes('general'));
@@ -229,7 +229,7 @@ console.log('\n=== packets.js: getDetailPreview ===');
type: 'ADVERT', name: 'TestNode', pubKey: 'abc123',
flags: { repeater: true }
});
- assert(result.includes('๐ก'));
+ assert(result.includes('href="/icons/phosphor-sprite.svg#ph-broadcast"'));
assert(result.includes('TestNode'));
assert(result.includes('hop-link'));
});
@@ -239,7 +239,7 @@ console.log('\n=== packets.js: getDetailPreview ===');
type: 'ADVERT', name: 'RoomNode', pubKey: 'abc',
flags: { room: true }
});
- assert(result.includes('๐ '));
+ assert(result.includes('href="/icons/phosphor-sprite.svg#ph-house-line"'));
});
test('getDetailPreview handles ADVERT sensor', () => {
@@ -247,7 +247,7 @@ console.log('\n=== packets.js: getDetailPreview ===');
type: 'ADVERT', name: 'Sensor1', pubKey: 'abc',
flags: { sensor: true }
});
- assert(result.includes('๐ก'));
+ assert(result.includes('href="/icons/phosphor-sprite.svg#ph-thermometer"'));
});
test('getDetailPreview handles ADVERT companion (default)', () => {
@@ -255,14 +255,14 @@ console.log('\n=== packets.js: getDetailPreview ===');
type: 'ADVERT', name: 'Comp', pubKey: 'abc',
flags: {}
});
- assert(result.includes('๐ป'));
+ assert(result.includes('href="/icons/phosphor-sprite.svg#ph-radio"'));
});
test('getDetailPreview handles GRP_TXT with channelHash (no_key)', () => {
const result = api.getDetailPreview({
type: 'GRP_TXT', channelHash: 0xAB, decryptionStatus: 'no_key'
});
- assert(result.includes('๐'));
+ assert(result.includes('href="/icons/phosphor-sprite.svg#ph-lock"'));
assert(result.includes('0xAB'));
assert(result.includes('no key'));
});
@@ -446,7 +446,7 @@ console.log('\n=== packets.js: getDetailPreview ===');
const result = api.getDetailPreview({
type: 'TXT_MSG', srcHash: 'abcdef01', destHash: '12345678'
});
- assert(result.includes('โ๏ธ'));
+ assert(result.includes('href="/icons/phosphor-sprite.svg#ph-envelope"'));
assert(result.includes('abcdef01'));
assert(result.includes('12345678'));
});
@@ -455,14 +455,14 @@ console.log('\n=== packets.js: getDetailPreview ===');
const result = api.getDetailPreview({
type: 'PATH', srcHash: 'aabb', destHash: 'ccdd'
});
- assert(result.includes('๐'));
+ assert(result.includes('href="/icons/phosphor-sprite.svg#ph-shuffle"'));
});
test('getDetailPreview handles REQ', () => {
const result = api.getDetailPreview({
type: 'REQ', srcHash: 'aa', destHash: 'bb'
});
- assert(result.includes('๐'));
+ assert(result.includes('href="/icons/phosphor-sprite.svg#ph-lock"'));
assert(result.includes('aa'));
});
@@ -470,7 +470,7 @@ console.log('\n=== packets.js: getDetailPreview ===');
const result = api.getDetailPreview({
type: 'RESPONSE', srcHash: 'aa', destHash: 'bb'
});
- assert(result.includes('๐'));
+ assert(result.includes('href="/icons/phosphor-sprite.svg#ph-lock"'));
});
test('getDetailPreview handles ANON_REQ', () => {
@@ -493,7 +493,7 @@ console.log('\n=== packets.js: getDetailPreview ===');
test('getDetailPreview handles public_key fallback', () => {
const result = api.getDetailPreview({ public_key: 'abcdef1234567890abcdef' });
- assert(result.includes('๐ก'));
+ assert(result.includes('href="/icons/phosphor-sprite.svg#ph-broadcast"'));
assert(result.includes('abcdef1234567890'));
});
@@ -957,7 +957,7 @@ console.log('\n=== packets.js: buildGroupRowHtml ===');
};
const result = api.buildGroupRowHtml(p);
assert(result.includes('group-header'));
- assert(result.includes('โถ')); // collapsed arrow
+ assert(result.includes('href="/icons/phosphor-sprite.svg#ph-caret-up"')); // collapsed arrow
});
test('buildGroupRowHtml shows observation count badge', () => {
@@ -969,7 +969,7 @@ console.log('\n=== packets.js: buildGroupRowHtml ===');
};
const result = api.buildGroupRowHtml(p);
assert(result.includes('badge-obs'));
- assert(result.includes('๐'));
+ assert(result.includes('href="/icons/phosphor-sprite.svg#ph-eye"'));
assert(result.includes('5'));
});
diff --git a/test-panel-corner.js b/test-panel-corner.js
index 06d4da8e..22299b27 100644
--- a/test-panel-corner.js
+++ b/test-panel-corner.js
@@ -147,6 +147,7 @@ function loadLiveModule(ctx) {
ctx.globalThis = ctx;
vm.createContext(ctx);
+ vm.runInContext(fs.readFileSync(path.join(__dirname, 'public', 'payload-labels.js'), 'utf8'), ctx);
vm.runInContext(src, ctx, { timeout: 3000 });
return ctx.window._panelCorner;
}
diff --git a/test-perf-disk-io-1120.js b/test-perf-disk-io-1120.js
index b9b69231..35509fda 100644
--- a/test-perf-disk-io-1120.js
+++ b/test-perf-disk-io-1120.js
@@ -156,7 +156,7 @@ await test('WAL >100 MB fires โ ๏ธ flag', async () => {
const html = sb.getHtml();
// The warning appears in the WAL Size card; assert proximity by extracting
// the WAL Size card's text content.
- const walSection = html.match(/150\.0MB[^<]*โ ๏ธ/);
+ const walSection = html.match(/150\.0MB\s*