Files
Maestro/e2e/fixtures/electron-app.ts
Pedram Amini bd7274797e MAESTRO: add Auto Run editing E2E tests (Task 6.2)
Created e2e/autorun-editing.spec.ts with 34 tests covering:
- Typing in edit mode (6 tests)
- Checkbox toggling (3 tests)
- Image paste and attachment (5 tests - skipped, need mocking)
- Mode switching (5 tests)
- Save and revert (3 tests)
- Search functionality (4 tests)
- Document selector (3 tests)
- Template variables (2 tests)
- Expanded modal (3 tests)
- Accessibility (3 tests)

Also expanded e2e/fixtures/electron-app.ts with 10 new Auto Run helper functions.
2025-12-14 05:05:49 -06:00

402 lines
10 KiB
TypeScript

/**
* Electron Application Fixture for E2E Testing
*
* This fixture handles launching and managing the Electron application
* for Playwright E2E tests. It provides utilities for interacting with
* the app's main window and IPC communication.
*/
import { test as base, _electron as electron, type ElectronApplication, type Page } from '@playwright/test';
import path from 'path';
import fs from 'fs';
import os from 'os';
// Interface for our extended test fixtures
interface ElectronTestFixtures {
electronApp: ElectronApplication;
window: Page;
appPath: string;
testDataDir: string;
}
/**
* Get the path to the Electron application
* In development, we use the built main process
* In CI/production, we could use the packaged app
*/
function getElectronPath(): string {
// For now, we run in development mode using the built main process
// The app must be built first: npm run build:main && npm run build:renderer
return require('electron') as unknown as string;
}
/**
* Get the path to the main entry point
*/
function getMainPath(): string {
return path.join(__dirname, '../../dist/main/index.js');
}
/**
* Create a unique test data directory for isolation
*/
function createTestDataDir(): string {
const testDir = path.join(os.tmpdir(), `maestro-e2e-${Date.now()}-${Math.random().toString(36).slice(2)}`);
fs.mkdirSync(testDir, { recursive: true });
return testDir;
}
/**
* Extended test with Electron fixtures
*
* Usage:
* ```typescript
* import { test, expect } from './fixtures/electron-app';
*
* test('should launch the app', async ({ electronApp, window }) => {
* await expect(window.locator('h1')).toBeVisible();
* });
* ```
*/
export const test = base.extend<ElectronTestFixtures>({
// Test data directory for isolation
testDataDir: async ({}, use) => {
const dir = createTestDataDir();
await use(dir);
// Cleanup after test
try {
fs.rmSync(dir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
},
// Path to the main entry point
appPath: async ({}, use) => {
const mainPath = getMainPath();
// Check if the app is built
if (!fs.existsSync(mainPath)) {
throw new Error(
`Electron main process not built. Run 'npm run build:main && npm run build:renderer' first.\n` +
`Expected path: ${mainPath}`
);
}
await use(mainPath);
},
// Launch Electron application
electronApp: async ({ appPath, testDataDir }, use) => {
const electronPath = getElectronPath();
// Launch the Electron app
const app = await electron.launch({
args: [appPath],
env: {
...process.env,
// Use isolated data directory for tests
MAESTRO_DATA_DIR: testDataDir,
// Disable hardware acceleration for CI
ELECTRON_DISABLE_GPU: '1',
// Set NODE_ENV to test
NODE_ENV: 'test',
// Ensure we're in a testing context
MAESTRO_E2E_TEST: 'true',
},
// Increase timeout for slow CI environments
timeout: 30000,
});
await use(app);
// Close the application after test
await app.close();
},
// Get the main window
window: async ({ electronApp }, use) => {
// Wait for the first window to be available
const window = await electronApp.firstWindow();
// Wait for the app to be ready (DOM loaded)
await window.waitForLoadState('domcontentloaded');
// Give the app a moment to initialize React
await window.waitForTimeout(500);
await use(window);
},
});
export { expect } from '@playwright/test';
/**
* Helper utilities for E2E tests
*/
export const helpers = {
/**
* Wait for the wizard to be visible
*/
async waitForWizard(window: Page): Promise<void> {
// The wizard modal should have a specific structure
// Looking for the wizard container or title
await window.waitForSelector('text=Create a Maestro Agent', { timeout: 10000 });
},
/**
* Open the wizard via keyboard shortcut
*/
async openWizardViaShortcut(window: Page): Promise<void> {
// Cmd+Shift+N opens the wizard
await window.keyboard.press('Meta+Shift+N');
await helpers.waitForWizard(window);
},
/**
* Select an agent in the wizard
*/
async selectAgent(window: Page, agentName: string): Promise<void> {
// Find and click the agent tile
const agentTile = window.locator(`text=${agentName}`).first();
await agentTile.click();
},
/**
* Enter a project name in the wizard
*/
async enterProjectName(window: Page, name: string): Promise<void> {
// Find the Name input field
const nameInput = window.locator('input[placeholder*="Project"]').or(
window.locator('input[placeholder*="Name"]')
);
await nameInput.fill(name);
},
/**
* Click the Next button in the wizard
*/
async clickNext(window: Page): Promise<void> {
const nextButton = window.locator('button:has-text("Next")').or(
window.locator('button:has-text("Continue")')
);
await nextButton.click();
},
/**
* Click the Back button in the wizard
*/
async clickBack(window: Page): Promise<void> {
const backButton = window.locator('button:has-text("Back")');
await backButton.click();
},
/**
* Select a directory in the wizard
* Note: This requires mocking the native dialog or using a pre-configured directory
*/
async selectDirectory(window: Page, dirPath: string): Promise<void> {
// The directory selection involves a native dialog
// For E2E tests, we might need to:
// 1. Mock the dialog result via IPC
// 2. Use a pre-selected directory
// 3. Set up the directory state before the test
// For now, we'll look for the directory input and interact with it
// This may need to be adjusted based on actual implementation
throw new Error('Directory selection requires dialog mocking - implement based on app specifics');
},
/**
* Wait for the wizard to close
*/
async waitForWizardClose(window: Page): Promise<void> {
// Wait for the wizard title to disappear
await window.waitForSelector('text=Create a Maestro Agent', {
state: 'hidden',
timeout: 10000,
});
},
/**
* Check if the app is showing the main UI
*/
async waitForMainUI(window: Page): Promise<void> {
// Wait for key elements of the main UI to be visible
// Adjust these selectors based on actual UI structure
await window.waitForSelector('[data-tour]', { timeout: 10000 }).catch(() => {
// data-tour attributes might not exist, try another approach
});
},
/**
* Create a temporary test directory structure
*/
createTestDirectory(basePath: string, structure: Record<string, string | null>): void {
for (const [relativePath, content] of Object.entries(structure)) {
const fullPath = path.join(basePath, relativePath);
const dir = path.dirname(fullPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
if (content !== null) {
fs.writeFileSync(fullPath, content, 'utf-8');
}
}
},
/**
* Clean up test directory
*/
cleanupTestDirectory(dirPath: string): void {
try {
fs.rmSync(dirPath, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
},
// ============================================
// Auto Run Helpers
// ============================================
/**
* Navigate to the Auto Run tab in the right panel
*/
async navigateToAutoRunTab(window: Page): Promise<boolean> {
const autoRunTab = window.locator('text=Auto Run');
if (await autoRunTab.count() > 0) {
await autoRunTab.first().click();
return true;
}
return false;
},
/**
* Switch Auto Run to edit mode
*/
async switchToEditMode(window: Page): Promise<boolean> {
const editButton = window.locator('button').filter({ hasText: 'Edit' });
if (await editButton.count() > 0 && await editButton.isVisible()) {
await editButton.first().click();
return true;
}
return false;
},
/**
* Switch Auto Run to preview mode
*/
async switchToPreviewMode(window: Page): Promise<boolean> {
const previewButton = window.locator('button').filter({ hasText: 'Preview' });
if (await previewButton.count() > 0 && await previewButton.isVisible()) {
await previewButton.first().click();
return true;
}
return false;
},
/**
* Get the Auto Run textarea element
*/
getAutoRunTextarea(window: Page) {
return window.locator('textarea');
},
/**
* Type content into the Auto Run editor
*/
async typeInAutoRunEditor(window: Page, content: string): Promise<boolean> {
const textarea = window.locator('textarea');
if (await textarea.count() > 0) {
await textarea.fill(content);
return true;
}
return false;
},
/**
* Get current content from Auto Run editor
*/
async getAutoRunContent(window: Page): Promise<string | null> {
const textarea = window.locator('textarea');
if (await textarea.count() > 0) {
return await textarea.inputValue();
}
return null;
},
/**
* Open the Auto Run expanded modal
*/
async openExpandedModal(window: Page): Promise<boolean> {
const expandButton = window.locator('button[title*="Expand"]').or(
window.locator('button[title*="full screen"]')
);
if (await expandButton.count() > 0) {
await expandButton.first().click();
return true;
}
return false;
},
/**
* Check if Auto Run is in edit mode
*/
async isInEditMode(window: Page): Promise<boolean> {
const textarea = window.locator('textarea');
return await textarea.count() > 0 && await textarea.isVisible();
},
/**
* Open search in Auto Run
*/
async openAutoRunSearch(window: Page): Promise<void> {
await window.keyboard.press('Meta+F');
},
/**
* Create an Auto Run test folder with sample documents
*/
createAutoRunTestFolder(basePath: string): string {
const autoRunFolder = path.join(basePath, 'Auto Run Docs');
fs.mkdirSync(autoRunFolder, { recursive: true });
// Create sample documents
fs.writeFileSync(
path.join(autoRunFolder, 'Phase 1.md'),
`# Phase 1: Setup
## Tasks
- [ ] Task 1: Initialize project
- [ ] Task 2: Configure environment
- [x] Task 3: Review documentation
## Notes
Sample content for testing Auto Run editing.
`
);
fs.writeFileSync(
path.join(autoRunFolder, 'Phase 2.md'),
`# Phase 2: Implementation
## Tasks
- [ ] Build feature A
- [ ] Build feature B
- [ ] Write tests
## Details
More content for the second phase.
`
);
return autoRunFolder;
},
};