diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a048d5371..6750b4a16 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -134,97 +134,12 @@ jobs: node scripts/check-css-vars.js node scripts/test-check-css-vars.js - - name: Run JS unit tests (packet-filter) + - name: Run JS unit tests (test-all.sh) + # One list: test-all.sh runs every unit file and prints a summary of + # the failed ones; the step fails if any file fails (#174). run: | - set -e - node test-packet-filter.js - node test-packet-filter-time.js - node test-issue-121-clear-filters-selection.js - node test-clear-filters.js - node test-issue-147-packets-url-detail-params.js - node test-packet-path-map.js - node test-confidence-indicator.js - node test-1659-analytics-warmup.js - node test-issue-120-distance-building.js - node test-app-api-inflight-cleanup-rejection.js - node test-channels-merge-1498-unit.js - node test-channels-observed-path-hash-size.js - node test-issue-1518-home-url.js - node test-channel-decrypt-insecure-context.js - node test-live-region-filter.js - node test-issue-2052-touch-target-css.js - node test-channel-proposals.js - node test-packet-detail-channel-xss.js - node test-channels-name-escaping.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-issue-117-ws-watchdog.js - node test-issue-111-drawer-version.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-1890-og-url.js - node test-issue-1375-scope-stats-fetch.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-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-issue-124-rx-coverage-viewport.js - node test-rx-coverage-escape.js - node test-issue-125-live-toggles-wiring.js - node test-coverage-gate.js - node test-node-reach-coverage.js - node test-reach-rank.js - node test-node-adverts.js - node test-issue-1107-live-layout.js - node test-issue-1833-legend-toggle-vcr-offset.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-analytics-table-ids-unique.js - node test-analytics-relay-airtime-dumbbell.js - node test-live-vcr-mode-contrast.js - node test-issue-1770-mobile-row-clamp.js - node test-a11y-axe-routes-coverage.js + node --version + sh test-all.sh - name: 🛡️ Preflight XSS gate — actual --diff check (PR only) # The fixture self-test above (test-preflight-xss-gate.js) only @@ -603,6 +518,13 @@ jobs: BASE_URL=http://localhost:13581 node test-live-dedup.js 2>&1 | tee -a e2e-output.txt BASE_URL=http://localhost:13581 node test-nodes-export-e2e.js 2>&1 | tee -a e2e-output.txt BASE_URL=http://localhost:13581 node test-show-neighbors.js 2>&1 | tee -a e2e-output.txt + # Root E2Es without a runner until #174. They do not use the fixture + # server: the two contrast gates render static markup, and the other two + # serve their own pages. + CHROMIUM_REQUIRE=1 node test-a11y-1705-subpath-hop-prefix-e2e.js 2>&1 | tee -a e2e-output.txt + 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 # #1616: slide-over focus-restore flake-gate. Runs the slide-over # E2E 20 consecutive times against the SAME backend instance so diff --git a/test-all.sh b/test-all.sh index 9184cd0a4..6a8b15d2f 100755 --- a/test-all.sh +++ b/test-all.sh @@ -1,126 +1,254 @@ #!/bin/sh -# Run all tests with coverage -set -e +# Run every unit test file, then print a summary. +# +# A failing file does not stop the run: every file runs, the failures are +# listed at the end, and the script exits 1 if any file failed. +# Only files that run with plain `node` and no server belong here. E2E files +# run in the Playwright step of .github/workflows/deploy.yml. + +passed=0 +failed=0 +failed_files= + +run() { + echo "" + echo "── $1" + if node "$1"; then + passed=$((passed + 1)) + else + failed=$((failed + 1)) + failed_files="$failed_files $1" + fi +} echo "═══════════════════════════════════════" echo " CoreScope — Test Suite" echo "═══════════════════════════════════════" -echo "" # Unit tests (deterministic, fast) +echo "" echo "── Unit Tests ──" -node test-packet-filter.js -node test-packet-filter-ux.js -node test-issue-121-clear-filters-selection.js -node test-clear-filters.js -node test-issue-147-packets-url-detail-params.js -node test-aging.js -node test-issue-1065-gesture-hints-gates.js -node test-frontend-helpers.js -node test-app-api-inflight-cleanup-rejection.js -node test-issue-120-distance-building.js -node test-privacy-page.js -node test-nav-dynamic-link-lifecycle.js -node test-nav-first-load-fit.js -node test-nav-priority-scheduler.js -node test-geo-filter.js -node test-fetch-all-nodes-pagination.js -node test-nodes-geo-scope-filter.js -node test-nodes-export.js -node test-nodes-export-wiring.js -node test-nodes-export-order.js -node test-nodes-export-narrow-wait.js -node test-my-repeaters-dashboard.js -node test-repeater-metric-scatter.js -node test-top-routes-overlay.js -node test-important-links-byte-filter.js -node test-url-state.js -node test-node-adverts.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-analytics-foreign-traffic-tab.js -node test-analytics-nodes-without-scope.js -node test-analytics-table-ids-unique.js -node test-analytics-relay-airtime-dumbbell.js -node test-live-vcr-mode-contrast.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-1883-redirect-history.js -node test-issue-1890-og-url.js -node test-traces.js -node test-live-multibyte-filter.js -node test-issue-124-rx-coverage-viewport.js -node test-rx-coverage-escape.js -node test-issue-125-live-toggles-wiring.js -node test-issue-2052-touch-target-css.js -node test-channel-proposals.js -node test-packet-detail-channel-xss.js -node test-channels-name-escaping.js +run test-packet-filter.js +run test-packet-filter-ux.js +run test-issue-121-clear-filters-selection.js +run test-clear-filters.js +run test-issue-147-packets-url-detail-params.js +run test-aging.js +run test-issue-1065-gesture-hints-gates.js +run test-frontend-helpers.js +run test-app-api-inflight-cleanup-rejection.js +run test-issue-120-distance-building.js +run test-privacy-page.js +run test-nav-dynamic-link-lifecycle.js +run test-nav-first-load-fit.js +run test-nav-priority-scheduler.js +run test-geo-filter.js +run test-fetch-all-nodes-pagination.js +run test-nodes-geo-scope-filter.js +run test-nodes-export.js +run test-nodes-export-wiring.js +run test-nodes-export-order.js +run test-nodes-export-narrow-wait.js +run test-my-repeaters-dashboard.js +run test-repeater-metric-scatter.js +run test-top-routes-overlay.js +run test-important-links-byte-filter.js +run test-url-state.js +run test-node-adverts.js +run test-perf-go-runtime.js +run test-channel-psk-ux.js +run test-channel-sidebar-layout.js +run test-channel-fluid-layout.js +run test-channel-modal-ux.js +run test-channel-decrypt-insecure-context.js +run test-channel-qr.js +run test-channel-qr-wiring.js +run test-channel-issue-1087.js +run test-issue-1409-no-encrypted-flood.js +run test-analytics-channels-integration.js +run test-analytics-foreign-traffic-tab.js +run test-analytics-nodes-without-scope.js +run test-analytics-table-ids-unique.js +run test-analytics-relay-airtime-dumbbell.js +run test-live-vcr-mode-contrast.js +run test-observers-headings.js +run test-issue-1789-observer-firmware-cols.js +run test-issue-1648-m1-emoji-scan.js +run test-issue-1648-m2-emoji-scan.js +run test-issue-1648-m3-emoji-scan.js +run test-issue-1648-m6-final-sweep.js +run test-issue-1648-m6-lint-self.js +run test-issue-1883-redirect-history.js +run test-issue-1890-og-url.js +run test-traces.js +run test-live-multibyte-filter.js +run test-issue-124-rx-coverage-viewport.js +run test-rx-coverage-escape.js +run test-issue-125-live-toggles-wiring.js +run test-issue-2052-touch-target-css.js +run test-channel-proposals.js +run test-packet-detail-channel-xss.js +run test-channels-name-escaping.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-issue-1380-cb-reset-button.js -node test-issue-1450-logo-aspect.js -node test-issue-1454-channels-toggle.js -node test-issue-1456-score-labels.js +run test-issue-1418-raw-hex-extraction.js +run test-issue-1418-edge-weights.js +run test-issue-1418-cb-preset-ramp.js +run test-issue-1418-spider-fan.js +run test-issue-1418-deeplink-hops-channels.js +run test-issue-1418-polish-review.js +run test-issue-1420-tile-providers.js +run test-issue-1614-tile-url-function.js +run test-issue-1438-marker-css-vars.js +run test-issue-1438-customizer-mcrole.js +run test-issue-1446-cb-preset-cascade.js +run test-issue-1380-cb-sim-overlay.js +run test-issue-1380-cb-reset-button.js +run test-issue-1450-logo-aspect.js +run test-issue-1454-channels-toggle.js +run 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-node-tile-helper.js -node test-issue-1485-live-anim-z.js -node test-issue-1532-live-fullscreen.js -node test-issue-1833-legend-toggle-vcr-offset.js -node test-naive-banner-tone.js -node test-issue-1473-reserved-prefixes.js -node test-issue-1473-prefix-generator.js -node test-issue-1770-mobile-row-clamp.js -node test-issue-1849-trace-hashbytes.js -node test-node-analytics-hop-chart.js -node test-analytics-hop-depth-ui.js -node test-channels-ping-bot-reply.js -node test-channels-observed-path-hash-size.js -node test-packet-path-map.js -node test-area-nodes-map.js -node test-ping-scores.js -node test-observer-neighbors-report-badge.js -node test-observer-direct-neighbors-panel.js -node test-analytics-areas-tab.js -node test-observer-neighbors-tool.js -node test-new-nodes-tool.js -node test-node-changes-tool.js -node test-network-digest-tool.js -node test-position-gaps-tool.js -node test-gps-sanity-tool.js -node test-map-scope-filter.js -node test-issue-117-ws-watchdog.js -node test-issue-111-drawer-version.js +run test-issue-1461-mobile-page-actions.js +run test-issue-1470-node-tile-helper.js +run test-issue-1485-live-anim-z.js +run test-live-anims.js +run test-issue-1532-live-fullscreen.js +run test-issue-1833-legend-toggle-vcr-offset.js +run test-naive-banner-tone.js +run test-issue-1473-reserved-prefixes.js +run test-issue-1473-prefix-generator.js +run test-issue-1770-mobile-row-clamp.js +run test-issue-1849-trace-hashbytes.js +run test-node-analytics-hop-chart.js +run test-analytics-hop-depth-ui.js +run test-channels-ping-bot-reply.js +run test-channels-observed-path-hash-size.js +run test-packet-path-map.js +run test-area-nodes-map.js +run test-ping-scores.js +run test-observer-neighbors-report-badge.js +run test-observer-direct-neighbors-panel.js +run test-analytics-areas-tab.js +run test-observer-neighbors-tool.js +run test-new-nodes-tool.js +run test-node-changes-tool.js +run test-network-digest-tool.js +run test-position-gaps-tool.js +run test-gps-sanity-tool.js +run test-map-scope-filter.js +run test-issue-117-ws-watchdog.js +run test-issue-111-drawer-version.js + +# Previously run only by the JS unit step in deploy.yml (#174) +run test-packet-filter-time.js +run test-confidence-indicator.js +run test-1659-analytics-warmup.js +run test-channels-merge-1498-unit.js +run test-issue-1518-home-url.js +run test-live-region-filter.js +run test-issue-1136-observer-iata-map.js +run test-channel-issue-1101.js +run test-observer-iata-1188.js +run test-pull-to-reconnect-1091.js +run test-issue-1279-p2-code-filter.js +run test-area-filter.js +run test-issue-1293-marker-shapes.js +run test-issue-1356-map-a11y.js +run test-issue-1360-pill-letter-count.js +run test-issue-1364-pill-no-clamp.js +run test-issue-1375-scope-stats-fetch.js +run test-issue-1361-cb-presets.js +run test-issue-1407-cb-preset-propagation.js +run test-issue-1412-customizer-no-override.js +run test-issue-1846-observers-width.js +run test-issue-1562-observers-summary.js +run test-issue-1509-nav-active-bg.js +run test-issue-1509-detect-preset.js +run test-live.js +run test-coverage-gate.js +run test-node-reach-coverage.js +run test-reach-rank.js +run test-issue-1107-live-layout.js +run test-issue-1619-feed-detail-card-draggable.js +run test-xss-escape-sinks.js +run test-preflight-xss-gate.js +run test-issue-1648-m4-emoji-scan.js +run test-issue-1753-copy-url-slash.js +run test-issue-1668-m3-typography.js +run test-mqtt-status-panel.js +run test-issue-1697-mqtt-mobile-e2e.js +run test-warmup-banner.js +run test-issue-1633-hide-1byte-hops.js +run test-issue-1668-m4-per-route.js +run test-a11y-axe-1668-selftest.js +run test-a11y-1716-rf-range-btn-active.js +run test-issue-1705-subpath-contrast.js +run test-a11y-axe-routes-coverage.js + +# Green unit tests that no runner registered before (#174 inventory) +run test-1108-region-hide-nodes.js +run test-a11y-1715-dark-role-swatches.js +run test-analytics-distance-view-path.js +run test-analytics-wardriving-tab.js +run test-anl1-tooltip-render.js +run test-channel-color-picker.js +run test-channel-decrypt-ecb.js +run test-channel-decrypt-m345.js +run test-channel-live-decrypt-userprefix.js +run test-channel-live-decrypt.js +run test-color-picker-ux.js +run test-compare-flood-filter.js +run test-compare-overlap.js +run test-embed-mode-1369.js +run test-geofilter-draft.js +run test-hash-color.js +run test-issue-1166-first-seen-column.js +run test-issue-1189-composed-cell.js +run test-issue-1189-live-iata-badge.js +run test-issue-1415-packets-layout.js +run test-issue-1488-marker-stroke-vars.js +run test-issue-1496-reset-all-complete.js +run test-issue-1563-aggregate-and-inflight.js +run test-issue-1574-live-map-max-nodes.js +run test-issue-1606-pagination.js +run test-issue-1644-redesign.js +run test-issue-1648-followup-phosphor-leaks.js +run test-issue-1648-m5-emoji-scan.js +run test-issue-1668-m2-contrast.js +run test-issue-1825-observer-node-cross-links.js +run test-issue-1836-crossnav-case-normalization.js +run test-issue-1843-node-qr-quiet-zone.js +run test-live-dt-cap-1524.js +run test-live-legend-helper.js +run test-node-reach-coverage-debounce.js +run test-observer-detail-view-path.js +run test-observer-naive-clock-1478.js +run test-path-inspector.js +run test-payload-labels-content.js +run test-payload-labels-namespace.js +run test-perf-anomaly.js +run test-perf-render-1258.js +run test-pull-to-reconnect.js +run test-rx-coverage-config-race.js +run test-slideover-1056-rowsel-strict.js +run test-map-clustering.js +run test-panel-corner.js +run test-customizer-v2.js + +# test-all.sh self-test (#174) +run test-test-all.js echo "" echo "═══════════════════════════════════════" +echo " $passed passed, $failed failed ($((passed + failed)) files)" +if [ "$failed" -gt 0 ]; then + echo " Failed:" + for f in $failed_files; do + echo " $f" + done + echo "═══════════════════════════════════════" + exit 1 +fi echo " All tests passed" echo "═══════════════════════════════════════" diff --git a/test-customizer-v2.js b/test-customizer-v2.js index a7163039c..be902e694 100644 --- a/test-customizer-v2.js +++ b/test-customizer-v2.js @@ -164,7 +164,9 @@ test('accepts valid opacity', () => { console.log('\ncomputeEffective:'); test('returns server defaults when overrides is {}', () => { const { api } = loadCustomizer(); - const defaults = { theme: { accent: '#aaa', text: '#bbb' }, nodeColors: { repeater: '#ccc' } }; + // The server config carries `home` since #525; without it computeEffective + // fills in DEFAULT_HOME. + const defaults = { theme: { accent: '#aaa', text: '#bbb' }, nodeColors: { repeater: '#ccc' }, home: { heroTitle: 'Mesh' } }; const result = api.computeEffective(defaults, {}); assert.deepStrictEqual(result, defaults); }); diff --git a/test-issue-1407-cb-preset-propagation.js b/test-issue-1407-cb-preset-propagation.js index 0201b528d..67e23d474 100644 --- a/test-issue-1407-cb-preset-propagation.js +++ b/test-issue-1407-cb-preset-propagation.js @@ -243,6 +243,29 @@ console.log('\n=== #1407 G: ROLE_COLORS follows the preset delivered by body[dat 'preset deut active via body[data-cb-preset] only: ROLE_COLORS.repeater === #FE6100 (got ' + viaBody + ')'); } +console.log('\n=== #1407 H: ROLE_COLORS falls back to documentElement while
is null ==='); +// Review nit from #178: roles.js loads in , where document.body is still +// null. _readCssVar must then resolve on documentElement, not hand null to +// getComputedStyle (which throws in browsers) and drop to the hardcoded default. +{ + const henv = makeSandbox(); + henv.sandbox.document.body = null; + const rootOnly = '#123456'; // differs from the Wong default, so a fallback shows + henv.sandbox.getComputedStyle = function (el) { + if (!el) throw new TypeError("getComputedStyle: parameter 1 is not of type 'Element'"); + return { + getPropertyValue: function (k) { + return el === henv.root && k === '--mc-role-repeater' ? rootOnly : ''; + } + }; + }; + vm.createContext(henv.sandbox); + vm.runInContext(rolesSrc, henv.sandbox); + const got = String(henv.sandbox.window.ROLE_COLORS.repeater).toLowerCase(); + assert(got === rootOnly, + 'document.body null: ROLE_COLORS.repeater resolves on documentElement (got ' + got + ')'); +} + console.log('\n=== Summary ==='); console.log(' passed: ' + passed); console.log(' failed: ' + failed); diff --git a/test-issue-1648-m3-emoji-scan.js b/test-issue-1648-m3-emoji-scan.js index a6effc2ec..5b207e831 100644 --- a/test-issue-1648-m3-emoji-scan.js +++ b/test-issue-1648-m3-emoji-scan.js @@ -68,22 +68,36 @@ const ALLOW_SUBSTRINGS = [ '// PATH_SYMBOLS_LEGEND', '// the ✕ disappears', '// userAdded so the', - // channels.js ping bot (2026-07-23, after the M3 sweep). The 🏓 is message - // *content* — the text of the CoreScopeBot reply, like the emoji in the - // home.js prose above — not UI chrome. - "text: '🏓 pong! '", - // channels.js ping-bot message avatar: still the 🤖 glyph because the sprite - // has no robot symbol yet. Known exception, matched narrowly so any other - // avatar/icon emoji still fails. TODO: add ph-robot to phosphor-sprite.svg, - // swap the avatar and drop this entry. - '>🤖', ]; +// Exact, file-specific exceptions. Each text must occur exactly once in its +// file; only that occurrence is cut out before the scan, so any other emoji on +// the same line, a second copy, or the same text in another file still fails. +const ALLOW_EXACT = { + 'channels.js': [ + // Ping bot (2026-07-23, after the M3 sweep). The 🏓 is message *content* + // (the text of the CoreScopeBot reply), not UI chrome. + "text: '🏓 pong! '", + // Ping-bot message avatar: still the 🤖 glyph because the sprite has no + // robot symbol yet. TODO: add ph-robot to phosphor-sprite.svg, swap the + // avatar and drop this entry. + '', + ], +}; + function scanFile(rel) { const abs = path.join(ROOT, rel); - const txt = fs.readFileSync(abs, 'utf8'); - const lines = txt.split('\n'); + let txt = fs.readFileSync(abs, 'utf8'); const hits = []; + for (const exact of ALLOW_EXACT[rel] || []) { + const n = txt.split(exact).length - 1; + if (n !== 1) { + hits.push({ file: rel, line: 0, kind: 'allow', text: `exception must occur exactly once, found ${n}: ${exact}` }); + continue; + } + txt = txt.replace(exact, ''); + } + const lines = txt.split('\n'); lines.forEach((line, idx) => { if (ALLOW_SUBSTRINGS.some(s => line.includes(s))) return; const e = EMOJI.test(line); diff --git a/test-map-clustering.js b/test-map-clustering.js index 25df326b5..43f48e8ff 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 pills read letter + count since #1360 (R = repeater, C = companion, M = room) + assert.ok(html.indexOf('>R3<') >= 0, `repeater pill (R3) not in html: ${html}`); + assert.ok(html.indexOf('>C2<') >= 0, `companion pill (C2) not in html: ${html}`); + assert.ok(html.indexOf('>M1<') >= 0, `room pill (M1) not in html: ${html}`); // CoreScope-themed wrapper class assert.ok((icon.className || '').indexOf('mc-cluster') >= 0, `expected mc-cluster class, got: ${icon.className}`); }); diff --git a/test-packet-path-map.js b/test-packet-path-map.js index c77df457d..8fa1540c4 100644 --- a/test-packet-path-map.js +++ b/test-packet-path-map.js @@ -1064,6 +1064,54 @@ function makeSandbox(apiImpl) { } catch (e) { failed++; console.log(' ❌ nodes with a known role get a role icon in their tooltip: ' + e.message); } })(); + await (async () => { + try { + // #174 (review of #177): every role icon and the first-to-hear flag + // are decorative sprite icons, hidden from screen readers. + const first = { + hops: 2, + points: [{ publicKey: 'pk3', name: 'SensorC', lat: 56.2, lon: 10.2, role: 'sensor' }], + observer: { name: 'FirstObs', lat: 56.3, lon: 10.3, role: 'client' }, + }; + const ctx = makeSandbox(() => Promise.resolve({ + hash: 'deadbeef', + branches: [ + { + hops: 1, + points: [{ publicKey: 'pk1', name: 'RepeaterA', lat: 56.0, lon: 10.0, role: 'repeater' }], + observer: { name: 'RoomObserver', lat: 56.1, lon: 10.1, role: 'room' }, + }, + first, + ], + first, + })); + + const tooltips = []; + ctx.L = { + map: () => ({ setView() { return this; }, fitBounds() {}, invalidateSize() {}, remove() {} }), + tileLayer: () => ({ addTo() { return this; } }), + circleMarker: () => ({ addTo() { return this; }, bindTooltip(t) { tooltips.push(t); return this; }, on() { return this; } }), + polyline: () => ({ addTo() { return this; } }), + }; + + await ctx.window.PacketPathMap.open('deadbeef'); + const icon = (name) => ' '; + for (const [name, glyph] of [['RepeaterA', 'broadcast'], ['RoomObserver', 'house-line'], ['SensorC', 'thermometer'], ['FirstObs', 'radio']]) { + assert.ok(tooltips.some((t) => t.startsWith(icon(glyph)) && t.includes(name)), + 'expected ' + name + ' to start with an aria-hidden #ph-' + glyph + ' icon, got: ' + JSON.stringify(tooltips)); + } + const flag = tooltips.filter((t) => t.includes('First to hear it: FirstObs')); + assert.strictEqual(flag.length, 1, 'expected one first-to-hear tooltip, got: ' + JSON.stringify(tooltips)); + assert.ok(flag[0].startsWith(icon('flag') + 'First to hear it: FirstObs (2 hops)'), + 'expected the first-to-hear tooltip to start with an aria-hidden #ph-flag icon, got: ' + flag[0]); + const svgs = tooltips.join('').match(/