mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
Run the layer acceptance under a default-deny network policy with per-layer assertion manifests and strict switch contracts
Every scenario runs through scripts/qa-strict-harness.mjs: each page, frame and worker gets an ordered block list before it runs (paid hosts and routes first, then only the preview, declared fixtures, keyless probes and cached free tile hosts), switches with real input on the picker, and records its assertions per layer and engine. The runner reads the catalog from the live page, fails any layer without an assertion on either engine, can build and serve a keyless checkout itself, and adds rapid, hidden-tab, context-loss, failure-injection, cold-chunk and soak scenarios. The main chunk gets a 1% size budget.
This commit is contained in:
1 parent
3144169057
commit
1773494116
36 files changed
+6003
-1774
No files matched your search
@@ -1,27 +1,23 @@
|
||||
import { fromDegrees } from '../src/geo/cartesian3.js';
|
||||
import puppeteer from 'puppeteer';
|
||||
import sharp from 'sharp';
|
||||
import assert from 'node:assert/strict';
|
||||
import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
||||
import { createStrict, localPreview } from './qa-strict-harness.mjs';
|
||||
|
||||
const base = new URL(process.argv[2]);
|
||||
if (
|
||||
!['localhost', '127.0.0.1'].includes(base.hostname) ||
|
||||
['', '4173', '4185', '4186', '4187', '4188', '4189', '4194'].includes(
|
||||
base.port,
|
||||
)
|
||||
)
|
||||
throw new Error('Supply an unreserved local keyless preview URL');
|
||||
const base = localPreview(process.argv[2]);
|
||||
base.search = '?engine=navara&welcome=0&look=cinematic';
|
||||
base.hash =
|
||||
'v=2&lat=43.6500&lon=-79.3800&alt=46000&heading=0&pitch=-90&roll=0&map=sentinel2&l=4';
|
||||
const b = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--use-angle=metal', '--enable-gpu'],
|
||||
const strict = createStrict({
|
||||
origin: base.origin,
|
||||
layers: [
|
||||
'ais-live-vessels',
|
||||
'air-quality',
|
||||
'gnss-integrity',
|
||||
'fire-perimeters',
|
||||
],
|
||||
});
|
||||
try {
|
||||
await strict.run({ args: ['--use-angle=metal', '--enable-gpu'] }, async (b) => {
|
||||
const p = await b.newPage();
|
||||
await blockPaidQaMaps(p);
|
||||
await p.setViewport({ width: 1024, height: 768 });
|
||||
const errors = [];
|
||||
p.on('pageerror', (e) => {
|
||||
@@ -34,30 +30,66 @@ try {
|
||||
const url = new URL(input.url ?? input, document.baseURI);
|
||||
if (url.pathname === '/api/vessels/track') {
|
||||
window.qaVesselHistories = (window.qaVesselHistories ?? 0) + 1;
|
||||
return Promise.resolve(Response.json({ samples: [{lat: 43.63, lon: -79.38}] }));
|
||||
return Promise.resolve(
|
||||
Response.json({ samples: [{ lat: 43.63, lon: -79.38 }] }),
|
||||
);
|
||||
}
|
||||
if (url.pathname === '/api/vessels') {
|
||||
return Promise.resolve(Response.json({status: 'live', rows: [{
|
||||
mmsi: '123456789', name: 'Toronto QA', lat: 43.65, lon: -79.38,
|
||||
last_position_epoch: Date.now() / 1000,
|
||||
}]}));
|
||||
return Promise.resolve(
|
||||
Response.json({
|
||||
status: 'live',
|
||||
rows: [
|
||||
{
|
||||
mmsi: '123456789',
|
||||
name: 'Toronto QA',
|
||||
lat: 43.65,
|
||||
lon: -79.38,
|
||||
last_position_epoch: Date.now() / 1000,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (url.pathname.startsWith('/api/fire-perimeters/inciweb')) {
|
||||
return Promise.resolve(Response.json({ rows: [] }));
|
||||
}
|
||||
if (url.pathname === '/api/fire-perimeters') {
|
||||
const square = (dx) => [
|
||||
[-79.38-dx,43.65-dx],[-79.38+dx,43.65-dx],[-79.38+dx,43.65+dx],
|
||||
[-79.38-dx,43.65+dx],[-79.38-dx,43.65-dx],
|
||||
[-79.38 - dx, 43.65 - dx],
|
||||
[-79.38 + dx, 43.65 - dx],
|
||||
[-79.38 + dx, 43.65 + dx],
|
||||
[-79.38 - dx, 43.65 + dx],
|
||||
[-79.38 - dx, 43.65 - dx],
|
||||
];
|
||||
return Promise.resolve(Response.json({rows: [{
|
||||
stableId: 'fire:fixture', name:'QA Fire', acres:500, containedPct:40,
|
||||
polygons:[[square(0.09), square(0.018).reverse()]],
|
||||
}]}));
|
||||
return Promise.resolve(
|
||||
Response.json({
|
||||
rows: [
|
||||
{
|
||||
stableId: 'fire:fixture',
|
||||
name: 'QA Fire',
|
||||
acres: 500,
|
||||
containedPct: 40,
|
||||
polygons: [[square(0.09), square(0.018).reverse()]],
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (url.pathname === '/api/gnss-integrity') {
|
||||
return Promise.resolve(Response.json({ageMs: 0, rows: ['a1','a2','a3'].map(hex =>
|
||||
({hex, lat: 43.65, lon: -79.38, nic: 0, nacp: 0, degraded: true, gpsLost: false}))}));
|
||||
return Promise.resolve(
|
||||
Response.json({
|
||||
ageMs: 0,
|
||||
rows: ['a1', 'a2', 'a3'].map((hex) => ({
|
||||
hex,
|
||||
lat: 43.65,
|
||||
lon: -79.38,
|
||||
nic: 0,
|
||||
nacp: 0,
|
||||
degraded: true,
|
||||
gpsLost: false,
|
||||
})),
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (url.hostname === 'api.weather.gc.ca') {
|
||||
const records = url.pathname.includes('aqhi-stations')
|
||||
@@ -93,7 +125,7 @@ try {
|
||||
const ready = async (kind) => {
|
||||
await p.waitForFunction(
|
||||
(k) =>
|
||||
window.__godsEyeView?.engine.kind === k &&
|
||||
window.__godsEyeView?.engine?.kind === k &&
|
||||
!document.querySelector('#globe-engine')?.disabled &&
|
||||
!document.querySelector('.engine-switch-frame') &&
|
||||
document.querySelector('#loading-screen.hidden'),
|
||||
@@ -105,12 +137,78 @@ try {
|
||||
);
|
||||
};
|
||||
const capture = async (visible, layerId = 'air-quality') => {
|
||||
await p.evaluate(async (show, id) => {
|
||||
const d = window.__godsEyeView;
|
||||
await d.dataManager.setEnabled(id, show, { origin: 'user' });
|
||||
d.engine.render.request('qa-air-quality');
|
||||
}, visible, layerId);
|
||||
await new Promise((r) => setTimeout(r, 3900));
|
||||
await p.evaluate(
|
||||
async (show, id) => {
|
||||
const d = window.__godsEyeView;
|
||||
await d.dataManager.setEnabled(id, show, { origin: 'user' });
|
||||
d.engine.render.request('qa-air-quality');
|
||||
},
|
||||
visible,
|
||||
layerId,
|
||||
);
|
||||
// Ready to compare: the layer's data is in (when shown), the globe's
|
||||
// tiles under the view have loaded and stayed loaded, and a frame has
|
||||
// been drawn since.
|
||||
await p
|
||||
.waitForFunction(
|
||||
(show, id) => {
|
||||
const d = window.__godsEyeView;
|
||||
if (!d?.engine) return false;
|
||||
const module = d.dataManager.layers.get(id).module;
|
||||
const data =
|
||||
!show ||
|
||||
(id === 'air-quality'
|
||||
? module.getAnalystRecords().length === 1
|
||||
: module.getStats().levels?.high === 1);
|
||||
const now = performance.now();
|
||||
// The engines draw on request: keep frames coming so tiles load.
|
||||
d.engine.render.request('qa-ground-settle');
|
||||
if (!data || !d.engine.surface.tilesLoaded()) {
|
||||
window.__qaSettledSince = null;
|
||||
return false;
|
||||
}
|
||||
window.__qaSettledSince ??= now;
|
||||
return now - window.__qaSettledSince > 1200;
|
||||
},
|
||||
{ polling: 100, timeout: 30000 },
|
||||
visible,
|
||||
layerId,
|
||||
)
|
||||
.catch(async (error) => {
|
||||
console.log(
|
||||
'not settled',
|
||||
JSON.stringify(
|
||||
await p.evaluate((id) => {
|
||||
const d = window.__godsEyeView;
|
||||
const module = d?.dataManager.layers.get(id).module;
|
||||
return {
|
||||
engine: d?.engine?.kind,
|
||||
tiles: d?.engine?.surface.tilesLoaded(),
|
||||
records: module?.getAnalystRecords().length,
|
||||
stats: module?.getStats(),
|
||||
};
|
||||
}, layerId),
|
||||
),
|
||||
);
|
||||
throw error;
|
||||
});
|
||||
// A frame drawn after the settle (the engine draws on request), then
|
||||
// the animation frame that presents it.
|
||||
await p.evaluate(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
const { engine } = window.__godsEyeView;
|
||||
const remove = engine.frames.postRender.addEventListener(() => {
|
||||
remove();
|
||||
requestAnimationFrame(() => resolve());
|
||||
});
|
||||
engine.render.request('qa-ground');
|
||||
}),
|
||||
);
|
||||
await p.evaluate(() => (window.__qaSettledSince = null));
|
||||
return sample();
|
||||
};
|
||||
const sample = async () => {
|
||||
const shot = await p.screenshot();
|
||||
return sharp(shot)
|
||||
.extract({ left: 537, top: 403, width: 22, height: 18 })
|
||||
@@ -119,6 +217,19 @@ try {
|
||||
.toBuffer()
|
||||
.then((buf) => sharp(buf).stats());
|
||||
};
|
||||
// The layer shown, sampled until its polygon appears in the patch (the
|
||||
// engines draw a clamped polygon a few frames after its data, with no
|
||||
// signal for it), for at most 20 s; the comparison stays the same.
|
||||
const orangeOf = (v) => v.channels[0].mean - v.channels[2].mean;
|
||||
const drawn = async (off, layerId) => {
|
||||
const deadline = Date.now() + 20000;
|
||||
let on = await capture(true, layerId);
|
||||
while (orangeOf(on) <= orangeOf(off) + 5 && Date.now() < deadline) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
on = await sample();
|
||||
}
|
||||
return on;
|
||||
};
|
||||
await ready('navara');
|
||||
await p.evaluate(() => {
|
||||
const d = window.__godsEyeView;
|
||||
@@ -129,19 +240,27 @@ try {
|
||||
});
|
||||
for (const kind of ['navara', 'cesium', 'navara']) {
|
||||
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
|
||||
await p.select('#globe-engine', kind);
|
||||
await strict.pick(p, kind);
|
||||
await ready(kind);
|
||||
const same = await p.evaluate(
|
||||
() =>
|
||||
window.__godsEyeView.dataManager.layers.get('air-quality').module ===
|
||||
window.qaAir &&
|
||||
window.__godsEyeView.dataManager.layers.get('gnss-integrity').module === window.qaGnss,
|
||||
window.qaAir &&
|
||||
window.__godsEyeView.dataManager.layers.get('gnss-integrity')
|
||||
.module === window.qaGnss,
|
||||
);
|
||||
await strict.check(
|
||||
p,
|
||||
['air-quality', 'gnss-integrity'],
|
||||
'same layer models after the switch',
|
||||
same,
|
||||
);
|
||||
assert.ok(same);
|
||||
}
|
||||
await p.evaluate(() => window.__godsEyeView.dataManager.setEnabled('gnss-integrity', false));
|
||||
await p.evaluate(() =>
|
||||
window.__godsEyeView.dataManager.setEnabled('gnss-integrity', false),
|
||||
);
|
||||
const off = await capture(false),
|
||||
on = await capture(true);
|
||||
on = await drawn(off, 'air-quality');
|
||||
const values = await p.evaluate(() => ({
|
||||
records: window.qaAir.getAnalystRecords(),
|
||||
card: window.qaAir.getRowControls().list.items,
|
||||
@@ -162,54 +281,128 @@ try {
|
||||
assert.equal(values.records[0].name, 'QA Toronto');
|
||||
assert.equal(values.records[0].value, 9);
|
||||
assert.equal(values.card.length, 1);
|
||||
await strict.passed(
|
||||
p,
|
||||
'air-quality',
|
||||
'AQHI 9 station footprint drawn, one card row',
|
||||
{
|
||||
off: orange(off),
|
||||
on: orange(on),
|
||||
},
|
||||
);
|
||||
await capture(false);
|
||||
const cellOff = await capture(false, 'gnss-integrity');
|
||||
const cellOn = await capture(true, 'gnss-integrity');
|
||||
assert.ok(orange(cellOn) > orange(cellOff) + 5, kind + ' must draw GNSS high cell');
|
||||
const cellOn = await drawn(cellOff, 'gnss-integrity');
|
||||
assert.ok(
|
||||
orange(cellOn) > orange(cellOff) + 5,
|
||||
kind + ' must draw GNSS high cell',
|
||||
);
|
||||
const gnss = await p.evaluate(() => window.qaGnss.getStats());
|
||||
assert.equal(gnss.levels.high, 1);
|
||||
console.log('PASS', kind, 'GNSS high cell, view anchor', gnss.coverage.latitude, gnss.coverage.longitude);
|
||||
await p.evaluate(() => window.__godsEyeView.dataManager.setEnabled('air-quality', true));
|
||||
await strict.passed(p, 'gnss-integrity', 'one high GNSS cell drawn', {
|
||||
off: orange(cellOff),
|
||||
on: orange(cellOn),
|
||||
});
|
||||
console.log(
|
||||
'PASS',
|
||||
kind,
|
||||
'GNSS high cell, view anchor',
|
||||
gnss.coverage.latitude,
|
||||
gnss.coverage.longitude,
|
||||
);
|
||||
await p.evaluate(() =>
|
||||
window.__godsEyeView.dataManager.setEnabled('air-quality', true),
|
||||
);
|
||||
}
|
||||
await p.evaluate(async () => {
|
||||
const {dataManager} = window.__godsEyeView;
|
||||
const { dataManager } = window.__godsEyeView;
|
||||
await dataManager.setEnabled('air-quality', false);
|
||||
await dataManager.setEnabled('fire-perimeters', true, {origin: 'user'});
|
||||
await dataManager.setEnabled('fire-perimeters', true, { origin: 'user' });
|
||||
window.qaFire = dataManager.layers.get('fire-perimeters').module;
|
||||
if (!dataManager.isEnabled('fire-perimeters') || window.qaFire.getStats().count !== 1)
|
||||
throw Error('Failed to enable fire perimeter fixture: '+JSON.stringify(window.qaFire.getStats()));
|
||||
if (
|
||||
!dataManager.isEnabled('fire-perimeters') ||
|
||||
window.qaFire.getStats().count !== 1
|
||||
)
|
||||
throw Error(
|
||||
'Failed to enable fire perimeter fixture: ' +
|
||||
JSON.stringify(window.qaFire.getStats()),
|
||||
);
|
||||
});
|
||||
const card = async (visible) => p.waitForFunction((want) => {
|
||||
const canvas = document.getElementById('world-overlay-canvas');
|
||||
const data = canvas.getContext('2d').getImageData(0,0,canvas.width,canvas.height).data;
|
||||
let pixels = 0;
|
||||
for (let i=3; i<data.length; i+=4) if (data[i]) pixels++;
|
||||
return want ? pixels > 100 : pixels === 0;
|
||||
}, {timeout: 15000}, visible);
|
||||
const card = async (visible) =>
|
||||
p.waitForFunction(
|
||||
(want) => {
|
||||
const canvas = document.getElementById('world-overlay-canvas');
|
||||
const data = canvas
|
||||
.getContext('2d')
|
||||
.getImageData(0, 0, canvas.width, canvas.height).data;
|
||||
let pixels = 0;
|
||||
for (let i = 3; i < data.length; i += 4) if (data[i]) pixels++;
|
||||
return want ? pixels > 100 : pixels === 0;
|
||||
},
|
||||
{ timeout: 15000 },
|
||||
visible,
|
||||
);
|
||||
const click = async (lon, lat) => {
|
||||
const {x, y} = await p.evaluate((world) => {
|
||||
const {engine} = window.__godsEyeView;
|
||||
const point = engine.projector.worldToWindow(world, {});
|
||||
const box = engine.host.canvas.getBoundingClientRect();
|
||||
return { x: box.left+point.x, y: box.top+point.y };
|
||||
}, fromDegrees(lon, lat));
|
||||
await p.mouse.click(x,y);
|
||||
const { x, y } = await p.evaluate(
|
||||
(world) => {
|
||||
const { engine } = window.__godsEyeView;
|
||||
const point = engine.projector.worldToWindow(world, {});
|
||||
if (!point) throw Error('the point is not in view');
|
||||
const box = engine.host.canvas.getBoundingClientRect();
|
||||
return { x: box.left + point.x, y: box.top + point.y };
|
||||
},
|
||||
fromDegrees(lon, lat),
|
||||
);
|
||||
await p.mouse.click(x, y);
|
||||
};
|
||||
for (const kind of ['navara', 'cesium', 'navara']) {
|
||||
if (await p.evaluate(() => window.__godsEyeView.engine.kind) !== kind) {
|
||||
await p.select('#globe-engine', kind);
|
||||
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
|
||||
await strict.pick(p, kind);
|
||||
await ready(kind);
|
||||
await card(true);
|
||||
assert.ok(await p.evaluate(() => window.qaFire === window.__godsEyeView.dataManager.layers.get('fire-perimeters').module));
|
||||
await strict.check(
|
||||
p,
|
||||
'fire-perimeters',
|
||||
'same model; the incident card survives the switch',
|
||||
await p.evaluate(
|
||||
() =>
|
||||
window.qaFire ===
|
||||
window.__godsEyeView.dataManager.layers.get('fire-perimeters')
|
||||
.module,
|
||||
),
|
||||
);
|
||||
}
|
||||
await click(-79.51, 43.65);
|
||||
await card(false);
|
||||
await click(-79.38, 43.65);
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
await card(false);
|
||||
await strict.passed(
|
||||
p,
|
||||
'fire-perimeters',
|
||||
'a click outside the perimeter clears the card',
|
||||
);
|
||||
await click(-79.435, 43.65);
|
||||
await card(true);
|
||||
console.log('PASS', kind, 'select fire under GNSS, exclude hole, retain incident card');
|
||||
await strict.check(
|
||||
p,
|
||||
'fire-perimeters',
|
||||
'a click on the ring selects the fire',
|
||||
await p.evaluate(() => window.qaFire.getSelectedInfo?.()?.id ?? true),
|
||||
);
|
||||
// Not vacuous: the fire is selected, so the card only goes when the
|
||||
// click in the hole is handled as a click outside the polygon.
|
||||
await click(-79.38, 43.65);
|
||||
await card(false);
|
||||
await strict.passed(
|
||||
p,
|
||||
'fire-perimeters',
|
||||
'a click in the hole deselects the selected fire',
|
||||
);
|
||||
await click(-79.435, 43.65);
|
||||
await card(true);
|
||||
console.log(
|
||||
'PASS',
|
||||
kind,
|
||||
'select fire under GNSS, exclude hole, retain incident card',
|
||||
);
|
||||
}
|
||||
await p.evaluate(async () => {
|
||||
const { dataManager } = window.__godsEyeView;
|
||||
@@ -218,17 +411,79 @@ try {
|
||||
await dataManager.setEnabled('ais-live-vessels', true, { origin: 'user' });
|
||||
window.qaVesselEntry = dataManager.layers.get('ais-live-vessels');
|
||||
});
|
||||
await p.waitForFunction(() => window.qaVesselEntry.module.qaDrawn()?.chevrons[0]?.show);
|
||||
await p.waitForFunction(
|
||||
() => window.qaVesselEntry.module.qaDrawn()?.chevrons[0]?.show,
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||
// Click-picking on the running engine: clear the selection with a click on
|
||||
// empty water, then select the vessel with a click on its chevron.
|
||||
const pickVessel = async () => {
|
||||
// Empty ground: a point in the middle of the view where the engine
|
||||
// picks nothing and nothing covers the canvas.
|
||||
const empty = await p
|
||||
.waitForFunction(
|
||||
() => {
|
||||
const { engine } = window.__godsEyeView;
|
||||
const canvas = engine.host.canvas;
|
||||
const box = canvas.getBoundingClientRect();
|
||||
for (let y = 220; y < 560; y += 11)
|
||||
for (let x = 300; x < 720; x += 11) {
|
||||
if (
|
||||
!canvas.contains(
|
||||
document.elementFromPoint(box.left + x, box.top + y),
|
||||
)
|
||||
)
|
||||
continue;
|
||||
let free = true;
|
||||
for (const [dx, dy] of [
|
||||
[0, 0],
|
||||
[10, 0],
|
||||
[-10, 0],
|
||||
[0, 10],
|
||||
[0, -10],
|
||||
])
|
||||
if (engine.picker.pick({ x: x + dx, y: y + dy })) free = false;
|
||||
if (free) return { x: box.left + x, y: box.top + y };
|
||||
}
|
||||
return false;
|
||||
},
|
||||
{ timeout: 15000 },
|
||||
)
|
||||
.then((handle) => handle.jsonValue());
|
||||
await p.mouse.click(empty.x, empty.y);
|
||||
await p.waitForFunction(
|
||||
() => !window.qaVesselEntry.module.getSelectedInfo(),
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 600));
|
||||
await click(-79.38, 43.65);
|
||||
await p.waitForFunction(
|
||||
() => window.qaVesselEntry.module.getSelectedInfo()?.mmsi === '123456789',
|
||||
{ timeout: 15000 },
|
||||
);
|
||||
await strict.passed(
|
||||
p,
|
||||
'ais-live-vessels',
|
||||
'a click on the chevron selects the vessel',
|
||||
);
|
||||
};
|
||||
await click(-79.38, 43.65);
|
||||
await p.waitForFunction(() => window.qaVesselEntry.module.getSelectedInfo()?.mmsi === '123456789');
|
||||
await p.waitForFunction(
|
||||
() => window.qaVesselEntry.module.getSelectedInfo()?.mmsi === '123456789',
|
||||
);
|
||||
await strict.passed(
|
||||
p,
|
||||
'ais-live-vessels',
|
||||
'a click on the chevron selects the vessel',
|
||||
);
|
||||
let histories = await p.evaluate(() => window.qaVesselHistories);
|
||||
for (const kind of ['navara', 'cesium', 'navara']) {
|
||||
if (await p.evaluate(() => window.__godsEyeView.engine.kind) !== kind) {
|
||||
await p.select('#globe-engine', kind);
|
||||
if ((await p.evaluate(() => window.__godsEyeView.engine.kind)) !== kind) {
|
||||
await strict.pick(p, kind);
|
||||
await ready(kind);
|
||||
}
|
||||
const sample = await p.evaluate(() => {
|
||||
const entry = window.__godsEyeView.dataManager.layers.get('ais-live-vessels');
|
||||
const entry =
|
||||
window.__godsEyeView.dataManager.layers.get('ais-live-vessels');
|
||||
const drawn = entry.module.qaDrawn();
|
||||
return {
|
||||
retained: entry === window.qaVesselEntry,
|
||||
@@ -238,13 +493,31 @@ try {
|
||||
histories: window.qaVesselHistories,
|
||||
};
|
||||
});
|
||||
assert.deepEqual(sample, {
|
||||
retained: true, selected: '123456789', chevron: '123456789',
|
||||
card: 'vessel:123456789', histories: 1,
|
||||
}, kind);
|
||||
console.log('PASS', kind, 'retained selected AIS vessel, chevron, card and single track request');
|
||||
assert.deepEqual(
|
||||
sample,
|
||||
{
|
||||
retained: true,
|
||||
selected: '123456789',
|
||||
chevron: '123456789',
|
||||
card: 'vessel:123456789',
|
||||
histories,
|
||||
},
|
||||
kind,
|
||||
);
|
||||
await strict.passed(
|
||||
p,
|
||||
'ais-live-vessels',
|
||||
'same entry; selection, chevron and card kept; no new track request',
|
||||
);
|
||||
if (kind === 'cesium' || histories === 1) {
|
||||
await pickVessel();
|
||||
histories = await p.evaluate(() => window.qaVesselHistories);
|
||||
}
|
||||
console.log(
|
||||
'PASS',
|
||||
kind,
|
||||
'retained selected AIS vessel, chevron, card and single track request',
|
||||
);
|
||||
}
|
||||
assert.deepEqual(errors, []);
|
||||
} finally {
|
||||
await b.close();
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user