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