feat: improve test setup and authentication handling

- Added `dev:test` script to package.json for streamlined testing without file watching.
- Introduced `kill-test-servers` script to ensure no existing servers are running on test ports before executing tests.
- Enhanced Playwright configuration to use mock agent for tests, ensuring consistent API responses and disabling rate limiting.
- Updated various test files to include authentication steps and handle login screens, improving reliability and reducing flakiness in tests.
- Added `global-setup` for e2e tests to ensure proper initialization before test execution.
This commit is contained in:
Test User
2025-12-30 00:06:27 -05:00
parent 59a6a23f9b
commit 46caae05d2
22 changed files with 376 additions and 33 deletions

View File

@@ -278,15 +278,74 @@ export async function apiListBranches(
/**
* Authenticate with the server using an API key
* This sets a session cookie that will be used for subsequent requests
* Uses browser context to ensure cookies are properly set
*/
export async function authenticateWithApiKey(page: Page, apiKey: string): Promise<boolean> {
try {
const response = await page.request.post(`${API_BASE_URL}/api/auth/login`, {
data: { apiKey },
});
const data = await response.json();
return data.success === true;
} catch {
// Ensure we're on a page (needed for cookies to work)
const currentUrl = page.url();
if (!currentUrl || currentUrl === 'about:blank') {
await page.goto('http://localhost:3007', { waitUntil: 'domcontentloaded' });
}
// Use browser context fetch to ensure cookies are set in the browser
const response = await page.evaluate(
async ({ url, apiKey }) => {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ apiKey }),
});
const data = await res.json();
return { success: data.success, token: data.token };
},
{ url: `${API_BASE_URL}/api/auth/login`, apiKey }
);
if (response.success && response.token) {
// Manually set the cookie in the browser context
// The server sets a cookie named 'automaker_session' (see SESSION_COOKIE_NAME in auth.ts)
await page.context().addCookies([
{
name: 'automaker_session',
value: response.token,
domain: 'localhost',
path: '/',
httpOnly: true,
sameSite: 'Lax',
},
]);
// Verify the session is working by polling auth status
// This replaces arbitrary timeout with actual condition check
let attempts = 0;
const maxAttempts = 10;
while (attempts < maxAttempts) {
const statusResponse = await page.evaluate(
async ({ url }) => {
const res = await fetch(url, {
credentials: 'include',
});
return res.json();
},
{ url: `${API_BASE_URL}/api/auth/status` }
);
if (statusResponse.authenticated === true) {
return true;
}
attempts++;
// Use a very short wait between polling attempts (this is acceptable for polling)
await page.waitForFunction(() => true, { timeout: 50 });
}
return false;
}
return false;
} catch (error) {
console.error('Authentication error:', error);
return false;
}
}

View File

@@ -1,4 +1,4 @@
import { Page } from '@playwright/test';
import { Page, expect } from '@playwright/test';
import { getByTestId, getButtonByText } from './elements';
/**
@@ -48,6 +48,72 @@ export async function pressShortcut(page: Page, key: string): Promise<void> {
await page.keyboard.press(key);
}
/**
* Navigate to a URL with authentication
* This wrapper ensures authentication happens before navigation
*/
export async function gotoWithAuth(page: Page, url: string): Promise<void> {
const { authenticateForTests } = await import('../api/client');
await authenticateForTests(page);
await page.goto(url);
}
/**
* Handle login screen if it appears after navigation
* Returns true if login was handled, false if no login screen was found
*/
export async function handleLoginScreenIfPresent(page: Page): Promise<boolean> {
// Check for login screen by waiting for either login input or app-container to be visible
// Use data-testid selector (preferred) with fallback to the old selector
const loginInput = page
.locator('[data-testid="login-api-key-input"], input[type="password"][placeholder*="API key"]')
.first();
const appContent = page.locator(
'[data-testid="welcome-view"], [data-testid="board-view"], [data-testid="context-view"], [data-testid="agent-view"]'
);
// Race between login screen and actual content
const loginVisible = await Promise.race([
loginInput
.waitFor({ state: 'visible', timeout: 5000 })
.then(() => true)
.catch(() => false),
appContent
.first()
.waitFor({ state: 'visible', timeout: 5000 })
.then(() => false)
.catch(() => false),
]);
if (loginVisible) {
const apiKey = process.env.AUTOMAKER_API_KEY || 'test-api-key-for-e2e-tests';
await loginInput.fill(apiKey);
// Wait a moment for the button to become enabled
await page.waitForTimeout(100);
// Wait for button to be enabled (it's disabled when input is empty)
const loginButton = page
.locator('[data-testid="login-submit-button"], button:has-text("Login")')
.first();
await expect(loginButton).toBeEnabled({ timeout: 5000 });
await loginButton.click();
// Wait for navigation away from login - either to content or URL change
await Promise.race([
page.waitForURL((url) => !url.pathname.includes('/login'), { timeout: 10000 }),
appContent.first().waitFor({ state: 'visible', timeout: 10000 }),
]).catch(() => {});
// Wait for page to load
await page.waitForLoadState('load');
return true;
}
return false;
}
/**
* Press a number key (0-9) on the keyboard
*/

View File

@@ -1,16 +1,37 @@
import { Page } from '@playwright/test';
import { clickElement } from '../core/interactions';
import { waitForElement } from '../core/waiting';
import { authenticateForTests } from '../api/client';
/**
* Navigate to the board/kanban view
* Note: Navigates directly to /board since index route shows WelcomeView
*/
export async function navigateToBoard(page: Page): Promise<void> {
// Authenticate before navigating
await authenticateForTests(page);
// Navigate directly to /board route
await page.goto('/board');
await page.waitForLoadState('load');
// Check if we're on the login screen and handle it
const loginInput = page
.locator('[data-testid="login-api-key-input"], input[type="password"][placeholder*="API key"]')
.first();
const isLoginScreen = await loginInput.isVisible({ timeout: 2000 }).catch(() => false);
if (isLoginScreen) {
const apiKey = process.env.AUTOMAKER_API_KEY || 'test-api-key-for-e2e-tests';
await loginInput.fill(apiKey);
await page.waitForTimeout(100);
await page
.locator('[data-testid="login-submit-button"], button:has-text("Login")')
.first()
.click();
await page.waitForURL('**/board', { timeout: 5000 });
await page.waitForLoadState('load');
}
// Wait for the board view to be visible
await waitForElement(page, 'board-view', { timeout: 10000 });
}
@@ -20,10 +41,30 @@ export async function navigateToBoard(page: Page): Promise<void> {
* Note: Navigates directly to /context since index route shows WelcomeView
*/
export async function navigateToContext(page: Page): Promise<void> {
// Authenticate before navigating
await authenticateForTests(page);
// Navigate directly to /context route
await page.goto('/context');
await page.waitForLoadState('load');
// Check if we're on the login screen and handle it
const loginInputCtx = page
.locator('[data-testid="login-api-key-input"], input[type="password"][placeholder*="API key"]')
.first();
const isLoginScreenCtx = await loginInputCtx.isVisible({ timeout: 2000 }).catch(() => false);
if (isLoginScreenCtx) {
const apiKey = process.env.AUTOMAKER_API_KEY || 'test-api-key-for-e2e-tests';
await loginInputCtx.fill(apiKey);
await page.waitForTimeout(100);
await page
.locator('[data-testid="login-submit-button"], button:has-text("Login")')
.first()
.click();
await page.waitForURL('**/context', { timeout: 5000 });
await page.waitForLoadState('load');
}
// Wait for loading to complete (if present)
const loadingElement = page.locator('[data-testid="context-view-loading"]');
try {
@@ -46,6 +87,9 @@ export async function navigateToContext(page: Page): Promise<void> {
* Note: Navigates directly to /spec since index route shows WelcomeView
*/
export async function navigateToSpec(page: Page): Promise<void> {
// Authenticate before navigating
await authenticateForTests(page);
// Navigate directly to /spec route
await page.goto('/spec');
await page.waitForLoadState('load');
@@ -76,10 +120,30 @@ export async function navigateToSpec(page: Page): Promise<void> {
* Note: Navigates directly to /agent since index route shows WelcomeView
*/
export async function navigateToAgent(page: Page): Promise<void> {
// Authenticate before navigating
await authenticateForTests(page);
// Navigate directly to /agent route
await page.goto('/agent');
await page.waitForLoadState('load');
// Check if we're on the login screen and handle it
const loginInputAgent = page
.locator('[data-testid="login-api-key-input"], input[type="password"][placeholder*="API key"]')
.first();
const isLoginScreenAgent = await loginInputAgent.isVisible({ timeout: 2000 }).catch(() => false);
if (isLoginScreenAgent) {
const apiKey = process.env.AUTOMAKER_API_KEY || 'test-api-key-for-e2e-tests';
await loginInputAgent.fill(apiKey);
await page.waitForTimeout(100);
await page
.locator('[data-testid="login-submit-button"], button:has-text("Login")')
.first()
.click();
await page.waitForURL('**/agent', { timeout: 5000 });
await page.waitForLoadState('load');
}
// Wait for the agent view to be visible
await waitForElement(page, 'agent-view', { timeout: 10000 });
}
@@ -89,6 +153,9 @@ export async function navigateToAgent(page: Page): Promise<void> {
* Note: Navigates directly to /settings since index route shows WelcomeView
*/
export async function navigateToSettings(page: Page): Promise<void> {
// Authenticate before navigating
await authenticateForTests(page);
// Navigate directly to /settings route
await page.goto('/settings');
await page.waitForLoadState('load');
@@ -114,8 +181,31 @@ export async function navigateToSetup(page: Page): Promise<void> {
* Navigate to the welcome view (clear project selection)
*/
export async function navigateToWelcome(page: Page): Promise<void> {
// Authenticate before navigating
await authenticateForTests(page);
await page.goto('/');
await page.waitForLoadState('load');
// Check if we're on the login screen and handle it
const loginInputWelcome = page
.locator('[data-testid="login-api-key-input"], input[type="password"][placeholder*="API key"]')
.first();
const isLoginScreenWelcome = await loginInputWelcome
.isVisible({ timeout: 2000 })
.catch(() => false);
if (isLoginScreenWelcome) {
const apiKey = process.env.AUTOMAKER_API_KEY || 'test-api-key-for-e2e-tests';
await loginInputWelcome.fill(apiKey);
await page.waitForTimeout(100);
await page
.locator('[data-testid="login-submit-button"], button:has-text("Login")')
.first()
.click();
await page.waitForURL('**/', { timeout: 5000 });
await page.waitForLoadState('load');
}
await waitForElement(page, 'welcome-view', { timeout: 10000 });
}