Files
app/plans/01-playwright-scaffolding.md
Albert b96159ec02 docs: Add comprehensive implementation plans for all todo items
Created detailed markdown plans for all items in todo.md:

1. 01-playwright-scaffolding.md - Base Playwright infrastructure
2. 02-magnitude-tests-comprehensive.md - Complete test coverage
3. 03-stream-ai-to-deepgram-tts.md - TTS latency optimization
4. 04-fix-galaxy-node-clicking.md - Galaxy navigation bugs
5. 05-dark-light-mode-theme.md - Dark/light mode with dynamic favicons
6. 06-fix-double-border-desktop.md - UI polish
7. 07-delete-backup-files.md - Code cleanup
8. 08-ai-transition-to-edit.md - Intelligent node creation flow
9. 09-umap-minimum-nodes-analysis.md - Technical analysis

Each plan includes:
- Detailed problem analysis
- Proposed solutions with code examples
- Manual Playwright MCP testing strategy
- Magnitude test specifications
- Implementation steps
- Success criteria

Ready to implement in sequence.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-09 21:07:42 +00:00

10 KiB

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

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

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

import { test as base, Page } from '@playwright/test';

type Fixtures = {
  authenticatedPage: Page;
  chatPage: Page;
  galaxyPage: Page;
};

export const test = base.extend<Fixtures>({
  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

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

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

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

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)

# 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

    mkdir -p tests/playwright/.auth
    mkdir -p tests/playwright/helpers
    
  2. Install dependencies

    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

    {
      "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:

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)