From eb627ef32372df54ee369a8d88ef3a16cbff68de Mon Sep 17 00:00:00 2001 From: webdevcody Date: Wed, 7 Jan 2026 23:01:57 -0500 Subject: [PATCH] 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. --- apps/ui/playwright.config.ts | 4 +- apps/ui/scripts/setup-e2e-fixtures.mjs | 157 ++++++++++++++++++ apps/ui/src/components/views/context-view.tsx | 8 + apps/ui/src/components/views/welcome-view.tsx | 9 + .../settings-startup-sync-race.spec.ts | 12 +- apps/ui/tests/utils/core/interactions.ts | 7 +- 6 files changed, 192 insertions(+), 5 deletions(-) diff --git a/apps/ui/playwright.config.ts b/apps/ui/playwright.config.ts index ba0b3482..f301fa30 100644 --- a/apps/ui/playwright.config.ts +++ b/apps/ui/playwright.config.ts @@ -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', }, diff --git a/apps/ui/scripts/setup-e2e-fixtures.mjs b/apps/ui/scripts/setup-e2e-fixtures.mjs index 62de432f..55424412 100644 --- a/apps/ui/scripts/setup-e2e-fixtures.mjs +++ b/apps/ui/scripts/setup-e2e-fixtures.mjs @@ -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 = ` Test Project A @@ -27,10 +32,153 @@ const SPEC_CONTENT = ` `; +// 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!'); } diff --git a/apps/ui/src/components/views/context-view.tsx b/apps/ui/src/components/views/context-view.tsx index ab33dbe8..41dc3816 100644 --- a/apps/ui/src/components/views/context-view.tsx +++ b/apps/ui/src/components/views/context-view.tsx @@ -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', + }); } }; diff --git a/apps/ui/src/components/views/welcome-view.tsx b/apps/ui/src/components/views/welcome-view.tsx index 33eb895c..b07c5188 100644 --- a/apps/ui/src/components/views/welcome-view.tsx +++ b/apps/ui/src/components/views/welcome-view.tsx @@ -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) { diff --git a/apps/ui/tests/settings/settings-startup-sync-race.spec.ts b/apps/ui/tests/settings/settings-startup-sync-race.spec.ts index 2cf43d44..1a5093f5 100644 --- a/apps/ui/tests/settings/settings-startup-sync-race.spec.ts +++ b/apps/ui/tests/settings/settings-startup-sync-race.spec.ts @@ -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=You’ve been logged out, text=Authentication Required') + .getByRole('heading', { name: /logged out/i }) + .or(page.locator('text=Authentication Required')) .first() .waitFor({ state: 'visible', timeout: 30000 }); diff --git a/apps/ui/tests/utils/core/interactions.ts b/apps/ui/tests/utils/core/interactions.ts index 22da6a18..9c52dd1f 100644 --- a/apps/ui/tests/utils/core/interactions.ts +++ b/apps/ui/tests/utils/core/interactions.ts @@ -20,11 +20,14 @@ export async function pressModifierEnter(page: Page): Promise { /** * 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 { // 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(); }