mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-13 23:02:22 +00:00
Share one render loop fixture across the render failure tests
Four test files built the same widget on CesiumWidget.prototype, viewer proxy, store and notice; renderLoopFixture.mjs now does it once. The notice test drives Cesium.Event instead of a hand-made double. frameEvents() drops tileset.tileLoad, which Cesium raises inside its own error handling, and the notice no longer guards a viewer without a canvas or requestRender.
This commit is contained in:
1 parent
df41c0affb
commit
73bd952736
8 files changed
+127
-252
No files matched your search
@@ -563,6 +563,7 @@
|
||||
"src/ui/renderFailureEventRetry.test.mjs",
|
||||
"src/app/renderFailureInlineRetry.test.mjs",
|
||||
"src/ui/renderFailureSecondLoop.test.mjs",
|
||||
"src/ui/renderLoopFixture.mjs",
|
||||
"src/layers/cctv/lifecycle.js",
|
||||
"src/layers/cctv/controls.js",
|
||||
"src/layers/cctv/geometry.js",
|
||||
|
||||
@@ -5,10 +5,9 @@ import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as Cesium from 'cesium';
|
||||
import { keepPanelRendering } from './embed.js';
|
||||
import { installRenderFailureNotice } from '../ui/renderFailure.js';
|
||||
import { renderLoopFixture } from '../ui/renderLoopFixture.mjs';
|
||||
|
||||
function setup() {
|
||||
const frames = [];
|
||||
let tick = null;
|
||||
let clock = 0;
|
||||
// The host reports the panel hidden: its own animation frames never arrive.
|
||||
@@ -18,8 +17,6 @@ function setup() {
|
||||
setInterval: (fn) => ((tick = fn), 1),
|
||||
clearInterval() {},
|
||||
};
|
||||
const previousRaf = globalThis.requestAnimationFrame;
|
||||
globalThis.requestAnimationFrame = (fn) => frames.push(fn);
|
||||
const previousError = console.error;
|
||||
console.error = () => {};
|
||||
const state = { failure: null, drawn: 0 };
|
||||
@@ -40,66 +37,33 @@ function setup() {
|
||||
this.postRender.raiseEvent(this);
|
||||
},
|
||||
};
|
||||
const widget = Object.create(Cesium.CesiumWidget.prototype);
|
||||
Object.assign(widget, {
|
||||
_scene: scene,
|
||||
_clock: { tick() {} },
|
||||
_canRender: true,
|
||||
_useDefaultRenderLoop: false,
|
||||
_renderLoopRunning: false,
|
||||
_showRenderLoopErrors: false,
|
||||
_targetFrameRate: undefined,
|
||||
isDestroyed: () => false,
|
||||
resize() {},
|
||||
});
|
||||
// CesiumWidget's own renderError handler, which marks the loop stopped.
|
||||
scene.renderError.addEventListener(() => {
|
||||
widget._useDefaultRenderLoop = false;
|
||||
widget._renderLoopRunning = false;
|
||||
});
|
||||
const viewer = {
|
||||
scene,
|
||||
cesiumWidget: widget,
|
||||
canvas: { addEventListener() {}, removeEventListener() {} },
|
||||
get useDefaultRenderLoop() {
|
||||
return widget.useDefaultRenderLoop;
|
||||
},
|
||||
set useDefaultRenderLoop(value) {
|
||||
widget.useDefaultRenderLoop = value;
|
||||
},
|
||||
isDestroyed: () => false,
|
||||
render: () => widget.render(),
|
||||
resize: () => widget.resize(),
|
||||
};
|
||||
const store = {
|
||||
renderFailure: null,
|
||||
publishRenderFailure(next) {
|
||||
this.renderFailure = next;
|
||||
},
|
||||
};
|
||||
const uninstall = installRenderFailureNotice(viewer, {
|
||||
store,
|
||||
allowReload: false,
|
||||
locationRef: { href: 'https://gev.example/', reload() {}, assign() {} },
|
||||
logError() {},
|
||||
});
|
||||
const { frames, widget, viewer, store, runFrames, restore } =
|
||||
renderLoopFixture(scene, {
|
||||
viewer: {
|
||||
isDestroyed: () => false,
|
||||
render: () => widget.render(),
|
||||
resize: () => widget.resize(),
|
||||
},
|
||||
notice: { allowReload: false },
|
||||
});
|
||||
const stop = keepPanelRendering(viewer, { windowRef, now: () => clock });
|
||||
viewer.useDefaultRenderLoop = true;
|
||||
const timerFrame = () => {
|
||||
clock += 400;
|
||||
tick();
|
||||
};
|
||||
const animationFrames = (n) => {
|
||||
for (let frame = 1; frame <= n; frame += 1)
|
||||
for (const render of frames.splice(0)) render(frame * 16);
|
||||
return {
|
||||
frames,
|
||||
state,
|
||||
store,
|
||||
timerFrame,
|
||||
animationFrames: runFrames,
|
||||
restore: () => {
|
||||
stop();
|
||||
restore();
|
||||
console.error = previousError;
|
||||
},
|
||||
};
|
||||
const restore = () => {
|
||||
stop();
|
||||
uninstall();
|
||||
globalThis.requestAnimationFrame = previousRaf;
|
||||
console.error = previousError;
|
||||
};
|
||||
return { frames, state, store, timerFrame, animationFrames, restore };
|
||||
}
|
||||
|
||||
for (const failure of ['caught', 'thrown']) {
|
||||
|
||||
@@ -21,7 +21,7 @@ export function installRenderFailureNotice(
|
||||
} = {},
|
||||
) {
|
||||
const { scene } = viewer;
|
||||
const canvas = viewer.canvas ?? scene.canvas;
|
||||
const { canvas } = viewer;
|
||||
let notice = null;
|
||||
let contextLost = false;
|
||||
// With rethrowRenderErrors, Scene throws the error it has just raised;
|
||||
@@ -36,7 +36,7 @@ export function installRenderFailureNotice(
|
||||
return;
|
||||
}
|
||||
viewer.useDefaultRenderLoop = true;
|
||||
scene.requestRender?.();
|
||||
scene.requestRender();
|
||||
}
|
||||
|
||||
function reset() {
|
||||
@@ -113,11 +113,11 @@ export function installRenderFailureNotice(
|
||||
raisedByScene = null;
|
||||
}),
|
||||
);
|
||||
canvas?.addEventListener?.('webglcontextlost', onContextLost);
|
||||
canvas.addEventListener('webglcontextlost', onContextLost);
|
||||
return () => {
|
||||
removeRenderError();
|
||||
unguard.forEach((restore) => restore());
|
||||
canvas?.removeEventListener?.('webglcontextlost', onContextLost);
|
||||
canvas.removeEventListener('webglcontextlost', onContextLost);
|
||||
clear();
|
||||
};
|
||||
}
|
||||
@@ -147,7 +147,6 @@ function frameEvents(viewer) {
|
||||
tileset?.loadProgress,
|
||||
tileset?.allTilesLoaded,
|
||||
tileset?.initialTilesLoaded,
|
||||
tileset?.tileLoad,
|
||||
);
|
||||
}
|
||||
return events;
|
||||
|
||||
@@ -1,27 +1,14 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as Cesium from 'cesium';
|
||||
import { installRenderFailureNotice } from './renderFailure.js';
|
||||
|
||||
function event() {
|
||||
const listeners = new Set();
|
||||
return {
|
||||
listeners,
|
||||
addEventListener(listener) {
|
||||
listeners.add(listener);
|
||||
return () => listeners.delete(listener);
|
||||
},
|
||||
raise(...args) {
|
||||
for (const listener of [...listeners]) listener(...args);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function fixture(t, options = {}) {
|
||||
const canvasListeners = new Map();
|
||||
const scene = {
|
||||
renderError: event(),
|
||||
preUpdate: event(),
|
||||
postRender: event(),
|
||||
renderError: new Cesium.Event(),
|
||||
preUpdate: new Cesium.Event(),
|
||||
postRender: new Cesium.Event(),
|
||||
requestRender() {
|
||||
this.requests = (this.requests ?? 0) + 1;
|
||||
},
|
||||
@@ -62,12 +49,12 @@ function fixture(t, options = {}) {
|
||||
// One Cesium frame: preUpdate, an optional render error, then postRender,
|
||||
// which Cesium raises after a failed frame too.
|
||||
const frame = (error) => {
|
||||
scene.preUpdate.raise(scene);
|
||||
scene.preUpdate.raiseEvent(scene);
|
||||
if (error) {
|
||||
viewer.useDefaultRenderLoop = false;
|
||||
scene.renderError.raise(scene, error);
|
||||
scene.renderError.raiseEvent(scene, error);
|
||||
}
|
||||
scene.postRender.raise(scene);
|
||||
scene.postRender.raiseEvent(scene);
|
||||
};
|
||||
return {
|
||||
viewer,
|
||||
@@ -105,8 +92,8 @@ test('Retry restarts the render loop and a clean frame clears the notice', (t) =
|
||||
f.frame();
|
||||
assert.equal(f.store.renderFailure, null);
|
||||
// Clearing stops the per-frame listeners.
|
||||
assert.equal(f.scene.preUpdate.listeners.size, 0);
|
||||
assert.equal(f.scene.postRender.listeners.size, 0);
|
||||
assert.equal(f.scene.preUpdate.numberOfListeners, 0);
|
||||
assert.equal(f.scene.postRender.numberOfListeners, 0);
|
||||
});
|
||||
|
||||
test('a loop restarted into the same error keeps one notice', (t) => {
|
||||
@@ -137,7 +124,7 @@ test('a new failure after recovery is a new appearance', (t) => {
|
||||
test('a render error without a value shows no detail', (t) => {
|
||||
const f = fixture(t);
|
||||
f.viewer.useDefaultRenderLoop = false;
|
||||
f.scene.renderError.raise(f.scene, undefined);
|
||||
f.scene.renderError.raiseEvent(f.scene, undefined);
|
||||
assert.equal(f.store.renderFailure.reason, 'render-error');
|
||||
assert.equal(f.store.renderFailure.detail, '');
|
||||
});
|
||||
@@ -179,8 +166,8 @@ test('uninstall removes the listeners and the notice', (t) => {
|
||||
f.frame(new Error('boom'));
|
||||
f.uninstall();
|
||||
assert.equal(f.store.renderFailure, null);
|
||||
assert.equal(f.scene.renderError.listeners.size, 0);
|
||||
assert.equal(f.scene.postRender.listeners.size, 0);
|
||||
assert.equal(f.scene.renderError.numberOfListeners, 0);
|
||||
assert.equal(f.scene.postRender.numberOfListeners, 0);
|
||||
assert.equal(f.canvasListeners.size, 0);
|
||||
f.frame(new Error('later'));
|
||||
assert.equal(f.store.renderFailure, null);
|
||||
|
||||
@@ -5,11 +5,9 @@ import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as Cesium from 'cesium';
|
||||
import { installRenderFailureNotice } from './renderFailure.js';
|
||||
import { renderLoopFixture } from './renderLoopFixture.mjs';
|
||||
|
||||
function setup() {
|
||||
const frames = [];
|
||||
const previousRaf = globalThis.requestAnimationFrame;
|
||||
globalThis.requestAnimationFrame = (fn) => frames.push(fn);
|
||||
// Scene.render as in Scene.js:4369: preUpdate always, preRender/postRender
|
||||
// only when a frame is rendered (requestRenderMode idle skips them).
|
||||
const scene = {
|
||||
@@ -34,53 +32,7 @@ function setup() {
|
||||
this.postRender.raiseEvent(this, time);
|
||||
},
|
||||
};
|
||||
const widget = Object.create(Cesium.CesiumWidget.prototype);
|
||||
Object.assign(widget, {
|
||||
_scene: scene,
|
||||
_clock: { tick() {} },
|
||||
_canRender: true,
|
||||
_useDefaultRenderLoop: false,
|
||||
_renderLoopRunning: false,
|
||||
_showRenderLoopErrors: false,
|
||||
_targetFrameRate: undefined,
|
||||
isDestroyed: () => false,
|
||||
resize() {},
|
||||
});
|
||||
scene.renderError.addEventListener(() => {
|
||||
widget._useDefaultRenderLoop = false;
|
||||
widget._renderLoopRunning = false;
|
||||
});
|
||||
const viewer = {
|
||||
scene,
|
||||
cesiumWidget: widget,
|
||||
canvas: { addEventListener() {}, removeEventListener() {} },
|
||||
get useDefaultRenderLoop() {
|
||||
return widget.useDefaultRenderLoop;
|
||||
},
|
||||
set useDefaultRenderLoop(value) {
|
||||
widget.useDefaultRenderLoop = value;
|
||||
},
|
||||
};
|
||||
const store = {
|
||||
renderFailure: null,
|
||||
publishRenderFailure(next) {
|
||||
this.renderFailure = next;
|
||||
},
|
||||
};
|
||||
const uninstall = installRenderFailureNotice(viewer, {
|
||||
store,
|
||||
locationRef: { href: 'https://gev.example/', reload() {}, assign() {} },
|
||||
logError() {},
|
||||
});
|
||||
const runFrames = (n) => {
|
||||
for (let i = 0; i < n; i += 1)
|
||||
for (const fn of frames.splice(0)) fn(16 * (i + 1));
|
||||
};
|
||||
const restore = () => {
|
||||
uninstall();
|
||||
globalThis.requestAnimationFrame = previousRaf;
|
||||
};
|
||||
return { frames, scene, widget, viewer, store, runFrames, restore };
|
||||
return { scene, ...renderLoopFixture(scene) };
|
||||
}
|
||||
|
||||
for (const eventName of ['postRender', 'preUpdate']) {
|
||||
@@ -200,7 +152,6 @@ const FRAME_EVENTS = {
|
||||
'tileset.allTilesLoaded': (v) => v.scene.primitives.get(0).allTilesLoaded,
|
||||
'tileset.initialTilesLoaded': (v) =>
|
||||
v.scene.primitives.get(0).initialTilesLoaded,
|
||||
'tileset.tileLoad': (v) => v.scene.primitives.get(0).tileLoad,
|
||||
};
|
||||
|
||||
for (const [name, pick] of Object.entries(FRAME_EVENTS)) {
|
||||
@@ -214,7 +165,6 @@ for (const [name, pick] of Object.entries(FRAME_EVENTS)) {
|
||||
loadProgress: new Cesium.Event(),
|
||||
allTilesLoaded: new Cesium.Event(),
|
||||
initialTilesLoaded: new Cesium.Event(),
|
||||
tileLoad: new Cesium.Event(),
|
||||
};
|
||||
const viewer = {
|
||||
scene: {
|
||||
|
||||
@@ -3,66 +3,21 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as Cesium from 'cesium';
|
||||
import { installRenderFailureNotice } from './renderFailure.js';
|
||||
|
||||
// What src/app/scene.js passes to the viewer (showRenderLoopErrors).
|
||||
const SHOW_RENDER_LOOP_ERRORS = false;
|
||||
import { renderLoopFixture } from './renderLoopFixture.mjs';
|
||||
|
||||
function runLoop(render) {
|
||||
const frames = [];
|
||||
const previousRaf = globalThis.requestAnimationFrame;
|
||||
globalThis.requestAnimationFrame = (fn) => frames.push(fn);
|
||||
try {
|
||||
const scene = {
|
||||
renderError: new Cesium.Event(),
|
||||
preUpdate: new Cesium.Event(),
|
||||
postRender: new Cesium.Event(),
|
||||
requestRender() {},
|
||||
initializeFrame() {},
|
||||
render,
|
||||
};
|
||||
const widget = Object.create(Cesium.CesiumWidget.prototype);
|
||||
Object.assign(widget, {
|
||||
_scene: scene,
|
||||
_clock: { tick() {} },
|
||||
_canRender: true,
|
||||
_useDefaultRenderLoop: false,
|
||||
_renderLoopRunning: false,
|
||||
_showRenderLoopErrors: SHOW_RENDER_LOOP_ERRORS,
|
||||
_targetFrameRate: undefined,
|
||||
isDestroyed: () => false,
|
||||
resize() {},
|
||||
showErrorPanel() {},
|
||||
});
|
||||
const viewer = {
|
||||
scene,
|
||||
cesiumWidget: widget,
|
||||
canvas: { addEventListener() {}, removeEventListener() {} },
|
||||
get useDefaultRenderLoop() {
|
||||
return widget.useDefaultRenderLoop;
|
||||
},
|
||||
set useDefaultRenderLoop(value) {
|
||||
widget.useDefaultRenderLoop = value;
|
||||
},
|
||||
};
|
||||
const store = {
|
||||
renderFailure: null,
|
||||
publishRenderFailure(next) {
|
||||
this.renderFailure = next;
|
||||
},
|
||||
};
|
||||
const logged = [];
|
||||
const uninstall = installRenderFailureNotice(viewer, {
|
||||
store,
|
||||
locationRef: { href: 'https://gev.example/', reload() {}, assign() {} },
|
||||
logError: (error) => logged.push(error),
|
||||
});
|
||||
viewer.useDefaultRenderLoop = true;
|
||||
while (frames.length) frames.shift()(16);
|
||||
return { widget, store, logged, uninstall };
|
||||
} finally {
|
||||
globalThis.requestAnimationFrame = previousRaf;
|
||||
}
|
||||
const scene = {
|
||||
renderError: new Cesium.Event(),
|
||||
preUpdate: new Cesium.Event(),
|
||||
postRender: new Cesium.Event(),
|
||||
requestRender() {},
|
||||
initializeFrame() {},
|
||||
render,
|
||||
};
|
||||
const fixture = renderLoopFixture(scene);
|
||||
fixture.viewer.useDefaultRenderLoop = true;
|
||||
fixture.runFrames(3);
|
||||
return { ...fixture, uninstall: fixture.restore };
|
||||
}
|
||||
|
||||
test('an error thrown outside tryAndCatchError still shows the notice', () => {
|
||||
|
||||
@@ -4,12 +4,9 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as Cesium from 'cesium';
|
||||
import { installRenderFailureNotice } from './renderFailure.js';
|
||||
import { renderLoopFixture } from './renderLoopFixture.mjs';
|
||||
|
||||
function setup({ targetFrameRate } = {}) {
|
||||
const frames = [];
|
||||
const previousRaf = globalThis.requestAnimationFrame;
|
||||
globalThis.requestAnimationFrame = (fn) => frames.push(fn);
|
||||
const scene = {
|
||||
renderError: new Cesium.Event(),
|
||||
preUpdate: new Cesium.Event(),
|
||||
@@ -29,57 +26,7 @@ function setup({ targetFrameRate } = {}) {
|
||||
this.postRender.raiseEvent(this, time);
|
||||
},
|
||||
};
|
||||
const widget = Object.create(Cesium.CesiumWidget.prototype);
|
||||
Object.assign(widget, {
|
||||
_scene: scene,
|
||||
_clock: { tick() {} },
|
||||
_canRender: true,
|
||||
_useDefaultRenderLoop: false,
|
||||
_renderLoopRunning: false,
|
||||
_showRenderLoopErrors: false,
|
||||
_targetFrameRate: targetFrameRate,
|
||||
isDestroyed: () => false,
|
||||
resize() {},
|
||||
});
|
||||
// CesiumWidget's own renderError handler (CesiumWidget.js:414).
|
||||
scene.renderError.addEventListener(() => {
|
||||
widget._useDefaultRenderLoop = false;
|
||||
widget._renderLoopRunning = false;
|
||||
});
|
||||
const viewer = {
|
||||
scene,
|
||||
cesiumWidget: widget,
|
||||
canvas: { addEventListener() {}, removeEventListener() {} },
|
||||
get useDefaultRenderLoop() {
|
||||
return widget.useDefaultRenderLoop;
|
||||
},
|
||||
set useDefaultRenderLoop(value) {
|
||||
widget.useDefaultRenderLoop = value;
|
||||
},
|
||||
};
|
||||
const store = {
|
||||
renderFailure: null,
|
||||
publishRenderFailure(next) {
|
||||
this.renderFailure = next;
|
||||
},
|
||||
};
|
||||
const uninstall = installRenderFailureNotice(viewer, {
|
||||
store,
|
||||
locationRef: { href: 'https://gev.example/', reload() {}, assign() {} },
|
||||
logError() {},
|
||||
});
|
||||
let time = 0;
|
||||
const runFrames = (n) => {
|
||||
for (let i = 0; i < n; i += 1) {
|
||||
time += 17;
|
||||
for (const fn of frames.splice(0)) fn(time);
|
||||
}
|
||||
};
|
||||
const restore = () => {
|
||||
uninstall();
|
||||
globalThis.requestAnimationFrame = previousRaf;
|
||||
};
|
||||
return { frames, scene, viewer, store, runFrames, restore };
|
||||
return { scene, ...renderLoopFixture(scene, { targetFrameRate }) };
|
||||
}
|
||||
|
||||
for (const targetFrameRate of [undefined, 60]) {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// A widget on Cesium's real CesiumWidget.prototype (startRenderLoop, the
|
||||
// useDefaultRenderLoop setter, render) around a scene double, with animation
|
||||
// frames queued by hand and the render failure notice installed.
|
||||
import * as Cesium from 'cesium';
|
||||
import { installRenderFailureNotice } from './renderFailure.js';
|
||||
|
||||
export function renderLoopFixture(
|
||||
scene,
|
||||
{ targetFrameRate, viewer: viewerExtras = {}, notice = {} } = {},
|
||||
) {
|
||||
const frames = [];
|
||||
const previousRaf = globalThis.requestAnimationFrame;
|
||||
globalThis.requestAnimationFrame = (fn) => frames.push(fn);
|
||||
const widget = Object.create(Cesium.CesiumWidget.prototype);
|
||||
Object.assign(widget, {
|
||||
_scene: scene,
|
||||
_clock: { tick() {} },
|
||||
_canRender: true,
|
||||
_useDefaultRenderLoop: false,
|
||||
_renderLoopRunning: false,
|
||||
// What src/app/scene.js passes to the viewer.
|
||||
_showRenderLoopErrors: false,
|
||||
_targetFrameRate: targetFrameRate,
|
||||
isDestroyed: () => false,
|
||||
resize() {},
|
||||
});
|
||||
// CesiumWidget's own renderError handler, which marks the loop stopped.
|
||||
scene.renderError.addEventListener(() => {
|
||||
widget._useDefaultRenderLoop = false;
|
||||
widget._renderLoopRunning = false;
|
||||
});
|
||||
const viewer = Object.assign(
|
||||
{
|
||||
scene,
|
||||
cesiumWidget: widget,
|
||||
canvas: { addEventListener() {}, removeEventListener() {} },
|
||||
get useDefaultRenderLoop() {
|
||||
return widget.useDefaultRenderLoop;
|
||||
},
|
||||
set useDefaultRenderLoop(value) {
|
||||
widget.useDefaultRenderLoop = value;
|
||||
},
|
||||
},
|
||||
viewerExtras,
|
||||
);
|
||||
const store = {
|
||||
renderFailure: null,
|
||||
publishRenderFailure(next) {
|
||||
this.renderFailure = next;
|
||||
},
|
||||
};
|
||||
const logged = [];
|
||||
const uninstall = installRenderFailureNotice(viewer, {
|
||||
store,
|
||||
locationRef: { href: 'https://gev.example/', reload() {}, assign() {} },
|
||||
logError: (error) => logged.push(error),
|
||||
...notice,
|
||||
});
|
||||
let time = 0;
|
||||
const runFrames = (n) => {
|
||||
for (let i = 0; i < n; i += 1) {
|
||||
// Above a 60 fps target's interval, so every frame draws.
|
||||
time += 17;
|
||||
for (const fn of frames.splice(0)) fn(time);
|
||||
}
|
||||
};
|
||||
const restore = () => {
|
||||
uninstall();
|
||||
globalThis.requestAnimationFrame = previousRaf;
|
||||
};
|
||||
return { frames, widget, viewer, store, logged, runFrames, restore };
|
||||
}
|
||||
Reference in new issue
Block a user