/** * Playwright Helper Functions * * Reusable test utilities for both Magnitude tests and Playwright MCP. * These helpers encapsulate common test flows to reduce duplication. */ import type { Page } from '@playwright/test'; export interface TestCredentials { username: string; password: string; } /** * Default test credentials for Bluesky OAuth login. * In production tests, these should come from environment variables. */ export const DEFAULT_TEST_CREDENTIALS: TestCredentials = { username: process.env.TEST_BLUESKY_USERNAME || 'test-user.bsky.social', password: process.env.TEST_BLUESKY_PASSWORD || 'test-password', }; /** * Navigate to the application home page and wait for it to load. */ export async function navigateToApp(page: Page, baseUrl: string = 'http://localhost:3000'): Promise { await page.goto(baseUrl); await page.waitForLoadState('networkidle'); } /** * Complete the Bluesky OAuth login flow. * * This function: * 1. Clicks the "Log in with Bluesky" button * 2. Waits for Bluesky OAuth page to load * 3. Fills in credentials * 4. Submits the form * 5. Waits for redirect back to the app * * @param page - Playwright page instance * @param credentials - Optional custom credentials (defaults to DEFAULT_TEST_CREDENTIALS) */ export async function loginWithBluesky( page: Page, credentials: TestCredentials = DEFAULT_TEST_CREDENTIALS ): Promise { // Click login button await page.click('text="Log in with Bluesky"'); // Wait for Bluesky OAuth page await page.waitForURL(/bsky\.social|bsky\.app/); // Fill in credentials await page.fill('input[name="identifier"]', credentials.username); await page.fill('input[name="password"]', credentials.password); // Submit login form await page.click('button[type="submit"]'); // Wait for redirect back to app await page.waitForURL(/localhost:3000/); await page.waitForLoadState('networkidle'); } /** * Start a new conversation by sending a message in the chat interface. * * @param page - Playwright page instance * @param message - The message to send */ export async function startConversation(page: Page, message: string): Promise { // Find the chat input (textarea or input field) const chatInput = page.locator('textarea, input[type="text"]').first(); // Type the message await chatInput.fill(message); // Submit (press Enter or click send button) await chatInput.press('Enter'); // Wait for AI response to appear await page.waitForSelector('text=/AI|Assistant|thinking/i', { timeout: 10000 }); } /** * Create a node draft from the current conversation. * * This assumes: * - User is already in a conversation * - The "Create Node" button is visible */ export async function createNodeDraft(page: Page): Promise { // Click "Create Node" button await page.click('text="Create Node"'); // Wait for navigation to edit page await page.waitForURL(/\/edit/); await page.waitForLoadState('networkidle'); // Verify we're on the edit page with draft content await page.waitForSelector('input[value]:not([value=""])', { timeout: 5000 }); } /** * Publish a node from the edit page. * * This assumes: * - User is already on the /edit page * - Draft content is loaded */ export async function publishNode(page: Page): Promise { // Click "Publish Node" button await page.click('text="Publish Node"'); // Wait for success notification await page.waitForSelector('text=/Node published|success/i', { timeout: 15000 }); // Wait for navigation back to conversation view await page.waitForURL(/\/chat|\/conversation/, { timeout: 5000 }); } /** * Complete end-to-end flow: Login → Conversation → Create Node → Publish * * This is the "happy path" test flow for the core user journey. * * @param page - Playwright page instance * @param message - Message to start conversation with * @param credentials - Optional custom credentials */ export async function completeNodePublishFlow( page: Page, message: string = 'Test conversation for node creation', credentials: TestCredentials = DEFAULT_TEST_CREDENTIALS ): Promise { await navigateToApp(page); await loginWithBluesky(page, credentials); await startConversation(page, message); await createNodeDraft(page); await publishNode(page); } /** * Edit node draft content before publishing. * * @param page - Playwright page instance * @param title - New title (undefined to skip) * @param content - New content (undefined to skip) */ export async function editNodeDraft( page: Page, title?: string, content?: string ): Promise { if (title !== undefined) { const titleInput = page.locator('input[label="Title"], input[placeholder*="title" i]').first(); await titleInput.fill(title); } if (content !== undefined) { const contentInput = page.locator('textarea[label="Content"], textarea[placeholder*="content" i]').first(); await contentInput.fill(content); } } /** * Wait for AI to finish responding in the chat. * * This looks for the typing indicator to disappear. */ export async function waitForAIResponse(page: Page, timeoutMs: number = 30000): Promise { // Wait for typing indicator to appear await page.waitForSelector('text=/thinking|typing|generating/i', { timeout: 5000, state: 'visible' }).catch(() => { // Indicator might appear and disappear quickly, that's okay }); // Wait for it to disappear await page.waitForSelector('text=/thinking|typing|generating/i', { timeout: timeoutMs, state: 'hidden' }).catch(() => { // Might already be gone, that's okay }); } /** * Logout from the application. */ export async function logout(page: Page): Promise { // Click user profile menu await page.click('[aria-label="User menu"], button:has-text("Profile")'); // Click logout button await page.click('text="Logout"'); // Wait for redirect to login page await page.waitForURL(/\/login|\/$/); } /** * Check if user is logged in by looking for authenticated UI elements. */ export async function isLoggedIn(page: Page): Promise { try { // Look for chat interface or user menu await page.waitForSelector('textarea, [aria-label="User menu"]', { timeout: 2000 }); return true; } catch { return false; } }