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:
80
apps/web/src/components/nav/GameControlButtons.tsx
Normal file
80
apps/web/src/components/nav/GameControlButtons.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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' })
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user