import assert from 'node:assert/strict' import { createRegionLoadingController } from '../../src/modules/cockpit/services/regionLoading.js' function event() { const listeners = new Set() return { addEventListener(fn) { listeners.add(fn); return () => listeners.delete(fn) }, raise(value) { for (const fn of listeners) fn(value) }, } } async function harness({ count = 2, budget = 1 } = {}) { let clock = 0, timer = null, renderRequested = false const models = new Map(), removed = [] const regions = Array.from({ length: count }, (_, index) => ({ id: count - index, contentCount: 1, decodedGeometryBytes: 1024, })) const controller = createRegionLoadingController({ regions, showBudgetPerFrame: budget, maxConcurrentLoads: count, load: async region => { const model = { show: false, tilesLoaded: false, tileVisible: event() } models.set(region.id, model) return model }, add() {}, remove(model) { removed.push(model) }, requestRender() { renderRequested = true }, distance: region => region.id * 10, readCameraKey: () => 'fixed', now: () => clock, setTimer(callback) { timer = callback; return 1 }, clearTimer() { timer = null }, }) controller.refresh() await Promise.resolve() for (const model of models.values()) model.tilesLoaded = true return { controller, models, removed, shown: () => [...models].filter(([, model]) => model.show).map(([id]) => id).sort((a, b) => a - b), renderPending: () => renderRequested, frame(at) { clock = at; renderRequested = false; controller.onRender() }, timer(at) { clock = at; const callback = timer; timer = null; callback?.() }, } } // 199ms postRender + 200ms refresh occur before the next 216ms render. // A scheduling callback is not a rendered frame, even after a long idle gap. for (const budget of [1, 2]) { const h = await harness({ count: budget * 2, budget }) try { const nearest = Array.from({ length: budget }, (_, index) => index + 1) h.frame(199) assert.deepEqual(h.shown(), nearest, 'First-show slots go to nearest ready regions') assert.equal(h.renderPending(), true, 'First show requests the next render to advance deferred work') h.timer(200) assert.deepEqual(h.shown(), nearest, 'Timer must share the current frame show budget') h.timer(400) h.controller.refresh() assert.deepEqual(h.shown(), nearest, 'Elapsed time and explicit refresh do not grant frame slots') for (let id = budget + 1; id <= budget * 2; id++) { assert.equal(h.controller.detailReady(id), false, 'Deferred regions retain coarse fallback') } // A new frame, not wall-clock advancement, grants the next budget. h.frame(400) assert.equal(h.shown().length, budget * 2, 'Ready work progresses on the next frame') } finally { h.controller.detach() } } // A newly shown model must actually render before its coarse fallback yields. { const h = await harness() try { h.frame(199) h.timer(200) assert.equal(h.controller.detailReady(1), false) h.models.get(1).tileVisible.raise({ content: { url: 'near', trianglesLength: 1 } }) h.frame(216) assert.equal(h.controller.detailReady(1), true) assert.equal(h.controller.detailReady(2), false) h.models.get(2).tileVisible.raise({ content: { url: 'far', trianglesLength: 1 } }) h.frame(233) assert.equal(h.controller.detailReady(2), true) } finally { h.controller.detach() } } // Exhausted show slots must not throttle pipeline release or resurrect disposed work. { const h = await harness() h.frame(199) h.controller.setPipeline(true) assert.equal(h.removed.length, 2, 'Pipeline change releases every loaded model immediately') assert.equal(h.controller.detailReady(1), false) assert.equal(h.controller.detailReady(2), false) h.controller.detach() h.timer(400) h.frame(416) h.controller.refresh() assert.equal(h.models.size, 2, 'Disposed controller does not start another model') assert.equal(h.removed.length, 2, 'Each model is removed exactly once') } console.log('PASS region frame budget: timer/render interleaving, nearest-first, progress, coarse handoff, disposal')