| name | arcanea-playwright-testing |
| description | Playwright E2E testing for the Arcanea web app. Use when writing, running, or debugging Playwright tests, automating browser interactions on the Arcanea platform, capturing screenshots for QA, testing UI flows (academy gate quizzes, lore pages, prompt books, auth), or setting up the test infrastructure. Triggers on: Playwright, E2E test, end-to-end, browser test, UI test, automation, screenshot, test the page. Sourced from Anthropic's official webapp-testing skill and adapted for Arcanea's Next.js 16 stack. |
Arcanea Playwright Testing
"Alera guards the Voice Gate at 528 Hz — Truth, expression. Tests are truth-tellers. They reveal what is real, not what we hope."
Quick Start
Decision Tree
Test need → Is it a pure unit (function/hook)?
├─ Yes → Jest in packages/
└─ No → Playwright E2E
Playwright task → Is the dev server running?
├─ No → Use with_server.py helper OR next dev directly
└─ Yes → Reconnaissance-then-action pattern
Run Arcanea Dev Server for Tests
pnpm --filter web dev
Setup
playwright.config.ts (apps/web)
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [['html', { outputFolder: 'playwright-report' }]],
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
webServer: {
command: 'pnpm --filter web dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120000,
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'Mobile Safari', use: { ...devices['iPhone 14'] } },
],
})
Reconnaissance-Then-Action Pattern
Always inspect before acting on dynamic Next.js pages:
import { test, expect } from '@playwright/test'
test('lore page loads', async ({ page }) => {
await page.goto('/lore')
await page.waitForLoadState('networkidle')
await page.screenshot({ path: 'tests/screenshots/lore-debug.png', fullPage: true })
const guardianCards = page.locator('[data-testid="guardian-card"]')
const count = await guardianCards.count()
console.log(`Found ${count} guardian cards`)
expect(count).toBeGreaterThanOrEqual(1)
})
Arcanea Test Patterns
Academy Gate Quiz Flow
test('gate quiz — foundation gate completion', async ({ page }) => {
await page.goto('/auth/sign-in')
await page.waitForLoadState('networkidle')
await page.fill('[data-testid="email-input"]', process.env.TEST_USER_EMAIL!)
await page.fill('[data-testid="password-input"]', process.env.TEST_USER_PASSWORD!)
await page.click('[data-testid="sign-in-btn"]')
await page.waitForURL('/academy')
await page.goto('/academy/gate-quiz?gate=foundation')
await page.waitForLoadState('networkidle')
for (let i = 0; i < 10; i++) {
const question = page.locator('[data-testid="quiz-question"]')
await expect(question).toBeVisible()
await page.locator('[data-testid="answer-option"]').first().click()
await page.locator('[data-testid="next-question-btn"]').click()
}
await expect(page.locator('[data-testid="gate-complete"]')).toBeVisible()
await expect(page.locator('[data-testid="guardian-name"]')).toContainText('Lyssandria')
})
Lore Navigation Test
test('lore explore — elements navigation', async ({ page }) => {
await page.goto('/lore')
await page.waitForLoadState('networkidle')
const elements = ['fire', 'water', 'earth', 'wind', 'void']
for (const element of elements) {
await page.goto(`/lore/elements/${element}`)
await page.waitForLoadState('networkidle')
const title = page.locator('h1, [data-testid="element-title"]')
await expect(title).toBeVisible()
await expect(title).toContainText(element, { ignoreCase: true })
}
})
Prompt Books CRUD Flow
test('prompt books — create and view prompt', async ({ page }) => {
await authenticateTestUser(page)
await page.goto('/prompt-books')
await page.waitForLoadState('networkidle')
await page.click('[data-testid="new-prompt-btn"]')
await page.waitForLoadState('networkidle')
await page.fill('[data-testid="prompt-title"]', 'Test Guardian Invocation')
await page.locator('[data-testid="prompt-editor"]').click()
await page.keyboard.type('Channel the essence of {element} through {gate}')
await page.click('[data-testid="save-prompt-btn"]')
await expect(page.locator('[data-testid="save-success"]')).toBeVisible()
await page.goto('/prompt-books')
await expect(page.locator('text=Test Guardian Invocation')).toBeVisible()
})
Visual Regression — Glass Design System
test('guardian card visual snapshot', async ({ page }) => {
await page.goto('/lore/guardians')
await page.waitForLoadState('networkidle')
await page.waitForTimeout(500)
const grid = page.locator('[data-testid="guardian-grid"]')
await expect(grid).toHaveScreenshot('guardian-grid.png', {
maxDiffPixelRatio: 0.02,
})
})
Auth Helper
import { type Page } from '@playwright/test'
export async function authenticateTestUser(page: Page) {
await page.goto('/auth/sign-in')
await page.waitForLoadState('networkidle')
await page.fill('[name="email"]', process.env.TEST_USER_EMAIL!)
await page.fill('[name="password"]', process.env.TEST_USER_PASSWORD!)
await page.click('[type="submit"]')
await page.waitForURL(/\/(academy|dashboard|lore)/)
}
Save auth state once
import { test as setup } from '@playwright/test'
import { authenticateTestUser } from './helpers/auth'
const authFile = 'playwright/.auth/user.json'
setup('authenticate', async ({ page }) => {
await authenticateTestUser(page)
await page.context().storageState({ path: authFile })
})
Common Patterns
Wait for Next.js hydration
await page.goto('/path')
await page.waitForLoadState('networkidle')
Capture screenshots for debugging
await page.screenshot({ path: `/tmp/debug-${Date.now()}.png`, fullPage: true })
Test data attributes — add to Arcanea components
<button data-testid="unlock-gate-btn" onClick={handleUnlock}>
Unlock Gate
</button>
await page.click('[data-testid="unlock-gate-btn"]')
Intercept API calls
await page.route('/api/guardians', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(MOCK_GUARDIANS),
})
})
Running Tests
cd apps/web
npx playwright test
npx playwright test tests/e2e/lore.spec.ts
npx playwright test --ui
npx playwright test --debug tests/e2e/gate-quiz.spec.ts
npx playwright test --update-snapshots
npx playwright show-report
Quick Checklist
Before shipping any Playwright test in Arcanea: