import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { DataLoaderService } from '../../core/data/data-loader.service'; import { BookmarksStore } from '../../shared/state/bookmarks.store'; import { DEFAULT_HUD_DISPLAY, HudDisplay, HudDockComponent } from './hud-dock.component'; class EmptyDataLoaderService { loadStars() { return Promise.resolve({ stars: [], positions: new Float32Array(0) }); } loadBodies() { return Promise.resolve([]); } loadExoplanets() { return Promise.resolve([]); } } describe('HudDockComponent', () => { let fixture: ComponentFixture; function host(): HTMLElement { return fixture.nativeElement as HTMLElement; } function tabNames(): string[] { return [...host().querySelectorAll('[role="tab"]')].map((tab) => tab.textContent?.trim() ?? ''); } function tab(name: string): HTMLButtonElement { const found = [...host().querySelectorAll('[role="tab"]')].find((t) => t.textContent?.trim() === name); if (!found) { throw new Error(`no "${name}" tab`); } return found; } function setReadout(): void { fixture.componentRef.setInput('eyebrow', 'Galactic Scale'); fixture.componentRef.setInput('title', 'Milky Way'); fixture.componentRef.setInput('subtitle', 'Barred spiral'); fixture.componentRef.setInput('readouts', [{ label: 'Arms', value: '5' }, { label: 'Luminosity', value: '1 L☉', derived: true }]); fixture.componentRef.setInput('note', 'Illustrative model.'); fixture.componentRef.setInput('range', '21.5 kpc'); } beforeEach(async () => { localStorage.clear(); await TestBed.configureTestingModule({ imports: [HudDockComponent], providers: [ { provide: DataLoaderService, useClass: EmptyDataLoaderService }, { provide: Router, useValue: { navigate: vi.fn().mockResolvedValue(true) } } ] }).compileComponents(); fixture = TestBed.createComponent(HudDockComponent); }); it('offers the search and what has been kept, when it has nothing else', () => { // Bookmarks are always offered: it is the only place that says the map can keep anything. fixture.detectChanges(); expect(tabNames()).toEqual(['Search', 'Bookmarks']); expect(host().querySelector('[role="tabpanel"]')).toBeNull(); }); it('grows a tab per thing it has been given', () => { setReadout(); fixture.componentRef.setInput('display', DEFAULT_HUD_DISPLAY); fixture.detectChanges(); expect(tabNames()).toEqual(['Search', 'Readout', 'Bookmarks', 'Display']); fixture.componentRef.setInput('routing', true); fixture.detectChanges(); expect(tabNames()).toEqual(['Search', 'Readout', 'Routes', 'Bookmarks', 'Display']); }); it('opens the default tab on mount and renders the readout from its inputs', () => { setReadout(); fixture.componentRef.setInput('defaultTab', 'readout'); fixture.detectChanges(); expect(tab('Readout').getAttribute('aria-selected')).toBe('true'); const text = host().textContent ?? ''; for (const expected of ['Galactic Scale', 'Milky Way', 'Barred spiral', 'Arms', '5', 'Illustrative model.', 'Derived, not catalogued', '21.5 kpc']) { expect(text).toContain(expected); } expect(host().querySelector('[data-testid="hud-title"]')?.textContent?.trim()).toBe('Milky Way'); }); it('leaves out the optional lines it was given nothing for', () => { fixture.componentRef.setInput('title', 'Local Stars'); fixture.componentRef.setInput('defaultTab', 'readout'); fixture.detectChanges(); expect(host().querySelector('dl')).toBeNull(); expect(host().textContent).not.toContain('undefined'); }); it('keeps the range on the strip whichever panel is open, and hides it when there is none', () => { setReadout(); fixture.componentRef.setInput('defaultTab', 'search'); fixture.detectChanges(); expect(host().textContent).toContain('21.5 kpc'); fixture.componentRef.setInput('range', ''); fixture.detectChanges(); expect(host().textContent).not.toContain('Range'); }); it('toggles a tab closed when it is clicked while open', () => { setReadout(); fixture.componentRef.setInput('defaultTab', 'readout'); fixture.detectChanges(); tab('Readout').click(); fixture.detectChanges(); expect(host().querySelector('[role="tabpanel"]')).toBeNull(); expect(tab('Readout').getAttribute('aria-selected')).toBe('false'); tab('Search').click(); fixture.detectChanges(); expect(host().querySelector('input[type="text"]')).not.toBeNull(); }); it('emits the flipped layer and nothing else', () => { fixture.componentRef.setInput('display', DEFAULT_HUD_DISPLAY); fixture.componentRef.setInput('defaultTab', 'display'); fixture.detectChanges(); const emitted: HudDisplay[] = []; fixture.componentInstance.displayChange.subscribe((display) => emitted.push(display)); const orbits = [...host().querySelectorAll('[aria-pressed]')].find((b) => b.textContent?.includes('Orbits')); orbits?.click(); expect(emitted).toEqual([{ ...DEFAULT_HUD_DISPLAY, orbits: false }]); }); it('reflects the layer state it is given as pressed buttons', () => { fixture.componentRef.setInput('display', { ...DEFAULT_HUD_DISPLAY, grid: false }); fixture.componentRef.setInput('defaultTab', 'display'); fixture.detectChanges(); const pressed = [...host().querySelectorAll('[aria-pressed]')].map((b) => `${b.textContent?.trim()}=${b.getAttribute('aria-pressed')}`); expect(pressed).toEqual(['Labels=true', 'Orbits=true', 'Grid=false', 'Deep sky=true', 'Sky=true', 'Systems=true', 'Jump links=false', 'Plan view=false']); }); it('says how to keep a place, rather than showing an empty list', () => { fixture.componentRef.setInput('defaultTab', 'bookmarks'); fixture.detectChanges(); expect(host().querySelector('#dock-panel-bookmarks ul')).toBeNull(); expect(host().textContent).toContain('Nothing kept yet'); }); it('lists what has been kept, newest first, and says which kind each is', () => { const bookmarks = TestBed.inject(BookmarksStore); bookmarks.toggle({ kind: 'star', id: 7, name: 'Sirius' }); bookmarks.toggle({ kind: 'body', id: 'earth', name: 'Earth' }); fixture.componentRef.setInput('defaultTab', 'bookmarks'); fixture.detectChanges(); const rows = [...host().querySelectorAll('#dock-panel-bookmarks li')].map((li) => li.textContent?.replace(/\s+/g, ' ').trim()); expect(rows[0]).toContain('Earth'); expect(rows[0]).toContain('Body'); expect(rows[1]).toContain('Sirius'); expect(rows[1]).toContain('System'); }); it('hands back the place that was chosen, whole', () => { const bookmarks = TestBed.inject(BookmarksStore); bookmarks.toggle({ kind: 'star', id: 7, name: 'Sirius' }); fixture.componentRef.setInput('defaultTab', 'bookmarks'); fixture.detectChanges(); const chosen: unknown[] = []; fixture.componentInstance.bookmarkChosen.subscribe((bookmark) => chosen.push(bookmark)); host().querySelector('#dock-panel-bookmarks li button')?.click(); expect(chosen).toEqual([{ kind: 'star', id: 7, name: 'Sirius' }]); }); it('forgets one without disturbing the rest', () => { const bookmarks = TestBed.inject(BookmarksStore); bookmarks.toggle({ kind: 'star', id: 7, name: 'Sirius' }); bookmarks.toggle({ kind: 'body', id: 'earth', name: 'Earth' }); fixture.componentRef.setInput('defaultTab', 'bookmarks'); fixture.detectChanges(); host().querySelector('[aria-label="Forget Earth"]')?.click(); fixture.detectChanges(); expect(bookmarks.bookmarks().map((bookmark) => bookmark.name)).toEqual(['Sirius']); expect(host().textContent).not.toContain('Earth'); }); it('keeps the star the readout is about, and says so on the control', () => { const bookmarks = TestBed.inject(BookmarksStore); setReadout(); fixture.componentRef.setInput('title', 'Sirius'); fixture.componentRef.setInput('keepableStarId', 7); fixture.componentRef.setInput('defaultTab', 'readout'); fixture.detectChanges(); const keep = host().querySelector('[aria-label="Keep Sirius"]'); expect(keep?.getAttribute('aria-pressed')).toBe('false'); keep?.click(); fixture.detectChanges(); expect(bookmarks.has('star', 7)).toBe(true); expect(host().querySelector('[aria-label="Forget Sirius"]')?.getAttribute('aria-pressed')).toBe('true'); }); it('keeps the one system whose catalogue id is zero, which truthiness would have lost', () => { // The Sun is star 0. A `@if (id; as ...)` reads that as "no star" and hides the control. const bookmarks = TestBed.inject(BookmarksStore); setReadout(); fixture.componentRef.setInput('title', 'Sol'); fixture.componentRef.setInput('keepableStarId', 0); fixture.componentRef.setInput('defaultTab', 'readout'); fixture.detectChanges(); host().querySelector('[aria-label="Keep Sol"]')?.click(); fixture.detectChanges(); expect(bookmarks.has('star', 0)).toBe(true); }); it('offers nothing to keep where the readout is a scale rather than a place', () => { setReadout(); fixture.componentRef.setInput('defaultTab', 'readout'); fixture.detectChanges(); expect(host().querySelector('[aria-label^="Keep"]')).toBeNull(); }); it('opens the search on "/" from anywhere but a text field', () => { setReadout(); fixture.componentRef.setInput('defaultTab', 'readout'); fixture.detectChanges(); document.dispatchEvent(new KeyboardEvent('keydown', { key: '/', bubbles: true })); fixture.detectChanges(); expect(tab('Search').getAttribute('aria-selected')).toBe('true'); }); it('hands the panel back to the readout once a result is picked', () => { setReadout(); fixture.componentRef.setInput('defaultTab', 'search'); fixture.detectChanges(); fixture.componentInstance.onPicked(); fixture.detectChanges(); expect(tab('Readout').getAttribute('aria-selected')).toBe('true'); }); it('will not plot from a departure that was typed but never chosen', () => { setReadout(); fixture.componentRef.setInput('routing', true); fixture.componentRef.setInput('currentStar', { id: 3, name: "Barnard's Star", subtitle: '1.8 pc' }); fixture.componentRef.setInput('routeOptions', [{ id: 7, name: 'Sirius', subtitle: '2.6 pc' }]); fixture.componentRef.setInput('defaultTab', 'routes'); fixture.detectChanges(); const plot = () => [...host().querySelectorAll('button')].find((button) => button.textContent?.includes('Plot route'))!; const type = (field: string, value: string) => { const input = host().querySelector(`#route-${field}`)!; input.value = value; input.dispatchEvent(new Event('input')); fixture.detectChanges(); }; type('to', 'Sir'); host().querySelector('#dock-panel-routes ul button')!.click(); fixture.detectChanges(); // With the departure field empty, the view's own star stands in for it. expect(plot().disabled).toBe(false); // Text that names no chosen star is not a departure: plotting from the view's star instead // would name one place and leave from another. type('from', 'Sol'); expect(plot().disabled).toBe(true); type('from', ''); expect(plot().disabled).toBe(false); }); it('does not replay the acquire wipe over the Routes panel, whose entries survive the trip', () => { setReadout(); fixture.componentRef.setInput('routing', true); fixture.componentRef.setInput('display', DEFAULT_HUD_DISPLAY); fixture.componentRef.setInput('defaultTab', 'routes'); fixture.detectChanges(); // The wipe clips its panel for 380 ms, which swallows clicks on entries that are already there. expect(host().querySelector('#dock-panel-routes')?.classList.contains('hud-acquire')).toBe(false); tab('Display').click(); fixture.detectChanges(); expect(host().querySelector('#dock-panel-display')?.classList.contains('hud-acquire')).toBe(true); }); it('keeps what the Routes panel was set to across a trip to another tab', () => { setReadout(); fixture.componentRef.setInput('routing', true); fixture.componentRef.setInput('routeOptions', [{ id: 7, name: 'Sirius', subtitle: '2.6 pc' }]); fixture.componentRef.setInput('defaultTab', 'routes'); fixture.detectChanges(); const destination = host().querySelector('#route-to')!; destination.value = 'Sir'; destination.dispatchEvent(new Event('input')); fixture.detectChanges(); host().querySelector('#dock-panel-routes ul button')!.click(); const range = host().querySelector('#route-range')!; range.value = '6'; range.dispatchEvent(new Event('input')); fixture.detectChanges(); tab('Readout').click(); fixture.detectChanges(); expect(host().querySelector('#dock-panel-routes')!.hidden).toBe(true); tab('Routes').click(); fixture.detectChanges(); expect(host().querySelector('#dock-panel-routes')!.hidden).toBe(false); expect(host().querySelector('#route-to')!.value).toBe('Sirius'); expect(host().querySelector('#route-range')!.value).toBe('6'); }); });