From c45fae8c5786b83fa6c938ef7f798de1a62b5ec2 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Mon, 2 Feb 2026 15:37:21 -0600 Subject: [PATCH] Add CardBack tests - TEST-015 complete (25 tests) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- frontend/src/game/objects/CardBack.spec.ts | 618 +++++++++++++++++++++ 1 file changed, 618 insertions(+) create mode 100644 frontend/src/game/objects/CardBack.spec.ts diff --git a/frontend/src/game/objects/CardBack.spec.ts b/frontend/src/game/objects/CardBack.spec.ts new file mode 100644 index 0000000..7628446 --- /dev/null +++ b/frontend/src/game/objects/CardBack.spec.ts @@ -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') + }) + }) +})