mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-08 00:37:38 +00:00
assertViewport allowed 0.001 degrees between the centre it asked for and the centre Leaflet reports back. Leaflet keeps the centre as a pixel coordinate, so a read-back centre is only accurate to the pixel it landed on, and one pixel spans 360/(256*2^zoom) degrees: 0.02197 at zoom 6, 0.00137 at zoom 10. The flat budget demanded 1/20 of a pixel at zoom 6 while its own comment said 'allow sub-pixel coordinate rounding'.
It passes on master because the container rounds favourably. It fails as soon as the layout changes: on the ON8AR fork, whose coverage page carries one extra toolbar row, the first assertion reported {lat:11.996338401936226, lng:33.99169921875001, zoom:6} against an expected 12/34 — 0.167 px of latitude and 0.378 px of longitude, so the view was right and the assertion was wrong. Any container-height change can trip it, fork or not.
The budget is now one pixel at the asserted zoom via a degPerPixel(zoom) helper, applied to both centre assertions and to the waitForFunction on the hash (zoom 10, where 0.001 was 0.73 px and merely lucky). For latitude the Mercator scale is the longitude figure times cos(lat), so the longitude figure is the looser bound there, deliberately: this asserts the view is where we asked, not a projection identity. A one-tile miss at zoom 6 is 5.6 degrees and still fails.
Verified by arithmetic rather than locally (the suite needs a served instance and a browser): the observed drift falls inside the new budget and a one-tile miss does not. CI run 35273657579 is green, with 'RX coverage viewport browser regressions OK' in the Playwright job.
Merged by the interim maintainer without a second human reviewer: CI is the independent check.
89 lines
5.4 KiB
JavaScript
89 lines
5.4 KiB
JavaScript
#!/usr/bin/env node
|
|
'use strict';
|
|
// #2032: real Leaflet viewport initialization, persistence and observer links.
|
|
const assert = require('assert');
|
|
const { chromium } = require('playwright');
|
|
const BASE = process.env.BASE_URL || 'http://localhost:3000';
|
|
(async () => {
|
|
const browser = await chromium.launch({ headless: true, executablePath: process.env.CHROMIUM_PATH || undefined });
|
|
try {
|
|
const page = await browser.newPage();
|
|
const errors = [];
|
|
page.on('pageerror', error => errors.push(error.message));
|
|
await page.addInitScript(() => {
|
|
let leaflet;
|
|
Object.defineProperty(window, 'L', {
|
|
configurable: true,
|
|
get: () => leaflet,
|
|
set(value) {
|
|
leaflet = value;
|
|
value.Map.addInitHook(function () { window.__rxTestMap = this; });
|
|
}
|
|
});
|
|
});
|
|
await page.route('**/api/config/client', async route => {
|
|
const response = await route.fetch();
|
|
await route.fulfill({ json: { ...await response.json(), clientRxCoverage: true } });
|
|
});
|
|
await page.route('**/api/config/map', route => route.fulfill({ json: { center: [12, 34], zoom: 6 } }));
|
|
await page.route('**/api/rx-leaderboard?*', route => route.fulfill({ json: { observers: [] } }));
|
|
let worldRequests = 0;
|
|
const observerCell = { type: 'Feature', geometry: { type: 'Polygon', coordinates: [[[40, 10], [41, 10], [41, 11], [40, 11], [40, 10]]] }, properties: { count: 1, has_sig: false, nodes: [] } };
|
|
await page.route('**/api/rx-coverage?*', route => {
|
|
if (new URL(route.request().url()).searchParams.get('bbox') === '-90,-180,90,180') worldRequests++;
|
|
return route.fulfill({ json: { type: 'FeatureCollection', features: [observerCell] } });
|
|
});
|
|
async function waitMap() {
|
|
await page.waitForFunction(() => window.__rxTestMap && window.__rxTestMap._loaded && document.querySelector('#rxMap.leaflet-container'));
|
|
}
|
|
async function viewport() {
|
|
return page.evaluate(() => { const m = window.__rxTestMap, c = m.getCenter(); return { lat: c.lat, lng: c.lng, zoom: m.getZoom() }; });
|
|
}
|
|
// Leaflet keeps the centre as a pixel coordinate, so a centre read back is
|
|
// only ever accurate to the pixel it landed on. One pixel spans
|
|
// 360/(256*2^zoom) degrees of longitude: 0.022° at zoom 6, 0.0014° at
|
|
// zoom 10. The budget below used to be a flat 0.001°, which at zoom 6
|
|
// demanded 1/20 of a pixel, so any change to the map container's size
|
|
// moved the pixel centre enough to fail an assertion about the *view*.
|
|
// For latitude the Mercator scale is this figure times cos(lat), so using
|
|
// the longitude figure is the looser bound there, deliberately.
|
|
const degPerPixel = (zoom) => 360 / (256 * Math.pow(2, zoom));
|
|
|
|
function assertViewport(actual, expected) {
|
|
const budget = degPerPixel(expected.zoom);
|
|
const off = (axis) => axis + ' ' + actual[axis] + ' is more than one pixel ('
|
|
+ budget.toFixed(5) + '°) from ' + expected[axis] + ': ' + JSON.stringify(actual);
|
|
assert(Math.abs(actual.lat - expected.lat) < budget, off('lat'));
|
|
assert(Math.abs(actual.lng - expected.lng) < budget, off('lng'));
|
|
assert.equal(actual.zoom, expected.zoom);
|
|
}
|
|
await page.goto(BASE + '/#/rx-coverage'); await waitMap();
|
|
assertViewport(await viewport(), { lat: 12, lng: 34, zoom: 6 });
|
|
await page.evaluate(() => window.__rxTestMap.setView([22, 44], 10, { animate: false }));
|
|
await page.waitForFunction((budget) => Math.abs(Number(new URLSearchParams(location.hash.split('?')[1]).get('lat')) - 22) < budget, degPerPixel(10));
|
|
assertViewport(await page.evaluate(() => JSON.parse(localStorage.getItem('rx-coverage-view'))), { lat: 22, lng: 44, zoom: 10 });
|
|
assert.equal(await page.evaluate(() => localStorage.getItem('map-view')), null, 'coverage must not write the main map\'s saved viewport');
|
|
await page.reload(); await waitMap(); assertViewport(await viewport(), { lat: 22, lng: 44, zoom: 10 });
|
|
// Remove the shareable URL to independently verify this page's saved state.
|
|
await page.goto(BASE + '/#/rx-coverage'); await page.reload(); await waitMap();
|
|
assertViewport(await viewport(), { lat: 22, lng: 44, zoom: 10 });
|
|
await page.goto(BASE + '/#/rx-coverage?days=14&rx=abcd&lat=0&lon=0&zoom=5'); await page.reload(); await waitMap();
|
|
await page.waitForTimeout(500);
|
|
assertViewport(await viewport(), { lat: 0, lng: 0, zoom: 5 });
|
|
assert.equal(worldRequests, 0, 'explicit observer viewport must not auto-fit');
|
|
await page.click('#rxDays button[data-days="30"]');
|
|
const params = new URLSearchParams(new URL(page.url()).hash.split('?')[1]);
|
|
assert.equal(params.get('days'), '30'); assert.equal(params.get('rx'), 'abcd'); assert.equal(params.get('lat'), '0.00000');
|
|
// Observer-only links still fit the observer's complete extent.
|
|
await page.goto(BASE + '/#/rx-coverage?rx=abcd'); await page.reload(); await waitMap();
|
|
await page.waitForFunction(() => {
|
|
const c = window.__rxTestMap.getCenter();
|
|
return c.lat > 10 && c.lat < 11 && c.lng > 40 && c.lng < 41;
|
|
});
|
|
assert(worldRequests > 0, 'observer-only links must request full extent');
|
|
if (process.env.SCREENSHOT_PATH) await page.screenshot({ path: process.env.SCREENSHOT_PATH, fullPage: true });
|
|
assert.deepStrictEqual(errors, []);
|
|
console.log('RX coverage viewport browser regressions OK');
|
|
} finally { await browser.close(); }
|
|
})().catch(error => { console.error(error); process.exit(1); });
|