Repo-wide grep sweep (apps/web/src, apps/api/src) for leftover German content strings missed by Tasks 1-9, mostly in spec fixtures/assertions that mirror already-translated seed content (monster/item names, POI titles and action labels, location names/descriptions) plus a few real source-file gaps: - inventory-detail-panel.component.ts: STAT_LABELS (Waffenschaden, Angriff, Leben, Rüstung) were never translated by Task 6; now match the identical English labels already used in inventory-page.component.html. - app-shell.component.html: aria-label="Spielinhalt" -> "Game content" (this file was outside every prior task's file list). - location-interaction-panel.component.spec.ts: dead NPC-quote fixture translated to match the real wounded-scout POI text. Code comments referencing German source-spec section titles or not-yet-seeded faction names, and inline calculation-documentation comments, are left as-is per the source spec's scope (dev-facing comments may stay German). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ACkMEDYiwtcfchqKkiUJNX
207 lines
6.9 KiB
TypeScript
207 lines
6.9 KiB
TypeScript
import { signal } from '@angular/core';
|
|
import { TestBed } from '@angular/core/testing';
|
|
import { By } from '@angular/platform-browser';
|
|
import { vi } from 'vitest';
|
|
import type { CharacterResponse, EquipmentResponse, InventoryItem, InventoryResponse } from '../../core/api/game-api.models';
|
|
import { WorldStore } from '../world/world.store';
|
|
import { InventoryDetailPanelComponent } from './inventory-detail-panel.component';
|
|
import { InventoryPageComponent } from './inventory-page.component';
|
|
import { InventoryStore } from './inventory.store';
|
|
|
|
const inventory: InventoryResponse = {
|
|
items: [
|
|
{
|
|
id: 'item-sword',
|
|
quantity: 1,
|
|
equipped: true,
|
|
item: {
|
|
key: 'worn-short-sword',
|
|
name: 'Abgenutztes Kurzschwert',
|
|
description: 'Item description.',
|
|
rarity: 'COMMON',
|
|
equipmentSlot: 'WEAPON',
|
|
weaponDamage: 8,
|
|
bonusAttack: 0,
|
|
bonusHp: 0,
|
|
bonusArmor: 0,
|
|
iconPath: '/images/items/worn-short-sword.png',
|
|
},
|
|
},
|
|
{
|
|
id: 'item-blade',
|
|
quantity: 1,
|
|
equipped: false,
|
|
item: {
|
|
key: 'bandit-blade',
|
|
name: 'Bandit Blade',
|
|
description: 'Item description.',
|
|
rarity: 'COMMON',
|
|
equipmentSlot: 'WEAPON',
|
|
weaponDamage: 11,
|
|
bonusAttack: 1,
|
|
bonusHp: 0,
|
|
bonusArmor: 0,
|
|
iconPath: '/images/items/bandit-blade.png',
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
const equipment: EquipmentResponse = {
|
|
slots: {
|
|
WEAPON: { characterItemId: 'item-sword', item: { key: 'worn-short-sword', name: 'Abgenutztes Kurzschwert', rarity: 'COMMON', iconPath: '/images/items/worn-short-sword.png' } },
|
|
HEAD: null,
|
|
CHEST: null,
|
|
HANDS: null,
|
|
LEGS: null,
|
|
FEET: null,
|
|
AMULET: null,
|
|
},
|
|
stats: { maxHp: 100, attack: 6, weaponDamage: 8, armor: 0 },
|
|
};
|
|
|
|
const character: CharacterResponse = {
|
|
id: 'character-1',
|
|
name: 'Aric Duskwalker',
|
|
renown: 1,
|
|
silver: 0,
|
|
currentHp: 100,
|
|
maxHp: 100,
|
|
attack: 6,
|
|
hpRegenPerSecond: 1,
|
|
hpRegenSince: null,
|
|
currentLocation: { id: 'loc-1', key: 'south-gate', name: 'South Gate' },
|
|
};
|
|
|
|
interface SetupOptions {
|
|
inventoryData?: InventoryResponse;
|
|
selectedItemId?: string | null;
|
|
selectedItem?: InventoryItem | null;
|
|
character?: CharacterResponse | null;
|
|
}
|
|
|
|
async function setup(options: SetupOptions = {}) {
|
|
const inventoryStore = {
|
|
inventory: signal(options.inventoryData ?? inventory),
|
|
equipment: signal(equipment),
|
|
selectedItemId: signal<string | null>(options.selectedItemId ?? null),
|
|
loading: signal(false),
|
|
equipping: signal(false),
|
|
error: signal<string | null>(null),
|
|
load: vi.fn(() => Promise.resolve()),
|
|
selectItem: vi.fn(),
|
|
selectedItem: vi.fn(() => options.selectedItem ?? null),
|
|
equip: vi.fn(() => Promise.resolve()),
|
|
};
|
|
const worldStore = {
|
|
character: signal(options.character === undefined ? character : options.character),
|
|
load: vi.fn(() => Promise.resolve()),
|
|
};
|
|
|
|
await TestBed.configureTestingModule({
|
|
imports: [InventoryPageComponent],
|
|
providers: [
|
|
{ provide: InventoryStore, useValue: inventoryStore },
|
|
{ provide: WorldStore, useValue: worldStore },
|
|
],
|
|
}).compileComponents();
|
|
|
|
const fixture = TestBed.createComponent(InventoryPageComponent);
|
|
fixture.detectChanges();
|
|
return { fixture, inventoryStore, worldStore };
|
|
}
|
|
|
|
describe('InventoryPageComponent', () => {
|
|
it('loads the inventory on init', async () => {
|
|
const { inventoryStore } = await setup();
|
|
expect(inventoryStore.load).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('loads the world state on init when no character has been loaded yet (direct navigation/hard refresh)', async () => {
|
|
const { worldStore } = await setup({ character: null });
|
|
expect(worldStore.load).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('does not call world load again when a character is already present', async () => {
|
|
const { worldStore } = await setup();
|
|
expect(worldStore.load).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('renders one tile per owned item', async () => {
|
|
const { fixture } = await setup();
|
|
const tiles = (fixture.nativeElement as HTMLElement).querySelectorAll('.inventory-page__slot');
|
|
expect(tiles.length).toBe(2);
|
|
});
|
|
|
|
it('marks the equipped item with a badge', async () => {
|
|
const { fixture } = await setup();
|
|
expect((fixture.nativeElement as HTMLElement).querySelector('[data-slot-equipped]')).not.toBeNull();
|
|
});
|
|
|
|
it('selects an item when its tile is clicked', async () => {
|
|
const { fixture, inventoryStore } = await setup();
|
|
(fixture.nativeElement as HTMLElement).querySelectorAll<HTMLButtonElement>('.inventory-page__slot')[1].click();
|
|
|
|
expect(inventoryStore.selectItem).toHaveBeenCalledWith('item-blade');
|
|
});
|
|
|
|
it('shows the equipment overview with all seven slots and empty ones as Empty', async () => {
|
|
const { fixture } = await setup();
|
|
const text = (fixture.nativeElement as HTMLElement).querySelector('.inventory-page__equipment-list')?.textContent ?? '';
|
|
|
|
expect(text).toContain('Weapon');
|
|
expect(text).toContain('Abgenutztes Kurzschwert');
|
|
expect(text).toContain('Head');
|
|
expect(text).toContain('Empty');
|
|
});
|
|
|
|
it('shows the effective stats summary from the equipment response', async () => {
|
|
const { fixture } = await setup();
|
|
const text = (fixture.nativeElement as HTMLElement).querySelector('[data-inventory-stats]')?.textContent ?? '';
|
|
|
|
expect(text).toContain('100');
|
|
expect(text).toContain('6');
|
|
expect(text).toContain('8');
|
|
});
|
|
|
|
it('passes the equipped item from the SAME slot as the selection — not just any equipped item — to the detail panel', async () => {
|
|
// item-helm (HEAD, equipped) is placed before item-sword (WEAPON, equipped) so that a
|
|
// regression which drops the equipmentSlot match (i.e. "find the first equipped item")
|
|
// would surface item-helm instead of item-sword, and this test would fail.
|
|
const threeItemInventory: InventoryResponse = {
|
|
items: [
|
|
{
|
|
id: 'item-helm',
|
|
quantity: 1,
|
|
equipped: true,
|
|
item: {
|
|
key: 'iron-helm',
|
|
name: 'Iron Helm',
|
|
description: 'Item description.',
|
|
rarity: 'COMMON',
|
|
equipmentSlot: 'HEAD',
|
|
weaponDamage: 0,
|
|
bonusAttack: 0,
|
|
bonusHp: 5,
|
|
bonusArmor: 2,
|
|
iconPath: '/images/items/iron-helm.png',
|
|
},
|
|
},
|
|
inventory.items[0], // item-sword, WEAPON, equipped
|
|
inventory.items[1], // item-blade, WEAPON, not equipped — this is the selection
|
|
],
|
|
};
|
|
|
|
const { fixture } = await setup({
|
|
inventoryData: threeItemInventory,
|
|
selectedItemId: 'item-blade',
|
|
selectedItem: threeItemInventory.items[2],
|
|
});
|
|
|
|
const panel = fixture.debugElement.query(By.directive(InventoryDetailPanelComponent))
|
|
.componentInstance as InventoryDetailPanelComponent;
|
|
|
|
expect(panel.equippedItemInSlot()?.id).toBe('item-sword');
|
|
});
|
|
});
|