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 { 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', () => {
|
||||
const mockEventEmitter = {
|
||||
on: vi.fn(),
|
||||
@@ -11,77 +8,97 @@ vi.mock('phaser', () => {
|
||||
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 {
|
||||
default: {
|
||||
Scene: class {
|
||||
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(),
|
||||
};
|
||||
},
|
||||
Scene: MockScene,
|
||||
Input: {
|
||||
Keyboard: {
|
||||
JustDown: vi.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
import { GameScene } from '../GameScene';
|
||||
import * as simulation from '../../engine/simulation/simulation';
|
||||
import * as generator from '../../engine/world/generator';
|
||||
|
||||
// Mock other modules
|
||||
vi.mock('../../rendering/DungeonRenderer', () => ({
|
||||
DungeonRenderer: vi.fn().mockImplementation(function() {
|
||||
return {
|
||||
initializeFloor: vi.fn(),
|
||||
|
||||
computeFov: vi.fn(),
|
||||
render: vi.fn(),
|
||||
showDamage: vi.fn(),
|
||||
spawnCorpse: vi.fn(),
|
||||
showWait: vi.fn(),
|
||||
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', () => ({
|
||||
generateWorld: vi.fn(),
|
||||
|
||||
}));
|
||||
|
||||
vi.mock('../../engine/world/world-logic', () => ({
|
||||
@@ -101,6 +117,7 @@ vi.mock('../../engine/world/world-logic', () => ({
|
||||
isBlocked: vi.fn(() => false),
|
||||
isPlayerOnExit: vi.fn(() => false),
|
||||
idx: vi.fn((w, x, y) => y * w.width + x),
|
||||
tryDestructTile: vi.fn(() => false),
|
||||
}));
|
||||
|
||||
describe('GameScene', () => {
|
||||
@@ -148,7 +165,6 @@ describe('GameScene', () => {
|
||||
mockWorld.actors.set(1, mockPlayer);
|
||||
|
||||
(generator.generateWorld as any).mockReturnValue({
|
||||
|
||||
world: mockWorld,
|
||||
playerId: 1,
|
||||
});
|
||||
@@ -163,9 +179,7 @@ describe('GameScene', () => {
|
||||
});
|
||||
|
||||
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) => {
|
||||
// simulate player being killed
|
||||
world.actors.delete(1);
|
||||
return [{ type: 'killed', targetId: 1, victimType: 'player', x: 1, y: 1 }];
|
||||
});
|
||||
@@ -175,19 +189,8 @@ describe('GameScene', () => {
|
||||
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' });
|
||||
|
||||
// 3. Verify showDeathScreen was called on the mock UI
|
||||
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({
|
||||
test: {
|
||||
setupFiles: ["src/__tests__/test-setup.ts"],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
|
||||
Reference in New Issue
Block a user