Register the Navara package workarounds and check them against the package

src/globe/navara/WORKAROUNDS.md lists every place the adapter works
around @navaramap/three 0.2.1: what the package does wrong or lacks, how
it is detected and what to delete once it is fixed.
scripts/qa-navara-canaries.mjs confirms three of the bugs on the real
package in the browser (batch ids never released, events of a deleted
vector layer kept queued, idle reported before a raster tile arrives) and
names the workaround to delete when one is gone.

The private members the adapter reads are probed when the engine starts,
with an error naming the member and the pinned version instead of a
silent fallback, and the test that only internals.js reaches private
members now scans the adapter's subfolders too.
This commit is contained in:
German Khokhlov committed 2026-11-10 15:22:43 +04:00
1 parent 9c2a2d6951
commit ffbceb977c
4 files changed
+625 -35

No files matched your search

+320
View File
@@ -0,0 +1,320 @@
#!/usr/bin/env node
// Package canaries deliberately bypass adapter workarounds. A passing canary
// means the upstream bug still exists. Fixture errors prove nothing about it.
import assert from 'node:assert/strict';
import { mkdtemp, rm } from 'node:fs/promises';
import path from 'node:path';
import puppeteer from 'puppeteer';
import { closeBrowser } from './qa/browser.mjs';
const origin = new URL(process.argv[2] ?? 'http://localhost:4300').origin;
assert.ok(['localhost', '127.0.0.1'].includes(new URL(origin).hostname));
assert.equal(
new URL(origin).port,
'4300',
'Use the dedicated keyless canary server on port 4300',
);
const profile = await mkdtemp(path.resolve('tmp_navara_canaries_profile_'));
const results = { confirmed: 0, gone: 0, fixtureErrors: 0 };
let browser;
try {
browser = await puppeteer.launch({
headless: true,
userDataDir: profile,
protocolTimeout: 60_000,
args: ['--use-angle=metal', '--enable-gpu'],
});
const page = await browser.newPage();
await page.setViewport({ width: 800, height: 600 });
page.setDefaultTimeout(30_000);
page.on('pageerror', (error) => console.error(error.message));
await page.setRequestInterception(true);
const pendingTiles = [];
const tile = {
contentType: 'image/png',
body: Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGP4DwQACfsD/fteaysAAAAASUVORK5CYII=',
'base64',
),
};
let holdTiles = true;
await page.exposeFunction('pendingTileCount', () => pendingTiles.length);
page.on('request', (request) => {
const url = new URL(request.url());
if (url.pathname === '/tmp_navara_canaries.html') {
request.respond({
contentType: 'text/html',
body: '<body style="margin:0"><div id="globe" style="width:800px;height:600px"></div></body>',
});
} else if (url.pathname.startsWith('/tmp_navara_canaries_tile/')) {
if (holdTiles) pendingTiles.push(request);
else request.respond(tile);
} else if (
url.origin === origin ||
['blob:', 'data:'].includes(url.protocol)
) {
request.continue();
} else request.abort();
});
await page.goto(`${origin}/tmp_navara_canaries.html`);
await page.evaluate(async () => {
// Use Vite's resolved package URLs so the adapter and fixture share classes.
const imports = await (
await fetch('/src/globe/navara/internals.js')
).text();
const sdkUrl = imports.match(
/from "([^"]*\/three\/dist\/index.js[^\"]*)"/,
)[1];
const optionsSource = await (
await fetch('/src/globe/navara/viewOptions.js')
).text();
const threeUrl = optionsSource.match(/from "([^"]+)"/)[1];
const [sdk, three, internals, options] = await Promise.all([
import(sdkUrl),
import(threeUrl),
import('/src/globe/navara/internals.js'),
import('/src/globe/navara/viewOptions.js'),
]);
const container = document.getElementById('globe');
const canvas = document.createElement('canvas');
container.appendChild(canvas);
canvas.getContext('webgl2', options.NAVARA_CONTEXT_ATTRIBUTES);
const view = new sdk.default(
options.navaraViewOptions({ container, canvas }),
);
await view.init();
internals.navaraHooks(view);
view.lit = false;
view.setCamera({
lng: 0,
lat: 0,
height: 18_000_000,
heading: 0,
pitch: -90,
roll: 0,
});
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const until = async (predicate, timeout = 5000) => {
const start = performance.now();
while (!predicate()) {
if (performance.now() - start > timeout)
throw new Error('Fixture did not become ready');
await wait(20);
}
};
const frame = () =>
new Promise((resolve, reject) => {
const timer = setTimeout(() => {
view.off('postRender', done);
reject(new Error('No rendered frame within five seconds'));
}, 5000);
const done = () => {
clearTimeout(timer);
view.off('postRender', done);
resolve();
};
view.on('postRender', done);
view.forceUpdate();
});
let ctx;
class Probe extends sdk.MeshDesc {
createMesh() {
ctx = this.ctx;
return new three.Group();
}
}
view.registerMesh('canaryProbe', Probe);
const probe = view.addMesh({ canaryProbe: {} });
await frame();
const polygon = {
type: 'Feature',
properties: {},
geometry: {
type: 'Polygon',
coordinates: [
[
[-8, -8],
[8, -8],
[8, 8],
[-8, 8],
[-8, -8],
],
],
},
};
window.canary = {
sdk,
three,
internals,
view,
canvas,
ctx,
probe,
wait,
until,
frame,
polygon,
};
});
async function check(name, remove, run) {
try {
const evidence = await page.evaluate(run);
assert.equal(
typeof evidence?.present,
'boolean',
'Fixture returned no verdict',
);
if (evidence.present) {
console.log(`PASS ${name}: bug confirmed; ${JSON.stringify(evidence)}`);
results.confirmed++;
} else {
console.error(
`BUG GONE ${name}: bug absent with fixture controls satisfied; review/delete ${remove}. ${JSON.stringify(evidence)}`,
);
results.gone++;
}
} catch (error) {
console.error(
`FIXTURE ERROR ${name}: ${error.message}; no conclusion about the bug; keep the workaround`,
);
results.fixtureErrors++;
}
}
await check(
'batch-ids-retained',
'src/globe/navara/movers/batchIds.js and its pool wiring',
async () => {
const { sdk, three, ctx, frame } = window.canary;
const batches = [];
for (let round = 0; round < 3; round++) {
const mesh = new three.InstancedMesh(
new three.PlaneGeometry(),
new three.MeshBasicMaterial(),
8,
);
const wrapper = new sdk.PickableInstancedMeshWrapper(mesh, 8, ctx);
ctx.registerPickableMesh('canary-batch', wrapper);
batches.push([...wrapper.batchIds]);
ctx.unregisterPickableMesh('canary-batch');
mesh.geometry.dispose();
mesh.material.dispose();
await frame();
}
const ids = batches.flat();
if (
batches.some(
(batch) =>
batch.length !== 8 ||
batch.some((id) => !Number.isInteger(id) || id <= 0) ||
new Set(batch).size !== batch.length,
)
)
throw new Error('Fixture did not allocate eight valid IDs per batch');
return {
present: new Set(ids).size === ids.length,
batches,
};
},
);
await check(
'deleted-vector-events-retained',
'src/globe/navara/featureQueue.js and dropOrphanFeatureChanges wiring',
async () => {
const { view, internals, polygon, frame, wait } = window.canary;
for (let attempt = 0; attempt < 12; attempt++) {
const source = view.addSource({ type: 'geojson', data: polygon });
const layer = view.addLayer({
type: 'vector',
source,
polygon: { color: 0xff0000 },
});
await wait(30 + attempt * 10);
layer.update({ type: 'vector', source, polygon: { color: 0x00ff00 } });
layer.delete();
source.delete();
await wait(100);
}
const orphaned = () =>
internals
.featureChangeQueue(view)
.events.filter(
(event) => !view.findLayerById(String(event.layer_id)),
);
const before = orphaned().length;
await frame();
await wait(500);
await frame();
const after = orphaned().length;
internals.navaraHooks(view).dropOrphanFeatureChanges();
if (before === 0)
throw new Error('Fixture did not produce orphaned vector events');
return { present: after > 0, before, after };
},
);
await check(
'idle-before-raster-arrives',
'maps.js SETTLE_MS/SETTLE_EVERY_MS redraw timer',
async () => {
const { view, internals, until } = window.canary;
await until(() => internals.isIdle(view));
let idle = false;
const onIdle = () => {
idle = true;
};
view.on('idle', onIdle);
const source = view.addSource({
type: 'raster-tile',
url: `${location.origin}/tmp_navara_canaries_tile/{z}/{x}/{y}.png`,
maxZoom: 1,
});
const layer = view.addLayer({ type: 'raster', source });
view.forceUpdate();
await until(() => idle);
view.off('idle', onIdle);
window.canary.raster = { source, layer };
const pending = await window.pendingTileCount();
if (pending === 0)
throw new Error('Fixture did not hold any raster requests');
return {
present: idle && pending > 0,
idle,
pending,
};
},
);
holdTiles = false;
for (const request of pendingTiles) await request.respond(tile);
if (pendingTiles.length) {
await page.waitForFunction(() =>
performance
.getEntriesByType('resource')
.some(
(entry) =>
entry.name.includes('/tmp_navara_canaries_tile/') &&
entry.responseEnd > 0,
),
);
}
await page.evaluate(() => {
window.canary.raster?.layer.delete();
window.canary.raster?.source.delete();
});
await page.evaluate(() => window.canary.view.dispose());
} catch (error) {
console.error(
`FIXTURE ERROR browser session: ${error.message}; no conclusion about the bugs; keep the workarounds`,
);
results.fixtureErrors++;
} finally {
if (browser) await closeBrowser(browser);
await rm(profile, { recursive: true, force: true });
console.log(
`${results.confirmed} bugs confirmed, ${results.gone} bugs gone, ${results.fixtureErrors} fixture errors`,
);
process.exitCode = results.gone === 0 && results.fixtureErrors === 0 ? 0 : 1;
}