Files
ashen-realms/apps/web/src/app/features/world/local-location.store.spec.ts
Bastian Wagner 5586e6c672 feat(web): translate error messages surfaced from API error codes to English
Translates the five client-side error-message maps (world, hunting, combat,
inventory, local-location stores) that re-translate already-English API
error codes back to display text, per design doc §4. Also fixes four
component specs that hard-coded the same German strings when asserting
rendered error text (combat-page, hunt-page, location-interaction-panel,
location-page).
2026-08-21 21:29:10 +02:00

140 lines
4.3 KiB
TypeScript

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<GameApiService> = {}, world: Partial<WorldStore> = {}) {
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<typeof trackResult>();
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<typeof trackResult>();
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');
});
});