feat: enhance E2E test setup and error handling

- Updated Playwright configuration to explicitly unset ALLOWED_ROOT_DIRECTORY for unrestricted testing paths.
- Improved E2E fixture setup script to reset server settings to a known state, ensuring test isolation.
- Enhanced error handling in ContextView and WelcomeView components to reset state and provide user feedback on failures.
- Updated tests to ensure proper navigation and visibility checks during logout processes, improving reliability.
This commit is contained in:
webdevcody
2026-01-07 23:01:57 -05:00
parent d8cdb0bf7a
commit eb627ef323
6 changed files with 192 additions and 5 deletions

View File

@@ -53,7 +53,9 @@ export default defineConfig({
process.env.AUTOMAKER_API_KEY || 'test-api-key-for-e2e-tests',
// Hide the API key banner to reduce log noise
AUTOMAKER_HIDE_API_KEY: 'true',
// No ALLOWED_ROOT_DIRECTORY restriction - allow all paths for testing
// Explicitly unset ALLOWED_ROOT_DIRECTORY to allow all paths for testing
// (prevents inheriting /projects from Docker or other environments)
ALLOWED_ROOT_DIRECTORY: '',
// Simulate containerized environment to skip sandbox confirmation dialogs
IS_CONTAINERIZED: 'true',
},

View File

@@ -3,9 +3,11 @@
/**
* Setup script for E2E test fixtures
* Creates the necessary test fixture directories and files before running Playwright tests
* Also resets the server's settings.json to a known state for test isolation
*/
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { fileURLToPath } from 'url';
@@ -16,6 +18,9 @@ const __dirname = path.dirname(__filename);
const WORKSPACE_ROOT = path.resolve(__dirname, '../../..');
const FIXTURE_PATH = path.join(WORKSPACE_ROOT, 'test/fixtures/projectA');
const SPEC_FILE_PATH = path.join(FIXTURE_PATH, '.automaker/app_spec.txt');
const SERVER_SETTINGS_PATH = path.join(WORKSPACE_ROOT, 'apps/server/data/settings.json');
// Create a shared test workspace directory that will be used as default for project creation
const TEST_WORKSPACE_DIR = path.join(os.tmpdir(), 'automaker-e2e-workspace');
const SPEC_CONTENT = `<app_spec>
<name>Test Project A</name>
@@ -27,10 +32,153 @@ const SPEC_CONTENT = `<app_spec>
</app_spec>
`;
// Clean settings.json for E2E tests - no current project so localStorage can control state
const E2E_SETTINGS = {
version: 4,
setupComplete: true,
isFirstRun: false,
skipClaudeSetup: false,
theme: 'dark',
sidebarOpen: true,
chatHistoryOpen: false,
kanbanCardDetailLevel: 'standard',
maxConcurrency: 3,
defaultSkipTests: true,
enableDependencyBlocking: true,
skipVerificationInAutoMode: false,
useWorktrees: false,
showProfilesOnly: false,
defaultPlanningMode: 'skip',
defaultRequirePlanApproval: false,
defaultAIProfileId: null,
muteDoneSound: false,
phaseModels: {
enhancementModel: { model: 'sonnet' },
fileDescriptionModel: { model: 'haiku' },
imageDescriptionModel: { model: 'haiku' },
validationModel: { model: 'sonnet' },
specGenerationModel: { model: 'opus' },
featureGenerationModel: { model: 'sonnet' },
backlogPlanningModel: { model: 'sonnet' },
projectAnalysisModel: { model: 'sonnet' },
suggestionsModel: { model: 'sonnet' },
},
enhancementModel: 'sonnet',
validationModel: 'opus',
enabledCursorModels: ['auto', 'composer-1'],
cursorDefaultModel: 'auto',
keyboardShortcuts: {
board: 'K',
agent: 'A',
spec: 'D',
context: 'C',
settings: 'S',
profiles: 'M',
terminal: 'T',
toggleSidebar: '`',
addFeature: 'N',
addContextFile: 'N',
startNext: 'G',
newSession: 'N',
openProject: 'O',
projectPicker: 'P',
cyclePrevProject: 'Q',
cycleNextProject: 'E',
addProfile: 'N',
splitTerminalRight: 'Alt+D',
splitTerminalDown: 'Alt+S',
closeTerminal: 'Alt+W',
tools: 'T',
ideation: 'I',
githubIssues: 'G',
githubPrs: 'R',
newTerminalTab: 'Alt+T',
},
aiProfiles: [
{
id: 'profile-heavy-task',
name: 'Heavy Task',
description: 'Claude Opus with Ultrathink for complex architecture, migrations, or deep debugging.',
model: 'opus',
thinkingLevel: 'ultrathink',
provider: 'claude',
isBuiltIn: true,
icon: 'Brain',
},
{
id: 'profile-balanced',
name: 'Balanced',
description: 'Claude Sonnet with medium thinking for typical development tasks.',
model: 'sonnet',
thinkingLevel: 'medium',
provider: 'claude',
isBuiltIn: true,
icon: 'Scale',
},
{
id: 'profile-quick-edit',
name: 'Quick Edit',
description: 'Claude Haiku for fast, simple edits and minor fixes.',
model: 'haiku',
thinkingLevel: 'none',
provider: 'claude',
isBuiltIn: true,
icon: 'Zap',
},
{
id: 'profile-cursor-refactoring',
name: 'Cursor Refactoring',
description: 'Cursor Composer 1 for refactoring tasks.',
provider: 'cursor',
cursorModel: 'composer-1',
isBuiltIn: true,
icon: 'Sparkles',
},
],
// Default test project using the fixture path - tests can override via route mocking if needed
projects: [
{
id: 'e2e-default-project',
name: 'E2E Test Project',
path: FIXTURE_PATH,
lastOpened: new Date().toISOString(),
},
],
trashedProjects: [],
currentProjectId: 'e2e-default-project',
projectHistory: [],
projectHistoryIndex: 0,
lastProjectDir: TEST_WORKSPACE_DIR,
recentFolders: [],
worktreePanelCollapsed: false,
lastSelectedSessionByProject: {},
autoLoadClaudeMd: false,
skipSandboxWarning: true,
codexAutoLoadAgents: false,
codexSandboxMode: 'workspace-write',
codexApprovalPolicy: 'on-request',
codexEnableWebSearch: false,
codexEnableImages: true,
codexAdditionalDirs: [],
mcpServers: [],
enableSandboxMode: false,
mcpAutoApproveTools: true,
mcpUnrestrictedTools: true,
promptCustomization: {},
localStorageMigrated: true,
};
function setupFixtures() {
console.log('Setting up E2E test fixtures...');
console.log(`Workspace root: ${WORKSPACE_ROOT}`);
console.log(`Fixture path: ${FIXTURE_PATH}`);
console.log(`Test workspace dir: ${TEST_WORKSPACE_DIR}`);
// Create test workspace directory for project creation tests
if (!fs.existsSync(TEST_WORKSPACE_DIR)) {
fs.mkdirSync(TEST_WORKSPACE_DIR, { recursive: true });
console.log(`Created test workspace directory: ${TEST_WORKSPACE_DIR}`);
}
// Create fixture directory
const specDir = path.dirname(SPEC_FILE_PATH);
@@ -43,6 +191,15 @@ function setupFixtures() {
fs.writeFileSync(SPEC_FILE_PATH, SPEC_CONTENT);
console.log(`Created fixture file: ${SPEC_FILE_PATH}`);
// Reset server settings.json to a clean state for E2E tests
const settingsDir = path.dirname(SERVER_SETTINGS_PATH);
if (!fs.existsSync(settingsDir)) {
fs.mkdirSync(settingsDir, { recursive: true });
console.log(`Created directory: ${settingsDir}`);
}
fs.writeFileSync(SERVER_SETTINGS_PATH, JSON.stringify(E2E_SETTINGS, null, 2));
console.log(`Reset server settings: ${SERVER_SETTINGS_PATH}`);
console.log('E2E test fixtures setup complete!');
}

View File

@@ -496,6 +496,14 @@ export function ContextView() {
setNewMarkdownContent('');
} catch (error) {
logger.error('Failed to create markdown:', error);
// Close dialog and reset state even on error to avoid stuck dialog
setIsCreateMarkdownOpen(false);
setNewMarkdownName('');
setNewMarkdownDescription('');
setNewMarkdownContent('');
toast.error('Failed to create markdown file', {
description: error instanceof Error ? error.message : 'Unknown error occurred',
});
}
};

View File

@@ -319,6 +319,9 @@ export function WelcomeView() {
projectPath: projectPath,
});
setShowInitDialog(true);
// Navigate to the board view (dialog shows as overlay)
navigate({ to: '/board' });
} catch (error) {
logger.error('Failed to create project:', error);
toast.error('Failed to create project', {
@@ -418,6 +421,9 @@ export function WelcomeView() {
});
setShowInitDialog(true);
// Navigate to the board view (dialog shows as overlay)
navigate({ to: '/board' });
// Kick off project analysis
analyzeProject(projectPath);
} catch (error) {
@@ -515,6 +521,9 @@ export function WelcomeView() {
});
setShowInitDialog(true);
// Navigate to the board view (dialog shows as overlay)
navigate({ to: '/board' });
// Kick off project analysis
analyzeProject(projectPath);
} catch (error) {

View File

@@ -120,13 +120,21 @@ test.describe('Settings startup sync race', () => {
};
expect(beforeLogout.projects?.length).toBeGreaterThan(0);
// Navigate to settings and click logout.
// Navigate to settings, then to Account section (logout button is only visible there)
await page.goto('/settings');
// Wait for settings view to load, then click on Account section
await page.locator('button:has-text("Account")').first().click();
// Wait for account section to be visible before clicking logout
await page
.locator('[data-testid="logout-button"]')
.waitFor({ state: 'visible', timeout: 10000 });
await page.locator('[data-testid="logout-button"]').click();
// Ensure we landed on logged-out or login (either is acceptable).
// Note: The page uses curly apostrophe (') so we match the heading role instead
await page
.locator('text=Youve been logged out, text=Authentication Required')
.getByRole('heading', { name: /logged out/i })
.or(page.locator('text=Authentication Required'))
.first()
.waitFor({ state: 'visible', timeout: 30000 });

View File

@@ -20,11 +20,14 @@ export async function pressModifierEnter(page: Page): Promise<void> {
/**
* Click an element by its data-testid attribute
* Waits for the element to be visible before clicking to avoid flaky tests
*/
export async function clickElement(page: Page, testId: string): Promise<void> {
// Wait for splash screen to disappear first (safety net)
await waitForSplashScreenToDisappear(page, 2000);
const element = await getByTestId(page, testId);
await waitForSplashScreenToDisappear(page, 5000);
const element = page.locator(`[data-testid="${testId}"]`);
// Wait for element to be visible and stable before clicking
await element.waitFor({ state: 'visible', timeout: 10000 });
await element.click();
}