From 104db110c906582bf6f662ac7d3c7141e8d55764 Mon Sep 17 00:00:00 2001 From: dborup Date: Sun, 4 Oct 2026 06:37:52 +0000 Subject: [PATCH 1/2] test(analytics): reproduce Scopes/Wardriving view state missing from the URL (#205) Unit tests (vm harness) and a Playwright E2E for ?sub=, ?swin= and ?wdwin=: URL wins over sessionStorage, unknown values fall back to the defaults, default URLs unchanged, clicks write the URL, reload and back/forward restore the view. Red on master. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/deploy.yml | 1 + test-all.sh | 1 + test-analytics-subtab-deeplinks-205.js | 535 ++++++++++++++++++ ...ssue-205-analytics-subtab-deeplinks-e2e.js | 203 +++++++ 4 files changed, 740 insertions(+) create mode 100644 test-analytics-subtab-deeplinks-205.js create mode 100644 test-issue-205-analytics-subtab-deeplinks-e2e.js diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index bc13dfdaa..a4528b4e3 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -495,6 +495,7 @@ jobs: CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-drag-manager-e2e.js 2>&1 | tee -a e2e-output.txt CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1567-corner-clears-drag-e2e.js 2>&1 | tee -a e2e-output.txt CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1306-collisions-terminology-e2e.js 2>&1 | tee -a e2e-output.txt + CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-205-analytics-subtab-deeplinks-e2e.js 2>&1 | tee -a e2e-output.txt CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1374-route-map-a11y-e2e.js 2>&1 | tee -a e2e-output.txt CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-channels-list-render-e2e.js 2>&1 | tee -a e2e-output.txt CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-channels-selection-flow-e2e.js 2>&1 | tee -a e2e-output.txt diff --git a/test-all.sh b/test-all.sh index edea7f19a..7062e38b4 100755 --- a/test-all.sh +++ b/test-all.sh @@ -145,6 +145,7 @@ run test-packet-filter-time.js run test-confidence-indicator.js run test-1659-analytics-warmup.js run test-analytics-tab-state-and-query.js +run test-analytics-subtab-deeplinks-205.js run test-channels-merge-1498-unit.js run test-issue-1518-home-url.js run test-live-region-filter.js diff --git a/test-analytics-subtab-deeplinks-205.js b/test-analytics-subtab-deeplinks-205.js new file mode 100644 index 000000000..15cd65fc4 --- /dev/null +++ b/test-analytics-subtab-deeplinks-205.js @@ -0,0 +1,535 @@ +/* test-analytics-subtab-deeplinks-205.js + * + * #205: the Scopes tab's sub-tab (Overview / Hop Depth / Regions / Hygiene) + * and its 1h/24h/7d window, and the Wardriving tab's window, lived only in + * sessionStorage, so a link to e.g. Hop Depth could not be shared. They are + * now in the hash next to ?tab=: + * + * sub= Scopes sub-tab + * swin=<1h|24h|7d> Scopes window + * wdwin=<1h|24h|7d> Wardriving window + * + * (?window= stays the global analytics time picker; it has other values and + * drives the shared loads, so the tabs get keys of their own.) + * + * Contract pinned here: + * - a URL value wins over sessionStorage, and is stored for the next plain + * visit of the tab; + * - without a URL value the stored value is used and written to the URL; + * - an unknown or hostile URL value falls back to the default (never to the + * stored value), never throws and never reaches a selector (#193/#194); + * - the default view keeps today's URL (no sub/swin/wdwin); + * - clicks write the URL; switching to another tab drops these keys. + * + * Runs the REAL app.js api(), url-state.js and analytics page in one vm with + * a fake fetch and a small fake DOM whose buttons come from the markup the + * page writes. + */ +'use strict'; + +const vm = require('vm'); +const fs = require('fs'); +const assert = require('assert'); + +let passed = 0, failed = 0; +function test(name, fn) { + return Promise.resolve() + .then(fn) + .then(() => { passed++; console.log(' ✅ ' + name); }) + .catch((e) => { failed++; console.log(' ❌ ' + name + ': ' + (e && e.message || e)); }); +} + +// Real responses of the CI fixture server for the shared endpoints (#172). +const REAL = JSON.parse(fs.readFileSync('test-fixtures/analytics-tabs-172.json', 'utf8')); + +const flush = async () => { for (let i = 0; i < 30; i++) await new Promise((r) => setImmediate(r)); }; + +function fakeClassList(initial) { + const s = new Set(initial); + return { + add: (c) => s.add(c), remove: (c) => s.delete(c), contains: (c) => s.has(c), + toggle: (c, on) => { if (on === undefined ? !s.has(c) : on) s.add(c); else s.delete(c); }, + }; +} + +const camel = (s) => s.replace(/-([a-z])/g, (_, c) => c.toUpperCase()); + +// A fake element whose querySelector('#id') / querySelectorAll('[data-x]') +// answer from the markup written to it, so the listeners the page attaches +// are kept on the same button objects the test clicks. +function fakeEl(id, byId) { + let html = ''; + let listCache = {}; + const listeners = {}; + const self = { + id, value: '', style: {}, dataset: {}, parentElement: null, + get innerHTML() { return html; }, + set innerHTML(v) { html = String(v); listCache = {}; }, + classList: fakeClassList([]), + addEventListener(type, fn) { (listeners[type] = listeners[type] || []).push(fn); }, removeEventListener() {}, + dispatch(type, ev) { for (const fn of listeners[type] || []) fn(ev); }, + contains: () => true, closest: () => null, + querySelector(sel) { + const m = /^#([\w-]+)$/.exec(sel); + if (m && html.indexOf('id="' + m[1] + '"') >= 0) return byId(m[1]); + return null; + }, + querySelectorAll(sel) { + const m = /^\[(data-[\w-]+)\]$/.exec(sel); + if (!m) return []; + if (!listCache[sel]) { + const re = new RegExp('