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 });