The map had one way to read a distance: the Range readout, a number for how far back the camera is. The local grid's five rings sat at 50 to 250 pc, fixed and unlabelled. They said nothing from inside a 2 pc hop, and nothing past 250 pc now that the Hipparcos stars Gaia places there are drawn. A scale bar now sits under the scale rail. It shows the longest round length (1, 2 or 5 x 10^n) that fits in 120 px, in AU inside a system and in parsecs or kiloparsecs outside. It is measured at the depth the view is centred on, since under perspective every depth has its own scale; under the plan view it is exact everywhere. The local grid's rings are now distances from the Sun, at a round step of about a fifth of the camera's distance and out past the camera: 50 to 350 pc from the opening view, 2 to 20 pc from twenty parsecs out. Each ring is labelled with its distance, on the side facing what the view is centred on, or across the far side of the grid when that is the Sun (the near side is under the camera and out of frame). The survey edge at 250 pc stays called out, as "Survey edge", whatever the step. The rounding lives in one place, scale-bar.ts, shared by the bar and the rings and tested there. Its formatter keeps three significant digits: one digit, enough for the bar's round lengths, printed the 250 pc ring as "300 pc". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
96 lines
3.9 KiB
TypeScript
96 lines
3.9 KiB
TypeScript
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
import { beforeEach, describe, expect, it } from 'vitest';
|
|
|
|
import { ViewLevel } from '../../shared/state/navigation.store';
|
|
import { StarmapHudComponent } from './starmap-hud.component';
|
|
|
|
describe('StarmapHudComponent', () => {
|
|
let fixture: ComponentFixture<StarmapHudComponent>;
|
|
|
|
function render(level: ViewLevel): HTMLElement {
|
|
fixture.componentRef.setInput('level', level);
|
|
fixture.detectChanges();
|
|
return fixture.nativeElement as HTMLElement;
|
|
}
|
|
|
|
function buttonLabels(host: HTMLElement): string[] {
|
|
return [...host.querySelectorAll('nav button')].map((button) => button.textContent?.trim() ?? '');
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
await TestBed.configureTestingModule({ imports: [StarmapHudComponent] }).compileComponents();
|
|
fixture = TestBed.createComponent(StarmapHudComponent);
|
|
});
|
|
|
|
it('offers the way back down from the outermost scale', () => {
|
|
// The two outer scales share one space, so the ladder goes both ways between them.
|
|
expect(buttonLabels(render('galactic'))).toEqual(['Solar Neighbourhood']);
|
|
});
|
|
|
|
it('offers the Milky Way from the solar neighbourhood', () => {
|
|
expect(buttonLabels(render('galaxy'))).toEqual(['Milky Way']);
|
|
});
|
|
|
|
it('offers both wider scales from inside a system', () => {
|
|
expect(buttonLabels(render('system'))).toEqual(['Milky Way', 'Solar Neighbourhood']);
|
|
});
|
|
|
|
it('never offers the system scale, which needs a star picked first', () => {
|
|
for (const level of ['galactic', 'galaxy', 'system'] as const) {
|
|
expect(buttonLabels(render(level))).not.toContain('System');
|
|
}
|
|
});
|
|
|
|
it('marks the current scale rather than making it a button that goes nowhere', () => {
|
|
// Load-bearing beyond tidiness: this marker is how the end-to-end tests tell which scale the
|
|
// view has actually settled at.
|
|
const host = render('galaxy');
|
|
const current = host.querySelector('[data-testid="hud-current-level"]');
|
|
expect(current?.textContent?.trim()).toBe('Solar Neighbourhood');
|
|
expect(current?.getAttribute('aria-current')).toBe('step');
|
|
expect(buttonLabels(host)).not.toContain('Solar Neighbourhood');
|
|
});
|
|
|
|
it('marks exactly one scale as current', () => {
|
|
for (const level of ['galactic', 'galaxy', 'system'] as const) {
|
|
expect(render(level).querySelectorAll('[data-testid="hud-current-level"]')).toHaveLength(1);
|
|
}
|
|
});
|
|
|
|
it('emits the scale that was asked for', () => {
|
|
const host = render('system');
|
|
const emitted: ViewLevel[] = [];
|
|
fixture.componentInstance.levelSelected.subscribe((level) => emitted.push(level));
|
|
|
|
host.querySelectorAll('nav button').forEach((button) => (button as HTMLButtonElement).click());
|
|
|
|
expect(emitted).toEqual(['galactic', 'galaxy']);
|
|
});
|
|
|
|
it('names what the view is holding on the banner across the top', () => {
|
|
fixture.componentRef.setInput('level', 'system');
|
|
fixture.componentRef.setInput('title', 'Sol');
|
|
fixture.detectChanges();
|
|
|
|
expect((fixture.nativeElement as HTMLElement).querySelector('[data-testid="hud-banner"]')?.textContent?.trim()).toBe('Sol');
|
|
});
|
|
|
|
it('shows no banner when the view is holding nothing', () => {
|
|
// An empty nameplate is worse than none: it reads as a selection that failed to resolve.
|
|
expect(render('galaxy').querySelector('[data-testid="hud-banner"]')).toBeNull();
|
|
});
|
|
|
|
it('shows the scale it is given, as a bar of that width', () => {
|
|
fixture.componentRef.setInput('scale', { label: '10 pc', widthPx: 100 });
|
|
const bar = render('galaxy').querySelector<HTMLElement>('[data-testid="hud-scale"]');
|
|
|
|
expect(bar?.getAttribute('aria-label')).toBe('Scale: 10 pc');
|
|
expect(bar?.textContent?.trim()).toBe('10 pc');
|
|
expect(bar?.querySelector<HTMLElement>('span:last-child')?.style.width).toBe('100px');
|
|
});
|
|
|
|
it('shows no scale bar while there is no scale to show', () => {
|
|
expect(render('galaxy').querySelector('[data-testid="hud-scale"]')).toBeNull();
|
|
});
|
|
});
|