mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
test: observe aircraft arrival before idle refresh
This commit is contained in:
1 parent
84d96971df
commit
69a87fe238
6 files changed
+291
-53
No files matched your search
@@ -4,3 +4,6 @@
|
|||||||
- Release unread responses from the optional Google-token probe so keyless
|
- Release unread responses from the optional Google-token probe so keyless
|
||||||
browser validation can settle; the Overpass gate recognizes only that probe's
|
browser validation can settle; the Overpass gate recognizes only that probe's
|
||||||
expected 404 as an unavailable capability (asturksever, #1264).
|
expected 404 as an unavailable capability (asturksever, #1264).
|
||||||
|
- Make tracking arrival checks respect the aircraft update cadence while checking
|
||||||
|
orientation on the first reveal, avoiding false failures at faster render
|
||||||
|
rates (asturksever, #1264).
|
||||||
@@ -5011,7 +5011,7 @@ pins and share links already supply complete boxes.
|
|||||||
- Fleet model eligibility is distance-based with on-screen priority and hard caps (`MODEL_MAX`, `MODEL_MAX_ALL`) to avoid draw-call explosions. Each model owns its own `modelMatrix`; shared scratch matrices are forbidden because they caused stacking/flicker.
|
- Fleet model eligibility is distance-based with on-screen priority and hard caps (`MODEL_MAX`, `MODEL_MAX_ALL`) to avoid draw-call explosions. Each model owns its own `modelMatrix`; shared scratch matrices are forbidden because they caused stacking/flicker.
|
||||||
- Tracked aircraft use standalone model primitives driven from the already-settled dead-reckoned display position, while the tracked Cesium entity remains billboard-backed so `viewer.trackedEntity` always has a ready bounding sphere.
|
- Tracked aircraft use standalone model primitives driven from the already-settled dead-reckoned display position, while the tracked Cesium entity remains billboard-backed so `viewer.trackedEntity` always has a ready bounding sphere.
|
||||||
- Flights and military layers mirror the same tracking invariants: no warm-up freeze/jump, altitude-scaled framing, trail head glued to the displayed plane, no pull-out when switching targets, and no cross-layer orphan when switching between commercial and military tracks.
|
- Flights and military layers mirror the same tracking invariants: no warm-up freeze/jump, altitude-scaled framing, trail head glued to the displayed plane, no pull-out when switching targets, and no cross-layer orphan when switching between commercial and military tracks.
|
||||||
- Regression surface: `npm run test:track` drives the real app headless with synthetic aircraft feeds and asserts the tracking invariants without depending on live OpenSky/adsb data. `src/data/trackedModelRegime.test.mjs` pins the tracked contact's threshold math, the enter/exit asymmetry, and the default-on / cockpit / TR-3B / deselect wiring in both layers.
|
- Regression surface: `npm run test:track` drives the real app headless with synthetic aircraft feeds and asserts the tracking invariants without depending on live OpenSky/adsb data. Arrival-rotation checks observe the first reveal within a bounded wall-clock window before the one-second idle fallback, rather than assuming the 80 ms fleet cadence fits inside four rendered frames; an incorrect first orientation fails immediately. `src/data/trackedModelRegime.test.mjs` pins the tracked contact's threshold math, the enter/exit asymmetry, and the default-on / cockpit / TR-3B / deselect wiring in both layers.
|
||||||
|
|
||||||
### TR-3B conversion Easter egg (August 2026)
|
### TR-3B conversion Easter egg (August 2026)
|
||||||
|
|
||||||
|
|||||||
@@ -1071,5 +1071,6 @@
|
|||||||
"src/tooling/changelogFragments.test.mjs",
|
"src/tooling/changelogFragments.test.mjs",
|
||||||
"src/data/gbfsProxyRateLimit.test.mjs",
|
"src/data/gbfsProxyRateLimit.test.mjs",
|
||||||
"src/terrainHeightsBounds.test.mjs",
|
"src/terrainHeightsBounds.test.mjs",
|
||||||
"server/providers/overpass/requestAdmission.test.mjs"
|
"server/providers/overpass/requestAdmission.test.mjs",
|
||||||
|
"src/tooling/trackArrivalObservation.test.mjs"
|
||||||
]
|
]
|
||||||
@@ -94,6 +94,7 @@
|
|||||||
|
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import puppeteer from 'puppeteer';
|
import puppeteer from 'puppeteer';
|
||||||
|
import { arrivalRotationPassed } from '../src/tooling/trackArrivalObservation.mjs';
|
||||||
import { classifyAircraft, CLASS_SCALE_3D, CLASS_MODEL_REAL } from '../src/data/aircraftClass.js';
|
import { classifyAircraft, CLASS_SCALE_3D, CLASS_MODEL_REAL } from '../src/data/aircraftClass.js';
|
||||||
import { ensureGeoidReady, geoidHeight } from '../src/data/geoid.js';
|
import { ensureGeoidReady, geoidHeight } from '../src/data/geoid.js';
|
||||||
|
|
||||||
@@ -2695,16 +2696,17 @@ async function main() {
|
|||||||
// reappear wearing its stale/creation-north nose for up to 1 s).
|
// reappear wearing its stale/creation-north nose for up to 1 s).
|
||||||
// Staged deterministically: park the camera nadir over the synthetic
|
// Staged deterministically: park the camera nadir over the synthetic
|
||||||
// cluster, let rotations settle, TAMPER a billboard's rotation, then
|
// cluster, let rotations settle, TAMPER a billboard's rotation, then
|
||||||
// trigger each path with the camera idle. The idle-drift catch-up
|
// trigger each path with the camera idle. The fleet runs every 80 ms,
|
||||||
// (ROTATION_REFRESH_MS = 1 s) can coincidentally mask a regression in a
|
// independently of rendered FPS. Observe the first completed settle and
|
||||||
// ~300 ms SwiftShader frame window, so a regression shows as an
|
// reveal frames within 500 ms of forcing moveEnd, before the 1 s idle
|
||||||
// intermittent (not guaranteed) failure — never a false failure.
|
// rotation refresh can repair an incorrect first reveal.
|
||||||
// ============================================================
|
// ============================================================
|
||||||
console.log('\nChange 4 — arrival rotation: moveEnd settle pass + horizon-reveal pass');
|
console.log('\nChange 4 — arrival rotation: moveEnd settle pass + horizon-reveal pass');
|
||||||
const arrival = await evalPage(async () => {
|
const arrival = await evalPage(async () => {
|
||||||
const v = window.__godsEyeView.viewer;
|
const v = window.__godsEyeView.viewer;
|
||||||
const dm = window.__godsEyeView.dataManager;
|
const dm = window.__godsEyeView.dataManager;
|
||||||
const { screenProjectedRotation } = await import(`${window.__gevQaSourceBase || '/src'}/data/iconOrientation.js`);
|
const { screenProjectedRotation } = await import(`${window.__gevQaSourceBase || '/src'}/data/iconOrientation.js`);
|
||||||
|
const { observeArrivalRotation } = await import(`${window.__gevQaSourceBase || '/src'}/tooling/trackArrivalObservation.mjs`);
|
||||||
// 3D models OFF for this phase: a model-handed-off billboard is hidden
|
// 3D models OFF for this phase: a model-handed-off billboard is hidden
|
||||||
// and skips rotation updates entirely — the probes need live billboards
|
// and skips rotation updates entirely — the probes need live billboards
|
||||||
// (this is also the app's default state the field report came from).
|
// (this is also the app's default state the field report came from).
|
||||||
@@ -2766,39 +2768,29 @@ async function main() {
|
|||||||
const bb = findBB(id);
|
const bb = findBB(id);
|
||||||
if (!bb || !bb.show) return { error: `${id} missing/hidden` };
|
if (!bb || !bb.show) return { error: `${id} missing/hidden` };
|
||||||
const r0 = bb.rotation; // settled tamper origin; correctness uses a fresh projection below
|
const r0 = bb.rotation; // settled tamper origin; correctness uses a fresh projection below
|
||||||
// (a) settle pass: tamper, then raise moveEnd with the camera IDLE —
|
const observed = await observeArrivalRotation({
|
||||||
// the pose signature is unchanged, so only the moveEnd hook can fix
|
scene: v.scene,
|
||||||
// this before the 1 s catch-up.
|
startMoveEnd() {
|
||||||
bb.rotation = r0 + 1.5;
|
// Only moveEnd can repair the tampered angle at the idle pose.
|
||||||
v.camera.moveEnd.raiseEvent();
|
bb.rotation = r0 + 1.5;
|
||||||
await nextFrames(2);
|
v.camera.moveEnd.raiseEvent();
|
||||||
const afterMoveEnd = bb.rotation;
|
},
|
||||||
// A real-GPU fleet tick can land near the edge of the frame budget and
|
startReveal() {
|
||||||
// advance the contact before the probe reads it. Compare with the
|
// Hide immediately after the confirmed settle frame. The next
|
||||||
// production projection at the CURRENT position, not the now-stale r0.
|
// eligible fleet tick must reveal AND re-aim in that same tick.
|
||||||
const expectedMoveEnd = screenProjectedRotation(v.scene, bb.position, course, null);
|
bb.rotation = r0 + 1.5;
|
||||||
// (b) reveal pass: tamper + hide — the next fleet tick must flip it
|
bb.show = false;
|
||||||
// visible AND correct the nose in that same tick (camera still idle,
|
},
|
||||||
// no moveEnd raised). Diagnostic fields (round 5): record WHICH frame
|
read() {
|
||||||
// the flip landed on (flipFrame; -1 = never within the extended
|
return {
|
||||||
// window) and whether a 3D model owns this id — distinguishes "tick
|
shown: bb.show,
|
||||||
// slow under load" from "occluder says hidden" from "model handoff".
|
rotationError: angDiff(bb.rotation,
|
||||||
bb.rotation = r0 + 1.5;
|
screenProjectedRotation(v.scene, bb.position, course, null)),
|
||||||
bb.show = false;
|
};
|
||||||
let flipFrame = -1;
|
},
|
||||||
for (let f = 1; f <= 6; f++) {
|
});
|
||||||
await nextFrames(1);
|
|
||||||
if (bb.show && flipFrame === -1) flipFrame = f;
|
|
||||||
if (f >= 2 && flipFrame !== -1) break;
|
|
||||||
}
|
|
||||||
const expectedReveal = screenProjectedRotation(v.scene, bb.position, course, null);
|
|
||||||
const hasModel = !!(window.__g3dFindModel && window.__g3dFindModel(id));
|
const hasModel = !!(window.__g3dFindModel && window.__g3dFindModel(id));
|
||||||
return {
|
return { r0, ...observed, hasModel };
|
||||||
r0,
|
|
||||||
dMoveEnd: angDiff(afterMoveEnd, expectedMoveEnd),
|
|
||||||
dReveal: angDiff(bb.rotation, expectedReveal),
|
|
||||||
shown: bb.show, flipFrame, hasModel,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
const flightCourse = window.__SYNTH.flights.find((f) => f.icao === 'aaa002')?.track ?? 0;
|
const flightCourse = window.__SYNTH.flights.find((f) => f.icao === 'aaa002')?.track ?? 0;
|
||||||
const militaryCourse = window.__SYNTH.military.find((m) => m.hex === 'bbb101')?.track ?? 0;
|
const militaryCourse = window.__SYNTH.military.find((m) => m.hex === 'bbb101')?.track ?? 0;
|
||||||
@@ -2807,22 +2799,13 @@ async function main() {
|
|||||||
military: await probe('bbb101', militaryCourse),
|
military: await probe('bbb101', militaryCourse),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const fmtArr = (a) => (!a ? `no result (${arrival?.error || 'phase error'})` : a.error ? a.error
|
const fmtArr = (a) => (!a ? `no result (${arrival?.error || 'phase error'})` : a.error ? `${a.error}; elapsed=${Number(a.elapsedMs).toFixed(1)} ms`
|
||||||
: `settle-err=${(a.dMoveEnd * 180 / Math.PI).toFixed(1)}° reveal-err=${(a.dReveal * 180 / Math.PI).toFixed(1)}° shown=${a.shown} flipFrame=${a.flipFrame} hasModel=${a.hasModel}`);
|
: `settle-err=${(a.dMoveEnd * 180 / Math.PI).toFixed(1)}° reveal-err=${(a.dReveal * 180 / Math.PI).toFixed(1)}° shown=${a.shown} flipFrame=${a.flipFrame} settle=${a.moveEndMs.toFixed(1)} ms reveal=${a.revealMs.toFixed(1)} ms total=${a.elapsedMs.toFixed(1)} ms hasModel=${a.hasModel}`);
|
||||||
const TAMPER_DEG = 1.5 * 180 / Math.PI; // ≈ 86° — an uncorrected nose stays this far off
|
const TAMPER_DEG = 1.5 * 180 / Math.PI; // ≈ 86° — an uncorrected nose stays this far off
|
||||||
// Reveal budget re-pinned 2 → 4 frames (round 5): the diagnostic probe
|
record(`arrival: moveEnd + first reveal both re-aim before idle refresh, flights (tamper was ${TAMPER_DEG.toFixed(0)}°)`,
|
||||||
// shows the military flip consistently lands on frame 2 — the exact edge
|
arrivalRotationPassed(arrival.flights), fmtArr(arrival.flights));
|
||||||
// of the old budget — so any load hiccup flaked it (flights lands frame
|
record(`arrival: moveEnd + first reveal both re-aim before idle refresh, military (tamper was ${TAMPER_DEG.toFixed(0)}°)`,
|
||||||
// 1). The regression this protects against is a reveal that waits for
|
arrivalRotationPassed(arrival.military), fmtArr(arrival.military));
|
||||||
// the 1 s idle catch-up (~30-60 frames) or never re-aims; 4 frames still
|
|
||||||
// catches that with a wide margin, with zero-headroom flakes gone.
|
|
||||||
const arrOk = (a) => !!a && !a.error && a.shown
|
|
||||||
&& (a.flipFrame === -1 ? false : a.flipFrame <= 4)
|
|
||||||
&& a.dMoveEnd * 180 / Math.PI < 10 && a.dReveal * 180 / Math.PI < 10;
|
|
||||||
record(`arrival: moveEnd + reveal both re-aim within 4 frames, flights (tamper was ${TAMPER_DEG.toFixed(0)}°)`,
|
|
||||||
arrOk(arrival.flights), fmtArr(arrival.flights));
|
|
||||||
record(`arrival: moveEnd + reveal both re-aim within 4 frames, military (tamper was ${TAMPER_DEG.toFixed(0)}°)`,
|
|
||||||
arrOk(arrival.military), fmtArr(arrival.military));
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// DISPLAY FLOOR (2026-08-19): a grounded contact must never render below
|
// DISPLAY FLOOR (2026-08-19): a grounded contact must never render below
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
/** A reveal must beat the one-second idle rotation refresh, regardless of FPS. */
|
||||||
|
export const ARRIVAL_OBSERVATION_MS = 500;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observe the first completed moveEnd frame and the first subsequent reveal.
|
||||||
|
* The deadline starts when moveEnd is forced, before its rotation baseline, so
|
||||||
|
* waiting for either frame cannot let the one-second idle refresh mask a bug.
|
||||||
|
* `read` snapshots visibility and rotation error against the current position.
|
||||||
|
*/
|
||||||
|
export function observeArrivalRotation({
|
||||||
|
scene,
|
||||||
|
startMoveEnd,
|
||||||
|
startReveal,
|
||||||
|
read,
|
||||||
|
now = () => performance.now(),
|
||||||
|
setTimer = setTimeout,
|
||||||
|
clearTimer = clearTimeout,
|
||||||
|
}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const startedAt = now();
|
||||||
|
let moveEnd = null;
|
||||||
|
let moveEndMs = null;
|
||||||
|
let revealFrames = 0;
|
||||||
|
let done = false;
|
||||||
|
let remove;
|
||||||
|
let timer;
|
||||||
|
const cleanup = () => {
|
||||||
|
done = true;
|
||||||
|
remove?.();
|
||||||
|
clearTimer(timer);
|
||||||
|
};
|
||||||
|
const finish = (result) => {
|
||||||
|
if (done) return;
|
||||||
|
cleanup();
|
||||||
|
resolve(result);
|
||||||
|
};
|
||||||
|
const expired = () => ({
|
||||||
|
error: `arrival ${moveEnd ? 'reveal' : 'moveEnd'} observation exceeded ${ARRIVAL_OBSERVATION_MS} ms`,
|
||||||
|
elapsedMs: now() - startedAt,
|
||||||
|
moveEndMs,
|
||||||
|
flipFrame: -1,
|
||||||
|
});
|
||||||
|
remove = scene.postRender.addEventListener(() => {
|
||||||
|
if (done) return;
|
||||||
|
try {
|
||||||
|
const elapsedMs = now() - startedAt;
|
||||||
|
if (elapsedMs >= ARRIVAL_OBSERVATION_MS) {
|
||||||
|
finish(expired());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sample = read();
|
||||||
|
if (moveEnd === null) {
|
||||||
|
moveEnd = sample;
|
||||||
|
moveEndMs = elapsedMs;
|
||||||
|
startReveal();
|
||||||
|
} else {
|
||||||
|
revealFrames++;
|
||||||
|
if (sample.shown) {
|
||||||
|
// Keep the FIRST visible result, including a wrong angle. Never
|
||||||
|
// wait for a subsequent tick to repair a failed reveal.
|
||||||
|
finish({
|
||||||
|
dMoveEnd: moveEnd.rotationError,
|
||||||
|
dReveal: sample.rotationError,
|
||||||
|
shown: sample.shown,
|
||||||
|
flipFrame: revealFrames,
|
||||||
|
moveEndMs,
|
||||||
|
revealMs: elapsedMs - moveEndMs,
|
||||||
|
elapsedMs,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scene.requestRender();
|
||||||
|
} catch (error) {
|
||||||
|
cleanup();
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
timer = setTimer(() => finish(expired()), ARRIVAL_OBSERVATION_MS);
|
||||||
|
try {
|
||||||
|
startMoveEnd();
|
||||||
|
scene.requestRender();
|
||||||
|
} catch (error) {
|
||||||
|
cleanup();
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Test the captured frames without retrying a bad first observation. */
|
||||||
|
export function arrivalRotationPassed(observation) {
|
||||||
|
return (
|
||||||
|
!!observation &&
|
||||||
|
!observation.error &&
|
||||||
|
observation.shown === true &&
|
||||||
|
observation.elapsedMs < ARRIVAL_OBSERVATION_MS &&
|
||||||
|
(observation.dMoveEnd * 180) / Math.PI < 10 &&
|
||||||
|
(observation.dReveal * 180) / Math.PI < 10
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {
|
||||||
|
ARRIVAL_OBSERVATION_MS,
|
||||||
|
arrivalRotationPassed,
|
||||||
|
observeArrivalRotation,
|
||||||
|
} from './trackArrivalObservation.mjs';
|
||||||
|
|
||||||
|
function fixture() {
|
||||||
|
let time = 0;
|
||||||
|
let state = { shown: true, rotationError: 0 };
|
||||||
|
const listeners = new Set();
|
||||||
|
const timers = new Map();
|
||||||
|
let renders = 0;
|
||||||
|
let moveEnds = 0;
|
||||||
|
const observation = observeArrivalRotation({
|
||||||
|
scene: {
|
||||||
|
postRender: {
|
||||||
|
addEventListener(listener) {
|
||||||
|
listeners.add(listener);
|
||||||
|
return () => listeners.delete(listener);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
requestRender() {
|
||||||
|
renders++;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
startMoveEnd() {
|
||||||
|
moveEnds++;
|
||||||
|
state.rotationError = 1.5;
|
||||||
|
},
|
||||||
|
startReveal() {
|
||||||
|
state = { shown: false, rotationError: 1.5 };
|
||||||
|
},
|
||||||
|
read: () => ({ ...state }),
|
||||||
|
now: () => time,
|
||||||
|
setTimer(callback, delay) {
|
||||||
|
const handle = {};
|
||||||
|
timers.set(handle, { callback, at: time + delay });
|
||||||
|
return handle;
|
||||||
|
},
|
||||||
|
clearTimer(handle) {
|
||||||
|
timers.delete(handle);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
observation,
|
||||||
|
frame(at, nextState) {
|
||||||
|
time = at;
|
||||||
|
state = { ...state, ...nextState };
|
||||||
|
for (const listener of [...listeners]) listener();
|
||||||
|
},
|
||||||
|
expire(at) {
|
||||||
|
time = at;
|
||||||
|
for (const timer of [...timers.values()])
|
||||||
|
if (timer.at <= at) timer.callback();
|
||||||
|
},
|
||||||
|
get resources() {
|
||||||
|
return {
|
||||||
|
listeners: listeners.size,
|
||||||
|
timers: timers.size,
|
||||||
|
renders,
|
||||||
|
moveEnds,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('80 ms fleet scheduling passes after more than four fast rendered frames', async () => {
|
||||||
|
const f = fixture();
|
||||||
|
f.frame(8, { shown: true, rotationError: 0 });
|
||||||
|
// The forced fleet tick was at 8 ms. A 125 FPS renderer can complete nine
|
||||||
|
// more frames before the next 80 ms fleet interval becomes eligible.
|
||||||
|
for (let at = 16; at < 88; at += 8) f.frame(at);
|
||||||
|
f.frame(88, { shown: true, rotationError: 0 });
|
||||||
|
const result = await f.observation;
|
||||||
|
assert.equal(result.flipFrame, 10);
|
||||||
|
assert.equal(result.moveEndMs, 8);
|
||||||
|
assert.equal(result.revealMs, 80);
|
||||||
|
assert.equal(result.elapsedMs, 88);
|
||||||
|
assert.equal(arrivalRotationPassed(result), true);
|
||||||
|
assert.equal(f.resources.listeners, 0);
|
||||||
|
assert.equal(f.resources.timers, 0);
|
||||||
|
assert.equal(f.resources.moveEnds, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a wrong first visible angle fails even if the next frame corrects it', async () => {
|
||||||
|
const f = fixture();
|
||||||
|
f.frame(8, { shown: true, rotationError: 0 });
|
||||||
|
f.frame(88, { shown: true, rotationError: 1.5 });
|
||||||
|
f.frame(96, { shown: true, rotationError: 0 });
|
||||||
|
const result = await f.observation;
|
||||||
|
assert.equal(result.dReveal, 1.5);
|
||||||
|
assert.equal(result.flipFrame, 1);
|
||||||
|
assert.equal(result.elapsedMs, 88);
|
||||||
|
assert.equal(arrivalRotationPassed(result), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a wrong first moveEnd angle is retained rather than repaired by reveal', async () => {
|
||||||
|
const f = fixture();
|
||||||
|
f.frame(8, { shown: true, rotationError: 1.5 });
|
||||||
|
f.frame(88, { shown: true, rotationError: 0 });
|
||||||
|
const result = await f.observation;
|
||||||
|
assert.equal(result.dMoveEnd, 1.5);
|
||||||
|
assert.equal(arrivalRotationPassed(result), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('no reveal fails at a deadline before the one-second idle catch-up', async () => {
|
||||||
|
const f = fixture();
|
||||||
|
assert.equal(ARRIVAL_OBSERVATION_MS, 500);
|
||||||
|
f.frame(8, { shown: true, rotationError: 0 });
|
||||||
|
f.frame(480);
|
||||||
|
f.expire(500);
|
||||||
|
const result = await f.observation;
|
||||||
|
assert.match(result.error, /reveal.*500 ms/);
|
||||||
|
assert.equal(arrivalRotationPassed(result), false);
|
||||||
|
assert.equal(f.resources.listeners, 0);
|
||||||
|
assert.equal(f.resources.timers, 0);
|
||||||
|
f.frame(1000, { shown: true, rotationError: 0 });
|
||||||
|
assert.equal(result.flipFrame, -1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a late visible frame fails even if the timeout callback is delayed', async () => {
|
||||||
|
const f = fixture();
|
||||||
|
f.frame(8, { shown: true, rotationError: 0 });
|
||||||
|
f.frame(501, { shown: true, rotationError: 0 });
|
||||||
|
const result = await f.observation;
|
||||||
|
assert.match(result.error, /reveal.*500 ms/);
|
||||||
|
assert.equal(result.elapsedMs, 501);
|
||||||
|
assert.equal(arrivalRotationPassed(result), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the deadline includes moveEnd setup rather than restarting when hidden', async () => {
|
||||||
|
const f = fixture();
|
||||||
|
f.frame(400, { shown: true, rotationError: 0 });
|
||||||
|
f.frame(600, { shown: true, rotationError: 0 });
|
||||||
|
const result = await f.observation;
|
||||||
|
assert.equal(result.moveEndMs, 400);
|
||||||
|
assert.equal(result.elapsedMs, 600);
|
||||||
|
assert.equal(arrivalRotationPassed(result), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('missing moveEnd frames fail without waiting for a render callback', async () => {
|
||||||
|
const f = fixture();
|
||||||
|
f.expire(500);
|
||||||
|
const result = await f.observation;
|
||||||
|
assert.match(result.error, /moveEnd.*500 ms/);
|
||||||
|
assert.equal(arrivalRotationPassed(result), false);
|
||||||
|
assert.equal(f.resources.listeners, 0);
|
||||||
|
assert.equal(f.resources.timers, 0);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user