feat: add GameControlButtons component with unit tests

Add reusable GameControlButtons component with Setup, New Game, and Quit
buttons for arcade game navigation. Includes comprehensive unit tests.

- Create GameControlButtons component with optional callbacks
- Add flexWrap: nowrap and whiteSpace: nowrap to prevent wrapping
- Write 10 unit tests covering all button behaviors
- All tests passing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-10-06 14:24:31 -05:00
parent 2248c34215
commit 1f45c17e0a
2 changed files with 183 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
import React from 'react'
interface GameControlButtonsProps {
onSetup?: () => void
onNewGame?: () => void
onQuit?: () => void
}
export function GameControlButtons({ onSetup, onNewGame, onQuit }: GameControlButtonsProps) {
const buttonBaseStyle: React.CSSProperties = {
background: 'linear-gradient(135deg, #3498db, #2980b9)',
border: 'none',
borderRadius: '8px',
padding: '6px 12px',
fontSize: '13px',
fontWeight: 'bold',
color: 'white',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '4px',
transition: 'all 0.2s ease',
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)'
}
const handleMouseEnter = (e: React.MouseEvent<HTMLButtonElement>) => {
e.currentTarget.style.background = 'linear-gradient(135deg, #2980b9, #1c6ca1)'
e.currentTarget.style.transform = 'translateY(-1px)'
e.currentTarget.style.boxShadow = '0 3px 6px rgba(0, 0, 0, 0.15)'
}
const handleMouseLeave = (e: React.MouseEvent<HTMLButtonElement>) => {
e.currentTarget.style.background = 'linear-gradient(135deg, #3498db, #2980b9)'
e.currentTarget.style.transform = 'translateY(0)'
e.currentTarget.style.boxShadow = '0 2px 4px rgba(0, 0, 0, 0.1)'
}
return (
<div style={{ display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'nowrap' }}>
{onSetup && (
<button
onClick={onSetup}
style={buttonBaseStyle}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
aria-label="Setup game"
>
<span></span>
<span style={{ whiteSpace: 'nowrap' }}>Setup</span>
</button>
)}
{onNewGame && (
<button
onClick={onNewGame}
style={buttonBaseStyle}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
aria-label="Start new game"
>
<span>🎮</span>
<span style={{ whiteSpace: 'nowrap' }}>New Game</span>
</button>
)}
{onQuit && (
<button
onClick={onQuit}
style={buttonBaseStyle}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
aria-label="Quit to arcade"
>
<span>🏟</span>
<span style={{ whiteSpace: 'nowrap' }}>Quit</span>
</button>
)}
</div>
)
}

View File

@@ -0,0 +1,103 @@
import React from 'react'
import { render, screen, fireEvent } from '@testing-library/react'
import { vi } from 'vitest'
import { GameControlButtons } from '../GameControlButtons'
describe('GameControlButtons', () => {
it('renders no buttons when no callbacks provided', () => {
const { container } = render(<GameControlButtons />)
expect(container.querySelector('button')).not.toBeInTheDocument()
})
it('renders Setup button when onSetup is provided', () => {
const onSetup = vi.fn()
render(<GameControlButtons onSetup={onSetup} />)
const setupButton = screen.getByLabelText('Setup game')
expect(setupButton).toBeInTheDocument()
expect(setupButton).toHaveTextContent('Setup')
})
it('renders New Game button when onNewGame is provided', () => {
const onNewGame = vi.fn()
render(<GameControlButtons onNewGame={onNewGame} />)
const newGameButton = screen.getByLabelText('Start new game')
expect(newGameButton).toBeInTheDocument()
expect(newGameButton).toHaveTextContent('New Game')
})
it('renders Quit button when onQuit is provided', () => {
const onQuit = vi.fn()
render(<GameControlButtons onQuit={onQuit} />)
const quitButton = screen.getByLabelText('Quit to arcade')
expect(quitButton).toBeInTheDocument()
expect(quitButton).toHaveTextContent('Quit')
})
it('renders all buttons when all callbacks are provided', () => {
const onSetup = vi.fn()
const onNewGame = vi.fn()
const onQuit = vi.fn()
render(<GameControlButtons onSetup={onSetup} onNewGame={onNewGame} onQuit={onQuit} />)
expect(screen.getByLabelText('Setup game')).toBeInTheDocument()
expect(screen.getByLabelText('Start new game')).toBeInTheDocument()
expect(screen.getByLabelText('Quit to arcade')).toBeInTheDocument()
})
it('calls onSetup when Setup button is clicked', () => {
const onSetup = vi.fn()
render(<GameControlButtons onSetup={onSetup} />)
const setupButton = screen.getByLabelText('Setup game')
fireEvent.click(setupButton)
expect(onSetup).toHaveBeenCalledTimes(1)
})
it('calls onNewGame when New Game button is clicked', () => {
const onNewGame = vi.fn()
render(<GameControlButtons onNewGame={onNewGame} />)
const newGameButton = screen.getByLabelText('Start new game')
fireEvent.click(newGameButton)
expect(onNewGame).toHaveBeenCalledTimes(1)
})
it('calls onQuit when Quit button is clicked', () => {
const onQuit = vi.fn()
render(<GameControlButtons onQuit={onQuit} />)
const quitButton = screen.getByLabelText('Quit to arcade')
fireEvent.click(quitButton)
expect(onQuit).toHaveBeenCalledTimes(1)
})
it('has proper styling to prevent text wrapping', () => {
const onNewGame = vi.fn()
render(<GameControlButtons onNewGame={onNewGame} />)
const newGameButton = screen.getByLabelText('Start new game')
const textSpan = newGameButton.querySelector('span:last-child')
expect(textSpan).toHaveStyle({ whiteSpace: 'nowrap' })
})
it('container has flexWrap nowrap to prevent button wrapping', () => {
const onSetup = vi.fn()
const onNewGame = vi.fn()
const onQuit = vi.fn()
const { container } = render(
<GameControlButtons onSetup={onSetup} onNewGame={onNewGame} onQuit={onQuit} />
)
const buttonContainer = container.firstChild as HTMLElement
expect(buttonContainer).toHaveStyle({ flexWrap: 'nowrap' })
})
})