Back to home page

EIC code displayed by LXR

 
 

    


Warning, /firebird/packages/firebird-ng/display/src/lib/services/three.service.spec.ts is written in an unsupported language. File is not indexed.

0001 import { TestBed } from '@angular/core/testing';
0002 import { ThreeService } from './three.service';
0003 import { PerfService } from './perf.service';
0004 import type { ThreeExtension } from '@dexvis/firebird-ng/api';
0005 
0006 /** A promise with its resolve/reject handles. */
0007 function deferred() {
0008   let resolve!: () => void;
0009   let reject!: (error: unknown) => void;
0010   const promise = new Promise<void>((res, rej) => { resolve = () => res(); reject = rej; });
0011   return { promise, resolve, reject };
0012 }
0013 
0014 /** The private members these specs drive. */
0015 type ThreeInternals = {
0016   createScene: (element: HTMLElement) => Promise<void>;
0017   attachRenderer: (element: HTMLElement) => void;
0018   startRendering: () => void;
0019   stopRendering: () => void;
0020   renderLoop: () => void;
0021   initialized: boolean;
0022   shouldRender: boolean;
0023   renderRequested: boolean;
0024   animationFrameId: number | null;
0025   views: unknown[];
0026   renderer: unknown;
0027   extensions: ThreeExtension[];
0028   frameContext: unknown;
0029   frameCallbacks: Array<() => void>;
0030 };
0031 
0032 describe('ThreeService', () => {
0033   let service: ThreeService;
0034   let internals: ThreeInternals;
0035 
0036   beforeEach(() => {
0037     TestBed.configureTestingModule({
0038       providers: [ThreeService, { provide: PerfService, useValue: { updateStats: () => undefined } }],
0039     });
0040     service = TestBed.inject(ThreeService);
0041     internals = service as unknown as ThreeInternals;
0042   });
0043 
0044   it('should throw error if container not found when using string id', async () => {
0045     // This test doesn't require WebGPU - it fails before renderer creation
0046     await expect(service.init('nonexistent-id')).rejects.toThrow(/Container element #nonexistent-id not found/);
0047   });
0048 
0049   describe('init re-entrancy (scene creation stubbed: no WebGPU in tests)', () => {
0050     let creation: ReturnType<typeof deferred>;
0051     let createScene: ReturnType<typeof vi.fn>;
0052     let attachRenderer: ReturnType<typeof vi.fn>;
0053     let startRendering: ReturnType<typeof vi.fn>;
0054 
0055     beforeEach(() => {
0056       creation = deferred();
0057       createScene = vi.fn(() => creation.promise);
0058       attachRenderer = vi.fn();
0059       startRendering = vi.fn();
0060       internals.createScene = createScene as unknown as ThreeInternals['createScene'];
0061       internals.attachRenderer = attachRenderer as unknown as ThreeInternals['attachRenderer'];
0062       internals.startRendering = startRendering as unknown as ThreeInternals['startRendering'];
0063     });
0064 
0065     it('creates the scene once for concurrent calls, and only the latest call attaches', async () => {
0066       const first = document.createElement('div');
0067       const second = document.createElement('div');
0068       const firstInit = service.init(first);
0069       const secondInit = service.init(second);
0070       creation.resolve();
0071 
0072       expect(await firstInit).toBe(false);
0073       expect(await secondInit).toBe(true);
0074       expect(createScene).toHaveBeenCalledTimes(1);
0075       expect(createScene).toHaveBeenCalledWith(first);
0076       expect(attachRenderer).toHaveBeenCalledTimes(1);
0077       expect(attachRenderer).toHaveBeenCalledWith(second);
0078       expect(startRendering).toHaveBeenCalledTimes(1);
0079     });
0080 
0081     it('does not start the loop when the page detached while the scene was being created', async () => {
0082       const pending = service.init(document.createElement('div'));
0083       service.detach();
0084       creation.resolve();
0085 
0086       expect(await pending).toBe(false);
0087       expect(startRendering).not.toHaveBeenCalled();
0088     });
0089 
0090     it('re-attaches on a later visit without creating the scene again', async () => {
0091       creation.resolve();
0092       expect(await service.init(document.createElement('div'))).toBe(true);
0093       service.detach();
0094       const revisit = document.createElement('div');
0095       expect(await service.init(revisit)).toBe(true);
0096 
0097       expect(createScene).toHaveBeenCalledTimes(1);
0098       expect(attachRenderer).toHaveBeenCalledWith(revisit);
0099       expect(startRendering).toHaveBeenCalledTimes(2);
0100     });
0101 
0102     it('retries the creation after a failed one', async () => {
0103       const failing = service.init(document.createElement('div'));
0104       creation.reject(new Error('no adapter'));
0105       await expect(failing).rejects.toThrow('no adapter');
0106 
0107       createScene.mockImplementation(() => Promise.resolve());
0108       expect(await service.init(document.createElement('div'))).toBe(true);
0109       expect(createScene).toHaveBeenCalledTimes(2);
0110     });
0111   });
0112 
0113   describe('render loop isolation', () => {
0114     function stubFrame() {
0115       const view = {
0116         dirty: true,
0117         controls: { update: () => undefined },
0118         renderFullFrame: vi.fn(),
0119         renderOverlays: vi.fn(),
0120         dispose: vi.fn(),
0121       };
0122       internals.initialized = true;
0123       internals.views = [view];
0124       internals.renderer = { domElement: document.createElement('canvas'), dispose: vi.fn() };
0125       internals.frameContext = { deltaTime: 0, renderer: internals.renderer, camera: null, invalidate: () => undefined };
0126       return view;
0127     }
0128 
0129     function runOneFrame() {
0130       service.invalidate();
0131       internals.shouldRender = true;
0132       internals.renderLoop();
0133       internals.stopRendering();
0134     }
0135 
0136     beforeEach(() => {
0137       // The loop schedules its next tick; nothing must run behind the test.
0138       vi.stubGlobal('requestAnimationFrame', vi.fn(() => 1));
0139       vi.stubGlobal('cancelAnimationFrame', vi.fn());
0140     });
0141 
0142     afterEach(() => {
0143       internals.stopRendering();
0144       vi.unstubAllGlobals();
0145     });
0146 
0147     it('disables a throwing onFrame hook and keeps rendering and the other hooks', () => {
0148       const view = stubFrame();
0149       const errors = vi.spyOn(console, 'error').mockImplementation(() => undefined);
0150       const bad: ThreeExtension = { onFrame: vi.fn(() => { throw new Error('boom'); }) };
0151       const good: ThreeExtension = { onFrame: vi.fn() };
0152       internals.extensions = [bad, good];
0153 
0154       runOneFrame();
0155       runOneFrame();
0156 
0157       expect(bad.onFrame).toHaveBeenCalledTimes(1);
0158       expect(good.onFrame).toHaveBeenCalledTimes(2);
0159       expect(view.renderFullFrame).toHaveBeenCalledTimes(2);
0160       expect(service.renderedFrameCount).toBe(2);
0161       expect(errors).toHaveBeenCalledTimes(1);
0162       errors.mockRestore();
0163     });
0164 
0165     it('removes a throwing frame callback and keeps the others', () => {
0166       stubFrame();
0167       const errors = vi.spyOn(console, 'error').mockImplementation(() => undefined);
0168       const bad = vi.fn(() => { throw new Error('boom'); });
0169       const good = vi.fn();
0170       service.addFrameCallback(bad);
0171       service.addFrameCallback(good);
0172 
0173       runOneFrame();
0174       runOneFrame();
0175 
0176       expect(bad).toHaveBeenCalledTimes(1);
0177       expect(good).toHaveBeenCalledTimes(2);
0178       expect(internals.frameCallbacks).toEqual([good]);
0179       errors.mockRestore();
0180     });
0181 
0182     it('stays idle on demand when nothing invalidated', () => {
0183       const view = stubFrame();
0184       runOneFrame();
0185       view.dirty = false;
0186       internals.shouldRender = true;
0187       internals.renderLoop();
0188       internals.stopRendering();
0189       expect(view.renderFullFrame).toHaveBeenCalledTimes(1);
0190     });
0191   });
0192 
0193   // On-demand scheduling over several views: dirty flags decide WHETHER a
0194   // frame renders, and a rendered frame repaints EVERY view (a skipped view
0195   // would show cleared background once the frame is presented).
0196   describe('render scheduling with several views', () => {
0197     type StubView = { dirty: boolean; controls: { update: () => void }; renderTo: ReturnType<typeof vi.fn>; dispose: () => void };
0198 
0199     function stubViews(count: number): StubView[] {
0200       const views = Array.from({ length: count }, () => ({
0201         dirty: false,
0202         controls: { update: () => undefined },
0203         renderTo: vi.fn(),
0204         dispose: () => undefined,
0205       }));
0206       internals.initialized = true;
0207       internals.views = views;
0208       internals.renderer = {
0209         domElement: document.createElement('canvas'),
0210         setScissorTest: vi.fn(),
0211         setViewport: vi.fn(),
0212         dispose: vi.fn(),
0213       };
0214       internals.renderRequested = false;
0215       return views;
0216     }
0217 
0218     function tick() {
0219       internals.shouldRender = true;
0220       internals.renderLoop();
0221       internals.stopRendering();
0222     }
0223 
0224     beforeEach(() => {
0225       vi.stubGlobal('requestAnimationFrame', vi.fn(() => 1));
0226       vi.stubGlobal('cancelAnimationFrame', vi.fn());
0227     });
0228 
0229     afterEach(() => {
0230       internals.stopRendering();
0231       vi.unstubAllGlobals();
0232     });
0233 
0234     it('invalidate() requests a frame and marks every view dirty', () => {
0235       const views = stubViews(3);
0236       service.invalidate();
0237       expect(internals.renderRequested).toBe(true);
0238       expect(views.every(view => view.dirty)).toBe(true);
0239     });
0240 
0241     it('renders nothing while no flag is set', () => {
0242       const views = stubViews(2);
0243       tick();
0244       expect(views.map(view => view.renderTo.mock.calls.length)).toEqual([0, 0]);
0245       expect(service.renderedFrameCount).toBe(0);
0246     });
0247 
0248     it('repaints all views when only one view is dirty, then clears every flag', () => {
0249       const views = stubViews(3);
0250       views[1].dirty = true;
0251       tick();
0252       expect(views.map(view => view.renderTo.mock.calls.length)).toEqual([1, 1, 1]);
0253       expect(views.map(view => view.dirty)).toEqual([false, false, false]);
0254       expect(service.renderedFrameCount).toBe(1);
0255 
0256       tick();
0257       expect(service.renderedFrameCount).toBe(1);
0258     });
0259 
0260     it('consumes an invalidate() in one frame', () => {
0261       const views = stubViews(2);
0262       service.invalidate();
0263       tick();
0264       tick();
0265       expect(views.map(view => view.renderTo.mock.calls.length)).toEqual([1, 1]);
0266       expect(internals.renderRequested).toBe(false);
0267     });
0268   });
0269 });