mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-11-11 12:37:33 +00:00
## Summary The map route deep link (`#/map?packet=<hash>[&obs=<id>]`) always opened in the canonical-path view. Two optional query params now choose the path picker's starting view, so a shared link shows exactly what the sender meant. | Param | Effect | |---|---| | `paths=all` | Opens the picker's **All** view (every unique path), the same as clicking **All** | | `observer=<pubkey>` | Full public key or a hex prefix of 8+ characters, case-insensitive. Picks that observer's **earliest** observation (by timestamp, then id), makes it the canonical route and isolates its path row, the same as clicking the row. "Back to packet" then links to that observation. | Examples: ``` #/map?packet=974ad4ecefbe3761&paths=all #/map?packet=974ad4ecefbe3761&observer=a1b2c3d4 ``` When params are combined, `obs=` wins over `observer=`, and `observer=` wins over `paths=all`. An unknown or malformed observer falls back to the current default view and logs a console warning. ## Changes - `public/route-view-utils.js`: two new pure helpers. - `resolveDeepLinkObservation(observations, params)` picks the observation and the initial view. - `pathGroupKey(rawHops, filterHops)` is the path picker's grouping key. It was written out twice in `route-view.js` and now lives in one place, which keeps the deep link and the picker rows in sync. - `public/map.js`: `loadRouteFromDeepLink` uses the helper and passes `initialPathView` through `drawPacketRouteMulti` to the renderer. - `public/route-view.js`: the picker count and rows use `pathGroupKey`. `sidebar._applyPathView` applies the view through the existing All and row click handlers, in the frame after the route's initial fit. ## Performance This doesn't touch a hot path. The work is one pass over a single packet's observations (tens at most) plus a scan of the picker rows, and it runs once per deep-link load. It adds no API calls. ## Config / customizer No new configurable values. ## Tests - New unit suite `tests/unit/test-map-deeplink-path-params.js` (29 checks). It covers prefix vs full key, case, short/non-hex keys, earliest-observation choice with tie-break, the precedence rules, `pathGroupKey`, and the source wiring. - `tests/unit/test-issue-1633-hide-1byte-hops.js` replayed a copy of the key logic. It now tests the real `pathGroupKey`. - New Playwright suite `tests/e2e/test-map-deeplink-path-params-e2e.js` (5 checks) on the seeded #1486 packet. It's registered in `scripts/non-unit-tests.json` and CI shard 3. With the `route-view.js` change removed, 3 of its 5 checks fail. - Local runs against the CI-prepared fixture: `test-all.sh` passes. Route-map a11y E2E 20/20, map mobile E2E 3/3, `test-e2e-playwright.js` 149/152 (3 skipped by the fixture, 0 failed). ## Browser validation Checked in a browser against a local server, using a scratch copy of the fixture that gives the seeded packet multi-hop paths through real GPS nodes. One observer had two observations, and the API listed the later one first. - `observer=<prefix>` picked that observer's earliest observation and drew only its path, with numbered markers and the map fitted to it. - `paths=all` drew the same picture as clicking **All** by hand.
107 lines
4.9 KiB
JavaScript
107 lines
4.9 KiB
JavaScript
/**
|
|
* E2E: map route deep-link path params.
|
|
*
|
|
* #/map?packet=<hash>&observer=<pubkey or 8+ char prefix>
|
|
* → that observer's observation is the canonical route and its path is
|
|
* isolated in the path picker (row active), as if the row was clicked.
|
|
* #/map?packet=<hash>&paths=all
|
|
* → the picker opens in its "All" union view, as if All was clicked.
|
|
*
|
|
* Uses the CI-seeded #1486 transmission (SEED_HASH): three observations,
|
|
* observers idx 1/2/3 on paths ["AA"], ["BB"], ["CC"]. When running
|
|
* locally, seed the same row (see .github/workflows/deploy.yml for the SQL).
|
|
*
|
|
* Usage: BASE_URL=http://localhost:13581 node test-map-deeplink-path-params-e2e.js
|
|
*/
|
|
'use strict';
|
|
const { chromium } = require('playwright');
|
|
|
|
const BASE = process.env.BASE_URL || 'http://localhost:13581';
|
|
const SEED_HASH = 'fae0c9e6d357a814';
|
|
|
|
let passed = 0, failed = 0;
|
|
async function step(name, fn) {
|
|
try { await fn(); passed++; console.log(' ✓ ' + name); }
|
|
catch (e) { failed++; console.error(' ✗ ' + name + ': ' + e.message); }
|
|
}
|
|
function assert(c, m) { if (!c) throw new Error(m || 'assertion failed'); }
|
|
|
|
// Cold-load a route deep link and wait until the path picker exists and the
|
|
// deep-linked view (if any) has had its frame to apply.
|
|
async function openRoute(page, query) {
|
|
await page.goto(BASE + '/#/map?packet=' + SEED_HASH + query, { waitUntil: 'domcontentloaded' });
|
|
await page.reload({ waitUntil: 'domcontentloaded' });
|
|
await page.waitForSelector('.mc-rt-path-row', { timeout: 15000 });
|
|
await page.evaluate(() => new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r))));
|
|
return page.evaluate(() => {
|
|
const sb = document.querySelector('.mc-rt-sidebar');
|
|
return {
|
|
rows: [...document.querySelectorAll('.mc-rt-path-row')].map(r => ({
|
|
key: r.dataset.pathKey, active: r.classList.contains('mc-rt-path-active')
|
|
})),
|
|
back: sb && sb.querySelector('.mc-rt-back-link').getAttribute('href'),
|
|
union: !!sb && Array.isArray(sb._unionOverlay),
|
|
};
|
|
});
|
|
}
|
|
const active = s => s.rows.filter(r => r.active).map(r => r.key);
|
|
|
|
(async () => {
|
|
const browser = await chromium.launch({
|
|
headless: true,
|
|
executablePath: process.env.CHROMIUM_PATH || undefined,
|
|
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
|
|
});
|
|
const ctx = await browser.newContext({ viewport: { width: 1400, height: 900 } });
|
|
const page = await ctx.newPage();
|
|
page.setDefaultTimeout(10000);
|
|
page.on('pageerror', (e) => console.error('[pageerror]', e.message));
|
|
|
|
console.log(`\n=== map deep-link path params E2E against ${BASE} ===`);
|
|
|
|
const resp = await ctx.request.get(BASE + '/api/packets/' + SEED_HASH);
|
|
const observations = (await resp.json()).observations || [];
|
|
const target = observations.find(o => JSON.parse(o.path_json || '[]').join() === 'BB');
|
|
assert(target, 'seeded observation with path ["BB"] not found');
|
|
const pubkey = String(target.observer_id);
|
|
|
|
await step('default view: no path isolated, no union view', async () => {
|
|
const s = await openRoute(page, '');
|
|
assert(s.rows.length === 3, 'expected 3 unique paths, got ' + s.rows.length);
|
|
assert(active(s).length === 0, 'no row should be active, got ' + active(s));
|
|
assert(!s.union, 'union view should not be open');
|
|
});
|
|
|
|
await step('observer=<8-char prefix> isolates that observer\'s path', async () => {
|
|
const s = await openRoute(page, '&observer=' + pubkey.slice(0, 8).toLowerCase());
|
|
assert(active(s).join() === 'BB', 'expected BB active, got ' + active(s));
|
|
assert(s.back === '#/packets/' + SEED_HASH + '?obs=' + target.id, 'back link should name the observation, got ' + s.back);
|
|
});
|
|
|
|
await step('observer=<full public key> isolates the same path', async () => {
|
|
const s = await openRoute(page, '&observer=' + pubkey);
|
|
assert(active(s).join() === 'BB', 'expected BB active, got ' + active(s));
|
|
});
|
|
|
|
await step('unmatched obs falls through to observer; back link names the selected observation', async () => {
|
|
const s = await openRoute(page, '&obs=999999999&observer=' + pubkey.slice(0, 8));
|
|
assert(active(s).join() === 'BB', 'expected BB active, got ' + active(s));
|
|
assert(s.back === '#/packets/' + SEED_HASH + '?obs=' + target.id, 'back link must not keep the unmatched obs, got ' + s.back);
|
|
});
|
|
|
|
await step('paths=all opens the All (union) view', async () => {
|
|
const s = await openRoute(page, '&paths=all');
|
|
assert(s.union, 'union overlay should be drawn');
|
|
assert(active(s).length === 0, 'no row should be active, got ' + active(s));
|
|
});
|
|
|
|
await step('unknown observer falls back to the default view', async () => {
|
|
const s = await openRoute(page, '&observer=00000000');
|
|
assert(active(s).length === 0 && !s.union, 'expected the default view');
|
|
});
|
|
|
|
await browser.close();
|
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
process.exit(failed === 0 ? 0 : 1);
|
|
})().catch(e => { console.error(e); process.exit(1); });
|