initial commit

This commit is contained in:
Madison Grubb
2026-02-10 23:32:26 -05:00
commit b7046dc0e6
133 changed files with 26080 additions and 0 deletions

38
test/e2e/utils/auth.js Normal file
View File

@@ -0,0 +1,38 @@
/**
* Authentication utilities for E2E tests.
*/
/**
* Login as admin user via API and then navigate so the session cookie is in context.
* More reliable than form submit + redirect for E2E.
* @param {import('@playwright/test').Page} page
* @param {string} identifier - Username (default test-admin)
* @param {string} password - Password (default test-admin-password)
* @returns {Promise<void>}
*/
export async function loginAsAdmin(page, identifier = 'test-admin', password = 'test-admin-password') {
// Login via API so the session cookie is set in the browser context
const response = await page.request.post('/api/auth/login', {
data: { identifier, password },
})
if (!response.ok()) {
const body = await response.body().catch(() => Buffer.from(''))
throw new Error(`Login API failed ${response.status()}: ${body.toString()}`)
}
// Navigate to home so the app sees the session
await page.goto('/')
await page.waitForLoadState('domcontentloaded')
}
/**
* Get session cookie from page context.
* @param {import('@playwright/test').BrowserContext} context
* @returns {Promise<string | null>} Session ID cookie value
*/
export async function getSessionCookie(context) {
const cookies = await context.cookies()
const sessionCookie = cookies.find(c => c.name === 'session_id')
return sessionCookie?.value || null
}

69
test/e2e/utils/server.js Normal file
View File

@@ -0,0 +1,69 @@
/**
* Server management utilities for E2E tests.
*/
import { existsSync, mkdirSync } from 'node:fs'
import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { execSync } from 'node:child_process'
const _dirname = dirname(fileURLToPath(import.meta.url))
const projectRoot = join(_dirname, '../../..')
const devCertsDir = join(projectRoot, '.dev-certs')
const devKey = join(devCertsDir, 'key.pem')
const devCert = join(devCertsDir, 'cert.pem')
/**
* Ensure .dev-certs directory exists and contains certificates.
* Generates certs if missing (same logic as scripts/gen-dev-cert.sh).
*/
export function ensureDevCerts() {
if (existsSync(devKey) && existsSync(devCert)) {
return // Certs already exist
}
// Create .dev-certs directory
mkdirSync(devCertsDir, { recursive: true })
// Generate self-signed cert for localhost/127.0.0.1
const SAN = 'subjectAltName=IP:127.0.0.1,DNS:localhost'
try {
execSync(
`openssl req -x509 -newkey rsa:2048 -keyout "${devKey}" -out "${devCert}" -days 365 -nodes -subj "/CN=localhost" -addext "${SAN}"`,
{ cwd: projectRoot, stdio: 'inherit' },
)
console.log('[test] Generated .dev-certs/key.pem and .dev-certs/cert.pem')
}
catch (error) {
throw new Error(`Failed to generate dev certificates: ${error.message}`)
}
}
/**
* Wait for server to be ready by polling the health endpoint.
* @param {string} baseURL
* @param {number} timeoutMs
* @returns {Promise<void>}
*/
export async function waitForServerReady(baseURL = 'https://localhost:3000', timeoutMs = 120000) {
const startTime = Date.now()
const checkInterval = 1000 // Check every second
while (Date.now() - startTime < timeoutMs) {
try {
const response = await fetch(`${baseURL}/health`, {
method: 'GET',
// @ts-ignore - ignoreHTTPSErrors is handled by Playwright context
})
if (response.ok) {
return // Server is ready
}
}
catch {
// Server not ready yet, continue polling
}
await new Promise(resolve => setTimeout(resolve, checkInterval))
}
throw new Error(`Server did not become ready within ${timeoutMs}ms`)
}

106
test/e2e/utils/webrtc.js Normal file
View File

@@ -0,0 +1,106 @@
/**
* WebRTC test utilities for E2E tests.
*/
/**
* Wait for video element to start playing.
* @param {import('@playwright/test').Page} page
* @param {string} selector - CSS selector for video element
* @param {number} timeoutMs - Timeout in milliseconds
* @returns {Promise<void>}
*/
export async function waitForVideoPlaying(page, selector = 'video', timeoutMs = 30000) {
await page.waitForSelector(selector, { timeout: timeoutMs })
// Wait for video to have metadata loaded and valid dimensions
await page.waitForFunction(
(sel) => {
const video = document.querySelector(sel)
if (!video) return false
// readyState >= 2 means HAVE_CURRENT_DATA (has metadata)
// videoWidth > 0 means video has actual dimensions (not black screen)
return video.readyState >= 2 && video.videoWidth > 0 && video.videoHeight > 0
},
selector,
{ timeout: timeoutMs },
)
}
/**
* Wait for session to appear in the active sessions list with hasStream: true.
* @param {import('@playwright/test').Page} page
* @param {string} sessionId - Session ID to wait for
* @param {number} timeoutMs - Timeout in milliseconds
* @returns {Promise<void>}
*/
export async function waitForSessionToAppear(page, sessionId, timeoutMs = 30000) {
const startTime = Date.now()
const pollInterval = 1000 // Poll every second
while (Date.now() - startTime < timeoutMs) {
try {
// Call /api/live endpoint to check for session
const response = await page.request.get('/api/live')
if (response.ok()) {
const sessions = await response.json()
const session = sessions.find(s => s.id === sessionId && s.hasStream === true)
if (session) {
return // Session found with stream
}
}
}
catch {
// API call failed, continue polling
}
await new Promise(resolve => setTimeout(resolve, pollInterval))
}
throw new Error(`Session ${sessionId} did not appear with hasStream: true within ${timeoutMs}ms`)
}
/**
* Get session ID from share-live page state.
* @param {import('@playwright/test').Page} page
* @returns {Promise<string | null>} Session ID or null if not found
*/
export async function getSessionIdFromPage(page) {
try {
// Brief delay so producer is registered on server
await new Promise(resolve => setTimeout(resolve, 1500))
// Use API to get active sessions (page has auth cookies)
const response = await page.request.get('/api/live')
if (response.ok()) {
const sessions = await response.json()
const withStream = sessions.filter(s => s.hasStream === true)
// Most recently updated session with stream is ours
const activeSession = withStream.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))[0]
return activeSession?.id || null
}
}
catch (error) {
console.error('[test] Failed to get session ID:', error)
}
return null
}
/**
* Verify video is playing (not paused, has dimensions).
* @param {import('@playwright/test').Page} page
* @param {string} selector - CSS selector for video element
* @returns {Promise<{ width: number, height: number, playing: boolean }>} Video dimensions and playing state.
*/
export async function getVideoState(page, selector = 'video') {
return await page.evaluate((sel) => {
const video = document.querySelector(sel)
if (!video) {
return { width: 0, height: 0, playing: false }
}
return {
width: video.videoWidth,
height: video.videoHeight,
playing: !video.paused && video.readyState >= 2,
}
}, selector)
}