diff --git a/.gitignore b/.gitignore index a249744..d9921ee 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ next-env.d.ts /playwright-report/ /playwright/.cache/ .playwright-mcp/ +tests/playwright/.auth/ # claude settings (keep .claude/CLAUDE.md but ignore user settings) .claude/settings.local.json diff --git a/package.json b/package.json index 7c0c8c3..b2ac5d8 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,9 @@ "start": "next start", "lint": "next lint", "test": "npx magnitude", + "test:playwright": "playwright test", + "test:playwright:ui": "playwright test --ui", + "test:playwright:debug": "playwright test --debug", "schema:apply": "node scripts/apply-schema.js", "schema:deploy": "./scripts/deploy-schema.sh" }, diff --git a/playwright.config.ts b/playwright.config.ts index 8d7080c..8366eba 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,40 +1,35 @@ import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ - testDir: './tests', + 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: 'http://localhost:3000', + baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000', trace: 'on-first-retry', + screenshot: 'only-on-failure', }, projects: [ - // Setup project - { - name: 'setup', - testMatch: /.*\.setup\.ts/, - }, - - // Chromium tests using authenticated state + { name: 'setup', testMatch: /.*\.setup\.ts/ }, { name: 'chromium', use: { ...devices['Desktop Chrome'], - // Use the saved authenticated state - storageState: '.playwright/.auth/user.json', + storageState: 'tests/playwright/.auth/user.json', }, dependencies: ['setup'], }, ], - // Run dev server before tests webServer: { command: 'pnpm dev', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI, + timeout: 120000, }, }); diff --git a/tests/playwright/auth.setup.ts b/tests/playwright/auth.setup.ts new file mode 100644 index 0000000..57a50eb --- /dev/null +++ b/tests/playwright/auth.setup.ts @@ -0,0 +1,12 @@ +import { test as setup, expect } from '@playwright/test'; + +const authFile = 'tests/playwright/.auth/user.json'; + +setup('authenticate', async ({ page }) => { + // For now, just create an empty auth file + // TODO: Implement actual OAuth flow when test credentials are available + console.log('[Auth Setup] Skipping authentication - implement OAuth flow with test credentials'); + + // Save empty state for now + await page.context().storageState({ path: authFile }); +}); diff --git a/tests/playwright/fixtures.ts b/tests/playwright/fixtures.ts new file mode 100644 index 0000000..a47369a --- /dev/null +++ b/tests/playwright/fixtures.ts @@ -0,0 +1,26 @@ +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) => { + 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'; diff --git a/tests/playwright/helpers/chat.ts b/tests/playwright/helpers/chat.ts new file mode 100644 index 0000000..e47c8b7 --- /dev/null +++ b/tests/playwright/helpers/chat.ts @@ -0,0 +1,35 @@ +import { Page, expect } from '@playwright/test'; + +export class ChatHelper { + constructor(private page: Page) {} + + async sendMessage(message: string) { + const input = this.page.locator('textarea[placeholder*="Type"], input[placeholder*="Type"]'); + await input.fill(message); + await input.press('Enter'); + } + + async waitForAIResponse() { + // Wait for typing indicator to appear then disappear + const typingIndicator = this.page.locator('[data-testid="typing-indicator"]').first(); + + try { + await typingIndicator.waitFor({ state: 'visible', timeout: 2000 }); + await typingIndicator.waitFor({ state: 'hidden', timeout: 30000 }); + } catch { + // Typing indicator might not appear for fast responses + await this.page.waitForTimeout(1000); + } + } + + async getLastMessage() { + const messages = this.page.locator('[data-testid="chat-message"]'); + const count = await messages.count(); + return count > 0 ? messages.nth(count - 1) : null; + } + + async getMessageCount() { + const messages = this.page.locator('[data-testid="chat-message"]'); + return await messages.count(); + } +} diff --git a/tests/playwright/helpers/galaxy.ts b/tests/playwright/helpers/galaxy.ts new file mode 100644 index 0000000..1d8fbb4 --- /dev/null +++ b/tests/playwright/helpers/galaxy.ts @@ -0,0 +1,28 @@ +import { Page } 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 }); + + // Give R3F time to render + await this.page.waitForTimeout(2000); + } + + async clickNode(nodeId: string) { + // Simulate node click via JavaScript event + 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 window globals set by galaxy component + return (window as any).__galaxyNodes?.length || 0; + }); + } +} diff --git a/tests/playwright/helpers/node.ts b/tests/playwright/helpers/node.ts new file mode 100644 index 0000000..0ff4a8a --- /dev/null +++ b/tests/playwright/helpers/node.ts @@ -0,0 +1,36 @@ +import { Page } from '@playwright/test'; + +export class NodeHelper { + constructor(private page: Page) {} + + async createNode(title: string, body: string) { + // This will vary based on actual implementation + // For now, placeholder that navigates to chat + await this.page.goto('/chat'); + + // TODO: Implement actual node creation flow + // This depends on how nodes are created in the UI + } + + async waitForUMAPCalculation(timeoutMs: number = 60000) { + // Poll /api/galaxy until nodes have coords_3d + const startTime = Date.now(); + + while (Date.now() - startTime < timeoutMs) { + try { + const response = await this.page.request.get('/api/galaxy'); + const data = await response.json(); + + if (data.nodes && data.nodes.every((n: any) => n.coords_3d !== null)) { + return true; + } + } catch { + // Continue polling + } + + await this.page.waitForTimeout(2000); + } + + return false; + } +} diff --git a/tests/playwright/smoke.spec.ts b/tests/playwright/smoke.spec.ts new file mode 100644 index 0000000..ec82b31 --- /dev/null +++ b/tests/playwright/smoke.spec.ts @@ -0,0 +1,21 @@ +import { test, expect } from './fixtures'; + +test.describe('Smoke Tests', () => { + test('homepage loads', async ({ page }) => { + await page.goto('/'); + await expect(page).toHaveTitle(/Ponderants/); + }); + + test('chat page loads', async ({ page }) => { + await page.goto('/chat'); + // Check for chat interface element + await expect(page.locator('textarea, input[type="text"]').first()).toBeVisible({ timeout: 10000 }); + }); + + test('galaxy page loads', async ({ page }) => { + await page.goto('/galaxy'); + // Check for canvas or empty state + const canvasOrEmpty = page.locator('canvas, [data-testid="empty-state"]').first(); + await expect(canvasOrEmpty).toBeVisible({ timeout: 10000 }); + }); +}); diff --git a/todo.md b/todo.md index dadbb11..941c087 100644 --- a/todo.md +++ b/todo.md @@ -2,6 +2,9 @@ Upcoming items that should be implemented (time-permitting): +- add base playwright scaffolding files to improve the efficiency of manual + playwright mcp testing as well as that of magnitude +- ADD MAGNITUDE TESTS FOR EVERYTHING, both existing and new additions - stream the AI output to deepgram for faster synthesis - fix the freaking galaxy node clicking -- when going directly to a node ID link, it redirects to /chat; when clicking on a node in /galaxy (either @@ -9,3 +12,5 @@ Upcoming items that should be implemented (time-permitting): - dark mode/light mode favicon and overall app theme - fix the double border on desktop between sidebar and conversation actions UI - delete "backup"/"old" page.tsx files +- allow ai to transition to edit in chat +- why wait for three nodes before umap?