feat: add hunt page, combat placeholder, and Jagd navigation
Wires up Slice 0.2 end-to-end: HuntPageComponent renders the hunting-unavailable/ready/loading/encounters-found states off HuntingStore and WorldStore, Angreifen hands the HuntEncounter id to a new inert CombatPlaceholderPageComponent via /combat/new, the Jagd nav entry is enabled with router-driven active state (matching Karte's), and the context panel now lists possible encounters for hunting-enabled locations. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
import { signal } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { Router, provideRouter } from '@angular/router';
|
||||
import { vi } from 'vitest';
|
||||
import type { CurrentLocationResponse, HuntResult } from '../../../core/api/game-api.models';
|
||||
import { WorldStore } from '../../world/world.store';
|
||||
import { HuntingStore } from '../hunting.store';
|
||||
import { HuntPageComponent } from './hunt-page.component';
|
||||
|
||||
const southGate: CurrentLocationResponse = {
|
||||
id: 'south-gate-id',
|
||||
key: 'south-gate',
|
||||
name: 'Südtor von Graufurt',
|
||||
description: 'Der letzte sichere Schritt vor den Aschenfeldern.',
|
||||
regionKey: 'ashen-fields',
|
||||
minRecommendedLevel: 1,
|
||||
maxRecommendedLevel: 1,
|
||||
dangerLevel: 0,
|
||||
isSafe: true,
|
||||
huntingEnabled: false,
|
||||
artworkPath: '/images/backgrounds/Suedtor.png',
|
||||
connections: [],
|
||||
possibleMonsters: [],
|
||||
};
|
||||
|
||||
const burnedRoad: CurrentLocationResponse = {
|
||||
...southGate,
|
||||
id: 'burned-road-id',
|
||||
key: 'burned-road',
|
||||
name: 'Verbrannte Straße',
|
||||
description: 'Die erste Jagdzone zwischen Asche und zerbrochenen Wagen.',
|
||||
isSafe: false,
|
||||
huntingEnabled: true,
|
||||
artworkPath: '/images/backgrounds/Aschestrasse.png',
|
||||
possibleMonsters: ['Aschenratte', 'Straßenräuber'],
|
||||
connections: [],
|
||||
};
|
||||
|
||||
const threeEncounterHunt: HuntResult = {
|
||||
id: 'hunt-id',
|
||||
location: { id: 'burned-road-id', key: 'burned-road', name: 'Verbrannte Straße' },
|
||||
encounters: [
|
||||
{
|
||||
id: 'encounter-1',
|
||||
monster: {
|
||||
key: 'ash-rat',
|
||||
name: 'Aschenratte',
|
||||
level: 1,
|
||||
artworkPath: '/images/enemies/AshRat.png',
|
||||
},
|
||||
dangerRating: 'WEAK',
|
||||
},
|
||||
{
|
||||
id: 'encounter-2',
|
||||
monster: {
|
||||
key: 'road-bandit',
|
||||
name: 'Straßenräuber',
|
||||
level: 3,
|
||||
artworkPath: '/images/enemies/RoadBandit.png',
|
||||
},
|
||||
dangerRating: 'MATCH',
|
||||
},
|
||||
{
|
||||
id: 'encounter-3',
|
||||
monster: {
|
||||
key: 'ash-rat',
|
||||
name: 'Aschenratte',
|
||||
level: 1,
|
||||
artworkPath: '/images/enemies/AshRat.png',
|
||||
},
|
||||
dangerRating: 'WEAK',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('HuntPageComponent', () => {
|
||||
let worldStore: {
|
||||
currentLocation: ReturnType<typeof signal<CurrentLocationResponse | null>>;
|
||||
};
|
||||
let huntingStore: {
|
||||
currentHunt: ReturnType<typeof signal<HuntResult | null>>;
|
||||
loading: ReturnType<typeof signal<boolean>>;
|
||||
error: ReturnType<typeof signal<string | null>>;
|
||||
encounters: () => HuntResult['encounters'];
|
||||
startHunt: ReturnType<typeof vi.fn>;
|
||||
refreshHunt: ReturnType<typeof vi.fn>;
|
||||
selectEncounter: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
let router: Router;
|
||||
|
||||
async function setup(location: CurrentLocationResponse | null, hunt: HuntResult | null = null) {
|
||||
worldStore = { currentLocation: signal(location) };
|
||||
const currentHunt = signal(hunt);
|
||||
huntingStore = {
|
||||
currentHunt,
|
||||
loading: signal(false),
|
||||
error: signal<string | null>(null),
|
||||
encounters: () => currentHunt()?.encounters ?? [],
|
||||
startHunt: vi.fn(() => Promise.resolve()),
|
||||
refreshHunt: vi.fn(() => Promise.resolve()),
|
||||
selectEncounter: vi.fn(),
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [HuntPageComponent],
|
||||
providers: [
|
||||
provideRouter([]),
|
||||
{ provide: WorldStore, useValue: worldStore },
|
||||
{ provide: HuntingStore, useValue: huntingStore },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
router = TestBed.inject(Router);
|
||||
vi.spyOn(router, 'navigate').mockResolvedValue(true);
|
||||
|
||||
const fixture = TestBed.createComponent(HuntPageComponent);
|
||||
fixture.detectChanges();
|
||||
return fixture;
|
||||
}
|
||||
|
||||
it('shows the hunting-unavailable state at the Südtor, with no Jagd beginnen button, and a working Zur Karte action', async () => {
|
||||
const fixture = await setup(southGate);
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
|
||||
expect(element.textContent).toContain('Keine Jagd verfügbar');
|
||||
expect(element.textContent).toContain(
|
||||
'Am Südtor von Graufurt gibt es keine regulären Jagdgebiete.',
|
||||
);
|
||||
expect(
|
||||
Array.from(element.querySelectorAll('button')).some(
|
||||
(button) => button.textContent?.trim() === 'Jagd beginnen',
|
||||
),
|
||||
).toBe(false);
|
||||
|
||||
const toWorldButton = element.querySelector<HTMLButtonElement>('[data-hunt-to-world]');
|
||||
expect(toWorldButton?.textContent?.trim()).toBe('Zur Karte');
|
||||
toWorldButton?.click();
|
||||
|
||||
expect(router.navigate).toHaveBeenCalledWith(['/world']);
|
||||
});
|
||||
|
||||
it('calls startHunt when Jagd beginnen is clicked at a hunting-enabled location', async () => {
|
||||
const fixture = await setup(burnedRoad);
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
|
||||
element.querySelector<HTMLButtonElement>('[data-hunt-start]')?.click();
|
||||
|
||||
expect(huntingStore.startHunt).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('renders 3 encounter cards, duplicates included, with the correct data', async () => {
|
||||
const fixture = await setup(burnedRoad, threeEncounterHunt);
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
|
||||
const cards = element.querySelectorAll('app-encounter-card');
|
||||
expect(cards.length).toBe(3);
|
||||
expect(element.textContent).toMatch(/Aschenratte[\s\S]*Straßenräuber[\s\S]*Aschenratte/);
|
||||
expect(element.querySelectorAll('img[src="/images/enemies/AshRat.png"]').length).toBe(2);
|
||||
expect(element.querySelectorAll('img[src="/images/enemies/RoadBandit.png"]').length).toBe(1);
|
||||
expect(element.textContent).toContain('Stufe 1');
|
||||
expect(element.textContent).toContain('Stufe 3');
|
||||
});
|
||||
|
||||
it('calls refreshHunt when Neu suchen is clicked', async () => {
|
||||
const fixture = await setup(burnedRoad, threeEncounterHunt);
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
|
||||
element.querySelector<HTMLButtonElement>('[data-hunt-refresh]')?.click();
|
||||
|
||||
expect(huntingStore.refreshHunt).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('navigates to /combat/new with the encounter id (not the monster key) when Angreifen is clicked', async () => {
|
||||
const fixture = await setup(burnedRoad, threeEncounterHunt);
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
|
||||
const attackButtons = Array.from(element.querySelectorAll('button')).filter(
|
||||
(button) => button.textContent?.trim() === 'Angreifen',
|
||||
);
|
||||
expect(attackButtons.length).toBe(3);
|
||||
|
||||
attackButtons[1].click();
|
||||
|
||||
expect(huntingStore.selectEncounter).toHaveBeenCalledWith('encounter-2');
|
||||
expect(router.navigate).toHaveBeenCalledWith(['/combat/new'], {
|
||||
queryParams: { encounterId: 'encounter-2' },
|
||||
});
|
||||
expect(router.navigate).not.toHaveBeenCalledWith(['/combat/new'], {
|
||||
queryParams: { encounterId: 'road-bandit' },
|
||||
});
|
||||
});
|
||||
|
||||
it('does not trigger a hunt automatically on page entry', async () => {
|
||||
await setup(burnedRoad);
|
||||
|
||||
expect(huntingStore.startHunt).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows a loading state and disables the triggering action', async () => {
|
||||
const fixture = await setup(burnedRoad);
|
||||
huntingStore.loading.set(true);
|
||||
fixture.detectChanges();
|
||||
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
expect(element.textContent).toContain('Du suchst nach Spuren...');
|
||||
expect(element.querySelector<HTMLButtonElement>('[data-hunt-start]')?.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it('displays a hunting error and retries via startHunt when there is no current hunt', async () => {
|
||||
const fixture = await setup(burnedRoad);
|
||||
huntingStore.error.set('An diesem Ort gibt es keine Jagdgebiete.');
|
||||
fixture.detectChanges();
|
||||
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
expect(element.querySelector('[role="alert"]')?.textContent).toContain(
|
||||
'An diesem Ort gibt es keine Jagdgebiete.',
|
||||
);
|
||||
element.querySelector<HTMLButtonElement>('[data-hunt-retry]')?.click();
|
||||
|
||||
expect(huntingStore.startHunt).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user