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:
parent
301a0fbc91
commit
a5fd278da3
4 changed files with 10023 additions and 0 deletions
9628
features/conversation-assistant/server/package-lock.json
generated
Normal file
9628
features/conversation-assistant/server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
35
features/landing/frontend/e2e/Dockerfile
Normal file
35
features/landing/frontend/e2e/Dockerfile
Normal 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"]
|
||||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Reference in a new issue