mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +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',
|
process.env.AUTOMAKER_API_KEY || 'test-api-key-for-e2e-tests',
|
||||||
// Hide the API key banner to reduce log noise
|
// Hide the API key banner to reduce log noise
|
||||||
AUTOMAKER_HIDE_API_KEY: 'true',
|
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
|
// Simulate containerized environment to skip sandbox confirmation dialogs
|
||||||
IS_CONTAINERIZED: 'true',
|
IS_CONTAINERIZED: 'true',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,9 +3,11 @@
|
|||||||
/**
|
/**
|
||||||
* Setup script for E2E test fixtures
|
* Setup script for E2E test fixtures
|
||||||
* Creates the necessary test fixture directories and files before running Playwright tests
|
* 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 fs from 'fs';
|
||||||
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
@@ -16,6 +18,9 @@ const __dirname = path.dirname(__filename);
|
|||||||
const WORKSPACE_ROOT = path.resolve(__dirname, '../../..');
|
const WORKSPACE_ROOT = path.resolve(__dirname, '../../..');
|
||||||
const FIXTURE_PATH = path.join(WORKSPACE_ROOT, 'test/fixtures/projectA');
|
const FIXTURE_PATH = path.join(WORKSPACE_ROOT, 'test/fixtures/projectA');
|
||||||
const SPEC_FILE_PATH = path.join(FIXTURE_PATH, '.automaker/app_spec.txt');
|
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>
|
const SPEC_CONTENT = `<app_spec>
|
||||||
<name>Test Project A</name>
|
<name>Test Project A</name>
|
||||||
@@ -27,10 +32,153 @@ const SPEC_CONTENT = `<app_spec>
|
|||||||
</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() {
|
function setupFixtures() {
|
||||||
console.log('Setting up E2E test fixtures...');
|
console.log('Setting up E2E test fixtures...');
|
||||||
console.log(`Workspace root: ${WORKSPACE_ROOT}`);
|
console.log(`Workspace root: ${WORKSPACE_ROOT}`);
|
||||||
console.log(`Fixture path: ${FIXTURE_PATH}`);
|
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
|
// Create fixture directory
|
||||||
const specDir = path.dirname(SPEC_FILE_PATH);
|
const specDir = path.dirname(SPEC_FILE_PATH);
|
||||||
@@ -43,6 +191,15 @@ function setupFixtures() {
|
|||||||
fs.writeFileSync(SPEC_FILE_PATH, SPEC_CONTENT);
|
fs.writeFileSync(SPEC_FILE_PATH, SPEC_CONTENT);
|
||||||
console.log(`Created fixture file: ${SPEC_FILE_PATH}`);
|
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!');
|
console.log('E2E test fixtures setup complete!');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -496,6 +496,14 @@ export function ContextView() {
|
|||||||
setNewMarkdownContent('');
|
setNewMarkdownContent('');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to create markdown:', 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,
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
setShowInitDialog(true);
|
setShowInitDialog(true);
|
||||||
|
|
||||||
|
// Navigate to the board view (dialog shows as overlay)
|
||||||
|
navigate({ to: '/board' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to create project:', error);
|
logger.error('Failed to create project:', error);
|
||||||
toast.error('Failed to create project', {
|
toast.error('Failed to create project', {
|
||||||
@@ -418,6 +421,9 @@ export function WelcomeView() {
|
|||||||
});
|
});
|
||||||
setShowInitDialog(true);
|
setShowInitDialog(true);
|
||||||
|
|
||||||
|
// Navigate to the board view (dialog shows as overlay)
|
||||||
|
navigate({ to: '/board' });
|
||||||
|
|
||||||
// Kick off project analysis
|
// Kick off project analysis
|
||||||
analyzeProject(projectPath);
|
analyzeProject(projectPath);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -515,6 +521,9 @@ export function WelcomeView() {
|
|||||||
});
|
});
|
||||||
setShowInitDialog(true);
|
setShowInitDialog(true);
|
||||||
|
|
||||||
|
// Navigate to the board view (dialog shows as overlay)
|
||||||
|
navigate({ to: '/board' });
|
||||||
|
|
||||||
// Kick off project analysis
|
// Kick off project analysis
|
||||||
analyzeProject(projectPath);
|
analyzeProject(projectPath);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -120,13 +120,21 @@ test.describe('Settings startup sync race', () => {
|
|||||||
};
|
};
|
||||||
expect(beforeLogout.projects?.length).toBeGreaterThan(0);
|
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');
|
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();
|
await page.locator('[data-testid="logout-button"]').click();
|
||||||
|
|
||||||
// Ensure we landed on logged-out or login (either is acceptable).
|
// 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
|
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()
|
.first()
|
||||||
.waitFor({ state: 'visible', timeout: 30000 });
|
.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
|
* 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> {
|
export async function clickElement(page: Page, testId: string): Promise<void> {
|
||||||
// Wait for splash screen to disappear first (safety net)
|
// Wait for splash screen to disappear first (safety net)
|
||||||
await waitForSplashScreenToDisappear(page, 2000);
|
await waitForSplashScreenToDisappear(page, 5000);
|
||||||
const element = await getByTestId(page, testId);
|
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();
|
await element.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user