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:
@@ -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');
|
||||
});
|
||||
|
||||
59
tests/magnitude/helpers.ts
Normal file
59
tests/magnitude/helpers.ts
Normal 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;
|
||||
@@ -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}`);
|
||||
});
|
||||
|
||||
78
tests/playwright/helpers.ts
Normal file
78
tests/playwright/helpers.ts
Normal 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;
|
||||
Reference in New Issue
Block a user