Warning, /firebird/packages/firebird-ng/api/src/lib/firebird-features.spec.ts is written in an unsupported language. File is not indexed.
0001 /**
0002 * Firebird's contributions to the shared startup machinery: the URL
0003 * shorthand grammar (`dex`, `geometry`, `event`), the server config
0004 * defaults, and event piece factory registration in provideFirebird().
0005 * The mechanism itself (config.* params, ?cmd=, server-before-URL ordering)
0006 * is covered by the @dexvis/app-features suite.
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 { CommandBusService, ServerConfigService, UrlStartupService, withCommandHandler } from '@dexvis/app-features';
0013 import { getEventPieceFactory } from '@dexvis/firebird-core/model';
0014 import type { EventPiece, EventPieceFactory } from '@dexvis/firebird-core';
0015 import { EventPiecePainter, type PainterMeta } from '@dexvis/firebird-core';
0016 import {
0017 firebirdFeatures,
0018 firebirdPack,
0019 withCameraPreset,
0020 withEventPiece,
0021 withFirebirdUrlShorthands,
0022 withPainter,
0023 } from './firebird-features';
0024 import { CAMERA_PRESETS, PAINTERS } from './tokens';
0025 import {
0026 CameraPresetCommandHandler,
0027 OpenDexCommandHandler,
0028 OpenGeometryCommandHandler,
0029 provideFirebird,
0030 ShowEventCommandHandler,
0031 } from '@dexvis/firebird-ng';
0032 import { defaultFirebirdConfig, ServerConfig } from './services/server-config';
0033
0034 @Injectable()
0035 class SpecPieceFactory implements EventPieceFactory {
0036 readonly type = 'spec.FirebirdFeaturesPiece';
0037 fromDexObject(): EventPiece { throw new Error('not used'); }
0038 }
0039
0040 describe('Firebird URL shorthands', () => {
0041 let service: UrlStartupService;
0042 let commandBus: CommandBusService;
0043
0044 beforeEach(() => {
0045 TestBed.configureTestingModule({
0046 providers: [
0047 provideZonelessChangeDetection(),
0048 provideHttpClient(),
0049 ...withFirebirdUrlShorthands().providers,
0050 // The built-in handlers build the shorthand commands (fromUrlArg).
0051 ...withCommandHandler(OpenGeometryCommandHandler).providers,
0052 ...withCommandHandler(OpenDexCommandHandler).providers,
0053 ...withCommandHandler(ShowEventCommandHandler).providers,
0054 ...withCommandHandler(CameraPresetCommandHandler).providers,
0055 ],
0056 });
0057 service = TestBed.inject(UrlStartupService);
0058 commandBus = TestBed.inject(CommandBusService);
0059 TestBed.inject(ServerConfigService).setUnitTestConfig({});
0060 });
0061
0062 it('turns dex/geometry/event shorthands into queued commands', () => {
0063 service.parse(new URLSearchParams('?dex=asset://data/sample.firebird.zip&geometry=exp://detector.root&event=2'));
0064 expect(commandBus.peekStartupCommands()).toEqual([
0065 { type: 'open-geometry', url: 'exp://detector.root', source: 'url' },
0066 { type: 'open-dex', url: 'asset://data/sample.firebird.zip', source: 'url' },
0067 { type: 'show-event', index: 2, source: 'url' },
0068 ]);
0069 });
0070
0071 it('skips empty shorthands and queues them before ?cmd=', () => {
0072 service.parse(new URLSearchParams('?cmd=camera-preset:endcap&dex=&event=1'));
0073 expect(commandBus.peekStartupCommands()).toEqual([
0074 { type: 'show-event', index: 1, source: 'url' },
0075 { type: 'camera-preset', name: 'endcap', source: 'url' },
0076 ]);
0077 });
0078 });
0079
0080 describe('provideFirebird', () => {
0081 it('boots with Firebird server config defaults, registers piece factories, and queues server commands', async () => {
0082 TestBed.configureTestingModule({
0083 providers: [
0084 provideZonelessChangeDetection(),
0085 provideHttpClient(),
0086 provideHttpClientTesting(),
0087 provideFirebird(withEventPiece(SpecPieceFactory)),
0088 ],
0089 });
0090 const serverConfig = TestBed.inject<ServerConfigService<ServerConfig>>(ServerConfigService);
0091 // Before the load answers, the config is Firebird's defaults.
0092 expect(serverConfig.configSignal()).toEqual(defaultFirebirdConfig);
0093 expect(getEventPieceFactory('spec.FirebirdFeaturesPiece')).toBeInstanceOf(SpecPieceFactory);
0094
0095 const httpMock = TestBed.inject(HttpTestingController);
0096 httpMock.expectOne('assets/config.jsonc').flush(JSON.stringify({
0097 servedByPyrobird: true,
0098 startupCommands: ['show-event:4'],
0099 }));
0100 await TestBed.inject(ApplicationInitStatus).donePromise;
0101
0102 expect(serverConfig.config.servedByPyrobird).toBe(true);
0103 expect(serverConfig.config.apiBaseUrl).toBe(''); // default kept
0104 expect(TestBed.inject(CommandBusService).peekStartupCommands().map(c => [c.type, c.source]))
0105 .toEqual([['show-event', 'server']]);
0106 httpMock.verify();
0107 });
0108 });
0109
0110 /** The values a feature contributes to one multi-provider token, in order. */
0111 function contributions(feature: { providers: unknown[] }, token: unknown): unknown[] {
0112 return (feature.providers as Array<{ provide?: unknown; useValue?: unknown }>)
0113 .filter(provider => provider.provide === token)
0114 .map(provider => provider.useValue);
0115 }
0116
0117 describe('firebirdFeatures', () => {
0118 const endcap = withCameraPreset({ name: 'endcap', position: [0, 0, 9000], target: [0, 0, 0] });
0119 const barrel = withCameraPreset({ name: 'barrel', position: [9000, 0, 0], target: [0, 0, 0] });
0120
0121 it('skips falsy entries, so a pack can include features conditionally', () => {
0122 const debug = false;
0123 const pack = firebirdFeatures(endcap, debug && barrel, null, undefined, barrel);
0124 expect(contributions(pack, CAMERA_PRESETS).map(preset => (preset as { name: string }).name)).toEqual(['endcap', 'barrel']);
0125 });
0126
0127 it('installs a feature id once: a later contribution replaces the earlier one at its position', () => {
0128 const closerEndcap = withCameraPreset({ name: 'endcap', position: [0, 0, 5000], target: [0, 0, 0] });
0129 const pack = firebirdFeatures(endcap, barrel, closerEndcap);
0130 expect(contributions(pack, CAMERA_PRESETS)).toEqual([
0131 { name: 'endcap', position: [0, 0, 5000], target: [0, 0, 0] },
0132 { name: 'barrel', position: [9000, 0, 0], target: [0, 0, 0] },
0133 ]);
0134 });
0135
0136 it('installs a pack passed twice once', () => {
0137 const pack = firebirdPack('spec-pack', endcap);
0138 expect(contributions(firebirdFeatures(pack, barrel, pack), CAMERA_PRESETS).length).toBe(2);
0139 });
0140 });
0141
0142 describe('withPainter', () => {
0143 class MetaPainter extends EventPiecePainter {
0144 static meta: PainterMeta = { id: 'spec-meta', forPieceTypes: ['spec.Hits', 'spec.Clusters'] };
0145 paint(): void { /* nothing to draw */ }
0146 }
0147
0148 class PlainPainter extends EventPiecePainter {
0149 paint(): void { /* nothing to draw */ }
0150 }
0151
0152 it('registers the painter for every type its static meta declares', () => {
0153 expect(contributions(withPainter(MetaPainter), PAINTERS)).toEqual([
0154 { forPieceType: 'spec.Hits', painterClass: MetaPainter },
0155 { forPieceType: 'spec.Clusters', painterClass: MetaPainter },
0156 ]);
0157 });
0158
0159 it('registers for the forPieceType option instead of the meta types', () => {
0160 expect(contributions(withPainter(MetaPainter, { forPieceType: 'spec.Tracks' }), PAINTERS))
0161 .toEqual([{ forPieceType: 'spec.Tracks', painterClass: MetaPainter }]);
0162 expect(contributions(withPainter(PlainPainter, { forPieceType: 'spec.Tracks' }), PAINTERS))
0163 .toEqual([{ forPieceType: 'spec.Tracks', painterClass: PlainPainter }]);
0164 });
0165
0166 it('throws for a painter with neither the option nor meta types', () => {
0167 expect(() => withPainter(PlainPainter))
0168 .toThrow('withPainter(PlainPainter): pass { forPieceType } or declare static meta.forPieceTypes');
0169 });
0170 });