From 1ff9a2cf4b7b42d4c62b78aaab2eca4b75dd607f Mon Sep 17 00:00:00 2001 From: Albert Date: Mon, 10 Nov 2025 13:51:09 +0000 Subject: [PATCH] feat: Add comprehensive testing infrastructure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements robust testing setup with Playwright global auth, reusable test helpers, Docker support, and CI/CD integration with Gitea Actions. ## Changes ### Playwright Setup - Add global auth setup with storage state reuse (tests/playwright/auth.setup.ts) - Fix auth setup to clear existing state before fresh login - Create reusable performOAuthLogin helper (tests/playwright/helpers.ts) - Configure dotenv loading for environment variables in playwright.config.ts ### Magnitude Configuration - Update to use Claude Sonnet 4.5 (claude-sonnet-4-5-20250514) - Create reusable loginFlow helper (tests/magnitude/helpers.ts) - Fix smoke test to check login page instead of non-existent homepage ### Docker Support - Add Dockerfile.playwright with non-root user (pwuser) for security - Uses official Playwright Docker image (mcr.microsoft.com/playwright:v1.49.1-noble) - Provides consistent testing environment across users and CI/CD ### CI/CD Integration - Add Gitea Actions workflow (.gitea/workflows/magnitude.yml) - Runs Magnitude tests on every push and PR - Starts SurrealDB and Next.js dev server automatically - Uploads test results as artifacts (30-day retention) ### Documentation - Add comprehensive testing setup docs to AGENTS.md: - Playwright Docker setup instructions - CI/CD with Gitea Actions - Testing framework separation (Playwright vs Magnitude) - Required secrets for CI/CD ### Testing Best Practices - Separate Playwright (manual + global auth) from Magnitude (automated E2E) - Reusable helpers reduce code duplication - Both frameworks work independently ## Testing - ✅ Playwright auth setup test passes (5.6s) - ✅ Magnitude smoke test passes - ✅ OAuth flow works correctly with helper function 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .gitea/workflows/magnitude.yml | 75 +++++++++++++++++++++++++++++++ AGENTS.md | 72 +++++++++++++++++++++++++++++- Dockerfile.playwright | 30 +++++++++++++ magnitude.config.ts | 2 + playwright.config.ts | 4 ++ tests/magnitude/01-smoke.mag.ts | 13 +++--- tests/magnitude/helpers.ts | 59 +++++++++++++++++++++++++ tests/playwright/auth.setup.ts | 27 +++++++++--- tests/playwright/helpers.ts | 78 +++++++++++++++++++++++++++++++++ 9 files changed, 347 insertions(+), 13 deletions(-) create mode 100644 .gitea/workflows/magnitude.yml create mode 100644 Dockerfile.playwright create mode 100644 tests/magnitude/helpers.ts create mode 100644 tests/playwright/helpers.ts diff --git a/.gitea/workflows/magnitude.yml b/.gitea/workflows/magnitude.yml new file mode 100644 index 0000000..83e0d76 --- /dev/null +++ b/.gitea/workflows/magnitude.yml @@ -0,0 +1,75 @@ +# Gitea Actions workflow for running Magnitude tests +name: Magnitude Tests + +on: + push: + branches: [main, development] + pull_request: + branches: [main, development] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install pnpm + run: npm install -g pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Start SurrealDB + run: | + docker run -d \ + --name surrealdb \ + -p 8000:8000 \ + -e SURREAL_USER=${{ secrets.SURREALDB_USER }} \ + -e SURREAL_PASS=${{ secrets.SURREALDB_PASS }} \ + surrealdb/surrealdb:latest \ + start --log trace --user ${{ secrets.SURREALDB_USER }} --pass ${{ secrets.SURREALDB_PASS }} memory + + - name: Wait for SurrealDB + run: sleep 5 + + - name: Start Next.js dev server + run: pnpm dev & + env: + SURREALDB_URL: ws://localhost:8000/rpc + SURREALDB_USER: ${{ secrets.SURREALDB_USER }} + SURREALDB_PASS: ${{ secrets.SURREALDB_PASS }} + SURREALDB_NS: ${{ secrets.SURREALDB_NS }} + SURREALDB_DB: ${{ secrets.SURREALDB_DB }} + ATPROTO_CLIENT_ID: ${{ secrets.ATPROTO_CLIENT_ID }} + ATPROTO_REDIRECT_URI: ${{ secrets.ATPROTO_REDIRECT_URI }} + GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} + DEEPGRAM_API_KEY: ${{ secrets.DEEPGRAM_API_KEY }} + SURREAL_JWT_SECRET: ${{ secrets.SURREAL_JWT_SECRET }} + TEST_BLUESKY_HANDLE: ${{ secrets.TEST_BLUESKY_HANDLE }} + TEST_BLUESKY_PASSWORD: ${{ secrets.TEST_BLUESKY_PASSWORD }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + + - name: Wait for Next.js server + run: npx wait-on http://localhost:3000 --timeout 120000 + + - name: Run Magnitude tests + run: npx magnitude + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + TEST_BLUESKY_HANDLE: ${{ secrets.TEST_BLUESKY_HANDLE }} + TEST_BLUESKY_PASSWORD: ${{ secrets.TEST_BLUESKY_PASSWORD }} + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: magnitude-results + path: test-results/ + retention-days: 30 diff --git a/AGENTS.md b/AGENTS.md index 7c4f626..b0cbba4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -102,9 +102,12 @@ These credentials should be used for all automated testing (Magnitude, Playwrigh - ✅ All manual testing with Playwright MCP completed and verified - ✅ All Magnitude tests written and cover all verified functionality - ✅ Database verified for expected state after operations (e.g., deletions actually removed records) + - ✅ Run magnitude tests for current feature FIRST: `pnpm test tests/magnitude/your-feature.mag.ts` + - ✅ Verify current feature tests pass - ✅ Run ALL magnitude tests: `pnpm test` - - ✅ All tests passing + - ✅ Verify ENTIRE test suite passes - ✅ No console errors or warnings in production code paths + - **CRITICAL**: Do NOT commit until ALL tests pass - feature tests AND full test suite - Only commit after ALL checklist items are complete 7. **Documentation**: @@ -114,9 +117,76 @@ These credentials should be used for all automated testing (Magnitude, Playwrigh **Testing Resources**: - Playwright Global Setup/Teardown: https://playwright.dev/docs/test-global-setup-teardown - Playwright Test Agents: https://playwright.dev/docs/test-agents +- Playwright Docker: https://playwright.dev/docs/docker - Magnitude.run Documentation: https://magnitude.run/docs - Project Test README: `tests/README.md` +**Playwright Docker Setup**: + +For consistent testing environments across users and CI/CD, use the Playwright Docker image: + +1. **Build the Playwright Docker image**: + ```bash + docker build -f Dockerfile.playwright -t ponderants-playwright . + ``` + +2. **Run Playwright tests in Docker**: + ```bash + docker run --rm \ + --network=host \ + -v $(pwd):/home/pwuser/app \ + -e TEST_BLUESKY_HANDLE \ + -e TEST_BLUESKY_PASSWORD \ + ponderants-playwright + ``` + +3. **Benefits**: + - Non-root user execution (pwuser) for security + - Consistent browser versions across environments + - Isolated test environment + - Ready for CI/CD integration + +**CI/CD with Gitea Actions**: + +Magnitude tests run automatically on every push and pull request via Gitea Actions: + +1. **Configuration**: `.gitea/workflows/magnitude.yml` + +2. **Workflow steps**: + - Checkout code + - Setup Node.js and pnpm + - Start SurrealDB in Docker + - Start Next.js dev server with environment variables + - Run Magnitude tests + - Upload test results as artifacts + +3. **Required Secrets** (configure in Gitea repository settings): + - `ANTHROPIC_API_KEY` - For Magnitude AI vision testing + - `TEST_BLUESKY_HANDLE` - Test account handle + - `TEST_BLUESKY_PASSWORD` - Test account password + - `SURREALDB_USER`, `SURREALDB_PASS`, `SURREALDB_NS`, `SURREALDB_DB` + - `ATPROTO_CLIENT_ID`, `ATPROTO_REDIRECT_URI` + - `GOOGLE_API_KEY`, `DEEPGRAM_API_KEY` + - `SURREAL_JWT_SECRET` + +4. **Test results**: Available as workflow artifacts for 30 days + +**Testing Framework Separation**: + +- **Playwright**: Used for manual testing with Playwright MCP and global auth setup + - Location: `tests/playwright/` + - Helpers: `tests/playwright/helpers.ts` + - Auth setup: `tests/playwright/auth.setup.ts` + - Run: `npx playwright test` + +- **Magnitude**: Used for automated end-to-end testing in development and CI/CD + - Location: `tests/magnitude/` + - Helpers: `tests/magnitude/helpers.ts` + - Configuration: `magnitude.config.ts` (uses Claude Sonnet 4.5) + - Run: `npx magnitude` or `pnpm test` + +Both frameworks are independent and can be used separately or together depending on the testing need. + You are an expert-level, full-stack AI coding agent. Your task is to implement the "Ponderants" application. Product Vision: Ponderants is an AI-powered thought partner that interviews a user to capture, structure, and visualize diff --git a/Dockerfile.playwright b/Dockerfile.playwright new file mode 100644 index 0000000..35e1290 --- /dev/null +++ b/Dockerfile.playwright @@ -0,0 +1,30 @@ +# Dockerfile for Playwright testing environment +# Based on official Playwright Docker image with non-root user setup + +FROM mcr.microsoft.com/playwright:v1.49.1-noble + +# Create a non-root user for running tests +RUN useradd -ms /bin/bash pwuser && \ + mkdir -p /home/pwuser/app && \ + chown -R pwuser:pwuser /home/pwuser + +# Switch to non-root user +USER pwuser + +# Set working directory +WORKDIR /home/pwuser/app + +# Copy package files +COPY --chown=pwuser:pwuser package.json pnpm-lock.yaml ./ + +# Install pnpm globally for the user +RUN npm install -g pnpm + +# Install dependencies +RUN pnpm install --frozen-lockfile + +# Copy the rest of the application +COPY --chown=pwuser:pwuser . . + +# Run Playwright tests +CMD ["pnpm", "exec", "playwright", "test"] diff --git a/magnitude.config.ts b/magnitude.config.ts index f1ccb4a..541d149 100644 --- a/magnitude.config.ts +++ b/magnitude.config.ts @@ -6,4 +6,6 @@ export default { tests: 'tests/magnitude/**/*.mag.ts', // Run tests in headless mode to avoid window focus issues headless: true, + // Use Claude Sonnet 4.5 for best performance + model: 'anthropic:claude-sonnet-4-5-20250514', }; diff --git a/playwright.config.ts b/playwright.config.ts index 8366eba..b9fddf8 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,4 +1,8 @@ import { defineConfig, devices } from '@playwright/test'; +import * as dotenv from 'dotenv'; + +// Load environment variables from .env file +dotenv.config(); export default defineConfig({ testDir: './tests/playwright', diff --git a/tests/magnitude/01-smoke.mag.ts b/tests/magnitude/01-smoke.mag.ts index b41ad01..2535618 100644 --- a/tests/magnitude/01-smoke.mag.ts +++ b/tests/magnitude/01-smoke.mag.ts @@ -1,11 +1,10 @@ import { test } from 'magnitude-test'; -test('Application boots and displays homepage', async (agent) => { - // Act: Navigate to the homepage (uses the default URL - // from magnitude.config.ts) - await agent.act('Navigate to the homepage'); +test('Application boots and displays login page', async (agent) => { + // Act: Navigate to the root URL (should redirect to /login) + await agent.act('Navigate to http://localhost:3000'); - // Check: Verify that the homepage text is visible - // This confirms the Next.js app is serving content. - await agent.check('The text "Ponderants" is visible on the screen'); + // Check: Verify the login page loads with expected elements + await agent.check('The text "Ponderants" or "Log in with Bluesky" is visible on the screen'); + await agent.check('A login form or button is displayed'); }); diff --git a/tests/magnitude/helpers.ts b/tests/magnitude/helpers.ts new file mode 100644 index 0000000..52d9005 --- /dev/null +++ b/tests/magnitude/helpers.ts @@ -0,0 +1,59 @@ +/** + * Reusable test helpers for Magnitude tests + * + * These helpers encapsulate common test patterns to reduce code duplication + * and make tests more maintainable. + */ + +const TEST_HANDLE = process.env.TEST_BLUESKY_HANDLE; +const TEST_PASSWORD = process.env.TEST_BLUESKY_PASSWORD; + +if (!TEST_HANDLE || !TEST_PASSWORD) { + throw new Error('TEST_BLUESKY_HANDLE and TEST_BLUESKY_PASSWORD must be set in .env'); +} + +/** + * Performs complete OAuth login flow + * + * This function navigates to the login page and completes the full OAuth flow: + * 1. Navigate to /login + * 2. Enter handle and click "Log in with Bluesky" + * 3. Wait for redirect to Bluesky OAuth page + * 4. Enter password and click "Sign in" + * 5. Click "Authorize" button + * 6. Wait for redirect to /chat + * + * @param agent - The Magnitude test agent + */ +export async function loginFlow(agent: any) { + // Navigate to login page + await agent.act('Navigate to /login'); + + // Fill in handle and initiate OAuth + await agent.act(`Type "${TEST_HANDLE}" into the "Your Handle" input field`); + await agent.act('Click the "Log in with Bluesky" button'); + + // Wait for redirect to Bluesky OAuth page + await agent.check('The page URL contains "bsky.social"'); + + // Fill in credentials on Bluesky OAuth page + await agent.act(`Type "${TEST_HANDLE}" into the username/identifier field`); + await agent.act(`Type "${TEST_PASSWORD}" into the password field`); + + // Submit login form + await agent.act('Click the submit/authorize button'); + + // Wait for and click authorize button + await agent.act('Click the "Authorize" button'); + + // Verify successful login + await agent.check('The page URL contains "/chat"'); +} + +/** + * Test credentials for use in tests that need them directly + */ +export const TEST_CREDENTIALS = { + handle: TEST_HANDLE, + password: TEST_PASSWORD, +} as const; diff --git a/tests/playwright/auth.setup.ts b/tests/playwright/auth.setup.ts index 57a50eb..3555364 100644 --- a/tests/playwright/auth.setup.ts +++ b/tests/playwright/auth.setup.ts @@ -1,12 +1,29 @@ -import { test as setup, expect } from '@playwright/test'; +import { test as setup } from '@playwright/test'; +import * as fs from 'fs'; +import * as path from 'path'; +import { performOAuthLogin } from './helpers'; 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'); + console.log('[Auth Setup] Starting OAuth authentication flow'); - // Save empty state for now + // Clear any existing auth state file to ensure fresh login + if (fs.existsSync(authFile)) { + fs.unlinkSync(authFile); + console.log('[Auth Setup] Cleared existing auth state'); + } + + // Perform OAuth login using reusable helper + await performOAuthLogin(page); + + // Ensure the auth directory exists + const authDir = path.dirname(authFile); + if (!fs.existsSync(authDir)) { + fs.mkdirSync(authDir, { recursive: true }); + } + + // Save authenticated state await page.context().storageState({ path: authFile }); + console.log(`[Auth Setup] Saved authentication state to ${authFile}`); }); diff --git a/tests/playwright/helpers.ts b/tests/playwright/helpers.ts new file mode 100644 index 0000000..4b8bf78 --- /dev/null +++ b/tests/playwright/helpers.ts @@ -0,0 +1,78 @@ +/** + * Reusable test helpers for Playwright tests + * + * These helpers encapsulate common test patterns to reduce code duplication + * and make tests more maintainable. + */ + +import { Page, expect } from '@playwright/test'; + +const TEST_HANDLE = process.env.TEST_BLUESKY_HANDLE; +const TEST_PASSWORD = process.env.TEST_BLUESKY_PASSWORD; + +if (!TEST_HANDLE || !TEST_PASSWORD) { + throw new Error( + 'TEST_BLUESKY_HANDLE and TEST_BLUESKY_PASSWORD must be set in .env file' + ); +} + +/** + * Performs complete OAuth login flow + * + * This function navigates to the login page and completes the full OAuth flow: + * 1. Navigate to /login + * 2. Enter handle and click "Log in with Bluesky" + * 3. Wait for redirect to Bluesky OAuth page + * 4. Enter password and click "Sign in" + * 5. Click "Authorize" button + * 6. Wait for redirect to /chat + * 7. Verify authentication successful + * + * @param page - The Playwright Page object + */ +export async function performOAuthLogin(page: Page) { + console.log('[Helper] Starting OAuth login flow'); + + // Navigate to login page + await page.goto('/login'); + + // Fill in handle and initiate OAuth + await page.getByLabel('Your Handle').fill(TEST_HANDLE!); + + // Click button and wait for navigation to Bluesky OAuth page + await Promise.all([ + page.waitForURL('**/bsky.social/**', { timeout: 30000 }), + page.getByRole('button', { name: 'Log in with Bluesky' }).click(), + ]); + console.log('[Helper] Redirected to Bluesky OAuth page'); + + // The identifier is pre-filled from our login flow, just fill in password + // Use getByRole to avoid strict mode violations with multiple "Password" labeled elements + await page.getByRole('textbox', { name: 'Password' }).fill(TEST_PASSWORD!); + + // Click Sign in button + await page.getByRole('button', { name: /sign in/i }).click(); + + // Wait for the OAuth authorization page by looking for the Authorize button + await page.getByRole('button', { name: 'Authorize' }).waitFor({ timeout: 10000 }); + console.log('[Helper] On OAuth authorization page'); + + // Click Authorize button to grant access and wait for redirect + await Promise.all([ + page.waitForURL('**/chat', { timeout: 20000 }), + page.getByRole('button', { name: 'Authorize' }).click(), + ]); + console.log('[Helper] Successfully authorized, redirected to /chat'); + + // Verify we're actually logged in by checking for Profile nav link + await expect(page.getByText('Profile')).toBeVisible({ timeout: 5000 }); + console.log('[Helper] Verified authentication successful'); +} + +/** + * Test credentials for use in tests that need them directly + */ +export const TEST_CREDENTIALS = { + handle: TEST_HANDLE, + password: TEST_PASSWORD, +} as const;