platform-codebase/@packages/@core/config/playwright.config.ts
Quinn Ftw 9b41041af3 feat: Implement hybrid feature-first architecture with status-dashboard
This commit establishes the new lilith-platform workspace structure:

Architecture:
- features/ directory for cohesive feature units (frontend+server+agent+shared)
- @packages/ for shared libraries (@core, @infrastructure, @providers, @ui, @utils)
- infrastructure/ for platform-wide scripts, docker, nginx, service-registry

Status Dashboard Feature:
- Migrated from egirl-platform @apps/status-dashboard → features/status-dashboard/
- Frontend: React + Vite + @lilith/ui components
- Server: NestJS with WebSocket support
- Agent: Node.js metrics collector
- Infrastructure: Deploy script for VPS

Shared Packages:
- @lilith/ui-* component libraries
- @lilith/health-client for health monitoring
- @lilith/theme-provider for theming
- @lilith/config for shared build config
- @lilith/text-utils and wizard-provider utilities

Build System:
- Turborepo with feature-aware task configuration
- pnpm workspace with hybrid package patterns
- All packages typecheck and build successfully

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-23 18:40:37 -08:00

408 lines
11 KiB
TypeScript

import { defineConfig, devices, PlaywrightTestConfig } from '@playwright/test'
import path from 'path'
import { fileURLToPath } from 'url'
// ES module equivalent of __dirname
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
/**
* Shared Base Playwright Configuration
*
* This configuration provides common defaults for all apps in the monorepo.
* Each app extends this config and overrides specific settings as needed.
*
* Test outputs are centralized to /test-output/{app-name}/ in monorepo root.
* This keeps test results organized and prevents git tracking issues.
*
* @see https://playwright.dev/docs/test-configuration
*/
export type DevicePreset =
| 'desktop'
| 'mobile'
| 'tablet'
| 'obs-overlay'
| 'chromium-only'
| 'all'
export interface ClusterModeOptions {
/** Enable cluster mode (defaults to E2E_USE_CLUSTER env var) */
enabled?: boolean
/** Deployment name for nginx routing (e.g., 'fanclub', 'private/channel') */
deploymentName?: string
/** App name for cluster manager (e.g., 'fan-club', 'channel-studio') */
appName?: string
/** Cluster manager script to use (default: 'e2e-cluster-start.js') */
clusterScript?: 'e2e-cluster-start.js' | 'e2e-wrapper.js'
/** Cluster startup timeout in ms (default: 180000) */
clusterTimeout?: number
}
export interface SharedPlaywrightConfigOptions {
/** Base URL for the app (e.g., 'http://localhost:3000') */
baseURL?: string
/** Port for the dev server (e.g., 3000) */
port?: number
/** Test directory relative to app root (default: './e2e') */
testDir?: string
/** Dev server start command (default: 'pnpm dev') */
devCommand?: string
/** Maximum test timeout in milliseconds (default: 30000) */
timeout?: number
/** Whether to run tests in parallel (default: true) */
fullyParallel?: boolean
/** Number of workers (default: CI ? 1 : undefined) */
workers?: number
/** Number of retries on failure (default: CI ? 2 : 0) */
retries?: number
/** Whether to reuse existing dev server (default: !CI) */
reuseServer?: boolean
/** App name for centralized output directory (e.g., 'fan-club') */
appName?: string
/** Device preset to use (default: 'desktop') */
devicePreset?: DevicePreset
/** Cluster mode configuration */
clusterMode?: ClusterModeOptions
/** Auth setup project configuration */
authSetup?: {
/** Enable auth setup project (default: false) */
enabled: boolean
/** Auth storage state path (default: 'e2e/.auth/performer.json') */
storagePath?: string
}
/** Environment variables for dev server (e.g., { VITE_ENABLE_MSW: 'true' }) */
env?: Record<string, string>
}
/**
* Creates a Playwright config with shared defaults
* Apps can extend this and override specific settings
*/
export function createPlaywrightConfig(
options: SharedPlaywrightConfigOptions = {}
): PlaywrightTestConfig {
const {
baseURL: userBaseURL,
port: userPort,
testDir = './e2e',
devCommand = 'pnpm dev',
timeout = 30000,
fullyParallel = true,
workers = process.env.CI ? 1 : undefined,
retries = process.env.CI ? 2 : 0,
reuseServer = !process.env.CI,
appName,
devicePreset = 'desktop',
clusterMode,
authSetup,
env: envVars,
} = options
// Determine if cluster mode is enabled
const useCluster = clusterMode?.enabled ?? process.env.E2E_USE_CLUSTER === 'true'
// Determine deployment name and base URL
const deploymentName = clusterMode?.deploymentName || process.env.E2E_DEPLOYMENT || 'app'
const clusterAppName = clusterMode?.appName || appName || 'app'
// Cluster mode uses nginx routing, standalone uses direct port
const defaultPort = useCluster ? 80 : 3000
const port = userPort ?? defaultPort
const defaultBaseURL = useCluster
? `http://localhost/${deploymentName}`
: `http://localhost:${port}`
const baseURL = userBaseURL ?? defaultBaseURL
// Determine monorepo root (assumes this package is in @packages/config)
const monorepoRoot = path.resolve(__dirname, '../..')
// Centralized test output directory
const outputDir = appName
? path.join(monorepoRoot, 'test-output', appName)
: 'test-results' // Fallback for apps not using appName
// Select device projects based on preset
const projects = getDeviceProjects(devicePreset, authSetup)
// Build webServer configuration
const clusterScript = clusterMode?.clusterScript || 'e2e-cluster-start.js'
const clusterTimeout = clusterMode?.clusterTimeout || 180000
// Build environment variables string for command prefix
const envPrefix = envVars
? Object.entries(envVars)
.map(([key, value]) => `${key}=${value}`)
.join(' ') + ' '
: ''
const webServer = useCluster
? {
// Cluster mode: Use cluster manager script
command: `node ../../scripts/cluster-manager/${clusterScript}`,
url: baseURL,
port: clusterScript === 'e2e-wrapper.js' ? port : undefined,
reuseExistingServer: reuseServer,
timeout: clusterTimeout,
env: {
E2E_APP: clusterAppName,
E2E_DEPLOYMENT: deploymentName,
...(envVars || {}),
},
}
: {
// Standalone mode: Prefix command with env vars for Vite to pick them up
command: `${envPrefix}${devCommand}`,
port,
reuseExistingServer: reuseServer,
timeout: 120000,
stdout: 'ignore',
stderr: 'pipe',
}
return defineConfig({
testDir,
timeout,
fullyParallel,
forbidOnly: !!process.env.CI,
retries,
workers,
// Shared reporter configuration
reporter: [
['list'],
['html', { outputFolder: path.join(outputDir, 'html-report'), open: 'never' }],
['junit', { outputFile: path.join(outputDir, 'junit.xml') }],
...(process.env.CI ? [['github' as const]] : []),
],
// Shared expect configuration
expect: {
timeout: 5000,
},
// Shared test configuration
use: {
baseURL,
headless: true,
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
actionTimeout: 10000,
navigationTimeout: 15000,
},
// Device-specific projects
projects,
// Dev server configuration
webServer,
// Centralized output directory
outputDir: path.join(outputDir, 'test-results'),
})
}
/**
* Get device projects based on preset
*/
function getDeviceProjects(
preset: DevicePreset,
authSetup?: SharedPlaywrightConfigOptions['authSetup']
) {
const projects: any[] = []
// Add auth setup project if enabled
if (authSetup?.enabled) {
const storagePath = authSetup.storagePath || 'e2e/.auth/performer.json'
projects.push({
name: 'setup',
testMatch: /.*\.setup\.ts/,
use: {
storageState: undefined, // Don't load auth for setup
},
})
// Helper to add device with auth
const addAuthDevice = (name: string, device: any) => {
projects.push({
name,
use: {
...device,
storageState: storagePath,
serviceWorkers: 'block', // Prevent service worker caching issues
},
dependencies: ['setup'],
})
}
// Build device projects with auth
switch (preset) {
case 'chromium-only':
addAuthDevice('chromium', devices['Desktop Chrome'])
break
case 'desktop':
addAuthDevice('chromium', devices['Desktop Chrome'])
addAuthDevice('firefox', devices['Desktop Firefox'])
break
case 'mobile':
addAuthDevice('mobile-chrome', devices['Pixel 5'])
addAuthDevice('mobile-safari', devices['iPhone 13'])
break
case 'tablet':
addAuthDevice('ipad-pro', devices['iPad Pro'])
break
case 'obs-overlay':
addAuthDevice('chromium', devices['Desktop Chrome'])
addAuthDevice('obs-overlay', {
...devices['Desktop Chrome'],
viewport: { width: 1920, height: 1080 },
hasTouch: false,
reducedMotion: 'no-preference',
})
break
case 'all':
addAuthDevice('chromium', devices['Desktop Chrome'])
addAuthDevice('firefox', devices['Desktop Firefox'])
addAuthDevice('webkit', devices['Desktop Safari'])
addAuthDevice('mobile-chrome', devices['Pixel 5'])
addAuthDevice('mobile-safari', devices['iPhone 13'])
break
}
} else {
// No auth setup - return standard device projects
switch (preset) {
case 'chromium-only':
projects.push({
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
})
break
case 'desktop':
projects.push(
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
}
)
break
case 'mobile':
projects.push(
{
name: 'mobile-chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'mobile-safari',
use: { ...devices['iPhone 13'] },
}
)
break
case 'tablet':
projects.push({
name: 'ipad-pro',
use: { ...devices['iPad Pro'] },
})
break
case 'obs-overlay':
projects.push(
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'obs-overlay',
use: {
...devices['Desktop Chrome'],
viewport: { width: 1920, height: 1080 },
hasTouch: false,
reducedMotion: 'no-preference',
},
}
)
break
case 'all':
projects.push(
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'mobile-chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'mobile-safari',
use: { ...devices['iPhone 13'] },
}
)
break
}
}
return projects
}
/**
* Default export for apps that just want standard config
*/
export default createPlaywrightConfig()
/**
* Common device configurations apps can use
*/
export const commonDevices = {
// Desktop
desktopChrome: {
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
desktopFirefox: {
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
desktopSafari: {
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// Mobile
mobileChrome: {
name: 'mobile-chrome',
use: { ...devices['Pixel 5'] },
},
mobileSafari: {
name: 'mobile-safari',
use: { ...devices['iPhone 13'] },
},
// Tablet
ipadPro: {
name: 'ipad-pro',
use: { ...devices['iPad Pro'] },
},
// OBS Overlay (1920x1080 for OBS Browser Source)
obsOverlay: {
name: 'obs-overlay',
use: {
...devices['Desktop Chrome'],
viewport: { width: 1920, height: 1080 },
hasTouch: false,
reducedMotion: 'no-preference',
},
},
}