import { describe, expect, it, vi } from 'vitest'; import { INTERACTIVE_PREVIEW_CELL_SIZE, PREVIEW_CELL_SIZE, REFINE_DELAY_MS } from './rendering'; import { ScenePresentationController } from './scene-presentation-controller'; import { sceneContentSignature } from './scene-demand'; import type { GraphDocument2d, RenderScene2d, SamplingMetadata, SceneLayer } from './types'; const document: GraphDocument2d = { viewport: { x_min: -4, x_max: 4, y_min: -3, y_max: 3, width: 800, height: 600 }, expressions: [ { id: 'curve', input_mode: 'text', source: 'y=sin(x)', enabled: true, color: '#f00' } ] }; const sampling: SamplingMetadata = { strategy: 'marching_squares', target_samples: 0, vertex_count: 0, segment_count: 0, max_depth: null, max_depth_reached: false, vertex_budget: null, vertex_budget_reached: false, warnings: [] }; function planDocument( planDocument: GraphDocument2d, interactive: boolean, preserveCameraSceneOnSettle = false, retainedSceneNeedsRefinement = false ) { return { document: planDocument, contentSignature: sceneContentSignature(planDocument), interactive, preserveCameraSceneOnSettle, retainedSceneNeedsRefinement }; } function scene(viewport = document.viewport): RenderScene2d { return { viewport, coverage: viewport, layers: [], diagnostics: [], variables: [], expression_roles: {}, model_revisions: {} }; } function sceneWithLayers(layers: SceneLayer[]): RenderScene2d { return { ...scene(), layers }; } function deferred() { let resolve!: (value: T) => void; let reject!: (error: unknown) => void; const promise = new Promise((resolvePromise, rejectPromise) => { resolve = resolvePromise; reject = rejectPromise; }); return { promise, resolve, reject }; } function harness() { const builds: Array<{ document: GraphDocument2d; result: ReturnType> }> = []; const timers = new Map void; delayMs: number }>(); let nextTimer = 1; const publish = vi.fn(); const setBusy = vi.fn(); const setError = vi.fn(); const controller = new ScenePresentationController({ publish, setBusy, setError, build: (requestDocument) => { const result = deferred(); builds.push({ document: requestDocument, result }); return result.promise; }, setTimer: (callback, delayMs) => { const handle = nextTimer++; timers.set(handle, { callback, delayMs }); return handle; }, clearTimer: (handle) => { timers.delete(handle); } }); return { controller, builds, timers, publish, setBusy, setError }; } describe('ScenePresentationController', () => { it('keeps a spatially tiled preview without a redundant whole-scene refinement', async () => { const test = harness(); test.controller.setPlan(planDocument(document, false)); expect(test.builds[0].document.grid_cell_size).toBe(PREVIEW_CELL_SIZE); test.builds[0].result.resolve( sceneWithLayers([ { type: 'polyline', geometry_id: 1, expression_id: 'curve', vertices: [], breaks: [], color: '#f00', line_style: 'solid', source_kind: 'implicit', sampling: sampling } ]) ); await test.builds[0].result.promise; expect(test.publish).toHaveBeenCalledWith(expect.any(Object), document); expect(test.timers.size).toBe(0); }); it('refines a general polar classifier after its preview is available', async () => { const test = harness(); test.controller.setPlan(planDocument(document, false)); test.builds[0].result.resolve( sceneWithLayers([ { type: 'fill', geometry_id: 1, expression_id: 'curve', rectangles: [[-1, 1, -1, 1]], color: '#f00' }, { type: 'polyline', geometry_id: 2, expression_id: 'curve', vertices: [], breaks: [], color: '#f00', line_style: 'solid', source_kind: 'polar_inequality', sampling } ]) ); await test.builds[0].result.promise; expect([...test.timers.values()][0].delayMs).toBe(REFINE_DELAY_MS); [...test.timers.values()][0].callback(); expect(test.builds[1].document).toBe(document); }); it('uses only a bounded preview while interaction remains active', () => { const test = harness(); test.controller.setPlan(planDocument(document, true)); expect(test.builds[0].document.grid_cell_size).toBe(INTERACTIVE_PREVIEW_CELL_SIZE); expect(test.timers.size).toBe(0); }); it('preserves the camera scene and schedules immediate refinement on settle', () => { const test = harness(); test.controller.setPlan(planDocument(document, false, true, true)); expect(test.builds).toHaveLength(0); expect([...test.timers.values()][0].delayMs).toBe(0); [...test.timers.values()][0].callback(); expect(test.builds[0].document).toBe(document); }); it('clears busy when a preserved settled scene needs no work', () => { const test = harness(); test.controller.setPlan(planDocument(document, false, true, false)); expect(test.builds).toHaveLength(0); expect(test.timers.size).toBe(0); expect(test.setBusy).toHaveBeenLastCalledWith(false); }); it('does not let an older completion replace a newer presented scene', async () => { const test = harness(); const firstDocument = document; const secondDocument = { ...document, viewport: { ...document.viewport, x_min: -2, x_max: 6 } }; test.controller.setPlan(planDocument(firstDocument, true)); test.controller.setPlan(planDocument(secondDocument, true)); test.builds[1].result.resolve(scene(secondDocument.viewport)); await test.builds[1].result.promise; test.builds[0].result.resolve(scene(firstDocument.viewport)); await test.builds[0].result.promise; expect(test.publish).toHaveBeenCalledTimes(1); expect(test.publish.mock.calls[0][1]).toBe(secondDocument); }); it('rejects live-model completions after a different model settles', async () => { const test = harness(); const animatedDocument = { ...document, variable_overrides: { b: 4 } }; const settledDocument = { ...document, expressions: document.expressions.map((expression) => ({ ...expression, source: expression.id === 'curve' ? 'b := 2' : expression.source })) }; test.controller.setPlan(planDocument(animatedDocument, true)); test.controller.setPlan(planDocument(settledDocument, false)); test.builds[0].result.resolve(scene()); await test.builds[0].result.promise; expect(test.publish).not.toHaveBeenCalled(); }); it('reports errors only for the current plan', async () => { const test = harness(); test.controller.setPlan(planDocument(document, true)); test.controller.setPlan(undefined); test.builds[0].result.reject(new Error('obsolete')); await expect(test.builds[0].result.promise).rejects.toThrow('obsolete'); expect(test.setError).not.toHaveBeenCalled(); expect(test.setBusy).toHaveBeenLastCalledWith(false); }); });