196 lines
5.8 KiB
TypeScript
196 lines
5.8 KiB
TypeScript
import { HttpErrorResponse } from '@angular/common/http';
|
|
import { TestBed } from '@angular/core/testing';
|
|
import { from, of, throwError } from 'rxjs';
|
|
import { vi } from 'vitest';
|
|
import type { Combat } from '../../core/api/game-api.models';
|
|
import { GameApiService } from '../../core/api/game-api.service';
|
|
import { CombatStore } from './combat.store';
|
|
|
|
const startedCombat: Combat = {
|
|
id: 'combat-1',
|
|
status: 'ACTIVE',
|
|
round: 1,
|
|
player: { name: 'Aric Duskwalker', maxHp: 100, currentHp: 100 },
|
|
monster: {
|
|
key: 'ash-rat',
|
|
name: 'Aschenratte',
|
|
level: 1,
|
|
maxHp: 45,
|
|
currentHp: 45,
|
|
artworkPath: '/images/monsters/ash-rat.png',
|
|
},
|
|
events: [],
|
|
rewards: null,
|
|
};
|
|
|
|
const afterAttack: Combat = {
|
|
...startedCombat,
|
|
round: 2,
|
|
player: { ...startedCombat.player, currentHp: 95 },
|
|
monster: { ...startedCombat.monster, currentHp: 31 },
|
|
events: [
|
|
{ round: 1, sequence: 1, type: 'DAMAGE', source: 'PLAYER', target: 'MONSTER', amount: 14 },
|
|
{ round: 1, sequence: 2, type: 'DAMAGE', source: 'MONSTER', target: 'PLAYER', amount: 5 },
|
|
],
|
|
};
|
|
|
|
describe('CombatStore', () => {
|
|
let api: {
|
|
startCombat: ReturnType<typeof vi.fn>;
|
|
getCombat: ReturnType<typeof vi.fn>;
|
|
getActiveCombat: ReturnType<typeof vi.fn>;
|
|
performCombatAction: ReturnType<typeof vi.fn>;
|
|
};
|
|
let store: CombatStore;
|
|
|
|
beforeEach(() => {
|
|
api = {
|
|
startCombat: vi.fn(() => of(startedCombat)),
|
|
getCombat: vi.fn(() => of(startedCombat)),
|
|
getActiveCombat: vi.fn(() => of(startedCombat)),
|
|
performCombatAction: vi.fn(() => of(afterAttack)),
|
|
};
|
|
|
|
TestBed.configureTestingModule({
|
|
providers: [CombatStore, { provide: GameApiService, useValue: api }],
|
|
});
|
|
store = TestBed.inject(CombatStore);
|
|
});
|
|
|
|
it('starts a combat and stores it', async () => {
|
|
await store.startCombat('encounter-1');
|
|
|
|
expect(api.startCombat).toHaveBeenCalledWith('encounter-1');
|
|
expect(store.combat()).toEqual(startedCombat);
|
|
expect(store.error()).toBeNull();
|
|
});
|
|
|
|
it('clears any previous combat and reports the mapped error when starting fails', async () => {
|
|
api.startCombat.mockReturnValue(
|
|
throwError(
|
|
() =>
|
|
new HttpErrorResponse({
|
|
status: 409,
|
|
error: { statusCode: 409, code: 'COMBAT_ALREADY_ACTIVE', message: 'Active.' },
|
|
}),
|
|
),
|
|
);
|
|
|
|
await store.startCombat('encounter-1');
|
|
|
|
expect(store.combat()).toBeNull();
|
|
expect(store.error()).toBe('Du befindest dich bereits in einem Kampf.');
|
|
expect(store.errorCode()).toBe('COMBAT_ALREADY_ACTIVE');
|
|
});
|
|
|
|
it('loads the running combat and clears the error that sent us looking for it', async () => {
|
|
api.startCombat.mockReturnValue(
|
|
throwError(
|
|
() =>
|
|
new HttpErrorResponse({
|
|
status: 409,
|
|
error: { statusCode: 409, code: 'COMBAT_ALREADY_ACTIVE', message: 'Active.' },
|
|
}),
|
|
),
|
|
);
|
|
await store.startCombat('encounter-1');
|
|
|
|
const active = await store.loadActiveCombat();
|
|
|
|
expect(api.getActiveCombat).toHaveBeenCalledOnce();
|
|
expect(active).toEqual(startedCombat);
|
|
expect(store.combat()).toEqual(startedCombat);
|
|
expect(store.error()).toBeNull();
|
|
expect(store.errorCode()).toBeNull();
|
|
});
|
|
|
|
it('resolves null and keeps the combat empty when no fight is running', async () => {
|
|
api.getActiveCombat.mockReturnValue(of(null));
|
|
|
|
const active = await store.loadActiveCombat();
|
|
|
|
expect(active).toBeNull();
|
|
expect(store.combat()).toBeNull();
|
|
});
|
|
|
|
it('loads a combat by id', async () => {
|
|
await store.loadCombat('combat-1');
|
|
|
|
expect(api.getCombat).toHaveBeenCalledWith('combat-1');
|
|
expect(store.combat()).toEqual(startedCombat);
|
|
});
|
|
|
|
it('reports the mapped error when loading an unknown combat', async () => {
|
|
api.getCombat.mockReturnValue(
|
|
throwError(
|
|
() =>
|
|
new HttpErrorResponse({
|
|
status: 404,
|
|
error: { statusCode: 404, code: 'COMBAT_NOT_FOUND', message: 'Not found.' },
|
|
}),
|
|
),
|
|
);
|
|
|
|
await store.loadCombat('unknown');
|
|
|
|
expect(store.error()).toBe('Dieser Kampf wurde nicht gefunden.');
|
|
});
|
|
|
|
it('sends only the ATTACK action and replaces combat with the server response', async () => {
|
|
await store.startCombat('encounter-1');
|
|
|
|
await store.attack();
|
|
|
|
expect(api.performCombatAction).toHaveBeenCalledWith('combat-1', 'ATTACK');
|
|
expect(store.combat()).toEqual(afterAttack);
|
|
});
|
|
|
|
it('does nothing when attacking without a loaded combat', async () => {
|
|
await store.attack();
|
|
|
|
expect(api.performCombatAction).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('ignores a second attack while the first is still pending', async () => {
|
|
await store.startCombat('encounter-1');
|
|
let resolveAttack!: (value: Combat) => void;
|
|
api.performCombatAction.mockReturnValue(
|
|
from(
|
|
new Promise<Combat>((resolve) => {
|
|
resolveAttack = resolve;
|
|
}),
|
|
),
|
|
);
|
|
|
|
const first = store.attack();
|
|
expect(store.actionPending()).toBe(true);
|
|
const second = store.attack();
|
|
|
|
resolveAttack(afterAttack);
|
|
await Promise.all([first, second]);
|
|
|
|
expect(api.performCombatAction).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('clears actionPending after a failed attack and keeps the previous combat state', async () => {
|
|
await store.startCombat('encounter-1');
|
|
api.performCombatAction.mockReturnValue(throwError(() => new Error('Netzwerkfehler')));
|
|
|
|
await store.attack();
|
|
|
|
expect(store.actionPending()).toBe(false);
|
|
expect(store.combat()).toEqual(startedCombat);
|
|
expect(store.error()).toBe('Netzwerkfehler');
|
|
});
|
|
|
|
it('clears the error message', async () => {
|
|
api.startCombat.mockReturnValue(throwError(() => new Error('x')));
|
|
await store.startCombat('encounter-1');
|
|
expect(store.error()).not.toBeNull();
|
|
|
|
store.clearError();
|
|
|
|
expect(store.error()).toBeNull();
|
|
});
|
|
});
|