mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
Merge pull request #1281 from bilawalsidhu/codex/fix-traffic-render-hold
fix(traffic): hold rendering only for animated content
This commit is contained in:
14 files changed
+623
-105
No files matched your search
@@ -0,0 +1,8 @@
|
||||
- Hold rendering only for animated content (asturksever, #1281).
|
||||
- Exercise current vector road sources and published camera bounds in Traffic fixture QA (asturksever, #1281).
|
||||
- Establish browser-level foreground visibility for Traffic QA and reject hidden-page samples (asturksever, #1281).
|
||||
- Supply CORS headers on cross-origin traffic fixtures and report browser exceptions without disabling browser security (asturksever, #1281).
|
||||
- Isolate the Traffic keyless browser scenario from the keyed page’s workers and intercepted cache (asturksever, #1281).
|
||||
- Restore minimized test windows before screenshots so native compositor pauses cannot stall browser acceptance (asturksever, #1281).
|
||||
- Intercept only Traffic provider URLs, leaving Cesium worker and module loading untouched (asturksever, #1281).
|
||||
- Settle an unfinished Traffic height correction when the last animated point leaves, so the layer stops reporting loading once its render hold is released (asturksever, #1281).
|
||||
@@ -27,6 +27,8 @@ existing batch size and one bounded upstream chunk. Larger requests return HTTP
|
||||
413 before disk-cache loading or upstream work; malformed coordinates remain 400.
|
||||
The browser continues to split larger workloads into sequential 64-point requests.
|
||||
|
||||
Traffic holds continuous rendering only while enabled with animated dots or a pulsing jam heat-line. Empty and high-altitude views release the hold. A failed load leaves no hold only when no animated content is retained: if an earlier load succeeded, ingestion keeps its animated points and the hold stays. Population admission reacquires the hold and resets animation time. Clear, disable, and destroy also release it. Retained fading dots keep animation alive until they are removed. When the last point leaves mid height correction, the correction is settled at once, before the hold is released, so it cannot keep the layer reporting loading.
|
||||
|
||||
Shared distance checks use `src/geoDistance.js` with `{ lat, lon }` points and
|
||||
the IUGG mean spherical Earth radius, 6371.0088 km. `greatCircleKm` is
|
||||
canonical; `greatCircleMeters` is its exact metre conversion. Missing,
|
||||
|
||||
@@ -18,3 +18,30 @@ export function chromeExecutablePath({
|
||||
if (env.PUPPETEER_EXECUTABLE_PATH) return env.PUPPETEER_EXECUTABLE_PATH;
|
||||
return exists(MACOS_CHROME) ? MACOS_CHROME : undefined;
|
||||
}
|
||||
|
||||
/** Restore an owned test window before capture: focus emulation alone does not
|
||||
* unminimize the native compositor, so CDP screenshots can otherwise stall. */
|
||||
export async function captureQaScreenshot(
|
||||
page,
|
||||
options,
|
||||
{ headful = true } = {},
|
||||
) {
|
||||
if (headful) {
|
||||
const session = await page.createCDPSession();
|
||||
try {
|
||||
const { windowId, bounds } = await session.send(
|
||||
'Browser.getWindowForTarget',
|
||||
);
|
||||
if (bounds.windowState === 'minimized') {
|
||||
await session.send('Browser.setWindowBounds', {
|
||||
windowId,
|
||||
bounds: { windowState: 'normal' },
|
||||
});
|
||||
}
|
||||
await page.bringToFront();
|
||||
} finally {
|
||||
await session.detach();
|
||||
}
|
||||
}
|
||||
return page.screenshot(options);
|
||||
}
|
||||
+57
-55
@@ -25,12 +25,13 @@
|
||||
* Visual proof saved to qa-shots/ (gitignored).
|
||||
*
|
||||
* Run: node scripts/qa-traffic.mjs --url http://localhost:4410
|
||||
* Add --fixtures for synthetic roads and the recorded flow tile without a key;
|
||||
* that mode does not qualify live source access or quota accounting.
|
||||
* Add --fixtures for synthetic road/flow tiles and a flat terrain surface without a key;
|
||||
* that mode does not qualify geographic terrain, live source access or quota accounting.
|
||||
* Exits non-zero on any FAIL. Does not commit anything.
|
||||
*/
|
||||
|
||||
import puppeteer from 'puppeteer';
|
||||
import { captureQaScreenshot } from './qa-chrome.mjs';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -44,10 +45,12 @@ const getOpt = (name, dflt) => {
|
||||
const i = argv.indexOf(name);
|
||||
return i >= 0 && argv[i + 1] ? argv[i + 1] : dflt;
|
||||
};
|
||||
const APP_URL = getOpt('--url', 'http://localhost:4410');
|
||||
const appUrl = new URL(getOpt('--url', 'http://localhost:4410'));
|
||||
appUrl.searchParams.set('welcome', '0');
|
||||
const APP_URL = appUrl.href;
|
||||
const HEADFUL = argv.includes('--headful');
|
||||
const FIXTURES = argv.includes('--fixtures');
|
||||
const fixtureResponse = FIXTURES ? (await import('./traffic-fixtures.mjs')).trafficFixtureResponse : () => null;
|
||||
const { installTrafficRoutes } = await import('./traffic-fixtures.mjs');
|
||||
|
||||
const CHROME_EXECUTABLE_CANDIDATES = [
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH,
|
||||
@@ -80,6 +83,10 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
/** Enable traffic + teleport, then poll the layer until settled. */
|
||||
async function settleTraffic(page, view, { minCount = 1, timeoutS = 30 } = {}) {
|
||||
// The app deliberately stops rendering hidden pages. Programmatic camera
|
||||
// changes do not activate a headful window the way a real UI click does.
|
||||
await page.bringToFront();
|
||||
await page.waitForFunction(() => document.visibilityState === 'visible');
|
||||
return page.evaluate(async (v, minC, tS) => {
|
||||
const gev = window.__godsEyeView;
|
||||
const dm = gev.dataManager;
|
||||
@@ -97,6 +104,7 @@ async function settleTraffic(page, view, { minCount = 1, timeoutS = 30 } = {}) {
|
||||
});
|
||||
let s = null;
|
||||
for (let i = 0; i < tS; i++) {
|
||||
if (document.visibilityState !== 'visible') throw new Error('Traffic QA page became hidden; rendering is intentionally paused');
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
s = mod.getStats();
|
||||
// Retained roads from the previous city can satisfy count/loading before
|
||||
@@ -122,8 +130,8 @@ async function main() {
|
||||
return;
|
||||
}
|
||||
|
||||
const statusBefore = FIXTURES ? {hasKey:true,dailyCount:0} : await fetch(`${APP_URL}/api/tomtom/status`).then((r) => r.json()).catch(() => null);
|
||||
if (FIXTURES) console.log('Source mode: synthetic roads and recorded flow tiles; live quota is not qualified.');
|
||||
const statusBefore = FIXTURES ? {hasKey:true,dailyCount:0} : await fetch(`${appUrl.origin}/api/tomtom/status`).then((r) => r.json()).catch(() => null);
|
||||
if (FIXTURES) console.log('Source mode: synthetic road/flow tiles and a flat terrain surface; live quota is not qualified.');
|
||||
if (!statusBefore?.hasKey) {
|
||||
console.error('\x1b[31mServer has no TomTom key — run against the keyed dev server (:4410).\x1b[0m');
|
||||
process.exitCode = 2;
|
||||
@@ -135,8 +143,10 @@ async function main() {
|
||||
headless: HEADFUL ? false : 'new',
|
||||
...(findChromeExecutable() ? { executablePath: findChromeExecutable() } : {}),
|
||||
args: [
|
||||
'--no-sandbox', '--disable-setuid-sandbox', '--use-gl=angle', '--use-angle=swiftshader',
|
||||
'--disable-dev-shm-usage', '--disable-web-security',
|
||||
'--no-sandbox', '--disable-setuid-sandbox',
|
||||
...(!HEADFUL ? ['--use-gl=angle', '--use-angle=swiftshader'] : []),
|
||||
'--disable-backgrounding-occluded-windows',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-background-timer-throttling', '--disable-renderer-backgrounding',
|
||||
'--window-size=1440,900',
|
||||
],
|
||||
@@ -144,24 +154,22 @@ async function main() {
|
||||
|
||||
let exitCode = 0;
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
const onFixture = (request) => {
|
||||
const response = fixtureResponse(request);
|
||||
if (response) void request.respond(response); else void request.continue();
|
||||
};
|
||||
if (FIXTURES) { await page.setRequestInterception(true); page.on('request', onFixture); }
|
||||
let page = await browser.newPage();
|
||||
// Keep this visible-page regression deterministic even if the OS starts
|
||||
// Chrome minimized. Use browser focus emulation, not patched DOM getters.
|
||||
const focusSession = await page.createCDPSession();
|
||||
await focusSession.send('Emulation.setFocusEmulationEnabled', { enabled: true });
|
||||
await page.setViewport({ width: 1440, height: 900 });
|
||||
|
||||
// Track flow-tile requests + traffic console lines for (ii)/(iii)/(v).
|
||||
// Track flow-tile requests for the (iii) budget check.
|
||||
const flowRequests = [];
|
||||
page.on('request', (req) => {
|
||||
if (req.url().includes('/api/tomtom/flow/')) flowRequests.push(req.url());
|
||||
});
|
||||
const trafficLogs = [];
|
||||
page.on('console', (msg) => {
|
||||
const t = msg.text();
|
||||
if (t.includes('[Data:Traffic]')) trafficLogs.push(t);
|
||||
});
|
||||
const pageErrors = [];
|
||||
if (FIXTURES) await installTrafficRoutes(page, { origin: appUrl.origin, fixtures: true, onError: error => pageErrors.push(error.message) });
|
||||
page.on('pageerror', error => { pageErrors.push(error.message); console.error('PAGE ERROR:', error.message); });
|
||||
page.on('console', message => { if (message.type() === 'error') console.error('BROWSER ERROR:', message.text()); });
|
||||
|
||||
console.log('Loading app...');
|
||||
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||||
@@ -194,7 +202,7 @@ async function main() {
|
||||
`coverage=${mumbai.flowCoveragePct}% tiles=${mumbai.tilesFetched}`);
|
||||
if (mumbai.mode !== 'live' || !(mumbai.count > 0) || !(colored > 0)) exitCode = 1;
|
||||
await sleep(1200);
|
||||
await page.screenshot({ path: path.join(SHOTS_DIR, 'traffic-live-flow.png') });
|
||||
await captureQaScreenshot(page, { path: path.join(SHOTS_DIR, 'traffic-live-flow.png') }, { headful: HEADFUL });
|
||||
}
|
||||
|
||||
// ── (iv) uncovered-roads param — hide vs sim ─────────────────────────────
|
||||
@@ -227,7 +235,7 @@ async function main() {
|
||||
(hid.flowBuckets?.sim || 0) === 0 && hid.count > 0,
|
||||
`count=${hid.count} sim=${hid.flowBuckets?.sim}`);
|
||||
if ((hid.flowBuckets?.sim || 0) !== 0) exitCode = 1;
|
||||
await page.screenshot({ path: path.join(SHOTS_DIR, 'traffic-live-hide-mode.png') });
|
||||
await captureQaScreenshot(page, { path: path.join(SHOTS_DIR, 'traffic-live-hide-mode.png') }, { headful: HEADFUL });
|
||||
} else {
|
||||
record('PARAM: hide mode renders zero sim (white) dots', null,
|
||||
'view had 100% coverage (no sim dots to hide) — inconclusive here, covered by unit tests');
|
||||
@@ -235,30 +243,26 @@ async function main() {
|
||||
|
||||
// ── (ii) C4 oblique bounds ───────────────────────────────────────────────
|
||||
console.log('\n(ii) C4 — oblique fetch bounds land at the look-at point...');
|
||||
trafficLogs.length = 0;
|
||||
await settleTraffic(page, { lon: -97.72, lat: 30.245, height: 2500, heading: 315, pitch: -20 }, { minCount: 100 });
|
||||
const oblique = await settleTraffic(page, { lon: -97.72, lat: 30.245, height: 2500, heading: 315, pitch: -20 }, { minCount: 100 });
|
||||
{
|
||||
const fetchLine = trafficLogs.find((t) => t.includes('fetch') && t.includes('['));
|
||||
let ok = false; let detail = 'no fetch log captured';
|
||||
if (fetchLine) {
|
||||
const m = fetchLine.match(/\[(-?[\d.]+),(-?[\d.]+),(-?[\d.]+),(-?[\d.]+)\]/);
|
||||
if (m) {
|
||||
const [s, w, n, e] = m.slice(1).map(Number);
|
||||
const cLat = (s + n) / 2; const cLon = (w + e) / 2;
|
||||
const dKm = Math.sqrt(((cLat - 30.245) * 111) ** 2 + ((cLon - (-97.72)) * 111 * Math.cos(30.27 * Math.PI / 180)) ** 2);
|
||||
ok = dKm > 1 && dKm <= 13; // look-at ~6.9 km ahead; clamp allows ≤12 (+margin)
|
||||
detail = `box center (${cLat.toFixed(4)},${cLon.toFixed(4)}) is ${dKm.toFixed(1)} km from camera (want 1–13 km; pre-fix bug: 25+ km)`;
|
||||
}
|
||||
const bounds = oblique?.roadBounds;
|
||||
let ok = false; let detail = 'no road bounds published';
|
||||
if (bounds) {
|
||||
const cLat = (bounds.south + bounds.north) / 2;
|
||||
const cLon = (bounds.west + bounds.east) / 2;
|
||||
const dKm = Math.hypot((cLat - 30.245) * 111, (cLon + 97.72) * 111 * Math.cos(30.27 * Math.PI / 180));
|
||||
ok = dKm > 1 && dKm <= 13;
|
||||
detail = `road box center is ${dKm.toFixed(1)} km from camera (want 1–13 km)`;
|
||||
}
|
||||
record('C4: oblique fetch box centers on the look-at point', ok, detail);
|
||||
if (!ok) exitCode = 1;
|
||||
await page.screenshot({ path: path.join(SHOTS_DIR, 'traffic-c4-oblique.png') });
|
||||
await captureQaScreenshot(page, { path: path.join(SHOTS_DIR, 'traffic-c4-oblique.png') }, { headful: HEADFUL });
|
||||
}
|
||||
|
||||
// ── (iii) budget honesty ─────────────────────────────────────────────────
|
||||
console.log('\n(iii) Budget — dailyCount grew by ≤ requests this run...');
|
||||
{
|
||||
const statusAfter = FIXTURES ? statusBefore : await fetch(`${APP_URL}/api/tomtom/status`).then((r) => r.json());
|
||||
const statusAfter = FIXTURES ? statusBefore : await fetch(`${appUrl.origin}/api/tomtom/status`).then((r) => r.json());
|
||||
const grew = statusAfter.dailyCount - statusBefore.dailyCount;
|
||||
const ok = grew >= 0 && grew <= flowRequests.length;
|
||||
record('BUDGET: /api/tomtom/status growth ≤ page tile requests', FIXTURES ? null : ok,
|
||||
@@ -268,25 +272,22 @@ async function main() {
|
||||
|
||||
// ── (v) KEYLESS fallback (intercepted — server key untouched) ────────────
|
||||
console.log('\n(v) KEYLESS — intercepted status, expect pure simulation...');
|
||||
if (FIXTURES) page.off('request', onFixture);
|
||||
await page.setRequestInterception(true);
|
||||
// Keyless is an independent fresh-session contract. Close the keyed page
|
||||
// so its workers, terrain work and intercepted cache cannot leak into it.
|
||||
await page.close();
|
||||
page = await browser.newPage();
|
||||
await page.setViewport({ width: 1440, height: 900 });
|
||||
const keylessFocus = await page.createCDPSession();
|
||||
await keylessFocus.send('Emulation.setFocusEmulationEnabled', { enabled: true });
|
||||
page.on('pageerror', error => { pageErrors.push(error.message); console.error('PAGE ERROR:', error.message); });
|
||||
page.on('console', message => { if (message.type() === 'error') console.error('BROWSER ERROR:', message.text()); });
|
||||
const keylessFlowReqs = [];
|
||||
page.on('request', (req) => {
|
||||
const url = req.url();
|
||||
if (url.includes('/api/tomtom/status')) {
|
||||
req.respond({ status: 200, contentType: 'application/json', body: JSON.stringify({ hasKey: false }) });
|
||||
return;
|
||||
}
|
||||
if (url.includes('/api/tomtom/flow/')) {
|
||||
keylessFlowReqs.push(url);
|
||||
req.respond({ status: 503, contentType: 'application/json', body: JSON.stringify({ error: 'no_key' }) });
|
||||
return;
|
||||
}
|
||||
const response = fixtureResponse(req);
|
||||
if (response) { void req.respond(response); return; }
|
||||
try { req.continue(); } catch { /* already handled */ }
|
||||
await installTrafficRoutes(page, {
|
||||
origin: appUrl.origin, fixtures: FIXTURES, keyless: true,
|
||||
onFlow: url => keylessFlowReqs.push(url),
|
||||
onError: error => pageErrors.push(error.message),
|
||||
});
|
||||
await page.reload({ waitUntil: 'domcontentloaded', timeout: 60000 });
|
||||
await page.goto(APP_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||||
await page.waitForFunction(
|
||||
() => window.__godsEyeView?.viewer && window.__godsEyeView?.dataManager,
|
||||
{ timeout: 60000 },
|
||||
@@ -316,8 +317,9 @@ async function main() {
|
||||
exitCode = 1;
|
||||
}
|
||||
await sleep(1000);
|
||||
await page.screenshot({ path: path.join(SHOTS_DIR, 'traffic-sim-keyless.png') });
|
||||
await captureQaScreenshot(page, { path: path.join(SHOTS_DIR, 'traffic-sim-keyless.png') }, { headful: HEADFUL });
|
||||
}
|
||||
record('BROWSER: no uncaught page exceptions', pageErrors.length === 0, pageErrors.join('; '));
|
||||
} catch (e) {
|
||||
console.error('\x1b[31mHarness error:\x1b[0m', e);
|
||||
exitCode = 3;
|
||||
|
||||
+161
-42
@@ -1,56 +1,175 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { decodeFlowTile } from '../src/layers/traffic/flowDecode.js';
|
||||
import { tilesForBounds } from '../src/data/tomtomTiles.js';
|
||||
import { PbfWriter } from 'pbf';
|
||||
|
||||
const tile = readFileSync(
|
||||
new URL(
|
||||
'../src/data/fixtures/tomtom-flow-austin-12-935-1686.pbf',
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
const corsHeaders = { 'access-control-allow-origin': '*' };
|
||||
const json = (body) => ({
|
||||
status: 200,
|
||||
headers: corsHeaders,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
/** Reuse the recorded flow tile for an explicitly synthetic browser exercise. */
|
||||
// Cesium heightmap-1.0: 65x65 little-endian uint16 heights, scale .2,
|
||||
// offset -1000, followed by child availability and a uniform land mask.
|
||||
const flatTerrain = Buffer.alloc(65 * 65 * 2 + 2);
|
||||
for (let i = 0; i < 65 * 65; i++) flatTerrain.writeUInt16LE(5000, i * 2);
|
||||
|
||||
/** Synthetic, geographically aligned motor roads at both source zooms. */
|
||||
export function trafficFixtureTile(zoom, flow = false) {
|
||||
const writer = new PbfWriter();
|
||||
writer.writeMessage(
|
||||
3,
|
||||
(_, layer) => {
|
||||
layer.writeVarintField(15, 2);
|
||||
layer.writeStringField(1, flow ? 'Traffic flow' : 'transportation');
|
||||
layer.writeVarintField(5, 4096);
|
||||
layer.writeStringField(3, flow ? 'traffic_level' : 'class');
|
||||
layer.writeMessage(
|
||||
4,
|
||||
(_, value) => {
|
||||
if (flow) value.writeDoubleField(3, 0.6);
|
||||
else value.writeStringField(1, 'primary');
|
||||
},
|
||||
null,
|
||||
);
|
||||
if (flow) {
|
||||
layer.writeStringField(3, 'road_type');
|
||||
layer.writeMessage(
|
||||
4,
|
||||
(_, value) => value.writeStringField(1, 'Major road'),
|
||||
null,
|
||||
);
|
||||
}
|
||||
// z12 has four times the span of z14. Keep the same world spacing,
|
||||
// and cover only alternate roads with flow to exercise hide/sim controls.
|
||||
const step = 256 * 2 ** (zoom - 14);
|
||||
for (let y = step; y < 4096; y += step) {
|
||||
if (flow && (y / step) % 2) continue;
|
||||
layer.writeMessage(
|
||||
2,
|
||||
(_, feature) => {
|
||||
feature.writeVarintField(3, 2);
|
||||
feature.writePackedVarint(2, flow ? [0, 0, 1, 1] : [0, 0]);
|
||||
feature.writePackedVarint(4, [9, 0, y * 2, 10, 8192, 0]);
|
||||
},
|
||||
null,
|
||||
);
|
||||
}
|
||||
},
|
||||
null,
|
||||
);
|
||||
return Buffer.from(writer.finish());
|
||||
}
|
||||
|
||||
/** Intercept the current vector sources; never fall through to live roads. */
|
||||
export function trafficFixtureResponse(request) {
|
||||
const url = new URL(request.url());
|
||||
if (url.hostname === 'terrain.reearth.land') {
|
||||
if (url.pathname.endsWith('/layer.json'))
|
||||
return json({
|
||||
tilejson: '2.1.0',
|
||||
format: 'heightmap-1.0',
|
||||
version: 'qa-flat',
|
||||
scheme: 'tms',
|
||||
projection: 'EPSG:4326',
|
||||
minzoom: 0,
|
||||
maxzoom: 0,
|
||||
tiles: ['{z}/{x}/{y}.terrain?v={version}'],
|
||||
available: [[{ startX: 0, startY: 0, endX: 1, endY: 0 }]],
|
||||
attribution:
|
||||
'Synthetic flat terrain for Traffic QA; not live elevation data',
|
||||
});
|
||||
if (/\/0\/[01]\/0\.terrain$/.test(url.pathname))
|
||||
return {
|
||||
status: 200,
|
||||
contentType: 'application/octet-stream',
|
||||
headers: corsHeaders,
|
||||
body: flatTerrain,
|
||||
};
|
||||
throw new Error(`Unexpected terrain fixture request: ${url.pathname}`);
|
||||
}
|
||||
if (url.pathname === '/api/tomtom/status')
|
||||
return json({ hasKey: true, dailyCount: 0 });
|
||||
if (/^\/api\/tomtom\/flow\/\d+\/\d+\/\d+\.pbf$/.test(url.pathname))
|
||||
return { status: 200, contentType: 'application/x-protobuf', body: tile };
|
||||
if (url.pathname !== '/api/overpass') return null;
|
||||
const query = new URLSearchParams(request.postData()).get('data') || '';
|
||||
if (!query.includes('highway')) return null;
|
||||
const match = query.match(/\((-?[\d.]+),(-?[\d.]+),(-?[\d.]+),(-?[\d.]+)\)/);
|
||||
if (!match) throw new Error('Missing road fixture viewport');
|
||||
const [south, west, north, east] = match.slice(1).map(Number);
|
||||
const inside = ([lon, lat]) =>
|
||||
lat >= south && lat <= north && lon >= west && lon <= east;
|
||||
const segments = tilesForBounds({ south, west, north, east }, 12).flatMap(
|
||||
({ z, x, y }) => decodeFlowTile(tile, z, x, y),
|
||||
const flow = url.pathname.match(
|
||||
/^\/api\/tomtom\/flow\/(\d+)\/\d+\/\d+\.pbf$/,
|
||||
);
|
||||
const elements = segments
|
||||
.filter((segment) => segment.coords.some(inside))
|
||||
.slice(0, 240)
|
||||
.map((segment, index) => ({
|
||||
type: 'way',
|
||||
id: index + 1,
|
||||
tags: { highway: 'primary', oneway: 'yes' },
|
||||
geometry: segment.coords.map(([lon, lat]) => ({ lon, lat })),
|
||||
}));
|
||||
const latitude = (south + north) / 2,
|
||||
longitude = (west + east) / 2;
|
||||
elements.push({
|
||||
type: 'way',
|
||||
id: 99999,
|
||||
tags: { highway: 'residential' },
|
||||
geometry: [
|
||||
{ lat: latitude, lon: longitude },
|
||||
{ lat: latitude + 0.003, lon: longitude + 0.003 },
|
||||
],
|
||||
if (flow)
|
||||
return {
|
||||
status: 200,
|
||||
contentType: 'application/x-protobuf',
|
||||
headers: corsHeaders,
|
||||
body: trafficFixtureTile(Number(flow[1]), true),
|
||||
};
|
||||
if (url.hostname !== 'tiles.openfreemap.org') return null;
|
||||
if (url.pathname === '/planet')
|
||||
return json({
|
||||
tilejson: '3.0.0',
|
||||
minzoom: 0,
|
||||
maxzoom: 14,
|
||||
tiles: ['https://tiles.openfreemap.org/qa-traffic/{z}/{x}/{y}.pbf'],
|
||||
});
|
||||
const road = url.pathname.match(/^\/qa-traffic\/(\d+)\/\d+\/\d+\.pbf$/);
|
||||
if (!road)
|
||||
throw new Error(`Unrecognized road fixture request: ${url.pathname}`);
|
||||
return {
|
||||
status: 200,
|
||||
contentType: 'application/x-protobuf',
|
||||
headers: corsHeaders,
|
||||
body: trafficFixtureTile(Number(road[1])),
|
||||
};
|
||||
}
|
||||
|
||||
/** Intercept provider URLs only; Cesium worker/module loading stays untouched. */
|
||||
export async function installTrafficRoutes(
|
||||
page,
|
||||
{
|
||||
origin,
|
||||
fixtures = false,
|
||||
keyless = false,
|
||||
onFlow = () => {},
|
||||
onError = () => {},
|
||||
},
|
||||
) {
|
||||
const session = await page.createCDPSession();
|
||||
const patterns = [
|
||||
`${origin}/api/tomtom/status*`,
|
||||
`${origin}/api/tomtom/flow/*`,
|
||||
...(fixtures
|
||||
? ['https://tiles.openfreemap.org/*', 'https://terrain.reearth.land/*']
|
||||
: []),
|
||||
].map((urlPattern) => ({ urlPattern, requestStage: 'Request' }));
|
||||
session.on('Fetch.requestPaused', async ({ requestId, request }) => {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
let response;
|
||||
if (keyless && url.pathname === '/api/tomtom/status') {
|
||||
response = json({ hasKey: false });
|
||||
} else if (keyless && url.pathname.startsWith('/api/tomtom/flow/')) {
|
||||
onFlow(request.url);
|
||||
response = { ...json({ error: 'no_key' }), status: 503 };
|
||||
} else if (fixtures) {
|
||||
response = trafficFixtureResponse({ url: () => request.url });
|
||||
}
|
||||
if (!response) {
|
||||
await session.send('Fetch.continueRequest', { requestId });
|
||||
return;
|
||||
}
|
||||
await session.send('Fetch.fulfillRequest', {
|
||||
requestId,
|
||||
responseCode: response.status,
|
||||
responseHeaders: Object.entries({
|
||||
'content-type': response.contentType,
|
||||
...response.headers,
|
||||
}).map(([name, value]) => ({ name, value })),
|
||||
body: Buffer.from(response.body).toString('base64'),
|
||||
});
|
||||
} catch (error) {
|
||||
if (page.isClosed()) return;
|
||||
onError(error);
|
||||
await session
|
||||
.send('Fetch.failRequest', { requestId, errorReason: 'Failed' })
|
||||
.catch(() => {});
|
||||
}
|
||||
});
|
||||
return json({ elements });
|
||||
await session.send('Fetch.enable', { patterns });
|
||||
return session;
|
||||
}
|
||||
@@ -20,6 +20,23 @@ export function createAnimation({
|
||||
parts,
|
||||
source,
|
||||
}) {
|
||||
function syncRenderDemand() {
|
||||
const needed = Boolean(
|
||||
layerState._enabled &&
|
||||
(layerState._dots.length || layerState._heatJamPrim),
|
||||
);
|
||||
// Height easing only runs in a held frame. Settle it before releasing, so
|
||||
// a population emptied mid-correction cannot report loading forever.
|
||||
if (!needed && layerState._heightPending) parts.retention.finishHeights();
|
||||
if (needed === Boolean(layerState._renderHeld)) return;
|
||||
layerState._renderHeld = needed;
|
||||
if (needed) {
|
||||
layerState._lastAnimTime = 0;
|
||||
services.render.holdContinuousRender('traffic');
|
||||
layerState._viewer?.scene?.requestRender?.();
|
||||
} else services.render.releaseContinuousRender('traffic');
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn animated dot primitives along a single road.
|
||||
*
|
||||
@@ -188,6 +205,7 @@ export function createAnimation({
|
||||
: null,
|
||||
});
|
||||
}
|
||||
syncRenderDemand();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -206,6 +224,8 @@ export function createAnimation({
|
||||
*/
|
||||
|
||||
function animate() {
|
||||
syncRenderDemand();
|
||||
if (!layerState._renderHeld) return;
|
||||
const now = Date.now();
|
||||
// Delta time in seconds, capped to avoid jumps when returning from background tab
|
||||
const dt = layerState._lastAnimTime
|
||||
@@ -339,6 +359,13 @@ export function createAnimation({
|
||||
layerState._count = 0;
|
||||
layerState._bucketCounts = { free: 0, slow: 0, jam: 0, sim: 0 };
|
||||
layerState._closedRoads = 0;
|
||||
syncRenderDemand();
|
||||
}
|
||||
return { spawnDotsForRoad, animate, maybeStopLight, clearDots };
|
||||
return {
|
||||
spawnDotsForRoad,
|
||||
animate,
|
||||
maybeStopLight,
|
||||
clearDots,
|
||||
syncRenderDemand,
|
||||
};
|
||||
}
|
||||
@@ -12,7 +12,6 @@ export function createLifecycle({
|
||||
parts,
|
||||
source,
|
||||
}) {
|
||||
const { holdContinuousRender, releaseContinuousRender } = services.render;
|
||||
const { resetFlowTileCache } = source;
|
||||
|
||||
const methods = {
|
||||
@@ -81,7 +80,7 @@ export function createLifecycle({
|
||||
layerState._enabled = true;
|
||||
layerState._surfaceFrameRemover = observeTrafficSurface(viewer.scene);
|
||||
if (layerState._roadMode !== 'tomtom') source.prefetch?.();
|
||||
holdContinuousRender('traffic'); // per-frame animator (perf wave 2)
|
||||
parts.animation.syncRenderDemand();
|
||||
layerState._lastAnimTime = 0;
|
||||
layerState._pointCollection.show = true;
|
||||
|
||||
@@ -142,7 +141,7 @@ export function createLifecycle({
|
||||
disable(viewer) {
|
||||
layerState._enabled = false;
|
||||
services.credits?.hideOsmCredit?.(layerState._viewer, 'traffic');
|
||||
releaseContinuousRender('traffic');
|
||||
parts.animation.syncRenderDemand();
|
||||
clearTimeout(layerState._fetchTimeout);
|
||||
clearInterval(layerState._enableKickTimer);
|
||||
layerState._enableKickTimer = null;
|
||||
|
||||
@@ -37,7 +37,14 @@ function setup(t) {
|
||||
visibleRoadsForAltitude: (roads) => roads,
|
||||
},
|
||||
};
|
||||
const ctx = { state, services: {}, parts, source: {} };
|
||||
const ctx = {
|
||||
state,
|
||||
services: {
|
||||
render: { holdContinuousRender() {}, releaseContinuousRender() {} },
|
||||
},
|
||||
parts,
|
||||
source: {},
|
||||
};
|
||||
parts.style = createStyle(ctx);
|
||||
parts.model = createModel(ctx);
|
||||
parts.animation = createAnimation(ctx);
|
||||
@@ -558,6 +565,37 @@ test('provisional opposite-direction heights cannot overwrite a measured sibling
|
||||
assert.notEqual(forward.waypoints, reverse.waypoints);
|
||||
});
|
||||
|
||||
test('a closure that empties the population mid-correction settles heights and clears loading before release', (t) => {
|
||||
const h = setup(t);
|
||||
h.parts.retention.reconcile(h.roads(), 450, true);
|
||||
h.settle();
|
||||
assert.equal(h.state._renderHeld, true);
|
||||
const road = h.state._roads[0];
|
||||
// Republishing the sole road starts a height correction.
|
||||
const corrected = h.roads(0, 120);
|
||||
h.parts.retention.reconcile(corrected, 450, true);
|
||||
assert.equal(h.state._heightPending, true);
|
||||
assert.equal(road.heightMoving, true);
|
||||
// The closure arrives before the next population pass, which then removes
|
||||
// the final points after their fade, long before the correction finishes.
|
||||
corrected[0].flow = { level: 0, closure: true };
|
||||
for (let i = 0; i < 20 && h.state._dots.length; i++) h.frame();
|
||||
assert.equal(h.state._dots.length, 0);
|
||||
assert.equal(h.state._heatJamPrim, null);
|
||||
assert.equal(h.state._renderHeld, false);
|
||||
assert.equal(h.state._heightPending, false);
|
||||
assert.equal(h.state._populationPending, false);
|
||||
assert.equal(road.heightMoving, false);
|
||||
assert.ok(
|
||||
Math.abs(C.Cartographic.fromCartesian(road.waypoints[0]).height - 120) <
|
||||
0.01,
|
||||
);
|
||||
// Frames driven by other layers stay idle and cannot revive the work.
|
||||
h.frame();
|
||||
assert.equal(h.state._heightPending, false);
|
||||
assert.equal(h.state._renderHeld, false);
|
||||
});
|
||||
|
||||
test('large height corrections obey a screen-pixel bound and reuse projection scratch objects', (t) => {
|
||||
const h = setup(t);
|
||||
h.state._viewer.scene = {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createAnimation } from './animation.js';
|
||||
|
||||
test('traffic render demand follows animated population and jam lines across empty, disable and re-enable', () => {
|
||||
const calls = [];
|
||||
const state = { _enabled: true, _dots: [], _motion: { rebuilds: 0 } };
|
||||
const animation = createAnimation({
|
||||
state,
|
||||
services: {
|
||||
render: {
|
||||
holdContinuousRender: (id) => calls.push(['hold', id]),
|
||||
releaseContinuousRender: (id) => calls.push(['release', id]),
|
||||
},
|
||||
},
|
||||
parts: {
|
||||
retention: { clear() {} },
|
||||
rendering: {
|
||||
removeHeatLines() {
|
||||
state._heatJamPrim = null;
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
animation.syncRenderDemand();
|
||||
assert.deepEqual(calls, []);
|
||||
state._dots.push({});
|
||||
animation.syncRenderDemand();
|
||||
animation.syncRenderDemand();
|
||||
assert.deepEqual(calls, [['hold', 'traffic']]);
|
||||
animation.clearDots();
|
||||
assert.deepEqual(calls.at(-1), ['release', 'traffic']);
|
||||
state._heatJamPrim = {};
|
||||
animation.syncRenderDemand();
|
||||
assert.deepEqual(calls.at(-1), ['hold', 'traffic']);
|
||||
state._enabled = false;
|
||||
animation.syncRenderDemand();
|
||||
assert.deepEqual(calls.at(-1), ['release', 'traffic']);
|
||||
state._enabled = true;
|
||||
animation.clearDots();
|
||||
assert.equal(state._renderHeld, false);
|
||||
state._dots.push({});
|
||||
animation.syncRenderDemand();
|
||||
assert.equal(state._renderHeld, true);
|
||||
});
|
||||
@@ -50,6 +50,7 @@ export function createRendering({
|
||||
layerState._heatSlowPrim = null;
|
||||
}
|
||||
layerState._heatLineCount = 0;
|
||||
parts.animation.syncRenderDemand?.();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,6 +153,7 @@ export function createRendering({
|
||||
}
|
||||
|
||||
layerState._heatLineCount = kept.length;
|
||||
parts.animation.syncRenderDemand?.();
|
||||
if (candidates.length > kept.length) {
|
||||
console.log(
|
||||
`[Data:Traffic] Heat-lines capped at ${HEAT_LINE_CAP} (${candidates.length} congested roads in view)`,
|
||||
|
||||
@@ -125,6 +125,7 @@ export function createRetention({ state, parts }) {
|
||||
count++;
|
||||
}
|
||||
state._count = count;
|
||||
parts.animation.syncRenderDemand?.();
|
||||
}
|
||||
function reconcile(roads, height, replace) {
|
||||
altitude = height;
|
||||
@@ -241,6 +242,29 @@ export function createRetention({ state, parts }) {
|
||||
road.heightMoving = moving;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Settle every retained road on its corrected heights at once. Easing only
|
||||
* runs inside the held animation frame, so once nothing is left to animate
|
||||
* the remaining correction would otherwise stay pending indefinitely.
|
||||
* Published roads whose record was just dropped are settled too, so a later
|
||||
* held frame cannot revive their correction.
|
||||
*/
|
||||
function finishHeights() {
|
||||
const roads = new Set(state._roads);
|
||||
for (const record of records.values()) roads.add(record.road);
|
||||
for (const road of roads) {
|
||||
if (!road.heightMoving) continue;
|
||||
for (let i = 0; i < road.waypoints.length; i++)
|
||||
Cesium.Cartesian3.clone(road.targetWaypoints[i], road.waypoints[i]);
|
||||
for (let i = 0; i < road.segmentDist.length; i++)
|
||||
road.segmentDist[i] = Cesium.Cartesian3.distance(
|
||||
road.waypoints[i],
|
||||
road.waypoints[i + 1],
|
||||
);
|
||||
road.heightMoving = false;
|
||||
}
|
||||
state._heightPending = false;
|
||||
}
|
||||
function clear() {
|
||||
if (timer !== null) {
|
||||
if (typeof cancelAnimationFrame === 'function')
|
||||
@@ -252,5 +276,5 @@ export function createRetention({ state, parts }) {
|
||||
state._populationPending = false;
|
||||
state._heightPending = false;
|
||||
}
|
||||
return { reconcile, easeHeights, clear, refreshCounts };
|
||||
return { reconcile, easeHeights, finishHeights, clear, refreshCounts };
|
||||
}
|
||||
@@ -3574,7 +3574,7 @@ test('actual Traffic, Bikeshare and Transit lifecycles retain sensitivity in eve
|
||||
source: {},
|
||||
parts: {
|
||||
flow: { ensureFlowStatus: noop },
|
||||
animation: { animate: noop, clearDots: noop },
|
||||
animation: { animate: noop, clearDots: noop, syncRenderDemand: noop },
|
||||
viewport: { onCameraChanged: noop },
|
||||
ingestion: { cancelActiveFetch: noop },
|
||||
},
|
||||
|
||||
+56
-1
@@ -1,6 +1,10 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { MACOS_CHROME, chromeExecutablePath } from '../scripts/qa-chrome.mjs';
|
||||
import {
|
||||
MACOS_CHROME,
|
||||
chromeExecutablePath,
|
||||
captureQaScreenshot,
|
||||
} from '../scripts/qa-chrome.mjs';
|
||||
|
||||
test('gates keep macOS Chrome where it is installed', () => {
|
||||
assert.equal(
|
||||
@@ -25,3 +29,54 @@ test('an explicit executable path wins', () => {
|
||||
'C:\\Chrome\\chrome.exe',
|
||||
);
|
||||
});
|
||||
|
||||
test('screenshots restore minimized test windows before capture and detach their session', async () => {
|
||||
const calls = [];
|
||||
const session = {
|
||||
send: async (method, params) => {
|
||||
calls.push([method, params]);
|
||||
return { windowId: 7, bounds: { windowState: 'minimized' } };
|
||||
},
|
||||
detach: async () => calls.push(['detach']),
|
||||
};
|
||||
const page = {
|
||||
createCDPSession: async () => session,
|
||||
bringToFront: async () => calls.push(['front']),
|
||||
screenshot: async (options) => {
|
||||
calls.push(['capture', options]);
|
||||
return 'pixels';
|
||||
},
|
||||
};
|
||||
assert.equal(
|
||||
await captureQaScreenshot(page, { path: 'proof.png' }),
|
||||
'pixels',
|
||||
);
|
||||
assert.deepEqual(
|
||||
calls.map((c) => c[0]),
|
||||
[
|
||||
'Browser.getWindowForTarget',
|
||||
'Browser.setWindowBounds',
|
||||
'front',
|
||||
'detach',
|
||||
'capture',
|
||||
],
|
||||
);
|
||||
assert.deepEqual(calls[1][1], {
|
||||
windowId: 7,
|
||||
bounds: { windowState: 'normal' },
|
||||
});
|
||||
});
|
||||
|
||||
test('headless capture does not manipulate a native window', async () => {
|
||||
const options = { path: 'proof.png' };
|
||||
const page = {
|
||||
screenshot: async (supplied) => {
|
||||
assert.equal(supplied, options);
|
||||
return 'pixels';
|
||||
},
|
||||
};
|
||||
assert.equal(
|
||||
await captureQaScreenshot(page, options, { headful: false }),
|
||||
'pixels',
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,170 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
trafficFixtureResponse,
|
||||
trafficFixtureTile,
|
||||
} from '../../scripts/traffic-fixtures.mjs';
|
||||
import { decodeOpenFreeMapTile } from '../sources/openFreeMap.js';
|
||||
import { isDrivableFlowSegment } from '../layers/traffic/roadModes.js';
|
||||
import { decodeFlowTile } from '../layers/traffic/flowDecode.js';
|
||||
|
||||
test('traffic fixtures provide aligned current road tiles and partial flow coverage', () => {
|
||||
for (const z of [12, 14]) {
|
||||
const roads = decodeOpenFreeMapTile(
|
||||
trafficFixtureTile(z),
|
||||
z,
|
||||
935,
|
||||
1686,
|
||||
).roads;
|
||||
const flow = decodeFlowTile(trafficFixtureTile(z, true), z, 935, 1686);
|
||||
assert.ok(roads.length > flow.length && flow.length > 0);
|
||||
for (const segment of flow) {
|
||||
assert.equal(segment.trafficLevel, 0.6);
|
||||
assert.equal(isDrivableFlowSegment(segment), true);
|
||||
assert.ok(
|
||||
roads.some(
|
||||
(road) =>
|
||||
JSON.stringify(road.coordinates) === JSON.stringify(segment.coords),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('fixture TileJSON and every road/flow request stay on intercepted sources', () => {
|
||||
const request = (url) => trafficFixtureResponse({ url: () => url });
|
||||
const tileJson = request('https://tiles.openfreemap.org/planet');
|
||||
assert.equal(tileJson.headers['access-control-allow-origin'], '*');
|
||||
const metadata = JSON.parse(tileJson.body);
|
||||
const tileUrl = metadata.tiles[0]
|
||||
.replace('{z}', '14')
|
||||
.replace('{x}', '935')
|
||||
.replace('{y}', '1686');
|
||||
assert.ok(
|
||||
decodeOpenFreeMapTile(request(tileUrl).body, 14, 935, 1686).roads.length,
|
||||
);
|
||||
assert.ok(
|
||||
decodeFlowTile(
|
||||
request('http://localhost/api/tomtom/flow/12/935/1686.pbf').body,
|
||||
12,
|
||||
935,
|
||||
1686,
|
||||
).length,
|
||||
);
|
||||
assert.throws(
|
||||
() => request('https://tiles.openfreemap.org/unexpected'),
|
||||
/Unrecognized/,
|
||||
);
|
||||
assert.equal(request('http://localhost/api/unrelated'), null);
|
||||
});
|
||||
|
||||
test('flat terrain fixture decodes through Cesium and advertises no network children', async () => {
|
||||
const { createServer } = await import('node:http');
|
||||
const { CesiumTerrainProvider } = await import('cesium');
|
||||
const server = createServer((req, res) => {
|
||||
const response = trafficFixtureResponse({
|
||||
url: () => `https://terrain.reearth.land${req.url}`,
|
||||
});
|
||||
res.writeHead(response.status, { 'content-type': response.contentType });
|
||||
res.end(response.body);
|
||||
});
|
||||
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
||||
try {
|
||||
const provider = await CesiumTerrainProvider.fromUrl(
|
||||
`http://127.0.0.1:${server.address().port}`,
|
||||
);
|
||||
for (const x of [0, 1]) {
|
||||
const data = await provider.requestTileGeometry(x, 0, 0);
|
||||
const rect = provider.tilingScheme.tileXYToRectangle(x, 0, 0);
|
||||
assert.equal(
|
||||
data.interpolateHeight(rect, (rect.west + rect.east) / 2, 0),
|
||||
0,
|
||||
);
|
||||
for (const dx of [0, 1])
|
||||
for (const dy of [0, 1]) {
|
||||
assert.equal(data.isChildAvailable(x, 0, x * 2 + dx, dy), false);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
server.closeAllConnections();
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
}
|
||||
});
|
||||
|
||||
test('fixture interception targets provider URLs and leaves app workers alone', async () => {
|
||||
const { installTrafficRoutes } =
|
||||
await import('../../scripts/traffic-fixtures.mjs');
|
||||
const sent = [];
|
||||
let paused;
|
||||
const session = {
|
||||
on: (_, handler) => {
|
||||
paused = handler;
|
||||
},
|
||||
send: async (name, args) => {
|
||||
sent.push({ name, args });
|
||||
},
|
||||
};
|
||||
await installTrafficRoutes(
|
||||
{ createCDPSession: async () => session, isClosed: () => false },
|
||||
{ origin: 'http://localhost:4173', fixtures: true },
|
||||
);
|
||||
const patterns = sent[0].args.patterns.map((p) => p.urlPattern);
|
||||
assert.deepEqual(patterns, [
|
||||
'http://localhost:4173/api/tomtom/status*',
|
||||
'http://localhost:4173/api/tomtom/flow/*',
|
||||
'https://tiles.openfreemap.org/*',
|
||||
'https://terrain.reearth.land/*',
|
||||
]);
|
||||
await paused({
|
||||
requestId: 'terrain',
|
||||
request: { url: 'https://terrain.reearth.land/layer.json' },
|
||||
});
|
||||
const reply = sent.at(-1);
|
||||
assert.equal(reply.name, 'Fetch.fulfillRequest');
|
||||
assert.equal(
|
||||
JSON.parse(Buffer.from(reply.args.body, 'base64')).format,
|
||||
'heightmap-1.0',
|
||||
);
|
||||
assert.ok(
|
||||
reply.args.responseHeaders.some(
|
||||
(h) => h.name === 'access-control-allow-origin' && h.value === '*',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('keyless interception refuses and records unexpected flow requests', async () => {
|
||||
const { installTrafficRoutes } =
|
||||
await import('../../scripts/traffic-fixtures.mjs');
|
||||
const sent = [],
|
||||
flows = [];
|
||||
let paused;
|
||||
const session = {
|
||||
on: (_, handler) => {
|
||||
paused = handler;
|
||||
},
|
||||
send: async (name, args) => {
|
||||
sent.push({ name, args });
|
||||
},
|
||||
};
|
||||
await installTrafficRoutes(
|
||||
{ createCDPSession: async () => session, isClosed: () => false },
|
||||
{
|
||||
origin: 'http://localhost:4173',
|
||||
keyless: true,
|
||||
onFlow: (url) => flows.push(url),
|
||||
},
|
||||
);
|
||||
await paused({
|
||||
requestId: 'status',
|
||||
request: { url: 'http://localhost:4173/api/tomtom/status' },
|
||||
});
|
||||
assert.deepEqual(JSON.parse(Buffer.from(sent.at(-1).args.body, 'base64')), {
|
||||
hasKey: false,
|
||||
});
|
||||
await paused({
|
||||
requestId: 'flow',
|
||||
request: { url: 'http://localhost:4173/api/tomtom/flow/12/1/1.pbf' },
|
||||
});
|
||||
assert.equal(sent.at(-1).args.responseCode, 503);
|
||||
assert.equal(flows.length, 1);
|
||||
});
|
||||
Reference in new issue
Block a user