import { HttpErrorResponse } from '@angular/common/http'; import { TestBed } from '@angular/core/testing'; import { Subject, of, throwError } from 'rxjs'; import { vi } from 'vitest'; import { GameApiService } from '../../core/api/game-api.service'; import { LocalLocationStore } from './local-location.store'; import { WorldStore } from './world.store'; const trackResult = { interactionKey: 'inspect-tracks', title: 'Verdächtige Spuren', text: 'Frische Stiefelabdrücke führen nach Osten.', }; function setup(api: Partial = {}, world: Partial = {}) { TestBed.configureTestingModule({ providers: [ LocalLocationStore, { provide: GameApiService, useValue: api }, { provide: WorldStore, useValue: { load: vi.fn(), ...world } }, ], }); return TestBed.inject(LocalLocationStore); } describe('LocalLocationStore', () => { it('loads the location through the world store rather than a second fetch path', async () => { const load = vi.fn().mockResolvedValue(undefined); const store = setup({}, { load }); await store.load(); expect(load).toHaveBeenCalledTimes(1); }); it('opens the panel with the result the server returned', async () => { const store = setup({ runLocationInteraction: vi.fn().mockReturnValue(of(trackResult)), }); await store.runInteraction('inspect-tracks'); expect(store.interactionResult()).toEqual(trackResult); expect(store.interactionError()).toBeNull(); expect(store.interactionOpen()).toBe(true); }); it('marks only the running interaction as pending', async () => { const pending = new Subject(); const store = setup({ runLocationInteraction: vi.fn().mockReturnValue(pending), }); const running = store.runInteraction('inspect-tracks'); expect(store.interactionPending()).toBe('inspect-tracks'); pending.next(trackResult); pending.complete(); await running; expect(store.interactionPending()).toBeNull(); }); it('ignores a second interaction while one is still running', async () => { const pending = new Subject(); const runLocationInteraction = vi.fn().mockReturnValue(pending); const store = setup({ runLocationInteraction }); const running = store.runInteraction('inspect-tracks'); await store.runInteraction('search-abandoned-wagon'); expect(runLocationInteraction).toHaveBeenCalledTimes(1); expect(runLocationInteraction).toHaveBeenCalledWith('inspect-tracks'); pending.next(trackResult); pending.complete(); await running; }); it('translates a rejected interaction into a readable message without navigating', async () => { const store = setup({ runLocationInteraction: vi.fn().mockReturnValue( throwError( () => new HttpErrorResponse({ status: 400, error: { code: 'LOCATION_INTERACTION_UNAVAILABLE' }, }), ), ), }); await store.runInteraction('inspect-tracks'); expect(store.interactionError()).toBe("There's nothing to discover here."); expect(store.interactionResult()).toBeNull(); expect(store.interactionOpen()).toBe(true); }); it('falls back to a generic message for an unmapped failure', async () => { const store = setup({ runLocationInteraction: vi .fn() .mockReturnValue(throwError(() => new HttpErrorResponse({ status: 500 }))), }); await store.runInteraction('inspect-tracks'); expect(store.interactionError()).toBe("This action isn't possible right now."); }); it('closes the panel without clearing the location', async () => { const store = setup({ runLocationInteraction: vi.fn().mockReturnValue(of(trackResult)), }); await store.runInteraction('inspect-tracks'); store.closeInteraction(); expect(store.interactionOpen()).toBe(false); expect(store.interactionResult()).toBeNull(); }); it('resolves the hotspot an action mirrors', () => { const store = setup(); expect( store.interactionKeyOf({ key: 'investigate-tracks', label: 'Spuren untersuchen', type: 'INVESTIGATE', iconKey: 'investigate', enabled: true, poiKey: 'inspect-tracks', }), ).toBe('inspect-tracks'); }); });