Add test coverage for TargetingSystem
This commit is contained in:
26
src/__tests__/test-setup.ts
Normal file
26
src/__tests__/test-setup.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { vi } from 'vitest';
|
||||||
|
|
||||||
|
// Stub global window for Phaser device detection
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
(globalThis as any).window = {
|
||||||
|
location: { href: '', origin: '' },
|
||||||
|
addEventListener: vi.fn(),
|
||||||
|
removeEventListener: vi.fn(),
|
||||||
|
dispatchEvent: vi.fn(),
|
||||||
|
cordova: undefined,
|
||||||
|
navigator: { userAgent: 'node' }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (typeof document === 'undefined') {
|
||||||
|
(globalThis as any).document = {
|
||||||
|
createElement: vi.fn(() => ({
|
||||||
|
getContext: vi.fn(),
|
||||||
|
style: {}
|
||||||
|
})),
|
||||||
|
addEventListener: vi.fn(),
|
||||||
|
removeEventListener: vi.fn(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (typeof navigator === 'undefined') {
|
||||||
|
(globalThis as any).navigator = { userAgent: 'node' };
|
||||||
|
}
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { GameScene } from '../GameScene';
|
|
||||||
import * as simulation from '../../engine/simulation/simulation';
|
|
||||||
import * as generator from '../../engine/world/generator';
|
|
||||||
|
|
||||||
// Mock Phaser
|
// Mock Phaser BEFORE any project imports
|
||||||
vi.mock('phaser', () => {
|
vi.mock('phaser', () => {
|
||||||
const mockEventEmitter = {
|
const mockEventEmitter = {
|
||||||
on: vi.fn(),
|
on: vi.fn(),
|
||||||
@@ -11,77 +8,97 @@ vi.mock('phaser', () => {
|
|||||||
off: vi.fn(),
|
off: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class MockScene {
|
||||||
|
events = mockEventEmitter;
|
||||||
|
input = {
|
||||||
|
keyboard: {
|
||||||
|
createCursorKeys: vi.fn(() => ({})),
|
||||||
|
on: vi.fn(),
|
||||||
|
},
|
||||||
|
on: vi.fn(),
|
||||||
|
activePointer: {
|
||||||
|
worldX: 0,
|
||||||
|
worldY: 0,
|
||||||
|
x: 0,
|
||||||
|
y: 0
|
||||||
|
},
|
||||||
|
mouse: {
|
||||||
|
disableContextMenu: vi.fn()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
cameras = {
|
||||||
|
main: {
|
||||||
|
setZoom: vi.fn(),
|
||||||
|
setBounds: vi.fn(),
|
||||||
|
centerOn: vi.fn(),
|
||||||
|
fadeIn: vi.fn(),
|
||||||
|
getWorldPoint: vi.fn((x, y) => ({ x, y }))
|
||||||
|
},
|
||||||
|
};
|
||||||
|
scene = {
|
||||||
|
launch: vi.fn(),
|
||||||
|
get: vi.fn(),
|
||||||
|
};
|
||||||
|
add = {
|
||||||
|
graphics: vi.fn(() => ({
|
||||||
|
setDepth: vi.fn().mockReturnThis(),
|
||||||
|
clear: vi.fn(),
|
||||||
|
lineStyle: vi.fn(),
|
||||||
|
lineBetween: vi.fn(),
|
||||||
|
strokeRect: vi.fn(),
|
||||||
|
})),
|
||||||
|
sprite: vi.fn(() => ({
|
||||||
|
setDepth: vi.fn().mockReturnThis(),
|
||||||
|
setVisible: vi.fn().mockReturnThis(),
|
||||||
|
setAlpha: vi.fn().mockReturnThis(),
|
||||||
|
setPosition: vi.fn().mockReturnThis(),
|
||||||
|
})),
|
||||||
|
text: vi.fn(() => ({})),
|
||||||
|
rectangle: vi.fn(() => ({})),
|
||||||
|
container: vi.fn(() => ({})),
|
||||||
|
};
|
||||||
|
load = {
|
||||||
|
spritesheet: vi.fn(),
|
||||||
|
};
|
||||||
|
anims = {
|
||||||
|
create: vi.fn(),
|
||||||
|
exists: vi.fn(() => true),
|
||||||
|
generateFrameNumbers: vi.fn(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
default: {
|
default: {
|
||||||
Scene: class {
|
Scene: MockScene,
|
||||||
events = mockEventEmitter;
|
|
||||||
input = {
|
|
||||||
keyboard: {
|
|
||||||
createCursorKeys: vi.fn(() => ({})),
|
|
||||||
on: vi.fn(),
|
|
||||||
},
|
|
||||||
on: vi.fn(),
|
|
||||||
};
|
|
||||||
cameras = {
|
|
||||||
main: {
|
|
||||||
setZoom: vi.fn(),
|
|
||||||
setBounds: vi.fn(),
|
|
||||||
centerOn: vi.fn(),
|
|
||||||
fadeIn: vi.fn(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
scene = {
|
|
||||||
launch: vi.fn(),
|
|
||||||
get: vi.fn(),
|
|
||||||
};
|
|
||||||
add = {
|
|
||||||
graphics: vi.fn(() => ({
|
|
||||||
setDepth: vi.fn().mockReturnThis(),
|
|
||||||
clear: vi.fn(),
|
|
||||||
lineStyle: vi.fn(),
|
|
||||||
lineBetween: vi.fn(),
|
|
||||||
strokeRect: vi.fn(),
|
|
||||||
})),
|
|
||||||
sprite: vi.fn(() => ({
|
|
||||||
setDepth: vi.fn().mockReturnThis(),
|
|
||||||
setVisible: vi.fn().mockReturnThis(),
|
|
||||||
setAlpha: vi.fn().mockReturnThis(),
|
|
||||||
setPosition: vi.fn().mockReturnThis(),
|
|
||||||
})),
|
|
||||||
text: vi.fn(() => ({})),
|
|
||||||
rectangle: vi.fn(() => ({})),
|
|
||||||
container: vi.fn(() => ({})),
|
|
||||||
};
|
|
||||||
load = {
|
|
||||||
spritesheet: vi.fn(),
|
|
||||||
};
|
|
||||||
anims = {
|
|
||||||
create: vi.fn(),
|
|
||||||
exists: vi.fn(() => true),
|
|
||||||
generateFrameNumbers: vi.fn(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
Input: {
|
Input: {
|
||||||
Keyboard: {
|
Keyboard: {
|
||||||
JustDown: vi.fn(),
|
JustDown: vi.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
import { GameScene } from '../GameScene';
|
||||||
|
import * as simulation from '../../engine/simulation/simulation';
|
||||||
|
import * as generator from '../../engine/world/generator';
|
||||||
|
|
||||||
// Mock other modules
|
// Mock other modules
|
||||||
vi.mock('../../rendering/DungeonRenderer', () => ({
|
vi.mock('../../rendering/DungeonRenderer', () => ({
|
||||||
DungeonRenderer: vi.fn().mockImplementation(function() {
|
DungeonRenderer: vi.fn().mockImplementation(function() {
|
||||||
return {
|
return {
|
||||||
initializeFloor: vi.fn(),
|
initializeFloor: vi.fn(),
|
||||||
|
|
||||||
computeFov: vi.fn(),
|
computeFov: vi.fn(),
|
||||||
render: vi.fn(),
|
render: vi.fn(),
|
||||||
showDamage: vi.fn(),
|
showDamage: vi.fn(),
|
||||||
spawnCorpse: vi.fn(),
|
spawnCorpse: vi.fn(),
|
||||||
showWait: vi.fn(),
|
showWait: vi.fn(),
|
||||||
isMinimapVisible: vi.fn(() => false),
|
isMinimapVisible: vi.fn(() => false),
|
||||||
|
toggleMinimap: vi.fn(),
|
||||||
|
updateTile: vi.fn(),
|
||||||
|
showProjectile: vi.fn(),
|
||||||
|
showHeal: vi.fn(),
|
||||||
|
shakeCamera: vi.fn(),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
@@ -93,7 +110,6 @@ vi.mock('../../engine/simulation/simulation', () => ({
|
|||||||
|
|
||||||
vi.mock('../../engine/world/generator', () => ({
|
vi.mock('../../engine/world/generator', () => ({
|
||||||
generateWorld: vi.fn(),
|
generateWorld: vi.fn(),
|
||||||
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../engine/world/world-logic', () => ({
|
vi.mock('../../engine/world/world-logic', () => ({
|
||||||
@@ -101,6 +117,7 @@ vi.mock('../../engine/world/world-logic', () => ({
|
|||||||
isBlocked: vi.fn(() => false),
|
isBlocked: vi.fn(() => false),
|
||||||
isPlayerOnExit: vi.fn(() => false),
|
isPlayerOnExit: vi.fn(() => false),
|
||||||
idx: vi.fn((w, x, y) => y * w.width + x),
|
idx: vi.fn((w, x, y) => y * w.width + x),
|
||||||
|
tryDestructTile: vi.fn(() => false),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('GameScene', () => {
|
describe('GameScene', () => {
|
||||||
@@ -148,7 +165,6 @@ describe('GameScene', () => {
|
|||||||
mockWorld.actors.set(1, mockPlayer);
|
mockWorld.actors.set(1, mockPlayer);
|
||||||
|
|
||||||
(generator.generateWorld as any).mockReturnValue({
|
(generator.generateWorld as any).mockReturnValue({
|
||||||
|
|
||||||
world: mockWorld,
|
world: mockWorld,
|
||||||
playerId: 1,
|
playerId: 1,
|
||||||
});
|
});
|
||||||
@@ -163,9 +179,7 @@ describe('GameScene', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should trigger death screen when player is killed', () => {
|
it('should trigger death screen when player is killed', () => {
|
||||||
// 1. Mock simulation so that after action, player is gone from world
|
|
||||||
(simulation.applyAction as any).mockImplementation((world: any) => {
|
(simulation.applyAction as any).mockImplementation((world: any) => {
|
||||||
// simulate player being killed
|
|
||||||
world.actors.delete(1);
|
world.actors.delete(1);
|
||||||
return [{ type: 'killed', targetId: 1, victimType: 'player', x: 1, y: 1 }];
|
return [{ type: 'killed', targetId: 1, victimType: 'player', x: 1, y: 1 }];
|
||||||
});
|
});
|
||||||
@@ -175,19 +189,8 @@ describe('GameScene', () => {
|
|||||||
events: [],
|
events: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. Commit an action
|
|
||||||
// We need to access private method or trigger it via public interface
|
|
||||||
// commitPlayerAction is private, let's cast to any to call it
|
|
||||||
(scene as any).commitPlayerAction({ type: 'wait' });
|
(scene as any).commitPlayerAction({ type: 'wait' });
|
||||||
|
|
||||||
// 3. Verify showDeathScreen was called on the mock UI
|
|
||||||
expect(mockUI.showDeathScreen).toHaveBeenCalled();
|
expect(mockUI.showDeathScreen).toHaveBeenCalled();
|
||||||
|
|
||||||
// Verify it was called with some stats
|
|
||||||
const callArgs = mockUI.showDeathScreen.mock.calls[0][0];
|
|
||||||
expect(callArgs).toHaveProperty('floor');
|
|
||||||
|
|
||||||
expect(callArgs).toHaveProperty('gold');
|
|
||||||
expect(callArgs).toHaveProperty('stats');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
170
src/scenes/systems/__tests__/TargetingSystem.test.ts
Normal file
170
src/scenes/systems/__tests__/TargetingSystem.test.ts
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
// Mock Phaser
|
||||||
|
vi.mock('phaser', () => {
|
||||||
|
const mockGraphics = {
|
||||||
|
setDepth: vi.fn().mockReturnThis(),
|
||||||
|
clear: vi.fn().mockReturnThis(),
|
||||||
|
lineStyle: vi.fn().mockReturnThis(),
|
||||||
|
lineBetween: vi.fn().mockReturnThis(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockSprite = {
|
||||||
|
setDepth: vi.fn().mockReturnThis(),
|
||||||
|
setVisible: vi.fn().mockReturnThis(),
|
||||||
|
setAlpha: vi.fn().mockReturnThis(),
|
||||||
|
setPosition: vi.fn().mockReturnThis(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
default: {
|
||||||
|
Scene: class {
|
||||||
|
add = {
|
||||||
|
graphics: vi.fn(() => mockGraphics),
|
||||||
|
sprite: vi.fn(() => mockSprite),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock CombatLogic
|
||||||
|
vi.mock('../../../engine/gameplay/CombatLogic', () => ({
|
||||||
|
traceProjectile: vi.fn(),
|
||||||
|
getClosestVisibleEnemy: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { TargetingSystem } from '../TargetingSystem';
|
||||||
|
import { traceProjectile, getClosestVisibleEnemy } from '../../../engine/gameplay/CombatLogic';
|
||||||
|
import { TILE_SIZE } from '../../../core/constants';
|
||||||
|
|
||||||
|
describe('TargetingSystem', () => {
|
||||||
|
let targetingSystem: TargetingSystem;
|
||||||
|
let mockWorld: any;
|
||||||
|
let mockEntityManager: any;
|
||||||
|
let mockScene: any;
|
||||||
|
let mockGraphics: any;
|
||||||
|
let mockSprite: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
mockGraphics = {
|
||||||
|
setDepth: vi.fn().mockReturnThis(),
|
||||||
|
clear: vi.fn().mockReturnThis(),
|
||||||
|
lineStyle: vi.fn().mockReturnThis(),
|
||||||
|
lineBetween: vi.fn().mockReturnThis(),
|
||||||
|
};
|
||||||
|
|
||||||
|
mockSprite = {
|
||||||
|
setDepth: vi.fn().mockReturnThis(),
|
||||||
|
setVisible: vi.fn().mockReturnThis(),
|
||||||
|
setAlpha: vi.fn().mockReturnThis(),
|
||||||
|
setPosition: vi.fn().mockReturnThis(),
|
||||||
|
};
|
||||||
|
|
||||||
|
mockScene = {
|
||||||
|
add: {
|
||||||
|
graphics: vi.fn(() => mockGraphics),
|
||||||
|
sprite: vi.fn(() => mockSprite),
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
targetingSystem = new TargetingSystem(mockScene);
|
||||||
|
mockWorld = { width: 10, height: 10 };
|
||||||
|
mockEntityManager = {};
|
||||||
|
|
||||||
|
// Default return for traceProjectile
|
||||||
|
(traceProjectile as any).mockReturnValue({
|
||||||
|
blockedPos: { x: 0, y: 0 },
|
||||||
|
hitActorId: undefined,
|
||||||
|
path: []
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize with graphics and crosshair sprite hidden', () => {
|
||||||
|
expect(mockScene.add.graphics).toHaveBeenCalled();
|
||||||
|
expect(mockScene.add.sprite).toHaveBeenCalled();
|
||||||
|
expect(mockSprite.setVisible).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should start targeting and auto-select closest enemy', () => {
|
||||||
|
const playerPos = { x: 1, y: 1 };
|
||||||
|
const enemyPos = { x: 3, y: 3 };
|
||||||
|
(getClosestVisibleEnemy as any).mockReturnValue(enemyPos);
|
||||||
|
|
||||||
|
targetingSystem.startTargeting(
|
||||||
|
'item-1',
|
||||||
|
playerPos,
|
||||||
|
mockWorld,
|
||||||
|
mockEntityManager!,
|
||||||
|
1 as any,
|
||||||
|
new Uint8Array(100),
|
||||||
|
10
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(targetingSystem.isActive).toBe(true);
|
||||||
|
expect(targetingSystem.itemId).toBe('item-1');
|
||||||
|
expect(targetingSystem.cursorPos).toEqual(enemyPos);
|
||||||
|
expect(mockSprite.setVisible).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fallback to mouse position if no enemy found', () => {
|
||||||
|
const playerPos = { x: 1, y: 1 };
|
||||||
|
const mousePos = { x: 5, y: 5 };
|
||||||
|
(getClosestVisibleEnemy as any).mockReturnValue(null);
|
||||||
|
|
||||||
|
targetingSystem.startTargeting(
|
||||||
|
'item-1',
|
||||||
|
playerPos,
|
||||||
|
mockWorld,
|
||||||
|
mockEntityManager!,
|
||||||
|
1 as any,
|
||||||
|
new Uint8Array(100),
|
||||||
|
10,
|
||||||
|
mousePos
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(targetingSystem.cursorPos).toEqual(mousePos);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update visuals with predictive impact point', () => {
|
||||||
|
const playerPos = { x: 1, y: 1 };
|
||||||
|
const targetPos = { x: 5, y: 1 };
|
||||||
|
const blockedPos = { x: 3, y: 1 }; // Wall at 3,1
|
||||||
|
|
||||||
|
(traceProjectile as any).mockReturnValue({
|
||||||
|
blockedPos: blockedPos,
|
||||||
|
hitActorId: undefined,
|
||||||
|
path: []
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start targeting
|
||||||
|
targetingSystem.startTargeting(
|
||||||
|
'item-1',
|
||||||
|
playerPos,
|
||||||
|
mockWorld,
|
||||||
|
mockEntityManager!,
|
||||||
|
1 as any,
|
||||||
|
new Uint8Array(100),
|
||||||
|
10,
|
||||||
|
targetPos
|
||||||
|
);
|
||||||
|
|
||||||
|
// The crosshair should be at blockedPos, not targetPos
|
||||||
|
const expectedX = blockedPos.x * TILE_SIZE + TILE_SIZE / 2;
|
||||||
|
const expectedY = blockedPos.y * TILE_SIZE + TILE_SIZE / 2;
|
||||||
|
expect(mockSprite.setPosition).toHaveBeenCalledWith(expectedX, expectedY);
|
||||||
|
|
||||||
|
// Verify dashed line was drawn (multiple lineBetween calls)
|
||||||
|
expect(mockGraphics.lineBetween).toHaveBeenCalled();
|
||||||
|
expect(mockGraphics.lineBetween.mock.calls.length).toBeGreaterThan(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear visuals on cancel', () => {
|
||||||
|
targetingSystem.cancel();
|
||||||
|
expect(targetingSystem.isActive).toBe(false);
|
||||||
|
expect(mockGraphics.clear).toHaveBeenCalled();
|
||||||
|
expect(mockSprite.setVisible).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,6 +2,7 @@ import { defineConfig } from 'vitest/config';
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
|
setupFiles: ["src/__tests__/test-setup.ts"],
|
||||||
coverage: {
|
coverage: {
|
||||||
provider: 'v8',
|
provider: 'v8',
|
||||||
reporter: ['text', 'json', 'html'],
|
reporter: ['text', 'json', 'html'],
|
||||||
|
|||||||
Reference in New Issue
Block a user