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>
408 lines
11 KiB
TypeScript
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',
|
|
},
|
|
},
|
|
}
|