feat: Add comprehensive testing infrastructure

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 <noreply@anthropic.com>
This commit is contained in:
2025-11-10 13:51:09 +00:00
parent a520814771
commit 1ff9a2cf4b
9 changed files with 347 additions and 13 deletions

View File

@@ -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

View File

@@ -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

30
Dockerfile.playwright Normal file
View File

@@ -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"]

View File

@@ -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',
};

View File

@@ -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',

View File

@@ -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');
});

View File

@@ -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;

View File

@@ -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}`);
});

View File

@@ -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;