import { Page } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
import { getWorkspaceRoot, assertSafeProjectPath } from '../core/safe-paths';
export { getWorkspaceRoot };
const WORKSPACE_ROOT = getWorkspaceRoot();
const FIXTURE_PATH = path.join(WORKSPACE_ROOT, 'test/fixtures/projectA');
// Original spec content for resetting between tests
const ORIGINAL_SPEC_CONTENT = `
Test Project A
A test fixture project for Playwright testing
- TypeScript
- React
`;
// Worker-isolated fixture path to avoid conflicts when running tests in parallel.
// Each Playwright worker gets its own copy of the fixture directory.
let _workerFixturePath: string | null = null;
/**
* Bootstrap the shared fixture directory if it doesn't exist.
* The fixture contains a nested .git/ dir so it can't be tracked by the
* parent repo — in CI this directory won't exist after checkout.
*/
function ensureFixtureExists(): void {
if (fs.existsSync(FIXTURE_PATH)) return;
fs.mkdirSync(path.join(FIXTURE_PATH, '.automaker/context'), { recursive: true });
fs.writeFileSync(path.join(FIXTURE_PATH, '.automaker/app_spec.txt'), ORIGINAL_SPEC_CONTENT);
fs.writeFileSync(path.join(FIXTURE_PATH, '.automaker/categories.json'), '[]');
fs.writeFileSync(
path.join(FIXTURE_PATH, '.automaker/context/context-metadata.json'),
'{"files": {}}'
);
}
/**
* Get a worker-isolated fixture path. Creates a copy of the fixture directory
* for this worker process so parallel tests don't conflict.
* Falls back to the shared fixture path for backwards compatibility.
*/
function getWorkerFixturePath(): string {
if (_workerFixturePath) return _workerFixturePath;
// Ensure the source fixture exists (may not in CI)
ensureFixtureExists();
if (!fs.existsSync(FIXTURE_PATH)) {
throw new Error(
`E2E source fixture is missing at ${FIXTURE_PATH}. ` +
'Run the setup script to create it: from apps/ui, run `node scripts/setup-e2e-fixtures.mjs` (or use `pnpm test`, which runs it via pretest).'
);
}
// Use process.pid + a unique suffix to isolate per-worker
const workerId = process.env.TEST_WORKER_INDEX || process.pid.toString();
const workerDir = path.join(WORKSPACE_ROOT, `test/fixtures/.worker-${workerId}`);
// Copy projectA fixture to worker directory if it doesn't exist
if (!fs.existsSync(workerDir)) {
fs.cpSync(FIXTURE_PATH, workerDir, { recursive: true });
}
_workerFixturePath = workerDir;
return workerDir;
}
/**
* Get the worker-isolated context path
*/
function getWorkerContextPath(): string {
return path.join(getWorkerFixturePath(), '.automaker/context');
}
/**
* Get the worker-isolated memory path
*/
function getWorkerMemoryPath(): string {
return path.join(getWorkerFixturePath(), '.automaker/memory');
}
/**
* Get the worker-isolated spec file path
*/
function getWorkerSpecPath(): string {
return path.join(getWorkerFixturePath(), '.automaker/app_spec.txt');
}
/**
* Reset the fixture's app_spec.txt to original content
*/
export function resetFixtureSpec(): void {
const specPath = getWorkerSpecPath();
const dir = path.dirname(specPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(specPath, ORIGINAL_SPEC_CONTENT);
}
/**
* Reset the context directory to empty state
*/
export function resetContextDirectory(): void {
const contextPath = getWorkerContextPath();
if (fs.existsSync(contextPath)) {
fs.rmSync(contextPath, { recursive: true });
}
fs.mkdirSync(contextPath, { recursive: true });
}
/**
* Reset the memory directory to empty state
*/
export function resetMemoryDirectory(): void {
const memoryPath = getWorkerMemoryPath();
if (fs.existsSync(memoryPath)) {
fs.rmSync(memoryPath, { recursive: true });
}
fs.mkdirSync(memoryPath, { recursive: true });
}
/**
* Resolve and validate a context fixture path to prevent path traversal
*/
function resolveContextFixturePath(filename: string): string {
const contextPath = getWorkerContextPath();
const resolved = path.resolve(contextPath, filename);
const base = path.resolve(contextPath) + path.sep;
if (!resolved.startsWith(base)) {
throw new Error(`Invalid context filename: ${filename}`);
}
return resolved;
}
/**
* Create a context file directly on disk (for test setup)
*/
export function createContextFileOnDisk(filename: string, content: string): void {
const filePath = resolveContextFixturePath(filename);
fs.writeFileSync(filePath, content);
}
/**
* Resolve and validate a memory fixture path to prevent path traversal
*/
function resolveMemoryFixturePath(filename: string): string {
const memoryPath = getWorkerMemoryPath();
const resolved = path.resolve(memoryPath, filename);
const base = path.resolve(memoryPath) + path.sep;
if (!resolved.startsWith(base)) {
throw new Error(`Invalid memory filename: ${filename}`);
}
return resolved;
}
/**
* Create a memory file directly on disk (for test setup)
*/
export function createMemoryFileOnDisk(filename: string, content: string): void {
const filePath = resolveMemoryFixturePath(filename);
fs.writeFileSync(filePath, content);
}
/**
* Check if a context file exists on disk
*/
export function contextFileExistsOnDisk(filename: string): boolean {
const filePath = resolveContextFixturePath(filename);
return fs.existsSync(filePath);
}
/**
* Check if a memory file exists on disk
*/
export function memoryFileExistsOnDisk(filename: string): boolean {
const filePath = resolveMemoryFixturePath(filename);
return fs.existsSync(filePath);
}
/**
* Set up localStorage with a project pointing to our test fixture
* Note: In CI, setup wizard is also skipped via NEXT_PUBLIC_SKIP_SETUP env var
* Project path must be under test/ or temp to avoid affecting the main project's git.
* Defaults to a worker-isolated copy of the fixture to support parallel test execution.
*/
export async function setupProjectWithFixture(
page: Page,
projectPath: string = getWorkerFixturePath()
): Promise {
assertSafeProjectPath(projectPath);
await page.addInitScript((pathArg: string) => {
const mockProject = {
id: 'test-project-fixture',
name: 'projectA',
path: pathArg,
lastOpened: new Date().toISOString(),
};
const mockState = {
state: {
projects: [mockProject],
currentProject: mockProject,
currentView: 'board',
theme: 'dark',
sidebarOpen: true,
skipSandboxWarning: true,
apiKeys: { anthropic: '', google: '' },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: 3,
},
version: 2, // Must match app-store.ts persist version
};
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
// Also mark setup as complete (fallback for when NEXT_PUBLIC_SKIP_SETUP isn't set)
const setupState = {
state: {
isFirstRun: false,
setupComplete: true,
currentStep: 'complete',
skipClaudeSetup: false,
},
version: 0, // setup-store.ts doesn't specify a version, so zustand defaults to 0
};
localStorage.setItem('automaker-setup', JSON.stringify(setupState));
// Set settings cache so the fast-hydrate path uses our fixture project.
// Without this, a stale settings cache from a previous test can override
// the project we just set in automaker-storage.
const settingsCache = {
setupComplete: true,
isFirstRun: false,
projects: [
{
id: mockProject.id,
name: mockProject.name,
path: mockProject.path,
lastOpened: mockProject.lastOpened,
},
],
currentProjectId: mockProject.id,
theme: 'dark',
sidebarOpen: true,
maxConcurrency: 3,
skipSandboxWarning: true,
};
localStorage.setItem('automaker-settings-cache', JSON.stringify(settingsCache));
// Disable splash screen in tests
localStorage.setItem('automaker-disable-splash', 'true');
}, projectPath);
}
/**
* Get the fixture path (worker-isolated for parallel test execution)
*/
export function getFixturePath(): string {
return getWorkerFixturePath();
}
/**
* Set up a mock project with the fixture path (for profile/settings tests that need a project).
* Options such as customProfilesCount are reserved for future use (e.g. mocking server profile state).
*/
export async function setupMockProjectWithProfiles(
page: Page,
_options?: { customProfilesCount?: number }
): Promise {
await setupProjectWithFixture(page);
}