Back to home page

EIC code displayed by LXR

 
 

    


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

0001 /**
0002  * provideFirebird() installs Firebird's built-ins as named sub-features, and
0003  * every registry follows one precedence rule: registration order, a later
0004  * contribution with the id of an earlier one replacing it in place,
0005  * `withoutFeatures()` dropping parts by feature id. Development builds warn
0006  * when two packs register factories for one piece type.
0007  */
0008 import { TestBed } from '@angular/core/testing';
0009 import { ApplicationInitStatus, Injectable, provideZonelessChangeDetection } from '@angular/core';
0010 import { provideHttpClient } from '@angular/common/http';
0011 import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
0012 import {
0013   AppCommand,
0014   AppFeatureInput,
0015   CommandBusService,
0016   CommandHandler,
0017   ConfigService,
0018   SERVER_CONFIG_OPTIONS,
0019   URL_ALIASES,
0020   URL_SHORTHANDS,
0021   withCommandHandler,
0022   withoutFeatures,
0023   withServerConfig,
0024   withUrlAlias,
0025   withUrlShorthand,
0026 } from '@dexvis/app-features';
0027 import { getEventPieceFactory } from '@dexvis/firebird-core/model';
0028 import type { DataExchange, DataLoaderMeta, EventDataLoader, EventPiece, EventPieceFactory } from '@dexvis/firebird-core';
0029 import { EventPiecePainter } from '@dexvis/firebird-core';
0030 import { provideFirebird } from './provide-firebird';
0031 import {
0032   DATA_SELECTOR_TABS,
0033   defaultFirebirdConfig,
0034   firebirdFeatures,
0035   firebirdPack,
0036   GEOMETRY_CUT_LIST_CONFIG,
0037   GEOMETRY_ROOT_FILTER_CONFIG,
0038   GEOMETRY_THEME_CONFIG,
0039   GEOMETRY_THEMES,
0040   injectCameraPresets,
0041   injectEventLoaders,
0042   injectGeometryLoaders,
0043   LAZY_THREE_EXTENSIONS,
0044   PAINTERS,
0045   resolveRootGeometryRules,
0046   ROOT_GEOMETRY_RULES,
0047   selectRootGeometryLoadRules,
0048   TRAJECTORY_EXCLUDED_COLLECTIONS_CONFIG,
0049   withCameraPreset,
0050   withDataSelectorTab,
0051   withEventLoader,
0052   withEventPiece,
0053   withGeometryTheme,
0054   withPainter,
0055 } from '@dexvis/firebird-ng/api';
0056 import { withFirebirdBuiltins } from './with-firebird-builtins';
0057 
0058 function boot(...features: AppFeatureInput[]): void {
0059   TestBed.configureTestingModule({
0060     providers: [
0061       provideZonelessChangeDetection(),
0062       provideHttpClient(),
0063       provideHttpClientTesting(),
0064       provideFirebird(...features),
0065     ],
0066   });
0067 }
0068 
0069 /** Runs the app initializers: config defaults, then the server config (answered with an empty one). */
0070 async function initialize(): Promise<void> {
0071   const initStatus = TestBed.inject(ApplicationInitStatus);
0072   TestBed.inject(HttpTestingController).expectOne('assets/config.jsonc').flush('{}');
0073   await initStatus.donePromise;
0074 }
0075 
0076 const eventLoaderIds = () => TestBed.runInInjectionContext(() => injectEventLoaders().map(loader => loader.meta.id));
0077 const tabIds = () => (TestBed.inject(DATA_SELECTOR_TABS, null) ?? []).map(tab => tab.id);
0078 const cameraPresetNames = () => TestBed.runInInjectionContext(() => injectCameraPresets().map(preset => preset.name));
0079 
0080 @Injectable()
0081 class PackDexLoader implements EventDataLoader {
0082   readonly meta: DataLoaderMeta = { id: 'firebird-dex', label: 'Pack DEX', fileExtensions: ['.json'] };
0083   canLoad(): boolean { return true; }
0084   loadEvents(): Promise<DataExchange> { return Promise.reject(new Error('not called in this spec')); }
0085 }
0086 
0087 @Injectable()
0088 class PackCameraPresetHandler implements CommandHandler {
0089   readonly type = 'camera-preset';
0090   readonly executed: AppCommand[] = [];
0091   execute(command: AppCommand): void { this.executed.push(command); }
0092 }
0093 
0094 /** A replacement decoder for the built-in BoxHit type. */
0095 @Injectable()
0096 class PackBoxHitFactory implements EventPieceFactory {
0097   readonly type = 'BoxHit';
0098   fromDexObject(): EventPiece { throw new Error('not used'); }
0099 }
0100 
0101 /** Two packs that both ship a decoder for one piece type. */
0102 @Injectable()
0103 class RingFactoryA implements EventPieceFactory {
0104   readonly type = 'spec.Ring';
0105   fromDexObject(): EventPiece { throw new Error('not used'); }
0106 }
0107 
0108 @Injectable()
0109 class RingFactoryB implements EventPieceFactory {
0110   readonly type = 'spec.Ring';
0111   fromDexObject(): EventPiece { throw new Error('not used'); }
0112 }
0113 
0114 describe('provideFirebird built-ins', () => {
0115   beforeEach(() => localStorage.clear());
0116 
0117   it('installs every built-in part', () => {
0118     boot();
0119     expect(TestBed.inject(SERVER_CONFIG_OPTIONS).defaults).toBe(defaultFirebirdConfig);
0120     expect(TestBed.inject(URL_SHORTHANDS).map(shorthand => shorthand.param)).toEqual(['geometry', 'dex', 'event']);
0121     expect(eventLoaderIds()).toEqual(['firebird-dex', 'root2dex', 'edm4eic-root']);
0122     expect(TestBed.runInInjectionContext(() => injectGeometryLoaders().map(loader => loader.meta.id))).toEqual(['root-geometry']);
0123     expect(tabIds()).toEqual(['presets', 'physics', 'manual']);
0124     expect(TestBed.inject(LAZY_THREE_EXTENSIONS, null)?.length).toBe(1);
0125     expect(TestBed.inject(GEOMETRY_THEMES, null)?.map(theme => theme.id)).toEqual(['grey']);
0126     expect(cameraPresetNames()).toEqual(['front', 'back', 'right', 'left', 'top', 'bottom', 'home']);
0127     expect(TestBed.inject(CommandBusService).knownTypes).toEqual(expect.arrayContaining([
0128       'open-geometry', 'open-dex', 'show-event', 'set-config', 'camera-preset', 'animate-collision',
0129     ]));
0130   });
0131 
0132   it('installs the built-ins once when the application passes them again', () => {
0133     boot(withFirebirdBuiltins());
0134     expect(eventLoaderIds()).toEqual(['firebird-dex', 'root2dex', 'edm4eic-root']);
0135     expect(tabIds()).toEqual(['presets', 'physics', 'manual']);
0136     expect(TestBed.inject(LAZY_THREE_EXTENSIONS, null)?.length).toBe(1);
0137   });
0138 
0139   it('leaves the geometry pipeline off: no theme, no edit rules, no cut list', async () => {
0140     boot();
0141     await initialize();
0142     const config = TestBed.inject(ConfigService);
0143     const filterName = config.declare(GEOMETRY_ROOT_FILTER_CONFIG).value;
0144     const cutListName = config.declare(GEOMETRY_CUT_LIST_CONFIG).value;
0145     expect(filterName).toBe('off');
0146     expect(cutListName).toBe('off');
0147     expect(config.declare(GEOMETRY_THEME_CONFIG).value).toBe('off');
0148     // What a geometry load posts to the worker (GeometryService builds it the same way)
0149     const rules = await resolveRootGeometryRules(TestBed.inject(ROOT_GEOMETRY_RULES, null) ?? []);
0150     expect(selectRootGeometryLoadRules(rules, filterName, cutListName)).toEqual({ editRules: [], cutList: [] });
0151   });
0152 
0153   it('keeps every sim hit collection in MC-truth trajectories until a pack lists some', async () => {
0154     boot();
0155     await initialize();
0156     expect(TestBed.inject(ConfigService).declare(TRAJECTORY_EXCLUDED_COLLECTIONS_CONFIG).value).toBe('');
0157   });
0158 
0159   it('drops a built-in part by its feature id', () => {
0160     boot(withoutFeatures('firebird.navigation-cube', 'data-selector-tab:physics', 'firebird.server-conversion'));
0161     expect(TestBed.inject(LAZY_THREE_EXTENSIONS, null)).toBeNull();
0162     expect(tabIds()).toEqual(['presets', 'manual']);
0163     expect(eventLoaderIds()).toEqual(['firebird-dex', 'root2dex']);
0164   });
0165 });
0166 
0167 /** An event loader class with its own id that claims nothing. */
0168 function specLoader(id: string) {
0169   @Injectable()
0170   class SpecLoader implements EventDataLoader {
0171     readonly meta: DataLoaderMeta = { id, label: id, fileExtensions: [`.${id}`] };
0172     canLoad(): boolean { return false; }
0173     loadEvents(): Promise<DataExchange> { return Promise.reject(new Error('not called in this spec')); }
0174   }
0175   return SpecLoader;
0176 }
0177 
0178 class PackBoxHitPainter extends EventPiecePainter {
0179   paint(): void { /* nothing to draw */ }
0180 }
0181 
0182 describe('registration order', () => {
0183   it('registers the application features after the built-ins, depth first in argument order', () => {
0184     boot(
0185       firebirdPack('outer', withEventLoader(specLoader('a')), firebirdFeatures(withEventLoader(specLoader('b')))),
0186       withEventLoader(specLoader('c')),
0187     );
0188     expect(eventLoaderIds()).toEqual(['firebird-dex', 'root2dex', 'edm4eic-root', 'a', 'b', 'c']);
0189   });
0190 
0191   it('keeps a built-in painter the default of its type when a pack adds another', () => {
0192     boot(firebirdPack('pack', withPainter(PackBoxHitPainter, { forPieceType: 'BoxHit' })));
0193     const boxHitPainters = (TestBed.inject(PAINTERS, null) ?? []).filter(painter => painter.forPieceType === 'BoxHit');
0194     expect(boxHitPainters.length).toBe(2);
0195     expect(boxHitPainters[0].load).toBeTypeOf('function');
0196     expect(boxHitPainters[1].painterClass).toBe(PackBoxHitPainter);
0197   });
0198 });
0199 
0200 describe('dropping a pack', () => {
0201   it('restores the built-in parts the pack had replaced', () => {
0202     const load = () => Promise.resolve(class {});
0203     boot(
0204       firebirdPack('my-experiment',
0205         withCameraPreset({ name: 'home', position: [0, 7000, 0], target: [0, 0, 0], up: [1, 0, 0] }),
0206         withDataSelectorTab({ id: 'manual', label: 'Pack manual', order: 30, load }),
0207       ),
0208       withoutFeatures('my-experiment'),
0209     );
0210     const home = TestBed.runInInjectionContext(() => injectCameraPresets().find(preset => preset.name === 'home'));
0211     expect(home).toEqual({ name: 'home', direction: [0, 1, 0], up: [1, 0, 0], fitGeometry: true });
0212     const manual = (TestBed.inject(DATA_SELECTOR_TABS, null) ?? []).find(tab => tab.id === 'manual');
0213     expect(manual?.label).toBe('Manual');
0214   });
0215 });
0216 
0217 describe('registry precedence: a later registration with the same id replaces the earlier one in place', () => {
0218   it('data selector tabs, by tab id', () => {
0219     const load = () => Promise.resolve(class {});
0220     boot(firebirdPack('pack', withDataSelectorTab({ id: 'manual', label: 'My files', order: 30, load })));
0221     const tabs = TestBed.inject(DATA_SELECTOR_TABS, null) ?? [];
0222     expect(tabs.map(tab => `${tab.id}:${tab.label}`)).toEqual(['presets:Presets', 'physics:Physics', 'manual:My files']);
0223   });
0224 
0225   it('event loaders, by meta.id: the replacement keeps the built-in place in the claiming order', () => {
0226     boot(firebirdPack('pack', withEventLoader(PackDexLoader)));
0227     expect(eventLoaderIds()).toEqual(['firebird-dex', 'root2dex', 'edm4eic-root']);
0228     expect(TestBed.runInInjectionContext(() => injectEventLoaders()[0])).toBeInstanceOf(PackDexLoader);
0229   });
0230 
0231   it('command handlers, by type', async () => {
0232     boot(firebirdPack('pack', withCommandHandler(PackCameraPresetHandler)));
0233     await TestBed.inject(CommandBusService).dispatch({ type: 'camera-preset', name: 'top' });
0234     const handler = TestBed.inject(CommandBusService) as unknown as { handlersByType: Map<string, CommandHandler> };
0235     expect(handler.handlersByType.get('camera-preset')).toBeInstanceOf(PackCameraPresetHandler);
0236   });
0237 
0238   it('camera presets, by name', () => {
0239     boot(firebirdPack('pack', withCameraPreset({ name: 'home', position: [0, 1, 2], target: [0, 0, 0] })));
0240     expect(cameraPresetNames()).toEqual(['front', 'back', 'right', 'left', 'top', 'bottom', 'home']);
0241     const home = TestBed.runInInjectionContext(() => injectCameraPresets().find(preset => preset.name === 'home'));
0242     expect(home).toEqual({ name: 'home', position: [0, 1, 2], target: [0, 0, 0] });
0243   });
0244 
0245   it('geometry themes, by id', () => {
0246     const load = () => Promise.resolve([]);
0247     boot(firebirdPack('pack', withGeometryTheme({ id: 'grey', label: 'Pack grey', load })));
0248     expect(TestBed.inject(GEOMETRY_THEMES, null)?.map(theme => theme.label)).toEqual(['Pack grey']);
0249   });
0250 
0251   it('URL aliases, by prefix', () => {
0252     boot(firebirdPack('a', withUrlAlias('data://', 'https://a/')), firebirdPack('b', withUrlAlias('data://', 'https://b/')));
0253     expect(TestBed.inject(URL_ALIASES)).toEqual([{ prefix: 'data://', base: 'https://b/' }]);
0254   });
0255 
0256   it('piece factories, by type: a pack replaces a built-in decoder without a warning', () => {
0257     const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
0258     boot(firebirdPack('pack', withEventPiece(PackBoxHitFactory)));
0259     TestBed.inject(ApplicationInitStatus);
0260     expect(getEventPieceFactory('BoxHit')).toBeInstanceOf(PackBoxHitFactory);
0261     expect(warn.mock.calls.map(call => String(call[0]))).not.toContainEqual(expect.stringContaining('Piece type'));
0262     warn.mockRestore();
0263   });
0264 
0265   it('two packs shipping factories for one piece type draw a development warning that names both', () => {
0266     const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
0267     boot(firebirdPack('pack-a', withEventPiece(RingFactoryA)), firebirdPack('pack-b', withEventPiece(RingFactoryB)));
0268     TestBed.inject(ApplicationInitStatus);
0269     expect(getEventPieceFactory('spec.Ring')).toBeInstanceOf(RingFactoryB);
0270     const message = warn.mock.calls.map(call => String(call[0])).find(text => text.includes(`Piece type 'spec.Ring'`));
0271     expect(message).toContain('RingFactoryA (pack-a)');
0272     expect(message).toContain('RingFactoryB (pack-b)');
0273     warn.mockRestore();
0274   });
0275 
0276   it('a pack that replaces a built-in feature by id draws no collision warning', () => {
0277     const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
0278     boot(firebirdPack('pack', withCameraPreset({ name: 'home', position: [0, 1, 2], target: [0, 0, 0] })));
0279     expect(warn).not.toHaveBeenCalled();
0280     warn.mockRestore();
0281   });
0282 
0283   it('the server config and the URL shorthands are built-in parts: a pack replaces them without a warning', () => {
0284     const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
0285     boot(firebirdPack('pack', withUrlShorthand('dex', 'open-file'), withServerConfig({ url: 'my/config.jsonc' })));
0286     expect(warn).not.toHaveBeenCalled();
0287     expect(TestBed.inject(URL_SHORTHANDS)).toContainEqual({ param: 'dex', commandType: 'open-file' });
0288     expect(TestBed.inject(SERVER_CONFIG_OPTIONS)).toEqual({ url: 'my/config.jsonc' });
0289     warn.mockRestore();
0290   });
0291 });