| |
| |
| |
| |
|
|
| import { Page, APIResponse } from '@playwright/test'; |
| import { API_BASE_URL, API_ENDPOINTS, WEB_BASE_URL } from '../core/constants'; |
|
|
| |
| |
| |
|
|
| export interface WorktreeInfo { |
| path: string; |
| branch: string; |
| isNew?: boolean; |
| hasChanges?: boolean; |
| changedFilesCount?: number; |
| } |
|
|
| export interface WorktreeListResponse { |
| success: boolean; |
| worktrees: WorktreeInfo[]; |
| error?: string; |
| } |
|
|
| export interface WorktreeCreateResponse { |
| success: boolean; |
| worktree?: WorktreeInfo; |
| error?: string; |
| } |
|
|
| export interface WorktreeDeleteResponse { |
| success: boolean; |
| error?: string; |
| } |
|
|
| export interface CommitResult { |
| committed: boolean; |
| branch?: string; |
| commitHash?: string; |
| message?: string; |
| } |
|
|
| export interface CommitResponse { |
| success: boolean; |
| result?: CommitResult; |
| error?: string; |
| } |
|
|
| export interface SwitchBranchResult { |
| previousBranch: string; |
| currentBranch: string; |
| message: string; |
| } |
|
|
| export interface SwitchBranchResponse { |
| success: boolean; |
| result?: SwitchBranchResult; |
| error?: string; |
| code?: string; |
| } |
|
|
| export interface BranchInfo { |
| name: string; |
| isCurrent: boolean; |
| } |
|
|
| export interface ListBranchesResult { |
| currentBranch: string; |
| branches: BranchInfo[]; |
| } |
|
|
| export interface ListBranchesResponse { |
| success: boolean; |
| result?: ListBranchesResult; |
| error?: string; |
| } |
|
|
| |
| |
| |
|
|
| export class WorktreeApiClient { |
| constructor(private page: Page) {} |
|
|
| |
| |
| |
| async create( |
| projectPath: string, |
| branchName: string, |
| baseBranch?: string |
| ): Promise<{ response: APIResponse; data: WorktreeCreateResponse }> { |
| const response = await this.page.request.post(API_ENDPOINTS.worktree.create, { |
| data: { |
| projectPath, |
| branchName, |
| baseBranch, |
| }, |
| }); |
| const data = await response.json(); |
| return { response, data }; |
| } |
|
|
| |
| |
| |
| async delete( |
| projectPath: string, |
| worktreePath: string, |
| deleteBranch: boolean = true |
| ): Promise<{ response: APIResponse; data: WorktreeDeleteResponse }> { |
| const response = await this.page.request.post(API_ENDPOINTS.worktree.delete, { |
| data: { |
| projectPath, |
| worktreePath, |
| deleteBranch, |
| }, |
| }); |
| const data = await response.json(); |
| return { response, data }; |
| } |
|
|
| |
| |
| |
| async list( |
| projectPath: string, |
| includeDetails: boolean = true |
| ): Promise<{ response: APIResponse; data: WorktreeListResponse }> { |
| const response = await this.page.request.post(API_ENDPOINTS.worktree.list, { |
| data: { |
| projectPath, |
| includeDetails, |
| }, |
| }); |
| const data = await response.json(); |
| return { response, data }; |
| } |
|
|
| |
| |
| |
| async commit( |
| worktreePath: string, |
| message: string |
| ): Promise<{ response: APIResponse; data: CommitResponse }> { |
| const response = await this.page.request.post(API_ENDPOINTS.worktree.commit, { |
| data: { |
| worktreePath, |
| message, |
| }, |
| }); |
| const data = await response.json(); |
| return { response, data }; |
| } |
|
|
| |
| |
| |
| async switchBranch( |
| worktreePath: string, |
| branchName: string |
| ): Promise<{ response: APIResponse; data: SwitchBranchResponse }> { |
| const response = await this.page.request.post(API_ENDPOINTS.worktree.switchBranch, { |
| data: { |
| worktreePath, |
| branchName, |
| }, |
| }); |
| const data = await response.json(); |
| return { response, data }; |
| } |
|
|
| |
| |
| |
| async listBranches( |
| worktreePath: string |
| ): Promise<{ response: APIResponse; data: ListBranchesResponse }> { |
| const response = await this.page.request.post(API_ENDPOINTS.worktree.listBranches, { |
| data: { |
| worktreePath, |
| }, |
| }); |
| const data = await response.json(); |
| return { response, data }; |
| } |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| export function createWorktreeApiClient(page: Page): WorktreeApiClient { |
| return new WorktreeApiClient(page); |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| export async function apiCreateWorktree( |
| page: Page, |
| projectPath: string, |
| branchName: string, |
| baseBranch?: string |
| ): Promise<{ response: APIResponse; data: WorktreeCreateResponse }> { |
| return new WorktreeApiClient(page).create(projectPath, branchName, baseBranch); |
| } |
|
|
| |
| |
| |
| export async function apiDeleteWorktree( |
| page: Page, |
| projectPath: string, |
| worktreePath: string, |
| deleteBranch: boolean = true |
| ): Promise<{ response: APIResponse; data: WorktreeDeleteResponse }> { |
| return new WorktreeApiClient(page).delete(projectPath, worktreePath, deleteBranch); |
| } |
|
|
| |
| |
| |
| export async function apiListWorktrees( |
| page: Page, |
| projectPath: string, |
| includeDetails: boolean = true |
| ): Promise<{ response: APIResponse; data: WorktreeListResponse }> { |
| return new WorktreeApiClient(page).list(projectPath, includeDetails); |
| } |
|
|
| |
| |
| |
| export async function apiCommitWorktree( |
| page: Page, |
| worktreePath: string, |
| message: string |
| ): Promise<{ response: APIResponse; data: CommitResponse }> { |
| return new WorktreeApiClient(page).commit(worktreePath, message); |
| } |
|
|
| |
| |
| |
| export async function apiSwitchBranch( |
| page: Page, |
| worktreePath: string, |
| branchName: string |
| ): Promise<{ response: APIResponse; data: SwitchBranchResponse }> { |
| return new WorktreeApiClient(page).switchBranch(worktreePath, branchName); |
| } |
|
|
| |
| |
| |
| export async function apiListBranches( |
| page: Page, |
| worktreePath: string |
| ): Promise<{ response: APIResponse; data: ListBranchesResponse }> { |
| return new WorktreeApiClient(page).listBranches(worktreePath); |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| export async function authenticateWithApiKey(page: Page, apiKey: string): Promise<boolean> { |
| try { |
| |
| try { |
| const statusRes = await page.request.get(`${API_BASE_URL}/api/auth/status`, { |
| timeout: 3000, |
| }); |
| const statusJson = (await statusRes.json().catch(() => null)) as { |
| authenticated?: boolean; |
| } | null; |
| if (statusJson?.authenticated === true) { |
| return true; |
| } |
| } catch { |
| |
| } |
|
|
| |
| |
| const start = Date.now(); |
| let authBackoff = 250; |
| while (Date.now() - start < 15000) { |
| try { |
| const health = await page.request.get(`${API_BASE_URL}/api/health`, { |
| timeout: 3000, |
| }); |
| if (health.ok()) break; |
| } catch { |
| |
| } |
| await page.waitForTimeout(authBackoff); |
| authBackoff = Math.min(authBackoff * 2, 2000); |
| } |
|
|
| |
| const currentUrl = page.url(); |
| if (!currentUrl || currentUrl === 'about:blank') { |
| await page.goto(WEB_BASE_URL, { waitUntil: 'domcontentloaded' }); |
| } |
|
|
| |
| |
| const loginResponse = await page.request.post(`${API_BASE_URL}/api/auth/login`, { |
| data: { apiKey }, |
| headers: { 'Content-Type': 'application/json' }, |
| timeout: 15000, |
| }); |
| const response = (await loginResponse.json().catch(() => null)) as { |
| success?: boolean; |
| token?: string; |
| } | null; |
|
|
| if (response?.success && response.token) { |
| |
| |
| await page.context().addCookies([ |
| { |
| name: 'automaker_session', |
| value: response.token, |
| domain: '127.0.0.1', |
| path: '/', |
| httpOnly: true, |
| sameSite: 'Lax', |
| }, |
| ]); |
|
|
| |
| const verifyRes = await page.request.get(`${API_BASE_URL}/api/auth/status`, { |
| timeout: 5000, |
| }); |
| const verifyJson = (await verifyRes.json().catch(() => null)) as { |
| authenticated?: boolean; |
| } | null; |
|
|
| return verifyJson?.authenticated === true; |
| } |
|
|
| return false; |
| } catch (error) { |
| console.error('Authentication error:', error); |
| return false; |
| } |
| } |
|
|
| |
| |
| |
| |
| export async function authenticateForTests(page: Page): Promise<boolean> { |
| |
| const apiKey = process.env.AUTOMAKER_API_KEY || 'test-api-key-for-e2e-tests'; |
| return authenticateWithApiKey(page, apiKey); |
| } |
|
|
| |
| |
| |
| |
| export async function checkBackendHealth(page: Page, timeout = 5000): Promise<boolean> { |
| try { |
| const response = await page.request.get(`${API_BASE_URL}/api/health`, { |
| timeout, |
| }); |
| return response.ok(); |
| } catch { |
| return false; |
| } |
| } |
|
|
| |
| |
| |
| |
| export async function waitForBackendHealth( |
| page: Page, |
| maxWaitMs = 30000, |
| checkIntervalMs = 500 |
| ): Promise<void> { |
| const startTime = Date.now(); |
| let backoff = checkIntervalMs; |
|
|
| while (Date.now() - startTime < maxWaitMs) { |
| if (await checkBackendHealth(page, Math.min(backoff, 3000))) { |
| return; |
| } |
| await page.waitForTimeout(backoff); |
| backoff = Math.min(backoff * 2, 2000); |
| } |
|
|
| throw new Error( |
| `Backend did not become healthy within ${maxWaitMs}ms. ` + |
| `Last health check failed or timed out.` |
| ); |
| } |
|
|