feat(web): add inventory page with grid, detail panel, and equipment overview
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
import { signal } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { vi } from 'vitest';
|
||||
import type { CharacterResponse, EquipmentResponse, InventoryResponse } from '../../core/api/game-api.models';
|
||||
import { WorldStore } from '../world/world.store';
|
||||
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',
|
||||
requiredLevel: 1,
|
||||
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',
|
||||
requiredLevel: 1,
|
||||
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',
|
||||
level: 1,
|
||||
experience: 0,
|
||||
silver: 0,
|
||||
currentHp: 100,
|
||||
maxHp: 100,
|
||||
attack: 6,
|
||||
currentLocation: { id: 'loc-1', key: 'south-gate', name: 'Südtor' },
|
||||
};
|
||||
|
||||
async function setup() {
|
||||
const inventoryStore = {
|
||||
inventory: signal(inventory),
|
||||
equipment: signal(equipment),
|
||||
selectedItemId: signal<string | null>(null),
|
||||
loading: signal(false),
|
||||
equipping: signal(false),
|
||||
error: signal<string | null>(null),
|
||||
load: vi.fn(() => Promise.resolve()),
|
||||
selectItem: vi.fn(),
|
||||
selectedItem: vi.fn(() => null),
|
||||
equip: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
const worldStore = { character: signal(character) };
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
describe('InventoryPageComponent', () => {
|
||||
it('loads the inventory on init', async () => {
|
||||
const { inventoryStore } = await setup();
|
||||
expect(inventoryStore.load).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user