Warning, /firebird/packages/epic/src/with-epic.spec.ts is written in an unsupported language. File is not indexed.
0001 /**
0002 * The ePIC pack through the public extension API: what it contributes to
0003 * each registry, that its options are config defaults every other source
0004 * overrides, that installing it twice installs it once, and that its
0005 * pre-build rules survive the trip to the geometry worker (structured clone).
0006 */
0007 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
0008 import { TestBed } from '@angular/core/testing';
0009 import { ApplicationInitStatus, provideZonelessChangeDetection } from '@angular/core';
0010 import { provideHttpClient } from '@angular/common/http';
0011 import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
0012 import { Group, Mesh } from 'three';
0013 import {
0014 type AppFeatureInput,
0015 CAMERA_LIMITS,
0016 COLLISION_INTRO,
0017 ConfigService,
0018 DATA_CATALOGS,
0019 GEOMETRY_POST_PROCESSORS,
0020 GEOMETRY_ROOT_FILTER_CONFIG,
0021 GEOMETRY_THEME_CONFIG,
0022 GEOMETRY_THEMES,
0023 GEOMETRY_URL_CONFIG,
0024 ROOT_GEOMETRY_RULES,
0025 TRAJECTORY_EXCLUDED_COLLECTIONS_CONFIG,
0026 URL_ALIASES,
0027 injectCameraPresets,
0028 injectToolbarActions,
0029 provideFirebird,
0030 resolveRootGeometryRules,
0031 selectRootGeometryLoadRules,
0032 withoutFeatures,
0033 } from '@dexvis/firebird-ng';
0034 import { EpicOptions, withEpic } from './with-epic';
0035 import { EpicCollisionIntro } from './epic-collision-intro';
0036
0037 const CRATERLAKE = 'https://seeeic.org/g/epic/artifacts/tgeo/epic_craterlake.root';
0038
0039 /**
0040 * Boots provideFirebird() with the given features, answers the server config
0041 * request with `serverFile`, and waits for the app initializers.
0042 */
0043 async function boot(features: AppFeatureInput[], serverFile: object = {}): Promise<void> {
0044 TestBed.configureTestingModule({
0045 providers: [
0046 provideZonelessChangeDetection(),
0047 provideHttpClient(),
0048 provideHttpClientTesting(),
0049 provideFirebird(...features),
0050 ],
0051 });
0052 const initStatus = TestBed.inject(ApplicationInitStatus);
0053 TestBed.inject(HttpTestingController).expectOne('assets/config.jsonc').flush(JSON.stringify(serverFile));
0054 await initStatus.donePromise;
0055 }
0056
0057 function bootEpic(options?: EpicOptions, ...extraFeatures: AppFeatureInput[]): Promise<void> {
0058 return boot([withEpic(options), ...extraFeatures]);
0059 }
0060
0061 function configValue(schema: typeof GEOMETRY_URL_CONFIG): string {
0062 return TestBed.inject(ConfigService).declare(schema).value;
0063 }
0064
0065 describe('withEpic', () => {
0066 const originalUrl = window.location.href;
0067
0068 beforeEach(() => localStorage.clear());
0069
0070 afterEach(() => {
0071 window.history.replaceState(null, '', originalUrl);
0072 localStorage.clear();
0073 vi.restoreAllMocks();
0074 });
0075
0076 it('points epic:// at the ePIC artifacts on seeeic.org and selects the ePIC pipeline by default', async () => {
0077 await bootEpic();
0078 expect(TestBed.inject(URL_ALIASES)).toContainEqual({ prefix: 'epic://', base: 'https://seeeic.org/g/epic/artifacts/' });
0079 expect(configValue(GEOMETRY_THEME_CONFIG)).toBe('cool2');
0080 expect(configValue(GEOMETRY_ROOT_FILTER_CONFIG)).toBe('default');
0081 expect(configValue(GEOMETRY_URL_CONFIG)).toBe(CRATERLAKE);
0082 });
0083
0084 it('keeps the Cherenkov sim hits out of MC-truth trajectories, as pyrobird does', async () => {
0085 await bootEpic();
0086 expect(configValue(TRAJECTORY_EXCLUDED_COLLECTIONS_CONFIG)).toBe('DIRCBarHits,DRICHHits,PFRICHHits');
0087 });
0088
0089 it('takes the default geometry as an option', async () => {
0090 await bootEpic({ geometry: 'epic://tgeo/epic_ip6.root' });
0091 expect(configValue(GEOMETRY_URL_CONFIG)).toBe('epic://tgeo/epic_ip6.root');
0092 });
0093
0094 describe('options are defaults: every other config source overrides them', () => {
0095 const option = 'epic://tgeo/epic_ip6.root';
0096
0097 it('server config.jsonc', async () => {
0098 await boot([withEpic({ geometry: option })], {
0099 userConfigs: { 'geometry.selectedGeometry': 'https://server/detector.root', 'geometry.themeName': 'grey' },
0100 });
0101 expect(configValue(GEOMETRY_URL_CONFIG)).toBe('https://server/detector.root');
0102 expect(configValue(GEOMETRY_THEME_CONFIG)).toBe('grey');
0103 });
0104
0105 it('a value the user saved', async () => {
0106 localStorage.setItem('geometry.selectedGeometry', 'https://saved/detector.root');
0107 await bootEpic({ geometry: option });
0108 expect(configValue(GEOMETRY_URL_CONFIG)).toBe('https://saved/detector.root');
0109 });
0110
0111 it('a ?config. URL value', async () => {
0112 window.history.replaceState(null, '', '/?config.geometry.selectedGeometry=https://link/detector.root&config.geometry.themeName=grey');
0113 await bootEpic({ geometry: option });
0114 expect(configValue(GEOMETRY_URL_CONFIG)).toBe('https://link/detector.root');
0115 expect(configValue(GEOMETRY_THEME_CONFIG)).toBe('grey');
0116 });
0117 });
0118
0119 it('registers its themes, post-processors, camera views and limits, intro and toolbar action', async () => {
0120 await bootEpic();
0121 expect(TestBed.inject(GEOMETRY_THEMES).map(theme => theme.id)).toEqual(['grey', 'cool2', 'cool2no', 'cad']);
0122 expect(TestBed.inject(GEOMETRY_POST_PROCESSORS).map(processor => processor.id)).toEqual(['epic.prettifier', 'epic.arranger']);
0123 expect(TestBed.inject(CAMERA_LIMITS)).toEqual({ minDistance: 750, maxDistance: 75000 });
0124 const presets = TestBed.runInInjectionContext(() => injectCameraPresets());
0125 expect(presets.find(preset => preset.name === 'home')).toEqual(
0126 expect.objectContaining({ position: [0, 7000, 0], target: [0, 0, 0], up: [1, 0, 0] }));
0127 expect(presets.map(preset => preset.name)).toEqual(expect.arrayContaining(['center', 'farforward']));
0128 expect(TestBed.runInInjectionContext(() => injectToolbarActions())).toEqual([
0129 expect.objectContaining({ id: 'collision', icon: 'close_fullscreen', command: { type: 'animate-collision' } }),
0130 ]);
0131 expect(await TestBed.inject(COLLISION_INTRO)()).toBe(EpicCollisionIntro);
0132 });
0133
0134 it('offers the ePIC datasets as DEX 1.0 files on seeeic.org/d', async () => {
0135 await bootEpic();
0136 const [catalog] = TestBed.inject(DATA_CATALOGS);
0137 const entryUrls = (catalog.entries ?? []).map(entry => entry.events).filter((url): url is string => !!url);
0138 const eventUrls = [...entryUrls, ...(catalog.eventSources ?? [])];
0139 expect(eventUrls.length).toBe(18);
0140 for (const url of eventUrls) {
0141 expect(url).toMatch(/^https:\/\/seeeic\.org\/d\/[\w.-]+\.v1\.firebird\.zip$/);
0142 }
0143 expect(catalog.entries?.[0]).toEqual(expect.objectContaining({ geometry: CRATERLAKE }));
0144 });
0145
0146 it('takes effect once when an application installs it twice: the later call wins, nothing is duplicated', async () => {
0147 const warn = vi.spyOn(console, 'warn');
0148 await boot([withEpic(), withEpic({ geometry: 'epic://tgeo/epic_ip6.root' })]);
0149 expect(configValue(GEOMETRY_URL_CONFIG)).toBe('epic://tgeo/epic_ip6.root');
0150 expect(TestBed.inject(URL_ALIASES).filter(alias => alias.prefix === 'epic://').length).toBe(1);
0151 expect(TestBed.inject(DATA_CATALOGS).length).toBe(1);
0152 expect(TestBed.inject(GEOMETRY_THEMES).map(theme => theme.id)).toEqual(['grey', 'cool2', 'cool2no', 'cad']);
0153 expect(TestBed.inject(GEOMETRY_POST_PROCESSORS).length).toBe(2);
0154 const presetNames = TestBed.runInInjectionContext(() => injectCameraPresets()).map(preset => preset.name);
0155 expect(presetNames.length).toBe(new Set(presetNames).size);
0156 expect(TestBed.runInInjectionContext(() => injectToolbarActions()).length).toBe(1);
0157 expect(warn.mock.calls.flat().join('\n')).not.toMatch(/\[AppFeatures\]|\[Firebird\]/);
0158 });
0159
0160 it('leaves the generic built-ins in effect when an application drops the pack', async () => {
0161 await bootEpic(undefined, withoutFeatures('epic'));
0162 expect(TestBed.inject(URL_ALIASES, null) ?? []).not.toContainEqual(expect.objectContaining({ prefix: 'epic://' }));
0163 expect(configValue(GEOMETRY_THEME_CONFIG)).toBe('off');
0164 expect(configValue(TRAJECTORY_EXCLUDED_COLLECTIONS_CONFIG)).toBe('');
0165 const home = TestBed.runInInjectionContext(() => injectCameraPresets().find(preset => preset.name === 'home'));
0166 expect(home).toEqual({ name: 'home', direction: [0, 1, 0], up: [1, 0, 0], fitGeometry: true });
0167 expect(TestBed.inject(CAMERA_LIMITS, null)).toBeNull();
0168 expect(TestBed.runInInjectionContext(() => injectToolbarActions())).toEqual([]);
0169 });
0170
0171 it('ships pre-build rules that survive the structured clone to the worker', async () => {
0172 await bootEpic();
0173 const rules = await resolveRootGeometryRules(TestBed.inject(ROOT_GEOMETRY_RULES));
0174 const selected = selectRootGeometryLoadRules(rules, 'default', 'central');
0175 expect(selected.editRules.length).toBe(15);
0176 expect(selected.cutList).toContain('Lumi');
0177 expect(structuredClone(selected)).toEqual(selected);
0178 });
0179 });
0180
0181 describe('EpicCollisionIntro', () => {
0182 it('flies the beams in from +-5 m, fades them in over 300 ms, and disposes them at the end', () => {
0183 const intro = new EpicCollisionIntro();
0184 const parent = new Group();
0185 intro.begin(parent);
0186 const [electron, ion] = parent.children as Mesh[];
0187 expect([electron.position.z, ion.position.z]).toEqual([5000, -5000]);
0188
0189 intro.update(150);
0190 expect((electron.material as { opacity: number }).opacity).toBeCloseTo(0.5);
0191 intro.update(500);
0192 expect([electron.position.z, ion.position.z]).toEqual([2500, -2500]);
0193 intro.update(1000);
0194 expect([electron.position.z + 0, ion.position.z + 0]).toEqual([0, 0]);
0195
0196 intro.end();
0197 expect(parent.children).toEqual([]);
0198 });
0199 });