Files
automaker/apps/ui/tests/feature-lifecycle.spec.ts
Kacper 26236d3d5b feat: enhance ESLint configuration and improve component error handling
- Updated ESLint configuration to include support for `.mjs` and `.cjs` file types, adding necessary global variables for Node.js and browser environments.
- Introduced a new `vite-env.d.ts` file to define environment variables for Vite, improving type safety.
- Refactored error handling in `file-browser-dialog.tsx`, `description-image-dropzone.tsx`, and `feature-image-upload.tsx` to omit error parameters, simplifying the catch blocks.
- Removed unused bug report button functionality from the sidebar, streamlining the component structure.
- Adjusted various components to improve code readability and maintainability, including updates to type imports and component props.

These changes aim to enhance the development experience by improving linting support and simplifying error handling across components.
2025-12-21 23:08:08 +01:00

460 lines
19 KiB
TypeScript

/**
* Feature Lifecycle End-to-End Tests
*
* Tests the complete feature lifecycle flow:
* 1. Create a feature in backlog
* 2. Drag to in_progress and wait for agent to finish
* 3. Verify it moves to waiting_approval (manual review)
* 4. Click commit and verify git status shows committed changes
* 5. Drag to verified column
* 6. Archive (complete) the feature
* 7. Open archive modal and restore the feature
* 8. Delete the feature
*
* NOTE: This test uses AUTOMAKER_MOCK_AGENT=true to mock the agent
* so it doesn't make real API calls during CI/CD runs.
*/
import { test, expect } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
import { exec } from 'child_process';
import { promisify } from 'util';
import {
waitForNetworkIdle,
createTestGitRepo,
cleanupTempDir,
createTempDirPath,
setupProjectWithPathNoWorktrees,
waitForBoardView,
clickAddFeature,
confirmAddFeature,
dragAndDropWithDndKit,
} from './utils';
const execAsync = promisify(exec);
// Create unique temp dir for this test run
const TEST_TEMP_DIR = createTempDirPath('feature-lifecycle-tests');
interface TestRepo {
path: string;
cleanup: () => Promise<void>;
}
// Configure all tests to run serially
test.describe.configure({ mode: 'serial' });
test.describe('Feature Lifecycle Tests', () => {
let testRepo: TestRepo;
let featureId: string;
test.beforeAll(async () => {
// Create test temp directory
if (!fs.existsSync(TEST_TEMP_DIR)) {
fs.mkdirSync(TEST_TEMP_DIR, { recursive: true });
}
});
test.beforeEach(async () => {
// Create a fresh test repo for each test
testRepo = await createTestGitRepo(TEST_TEMP_DIR);
});
test.afterEach(async () => {
// Cleanup test repo after each test
if (testRepo) {
await testRepo.cleanup();
}
});
test.afterAll(async () => {
// Cleanup temp directory
cleanupTempDir(TEST_TEMP_DIR);
});
// this one fails in github actions for some reason
test.skip('complete feature lifecycle: create -> in_progress -> waiting_approval -> commit -> verified -> archive -> restore -> delete', async ({
page,
}) => {
// Increase timeout for this comprehensive test
test.setTimeout(120000);
// ==========================================================================
// Step 1: Setup and create a feature in backlog
// ==========================================================================
// Use no-worktrees setup to avoid worktree-related filtering/initialization issues
await setupProjectWithPathNoWorktrees(page, testRepo.path);
await page.goto('/');
await waitForNetworkIdle(page);
await waitForBoardView(page);
// Wait a bit for the UI to fully load
await page.waitForTimeout(1000);
// Click add feature button
await clickAddFeature(page);
// Fill in the feature details - requesting a file with "yellow" content
const featureDescription = 'Create a file named yellow.txt that contains the text yellow';
const descriptionInput = page.locator('[data-testid="add-feature-dialog"] textarea').first();
await descriptionInput.fill(featureDescription);
// Confirm the feature creation
await confirmAddFeature(page);
// Debug: Check the filesystem to see if feature was created
const featuresDir = path.join(testRepo.path, '.automaker', 'features');
// Wait for the feature to be created in the filesystem
await expect(async () => {
const dirs = fs.readdirSync(featuresDir);
expect(dirs.length).toBeGreaterThan(0);
}).toPass({ timeout: 10000 });
// Reload to force features to load from filesystem
await page.reload();
await waitForNetworkIdle(page);
await waitForBoardView(page);
// Wait for the feature card to appear on the board
const featureCard = page.getByText(featureDescription).first();
await expect(featureCard).toBeVisible({ timeout: 15000 });
// Get the feature ID from the filesystem
const featureDirs = fs.readdirSync(featuresDir);
featureId = featureDirs[0];
// Now get the actual card element by testid
const featureCardByTestId = page.locator(`[data-testid="kanban-card-${featureId}"]`);
await expect(featureCardByTestId).toBeVisible({ timeout: 10000 });
// ==========================================================================
// Step 2: Drag feature to in_progress and wait for agent to finish
// ==========================================================================
const dragHandle = page.locator(`[data-testid="drag-handle-${featureId}"]`);
const inProgressColumn = page.locator('[data-testid="kanban-column-in_progress"]');
// Perform the drag and drop using dnd-kit compatible method
await dragAndDropWithDndKit(page, dragHandle, inProgressColumn);
// First verify that the drag succeeded by checking for in_progress status
// This helps diagnose if the drag-drop is working or not
await expect(async () => {
const featureData = JSON.parse(
fs.readFileSync(path.join(featuresDir, featureId, 'feature.json'), 'utf-8')
);
// Feature should be either in_progress (agent running) or waiting_approval (agent done)
expect(['in_progress', 'waiting_approval']).toContain(featureData.status);
}).toPass({ timeout: 15000 });
// The mock agent should complete quickly (about 1.3 seconds based on the sleep times)
// Wait for the feature to move to waiting_approval (manual review)
// The status changes are: in_progress -> waiting_approval after agent completes
await expect(async () => {
const featureData = JSON.parse(
fs.readFileSync(path.join(featuresDir, featureId, 'feature.json'), 'utf-8')
);
expect(featureData.status).toBe('waiting_approval');
}).toPass({ timeout: 30000 });
// Refresh page to ensure UI reflects the status change
await page.reload();
await waitForNetworkIdle(page);
await waitForBoardView(page);
// ==========================================================================
// Step 3: Verify feature is in waiting_approval (manual review) column
// ==========================================================================
const waitingApprovalColumn = page.locator('[data-testid="kanban-column-waiting_approval"]');
const cardInWaitingApproval = waitingApprovalColumn.locator(
`[data-testid="kanban-card-${featureId}"]`
);
await expect(cardInWaitingApproval).toBeVisible({ timeout: 10000 });
// Verify the mock agent created the yellow.txt file
const yellowFilePath = path.join(testRepo.path, 'yellow.txt');
expect(fs.existsSync(yellowFilePath)).toBe(true);
const yellowContent = fs.readFileSync(yellowFilePath, 'utf-8');
expect(yellowContent).toBe('yellow');
// ==========================================================================
// Step 4: Click commit and verify git status shows committed changes
// ==========================================================================
// The commit button should be visible on the card in waiting_approval
const commitButton = page.locator(`[data-testid="commit-${featureId}"]`);
await expect(commitButton).toBeVisible({ timeout: 5000 });
await commitButton.click();
// Wait for the commit to process
await page.waitForTimeout(2000);
// Verify git status shows clean (changes committed)
const { stdout: gitStatus } = await execAsync('git status --porcelain', {
cwd: testRepo.path,
});
// After commit, the yellow.txt file should be committed, so git status should be clean
// (only .automaker directory might have changes)
expect(gitStatus.includes('yellow.txt')).toBe(false);
// Verify the commit exists in git log
const { stdout: gitLog } = await execAsync('git log --oneline -1', {
cwd: testRepo.path,
});
expect(gitLog.toLowerCase()).toContain('yellow');
// ==========================================================================
// Step 5: Verify feature moved to verified column after commit
// ==========================================================================
// Feature should automatically move to verified after commit
await page.reload();
await waitForNetworkIdle(page);
await waitForBoardView(page);
const verifiedColumn = page.locator('[data-testid="kanban-column-verified"]');
const cardInVerified = verifiedColumn.locator(`[data-testid="kanban-card-${featureId}"]`);
await expect(cardInVerified).toBeVisible({ timeout: 10000 });
// ==========================================================================
// Step 6: Archive (complete) the feature
// ==========================================================================
// Click the Complete button on the verified card
const completeButton = page.locator(`[data-testid="complete-${featureId}"]`);
await expect(completeButton).toBeVisible({ timeout: 5000 });
await completeButton.click();
// Wait for the archive action to complete
await page.waitForTimeout(1000);
// Verify the feature is no longer visible on the board (it's archived)
await expect(cardInVerified).not.toBeVisible({ timeout: 5000 });
// Verify feature status is completed in filesystem
const featureData = JSON.parse(
fs.readFileSync(path.join(featuresDir, featureId, 'feature.json'), 'utf-8')
);
expect(featureData.status).toBe('completed');
// ==========================================================================
// Step 7: Open archive modal and restore the feature
// ==========================================================================
// Click the completed features button to open the archive modal
const completedFeaturesButton = page.locator('[data-testid="completed-features-button"]');
await expect(completedFeaturesButton).toBeVisible({ timeout: 5000 });
await completedFeaturesButton.click();
// Wait for the modal to open
const completedModal = page.locator('[data-testid="completed-features-modal"]');
await expect(completedModal).toBeVisible({ timeout: 5000 });
// Verify the archived feature is shown in the modal
const archivedCard = completedModal.locator(`[data-testid="completed-card-${featureId}"]`);
await expect(archivedCard).toBeVisible({ timeout: 5000 });
// Click the restore button
const restoreButton = page.locator(`[data-testid="unarchive-${featureId}"]`);
await expect(restoreButton).toBeVisible({ timeout: 5000 });
await restoreButton.click();
// Wait for the restore action to complete
await page.waitForTimeout(1000);
// Close the modal - use first() to select the footer Close button, not the X button
const closeButton = completedModal.locator('button:has-text("Close")').first();
await closeButton.click();
await expect(completedModal).not.toBeVisible({ timeout: 5000 });
// Verify the feature is back in the verified column
const restoredCard = verifiedColumn.locator(`[data-testid="kanban-card-${featureId}"]`);
await expect(restoredCard).toBeVisible({ timeout: 10000 });
// Verify feature status is verified in filesystem
const restoredFeatureData = JSON.parse(
fs.readFileSync(path.join(featuresDir, featureId, 'feature.json'), 'utf-8')
);
expect(restoredFeatureData.status).toBe('verified');
// ==========================================================================
// Step 8: Delete the feature and verify it's removed
// ==========================================================================
// Click the delete button on the verified card
const deleteButton = page.locator(`[data-testid="delete-verified-${featureId}"]`);
await expect(deleteButton).toBeVisible({ timeout: 5000 });
await deleteButton.click();
// Wait for the confirmation dialog
const confirmDialog = page.locator('[data-testid="delete-confirmation-dialog"]');
await expect(confirmDialog).toBeVisible({ timeout: 5000 });
// Click the confirm delete button
const confirmDeleteButton = page.locator('[data-testid="confirm-delete-button"]');
await confirmDeleteButton.click();
// Wait for the delete action to complete
await page.waitForTimeout(1000);
// Verify the feature is no longer visible on the board
await expect(restoredCard).not.toBeVisible({ timeout: 5000 });
// Verify the feature directory is deleted from filesystem
const featureDirExists = fs.existsSync(path.join(featuresDir, featureId));
expect(featureDirExists).toBe(false);
});
// this one fails in github actions for some reason
test.skip("stop and restart feature: create -> in_progress -> stop -> restart should work without 'Feature not found' error", async ({
page,
}) => {
// This test verifies that stopping a feature and restarting it works correctly
// Bug: Previously, stopping a feature and immediately restarting could cause
// "Feature not found" error due to race conditions
test.setTimeout(120000);
// ==========================================================================
// Step 1: Setup and create a feature in backlog
// ==========================================================================
await setupProjectWithPathNoWorktrees(page, testRepo.path);
await page.goto('/');
await waitForNetworkIdle(page);
await waitForBoardView(page);
await page.waitForTimeout(1000);
// Click add feature button
await clickAddFeature(page);
// Fill in the feature details
const featureDescription = 'Create a file named test-restart.txt';
const descriptionInput = page.locator('[data-testid="add-feature-dialog"] textarea').first();
await descriptionInput.fill(featureDescription);
// Confirm the feature creation
await confirmAddFeature(page);
// Wait for the feature to be created in the filesystem
const featuresDir = path.join(testRepo.path, '.automaker', 'features');
await expect(async () => {
const dirs = fs.readdirSync(featuresDir);
expect(dirs.length).toBeGreaterThan(0);
}).toPass({ timeout: 10000 });
// Get the feature ID
const featureDirs = fs.readdirSync(featuresDir);
const testFeatureId = featureDirs[0];
// Reload to ensure features are loaded
await page.reload();
await waitForNetworkIdle(page);
await waitForBoardView(page);
// Wait for the feature card to appear
const featureCard = page.locator(`[data-testid="kanban-card-${testFeatureId}"]`);
await expect(featureCard).toBeVisible({ timeout: 10000 });
// ==========================================================================
// Step 2: Drag feature to in_progress (first start)
// ==========================================================================
const dragHandle = page.locator(`[data-testid="drag-handle-${testFeatureId}"]`);
const inProgressColumn = page.locator('[data-testid="kanban-column-in_progress"]');
await dragAndDropWithDndKit(page, dragHandle, inProgressColumn);
// Verify feature file still exists and is readable
const featureFilePath = path.join(featuresDir, testFeatureId, 'feature.json');
expect(fs.existsSync(featureFilePath)).toBe(true);
// First verify that the drag succeeded by checking for in_progress status
await expect(async () => {
const featureData = JSON.parse(fs.readFileSync(featureFilePath, 'utf-8'));
// Feature should be either in_progress (agent running) or waiting_approval (agent done)
expect(['in_progress', 'waiting_approval']).toContain(featureData.status);
}).toPass({ timeout: 15000 });
// ==========================================================================
// Step 3: Wait for the mock agent to complete (it's fast in mock mode)
// ==========================================================================
// The mock agent completes quickly, so we wait for it to finish
await expect(async () => {
const featureData = JSON.parse(fs.readFileSync(featureFilePath, 'utf-8'));
expect(featureData.status).toBe('waiting_approval');
}).toPass({ timeout: 30000 });
// Verify feature file still exists after completion
expect(fs.existsSync(featureFilePath)).toBe(true);
const featureDataAfterComplete = JSON.parse(fs.readFileSync(featureFilePath, 'utf-8'));
console.log('Feature status after first run:', featureDataAfterComplete.status);
// Reload to ensure clean state
await page.reload();
await waitForNetworkIdle(page);
await waitForBoardView(page);
// ==========================================================================
// Step 4: Move feature back to backlog to simulate stop scenario
// ==========================================================================
// Feature is in waiting_approval, drag it back to backlog
const backlogColumn = page.locator('[data-testid="kanban-column-backlog"]');
const currentCard = page.locator(`[data-testid="kanban-card-${testFeatureId}"]`);
const currentDragHandle = page.locator(`[data-testid="drag-handle-${testFeatureId}"]`);
await expect(currentCard).toBeVisible({ timeout: 10000 });
await dragAndDropWithDndKit(page, currentDragHandle, backlogColumn);
await page.waitForTimeout(500);
// Verify feature is in backlog
await expect(async () => {
const data = JSON.parse(fs.readFileSync(featureFilePath, 'utf-8'));
expect(data.status).toBe('backlog');
}).toPass({ timeout: 10000 });
// Reload to ensure clean state
await page.reload();
await waitForNetworkIdle(page);
await waitForBoardView(page);
// ==========================================================================
// Step 5: Restart the feature (drag to in_progress again)
// ==========================================================================
const restartCard = page.locator(`[data-testid="kanban-card-${testFeatureId}"]`);
await expect(restartCard).toBeVisible({ timeout: 10000 });
const restartDragHandle = page.locator(`[data-testid="drag-handle-${testFeatureId}"]`);
const inProgressColumnRestart = page.locator('[data-testid="kanban-column-in_progress"]');
// Listen for console errors to catch "Feature not found"
const consoleErrors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error') {
consoleErrors.push(msg.text());
}
});
// Drag to in_progress to restart
await dragAndDropWithDndKit(page, restartDragHandle, inProgressColumnRestart);
// Verify the feature file still exists
expect(fs.existsSync(featureFilePath)).toBe(true);
// First verify that the restart drag succeeded by checking for in_progress status
await expect(async () => {
const data = JSON.parse(fs.readFileSync(featureFilePath, 'utf-8'));
// Feature should be either in_progress (agent running) or waiting_approval (agent done)
expect(['in_progress', 'waiting_approval']).toContain(data.status);
}).toPass({ timeout: 15000 });
// Verify no "Feature not found" errors in console
const featureNotFoundErrors = consoleErrors.filter(
(err) => err.includes('not found') || err.includes('Feature')
);
expect(featureNotFoundErrors).toEqual([]);
// Wait for the mock agent to complete and move to waiting_approval
await expect(async () => {
const data = JSON.parse(fs.readFileSync(featureFilePath, 'utf-8'));
expect(data.status).toBe('waiting_approval');
}).toPass({ timeout: 30000 });
console.log('Feature successfully restarted after stop!');
});
});