Back to home page

EIC code displayed by LXR

 
 

    


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