mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
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:
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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!');
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user