import { describe, it, expect } from 'vitest' import { mount } from '@vue/test-utils' import DiceShapes from '~/components/Gameplay/DiceShapes.vue' describe('DiceShapes', () => { const defaultD6Props = { type: 'd6' as const, value: 5, } const defaultD20Props = { type: 'd20' as const, value: 15, } // ============================================================================ // D6 Rendering Tests // ============================================================================ describe('D6 Shape Rendering', () => { it('renders d6 die container when type is d6', () => { /** * The d6 die should render a square-shaped die with rounded corners, * decorative corner dots suggesting pip positions, and the value centered. */ const wrapper = mount(DiceShapes, { props: defaultD6Props, }) expect(wrapper.find('.die-container').exists()).toBe(true) expect(wrapper.find('svg.die-shape').exists()).toBe(true) }) it('renders rect element for d6 die body', () => { /** * D6 dice use a rounded rectangle (rect with rx/ry) to create the * classic square die shape with softened corners. */ const wrapper = mount(DiceShapes, { props: defaultD6Props, }) const rect = wrapper.find('rect') expect(rect.exists()).toBe(true) expect(rect.attributes('rx')).toBe('10') expect(rect.attributes('ry')).toBe('10') }) it('renders four corner dots for d6 decoration', () => { /** * Four decorative dots in the corners suggest the pip positions * of a physical die, adding visual authenticity. */ const wrapper = mount(DiceShapes, { props: defaultD6Props, }) const circles = wrapper.findAll('circle') expect(circles).toHaveLength(4) }) it('displays the value in the center', () => { const wrapper = mount(DiceShapes, { props: { ...defaultD6Props, value: 3 }, }) expect(wrapper.find('.die-value').text()).toBe('3') }) it('displays label when provided', () => { const wrapper = mount(DiceShapes, { props: { ...defaultD6Props, label: 'd6 (One)' }, }) expect(wrapper.find('.die-label').exists()).toBe(true) expect(wrapper.find('.die-label').text()).toBe('d6 (One)') }) it('displays sublabel when provided', () => { const wrapper = mount(DiceShapes, { props: { ...defaultD6Props, sublabel: '(3 + 2)' }, }) expect(wrapper.find('.die-sublabel').exists()).toBe(true) expect(wrapper.find('.die-sublabel').text()).toBe('(3 + 2)') }) it('hides label when not provided', () => { const wrapper = mount(DiceShapes, { props: defaultD6Props, }) expect(wrapper.find('.die-label').exists()).toBe(false) }) }) // ============================================================================ // D20 Rendering Tests // ============================================================================ describe('D20 Shape Rendering', () => { it('renders d20 die container when type is d20', () => { /** * The d20 die renders a hexagonal shape inspired by the icosahedron * geometry of a real d20, with facet lines for 3D effect. */ const wrapper = mount(DiceShapes, { props: defaultD20Props, }) expect(wrapper.find('.die-container').exists()).toBe(true) expect(wrapper.find('svg.die-shape').exists()).toBe(true) }) it('renders polygon element for d20 hexagonal shape', () => { /** * D20 uses a 6-sided polygon (hexagon) rotated with flat top * to suggest the multi-faceted nature of an icosahedron. */ const wrapper = mount(DiceShapes, { props: defaultD20Props, }) const polygon = wrapper.find('polygon') expect(polygon.exists()).toBe(true) // Verify points attribute contains 6 coordinate pairs const points = polygon.attributes('points') expect(points).toBeDefined() const pointPairs = points!.split(' ') expect(pointPairs).toHaveLength(6) }) it('renders facet lines for 3D effect', () => { /** * Two vertical lines inside the hexagon create the illusion * of depth and the faceted surface of a d20. */ const wrapper = mount(DiceShapes, { props: defaultD20Props, }) const lines = wrapper.findAll('line') expect(lines).toHaveLength(2) }) it('displays the value with large styling', () => { const wrapper = mount(DiceShapes, { props: defaultD20Props, }) const valueEl = wrapper.find('.die-value') expect(valueEl.text()).toBe('15') expect(valueEl.classes()).toContain('die-value-large') }) it('displays label when provided', () => { const wrapper = mount(DiceShapes, { props: { ...defaultD20Props, label: 'Resolution' }, }) expect(wrapper.find('.die-label').text()).toBe('Resolution') }) }) // ============================================================================ // Color Calculation Tests // ============================================================================ describe('Color Calculations', () => { it('applies fill color from color prop', () => { /** * The color prop (hex without #) should be converted to a proper * CSS color value and applied to the die fill. */ const wrapper = mount(DiceShapes, { props: { ...defaultD6Props, color: '0066ff' }, }) const rect = wrapper.find('rect') expect(rect.attributes('fill')).toBe('#0066ff') }) it('uses default red color when no color prop provided', () => { /** * Default dice color is cc0000 (red) matching the traditional * Strat-O-Matic dice color. */ const wrapper = mount(DiceShapes, { props: defaultD6Props, }) const rect = wrapper.find('rect') expect(rect.attributes('fill')).toBe('#cc0000') }) it('calculates darker stroke color from fill', () => { /** * The stroke color should be a darkened version of the fill * to create depth and definition around the die edge. */ const wrapper = mount(DiceShapes, { props: { ...defaultD6Props, color: 'ffffff' }, }) const rect = wrapper.find('rect') const stroke = rect.attributes('stroke') // White (ffffff) darkened by 20% should be #cccccc expect(stroke).toBe('#cccccc') }) it('uses white text on dark backgrounds', () => { /** * When the die color is dark (low luminance), the value text * should be white for readability. */ const wrapper = mount(DiceShapes, { props: { ...defaultD6Props, color: '000000' }, }) const valueEl = wrapper.find('.die-value') expect(valueEl.attributes('style')).toContain('color: #ffffff') }) it('uses dark text on light backgrounds', () => { /** * When the die color is light (high luminance), the value text * should be dark (#1a1a1a) for readability. */ const wrapper = mount(DiceShapes, { props: { ...defaultD6Props, color: 'ffffff' }, }) const valueEl = wrapper.find('.die-value') expect(valueEl.attributes('style')).toContain('color: #1a1a1a') }) it('uses white corner dots on dark d6 backgrounds', () => { const wrapper = mount(DiceShapes, { props: { ...defaultD6Props, color: '000066' }, }) const circles = wrapper.findAll('circle') expect(circles[0].attributes('fill')).toBe('#ffffff') }) it('uses dark corner dots on light d6 backgrounds', () => { const wrapper = mount(DiceShapes, { props: { ...defaultD6Props, color: 'ffff00' }, }) const circles = wrapper.findAll('circle') expect(circles[0].attributes('fill')).toBe('#333333') }) }) // ============================================================================ // Size Prop Tests // ============================================================================ describe('Size Configuration', () => { it('applies default size of 100px', () => { const wrapper = mount(DiceShapes, { props: defaultD6Props, }) const container = wrapper.find('.die-container') expect(container.attributes('style')).toContain('width: 100px') expect(container.attributes('style')).toContain('height: 100px') }) it('applies custom size from prop', () => { const wrapper = mount(DiceShapes, { props: { ...defaultD6Props, size: 80 }, }) const container = wrapper.find('.die-container') expect(container.attributes('style')).toContain('width: 80px') expect(container.attributes('style')).toContain('height: 80px') }) it('scales SVG viewBox to match size', () => { const wrapper = mount(DiceShapes, { props: { ...defaultD6Props, size: 120 }, }) const svg = wrapper.find('svg') expect(svg.attributes('viewBox')).toBe('0 0 120 120') }) it('scales d6 rect dimensions proportionally', () => { const wrapper = mount(DiceShapes, { props: { ...defaultD6Props, size: 80 }, }) const rect = wrapper.find('rect') // width and height should be size - 8 expect(rect.attributes('width')).toBe('72') expect(rect.attributes('height')).toBe('72') }) }) // ============================================================================ // Hexagon Point Calculation Tests // ============================================================================ describe('Hexagon Point Calculation', () => { it('generates valid hexagon points for d20', () => { /** * The hexagon points should form a valid 6-sided polygon * with coordinates that create a symmetrical shape. */ const wrapper = mount(DiceShapes, { props: { ...defaultD20Props, size: 100 }, }) const polygon = wrapper.find('polygon') const points = polygon.attributes('points')! const pointPairs = points.split(' ') // Each pair should be valid x,y coordinates pointPairs.forEach(pair => { const [x, y] = pair.split(',').map(Number) expect(x).toBeGreaterThanOrEqual(0) expect(x).toBeLessThanOrEqual(100) expect(y).toBeGreaterThanOrEqual(0) expect(y).toBeLessThanOrEqual(100) }) }) it('scales hexagon points with size prop', () => { const smallWrapper = mount(DiceShapes, { props: { ...defaultD20Props, size: 50 }, }) const largeWrapper = mount(DiceShapes, { props: { ...defaultD20Props, size: 100 }, }) const smallPoints = smallWrapper.find('polygon').attributes('points')! const largePoints = largeWrapper.find('polygon').attributes('points')! // First point of small should be roughly half the large const smallFirst = smallPoints.split(' ')[0].split(',').map(Number) const largeFirst = largePoints.split(' ')[0].split(',').map(Number) expect(smallFirst[0]).toBeCloseTo(largeFirst[0] / 2, 0) }) }) // ============================================================================ // Edge Cases // ============================================================================ describe('Edge Cases', () => { it('handles string value prop', () => { const wrapper = mount(DiceShapes, { props: { ...defaultD6Props, value: '20' }, }) expect(wrapper.find('.die-value').text()).toBe('20') }) it('handles very dark colors (near black)', () => { const wrapper = mount(DiceShapes, { props: { ...defaultD6Props, color: '0a0a0a' }, }) const valueEl = wrapper.find('.die-value') expect(valueEl.attributes('style')).toContain('color: #ffffff') }) it('handles very light colors (near white)', () => { const wrapper = mount(DiceShapes, { props: { ...defaultD6Props, color: 'f5f5f5' }, }) const valueEl = wrapper.find('.die-value') expect(valueEl.attributes('style')).toContain('color: #1a1a1a') }) it('handles mid-luminance colors correctly', () => { /** * Colors near the 50% luminance threshold should still pick * appropriate contrast. 808080 (gray) has exactly 50% luminance. */ const wrapper = mount(DiceShapes, { props: { ...defaultD6Props, color: '808080' }, }) // Gray is exactly at threshold, should use white text const valueEl = wrapper.find('.die-value') expect(valueEl.attributes('style')).toBeDefined() }) it('handles team colors correctly - red team', () => { const wrapper = mount(DiceShapes, { props: { ...defaultD6Props, color: 'cc0000' }, }) const rect = wrapper.find('rect') expect(rect.attributes('fill')).toBe('#cc0000') // Text should be white on red const valueEl = wrapper.find('.die-value') expect(valueEl.attributes('style')).toContain('color: #ffffff') }) it('handles team colors correctly - blue team', () => { const wrapper = mount(DiceShapes, { props: { ...defaultD6Props, color: '0066cc' }, }) const rect = wrapper.find('rect') expect(rect.attributes('fill')).toBe('#0066cc') // Text should be white on blue const valueEl = wrapper.find('.die-value') expect(valueEl.attributes('style')).toContain('color: #ffffff') }) it('handles team colors correctly - yellow team', () => { const wrapper = mount(DiceShapes, { props: { ...defaultD6Props, color: 'ffcc00' }, }) const rect = wrapper.find('rect') expect(rect.attributes('fill')).toBe('#ffcc00') // Text should be dark on yellow const valueEl = wrapper.find('.die-value') expect(valueEl.attributes('style')).toContain('color: #1a1a1a') }) }) // ============================================================================ // Slot Content Tests // ============================================================================ describe('Slot Content', () => { it('renders slot content instead of value when provided', () => { /** * The die-value slot allows custom content to be rendered * instead of the raw number value. */ const wrapper = mount(DiceShapes, { props: defaultD6Props, slots: { default: '', }, }) expect(wrapper.find('.custom-value').exists()).toBe(true) expect(wrapper.find('.custom-value').text()).toBe('★') }) it('falls back to value prop when no slot content', () => { const wrapper = mount(DiceShapes, { props: { ...defaultD6Props, value: 6 }, }) expect(wrapper.find('.die-value').text()).toBe('6') }) }) })