Add CardBack tests - TEST-015 complete (25 tests)

Quick win #1: Test coverage for CardBack game object

Tests cover:
- Constructor with sprite (texture exists)
- Constructor with fallback graphics (texture missing)
- Initial positioning and sizing
- setCardSize() for all sizes (small, medium, large)
- setSize() alias for method chaining
- getDimensions() returns correct values
- Fallback graphics rendering (background, border, pattern, emblem)
- destroy() cleanup for both sprite and graphics paths
- Integration tests for full lifecycle

Results:
- 25 new tests, all passing
- CardBack coverage: 0% → ~95%
- Game engine coverage starting point established
- Phaser mocking pattern validated

All tests pass (1025/1025)
This commit is contained in:
Cal Corum 2026-02-02 15:37:21 -06:00
parent 0d416028c0
commit c45fae8c57

View File

@ -0,0 +1,618 @@
/**
* Tests for CardBack game object.
*
* Verifies card back rendering, sizing, and fallback behavior.
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
// Disabling explicit-any for test mocks - Phaser types are complex and mocking requires any
import { describe, it, expect, beforeEach, vi } from 'vitest'
import type { CardSize } from '@/types/phaser'
// =============================================================================
// Mocks (must be defined before imports that use them)
// =============================================================================
/**
* Mock Phaser.GameObjects.Sprite
*/
class MockSprite {
x: number
y: number
texture: string
displayWidth: number = 0
displayHeight: number = 0
constructor(x: number, y: number, texture: string) {
this.x = x
this.y = y
this.texture = texture
}
setDisplaySize(width: number, height: number): this {
this.displayWidth = width
this.displayHeight = height
return this
}
destroy(): void {
// Mock destroy
}
}
/**
* Mock Phaser.GameObjects.Graphics
*/
class MockGraphics {
fillStyleCalls: Array<{ color: number; alpha: number }> = []
fillRoundedRectCalls: Array<{ x: number; y: number; w: number; h: number; r: number }> = []
lineStyleCalls: Array<{ width: number; color: number; alpha: number }> = []
strokeRoundedRectCalls: Array<{ x: number; y: number; w: number; h: number; r: number }> = []
lineBetweenCalls: Array<{ x1: number; y1: number; x2: number; y2: number }> = []
fillCircleCalls: Array<{ x: number; y: number; r: number }> = []
clear(): this {
this.fillStyleCalls = []
this.fillRoundedRectCalls = []
this.lineStyleCalls = []
this.strokeRoundedRectCalls = []
this.lineBetweenCalls = []
this.fillCircleCalls = []
return this
}
fillStyle(color: number, alpha: number): this {
this.fillStyleCalls.push({ color, alpha })
return this
}
fillRoundedRect(x: number, y: number, w: number, h: number, r: number): this {
this.fillRoundedRectCalls.push({ x, y, w, h, r })
return this
}
lineStyle(width: number, color: number, alpha: number): this {
this.lineStyleCalls.push({ width, color, alpha })
return this
}
strokeRoundedRect(x: number, y: number, w: number, h: number, r: number): this {
this.strokeRoundedRectCalls.push({ x, y, w, h, r })
return this
}
lineBetween(x1: number, y1: number, x2: number, y2: number): this {
this.lineBetweenCalls.push({ x1, y1, x2, y2 })
return this
}
fillCircle(x: number, y: number, r: number): this {
this.fillCircleCalls.push({ x, y, r })
return this
}
destroy(): void {
// Mock destroy
}
}
/**
* Mock Phaser.Scene
*/
class MockScene {
textures = {
exists: vi.fn(),
}
add = {
existing: vi.fn(),
sprite: vi.fn((x: number, y: number, texture: string) => new MockSprite(x, y, texture)),
graphics: vi.fn(() => new MockGraphics()),
}
}
/**
* Mock the asset loader function
*/
vi.mock('../assets/loader', () => ({
getCardBackTextureKey: vi.fn(() => 'card-back'),
}))
/**
* Mock Phaser module
*
* Note: We define the mock inline to avoid hoisting issues with class definitions
*/
vi.mock('phaser', () => {
// Define MockContainer inline within the mock factory
class MockContainerFactory {
x: number
y: number
scene: any
children: any[] = []
constructor(scene: any, x: number, y: number) {
this.scene = scene
this.x = x
this.y = y
}
add(child: any): this {
this.children.push(child)
return this
}
destroy(): void {
// Mock destroy
}
}
return {
default: {
GameObjects: {
Container: MockContainerFactory,
},
},
}
})
// Import CardBack AFTER mocks are set up
import { CardBack } from './CardBack'
// =============================================================================
// Tests
// =============================================================================
describe('CardBack', () => {
let mockScene: MockScene
beforeEach(() => {
mockScene = new MockScene()
vi.clearAllMocks()
})
describe('constructor', () => {
it('creates a card back with sprite when texture exists', () => {
/**
* Test that CardBack uses sprite when texture is available.
*
* When the card back texture is loaded, CardBack should create
* a sprite instead of fallback graphics.
*/
mockScene.textures.exists.mockReturnValue(true)
const cardBack = new CardBack(mockScene as any, 100, 200, 'medium')
expect(mockScene.textures.exists).toHaveBeenCalledWith('card-back')
expect(mockScene.add.sprite).toHaveBeenCalledWith(0, 0, 'card-back')
expect(mockScene.add.graphics).not.toHaveBeenCalled()
expect(mockScene.add.existing).toHaveBeenCalledWith(cardBack)
})
it('creates a card back with fallback graphics when texture missing', () => {
/**
* Test that CardBack falls back to graphics when texture unavailable.
*
* When the card back texture is not loaded, CardBack should create
* fallback graphics to ensure card backs are always visible.
*/
mockScene.textures.exists.mockReturnValue(false)
const cardBack = new CardBack(mockScene as any, 100, 200, 'medium')
expect(mockScene.textures.exists).toHaveBeenCalledWith('card-back')
expect(mockScene.add.sprite).not.toHaveBeenCalled()
expect(mockScene.add.graphics).toHaveBeenCalled()
expect(mockScene.add.existing).toHaveBeenCalledWith(cardBack)
})
it('sets initial position correctly', () => {
/**
* Test that CardBack is positioned at constructor coordinates.
*
* The x,y coordinates passed to constructor should set the
* container position correctly.
*/
mockScene.textures.exists.mockReturnValue(true)
const cardBack = new CardBack(mockScene as any, 150, 250, 'medium')
expect(cardBack.x).toBe(150)
expect(cardBack.y).toBe(250)
})
it('sets initial size correctly', () => {
/**
* Test that CardBack respects initial size parameter.
*
* The size parameter should be stored and applied to the
* card back display.
*/
mockScene.textures.exists.mockReturnValue(true)
const cardBack = new CardBack(mockScene as any, 100, 200, 'large')
expect(cardBack.getCardSize()).toBe('large')
})
it('defaults to medium size when not specified', () => {
/**
* Test that CardBack defaults to medium size.
*
* When no size is specified in constructor, medium should
* be used as the default.
*/
mockScene.textures.exists.mockReturnValue(true)
const cardBack = new CardBack(mockScene as any, 100, 200)
expect(cardBack.getCardSize()).toBe('medium')
})
})
describe('setCardSize', () => {
it('updates sprite display size when using sprite', () => {
/**
* Test that setCardSize updates sprite dimensions.
*
* When using a sprite, changing the size should update
* the sprite's display dimensions.
*/
mockScene.textures.exists.mockReturnValue(true)
const cardBack = new CardBack(mockScene as any, 100, 200, 'small')
const sprite = (cardBack as any).sprite as MockSprite
cardBack.setCardSize('large')
// Large size is 150x210 from CARD_SIZES
expect(sprite.displayWidth).toBe(150)
expect(sprite.displayHeight).toBe(210)
})
it('redraws fallback graphics when using graphics', () => {
/**
* Test that setCardSize redraws fallback graphics.
*
* When using fallback graphics, changing the size should
* redraw the graphics at the new dimensions.
*/
mockScene.textures.exists.mockReturnValue(false)
const cardBack = new CardBack(mockScene as any, 100, 200, 'small')
const graphics = (cardBack as any).fallbackGraphics as MockGraphics
// Clear the initial draw calls
graphics.clear()
graphics.fillStyleCalls = []
graphics.fillRoundedRectCalls = []
cardBack.setCardSize('large')
// Should have redrawn with new size
expect(graphics.fillStyleCalls.length).toBeGreaterThan(0)
expect(graphics.fillRoundedRectCalls.length).toBeGreaterThan(0)
})
it('updates current size', () => {
/**
* Test that setCardSize updates the stored size.
*
* The current size should be tracked and retrievable.
*/
mockScene.textures.exists.mockReturnValue(true)
const cardBack = new CardBack(mockScene as any, 100, 200, 'small')
cardBack.setCardSize('medium')
expect(cardBack.getCardSize()).toBe('medium')
})
})
describe('setSize alias', () => {
it('calls setCardSize internally', () => {
/**
* Test that setSize is an alias for setCardSize.
*
* The setSize method should delegate to setCardSize
* for consistency with the example in the file comment.
*/
mockScene.textures.exists.mockReturnValue(true)
const cardBack = new CardBack(mockScene as any, 100, 200, 'small')
const result = cardBack.setSize('large')
expect(cardBack.getCardSize()).toBe('large')
expect(result).toBe(cardBack) // Should return this for chaining
})
})
describe('getDimensions', () => {
it('returns correct dimensions for small size', () => {
/**
* Test that getDimensions returns correct small dimensions.
*
* Small cards should return 60x83 dimensions.
*/
mockScene.textures.exists.mockReturnValue(true)
const cardBack = new CardBack(mockScene as any, 100, 200, 'small')
const dimensions = cardBack.getDimensions()
expect(dimensions.width).toBe(60)
expect(dimensions.height).toBe(84)
})
it('returns correct dimensions for medium size', () => {
/**
* Test that getDimensions returns correct medium dimensions.
*
* Medium cards should return 100x140 dimensions.
*/
mockScene.textures.exists.mockReturnValue(true)
const cardBack = new CardBack(mockScene as any, 100, 200, 'medium')
const dimensions = cardBack.getDimensions()
expect(dimensions.width).toBe(100)
expect(dimensions.height).toBe(140)
})
it('returns correct dimensions for large size', () => {
/**
* Test that getDimensions returns correct large dimensions.
*
* Large cards should return 150x210 dimensions.
*/
mockScene.textures.exists.mockReturnValue(true)
const cardBack = new CardBack(mockScene as any, 100, 200, 'large')
const dimensions = cardBack.getDimensions()
expect(dimensions.width).toBe(150)
expect(dimensions.height).toBe(210)
})
it('returns updated dimensions after size change', () => {
/**
* Test that getDimensions reflects size changes.
*
* After changing size, getDimensions should return the
* new dimensions.
*/
mockScene.textures.exists.mockReturnValue(true)
const cardBack = new CardBack(mockScene as any, 100, 200, 'small')
cardBack.setCardSize('large')
const dimensions = cardBack.getDimensions()
expect(dimensions.width).toBe(150)
expect(dimensions.height).toBe(210)
})
})
describe('fallback graphics rendering', () => {
it('draws card background with correct color', () => {
/**
* Test that fallback graphics use correct background color.
*
* The default background color should be used for the
* card back rectangle.
*/
mockScene.textures.exists.mockReturnValue(false)
const cardBack = new CardBack(mockScene as any, 100, 200, 'medium')
const graphics = (cardBack as any).fallbackGraphics as MockGraphics
const fillCalls = graphics.fillStyleCalls
expect(fillCalls.length).toBeGreaterThan(0)
// DEFAULT_BACK_COLOR = 0x2d3748
expect(fillCalls[0].color).toBe(0x2d3748)
})
it('draws rounded rectangle for card shape', () => {
/**
* Test that fallback graphics draw rounded rectangle.
*
* The card back should be a rounded rectangle with
* appropriate corner radius.
*/
mockScene.textures.exists.mockReturnValue(false)
const cardBack = new CardBack(mockScene as any, 100, 200, 'medium')
const graphics = (cardBack as any).fallbackGraphics as MockGraphics
expect(graphics.fillRoundedRectCalls.length).toBeGreaterThan(0)
const rect = graphics.fillRoundedRectCalls[0]
expect(rect.r).toBe(8) // CARD_CORNER_RADIUS
})
it('draws border around card', () => {
/**
* Test that fallback graphics include a border.
*
* The card back should have a visible border stroke
* around the rounded rectangle.
*/
mockScene.textures.exists.mockReturnValue(false)
const cardBack = new CardBack(mockScene as any, 100, 200, 'medium')
const graphics = (cardBack as any).fallbackGraphics as MockGraphics
expect(graphics.strokeRoundedRectCalls.length).toBeGreaterThan(0)
})
it('draws pattern lines on card back', () => {
/**
* Test that fallback graphics include pattern lines.
*
* The card back should have horizontal pattern lines
* to make it visually distinct.
*/
mockScene.textures.exists.mockReturnValue(false)
const cardBack = new CardBack(mockScene as any, 100, 200, 'medium')
const graphics = (cardBack as any).fallbackGraphics as MockGraphics
// Should have multiple pattern lines
expect(graphics.lineBetweenCalls.length).toBeGreaterThan(5)
})
it('draws center emblem circle', () => {
/**
* Test that fallback graphics include center emblem.
*
* The card back should have a center circle as a
* placeholder emblem.
*/
mockScene.textures.exists.mockReturnValue(false)
const cardBack = new CardBack(mockScene as any, 100, 200, 'medium')
const graphics = (cardBack as any).fallbackGraphics as MockGraphics
expect(graphics.fillCircleCalls.length).toBeGreaterThan(0)
const circle = graphics.fillCircleCalls[0]
expect(circle.x).toBe(0) // Centered
expect(circle.y).toBe(0) // Centered
})
it('centers graphics correctly for all sizes', () => {
/**
* Test that fallback graphics are centered regardless of size.
*
* The fallback graphics should be drawn centered at (0, 0)
* relative to the container for all card sizes.
*/
mockScene.textures.exists.mockReturnValue(false)
const sizes: CardSize[] = ['small', 'medium', 'large']
sizes.forEach((size) => {
const cardBack = new CardBack(mockScene as any, 100, 200, size)
const graphics = (cardBack as any).fallbackGraphics as MockGraphics
const rect = graphics.fillRoundedRectCalls[0]
// Should be centered (negative half width/height)
expect(rect.x).toBeLessThan(0)
expect(rect.y).toBeLessThan(0)
})
})
})
describe('destroy', () => {
it('destroys sprite when using sprite', () => {
/**
* Test that destroy cleans up sprite.
*
* When CardBack is destroyed, the sprite should be
* destroyed and the reference cleared.
*/
mockScene.textures.exists.mockReturnValue(true)
const cardBack = new CardBack(mockScene as any, 100, 200, 'medium')
const sprite = (cardBack as any).sprite as MockSprite
const destroySpy = vi.spyOn(sprite, 'destroy')
cardBack.destroy()
expect(destroySpy).toHaveBeenCalled()
expect((cardBack as any).sprite).toBeNull()
})
it('destroys graphics when using fallback', () => {
/**
* Test that destroy cleans up fallback graphics.
*
* When CardBack is destroyed, the fallback graphics should
* be destroyed and the reference cleared.
*/
mockScene.textures.exists.mockReturnValue(false)
const cardBack = new CardBack(mockScene as any, 100, 200, 'medium')
const graphics = (cardBack as any).fallbackGraphics as MockGraphics
const destroySpy = vi.spyOn(graphics, 'destroy')
cardBack.destroy()
expect(destroySpy).toHaveBeenCalled()
expect((cardBack as any).fallbackGraphics).toBeNull()
})
it('handles destroy when sprite is already null', () => {
/**
* Test that destroy handles null sprite gracefully.
*
* Destroy should not error if sprite is already null.
*/
mockScene.textures.exists.mockReturnValue(true)
const cardBack = new CardBack(mockScene as any, 100, 200, 'medium')
;(cardBack as any).sprite = null
expect(() => cardBack.destroy()).not.toThrow()
})
it('handles destroy when graphics is already null', () => {
/**
* Test that destroy handles null graphics gracefully.
*
* Destroy should not error if fallback graphics is already null.
*/
mockScene.textures.exists.mockReturnValue(false)
const cardBack = new CardBack(mockScene as any, 100, 200, 'medium')
;(cardBack as any).fallbackGraphics = null
expect(() => cardBack.destroy()).not.toThrow()
})
})
describe('integration', () => {
it('can create, resize, and destroy a card back', () => {
/**
* Test full lifecycle of a CardBack.
*
* This integration test verifies that a CardBack can be
* created, resized multiple times, and destroyed without errors.
*/
mockScene.textures.exists.mockReturnValue(true)
const cardBack = new CardBack(mockScene as any, 100, 200, 'small')
expect(cardBack.getCardSize()).toBe('small')
cardBack.setCardSize('medium')
expect(cardBack.getCardSize()).toBe('medium')
cardBack.setCardSize('large')
expect(cardBack.getCardSize()).toBe('large')
expect(() => cardBack.destroy()).not.toThrow()
})
it('switches between sizes correctly with method chaining', () => {
/**
* Test method chaining with setSize.
*
* The setSize method should return this to allow
* method chaining.
*/
mockScene.textures.exists.mockReturnValue(true)
const cardBack = new CardBack(mockScene as any, 100, 200, 'small')
const result = cardBack.setSize('medium').setSize('large')
expect(result).toBe(cardBack)
expect(cardBack.getCardSize()).toBe('large')
})
})
})