test(landing): add E2E and unit tests for FloatingSettings

Testing infrastructure for FloatingSettings component:
- Add E2E Playwright tests for settings triggers
- Add unit tests for FloatingSettings trigger behavior
- Add e2e Dockerfile for containerized testing
- Update conversation-assistant server package-lock

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Quinn Ftw 2025-12-28 21:25:40 -08:00
parent 301a0fbc91
commit a5fd278da3
4 changed files with 10023 additions and 0 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,35 @@
# E2E Testing Dockerfile for Landing Frontend
# Uses Microsoft's Playwright base image for browser testing
#
# Build from the frontend directory:
# docker build -f e2e/Dockerfile -t landing-e2e-test .
#
# Run E2E tests:
# docker run --rm landing-e2e-test
FROM mcr.microsoft.com/playwright:v1.49.1-noble
WORKDIR /app
# Install pnpm globally
RUN npm install -g pnpm
# Copy package files
COPY package.json pnpm-lock.yaml* ./
# Install dependencies (includes linked packages from registry)
RUN pnpm install --frozen-lockfile || pnpm install
# Copy source code
COPY . .
# Build the application
RUN pnpm build
# Set environment for CI
ENV CI=true
ENV BASE_URL=http://localhost:3100
# Default: run E2E tests
# Override with: docker run --rm landing-e2e-test pnpm test
CMD ["pnpm", "test:e2e"]

View file

@ -0,0 +1,199 @@
/**
* E2E Tests for FloatingSettings Trigger Modes
*
* Tests the trigger mode functionality that controls when sounds play:
* - All: All sounds play (default)
* - No Hover: Suppresses hover sounds
* - Clicks: Only click events + feedback
* - Feedback: Only success/error sounds
* - Mute: No sounds
*/
import { test, expect } from '@playwright/test'
test.describe('FloatingSettings Trigger Modes', () => {
test.beforeEach(async ({ page }) => {
// Clear localStorage to start fresh
await page.goto('/')
await page.evaluate(() => localStorage.clear())
await page.reload()
})
test('should show floating settings button', async ({ page }) => {
const settingsButton = page.getByRole('button', { name: /settings/i })
await expect(settingsButton).toBeVisible()
})
test('should expand settings menu on click', async ({ page }) => {
const settingsButton = page.getByRole('button', { name: /settings/i })
await settingsButton.click()
// Should show close button when expanded
const closeButton = page.getByRole('button', { name: /close settings/i })
await expect(closeButton).toBeVisible()
})
test('should show Triggers category button', async ({ page }) => {
const settingsButton = page.getByRole('button', { name: /settings/i })
await settingsButton.click()
// Triggers button should be visible
const triggersButton = page.getByRole('button', { name: /triggers/i })
await expect(triggersButton).toBeVisible()
})
test('should default to "All" trigger mode', async ({ page }) => {
const settingsButton = page.getByRole('button', { name: /settings/i })
await settingsButton.click()
// Should show "All" as current selection
const triggersButton = page.getByRole('button', { name: /triggers.*all/i })
await expect(triggersButton).toBeVisible()
})
test('should expand trigger options when clicked', async ({ page }) => {
const settingsButton = page.getByRole('button', { name: /settings/i })
await settingsButton.click()
const triggersButton = page.getByRole('button', { name: /triggers/i })
await triggersButton.click()
// All 5 options should be visible
await expect(page.getByRole('button', { name: /trigger mode to all/i })).toBeVisible()
await expect(page.getByRole('button', { name: /trigger mode to no hover/i })).toBeVisible()
await expect(page.getByRole('button', { name: /trigger mode to clicks/i })).toBeVisible()
await expect(page.getByRole('button', { name: /trigger mode to feedback/i })).toBeVisible()
await expect(page.getByRole('button', { name: /trigger mode to mute/i })).toBeVisible()
})
test('should change trigger mode to No Hover', async ({ page }) => {
const settingsButton = page.getByRole('button', { name: /settings/i })
await settingsButton.click()
const triggersButton = page.getByRole('button', { name: /triggers/i })
await triggersButton.click()
// Select No Hover
const noHoverOption = page.getByRole('button', { name: /trigger mode to no hover/i })
await noHoverOption.click()
// Should update button text
await expect(page.getByRole('button', { name: /triggers.*no hover/i })).toBeVisible()
})
test('should change trigger mode to Clicks', async ({ page }) => {
const settingsButton = page.getByRole('button', { name: /settings/i })
await settingsButton.click()
const triggersButton = page.getByRole('button', { name: /triggers/i })
await triggersButton.click()
// Select Clicks
const clicksOption = page.getByRole('button', { name: /trigger mode to clicks/i })
await clicksOption.click()
// Should update button text
await expect(page.getByRole('button', { name: /triggers.*clicks/i })).toBeVisible()
})
test('should change trigger mode to Feedback', async ({ page }) => {
const settingsButton = page.getByRole('button', { name: /settings/i })
await settingsButton.click()
const triggersButton = page.getByRole('button', { name: /triggers/i })
await triggersButton.click()
// Select Feedback
const feedbackOption = page.getByRole('button', { name: /trigger mode to feedback/i })
await feedbackOption.click()
// Should update button text
await expect(page.getByRole('button', { name: /triggers.*feedback/i })).toBeVisible()
})
test('should change trigger mode to Mute', async ({ page }) => {
const settingsButton = page.getByRole('button', { name: /settings/i })
await settingsButton.click()
const triggersButton = page.getByRole('button', { name: /triggers/i })
await triggersButton.click()
// Select Mute
const muteOption = page.getByRole('button', { name: /trigger mode to mute/i })
await muteOption.click()
// Should update button text
await expect(page.getByRole('button', { name: /triggers.*mute/i })).toBeVisible()
})
test('should persist trigger mode to localStorage', async ({ page }) => {
const settingsButton = page.getByRole('button', { name: /settings/i })
await settingsButton.click()
const triggersButton = page.getByRole('button', { name: /triggers/i })
await triggersButton.click()
// Select Feedback mode
const feedbackOption = page.getByRole('button', { name: /trigger mode to feedback/i })
await feedbackOption.click()
// Check localStorage
const storedMode = await page.evaluate(() => localStorage.getItem('lilith-sound-triggers'))
expect(storedMode).toBe('feedback')
})
test('should restore trigger mode from localStorage on reload', async ({ page }) => {
// First select a trigger mode
const settingsButton = page.getByRole('button', { name: /settings/i })
await settingsButton.click()
const triggersButton = page.getByRole('button', { name: /triggers/i })
await triggersButton.click()
// Select Clicks mode
const clicksOption = page.getByRole('button', { name: /trigger mode to clicks/i })
await clicksOption.click()
// Verify localStorage was set
const storedMode = await page.evaluate(() => localStorage.getItem('lilith-sound-triggers'))
expect(storedMode).toBe('clicks')
// Reload the page
await page.reload()
// Re-open settings
await page.getByRole('button', { name: /settings/i }).click()
// Should show "Clicks" as current selection (restored from localStorage)
await expect(page.getByRole('button', { name: /triggers.*clicks/i })).toBeVisible()
})
test('should collapse trigger options when clicking elsewhere', async ({ page }) => {
const settingsButton = page.getByRole('button', { name: /settings/i })
await settingsButton.click()
const triggersButton = page.getByRole('button', { name: /triggers/i })
await triggersButton.click()
// Options should be visible
await expect(page.getByRole('button', { name: /trigger mode to all/i })).toBeVisible()
// Click on Volume button to switch categories
const volumeButton = page.getByRole('button', { name: /volume/i })
await volumeButton.click()
// Trigger options should collapse (Volume options should show instead)
await expect(page.getByRole('button', { name: /trigger mode to all/i })).not.toBeVisible()
})
test('should show all 4 settings categories', async ({ page }) => {
const settingsButton = page.getByRole('button', { name: /settings/i })
await settingsButton.click()
// All 4 category buttons should be visible
await expect(page.getByRole('button', { name: /particle/i })).toBeVisible()
await expect(page.getByRole('button', { name: /sound/i })).toBeVisible()
await expect(page.getByRole('button', { name: /volume/i })).toBeVisible()
await expect(page.getByRole('button', { name: /triggers/i })).toBeVisible()
})
})

View file

@ -0,0 +1,161 @@
/**
* Unit Tests for FloatingSettings Trigger Mode functionality
*
* Tests the trigger mode UI and state management:
* - Renders all 5 trigger mode options
* - Changes trigger mode on selection
* - Persists to soundEngine
*/
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { describe, it, expect, vi, beforeEach } from 'vitest'
// Mock framer-motion
vi.mock('framer-motion', () => ({
motion: {
div: ({ children, ...props }: { children: React.ReactNode }) => <div {...props}>{children}</div>,
button: ({ children, ...props }: { children: React.ReactNode }) => <button {...props}>{children}</button>,
},
AnimatePresence: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}))
// Mock sound engine with trigger mode support
vi.mock('@ui/effects-sound', () => ({
soundEngine: {
isEnabled: vi.fn(() => false),
enable: vi.fn(),
disable: vi.fn(),
play: vi.fn(),
getPack: vi.fn(() => 'human'),
setPack: vi.fn(),
getVolume: vi.fn(() => 0.5),
setVolume: vi.fn(),
getTriggerMode: vi.fn(() => 'all'),
setTriggerMode: vi.fn(),
},
}))
// Mock particle effects
vi.mock('@ui/effects-mouse', () => ({
getStoredOrRandomStyle: vi.fn(() => 'glow'),
setParticleStyle: vi.fn(),
PARTICLE_STYLES: ['off', 'glow', 'party', 'snow', 'glitter', 'stars'],
}))
// Mock i18n
vi.mock('@lilith/i18n', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
// Import after mocks are set up
import FloatingSettings from '../FloatingSettings'
import { soundEngine } from '@ui/effects-sound'
describe('FloatingSettings Trigger Modes', () => {
beforeEach(() => {
vi.clearAllMocks()
})
const openSettings = () => {
const settingsButton = screen.getByRole('button', { name: /settings/i })
fireEvent.click(settingsButton)
}
const openTriggers = () => {
openSettings()
const triggersButton = screen.getByRole('button', { name: /triggers/i })
fireEvent.click(triggersButton)
}
describe('Rendering', () => {
it('renders the settings FAB button', () => {
render(<FloatingSettings />)
expect(screen.getByRole('button', { name: /settings/i })).toBeInTheDocument()
})
it('shows Triggers category button when expanded', async () => {
render(<FloatingSettings />)
openSettings()
await waitFor(() => {
expect(screen.getByRole('button', { name: /triggers/i })).toBeInTheDocument()
})
})
it('shows all 5 trigger options when Triggers category is clicked', async () => {
render(<FloatingSettings />)
openTriggers()
await waitFor(() => {
expect(screen.getByRole('button', { name: /trigger mode to all/i })).toBeInTheDocument()
expect(screen.getByRole('button', { name: /trigger mode to no hover/i })).toBeInTheDocument()
expect(screen.getByRole('button', { name: /trigger mode to clicks/i })).toBeInTheDocument()
expect(screen.getByRole('button', { name: /trigger mode to feedback/i })).toBeInTheDocument()
expect(screen.getByRole('button', { name: /trigger mode to mute/i })).toBeInTheDocument()
})
})
})
describe('Trigger Mode Selection', () => {
it('calls soundEngine.setTriggerMode when selecting "No Hover"', async () => {
render(<FloatingSettings />)
openTriggers()
const noHoverOption = await screen.findByRole('button', { name: /trigger mode to no hover/i })
fireEvent.click(noHoverOption)
expect(soundEngine.setTriggerMode).toHaveBeenCalledWith('no-hover')
})
it('calls soundEngine.setTriggerMode when selecting "Clicks"', async () => {
render(<FloatingSettings />)
openTriggers()
const clicksOption = await screen.findByRole('button', { name: /trigger mode to clicks/i })
fireEvent.click(clicksOption)
expect(soundEngine.setTriggerMode).toHaveBeenCalledWith('clicks')
})
it('calls soundEngine.setTriggerMode when selecting "Feedback"', async () => {
render(<FloatingSettings />)
openTriggers()
const feedbackOption = await screen.findByRole('button', { name: /trigger mode to feedback/i })
fireEvent.click(feedbackOption)
expect(soundEngine.setTriggerMode).toHaveBeenCalledWith('feedback')
})
it('calls soundEngine.setTriggerMode when selecting "Mute"', async () => {
render(<FloatingSettings />)
openTriggers()
const muteOption = await screen.findByRole('button', { name: /trigger mode to mute/i })
fireEvent.click(muteOption)
expect(soundEngine.setTriggerMode).toHaveBeenCalledWith('off')
})
})
describe('Initial State', () => {
it('initializes trigger mode from soundEngine', () => {
vi.mocked(soundEngine.getTriggerMode).mockReturnValue('feedback')
render(<FloatingSettings />)
openSettings()
// Button should show current mode
expect(screen.getByRole('button', { name: /triggers.*feedback/i })).toBeInTheDocument()
})
it('defaults to "All" when soundEngine returns "all"', () => {
vi.mocked(soundEngine.getTriggerMode).mockReturnValue('all')
render(<FloatingSettings />)
openSettings()
expect(screen.getByRole('button', { name: /triggers.*all/i })).toBeInTheDocument()
})
})
})