test: add combat-actions and UI component tests — 40 new tests covering processCombatTick, Card, Button, Badge
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m24s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m24s
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
# Circular Dependencies
|
# Circular Dependencies
|
||||||
Generated: 2026-05-25T16:26:37.693Z
|
Generated: 2026-05-25T18:18:45.184Z
|
||||||
Found: 6 circular chain(s) — these MUST be fixed before modifying involved files.
|
Found: 6 circular chain(s) — these MUST be fixed before modifying involved files.
|
||||||
|
|
||||||
1. Processed 135 files (1.6s) (2 warnings)
|
1. Processed 135 files (1.6s) (2 warnings)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"generated": "2026-05-25T16:26:35.898Z",
|
"generated": "2026-05-25T18:18:43.369Z",
|
||||||
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
|
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
|
||||||
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
|
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
|
||||||
},
|
},
|
||||||
@@ -143,7 +143,6 @@
|
|||||||
"types.ts"
|
"types.ts"
|
||||||
],
|
],
|
||||||
"crafting-equipment.ts": [
|
"crafting-equipment.ts": [
|
||||||
"crafting-utils.ts",
|
|
||||||
"data/crafting-recipes.ts",
|
"data/crafting-recipes.ts",
|
||||||
"data/equipment/index.ts",
|
"data/equipment/index.ts",
|
||||||
"types.ts",
|
"types.ts",
|
||||||
@@ -168,9 +167,7 @@
|
|||||||
"data/attunements.ts": [
|
"data/attunements.ts": [
|
||||||
"types.ts"
|
"types.ts"
|
||||||
],
|
],
|
||||||
"data/crafting-recipes.ts": [
|
"data/crafting-recipes.ts": [],
|
||||||
"data/equipment/types.ts"
|
|
||||||
],
|
|
||||||
"data/disciplines/base.ts": [
|
"data/disciplines/base.ts": [
|
||||||
"types/disciplines.ts"
|
"types/disciplines.ts"
|
||||||
],
|
],
|
||||||
@@ -226,8 +223,7 @@
|
|||||||
"data/equipment/index.ts"
|
"data/equipment/index.ts"
|
||||||
],
|
],
|
||||||
"data/enchantments/defense-effects.ts": [
|
"data/enchantments/defense-effects.ts": [
|
||||||
"data/enchantment-types.ts",
|
"data/enchantment-types.ts"
|
||||||
"data/equipment/index.ts"
|
|
||||||
],
|
],
|
||||||
"data/enchantments/elemental-effects.ts": [
|
"data/enchantments/elemental-effects.ts": [
|
||||||
"data/enchantment-types.ts",
|
"data/enchantment-types.ts",
|
||||||
@@ -439,8 +435,7 @@
|
|||||||
"effects/discipline-effects.ts",
|
"effects/discipline-effects.ts",
|
||||||
"stores/combat-state.types.ts",
|
"stores/combat-state.types.ts",
|
||||||
"types.ts",
|
"types.ts",
|
||||||
"utils/index.ts",
|
"utils/index.ts"
|
||||||
"utils/result.ts"
|
|
||||||
],
|
],
|
||||||
"stores/combat-state.types.ts": [
|
"stores/combat-state.types.ts": [
|
||||||
"types.ts"
|
"types.ts"
|
||||||
@@ -448,7 +443,6 @@
|
|||||||
"stores/combatStore.ts": [
|
"stores/combatStore.ts": [
|
||||||
"stores/combat-actions.ts",
|
"stores/combat-actions.ts",
|
||||||
"stores/combat-state.types.ts",
|
"stores/combat-state.types.ts",
|
||||||
"stores/prestigeStore.ts",
|
|
||||||
"types.ts",
|
"types.ts",
|
||||||
"utils/activity-log.ts",
|
"utils/activity-log.ts",
|
||||||
"utils/index.ts",
|
"utils/index.ts",
|
||||||
@@ -471,7 +465,6 @@
|
|||||||
"stores/craftingStore.types.ts",
|
"stores/craftingStore.types.ts",
|
||||||
"stores/manaStore.ts",
|
"stores/manaStore.ts",
|
||||||
"stores/uiStore.ts",
|
"stores/uiStore.ts",
|
||||||
"types.ts",
|
|
||||||
"types/equipmentSlot.ts",
|
"types/equipmentSlot.ts",
|
||||||
"utils/result.ts",
|
"utils/result.ts",
|
||||||
"utils/safe-persist.ts"
|
"utils/safe-persist.ts"
|
||||||
@@ -499,7 +492,6 @@
|
|||||||
"stores/gameActions.ts": [
|
"stores/gameActions.ts": [
|
||||||
"effects/discipline-effects.ts",
|
"effects/discipline-effects.ts",
|
||||||
"stores/combatStore.ts",
|
"stores/combatStore.ts",
|
||||||
"stores/discipline-slice.ts",
|
|
||||||
"stores/gameStore.ts",
|
"stores/gameStore.ts",
|
||||||
"stores/manaStore.ts",
|
"stores/manaStore.ts",
|
||||||
"stores/prestigeStore.ts",
|
"stores/prestigeStore.ts",
|
||||||
@@ -510,20 +502,16 @@
|
|||||||
"constants.ts",
|
"constants.ts",
|
||||||
"effects.ts",
|
"effects.ts",
|
||||||
"effects/discipline-effects.ts",
|
"effects/discipline-effects.ts",
|
||||||
"stores/combatStore.ts",
|
|
||||||
"stores/craftingStore.ts",
|
"stores/craftingStore.ts",
|
||||||
"stores/discipline-slice.ts",
|
|
||||||
"stores/gameStore.ts",
|
"stores/gameStore.ts",
|
||||||
"stores/manaStore.ts",
|
"stores/manaStore.ts",
|
||||||
"stores/prestigeStore.ts",
|
"stores/prestigeStore.ts",
|
||||||
"stores/uiStore.ts",
|
|
||||||
"utils/index.ts"
|
"utils/index.ts"
|
||||||
],
|
],
|
||||||
"stores/gameLoopActions.ts": [
|
"stores/gameLoopActions.ts": [
|
||||||
"constants.ts",
|
"constants.ts",
|
||||||
"effects/discipline-effects.ts",
|
"effects/discipline-effects.ts",
|
||||||
"stores/combatStore.ts",
|
"stores/combatStore.ts",
|
||||||
"stores/discipline-slice.ts",
|
|
||||||
"stores/gameStore.ts",
|
"stores/gameStore.ts",
|
||||||
"stores/manaStore.ts",
|
"stores/manaStore.ts",
|
||||||
"stores/prestigeStore.ts",
|
"stores/prestigeStore.ts",
|
||||||
|
|||||||
@@ -181,6 +181,7 @@ Mana-Loop/
|
|||||||
│ │ │ ├── toggle.tsx
|
│ │ │ ├── toggle.tsx
|
||||||
│ │ │ ├── tooltip-info.tsx
|
│ │ │ ├── tooltip-info.tsx
|
||||||
│ │ │ ├── tooltip.tsx
|
│ │ │ ├── tooltip.tsx
|
||||||
|
│ │ │ ├── ui-components.test.tsx
|
||||||
│ │ │ └── value-display.tsx
|
│ │ │ └── value-display.tsx
|
||||||
│ │ └── ErrorBoundary.tsx
|
│ │ └── ErrorBoundary.tsx
|
||||||
│ ├── hooks/
|
│ ├── hooks/
|
||||||
@@ -193,6 +194,7 @@ Mana-Loop/
|
|||||||
│ │ │ ├── achievements.test.ts
|
│ │ │ ├── achievements.test.ts
|
||||||
│ │ │ ├── activity-log.test.ts
|
│ │ │ ├── activity-log.test.ts
|
||||||
│ │ │ ├── bug-fixes.test.ts
|
│ │ │ ├── bug-fixes.test.ts
|
||||||
|
│ │ │ ├── combat-actions.test.ts
|
||||||
│ │ │ ├── combat-utils.test.ts
|
│ │ │ ├── combat-utils.test.ts
|
||||||
│ │ │ ├── computed-stats.test.ts
|
│ │ │ ├── computed-stats.test.ts
|
||||||
│ │ │ ├── crafting-utils-basic.test.ts
|
│ │ │ ├── crafting-utils-basic.test.ts
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
@@ -0,0 +1,186 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
CardFooter,
|
||||||
|
} from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// UI COMPONENT TESTS — Card
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
describe('Card', () => {
|
||||||
|
it('should render with children', () => {
|
||||||
|
render(<Card data-testid="card">Hello World</Card>);
|
||||||
|
expect(screen.getByTestId('card')).toHaveTextContent('Hello World');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept custom className', () => {
|
||||||
|
render(<Card data-testid="card" className="custom-class">Test</Card>);
|
||||||
|
expect(screen.getByTestId('card')).toHaveClass('custom-class');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have data-slot attribute', () => {
|
||||||
|
render(<Card data-testid="card">Test</Card>);
|
||||||
|
expect(screen.getByTestId('card')).toHaveAttribute('data-slot', 'card');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render CardHeader with children', () => {
|
||||||
|
render(
|
||||||
|
<Card>
|
||||||
|
<CardHeader data-testid="header">Header Text</CardHeader>
|
||||||
|
</Card>,
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('header')).toHaveTextContent('Header Text');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render CardTitle', () => {
|
||||||
|
render(
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle data-testid="title">My Title</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>,
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('title')).toHaveTextContent('My Title');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render CardDescription', () => {
|
||||||
|
render(
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardDescription data-testid="desc">Description text</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>,
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('desc')).toHaveTextContent('Description text');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render CardContent', () => {
|
||||||
|
render(
|
||||||
|
<Card>
|
||||||
|
<CardContent data-testid="content">Content here</CardContent>
|
||||||
|
</Card>,
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('content')).toHaveTextContent('Content here');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render CardFooter', () => {
|
||||||
|
render(
|
||||||
|
<Card>
|
||||||
|
<CardFooter data-testid="footer">Footer text</CardFooter>
|
||||||
|
</Card>,
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('footer')).toHaveTextContent('Footer text');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// UI COMPONENT TESTS — Button
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
describe('Button', () => {
|
||||||
|
it('should render with children text', () => {
|
||||||
|
render(<Button data-testid="btn">Click Me</Button>);
|
||||||
|
expect(screen.getByTestId('btn')).toHaveTextContent('Click Me');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have button role', () => {
|
||||||
|
render(<Button data-testid="btn">Test</Button>);
|
||||||
|
expect(screen.getByTestId('btn').tagName).toBe('BUTTON');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply variant classes', () => {
|
||||||
|
render(<Button data-testid="btn" variant="destructive">Delete</Button>);
|
||||||
|
expect(screen.getByTestId('btn')).toHaveClass('bg-destructive');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply size classes', () => {
|
||||||
|
render(<Button data-testid="btn" size="sm">Small</Button>);
|
||||||
|
expect(screen.getByTestId('btn')).toHaveClass('h-8');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply custom className', () => {
|
||||||
|
render(<Button data-testid="btn" className="custom">Custom</Button>);
|
||||||
|
expect(screen.getByTestId('btn')).toHaveClass('custom');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render as child component when asChild is true', () => {
|
||||||
|
render(
|
||||||
|
<Button asChild data-testid="btn">
|
||||||
|
<a href="/test">Link Button</a>
|
||||||
|
</Button>,
|
||||||
|
);
|
||||||
|
const el = screen.getByTestId('btn');
|
||||||
|
expect(el.tagName).toBe('A');
|
||||||
|
expect(el).toHaveAttribute('href', '/test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have data-slot attribute', () => {
|
||||||
|
render(<Button data-testid="btn">Test</Button>);
|
||||||
|
expect(screen.getByTestId('btn')).toHaveAttribute('data-slot', 'button');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// UI COMPONENT TESTS — Badge
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
describe('Badge', () => {
|
||||||
|
it('should render with children text', () => {
|
||||||
|
render(<Badge data-testid="badge">New</Badge>);
|
||||||
|
expect(screen.getByTestId('badge')).toHaveTextContent('New');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render as span by default', () => {
|
||||||
|
render(<Badge data-testid="badge">Test</Badge>);
|
||||||
|
expect(screen.getByTestId('badge').tagName).toBe('SPAN');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply default variant classes', () => {
|
||||||
|
render(<Badge data-testid="badge">Default</Badge>);
|
||||||
|
expect(screen.getByTestId('badge')).toHaveClass('bg-primary');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply destructive variant', () => {
|
||||||
|
render(<Badge data-testid="badge" variant="destructive">Error</Badge>);
|
||||||
|
expect(screen.getByTestId('badge')).toHaveClass('bg-destructive');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply outline variant', () => {
|
||||||
|
render(<Badge data-testid="badge" variant="outline">Outline</Badge>);
|
||||||
|
expect(screen.getByTestId('badge')).toHaveClass('text-foreground');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply secondary variant', () => {
|
||||||
|
render(<Badge data-testid="badge" variant="secondary">Secondary</Badge>);
|
||||||
|
expect(screen.getByTestId('badge')).toHaveClass('bg-secondary');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply custom className', () => {
|
||||||
|
render(<Badge data-testid="badge" className="custom-badge">Custom</Badge>);
|
||||||
|
expect(screen.getByTestId('badge')).toHaveClass('custom-badge');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept asChild prop', () => {
|
||||||
|
render(
|
||||||
|
<Badge asChild data-testid="badge">
|
||||||
|
<a href="/link">Link Badge</a>
|
||||||
|
</Badge>,
|
||||||
|
);
|
||||||
|
const el = screen.getByTestId('badge');
|
||||||
|
expect(el.tagName).toBe('A');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have data-slot attribute', () => {
|
||||||
|
render(<Badge data-testid="badge">Test</Badge>);
|
||||||
|
expect(screen.getByTestId('badge')).toHaveAttribute('data-slot', 'badge');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
|
import { processCombatTick, makeInitialSpells } from '../stores/combat-actions';
|
||||||
|
import { useCombatStore, makeInitialSpells as makeStoreInitialSpells } from '../stores/combatStore';
|
||||||
|
import { useManaStore, makeInitialElements } from '../stores/manaStore';
|
||||||
|
import { useGameStore } from '../stores/gameStore';
|
||||||
|
import { usePrestigeStore } from '../stores/prestigeStore';
|
||||||
|
import { useUIStore } from '../stores/uiStore';
|
||||||
|
import { useDisciplineStore } from '../stores/discipline-slice';
|
||||||
|
import { getFloorMaxHP } from '../utils';
|
||||||
|
import type { CombatTickResult } from '../stores/combat-actions';
|
||||||
|
|
||||||
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function resetStores() {
|
||||||
|
useUIStore.setState({ paused: false, gameOver: false, victory: false, logs: [] });
|
||||||
|
useGameStore.setState({ day: 1, hour: 0, incursionStrength: 0, containmentWards: 0, initialized: true });
|
||||||
|
useManaStore.setState({ rawMana: 1000, meditateTicks: 0, totalManaGathered: 0, elements: makeInitialElements(500, {}) });
|
||||||
|
useCombatStore.setState({
|
||||||
|
currentFloor: 1,
|
||||||
|
floorHP: getFloorMaxHP(1),
|
||||||
|
floorMaxHP: getFloorMaxHP(1),
|
||||||
|
maxFloorReached: 1,
|
||||||
|
activeSpell: 'manaBolt',
|
||||||
|
currentAction: 'climb',
|
||||||
|
castProgress: 0,
|
||||||
|
spireMode: false,
|
||||||
|
currentRoom: { roomType: 'combat', enemies: [] },
|
||||||
|
clearedFloors: {},
|
||||||
|
climbDirection: null,
|
||||||
|
isDescending: false,
|
||||||
|
golemancy: { enabledGolems: [], summonedGolems: [], lastSummonFloor: 0 },
|
||||||
|
equipmentSpellStates: [],
|
||||||
|
comboHitCount: 0,
|
||||||
|
floorHitCount: 0,
|
||||||
|
spells: makeStoreInitialSpells(),
|
||||||
|
activityLog: [],
|
||||||
|
achievements: { unlocked: [], progress: {} },
|
||||||
|
totalSpellsCast: 0,
|
||||||
|
totalDamageDealt: 0,
|
||||||
|
totalCraftsCompleted: 0,
|
||||||
|
});
|
||||||
|
usePrestigeStore.setState({
|
||||||
|
loopCount: 0, insight: 0, totalInsight: 0, loopInsight: 0,
|
||||||
|
prestigeUpgrades: {}, pactSlots: 1, defeatedGuardians: [],
|
||||||
|
signedPacts: [], signedPactDetails: {},
|
||||||
|
pactRitualFloor: null, pactRitualProgress: 0,
|
||||||
|
});
|
||||||
|
useDisciplineStore.setState({ disciplines: {}, activeIds: [], concurrentLimit: 1, totalXP: 0, processedPerks: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
function runCombatTick(rawMana: number, elements: Record<string, { current: number; max: number; unlocked: boolean }>): CombatTickResult {
|
||||||
|
return processCombatTick(
|
||||||
|
() => useCombatStore.getState(),
|
||||||
|
(partial) => useCombatStore.setState(partial),
|
||||||
|
rawMana,
|
||||||
|
elements,
|
||||||
|
1000, // maxMana
|
||||||
|
1, // attackSpeedMult
|
||||||
|
vi.fn(), // onFloorCleared
|
||||||
|
(dmg) => ({ rawMana, elements, modifiedDamage: dmg }), // onDamageDealt (no modifiers)
|
||||||
|
[], // signedPacts
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// COMBAT ACTIONS — processCombatTick
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
describe('processCombatTick', () => {
|
||||||
|
beforeEach(resetStores);
|
||||||
|
|
||||||
|
describe('basic mechanics', () => {
|
||||||
|
it('should return default result when currentAction is not climb', () => {
|
||||||
|
useCombatStore.setState({ currentAction: 'meditate' });
|
||||||
|
const elements = makeInitialElements(500, {});
|
||||||
|
const result = runCombatTick(1000, elements);
|
||||||
|
expect(result.currentFloor).toBe(1);
|
||||||
|
expect(result.floorHP).toBe(getFloorMaxHP(1));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return default result when spell is not found', () => {
|
||||||
|
useCombatStore.setState({ activeSpell: 'nonexistentSpell' });
|
||||||
|
const elements = makeInitialElements(500, {});
|
||||||
|
const result = runCombatTick(1000, elements);
|
||||||
|
expect(result.currentFloor).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should progress cast bar on each tick', () => {
|
||||||
|
const elements = makeInitialElements(500, {});
|
||||||
|
const result = runCombatTick(1000, elements);
|
||||||
|
expect(result.castProgress).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reduce floor HP when cast completes', () => {
|
||||||
|
// Set cast progress close to 1 so next cast completes
|
||||||
|
useCombatStore.setState({ castProgress: 0.99 });
|
||||||
|
const elements = makeInitialElements(500, {});
|
||||||
|
const initialHP = useCombatStore.getState().floorHP;
|
||||||
|
const result = runCombatTick(1000, elements);
|
||||||
|
// Either HP decreased or floor advanced (if HP reached 0)
|
||||||
|
expect(result.floorHP).toBeLessThanOrEqual(initialHP);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('floor advancement', () => {
|
||||||
|
it('should advance floor when HP reaches 0', () => {
|
||||||
|
// Set floor HP very low so it's cleared in one cast
|
||||||
|
useCombatStore.setState({ floorHP: 1, castProgress: 0.99 });
|
||||||
|
const elements = makeInitialElements(500, {});
|
||||||
|
const result = runCombatTick(1000, elements);
|
||||||
|
expect(result.currentFloor).toBe(2);
|
||||||
|
expect(result.floorHP).toBe(getFloorMaxHP(2));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update maxFloorReached when advancing', () => {
|
||||||
|
useCombatStore.setState({ floorHP: 1, castProgress: 0.99, maxFloorReached: 5 });
|
||||||
|
const elements = makeInitialElements(500, {});
|
||||||
|
const result = runCombatTick(1000, elements);
|
||||||
|
expect(result.maxFloorReached).toBeGreaterThanOrEqual(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not advance beyond floor 100', () => {
|
||||||
|
useCombatStore.setState({ currentFloor: 100, floorHP: 1, castProgress: 0.99 });
|
||||||
|
const elements = makeInitialElements(500, {});
|
||||||
|
const result = runCombatTick(1000, elements);
|
||||||
|
expect(result.currentFloor).toBeLessThanOrEqual(100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mana cost', () => {
|
||||||
|
it('should deduct raw mana when spell costs raw mana', () => {
|
||||||
|
const elements = makeInitialElements(500, {});
|
||||||
|
const startMana = 1000;
|
||||||
|
const result = runCombatTick(startMana, elements);
|
||||||
|
// manaBolt costs raw mana, so after casting, rawMana should decrease
|
||||||
|
expect(result.rawMana).toBeLessThanOrEqual(startMana);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not cast when insufficient mana', () => {
|
||||||
|
const elements = makeInitialElements(500, {});
|
||||||
|
const state = useCombatStore.getState();
|
||||||
|
// With 0 mana and 0 progress, no cast should complete
|
||||||
|
const result = processCombatTick(
|
||||||
|
() => state,
|
||||||
|
() => {},
|
||||||
|
0, // no mana
|
||||||
|
elements,
|
||||||
|
1000,
|
||||||
|
1,
|
||||||
|
vi.fn(),
|
||||||
|
(dmg) => ({ rawMana: 0, elements, modifiedDamage: dmg }),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
expect(result.rawMana).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('equipment spell states', () => {
|
||||||
|
it('should initialize empty equipmentSpellStates', () => {
|
||||||
|
const elements = makeInitialElements(500, {});
|
||||||
|
const result = runCombatTick(1000, elements);
|
||||||
|
expect(result.equipmentSpellStates).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not error with empty equipment spell states', () => {
|
||||||
|
useCombatStore.setState({ equipmentSpellStates: [] });
|
||||||
|
const elements = makeInitialElements(500, {});
|
||||||
|
expect(() => runCombatTick(1000, elements)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('error handling', () => {
|
||||||
|
it('should return safe defaults when spell def is missing (early return path)', () => {
|
||||||
|
// When the spell definition is not found, processCombatTick returns
|
||||||
|
// a default result without crashing — this exercises the early return
|
||||||
|
// path that acts as a safety net.
|
||||||
|
useCombatStore.setState({ activeSpell: 'totallyInvalidSpell' });
|
||||||
|
const elements = makeInitialElements(500, {});
|
||||||
|
const result = processCombatTick(
|
||||||
|
() => useCombatStore.getState(),
|
||||||
|
() => {},
|
||||||
|
1000,
|
||||||
|
elements,
|
||||||
|
1000,
|
||||||
|
1,
|
||||||
|
vi.fn(),
|
||||||
|
(dmg) => ({ rawMana: 1000, elements, modifiedDamage: dmg }),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.rawMana).toBe(1000);
|
||||||
|
expect(result.logMessages).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw when onDamageDealt callback throws', () => {
|
||||||
|
// The try/catch in processCombatTick should catch errors from callbacks
|
||||||
|
// and return safe defaults instead of crashing
|
||||||
|
const elements = makeInitialElements(500, {});
|
||||||
|
useCombatStore.setState({ castProgress: 0.99 });
|
||||||
|
expect(() => {
|
||||||
|
processCombatTick(
|
||||||
|
() => useCombatStore.getState(),
|
||||||
|
() => {},
|
||||||
|
1000,
|
||||||
|
elements,
|
||||||
|
1000,
|
||||||
|
1,
|
||||||
|
vi.fn(),
|
||||||
|
(_dmg) => { throw new Error('damage callback error'); },
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
}).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('helper: makeInitialSpells', () => {
|
||||||
|
it('should create manaBolt as default spell', () => {
|
||||||
|
const spells = makeInitialSpells();
|
||||||
|
expect(spells.manaBolt).toBeDefined();
|
||||||
|
expect(spells.manaBolt.learned).toBe(true);
|
||||||
|
expect(spells.manaBolt.level).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep additional spells when specified', () => {
|
||||||
|
const spells = makeInitialSpells(['fireball']);
|
||||||
|
expect(spells.manaBolt).toBeDefined();
|
||||||
|
expect(spells.fireball).toBeDefined();
|
||||||
|
expect(spells.fireball.learned).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not duplicate manaBolt when included in keep list', () => {
|
||||||
|
const spells = makeInitialSpells(['manaBolt']);
|
||||||
|
expect(Object.keys(spells).length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+1
-1
@@ -6,7 +6,7 @@ export default defineConfig({
|
|||||||
environment: 'jsdom',
|
environment: 'jsdom',
|
||||||
globals: true,
|
globals: true,
|
||||||
setupFiles: ['./src/test/setup.ts'],
|
setupFiles: ['./src/test/setup.ts'],
|
||||||
include: ['src/**/*.{test,spec}.ts'],
|
include: ['src/**/*.{test,spec}.{ts,tsx}'],
|
||||||
coverage: {
|
coverage: {
|
||||||
reporter: ['text', 'json'],
|
reporter: ['text', 'json'],
|
||||||
exclude: [
|
exclude: [
|
||||||
|
|||||||
Reference in New Issue
Block a user