Files
ashen-realms/apps/web/src/app/features/inventory/inventory-page.component.spec.ts
Bastian Wagner c0401d5c60 feat(inventory): remove the requiredLevel equip gate from the detail panel
The API can no longer emit ITEM_LEVEL_REQUIREMENT_NOT_MET (Task 8 removed
the level gate server-side) and InventoryItem.item no longer carries
requiredLevel (Task 12). Drop the characterLevel input, the
meetsLevelRequirement computed, the associated template branch and copy,
and the dead German error-message mapping. Update fixtures across the
inventory specs to the real InventoryItem/CharacterResponse shapes.
2026-08-21 15:06:00 +02:00

202 lines
6.7 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',
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: 'Räuberklinge',
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: 0,
silver: 0,
currentHp: 100,
maxHp: 100,
attack: 6,
currentLocation: { id: 'loc-1', key: 'south-gate', name: 'Südtor' },
};
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 Leer', async () => {
const { fixture } = await setup();
const text = (fixture.nativeElement as HTMLElement).querySelector('.inventory-page__equipment-list')?.textContent ?? '';
expect(text).toContain('Waffe');
expect(text).toContain('Abgenutztes Kurzschwert');
expect(text).toContain('Kopf');
expect(text).toContain('Leer');
});
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: 'Eiserner Helm',
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');
});
});