mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
test(qa): sprite and enrichment checks on both engines through the probe
The QA probe gains drawn(layerId): what a layer draws per object (glyph
image, scale, shown, screen rotation, alpha, position; its 3D model's
scale, shown, ready), from the flights layer's qaDrawn(), which reads the
renderer's handles, so it is the same on every engine.
qa-sprites-b5 and qa-enrich-ambient take --engine=cesium|navara: without a
viewer they read the layer through the probe and move the engine camera;
the military rows/cluster are skipped where the engine has no military
layer. Cesium verdicts unchanged (sprites 9/9; enrich 9/2, the same E8/E10
failures as on 7c7422e2); Navara: sprites 7/7 (flights rows and 3D cluster),
enrich 9/2 (the same two). Task 10b, checklist item 7.
This commit is contained in:
1 parent
03a0ff3a01
commit
132a941e9a
5 files changed
+135
-46
No files matched your search
@@ -66,9 +66,14 @@ const getOpt = (name, dflt) => {
|
||||
return i >= 0 && argv[i + 1] ? argv[i + 1] : dflt;
|
||||
};
|
||||
|
||||
const APP_URL = getOpt('--url', 'http://localhost:4173');
|
||||
const BASE_URL = getOpt('--url', 'http://localhost:4173');
|
||||
// --engine=cesium|navara (phase 3): Navara reads the layer through the QA
|
||||
// probe (window.__godsEyeView.layers.drawn) and moves the engine camera.
|
||||
const ENGINE = (argv.find((arg) => arg.startsWith('--engine=')) ?? '--engine=cesium').slice(9);
|
||||
const APP_URL =
|
||||
ENGINE === 'cesium' ? BASE_URL : `${BASE_URL.replace(/\/$/, '')}/?engine=${ENGINE}`;
|
||||
const HEADFUL = getFlag('--headful');
|
||||
const SHOT_DIR = path.resolve('qa-shots/enrich-ambient');
|
||||
const SHOT_DIR = path.resolve(ENGINE === 'cesium' ? 'qa-shots/enrich-ambient' : `qa-shots/enrich-ambient-${ENGINE}`);
|
||||
|
||||
// Client-side bounds under test (flights.js constants):
|
||||
const MAX_INFLIGHT = 4; // ENRICH_MAX_INFLIGHT
|
||||
@@ -289,7 +294,7 @@ async function main() {
|
||||
console.log('Loading app...');
|
||||
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||||
await page.waitForFunction(
|
||||
() => window.__godsEyeView && window.__godsEyeView.viewer && window.__godsEyeView.dataManager,
|
||||
() => window.__godsEyeView && window.__godsEyeView.engine && window.__godsEyeView.dataManager && window.__godsEyeView.layers,
|
||||
{ timeout: 60000, polling: 200 }
|
||||
);
|
||||
console.log(' App globals ready.');
|
||||
@@ -298,6 +303,12 @@ async function main() {
|
||||
await page.evaluate(() => {
|
||||
window.__collectBillboards = function () {
|
||||
const v = window.__godsEyeView.viewer;
|
||||
// Without a viewer (Navara): what the flights layer draws, through
|
||||
// the engine-neutral probe.
|
||||
if (!v)
|
||||
return (window.__godsEyeView.layers.drawn('flights') ?? [])
|
||||
.filter((row) => !row.trackedModel)
|
||||
.map(({ id, image, scale, show }) => ({ id, image, scale, show }));
|
||||
const out = [];
|
||||
const walk = (coll) => {
|
||||
const n = coll.length;
|
||||
@@ -321,12 +332,12 @@ async function main() {
|
||||
// framing the fleet when the layer polls — otherwise the frustum test
|
||||
// (correctly) skips off-screen planes.
|
||||
const centerLon = ROW_LON0 + ((SPEC.planes.length - 1) / 2) * ROW_STEP_DEG;
|
||||
await page.evaluate(({ lat, lon, height }) => {
|
||||
const v = window.__godsEyeView.viewer;
|
||||
v.camera.cancelFlight(); // the boot fly-to-Austin otherwise stomps setView
|
||||
const C3 = v.camera.position.constructor;
|
||||
v.camera.setView({
|
||||
destination: C3.fromDegrees(lon, lat, height),
|
||||
await page.evaluate(async ({ lat, lon, height }) => {
|
||||
const camera = window.__godsEyeView.engine.camera;
|
||||
camera.cancelFlight(); // the boot fly-to-Austin otherwise stomps setView
|
||||
const geo = await import('/src/geo/index.js');
|
||||
camera.setView({
|
||||
destination: geo.cartesian3.fromDegrees(lon, lat, height),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
}, { lat: ROW_LAT, lon: centerLon, height: 20000 });
|
||||
@@ -334,7 +345,7 @@ async function main() {
|
||||
console.log('Priming straight-flight history through the render delay (30 s)...');
|
||||
const primed = await page.evaluate(async () => {
|
||||
const dm = window.__godsEyeView.dataManager;
|
||||
const v = window.__godsEyeView.viewer;
|
||||
const v = window.__godsEyeView.viewer ?? null;
|
||||
window.__ENR.timeOffsetSec = -32;
|
||||
await dm.setEnabled('flights', true);
|
||||
const fl = dm.layers.get('flights').module;
|
||||
@@ -456,7 +467,7 @@ async function main() {
|
||||
console.log('\nE8 — next poll after enrichment');
|
||||
await page.evaluate(async () => {
|
||||
const dm = window.__godsEyeView.dataManager;
|
||||
const v = window.__godsEyeView.viewer;
|
||||
const v = window.__godsEyeView.viewer ?? null;
|
||||
await dm.layers.get('flights').module.update(v);
|
||||
});
|
||||
await sleep(500);
|
||||
@@ -493,7 +504,7 @@ async function main() {
|
||||
const startsBeforeBatch = await page.evaluate(() => window.__ENRICH_LOG.starts.length);
|
||||
await page.evaluate(async ({ hexes, rowLat, rowLon0, stepDeg }) => {
|
||||
const dm = window.__godsEyeView.dataManager;
|
||||
const v = window.__godsEyeView.viewer;
|
||||
const v = window.__godsEyeView.viewer ?? null;
|
||||
// Two fresh rows just north of the original one — still inside the
|
||||
// top-down 20 km frame, so the sweep's frustum test keeps them.
|
||||
// STATIONARY (speed 0): moving planes drift north out of the frustum at
|
||||
@@ -522,7 +533,7 @@ async function main() {
|
||||
// One more poll while exhausted — still nothing new.
|
||||
await page.evaluate(async () => {
|
||||
const dm = window.__godsEyeView.dataManager;
|
||||
await dm.layers.get('flights').module.update(window.__godsEyeView.viewer);
|
||||
await dm.layers.get('flights').module.update(window.__godsEyeView.viewer ?? null);
|
||||
});
|
||||
await sleep(1000);
|
||||
const exhausted = await page.evaluate(() => ({
|
||||
@@ -544,7 +555,7 @@ async function main() {
|
||||
await sleep(RESUME_WINDOW_MS + 200); // a full (shortened) refill window elapses
|
||||
await page.evaluate(async () => {
|
||||
const dm = window.__godsEyeView.dataManager;
|
||||
await dm.layers.get('flights').module.update(window.__godsEyeView.viewer);
|
||||
await dm.layers.get('flights').module.update(window.__godsEyeView.viewer ?? null);
|
||||
});
|
||||
const expectedTotal = startsBeforeBatch + BATCH_COUNT; // every batch plane eventually requested
|
||||
const resumed = await page.waitForFunction(
|
||||
|
||||
+57
-32
@@ -53,9 +53,15 @@ const getOpt = (name, dflt) => {
|
||||
return i >= 0 && argv[i + 1] ? argv[i + 1] : dflt;
|
||||
};
|
||||
|
||||
const APP_URL = getOpt('--url', 'http://localhost:4173');
|
||||
const BASE_URL = getOpt('--url', 'http://localhost:4173');
|
||||
// --engine=cesium|navara (phase 3): without a viewer the layer is read through
|
||||
// the QA probe (window.__godsEyeView.layers.drawn) and the military checks are
|
||||
// skipped where the engine has no military layer.
|
||||
const ENGINE = (argv.find((arg) => arg.startsWith('--engine=')) ?? '--engine=cesium').slice(9);
|
||||
const APP_URL =
|
||||
ENGINE === 'cesium' ? BASE_URL : `${BASE_URL.replace(/\/$/, '')}/?engine=${ENGINE}`;
|
||||
const HEADFUL = getFlag('--headful');
|
||||
const SHOT_DIR = path.resolve('qa-shots/b5');
|
||||
const SHOT_DIR = path.resolve(ENGINE === 'cesium' ? 'qa-shots/b5' : `qa-shots/b5-${ENGINE}`);
|
||||
|
||||
const FL_MODEL_SCALE = 1; // flights.js (airplane.glb is transform-applied and meter-scale)
|
||||
const MIL_PLANE_MODEL_SCALE = 1; // military heavy classes share the same baked airplane.glb
|
||||
@@ -275,7 +281,7 @@ async function main() {
|
||||
console.log('Loading app...');
|
||||
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||||
await page.waitForFunction(
|
||||
() => window.__godsEyeView && window.__godsEyeView.viewer && window.__godsEyeView.dataManager,
|
||||
() => window.__godsEyeView && window.__godsEyeView.engine && window.__godsEyeView.dataManager && window.__godsEyeView.layers,
|
||||
{ timeout: 60000, polling: 200 }
|
||||
);
|
||||
console.log(' App globals ready.');
|
||||
@@ -285,6 +291,10 @@ async function main() {
|
||||
// Every billboard (image + alignedAxis + id) in every collection.
|
||||
window.__collectBillboards = function () {
|
||||
const v = window.__godsEyeView.viewer;
|
||||
if (!v)
|
||||
return (window.__godsEyeView.layers.drawn('flights') ?? [])
|
||||
.filter((row) => !row.trackedModel)
|
||||
.map(({ id, image, scale, show }) => ({ id, image, scale, show }));
|
||||
const out = [];
|
||||
const walk = (coll) => {
|
||||
const n = coll.length;
|
||||
@@ -304,6 +314,10 @@ async function main() {
|
||||
// Every glTF model primitive (modelMatrix + ready flag), with id + scale.
|
||||
window.__collectModels = function () {
|
||||
const v = window.__godsEyeView.viewer;
|
||||
if (!v)
|
||||
return (window.__godsEyeView.layers.drawn('flights') ?? [])
|
||||
.filter((row) => row.model && (row.trackedModel || !row.tracked))
|
||||
.map(({ id, model }) => ({ id, ...model, radius: null }));
|
||||
const out = [];
|
||||
const walk = (coll) => {
|
||||
const n = coll.length;
|
||||
@@ -331,14 +345,14 @@ async function main() {
|
||||
};
|
||||
// Canvas coordinates of every synthetic plane at its displayed position —
|
||||
// ground truth for identifying which glyph is which in a screenshot.
|
||||
window.__planeCanvasCoords = function (layer, renderDelaySec, altOf) {
|
||||
const v = window.__godsEyeView.viewer;
|
||||
window.__planeCanvasCoords = async function (layer, renderDelaySec, altOf) {
|
||||
const geo = await import('/src/geo/index.js');
|
||||
const { engine } = window.__godsEyeView;
|
||||
const S = window.__SPR;
|
||||
const C3 = v.camera.position.constructor;
|
||||
return S[layer].map((p, i) => {
|
||||
const s = window.__displayedState(layer, i, renderDelaySec);
|
||||
const pos = C3.fromDegrees(s.lon, s.lat, altOf === 'ft' ? p.altFt * 0.3048 : p.altM);
|
||||
const c = v.scene.cartesianToCanvasCoordinates(pos);
|
||||
const pos = geo.cartesian3.fromDegrees(s.lon, s.lat, altOf === 'ft' ? p.altFt * 0.3048 : p.altM);
|
||||
const c = engine.projector.worldToWindow(pos);
|
||||
return `${p.icao || p.hex}=(${c ? Math.round(c.x) : '-'},${c ? Math.round(c.y) : '-'})`;
|
||||
}).join(' ');
|
||||
};
|
||||
@@ -348,26 +362,29 @@ async function main() {
|
||||
console.log('Priming straight-flight history through the render delay (30 s / 15 s)...');
|
||||
const primed = await page.evaluate(async () => {
|
||||
const dm = window.__godsEyeView.dataManager;
|
||||
const v = window.__godsEyeView.viewer;
|
||||
const v = window.__godsEyeView.viewer ?? null;
|
||||
const hasMil = dm.layers.has('military');
|
||||
window.__SPR.timeOffsetSec = -32;
|
||||
await dm.setEnabled('flights', true);
|
||||
await dm.setEnabled('military', true);
|
||||
if (hasMil) await dm.setEnabled('military', true);
|
||||
const fl = dm.layers.get('flights').module;
|
||||
const mil = dm.layers.get('military').module;
|
||||
const mil = hasMil ? dm.layers.get('military').module : null;
|
||||
for (const off of [-24, -16, -8, 0]) {
|
||||
window.__SPR.timeOffsetSec = off;
|
||||
await fl.update(v);
|
||||
await mil.update(v);
|
||||
await mil?.update(v);
|
||||
}
|
||||
window.__SPR.timeOffsetSec = 0;
|
||||
window.__SPR_DRIVER = setInterval(() => { fl.update(v); mil.update(v); }, 15000);
|
||||
return { fl: fl.getStats().count, mil: mil.getStats().count, hits: window.__SPR_HITS };
|
||||
window.__SPR_DRIVER = setInterval(() => { fl.update(v); mil?.update(v); }, 15000);
|
||||
return { fl: fl.getStats().count, mil: mil ? mil.getStats().count : null, hits: window.__SPR_HITS };
|
||||
});
|
||||
console.log(` flights count=${primed.fl} military count=${primed.mil} | shim hits opensky=${primed.hits.opensky} mil=${primed.hits.mil}`);
|
||||
const hasMilitary = primed.mil !== null;
|
||||
if (!hasMilitary) console.log(` [SKIP] the military rows — no military layer on ${ENGINE}`);
|
||||
record('B1 ingest: synthetic planes in both layers',
|
||||
primed.fl === SPRITES.flights.length && primed.mil === SPRITES.military.length,
|
||||
`flights=${primed.fl}/${SPRITES.flights.length} military=${primed.mil}/${SPRITES.military.length}`);
|
||||
if (!(primed.fl > 0 && primed.mil > 0)) { finish(); return; }
|
||||
primed.fl === SPRITES.flights.length && (!hasMilitary || primed.mil === SPRITES.military.length),
|
||||
`flights=${primed.fl}/${SPRITES.flights.length} military=${hasMilitary ? `${primed.mil}/${SPRITES.military.length}` : 'n/a'}`);
|
||||
if (!(primed.fl > 0 && (!hasMilitary || primed.mil > 0))) { finish(); return; }
|
||||
|
||||
// ========================================================================
|
||||
// B2 — billboard glyph + scale per class (machine check)
|
||||
@@ -384,7 +401,7 @@ async function main() {
|
||||
if (!bb || bb.image !== wantImg) glyphBad.push(`${id}(${klass})${bb ? ':wrong-image' : ':missing'}`);
|
||||
if (bb && Math.abs(bb.scale - wantScale) > 1e-9) scaleBad.push(`${id}: ${bb.scale} != ${wantScale}`);
|
||||
}
|
||||
for (const [id, klass] of expectedMilitary) {
|
||||
for (const [id, klass] of hasMilitary ? expectedMilitary : []) {
|
||||
const bb = bbById.get(id);
|
||||
const wantImg = aircraftIcon(klass);
|
||||
const wantScale = MIL_BILLBOARD_SCALE * (CLASS_SCALE_2D[klass] || 1);
|
||||
@@ -392,24 +409,25 @@ async function main() {
|
||||
if (bb && Math.abs(bb.scale - wantScale) > 1e-9) scaleBad.push(`${id}: ${bb.scale} != ${wantScale}`);
|
||||
}
|
||||
record('B2a: every synthetic billboard has its class glyph (data-URI match)',
|
||||
glyphBad.length === 0, glyphBad.length ? glyphBad.join(' ') : `${expectedFlights.size + expectedMilitary.size} billboards matched`);
|
||||
glyphBad.length === 0, glyphBad.length ? glyphBad.join(' ') : `${expectedFlights.size + (hasMilitary ? expectedMilitary.size : 0)} billboards matched`);
|
||||
record('B2b: every synthetic billboard has its class scale',
|
||||
scaleBad.length === 0, scaleBad.length ? scaleBad.join(' ') : 'all scales exact');
|
||||
const distinctGlyphs = new Set([...expectedFlights.values(), ...expectedMilitary.values()].map((k) => aircraftIcon(k)));
|
||||
record('B2c: all 8 classes exercised with 8 distinct glyphs',
|
||||
distinctGlyphs.size === 8, `${distinctGlyphs.size} distinct data URIs`);
|
||||
const distinctGlyphs = new Set([...expectedFlights.values(), ...(hasMilitary ? expectedMilitary.values() : [])].map((k) => aircraftIcon(k)));
|
||||
const wantGlyphs = hasMilitary ? 8 : new Set(expectedFlights.values()).size;
|
||||
record(`B2c: all ${wantGlyphs} classes exercised with ${wantGlyphs} distinct glyphs`,
|
||||
distinctGlyphs.size === wantGlyphs, `${distinctGlyphs.size} distinct data URIs`);
|
||||
|
||||
// ========================================================================
|
||||
// 2D screenshots
|
||||
// ========================================================================
|
||||
console.log('\n2D screenshots → qa-shots/b5/');
|
||||
const topDown = async (lat, lon, height) => {
|
||||
await page.evaluate(({ lat, lon, height }) => {
|
||||
const v = window.__godsEyeView.viewer;
|
||||
v.camera.cancelFlight(); // the boot fly-to-Austin otherwise stomps setView
|
||||
const C3 = v.camera.position.constructor;
|
||||
v.camera.setView({
|
||||
destination: C3.fromDegrees(lon, lat, height),
|
||||
await page.evaluate(async ({ lat, lon, height }) => {
|
||||
const camera = window.__godsEyeView.engine.camera;
|
||||
camera.cancelFlight(); // the boot fly-to-Austin otherwise stomps setView
|
||||
const geo = await import('/src/geo/index.js');
|
||||
camera.setView({
|
||||
destination: geo.cartesian3.fromDegrees(lon, lat, height),
|
||||
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
||||
});
|
||||
}, { lat, lon, height });
|
||||
@@ -422,6 +440,7 @@ async function main() {
|
||||
const zoomToRange = async (want) => {
|
||||
await page.evaluate((want) => {
|
||||
const v = window.__godsEyeView.viewer;
|
||||
if (!v) return; // the engine follow keeps its own range (screenshots only)
|
||||
const p = v.camera.position;
|
||||
const d = Math.hypot(p.x, p.y, p.z);
|
||||
if (d > 1e6) return; // not in a tracked reference frame — leave it alone
|
||||
@@ -441,10 +460,12 @@ async function main() {
|
||||
|
||||
// Row widths: military spans 7 steps (~8.1 km) → frame ~13.9 km at 12 km up;
|
||||
// flights spans 5 steps (~5.8 km, +caa004's eastward drift) → ~10.4 km at 9 km up.
|
||||
await topDown(await rowLat('military', 4, 15), ROW_LON0 + 3.5 * ROW_STEP_DEG, 17000);
|
||||
await page.screenshot({ path: path.join(SHOT_DIR, 'b5-2d-military-row.png') });
|
||||
console.log(' saved b5-2d-military-row.png (8 classes, amber)');
|
||||
console.log(` glyph positions: ${await page.evaluate(() => window.__planeCanvasCoords('military', 15, 'ft'))}`);
|
||||
if (hasMilitary) {
|
||||
await topDown(await rowLat('military', 4, 15), ROW_LON0 + 3.5 * ROW_STEP_DEG, 17000);
|
||||
await page.screenshot({ path: path.join(SHOT_DIR, 'b5-2d-military-row.png') });
|
||||
console.log(' saved b5-2d-military-row.png (8 classes, amber)');
|
||||
console.log(` glyph positions: ${await page.evaluate(() => window.__planeCanvasCoords('military', 15, 'ft'))}`);
|
||||
}
|
||||
|
||||
await topDown(await rowLat('flights', 2, 30), ROW_LON0 + 2.5 * ROW_STEP_DEG, 12000);
|
||||
await page.screenshot({ path: path.join(SHOT_DIR, 'b5-2d-flights-row.png') });
|
||||
@@ -507,6 +528,9 @@ async function main() {
|
||||
// ========================================================================
|
||||
// B3 + 3D screenshots — military cluster
|
||||
// ========================================================================
|
||||
if (!hasMilitary) {
|
||||
console.log(` [SKIP] 3D military — no military layer on ${ENGINE}`);
|
||||
} else {
|
||||
console.log('\n3D — military cluster (jet.glb, per-class scale)');
|
||||
await page.evaluate(() => {
|
||||
const dm = window.__godsEyeView.dataManager;
|
||||
@@ -551,6 +575,7 @@ async function main() {
|
||||
await page.screenshot({ path: path.join(SHOT_DIR, 'b5-3d-military-close.png') });
|
||||
console.log(' saved b5-3d-military-close.png (450 m range: C17 left, F16 center, B744 right)');
|
||||
}
|
||||
}
|
||||
|
||||
record('B4: no console errors during QA run', consoleErrors.length === 0,
|
||||
consoleErrors.length
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
// plain { x, y, z }, screen a window point or null)
|
||||
// screenOf(id, objectId) the window point of one object, or null
|
||||
// selected(id) the layer's tracked/selected info, or null
|
||||
// drawn(id) what the layer draws per object (glyph, model),
|
||||
// as plain data, or null (layers with qaDrawn)
|
||||
// following() { label, native } of engine.camera.following()
|
||||
// movers() engine.movers.list()
|
||||
// framesDrawn() frames drawn since the probe started
|
||||
@@ -67,6 +69,7 @@ export function createQaProbe({ engine, dataManager, availability }) {
|
||||
return row ? screenOf(row.position) : null;
|
||||
},
|
||||
selected: (layerId) => moduleOf(layerId)?.getTrackedInfo?.() ?? null,
|
||||
drawn: (layerId) => moduleOf(layerId)?.qaDrawn?.() ?? null,
|
||||
following() {
|
||||
const handle = engine.camera.following();
|
||||
if (!handle) return null;
|
||||
|
||||
@@ -809,6 +809,52 @@ export function createQueries({
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* What this layer draws, as plain data (QA: the engine-neutral probe,
|
||||
* window.__godsEyeView.layers.drawn): per contact its glyph (image,
|
||||
* scale, shown, screen rotation, alpha, position) and its 3D model, read
|
||||
* from the renderer's handles — the same on every engine.
|
||||
* @returns {Array<object>}
|
||||
*/
|
||||
qaDrawn() {
|
||||
const out = [];
|
||||
for (const [id, sprite] of flightState._billboards) {
|
||||
const model = flightState._models.get(id);
|
||||
out.push({
|
||||
id,
|
||||
tracked: id === flightState._trackedIcao,
|
||||
image: sprite.image,
|
||||
scale: sprite.scale,
|
||||
width: sprite.width,
|
||||
show: sprite.show,
|
||||
rotation: sprite.rotation,
|
||||
alpha: sprite.color?.alpha ?? null,
|
||||
position: sprite.position
|
||||
? {
|
||||
x: sprite.position.x,
|
||||
y: sprite.position.y,
|
||||
z: sprite.position.z,
|
||||
}
|
||||
: null,
|
||||
model: model
|
||||
? { scale: model.scale, show: model.show, ready: model.ready }
|
||||
: null,
|
||||
});
|
||||
}
|
||||
const tracked = flightState._trackedModel;
|
||||
if (flightState._trackedIcao && tracked)
|
||||
out.push({
|
||||
id: flightState._trackedIcao,
|
||||
trackedModel: true,
|
||||
model: {
|
||||
scale: tracked.scale,
|
||||
show: tracked.show,
|
||||
ready: tracked.ready,
|
||||
},
|
||||
});
|
||||
return out;
|
||||
},
|
||||
|
||||
/**
|
||||
* Stop tracking the currently followed aircraft (no-op if none).
|
||||
* @returns {boolean} Always true.
|
||||
|
||||
@@ -291,6 +291,10 @@ test('the flights logic draws and follows on the Navara renderer (fake engine)',
|
||||
'abc123',
|
||||
'def456',
|
||||
]);
|
||||
// What it draws, as the QA probe reads it (the same on every engine).
|
||||
const drawn = layer.qaDrawn();
|
||||
assert.deepEqual(drawn.map((row) => row.id).sort(), ['abc123', 'def456']);
|
||||
assert.ok(drawn.every((row) => row.show && row.image.startsWith('data:')));
|
||||
// A click on a contact selects it; the engine follows with its label.
|
||||
engine.fake.script('pick', () => ({ id: 'def456' }));
|
||||
engine.fake.script('drillPick', () => [{ id: 'def456' }]);
|
||||
|
||||
Reference in new issue
Block a user