# Plan: Add Playwright Scaffolding Files **Priority:** CRITICAL - Must be done first **Dependencies:** None **Affects:** All future testing work ## Overview Create base Playwright scaffolding files to improve efficiency of both manual Playwright MCP testing and Magnitude test automation. This infrastructure will make it easier to write, run, and maintain browser-based tests. ## Current State - No Playwright configuration file exists - No test helpers or utilities for common operations - No fixtures for authenticated states - Manual testing with Playwright MCP is inefficient - Magnitude tests will lack reusable components ## Proposed Solution Create a complete Playwright testing infrastructure with: ### 1. Core Configuration Files #### `playwright.config.ts` ```typescript import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './tests/playwright', fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, reporter: 'html', use: { baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000', trace: 'on-first-retry', screenshot: 'only-on-failure', }, projects: [ // Setup authentication { name: 'setup', testMatch: /.*\.setup\.ts/ }, // Desktop browsers { name: 'chromium', use: { ...devices['Desktop Chrome'], storageState: 'tests/playwright/.auth/user.json', }, dependencies: ['setup'], }, { name: 'firefox', use: { ...devices['Desktop Firefox'], storageState: 'tests/playwright/.auth/user.json', }, dependencies: ['setup'], }, // Mobile browsers { name: 'mobile-chrome', use: { ...devices['Pixel 5'], storageState: 'tests/playwright/.auth/user.json', }, dependencies: ['setup'], }, ], webServer: { command: 'pnpm dev', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI, }, }); ``` #### `.gitignore` updates ``` # Playwright tests/playwright/.auth/ test-results/ playwright-report/ playwright/.cache/ ``` ### 2. Authentication Setup #### `tests/playwright/auth.setup.ts` ```typescript import { test as setup, expect } from '@playwright/test'; const authFile = 'tests/playwright/.auth/user.json'; setup('authenticate', async ({ page }) => { // Check if auth already exists and is valid const authCookie = process.env.PLAYWRIGHT_AUTH_COOKIE; if (authCookie) { // Use existing auth from environment await page.context().addCookies([{ name: 'ponderants-auth', value: authCookie, domain: 'localhost', path: '/', }]); } else { // Perform OAuth login await page.goto('/'); await page.click('text=Log in with Bluesky'); // Wait for OAuth redirect await page.waitForURL(/bsky\.social/); // Fill in credentials await page.fill('input[name="identifier"]', process.env.TEST_USER_HANDLE!); await page.fill('input[name="password"]', process.env.TEST_USER_PASSWORD!); await page.click('button[type="submit"]'); // Wait for redirect back to app await page.waitForURL(/localhost:3000/); // Verify we're authenticated await expect(page.getByRole('button', { name: /profile/i })).toBeVisible(); } // Save signed-in state await page.context().storageState({ path: authFile }); }); ``` ### 3. Test Fixtures and Helpers #### `tests/playwright/fixtures.ts` ```typescript import { test as base, Page } from '@playwright/test'; type Fixtures = { authenticatedPage: Page; chatPage: Page; galaxyPage: Page; }; export const test = base.extend({ authenticatedPage: async ({ page }, use) => { // Already authenticated via setup await page.goto('/'); await use(page); }, chatPage: async ({ page }, use) => { await page.goto('/chat'); await use(page); }, galaxyPage: async ({ page }, use) => { await page.goto('/galaxy'); await use(page); }, }); export { expect } from '@playwright/test'; ``` #### `tests/playwright/helpers/chat.ts` ```typescript import { Page, expect } from '@playwright/test'; export class ChatHelper { constructor(private page: Page) {} async sendMessage(message: string) { await this.page.fill('textarea[placeholder*="Type"]', message); await this.page.press('textarea[placeholder*="Type"]', 'Enter'); } async waitForAIResponse() { // Wait for typing indicator to disappear await this.page.waitForSelector('[data-testid="typing-indicator"]', { state: 'hidden', timeout: 30000, }); } async createNode() { await this.page.click('button:has-text("Create Node")'); } async getLastMessage() { const messages = this.page.locator('[data-testid="chat-message"]'); const count = await messages.count(); return messages.nth(count - 1); } } ``` #### `tests/playwright/helpers/galaxy.ts` ```typescript import { Page, expect } from '@playwright/test'; export class GalaxyHelper { constructor(private page: Page) {} async waitForGalaxyLoad() { // Wait for canvas to be visible await this.page.waitForSelector('canvas', { timeout: 10000 }); // Wait for nodes to be loaded (check for at least one node) await this.page.waitForFunction(() => { const canvas = document.querySelector('canvas'); return canvas && canvas.getContext('2d') !== null; }); } async clickNode(nodeId: string) { // Find and click on a specific node await this.page.evaluate((id) => { const event = new CustomEvent('node-click', { detail: { nodeId: id } }); window.dispatchEvent(event); }, nodeId); } async getNodeCount() { return await this.page.evaluate(() => { // Access R3F scene data return window.__galaxyNodes?.length || 0; }); } } ``` #### `tests/playwright/helpers/node.ts` ```typescript import { Page, expect } from '@playwright/test'; export class NodeHelper { constructor(private page: Page) {} async createNode(title: string, body: string) { // Navigate to chat await this.page.goto('/chat'); // Trigger node creation await this.page.fill('input[placeholder*="Title"]', title); await this.page.fill('textarea[placeholder*="Body"]', body); await this.page.click('button:has-text("Publish")'); // Wait for success await expect(this.page.getByText(/published/i)).toBeVisible(); } async waitForUMAPCalculation() { // Poll /api/galaxy until nodes have coords_3d await this.page.waitForFunction(async () => { const response = await fetch('/api/galaxy'); const data = await response.json(); return data.nodes.every((n: any) => n.coords_3d !== null); }, { timeout: 60000 }); } } ``` ### 4. Example Tests #### `tests/playwright/smoke.spec.ts` ```typescript import { test, expect } from './fixtures'; test.describe('Smoke Tests', () => { test('homepage loads', async ({ page }) => { await page.goto('/'); await expect(page).toHaveTitle(/Ponderants/); }); test('authenticated user can access chat', async ({ chatPage }) => { await expect(chatPage.getByRole('textbox')).toBeVisible(); }); test('authenticated user can access galaxy', async ({ galaxyPage }) => { await expect(galaxyPage.locator('canvas')).toBeVisible(); }); }); ``` ### 5. Environment Variables #### `.env.test` (template) ```bash # Test user credentials TEST_USER_HANDLE=your-test-handle.bsky.social TEST_USER_PASSWORD=your-test-password # Optional: Pre-authenticated cookie for faster tests PLAYWRIGHT_AUTH_COOKIE= # Playwright settings PLAYWRIGHT_BASE_URL=http://localhost:3000 ``` ## Implementation Steps 1. **Create directory structure** ```bash mkdir -p tests/playwright/.auth mkdir -p tests/playwright/helpers ``` 2. **Install dependencies** ```bash pnpm add -D @playwright/test ``` 3. **Create configuration files** - playwright.config.ts - .gitignore updates - .env.test template 4. **Create authentication setup** - auth.setup.ts 5. **Create fixtures and helpers** - fixtures.ts - helpers/chat.ts - helpers/galaxy.ts - helpers/node.ts 6. **Create example tests** - smoke.spec.ts 7. **Update package.json scripts** ```json { "scripts": { "test:playwright": "playwright test", "test:playwright:ui": "playwright test --ui", "test:playwright:debug": "playwright test --debug" } } ``` ## Testing Plan ### Manual Playwright MCP Testing 1. Open Playwright MCP browser 2. Navigate to http://localhost:3000 3. Test authentication flow 4. Test chat interaction 5. Test node creation 6. Test galaxy visualization 7. Test node detail modal ### Magnitude Test Integration After manual testing succeeds, create magnitude tests that use the same patterns: ```typescript import { test } from 'magnitude-test'; test('User can create and view nodes in galaxy', async (agent) => { await agent.open('http://localhost:3000'); await agent.act('Log in with test credentials'); await agent.check('User is authenticated'); await agent.act('Navigate to chat'); await agent.act('Create a new node with title "Test Node" and body "Test content"'); await agent.check('Node is published to Bluesky'); await agent.act('Navigate to galaxy'); await agent.check('Can see the new node in 3D visualization'); }); ``` ## Success Criteria - ✅ Playwright configuration file exists - ✅ Authentication setup works (both OAuth and cookie-based) - ✅ Helper classes for chat, galaxy, and node operations exist - ✅ Example smoke tests pass - ✅ Can run `pnpm test:playwright` successfully - ✅ Manual Playwright MCP testing is efficient with helpers - ✅ Foundation exists for Magnitude test creation ## Files to Create 1. `playwright.config.ts` - Main configuration 2. `tests/playwright/auth.setup.ts` - Authentication setup 3. `tests/playwright/fixtures.ts` - Custom fixtures 4. `tests/playwright/helpers/chat.ts` - Chat helper functions 5. `tests/playwright/helpers/galaxy.ts` - Galaxy helper functions 6. `tests/playwright/helpers/node.ts` - Node helper functions 7. `tests/playwright/smoke.spec.ts` - Example smoke tests 8. `.env.test` - Environment variables template ## Files to Update 1. `.gitignore` - Add Playwright artifacts 2. `package.json` - Add test scripts 3. `README.md` - Document testing approach (optional)