import { signal } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { vi } from 'vitest'; import type { QuestView } from '../../core/api/game-api.models'; import { QuestPageComponent } from './quest-page.component'; import { QuestStore } from './quest.store'; function quest(over: Partial = {}): QuestView { return { key: 'trouble-beyond-the-gate', title: 'Trouble Beyond the Gate', description: 'The warden wants five Ashen Pelts.', status: 'ACTIVE', objectives: [ { key: 'collect-pelts-first', description: 'Collect Ashen Pelts', type: 'COLLECT_ITEM', targetKey: 'ash-pelt', required: 5, current: 1, completed: false, }, ], currentObjectiveKey: 'collect-pelts-first', hint: null, ...over, }; } async function setup(quests: QuestView[], error: string | null = null) { const questsSignal = signal(quests); const store = { quests: questsSignal.asReadonly(), activeQuests: signal(quests.filter((entry) => entry.status === 'ACTIVE')), completedQuests: signal( quests.filter((entry) => entry.status === 'COMPLETED'), ), availableQuests: signal( quests.filter((entry) => entry.status === 'AVAILABLE'), ), loading: signal(false), error: signal(error), load: vi.fn().mockResolvedValue(undefined), setQuest: vi.fn(), }; await TestBed.configureTestingModule({ imports: [QuestPageComponent], providers: [{ provide: QuestStore, useValue: store }], }).compileComponents(); const fixture = TestBed.createComponent(QuestPageComponent); fixture.detectChanges(); return { fixture, store, text: () => fixture.nativeElement.textContent ?? '' }; } describe('QuestPageComponent', () => { it('loads the quest log on init', async () => { const { store } = await setup([quest()]); expect(store.load).toHaveBeenCalledOnce(); }); it('renders the active quest with its current objective', async () => { const { fixture, text } = await setup([quest()]); expect(text()).toContain('Trouble Beyond the Gate'); expect(text()).toContain('The warden wants five Ashen Pelts.'); expect( fixture.nativeElement.querySelector('[data-objective-description]') .textContent, ).toContain('Collect Ashen Pelts'); }); it('renders collect progress as current over required', async () => { const { fixture } = await setup([quest()]); // The block slice ยง12 prints verbatim. expect( fixture.nativeElement .querySelector('[data-objective-progress]') .textContent.replace(/\s+/g, ' ') .trim(), ).toBe('1 / 5'); }); it('renders the blocked hint under the objective', async () => { const { fixture } = await setup([ quest({ hint: 'You cannot carry enough pelts. Return to the South Gate Warden.', }), ]); expect( fixture.nativeElement.querySelector('[data-objective-hint]').textContent, ).toContain('You cannot carry enough pelts.'); }); it('shows no hint when the step is merely unfinished', async () => { const { fixture } = await setup([quest()]); expect( fixture.nativeElement.querySelector('[data-objective-hint]'), ).toBeNull(); }); it('shows an empty state when nothing is taken on', async () => { const { text } = await setup([]); expect(text()).toContain('You have taken nothing on.'); }); it('lists completed quests separately from active ones', async () => { const { fixture, text } = await setup([ quest({ key: 'done-quest', title: 'An Older Errand', status: 'COMPLETED', currentObjectiveKey: null, }), ]); expect(text()).toContain('An Older Errand'); expect( fixture.nativeElement.querySelector('[data-quest-completed]'), ).not.toBeNull(); // The active panel is still empty, so the two do not blur together. expect(text()).toContain('You have taken nothing on.'); }); it('offers a retry when the log could not be read', async () => { const { fixture, store } = await setup([], 'Could not load your quests.'); fixture.nativeElement.querySelector('[data-quest-retry]').click(); expect(store.load).toHaveBeenCalledTimes(2); }); });