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.
202 lines
6.7 KiB
TypeScript
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');
|
|
});
|
|
});
|