import { ComponentFixture, TestBed } from '@angular/core/testing'; import type { LocationPointOfInterest, NpcMarker, } from '../../../core/api/game-api.models'; import { LocationPoiComponent } from './location-poi.component'; const wagon: LocationPointOfInterest = { key: 'search-abandoned-wagon', title: 'Abandoned Wagon', actionLabel: 'Search', type: 'SEARCH', iconKey: 'search', xPercent: 80, yPercent: 68, enabled: true, }; async function setup( poi: LocationPointOfInterest = wagon, busy = false, markers: NpcMarker[] = [], ): Promise<{ fixture: ComponentFixture; activated: LocationPointOfInterest[]; button: HTMLButtonElement; }> { await TestBed.configureTestingModule({ imports: [LocationPoiComponent], }).compileComponents(); const fixture = TestBed.createComponent(LocationPoiComponent); fixture.componentRef.setInput('poi', poi); fixture.componentRef.setInput('busy', busy); fixture.componentRef.setInput('markers', markers); const activated: LocationPointOfInterest[] = []; fixture.componentInstance.activate.subscribe((value) => activated.push(value)); fixture.detectChanges(); return { fixture, activated, button: fixture.nativeElement.querySelector('button') as HTMLButtonElement, }; } describe('LocationPoiComponent', () => { it('anchors itself with the percentage coordinates so it scales with the artwork', async () => { const { fixture } = await setup(); const host = fixture.nativeElement as HTMLElement; expect(host.style.left).toBe('80%'); expect(host.style.top).toBe('68%'); }); it('renders title and action label', async () => { const { fixture } = await setup(); const text = (fixture.nativeElement as HTMLElement).textContent ?? ''; expect(text).toContain('Abandoned Wagon'); expect(text).toContain('Search'); }); it('emits the whole hotspot when clicked', async () => { const { button, activated } = await setup(); button.click(); expect(activated).toEqual([wagon]); }); it('is reachable and activatable from the keyboard', async () => { const { button, activated } = await setup(); // A native button gives Enter and Space for free; assert it stayed a // button rather than becoming a click-only div. expect(button.tagName).toBe('BUTTON'); expect(button.tabIndex).toBe(0); button.focus(); expect(document.activeElement).toBe(button); button.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); button.click(); expect(activated).toHaveLength(1); }); it('does nothing when the hotspot is disabled', async () => { const { button, activated } = await setup({ ...wagon, enabled: false }); expect(button.disabled).toBe(true); button.click(); expect(activated).toEqual([]); }); it('does nothing while an interaction is already running', async () => { const { button, activated } = await setup(wagon, true); button.click(); expect(activated).toEqual([]); }); it('describes itself with both title and action for screen readers', async () => { const { button } = await setup(); expect(button.getAttribute('aria-label')).toBe('Abandoned Wagon: Search'); }); it('falls back to the title alone when a hotspot has no action label', async () => { const { button } = await setup({ ...wagon, actionLabel: undefined, }); expect(button.getAttribute('aria-label')).toBe('Abandoned Wagon'); }); it('renders no badge without markers', async () => { const { fixture } = await setup(); expect(fixture.nativeElement.querySelector('[data-poi-badge]')).toBeNull(); }); it('renders a badge for a quest that can be started here', async () => { const { fixture, button } = await setup(wagon, false, ['QUEST_AVAILABLE']); expect( fixture.nativeElement.querySelector('[data-poi-badge]').textContent.trim(), ).toBe('!'); expect(button.getAttribute('aria-label')).toBe( 'Abandoned Wagon: Search, quest available', ); }); it('renders a badge when the current step waits here', async () => { const { fixture, button } = await setup(wagon, false, ['QUEST_TURN_IN']); expect( fixture.nativeElement.querySelector('[data-poi-badge]').textContent.trim(), ).toBe('?'); expect(button.getAttribute('aria-label')).toContain('quest step ready'); }); it('ignores markers that are not quest markers', async () => { const { fixture } = await setup(wagon, false, ['MERCHANT', 'EXCHANGE']); expect(fixture.nativeElement.querySelector('[data-poi-badge]')).toBeNull(); }); it('renders one badge at most, preferring the waiting step', async () => { const { fixture } = await setup(wagon, false, [ 'MERCHANT', 'QUEST_AVAILABLE', 'QUEST_TURN_IN', ]); const badges = fixture.nativeElement.querySelectorAll('[data-poi-badge]'); expect(badges).toHaveLength(1); expect(badges[0].textContent.trim()).toBe('?'); }); });