Back to home page

EIC code displayed by LXR

 
 

    


Warning, /firebird/packages/firebird-ng/src/lib/builtin-command-handlers.spec.ts is written in an unsupported language. File is not indexed.

0001 /**
0002  * The built-in command handlers: what each command needs, which display
0003  * service it reaches, and the errors a deep link or batch script sees when a
0004  * command cannot run. The display services are stubs: the handlers resolve
0005  * them through dynamic imports, which TestBed's providers answer.
0006  */
0007 import { TestBed } from '@angular/core/testing';
0008 import { Type, provideZonelessChangeDetection, signal } from '@angular/core';
0009 import { AppCommand, CommandHandler, ConfigService } from '@dexvis/app-features';
0010 import {
0011   AnimateCollisionCommandHandler,
0012   CameraPresetCommandHandler,
0013   OpenDexCommandHandler,
0014   OpenGeometryCommandHandler,
0015   SetConfigCommandHandler,
0016   ShowEventCommandHandler,
0017 } from './builtin-command-handlers';
0018 import { withCameraPreset } from '@dexvis/firebird-ng/api';
0019 import { DataModelService, EventDisplayService, ThreeService } from '@dexvis/firebird-ng/display';
0020 
0021 const dataModel = {
0022   entries: signal<unknown[]>([]),
0023   setCurrentEntry: vi.fn(),
0024 };
0025 const three = { applyCameraPreset: vi.fn() };
0026 const eventDisplay = {
0027   animateWithCollision: vi.fn(() => Promise.resolve()),
0028   openGeometry: vi.fn((url: string) => Promise.resolve(url)),
0029   openEvents: vi.fn((url: string) => Promise.resolve(url)),
0030 };
0031 
0032 function handler<T extends CommandHandler>(handlerClass: Type<T>): T {
0033   return TestBed.inject(handlerClass);
0034 }
0035 
0036 describe('built-in command handlers', () => {
0037   beforeEach(() => {
0038     localStorage.clear();
0039     dataModel.entries.set([]);
0040     TestBed.configureTestingModule({
0041       providers: [
0042         provideZonelessChangeDetection(),
0043         ...withCameraPreset({ name: 'endcap', position: [0, 0, 9000], target: [0, 0, 0] }).providers,
0044         OpenGeometryCommandHandler,
0045         OpenDexCommandHandler,
0046         ShowEventCommandHandler,
0047         SetConfigCommandHandler,
0048         CameraPresetCommandHandler,
0049         AnimateCollisionCommandHandler,
0050         { provide: DataModelService, useValue: dataModel },
0051         { provide: ThreeService, useValue: three },
0052         { provide: EventDisplayService, useValue: eventDisplay },
0053       ],
0054     });
0055   });
0056 
0057   describe('open-geometry', () => {
0058     it('builds the command from a URL argument', () => {
0059       expect(handler(OpenGeometryCommandHandler).fromUrlArg('https://host/detector.geo'))
0060         .toEqual({ type: 'open-geometry', url: 'https://host/detector.geo' });
0061     });
0062 
0063     it('loads through the display, which picks the claiming loader', async () => {
0064       await handler(OpenGeometryCommandHandler).execute({ type: 'open-geometry', url: 'https://host/detector.geo' });
0065       expect(eventDisplay.openGeometry).toHaveBeenCalledWith('https://host/detector.geo');
0066     });
0067 
0068     it("fails with the display's reason", async () => {
0069       eventDisplay.openGeometry.mockRejectedValueOnce(new Error("No geometry loader claims 'detector.gdml'"));
0070       await expect(handler(OpenGeometryCommandHandler).execute({ type: 'open-geometry', url: 'detector.gdml' }))
0071         .rejects.toThrow("No geometry loader claims 'detector.gdml'");
0072     });
0073 
0074     it('requires a url', async () => {
0075       await expect(handler(OpenGeometryCommandHandler).execute({ type: 'open-geometry' }))
0076         .rejects.toThrow("open-geometry: 'url' argument is required");
0077     });
0078   });
0079 
0080   describe('open-dex', () => {
0081     it('builds the command from a URL argument', () => {
0082       expect(handler(OpenDexCommandHandler).fromUrlArg('https://host/events.json'))
0083         .toEqual({ type: 'open-dex', url: 'https://host/events.json' });
0084     });
0085 
0086     it('loads through the display, which picks the claiming loader', async () => {
0087       await handler(OpenDexCommandHandler).execute({ type: 'open-dex', url: 'https://host/events.json' });
0088       expect(eventDisplay.openEvents).toHaveBeenCalledWith('https://host/events.json');
0089     });
0090 
0091     it("fails with the display's reason", async () => {
0092       eventDisplay.openEvents.mockRejectedValueOnce(new Error('HTTP 404 Not Found (https://host/events.json)'));
0093       await expect(handler(OpenDexCommandHandler).execute({ type: 'open-dex', url: 'https://host/events.json' }))
0094         .rejects.toThrow('HTTP 404 Not Found (https://host/events.json)');
0095     });
0096 
0097     it('requires a url', async () => {
0098       await expect(handler(OpenDexCommandHandler).execute({ type: 'open-dex' }))
0099         .rejects.toThrow("open-dex: 'url' argument is required");
0100     });
0101   });
0102 
0103   describe('show-event', () => {
0104     it('parses the index from a URL argument', () => {
0105       expect(handler(ShowEventCommandHandler).fromUrlArg('3')).toEqual({ type: 'show-event', index: 3 });
0106     });
0107 
0108     it('selects the entry with that index', async () => {
0109       const entries = [{ id: 'a' }, { id: 'b' }];
0110       dataModel.entries.set(entries);
0111       await handler(ShowEventCommandHandler).execute({ type: 'show-event', index: 1 });
0112       expect(dataModel.setCurrentEntry).toHaveBeenCalledWith(entries[1]);
0113     });
0114 
0115     it('rejects an index outside the loaded entries', async () => {
0116       dataModel.entries.set([{ id: 'a' }, { id: 'b' }]);
0117       await expect(handler(ShowEventCommandHandler).execute({ type: 'show-event', index: 5 }))
0118         .rejects.toThrow('show-event: index 5 out of range (0..1)');
0119       expect(dataModel.setCurrentEntry).not.toHaveBeenCalled();
0120     });
0121 
0122     it('requires a numeric index', async () => {
0123       await expect(handler(ShowEventCommandHandler).execute({ type: 'show-event', index: 'last' }))
0124         .rejects.toThrow("show-event: numeric 'index' argument is required");
0125     });
0126   });
0127 
0128   describe('set-config', () => {
0129     const command = (source: AppCommand['source'], value: unknown): AppCommand =>
0130       ({ type: 'set-config', key: 'spec.width', value, source });
0131 
0132     it('splits a URL argument at the first =', () => {
0133       expect(handler(SetConfigCommandHandler).fromUrlArg('spec.url=https://host/?a=b'))
0134         .toEqual({ type: 'set-config', key: 'spec.url', value: 'https://host/?a=b' });
0135       expect(handler(SetConfigCommandHandler).fromUrlArg('spec.flag'))
0136         .toEqual({ type: 'set-config', key: 'spec.flag', value: '' });
0137     });
0138 
0139     for (const source of ['url', 'server', 'batch'] as const) {
0140       it(`applies a ${source} value for the session only`, () => {
0141         const property = TestBed.inject(ConfigService).declare({ key: 'spec.width', default: 1 });
0142         handler(SetConfigCommandHandler).execute(command(source, '7'));
0143         expect(property.value).toBe(7);
0144         expect(property.hasSessionOverride).toBe(true);
0145         expect(property.hasStoredValue()).toBe(false);
0146       });
0147     }
0148 
0149     it('persists a ui value, converted to the declared type', () => {
0150       const property = TestBed.inject(ConfigService).declare({ key: 'spec.width', default: 1 });
0151       handler(SetConfigCommandHandler).execute(command('ui', '7'));
0152       expect(property.value).toBe(7);
0153       expect(property.hasSessionOverride).toBe(false);
0154       expect(property.hasStoredValue()).toBe(true);
0155     });
0156 
0157     it('keeps a ui value for a key no code declared yet, typed by the later declaration', () => {
0158       handler(SetConfigCommandHandler).execute(command('ui', '7'));
0159       const property = TestBed.inject(ConfigService).declare({ key: 'spec.width', default: 1 });
0160       expect(property.value).toBe(7);
0161     });
0162 
0163     it('requires a key', () => {
0164       expect(() => handler(SetConfigCommandHandler).execute({ type: 'set-config', value: 1 }))
0165         .toThrow("set-config: 'key' argument is required");
0166     });
0167   });
0168 
0169   describe('camera-preset', () => {
0170     it('moves the camera to a registered preset', async () => {
0171       await handler(CameraPresetCommandHandler).execute({ type: 'camera-preset', name: 'endcap' });
0172       expect(three.applyCameraPreset).toHaveBeenCalledWith(expect.objectContaining({ name: 'endcap' }));
0173     });
0174 
0175     it('names the known presets for an unknown one', async () => {
0176       await expect(handler(CameraPresetCommandHandler).execute({ type: 'camera-preset', name: 'barrel' }))
0177         .rejects.toThrow("camera-preset: unknown preset 'barrel'. Known: endcap");
0178       expect(three.applyCameraPreset).not.toHaveBeenCalled();
0179     });
0180   });
0181 
0182   describe('animate-collision', () => {
0183     it('starts the collision animation of the display', async () => {
0184       await handler(AnimateCollisionCommandHandler).execute();
0185       expect(eventDisplay.animateWithCollision).toHaveBeenCalledTimes(1);
0186     });
0187   });
0188 });