Merge branch 'main' of github.com:AutoMaker-Org/automaker into claude/task-dependency-graph-iPz1k

This commit is contained in:
Test User
2025-12-22 17:01:18 -05:00
25 changed files with 1147 additions and 5546 deletions

111
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,111 @@
name: Release Build
on:
release:
types: [published]
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Extract version from tag
id: version
shell: bash
run: |
# Remove 'v' prefix if present (e.g., "v1.2.3" -> "1.2.3")
VERSION="${{ github.event.release.tag_name }}"
VERSION="${VERSION#v}"
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "Extracted version: ${VERSION}"
- name: Update package.json version
shell: bash
run: |
node apps/ui/scripts/update-version.mjs "${{ steps.version.outputs.version }}"
- name: Setup project
uses: ./.github/actions/setup-project
with:
check-lockfile: 'true'
- name: Build Electron app (macOS)
if: matrix.os == 'macos-latest'
shell: bash
run: npm run build:electron:mac --workspace=apps/ui
env:
CSC_IDENTITY_AUTO_DISCOVERY: false
- name: Build Electron app (Windows)
if: matrix.os == 'windows-latest'
shell: bash
run: npm run build:electron:win --workspace=apps/ui
- name: Build Electron app (Linux)
if: matrix.os == 'ubuntu-latest'
shell: bash
run: npm run build:electron:linux --workspace=apps/ui
- name: Upload macOS artifacts
if: matrix.os == 'macos-latest'
uses: actions/upload-artifact@v4
with:
name: macos-builds
path: apps/ui/release/*.{dmg,zip}
retention-days: 30
- name: Upload Windows artifacts
if: matrix.os == 'windows-latest'
uses: actions/upload-artifact@v4
with:
name: windows-builds
path: apps/ui/release/*.exe
retention-days: 30
- name: Upload Linux artifacts
if: matrix.os == 'ubuntu-latest'
uses: actions/upload-artifact@v4
with:
name: linux-builds
path: apps/ui/release/*.{AppImage,deb}
retention-days: 30
upload:
needs: build
runs-on: ubuntu-latest
if: github.event.release.draft == false
steps:
- name: Download macOS artifacts
uses: actions/download-artifact@v4
with:
name: macos-builds
path: artifacts/macos-builds
- name: Download Windows artifacts
uses: actions/download-artifact@v4
with:
name: windows-builds
path: artifacts/windows-builds
- name: Download Linux artifacts
uses: actions/download-artifact@v4
with:
name: linux-builds
path: artifacts/linux-builds
- name: Upload to GitHub Release
uses: softprops/action-gh-release@v2
with:
files: |
artifacts/macos-builds/*
artifacts/windows-builds/*
artifacts/linux-builds/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -0,0 +1,46 @@
#!/usr/bin/env node
/**
* Updates the version in apps/ui/package.json
* Usage: node scripts/update-version.mjs <version>
* Example: node scripts/update-version.mjs 1.2.3
*/
import { readFileSync, writeFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const version = process.argv[2];
if (!version) {
console.error('Error: Version argument is required');
console.error('Usage: node scripts/update-version.mjs <version>');
process.exit(1);
}
// Remove 'v' prefix if present (e.g., "v1.2.3" -> "1.2.3")
const cleanVersion = version.startsWith('v') ? version.slice(1) : version;
// Validate version format (basic semver check)
if (!/^\d+\.\d+\.\d+/.test(cleanVersion)) {
console.error(`Error: Invalid version format: ${cleanVersion}`);
console.error('Expected format: X.Y.Z (e.g., 1.2.3)');
process.exit(1);
}
const packageJsonPath = join(__dirname, '..', 'package.json');
try {
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
const oldVersion = packageJson.version;
packageJson.version = cleanVersion;
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf8');
console.log(`Updated version from ${oldVersion} to ${cleanVersion}`);
} catch (error) {
console.error(`Error updating version: ${error.message}`);
process.exit(1);
}

View File

@@ -0,0 +1,89 @@
/**
* Start New Chat Session E2E Test
*
* Happy path: Start a new agent chat session
*/
import { test, expect } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
import {
createTempDirPath,
cleanupTempDir,
setupRealProject,
waitForNetworkIdle,
navigateToAgent,
clickNewSessionButton,
waitForNewSession,
countSessionItems,
} from '../utils';
const TEST_TEMP_DIR = createTempDirPath('agent-session-test');
test.describe('Agent Chat Session', () => {
let projectPath: string;
const projectName = `test-project-${Date.now()}`;
test.beforeAll(async () => {
if (!fs.existsSync(TEST_TEMP_DIR)) {
fs.mkdirSync(TEST_TEMP_DIR, { recursive: true });
}
projectPath = path.join(TEST_TEMP_DIR, projectName);
fs.mkdirSync(projectPath, { recursive: true });
fs.writeFileSync(
path.join(projectPath, 'package.json'),
JSON.stringify({ name: projectName, version: '1.0.0' }, null, 2)
);
const automakerDir = path.join(projectPath, '.automaker');
fs.mkdirSync(automakerDir, { recursive: true });
fs.mkdirSync(path.join(automakerDir, 'features'), { recursive: true });
fs.mkdirSync(path.join(automakerDir, 'context'), { recursive: true });
fs.mkdirSync(path.join(automakerDir, 'sessions'), { recursive: true });
fs.writeFileSync(
path.join(automakerDir, 'categories.json'),
JSON.stringify({ categories: [] }, null, 2)
);
fs.writeFileSync(
path.join(automakerDir, 'app_spec.txt'),
`# ${projectName}\n\nA test project for e2e testing.`
);
});
test.afterAll(async () => {
cleanupTempDir(TEST_TEMP_DIR);
});
test('should start a new agent chat session', async ({ page }) => {
await setupRealProject(page, projectPath, projectName, { setAsCurrent: true });
await page.goto('/');
await waitForNetworkIdle(page);
// Navigate to agent view
await navigateToAgent(page);
// Verify we're on the agent view
await expect(page.locator('[data-testid="agent-view"]')).toBeVisible({ timeout: 10000 });
// Click new session button
await clickNewSessionButton(page);
// Wait for new session to appear in the list
await waitForNewSession(page, { timeout: 10000 });
// Verify at least one session exists
const sessionCount = await countSessionItems(page);
expect(sessionCount).toBeGreaterThanOrEqual(1);
// Verify the message list is visible (indicates a session is selected)
await expect(page.locator('[data-testid="message-list"]')).toBeVisible({ timeout: 5000 });
// Verify the agent input is visible
await expect(page.locator('[data-testid="agent-input"]')).toBeVisible();
});
});

View File

@@ -1,642 +0,0 @@
import { test, expect } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
import {
resetContextDirectory,
createContextFileOnDisk,
contextFileExistsOnDisk,
setupProjectWithFixture,
getFixturePath,
navigateToContext,
waitForFileContentToLoad,
switchToEditMode,
waitForContextFile,
selectContextFile,
simulateFileDrop,
setContextEditorContent,
getContextEditorContent,
clickElement,
fillInput,
getByTestId,
waitForNetworkIdle,
} from './utils';
const WORKSPACE_ROOT = path.resolve(process.cwd(), '../..');
const TEST_IMAGE_SRC = path.join(WORKSPACE_ROOT, 'apps/ui/public/logo.png');
// Configure all tests to run serially to prevent interference with shared context directory
test.describe.configure({ mode: 'serial' });
// ============================================================================
// Test Suite 1: Context View - File Management
// ============================================================================
test.describe('Context View - File Management', () => {
test.beforeEach(async () => {
resetContextDirectory();
});
test.afterEach(async () => {
resetContextDirectory();
});
test('should create a new MD context file', async ({ page }) => {
await setupProjectWithFixture(page, getFixturePath());
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToContext(page);
// Click Create Markdown button
await clickElement(page, 'create-markdown-button');
await page.waitForSelector('[data-testid="create-markdown-dialog"]', {
timeout: 5000,
});
// Enter filename
await fillInput(page, 'new-markdown-name', 'test-context.md');
// Enter content
const testContent = '# Test Context\n\nThis is test content';
await fillInput(page, 'new-markdown-content', testContent);
// Click confirm
await clickElement(page, 'confirm-create-markdown');
// Wait for dialog to close
await page.waitForFunction(
() => !document.querySelector('[data-testid="create-markdown-dialog"]'),
{ timeout: 5000 }
);
// Wait for file list to refresh (file should appear)
await waitForContextFile(page, 'test-context.md', 10000);
// Verify file appears in list
const fileButton = await getByTestId(page, 'context-file-test-context.md');
await expect(fileButton).toBeVisible();
// Click on the file and wait for it to be selected
await selectContextFile(page, 'test-context.md');
// Wait for content to load
await waitForFileContentToLoad(page);
// Switch to edit mode if in preview mode (markdown files default to preview)
await switchToEditMode(page);
// Wait for editor to be visible
await page.waitForSelector('[data-testid="context-editor"]', {
timeout: 5000,
});
// Verify content in editor
const editorContent = await getContextEditorContent(page);
expect(editorContent).toBe(testContent);
});
test('should edit an existing MD context file', async ({ page }) => {
// Create a test file on disk first
const originalContent = '# Original Content\n\nThis will be edited.';
createContextFileOnDisk('edit-test.md', originalContent);
await setupProjectWithFixture(page, getFixturePath());
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToContext(page);
// Click on the existing file and wait for it to be selected
await selectContextFile(page, 'edit-test.md');
// Wait for file content to load
await waitForFileContentToLoad(page);
// Switch to edit mode (markdown files open in preview mode by default)
await switchToEditMode(page);
// Wait for editor
await page.waitForSelector('[data-testid="context-editor"]', {
timeout: 5000,
});
// Modify content
const newContent = '# Modified Content\n\nThis has been edited.';
await setContextEditorContent(page, newContent);
// Click save
await clickElement(page, 'save-context-file');
// Wait for save to complete
await page.waitForFunction(
() =>
document.querySelector('[data-testid="save-context-file"]')?.textContent?.includes('Saved'),
{ timeout: 5000 }
);
// Reload page
await page.reload();
await waitForNetworkIdle(page);
// Navigate back to context view
await navigateToContext(page);
// Wait for file to appear after reload and select it
await selectContextFile(page, 'edit-test.md');
// Wait for content to load
await waitForFileContentToLoad(page);
// Switch to edit mode (markdown files open in preview mode)
await switchToEditMode(page);
await page.waitForSelector('[data-testid="context-editor"]', {
timeout: 5000,
});
// Verify content persisted
const persistedContent = await getContextEditorContent(page);
expect(persistedContent).toBe(newContent);
});
test('should remove an MD context file', async ({ page }) => {
// Create a test file on disk first
createContextFileOnDisk('delete-test.md', '# Delete Me');
await setupProjectWithFixture(page, getFixturePath());
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToContext(page);
// Click on the file to select it
const fileButton = await getByTestId(page, 'context-file-delete-test.md');
await fileButton.waitFor({ state: 'visible', timeout: 5000 });
await fileButton.click();
// Click delete button
await clickElement(page, 'delete-context-file');
// Wait for delete dialog
await page.waitForSelector('[data-testid="delete-context-dialog"]', {
timeout: 5000,
});
// Confirm deletion
await clickElement(page, 'confirm-delete-file');
// Wait for dialog to close
await page.waitForFunction(
() => !document.querySelector('[data-testid="delete-context-dialog"]'),
{ timeout: 5000 }
);
// Verify file is removed from list
const deletedFile = await getByTestId(page, 'context-file-delete-test.md');
await expect(deletedFile).not.toBeVisible();
// Verify file is removed from disk
expect(contextFileExistsOnDisk('delete-test.md')).toBe(false);
});
test('should upload an image context file', async ({ page }) => {
await setupProjectWithFixture(page, getFixturePath());
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToContext(page);
// Use the hidden file input to upload an image directly
// The "Import File" button triggers this input
const fileInput = page.locator('[data-testid="file-import-input"]');
await fileInput.setInputFiles(TEST_IMAGE_SRC);
// Wait for file to appear in the list (filename is extracted from path)
await waitForContextFile(page, 'logo.png', 10000);
// Verify file appears in list
const fileButton = await getByTestId(page, 'context-file-logo.png');
await expect(fileButton).toBeVisible();
// Click on the image to view it
await fileButton.click();
// Verify image preview is displayed
await page.waitForSelector('[data-testid="image-preview"]', {
timeout: 5000,
});
const imagePreview = await getByTestId(page, 'image-preview');
await expect(imagePreview).toBeVisible();
});
test('should remove an image context file', async ({ page }) => {
// Create a test image file on disk as base64 data URL (matching app's storage format)
const imageContent = fs.readFileSync(TEST_IMAGE_SRC);
const base64DataUrl = `data:image/png;base64,${imageContent.toString('base64')}`;
const contextPath = path.join(getFixturePath(), '.automaker/context');
fs.writeFileSync(path.join(contextPath, 'delete-image.png'), base64DataUrl);
await setupProjectWithFixture(page, getFixturePath());
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToContext(page);
// Wait for the image file and select it
await selectContextFile(page, 'delete-image.png');
// Wait for file content (image preview) to load
await waitForFileContentToLoad(page);
// Click delete button
await clickElement(page, 'delete-context-file');
// Wait for delete dialog
await page.waitForSelector('[data-testid="delete-context-dialog"]', {
timeout: 5000,
});
// Confirm deletion
await clickElement(page, 'confirm-delete-file');
// Wait for dialog to close
await page.waitForFunction(
() => !document.querySelector('[data-testid="delete-context-dialog"]'),
{ timeout: 5000 }
);
// Verify file is removed from list
const deletedImageFile = await getByTestId(page, 'context-file-delete-image.png');
await expect(deletedImageFile).not.toBeVisible();
});
test('should toggle markdown preview mode', async ({ page }) => {
// Create a markdown file with content
const mdContent =
'# Heading\n\n**Bold text** and *italic text*\n\n- List item 1\n- List item 2';
createContextFileOnDisk('preview-test.md', mdContent);
await setupProjectWithFixture(page, getFixturePath());
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToContext(page);
// Click on the markdown file
const fileButton = await getByTestId(page, 'context-file-preview-test.md');
await fileButton.waitFor({ state: 'visible', timeout: 5000 });
await fileButton.click();
// Wait for content to load (markdown files open in preview mode by default)
await waitForFileContentToLoad(page);
// Check if preview button is visible (indicates it's a markdown file)
const previewToggle = await getByTestId(page, 'toggle-preview-mode');
await expect(previewToggle).toBeVisible();
// Markdown files always open in preview mode by default (see context-view.tsx:163)
// Verify we're in preview mode
const markdownPreview = await getByTestId(page, 'markdown-preview');
await expect(markdownPreview).toBeVisible();
// Click to switch to edit mode
await previewToggle.click();
await page.waitForSelector('[data-testid="context-editor"]', {
timeout: 5000,
});
// Verify editor is shown
const editor = await getByTestId(page, 'context-editor');
await expect(editor).toBeVisible();
await expect(markdownPreview).not.toBeVisible();
// Click to switch back to preview mode
await previewToggle.click();
await page.waitForSelector('[data-testid="markdown-preview"]', {
timeout: 5000,
});
// Verify preview is shown
await expect(markdownPreview).toBeVisible();
});
});
// ============================================================================
// Test Suite 2: Context View - Drag and Drop
// ============================================================================
test.describe('Context View - Drag and Drop', () => {
test.beforeEach(async () => {
resetContextDirectory();
});
test.afterEach(async () => {
resetContextDirectory();
});
test('should handle drag and drop of MD file onto textarea in add dialog', async ({ page }) => {
await setupProjectWithFixture(page, getFixturePath());
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToContext(page);
// Open create markdown dialog
await clickElement(page, 'create-markdown-button');
await page.waitForSelector('[data-testid="create-markdown-dialog"]', {
timeout: 5000,
});
// Simulate drag and drop of a .md file onto the textarea
const droppedContent = '# Dropped Content\n\nThis was dragged and dropped.';
await simulateFileDrop(
page,
'[data-testid="new-markdown-content"]',
'dropped-file.md',
droppedContent
);
// Wait for content to be populated in textarea
const textarea = await getByTestId(page, 'new-markdown-content');
await textarea.waitFor({ state: 'visible' });
await expect(textarea).toHaveValue(droppedContent);
// Verify content is populated in textarea
const textareaContent = await textarea.inputValue();
expect(textareaContent).toBe(droppedContent);
// Verify filename is auto-filled
const filenameValue = await page.locator('[data-testid="new-markdown-name"]').inputValue();
expect(filenameValue).toBe('dropped-file.md');
// Confirm and create the file
await clickElement(page, 'confirm-create-markdown');
// Wait for dialog to close
await page.waitForFunction(
() => !document.querySelector('[data-testid="create-markdown-dialog"]'),
{ timeout: 5000 }
);
// Verify file was created
const droppedFile = await getByTestId(page, 'context-file-dropped-file.md');
await expect(droppedFile).toBeVisible();
});
test('should handle drag and drop of file onto main view', async ({ page }) => {
await setupProjectWithFixture(page, getFixturePath());
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToContext(page);
// Wait for the context view to be fully loaded
await page.waitForSelector('[data-testid="context-file-list"]', {
timeout: 5000,
});
// Simulate drag and drop onto the drop zone
const droppedContent = 'This is a text file dropped onto the main view.';
await simulateFileDrop(
page,
'[data-testid="context-drop-zone"]',
'main-drop.txt',
droppedContent
);
// Wait for file to appear in the list (drag-drop triggers file creation)
await waitForContextFile(page, 'main-drop.txt', 15000);
// Verify file appears in the file list
const fileButton = await getByTestId(page, 'context-file-main-drop.txt');
await expect(fileButton).toBeVisible();
// Select file and verify content
await fileButton.click();
await page.waitForSelector('[data-testid="context-editor"]', {
timeout: 5000,
});
const editorContent = await getContextEditorContent(page);
expect(editorContent).toBe(droppedContent);
});
});
// ============================================================================
// Test Suite 3: Context View - Edge Cases
// ============================================================================
test.describe('Context View - Edge Cases', () => {
test.beforeEach(async () => {
resetContextDirectory();
});
test.afterEach(async () => {
resetContextDirectory();
});
test('should handle duplicate filename (overwrite behavior)', async ({ page }) => {
// Create an existing file
createContextFileOnDisk('test.md', '# Original Content');
await setupProjectWithFixture(page, getFixturePath());
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToContext(page);
// Verify the original file exists
const originalFile = await getByTestId(page, 'context-file-test.md');
await expect(originalFile).toBeVisible();
// Try to create another file with the same name
await clickElement(page, 'create-markdown-button');
await page.waitForSelector('[data-testid="create-markdown-dialog"]', {
timeout: 5000,
});
await fillInput(page, 'new-markdown-name', 'test.md');
await fillInput(page, 'new-markdown-content', '# New Content - Overwritten');
await clickElement(page, 'confirm-create-markdown');
// Wait for dialog to close
await page.waitForFunction(
() => !document.querySelector('[data-testid="create-markdown-dialog"]'),
{ timeout: 5000 }
);
// File should still exist (was overwritten)
await expect(originalFile).toBeVisible();
// Select the file and verify the new content
await originalFile.click();
// Wait for content to load
await waitForFileContentToLoad(page);
// Switch to edit mode (markdown files open in preview mode)
await switchToEditMode(page);
await page.waitForSelector('[data-testid="context-editor"]', {
timeout: 5000,
});
const editorContent = await getContextEditorContent(page);
expect(editorContent).toBe('# New Content - Overwritten');
});
test('should handle special characters in filename', async ({ page }) => {
await setupProjectWithFixture(page, getFixturePath());
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToContext(page);
// Test file with parentheses
await clickElement(page, 'create-markdown-button');
await page.waitForSelector('[data-testid="create-markdown-dialog"]', {
timeout: 5000,
});
await fillInput(page, 'new-markdown-name', 'context (1).md');
await fillInput(page, 'new-markdown-content', 'Content with parentheses in filename');
await clickElement(page, 'confirm-create-markdown');
await page.waitForFunction(
() => !document.querySelector('[data-testid="create-markdown-dialog"]'),
{ timeout: 5000 }
);
// Verify file is created - use CSS escape for special characters
const fileWithParens = await getByTestId(page, 'context-file-context (1).md');
await expect(fileWithParens).toBeVisible();
// Test file with hyphens and underscores
await clickElement(page, 'create-markdown-button');
await page.waitForSelector('[data-testid="create-markdown-dialog"]', {
timeout: 5000,
});
await fillInput(page, 'new-markdown-name', 'test-file_v2.md');
await fillInput(page, 'new-markdown-content', 'Content with hyphens and underscores');
await clickElement(page, 'confirm-create-markdown');
await page.waitForFunction(
() => !document.querySelector('[data-testid="create-markdown-dialog"]'),
{ timeout: 5000 }
);
// Verify file is created
const fileWithHyphens = await getByTestId(page, 'context-file-test-file_v2.md');
await expect(fileWithHyphens).toBeVisible();
// Verify both files are accessible
await fileWithHyphens.click();
// Wait for content to load
await waitForFileContentToLoad(page);
// Switch to edit mode (markdown files open in preview mode)
await switchToEditMode(page);
await page.waitForSelector('[data-testid="context-editor"]', {
timeout: 5000,
});
const content = await getContextEditorContent(page);
expect(content).toBe('Content with hyphens and underscores');
});
test('should handle empty content', async ({ page }) => {
await setupProjectWithFixture(page, getFixturePath());
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToContext(page);
// Create file with empty content
await clickElement(page, 'create-markdown-button');
await page.waitForSelector('[data-testid="create-markdown-dialog"]', {
timeout: 5000,
});
await fillInput(page, 'new-markdown-name', 'empty-file.md');
// Don't fill any content - leave it empty
await clickElement(page, 'confirm-create-markdown');
await page.waitForFunction(
() => !document.querySelector('[data-testid="create-markdown-dialog"]'),
{ timeout: 5000 }
);
// Verify file is created
const emptyFile = await getByTestId(page, 'context-file-empty-file.md');
await expect(emptyFile).toBeVisible();
// Select file and verify editor shows empty content
await emptyFile.click();
// Wait for content to load
await waitForFileContentToLoad(page);
// Switch to edit mode (markdown files open in preview mode)
await switchToEditMode(page);
await page.waitForSelector('[data-testid="context-editor"]', {
timeout: 5000,
});
const editorContent = await getContextEditorContent(page);
expect(editorContent).toBe('');
// Verify save works with empty content
// The save button should be disabled when there are no changes
// Let's add some content first, then clear it and save
await setContextEditorContent(page, 'temporary');
await setContextEditorContent(page, '');
// Save should work
await clickElement(page, 'save-context-file');
await page.waitForFunction(
() =>
document.querySelector('[data-testid="save-context-file"]')?.textContent?.includes('Saved'),
{ timeout: 5000 }
);
});
test('should verify persistence across page refresh', async ({ page }) => {
// Create a file directly on disk to ensure it persists across refreshes
const testContent = '# Persistence Test\n\nThis content should persist.';
createContextFileOnDisk('persist-test.md', testContent);
await setupProjectWithFixture(page, getFixturePath());
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToContext(page);
// Verify file exists before refresh
await waitForContextFile(page, 'persist-test.md', 10000);
// Refresh the page
await page.reload();
await waitForNetworkIdle(page);
// Navigate back to context view
await navigateToContext(page);
// Select the file after refresh (uses robust clicking mechanism)
await selectContextFile(page, 'persist-test.md');
// Wait for file content to load
await waitForFileContentToLoad(page);
// Switch to edit mode (markdown files open in preview mode)
await switchToEditMode(page);
await page.waitForSelector('[data-testid="context-editor"]', {
timeout: 5000,
});
const persistedContent = await getContextEditorContent(page);
expect(persistedContent).toBe(testContent);
});
});

View File

@@ -0,0 +1,146 @@
/**
* Add Context Image E2E Test
*
* Happy path: Import an image file to the context via the UI
*/
import { test, expect } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
import {
resetContextDirectory,
setupProjectWithFixture,
getFixturePath,
navigateToContext,
waitForContextFile,
waitForNetworkIdle,
} from '../utils';
test.describe('Add Context Image', () => {
let testImagePath: string;
test.beforeAll(async () => {
// Create a simple test image (1x1 red PNG)
const fixturePath = getFixturePath();
testImagePath = path.join(fixturePath, '..', 'test-image.png');
// Create a minimal PNG (1x1 pixel red image)
const pngHeader = Buffer.from([
0x89,
0x50,
0x4e,
0x47,
0x0d,
0x0a,
0x1a,
0x0a, // PNG signature
0x00,
0x00,
0x00,
0x0d, // IHDR chunk length
0x49,
0x48,
0x44,
0x52, // IHDR
0x00,
0x00,
0x00,
0x01, // width: 1
0x00,
0x00,
0x00,
0x01, // height: 1
0x08,
0x02, // bit depth: 8, color type: 2 (RGB)
0x00,
0x00,
0x00, // compression, filter, interlace
0x90,
0x77,
0x53,
0xde, // IHDR CRC
0x00,
0x00,
0x00,
0x0c, // IDAT chunk length
0x49,
0x44,
0x41,
0x54, // IDAT
0x08,
0xd7,
0x63,
0xf8,
0xcf,
0xc0,
0x00,
0x00,
0x01,
0x01,
0x01,
0x00, // compressed data
0x18,
0xdd,
0x8d,
0xb4, // IDAT CRC
0x00,
0x00,
0x00,
0x00, // IEND chunk length
0x49,
0x45,
0x4e,
0x44, // IEND
0xae,
0x42,
0x60,
0x82, // IEND CRC
]);
fs.writeFileSync(testImagePath, pngHeader);
});
test.beforeEach(async () => {
resetContextDirectory();
});
test.afterEach(async () => {
resetContextDirectory();
});
test.afterAll(async () => {
// Clean up test image
if (fs.existsSync(testImagePath)) {
fs.unlinkSync(testImagePath);
}
});
test('should import an image file to context', async ({ page }) => {
await setupProjectWithFixture(page, getFixturePath());
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToContext(page);
// Get the file input element and set the file
const fileInput = page.locator('[data-testid="file-import-input"]');
// Use setInputFiles to upload the image
await fileInput.setInputFiles(testImagePath);
// Wait for the file to appear in the list (filename should be the base name)
const fileName = path.basename(testImagePath);
await waitForContextFile(page, fileName, 15000);
// Verify the file appears in the list
const fileButton = page.locator(`[data-testid="context-file-${fileName}"]`);
await expect(fileButton).toBeVisible();
// Verify the file exists on disk
const fixturePath = getFixturePath();
const contextImagePath = path.join(fixturePath, '.automaker', 'context', fileName);
await expect(async () => {
expect(fs.existsSync(contextImagePath)).toBe(true);
}).toPass({ timeout: 5000 });
});
});

View File

@@ -0,0 +1,67 @@
/**
* Context File Management E2E Test
*
* Happy path: Create a markdown context file
*/
import { test, expect } from '@playwright/test';
import {
resetContextDirectory,
setupProjectWithFixture,
getFixturePath,
navigateToContext,
waitForFileContentToLoad,
switchToEditMode,
waitForContextFile,
clickElement,
fillInput,
getByTestId,
waitForNetworkIdle,
getContextEditorContent,
} from '../utils';
test.describe('Context File Management', () => {
test.beforeEach(async () => {
resetContextDirectory();
});
test.afterEach(async () => {
resetContextDirectory();
});
test('should create a new markdown context file', async ({ page }) => {
await setupProjectWithFixture(page, getFixturePath());
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToContext(page);
await clickElement(page, 'create-markdown-button');
await page.waitForSelector('[data-testid="create-markdown-dialog"]', { timeout: 5000 });
await fillInput(page, 'new-markdown-name', 'test-context.md');
const testContent = '# Test Context\n\nThis is test content';
await fillInput(page, 'new-markdown-content', testContent);
await clickElement(page, 'confirm-create-markdown');
await page.waitForFunction(
() => !document.querySelector('[data-testid="create-markdown-dialog"]'),
{ timeout: 5000 }
);
await waitForContextFile(page, 'test-context.md', 10000);
const fileButton = await getByTestId(page, 'context-file-test-context.md');
await expect(fileButton).toBeVisible();
await fileButton.click();
await waitForFileContentToLoad(page);
await switchToEditMode(page);
await page.waitForSelector('[data-testid="context-editor"]', { timeout: 5000 });
const editorContent = await getContextEditorContent(page);
expect(editorContent).toBe(testContent);
});
});

View File

@@ -0,0 +1,75 @@
/**
* Delete Context File E2E Test
*
* Happy path: Delete a context file via the UI
*/
import { test, expect } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
import {
resetContextDirectory,
setupProjectWithFixture,
getFixturePath,
navigateToContext,
waitForContextFile,
selectContextFile,
deleteSelectedContextFile,
clickElement,
fillInput,
waitForNetworkIdle,
} from '../utils';
test.describe('Delete Context File', () => {
test.beforeEach(async () => {
resetContextDirectory();
});
test.afterEach(async () => {
resetContextDirectory();
});
test('should delete a context file via the UI', async ({ page }) => {
const fileName = 'to-delete.md';
await setupProjectWithFixture(page, getFixturePath());
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToContext(page);
// First create a context file to delete
await clickElement(page, 'create-markdown-button');
await page.waitForSelector('[data-testid="create-markdown-dialog"]', { timeout: 5000 });
await fillInput(page, 'new-markdown-name', fileName);
await fillInput(page, 'new-markdown-content', '# Test File\n\nThis file will be deleted.');
await clickElement(page, 'confirm-create-markdown');
await page.waitForFunction(
() => !document.querySelector('[data-testid="create-markdown-dialog"]'),
{ timeout: 5000 }
);
// Wait for the file to appear in the list
await waitForContextFile(page, fileName, 10000);
// Select the file
await selectContextFile(page, fileName);
// Delete the selected file
await deleteSelectedContextFile(page);
// Verify the file is no longer in the list
await expect(async () => {
const fileButton = page.locator(`[data-testid="context-file-${fileName}"]`);
expect(await fileButton.count()).toBe(0);
}).toPass({ timeout: 10000 });
// Verify the file is deleted from the filesystem
const fixturePath = getFixturePath();
const contextPath = path.join(fixturePath, '.automaker', 'context', fileName);
expect(fs.existsSync(contextPath)).toBe(false);
});
});

View File

@@ -1,459 +0,0 @@
/**
* 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!');
});
});

View File

@@ -0,0 +1,85 @@
/**
* Feature Backlog E2E Test
*
* Happy path: Add a feature to the backlog
*/
import { test, expect } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
import {
createTempDirPath,
cleanupTempDir,
setupRealProject,
waitForNetworkIdle,
clickAddFeature,
fillAddFeatureDialog,
confirmAddFeature,
} from '../utils';
const TEST_TEMP_DIR = createTempDirPath('feature-backlog-test');
test.describe('Feature Backlog', () => {
let projectPath: string;
const projectName = `test-project-${Date.now()}`;
test.beforeAll(async () => {
if (!fs.existsSync(TEST_TEMP_DIR)) {
fs.mkdirSync(TEST_TEMP_DIR, { recursive: true });
}
projectPath = path.join(TEST_TEMP_DIR, projectName);
fs.mkdirSync(projectPath, { recursive: true });
fs.writeFileSync(
path.join(projectPath, 'package.json'),
JSON.stringify({ name: projectName, version: '1.0.0' }, null, 2)
);
const automakerDir = path.join(projectPath, '.automaker');
fs.mkdirSync(automakerDir, { recursive: true });
fs.mkdirSync(path.join(automakerDir, 'features'), { recursive: true });
fs.mkdirSync(path.join(automakerDir, 'context'), { recursive: true });
fs.writeFileSync(
path.join(automakerDir, 'categories.json'),
JSON.stringify({ categories: [] }, null, 2)
);
fs.writeFileSync(
path.join(automakerDir, 'app_spec.txt'),
`# ${projectName}\n\nA test project for e2e testing.`
);
});
test.afterAll(async () => {
cleanupTempDir(TEST_TEMP_DIR);
});
test('should add a new feature to the backlog', async ({ page }) => {
const featureDescription = `Test feature ${Date.now()}`;
await setupRealProject(page, projectPath, projectName, { setAsCurrent: true });
await page.goto('/board');
await waitForNetworkIdle(page);
await expect(page.locator('[data-testid="board-view"]')).toBeVisible({ timeout: 10000 });
await expect(page.locator('[data-testid="kanban-column-backlog"]')).toBeVisible({
timeout: 5000,
});
await clickAddFeature(page);
await fillAddFeatureDialog(page, featureDescription);
await confirmAddFeature(page);
// Wait for the feature to appear in the backlog
await expect(async () => {
const backlogColumn = page.locator('[data-testid="kanban-column-backlog"]');
const featureCard = backlogColumn.locator('[data-testid^="kanban-card-"]').filter({
hasText: featureDescription,
});
expect(await featureCard.count()).toBeGreaterThan(0);
}).toPass({ timeout: 10000 });
});
});

View File

@@ -0,0 +1,139 @@
/**
* Edit Feature E2E Test
*
* Happy path: Edit an existing feature's description and verify changes persist
*/
import { test, expect } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
import {
createTempDirPath,
cleanupTempDir,
setupRealProject,
waitForNetworkIdle,
clickAddFeature,
fillAddFeatureDialog,
confirmAddFeature,
clickElement,
} from '../utils';
const TEST_TEMP_DIR = createTempDirPath('edit-feature-test');
test.describe('Edit Feature', () => {
let projectPath: string;
const projectName = `test-project-${Date.now()}`;
test.beforeAll(async () => {
if (!fs.existsSync(TEST_TEMP_DIR)) {
fs.mkdirSync(TEST_TEMP_DIR, { recursive: true });
}
projectPath = path.join(TEST_TEMP_DIR, projectName);
fs.mkdirSync(projectPath, { recursive: true });
fs.writeFileSync(
path.join(projectPath, 'package.json'),
JSON.stringify({ name: projectName, version: '1.0.0' }, null, 2)
);
const automakerDir = path.join(projectPath, '.automaker');
fs.mkdirSync(automakerDir, { recursive: true });
fs.mkdirSync(path.join(automakerDir, 'features'), { recursive: true });
fs.mkdirSync(path.join(automakerDir, 'context'), { recursive: true });
fs.writeFileSync(
path.join(automakerDir, 'categories.json'),
JSON.stringify({ categories: [] }, null, 2)
);
fs.writeFileSync(
path.join(automakerDir, 'app_spec.txt'),
`# ${projectName}\n\nA test project for e2e testing.`
);
});
test.afterAll(async () => {
cleanupTempDir(TEST_TEMP_DIR);
});
test('should edit an existing feature description', async ({ page }) => {
const originalDescription = `Original feature ${Date.now()}`;
const updatedDescription = `Updated feature ${Date.now()}`;
await setupRealProject(page, projectPath, projectName, { setAsCurrent: true });
await page.goto('/board');
await waitForNetworkIdle(page);
await expect(page.locator('[data-testid="board-view"]')).toBeVisible({ timeout: 10000 });
await expect(page.locator('[data-testid="kanban-column-backlog"]')).toBeVisible({
timeout: 5000,
});
// Create a feature first
await clickAddFeature(page);
await fillAddFeatureDialog(page, originalDescription);
await confirmAddFeature(page);
// Wait for the feature to appear in the backlog
await expect(async () => {
const backlogColumn = page.locator('[data-testid="kanban-column-backlog"]');
const featureCard = backlogColumn.locator('[data-testid^="kanban-card-"]').filter({
hasText: originalDescription,
});
expect(await featureCard.count()).toBeGreaterThan(0);
}).toPass({ timeout: 10000 });
// Get the feature ID from the card
const featureCard = page
.locator('[data-testid="kanban-column-backlog"]')
.locator('[data-testid^="kanban-card-"]')
.filter({ hasText: originalDescription })
.first();
const cardTestId = await featureCard.getAttribute('data-testid');
const featureId = cardTestId?.replace('kanban-card-', '');
// Collapse the sidebar first to avoid it intercepting clicks
const collapseSidebarButton = page.locator('button:has-text("Collapse sidebar")');
if (await collapseSidebarButton.isVisible()) {
await collapseSidebarButton.click();
await page.waitForTimeout(300); // Wait for sidebar animation
}
// Click the edit button on the card using JavaScript click to bypass pointer interception
const editButton = page.locator(`[data-testid="edit-backlog-${featureId}"]`);
await expect(editButton).toBeVisible({ timeout: 5000 });
await editButton.evaluate((el) => (el as HTMLElement).click());
// Wait for edit dialog to appear
await expect(page.locator('[data-testid="edit-feature-dialog"]')).toBeVisible({
timeout: 10000,
});
// Update the description - the input is inside the DescriptionImageDropZone
const descriptionInput = page
.locator('[data-testid="edit-feature-dialog"]')
.getByPlaceholder('Describe the feature...');
await expect(descriptionInput).toBeVisible({ timeout: 5000 });
await descriptionInput.fill(updatedDescription);
// Save changes
await clickElement(page, 'confirm-edit-feature');
// Wait for dialog to close
await page.waitForFunction(
() => !document.querySelector('[data-testid="edit-feature-dialog"]'),
{ timeout: 5000 }
);
// Verify the updated description appears in the card
await expect(async () => {
const backlogColumn = page.locator('[data-testid="kanban-column-backlog"]');
const updatedCard = backlogColumn.locator('[data-testid^="kanban-card-"]').filter({
hasText: updatedDescription,
});
expect(await updatedCard.count()).toBeGreaterThan(0);
}).toPass({ timeout: 10000 });
});
});

View File

@@ -0,0 +1,119 @@
/**
* Feature Manual Review Flow E2E Test
*
* Happy path: Manually verify a feature in the waiting_approval column
*
* This test verifies that:
* 1. A feature in waiting_approval column shows the mark as verified button
* 2. Clicking mark as verified moves the feature to the verified column
*
* Note: For waiting_approval features, the button is "mark-as-verified-{id}" not "manual-verify-{id}"
*/
import { test, expect } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
import {
createTempDirPath,
cleanupTempDir,
setupRealProject,
waitForNetworkIdle,
getKanbanColumn,
} from '../utils';
const TEST_TEMP_DIR = createTempDirPath('manual-review-test');
test.describe('Feature Manual Review Flow', () => {
let projectPath: string;
const projectName = `test-project-${Date.now()}`;
const featureId = 'test-feature-manual-review';
test.beforeAll(async () => {
if (!fs.existsSync(TEST_TEMP_DIR)) {
fs.mkdirSync(TEST_TEMP_DIR, { recursive: true });
}
projectPath = path.join(TEST_TEMP_DIR, projectName);
fs.mkdirSync(projectPath, { recursive: true });
fs.writeFileSync(
path.join(projectPath, 'package.json'),
JSON.stringify({ name: projectName, version: '1.0.0' }, null, 2)
);
const automakerDir = path.join(projectPath, '.automaker');
fs.mkdirSync(automakerDir, { recursive: true });
fs.mkdirSync(path.join(automakerDir, 'features'), { recursive: true });
fs.mkdirSync(path.join(automakerDir, 'context'), { recursive: true });
fs.writeFileSync(
path.join(automakerDir, 'categories.json'),
JSON.stringify({ categories: [] }, null, 2)
);
fs.writeFileSync(
path.join(automakerDir, 'app_spec.txt'),
`# ${projectName}\n\nA test project for e2e testing.`
);
// Create a feature file that is in waiting_approval status
const featureDir = path.join(automakerDir, 'features', featureId);
fs.mkdirSync(featureDir, { recursive: true });
const feature = {
id: featureId,
description: 'Test feature for manual review flow',
category: 'test',
status: 'waiting_approval',
skipTests: true,
model: 'sonnet',
thinkingLevel: 'none',
createdAt: new Date().toISOString(),
branchName: '',
priority: 2,
};
fs.writeFileSync(path.join(featureDir, 'feature.json'), JSON.stringify(feature, null, 2));
});
test.afterAll(async () => {
cleanupTempDir(TEST_TEMP_DIR);
});
test('should manually verify a feature in waiting_approval column', async ({ page }) => {
await setupRealProject(page, projectPath, projectName, { setAsCurrent: true });
await page.goto('/board');
await waitForNetworkIdle(page);
await expect(page.locator('[data-testid="board-view"]')).toBeVisible({ timeout: 10000 });
// Verify the feature appears in the waiting_approval column
const waitingApprovalColumn = await getKanbanColumn(page, 'waiting_approval');
await expect(waitingApprovalColumn).toBeVisible({ timeout: 5000 });
const featureCard = page.locator(`[data-testid="kanban-card-${featureId}"]`);
await expect(featureCard).toBeVisible({ timeout: 10000 });
// For waiting_approval features, the button is "mark-as-verified-{id}"
const markAsVerifiedButton = page.locator(`[data-testid="mark-as-verified-${featureId}"]`);
await expect(markAsVerifiedButton).toBeVisible({ timeout: 5000 });
// Click the mark as verified button
await markAsVerifiedButton.click();
// Wait for the feature to move to verified column
await expect(async () => {
const verifiedColumn = await getKanbanColumn(page, 'verified');
const cardInVerified = verifiedColumn.locator(`[data-testid="kanban-card-${featureId}"]`);
expect(await cardInVerified.count()).toBe(1);
}).toPass({ timeout: 15000 });
// Verify the feature is no longer in waiting_approval column
await expect(async () => {
const waitingColumn = await getKanbanColumn(page, 'waiting_approval');
const cardInWaiting = waitingColumn.locator(`[data-testid="kanban-card-${featureId}"]`);
expect(await cardInWaiting.count()).toBe(0);
}).toPass({ timeout: 5000 });
});
});

View File

@@ -0,0 +1,107 @@
/**
* Feature Skip Tests Toggle E2E Test
*
* Happy path: Create a feature with default settings (skipTests=true) and verify the badge appears
*
* Note: The app defaults to skipTests=true (manual verification required), so we don't need to
* toggle anything. We just verify the badge appears by default.
*/
import { test, expect } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
import {
createTempDirPath,
cleanupTempDir,
setupRealProject,
waitForNetworkIdle,
clickAddFeature,
fillAddFeatureDialog,
confirmAddFeature,
isSkipTestsBadgeVisible,
} from '../utils';
const TEST_TEMP_DIR = createTempDirPath('skip-tests-toggle-test');
test.describe('Feature Skip Tests Badge', () => {
let projectPath: string;
const projectName = `test-project-${Date.now()}`;
test.beforeAll(async () => {
if (!fs.existsSync(TEST_TEMP_DIR)) {
fs.mkdirSync(TEST_TEMP_DIR, { recursive: true });
}
projectPath = path.join(TEST_TEMP_DIR, projectName);
fs.mkdirSync(projectPath, { recursive: true });
fs.writeFileSync(
path.join(projectPath, 'package.json'),
JSON.stringify({ name: projectName, version: '1.0.0' }, null, 2)
);
const automakerDir = path.join(projectPath, '.automaker');
fs.mkdirSync(automakerDir, { recursive: true });
fs.mkdirSync(path.join(automakerDir, 'features'), { recursive: true });
fs.mkdirSync(path.join(automakerDir, 'context'), { recursive: true });
fs.writeFileSync(
path.join(automakerDir, 'categories.json'),
JSON.stringify({ categories: [] }, null, 2)
);
fs.writeFileSync(
path.join(automakerDir, 'app_spec.txt'),
`# ${projectName}\n\nA test project for e2e testing.`
);
});
test.afterAll(async () => {
cleanupTempDir(TEST_TEMP_DIR);
});
test('should show skip tests badge for new feature with default settings', async ({ page }) => {
const featureDescription = `Skip tests feature ${Date.now()}`;
await setupRealProject(page, projectPath, projectName, { setAsCurrent: true });
await page.goto('/board');
await waitForNetworkIdle(page);
await expect(page.locator('[data-testid="board-view"]')).toBeVisible({ timeout: 10000 });
await expect(page.locator('[data-testid="kanban-column-backlog"]')).toBeVisible({
timeout: 5000,
});
// Open the add feature dialog and add feature with default settings
// Default is skipTests=true (manual verification required)
await clickAddFeature(page);
await fillAddFeatureDialog(page, featureDescription);
await confirmAddFeature(page);
// Wait for the feature to appear in the backlog
await expect(async () => {
const backlogColumn = page.locator('[data-testid="kanban-column-backlog"]');
const featureCard = backlogColumn.locator('[data-testid^="kanban-card-"]').filter({
hasText: featureDescription,
});
expect(await featureCard.count()).toBeGreaterThan(0);
}).toPass({ timeout: 10000 });
// Get the feature ID from the card
const featureCard = page
.locator('[data-testid="kanban-column-backlog"]')
.locator('[data-testid^="kanban-card-"]')
.filter({ hasText: featureDescription })
.first();
const cardTestId = await featureCard.getAttribute('data-testid');
const featureId = cardTestId?.replace('kanban-card-', '');
// Verify the skip tests badge is visible on the card (should be there by default)
expect(featureId).toBeDefined();
await expect(async () => {
const badgeVisible = await isSkipTestsBadgeVisible(page, featureId!);
expect(badgeVisible).toBe(true);
}).toPass({ timeout: 5000 });
});
});

View File

@@ -0,0 +1,60 @@
/**
* Worktree Integration E2E Test
*
* Happy path: Display worktree selector with main branch
*/
import { test, expect } from '@playwright/test';
import * as fs from 'fs';
import {
waitForNetworkIdle,
createTestGitRepo,
cleanupTempDir,
createTempDirPath,
setupProjectWithPath,
waitForBoardView,
} from '../utils';
const TEST_TEMP_DIR = createTempDirPath('worktree-tests');
interface TestRepo {
path: string;
cleanup: () => Promise<void>;
}
test.describe('Worktree Integration', () => {
let testRepo: TestRepo;
test.beforeAll(async () => {
if (!fs.existsSync(TEST_TEMP_DIR)) {
fs.mkdirSync(TEST_TEMP_DIR, { recursive: true });
}
});
test.beforeEach(async () => {
testRepo = await createTestGitRepo(TEST_TEMP_DIR);
});
test.afterEach(async () => {
if (testRepo) {
await testRepo.cleanup();
}
});
test.afterAll(async () => {
cleanupTempDir(TEST_TEMP_DIR);
});
test('should display worktree selector with main branch', async ({ page }) => {
await setupProjectWithPath(page, testRepo.path);
await page.goto('/');
await waitForNetworkIdle(page);
await waitForBoardView(page);
const branchLabel = page.getByText('Branch:');
await expect(branchLabel).toBeVisible({ timeout: 10000 });
const mainBranchButton = page.locator('[data-testid="worktree-branch-main"]');
await expect(mainBranchButton).toBeVisible({ timeout: 15000 });
});
});

View File

@@ -1,291 +0,0 @@
/**
* Kanban Board Responsive Scaling Tests
*
* Tests that the Kanban board columns scale intelligently to fill
* the available window space without dead space or content being cut off.
*/
import { test, expect } from '@playwright/test';
import * as fs from 'fs';
import {
waitForNetworkIdle,
createTestGitRepo,
cleanupTempDir,
createTempDirPath,
setupProjectWithPathNoWorktrees,
waitForBoardView,
} from './utils';
// Create unique temp dir for this test run
const TEST_TEMP_DIR = createTempDirPath('kanban-responsive-tests');
interface TestRepo {
path: string;
cleanup: () => Promise<void>;
}
test.describe('Kanban Responsive Scaling Tests', () => {
let testRepo: TestRepo;
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);
});
test('kanban columns should scale to fill available width at different viewport sizes', async ({
page,
}) => {
// Setup project and navigate to board view
await setupProjectWithPathNoWorktrees(page, testRepo.path);
await page.goto('/');
await waitForNetworkIdle(page);
await waitForBoardView(page);
// Wait for the board to fully render
await page.waitForTimeout(500);
// Get all four kanban columns
const backlogColumn = page.locator('[data-testid="kanban-column-backlog"]');
const inProgressColumn = page.locator('[data-testid="kanban-column-in_progress"]');
const waitingApprovalColumn = page.locator('[data-testid="kanban-column-waiting_approval"]');
const verifiedColumn = page.locator('[data-testid="kanban-column-verified"]');
// Verify all columns are visible
await expect(backlogColumn).toBeVisible();
await expect(inProgressColumn).toBeVisible();
await expect(waitingApprovalColumn).toBeVisible();
await expect(verifiedColumn).toBeVisible();
// Test at different viewport widths
const viewportWidths = [1024, 1280, 1440, 1920];
for (const width of viewportWidths) {
// Set viewport size
await page.setViewportSize({ width, height: 900 });
await page.waitForTimeout(300); // Wait for resize to take effect
// Get column widths
const backlogBox = await backlogColumn.boundingBox();
const inProgressBox = await inProgressColumn.boundingBox();
const waitingApprovalBox = await waitingApprovalColumn.boundingBox();
const verifiedBox = await verifiedColumn.boundingBox();
expect(backlogBox).not.toBeNull();
expect(inProgressBox).not.toBeNull();
expect(waitingApprovalBox).not.toBeNull();
expect(verifiedBox).not.toBeNull();
if (backlogBox && inProgressBox && waitingApprovalBox && verifiedBox) {
// All columns should have the same width
const columnWidths = [
backlogBox.width,
inProgressBox.width,
waitingApprovalBox.width,
verifiedBox.width,
];
// All columns should be equal width (within 2px tolerance for rounding)
const baseWidth = columnWidths[0];
for (const columnWidth of columnWidths) {
expect(Math.abs(columnWidth - baseWidth)).toBeLessThan(2);
}
// Column width should be at least minimum (280px)
// No max-width - columns scale evenly to fill available viewport
expect(baseWidth).toBeGreaterThanOrEqual(280);
// Columns should not overlap (check x positions)
expect(inProgressBox.x).toBeGreaterThan(backlogBox.x + backlogBox.width - 5);
expect(waitingApprovalBox.x).toBeGreaterThan(inProgressBox.x + inProgressBox.width - 5);
expect(verifiedBox.x).toBeGreaterThan(waitingApprovalBox.x + waitingApprovalBox.width - 5);
}
}
});
test('kanban columns should be centered in the viewport', async ({ page }) => {
// Setup project and navigate to board view
await setupProjectWithPathNoWorktrees(page, testRepo.path);
await page.goto('/');
await waitForNetworkIdle(page);
await waitForBoardView(page);
// Wait for the board to fully render
await page.waitForTimeout(500);
// Set a specific viewport size
await page.setViewportSize({ width: 1600, height: 900 });
await page.waitForTimeout(300);
// Get the first and last columns
const backlogColumn = page.locator('[data-testid="kanban-column-backlog"]');
const verifiedColumn = page.locator('[data-testid="kanban-column-verified"]');
const backlogBox = await backlogColumn.boundingBox();
const verifiedBox = await verifiedColumn.boundingBox();
expect(backlogBox).not.toBeNull();
expect(verifiedBox).not.toBeNull();
if (backlogBox && verifiedBox) {
// Get the actual container width (accounting for sidebar)
// The board-view container is inside a flex container that accounts for sidebar
const containerWidth = await page.evaluate(() => {
const boardView = document.querySelector('[data-testid="board-view"]');
if (!boardView) return window.innerWidth;
const parent = boardView.parentElement;
return parent ? parent.clientWidth : window.innerWidth;
});
// Calculate the left and right margins relative to the container
// The bounding box x is relative to the viewport, so we need to find where
// the container starts relative to the viewport
const containerLeft = await page.evaluate(() => {
const boardView = document.querySelector('[data-testid="board-view"]');
if (!boardView) return 0;
const parent = boardView.parentElement;
if (!parent) return 0;
const rect = parent.getBoundingClientRect();
return rect.left;
});
// Calculate margins relative to the container
const leftMargin = backlogBox.x - containerLeft;
const rightMargin = containerWidth - (verifiedBox.x + verifiedBox.width - containerLeft);
// The margins should be roughly equal (columns are centered)
// Allow for some tolerance due to padding and gaps
const marginDifference = Math.abs(leftMargin - rightMargin);
expect(marginDifference).toBeLessThan(50); // Should be reasonably centered
}
});
test('kanban columns should have no horizontal scrollbar at standard viewport width', async ({
page,
}) => {
// Setup project and navigate to board view
await setupProjectWithPathNoWorktrees(page, testRepo.path);
await page.goto('/');
await waitForNetworkIdle(page);
await waitForBoardView(page);
// Set a standard viewport size (1400px which is the default window width)
await page.setViewportSize({ width: 1400, height: 900 });
await page.waitForTimeout(300);
// Check if horizontal scrollbar is present by comparing scrollWidth and clientWidth
const hasHorizontalScroll = await page.evaluate(() => {
const boardContainer = document.querySelector('[data-testid="board-view"]');
if (!boardContainer) return false;
return boardContainer.scrollWidth > boardContainer.clientWidth;
});
// There should be no horizontal scroll at standard width since columns scale down
expect(hasHorizontalScroll).toBe(false);
});
test('kanban columns should fit at minimum width (1500px - Electron minimum)', async ({
page,
}) => {
// Setup project and navigate to board view
await setupProjectWithPathNoWorktrees(page, testRepo.path);
await page.goto('/');
await waitForNetworkIdle(page);
await waitForBoardView(page);
// Set viewport to the new Electron minimum width (1500px)
await page.setViewportSize({ width: 1500, height: 900 });
await page.waitForTimeout(300);
// Get all four kanban columns
const backlogColumn = page.locator('[data-testid="kanban-column-backlog"]');
const verifiedColumn = page.locator('[data-testid="kanban-column-verified"]');
// Verify columns are visible
await expect(backlogColumn).toBeVisible();
await expect(verifiedColumn).toBeVisible();
// Check if horizontal scrollbar is present
const hasHorizontalScroll = await page.evaluate(() => {
const boardContainer = document.querySelector('[data-testid="board-view"]');
if (!boardContainer) return false;
return boardContainer.scrollWidth > boardContainer.clientWidth;
});
// There should be no horizontal scroll at minimum width
expect(hasHorizontalScroll).toBe(false);
// Verify columns are at least minimum width (280px)
const backlogBox = await backlogColumn.boundingBox();
expect(backlogBox).not.toBeNull();
if (backlogBox) {
expect(backlogBox.width).toBeGreaterThanOrEqual(280);
}
});
test('kanban columns should scale correctly when sidebar is collapsed', async ({ page }) => {
// Setup project and navigate to board view
await setupProjectWithPathNoWorktrees(page, testRepo.path);
await page.goto('/');
await waitForNetworkIdle(page);
await waitForBoardView(page);
// Set a viewport size
await page.setViewportSize({ width: 1600, height: 900 });
await page.waitForTimeout(300);
// Get initial column width
const backlogColumn = page.locator('[data-testid="kanban-column-backlog"]');
const initialBox = await backlogColumn.boundingBox();
expect(initialBox).not.toBeNull();
// Find and click the sidebar collapse button
const collapseButton = page.locator('[data-testid="sidebar-collapse-button"]');
if (await collapseButton.isVisible()) {
await collapseButton.click();
// Wait for sidebar transition (300ms) + buffer
await page.waitForTimeout(400);
// Get column width after collapse
const collapsedBox = await backlogColumn.boundingBox();
expect(collapsedBox).not.toBeNull();
if (initialBox && collapsedBox) {
// Column should be wider or same after sidebar collapse (more space available)
// Allow for small variations due to transitions
expect(collapsedBox.width).toBeGreaterThanOrEqual(initialBox.width - 5);
// Width should still be at least minimum
expect(collapsedBox.width).toBeGreaterThanOrEqual(280);
}
// Verify no horizontal scrollbar after collapse
const hasHorizontalScroll = await page.evaluate(() => {
const boardContainer = document.querySelector('[data-testid="board-view"]');
if (!boardContainer) return false;
return boardContainer.scrollWidth > boardContainer.clientWidth;
});
expect(hasHorizontalScroll).toBe(false);
}
});
});

View File

@@ -1,982 +0,0 @@
import { test, expect } from '@playwright/test';
import {
setupMockProjectWithProfiles,
waitForNetworkIdle,
navigateToProfiles,
clickNewProfileButton,
clickEmptyState,
fillProfileForm,
saveProfile,
cancelProfileDialog,
clickEditProfile,
clickDeleteProfile,
confirmDeleteProfile,
cancelDeleteProfile,
fillProfileName,
fillProfileDescription,
selectIcon,
selectModel,
selectThinkingLevel,
isAddProfileDialogOpen,
isEditProfileDialogOpen,
isDeleteConfirmDialogOpen,
getProfileName,
getProfileDescription,
getProfileModel,
getProfileThinkingLevel,
isBuiltInProfile,
isEditButtonVisible,
isDeleteButtonVisible,
dragProfile,
getProfileOrder,
clickRefreshDefaults,
countCustomProfiles,
countBuiltInProfiles,
getProfileCard,
waitForSuccessToast,
waitForToast,
waitForErrorToast,
waitForDialogClose,
pressModifierEnter,
clickElement,
} from './utils';
test.describe('AI Profiles View', () => {
// ============================================================================
// Profile Creation Tests
// ============================================================================
test.describe('Profile Creation', () => {
test.beforeEach(async ({ page }) => {
// Start with no custom profiles (only built-in)
await setupMockProjectWithProfiles(page, { customProfilesCount: 0 });
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToProfiles(page);
});
test('should create profile via header button', async ({ page }) => {
// Click the "New Profile" button
await clickNewProfileButton(page);
// Verify dialog is open
expect(await isAddProfileDialogOpen(page)).toBe(true);
// Fill in profile data
await fillProfileForm(page, {
name: 'Test Profile',
description: 'A test profile',
icon: 'Brain',
model: 'sonnet',
thinkingLevel: 'medium',
});
// Save the profile
await saveProfile(page);
// Verify success toast
await waitForSuccessToast(page, 'Profile created');
// Verify profile appears in the list
const customCount = await countCustomProfiles(page);
expect(customCount).toBe(1);
// Verify profile details - get the dynamic profile ID
// (Note: Profile IDs are dynamically generated, not "custom-profile-1")
// We can verify count but skip checking the specific profile name since ID is dynamic
});
test('should create profile via empty state', async ({ page }) => {
// Click the empty state card
await clickEmptyState(page);
// Verify dialog is open
expect(await isAddProfileDialogOpen(page)).toBe(true);
// Fill and save
await fillProfileForm(page, {
name: 'Empty State Profile',
description: 'Created from empty state',
model: 'opus',
});
await saveProfile(page);
// Verify profile was created
await waitForSuccessToast(page, 'Profile created');
const customCount = await countCustomProfiles(page);
expect(customCount).toBe(1);
});
test('should create profile with each icon option', async ({ page }) => {
const icons = ['Brain', 'Zap', 'Scale', 'Cpu', 'Rocket', 'Sparkles'];
for (const icon of icons) {
await clickNewProfileButton(page);
await fillProfileForm(page, {
name: `Profile with ${icon}`,
model: 'haiku',
icon,
});
await saveProfile(page);
await waitForSuccessToast(page, 'Profile created');
// Ensure dialog is fully closed before next iteration
await waitForDialogClose(page);
}
// Verify all profiles were created
const customCount = await countCustomProfiles(page);
expect(customCount).toBe(icons.length);
});
test('should create profile with each model option', async ({ page }) => {
const models = ['haiku', 'sonnet', 'opus'];
for (const model of models) {
await clickNewProfileButton(page);
await fillProfileForm(page, {
name: `Profile with ${model}`,
model,
});
await saveProfile(page);
await waitForSuccessToast(page, 'Profile created');
// Ensure dialog is fully closed before next iteration
await waitForDialogClose(page);
}
// Verify all profiles were created
const customCount = await countCustomProfiles(page);
expect(customCount).toBe(models.length);
});
test('should create profile with different thinking levels', async ({ page }) => {
const levels = ['none', 'low', 'medium', 'high', 'ultrathink'];
for (const level of levels) {
await clickNewProfileButton(page);
await fillProfileForm(page, {
name: `Profile with ${level}`,
model: 'opus', // Opus supports all thinking levels
thinkingLevel: level,
});
await saveProfile(page);
await waitForSuccessToast(page, 'Profile created');
// Ensure dialog is fully closed before next iteration
await waitForDialogClose(page);
}
// Verify all profiles were created
const customCount = await countCustomProfiles(page);
expect(customCount).toBe(levels.length);
});
test('should show warning toast when selecting ultrathink', async ({ page }) => {
await clickNewProfileButton(page);
await fillProfileForm(page, {
name: 'Ultrathink Profile',
model: 'opus',
});
// Select ultrathink
await selectThinkingLevel(page, 'ultrathink');
// Verify warning toast appears
await waitForToast(page, 'Ultrathink uses extensive reasoning');
});
test('should cancel profile creation', async ({ page }) => {
await clickNewProfileButton(page);
// Fill partial data
await fillProfileName(page, 'Cancelled Profile');
// Cancel
await cancelProfileDialog(page);
// Verify dialog is closed
expect(await isAddProfileDialogOpen(page)).toBe(false);
// Verify no profile was created
const customCount = await countCustomProfiles(page);
expect(customCount).toBe(0);
});
test('should close dialog on overlay click', async ({ page }) => {
await clickNewProfileButton(page);
// Click the backdrop/overlay to close the dialog
// The dialog overlay is the background outside the dialog content
const dialogBackdrop = page.locator('[data-radix-dialog-overlay]');
if ((await dialogBackdrop.count()) > 0) {
await dialogBackdrop.click({ position: { x: 10, y: 10 } });
} else {
// Fallback: press Escape key
await page.keyboard.press('Escape');
}
// Wait for dialog to fully close (handles animation)
await waitForDialogClose(page);
// Verify dialog is closed
expect(await isAddProfileDialogOpen(page)).toBe(false);
// Verify no profile was created
const customCount = await countCustomProfiles(page);
expect(customCount).toBe(0);
});
});
// ============================================================================
// Profile Editing Tests
// ============================================================================
test.describe('Profile Editing', () => {
test.beforeEach(async ({ page }) => {
// Start with one custom profile
await setupMockProjectWithProfiles(page, { customProfilesCount: 1 });
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToProfiles(page);
});
test('should edit profile name', async ({ page }) => {
// Click edit button for the custom profile
await clickEditProfile(page, 'custom-profile-1');
// Verify dialog is open
expect(await isEditProfileDialogOpen(page)).toBe(true);
// Update name
await fillProfileName(page, 'Updated Profile Name');
// Save
await saveProfile(page);
// Verify success toast
await waitForSuccessToast(page, 'Profile updated');
// Verify name was updated
const profileName = await getProfileName(page, 'custom-profile-1');
expect(profileName).toContain('Updated Profile Name');
});
test('should edit profile description', async ({ page }) => {
await clickEditProfile(page, 'custom-profile-1');
// Update description
await fillProfileDescription(page, 'Updated description');
await saveProfile(page);
await waitForSuccessToast(page, 'Profile updated');
// Verify description was updated
const description = await getProfileDescription(page, 'custom-profile-1');
expect(description).toContain('Updated description');
});
test('should change profile icon', async ({ page }) => {
await clickEditProfile(page, 'custom-profile-1');
// Change icon to a different one
await selectIcon(page, 'Rocket');
await saveProfile(page);
await waitForSuccessToast(page, 'Profile updated');
// Verify icon was changed (visual check via profile card)
const card = await getProfileCard(page, 'custom-profile-1');
const rocketIcon = card.locator('svg[class*="lucide-rocket"]');
expect(await rocketIcon.isVisible()).toBe(true);
});
test('should change profile model', async ({ page }) => {
await clickEditProfile(page, 'custom-profile-1');
// Change model
await selectModel(page, 'opus');
await saveProfile(page);
await waitForSuccessToast(page, 'Profile updated');
// Verify model badge was updated
const model = await getProfileModel(page, 'custom-profile-1');
expect(model.toLowerCase()).toContain('opus');
});
test('should change thinking level', async ({ page }) => {
await clickEditProfile(page, 'custom-profile-1');
// Ensure model supports thinking
await selectModel(page, 'sonnet');
await selectThinkingLevel(page, 'high');
await saveProfile(page);
await waitForSuccessToast(page, 'Profile updated');
// Verify thinking level badge was updated
const thinkingLevel = await getProfileThinkingLevel(page, 'custom-profile-1');
expect(thinkingLevel?.toLowerCase()).toContain('high');
});
test('should cancel edit without saving', async ({ page }) => {
// Get original name
const originalName = await getProfileName(page, 'custom-profile-1');
await clickEditProfile(page, 'custom-profile-1');
// Change name
await fillProfileName(page, 'Should Not Save');
// Cancel
await cancelProfileDialog(page);
// Verify dialog is closed
expect(await isEditProfileDialogOpen(page)).toBe(false);
// Verify name was NOT changed
const currentName = await getProfileName(page, 'custom-profile-1');
expect(currentName).toBe(originalName);
});
});
// ============================================================================
// Profile Deletion Tests
// ============================================================================
test.describe('Profile Deletion', () => {
test.beforeEach(async ({ page }) => {
// Start with 2 custom profiles
await setupMockProjectWithProfiles(page, { customProfilesCount: 2 });
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToProfiles(page);
});
test('should delete profile with confirmation', async ({ page }) => {
// Get initial count
const initialCount = await countCustomProfiles(page);
expect(initialCount).toBe(2);
// Click delete button
await clickDeleteProfile(page, 'custom-profile-1');
// Verify confirmation dialog is open
expect(await isDeleteConfirmDialogOpen(page)).toBe(true);
// Confirm deletion
await confirmDeleteProfile(page);
// Verify success toast
await waitForSuccessToast(page, 'Profile deleted');
// Verify profile was removed
const finalCount = await countCustomProfiles(page);
expect(finalCount).toBe(1);
});
test('should delete via keyboard shortcut (Cmd+Enter)', async ({ page }) => {
await clickDeleteProfile(page, 'custom-profile-1');
// Press Cmd/Ctrl+Enter to confirm (platform-aware)
await pressModifierEnter(page);
// Verify profile was deleted
await waitForSuccessToast(page, 'Profile deleted');
const finalCount = await countCustomProfiles(page);
expect(finalCount).toBe(1);
});
test('should cancel deletion', async ({ page }) => {
const initialCount = await countCustomProfiles(page);
await clickDeleteProfile(page, 'custom-profile-1');
// Cancel deletion
await cancelDeleteProfile(page);
// Verify dialog is closed
expect(await isDeleteConfirmDialogOpen(page)).toBe(false);
// Verify profile was NOT deleted
const finalCount = await countCustomProfiles(page);
expect(finalCount).toBe(initialCount);
});
test('should not show delete button for built-in profiles', async ({ page }) => {
// Check delete button visibility for built-in profile
const isDeleteVisible = await isDeleteButtonVisible(page, 'profile-heavy-task');
expect(isDeleteVisible).toBe(false);
});
test('should show delete button for custom profiles', async ({ page }) => {
// Check delete button visibility for custom profile
const isDeleteVisible = await isDeleteButtonVisible(page, 'custom-profile-1');
expect(isDeleteVisible).toBe(true);
});
});
// ============================================================================
// Profile Reordering Tests
// ============================================================================
test.describe('Profile Reordering', () => {
test.beforeEach(async ({ page }) => {
// Start with 3 custom profiles for reordering
await setupMockProjectWithProfiles(page, { customProfilesCount: 3 });
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToProfiles(page);
});
test('should drag first profile to last position', async ({ page }) => {
// Get initial order - custom profiles come first (0, 1, 2), then built-in (3, 4, 5)
const initialOrder = await getProfileOrder(page);
// Drag first profile (index 0) to last position (index 5)
await dragProfile(page, 0, 5);
// Get new order
const newOrder = await getProfileOrder(page);
// Verify order changed - the first item should now be at a different position
expect(newOrder).not.toEqual(initialOrder);
});
test.skip('should drag profile to earlier position', async ({ page }) => {
// Note: Skipped because dnd-kit in grid layout doesn't reliably support
// dragging items backwards. Forward drags work correctly.
const initialOrder = await getProfileOrder(page);
// Drag from position 3 to position 1 (moving backward)
await dragProfile(page, 3, 1);
const newOrder = await getProfileOrder(page);
// Verify order changed
expect(newOrder).not.toEqual(initialOrder);
});
test('should drag profile to middle position', async ({ page }) => {
const initialOrder = await getProfileOrder(page);
// Drag first profile to middle position
await dragProfile(page, 0, 3);
const newOrder = await getProfileOrder(page);
// Verify order changed
expect(newOrder).not.toEqual(initialOrder);
});
test('should persist order after creating new profile', async ({ page }) => {
// Get initial order
const initialOrder = await getProfileOrder(page);
// Reorder profiles - move first to position 3
await dragProfile(page, 0, 3);
const orderAfterDrag = await getProfileOrder(page);
// Verify drag worked
expect(orderAfterDrag).not.toEqual(initialOrder);
// Create a new profile
await clickNewProfileButton(page);
await fillProfileForm(page, {
name: 'New Profile',
model: 'haiku',
});
await saveProfile(page);
await waitForSuccessToast(page, 'Profile created');
// Get order after creation - new profile should be added
const orderAfterCreate = await getProfileOrder(page);
// The new profile should be added (so we have one more profile)
expect(orderAfterCreate.length).toBe(orderAfterDrag.length + 1);
});
test('should show drag handle on all profiles', async ({ page }) => {
// Check for drag handles on both built-in and custom profiles
const builtInDragHandle = page.locator(
'[data-testid="profile-drag-handle-profile-heavy-task"]'
);
const customDragHandle = page.locator('[data-testid="profile-drag-handle-custom-profile-1"]');
expect(await builtInDragHandle.isVisible()).toBe(true);
expect(await customDragHandle.isVisible()).toBe(true);
});
});
// ============================================================================
// Form Validation Tests
// ============================================================================
test.describe('Form Validation', () => {
test.beforeEach(async ({ page }) => {
await setupMockProjectWithProfiles(page, { customProfilesCount: 0 });
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToProfiles(page);
});
test('should reject empty profile name', async ({ page }) => {
await clickNewProfileButton(page);
// Try to save without entering a name
await clickElement(page, 'save-profile-button');
// Should show error toast
await waitForErrorToast(page, 'Please enter a profile name');
// Dialog should still be open
expect(await isAddProfileDialogOpen(page)).toBe(true);
});
test('should reject whitespace-only name', async ({ page }) => {
await clickNewProfileButton(page);
// Enter only whitespace
await fillProfileName(page, ' ');
// Try to save
await clickElement(page, 'save-profile-button');
// Should show error toast
await waitForErrorToast(page, 'Please enter a profile name');
// Dialog should still be open
expect(await isAddProfileDialogOpen(page)).toBe(true);
});
test('should accept valid profile name', async ({ page }) => {
await clickNewProfileButton(page);
await fillProfileForm(page, {
name: 'Valid Profile Name',
model: 'haiku',
});
await saveProfile(page);
// Should show success toast
await waitForSuccessToast(page, 'Profile created');
// Dialog should be closed
expect(await isAddProfileDialogOpen(page)).toBe(false);
});
test('should handle very long profile name', async ({ page }) => {
await clickNewProfileButton(page);
// Create a 200-character name
const longName = 'A'.repeat(200);
await fillProfileName(page, longName);
await fillProfileForm(page, { model: 'haiku' });
await saveProfile(page);
// Should successfully create the profile
await waitForSuccessToast(page, 'Profile created');
});
test('should handle special characters in name and description', async ({ page }) => {
await clickNewProfileButton(page);
await fillProfileForm(page, {
name: 'Test <>&" Profile',
description: 'Description with special chars: <>&"\'',
model: 'haiku',
});
await saveProfile(page);
// Should successfully create
await waitForSuccessToast(page, 'Profile created');
// Verify name is displayed correctly (without HTML injection)
const customCount = await countCustomProfiles(page);
expect(customCount).toBe(1);
});
test('should allow empty description', async ({ page }) => {
await clickNewProfileButton(page);
await fillProfileForm(page, {
name: 'Profile Without Description',
description: '',
model: 'haiku',
});
await saveProfile(page);
// Should successfully create
await waitForSuccessToast(page, 'Profile created');
});
test('should show thinking level controls when model supports it', async ({ page }) => {
await clickNewProfileButton(page);
// Select a model that supports thinking (all current models do)
await selectModel(page, 'opus');
// Verify that the thinking level section is visible
const thinkingLevelLabel = page.locator('text="Thinking Level"');
await expect(thinkingLevelLabel).toBeVisible();
// Verify thinking level options are available
const thinkingSelector = page.locator('[data-testid^="thinking-select-"]');
await expect(thinkingSelector.first()).toBeVisible();
});
});
// ============================================================================
// Keyboard Shortcuts Tests
// ============================================================================
test.describe('Keyboard Shortcuts', () => {
test.beforeEach(async ({ page }) => {
await setupMockProjectWithProfiles(page, { customProfilesCount: 1 });
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToProfiles(page);
});
test('should save new profile with Cmd+Enter', async ({ page }) => {
await clickNewProfileButton(page);
await fillProfileForm(page, {
name: 'Shortcut Profile',
model: 'haiku',
});
// Press Cmd/Ctrl+Enter to save (platform-aware)
await pressModifierEnter(page);
// Should save and show success toast
await waitForSuccessToast(page, 'Profile created');
// Wait for dialog to fully close
await waitForDialogClose(page);
// Dialog should be closed
expect(await isAddProfileDialogOpen(page)).toBe(false);
});
test('should save edit with Cmd+Enter', async ({ page }) => {
await clickEditProfile(page, 'custom-profile-1');
await fillProfileName(page, 'Edited via Shortcut');
// Press Cmd/Ctrl+Enter to save (platform-aware)
await pressModifierEnter(page);
// Should save and show success toast
await waitForSuccessToast(page, 'Profile updated');
});
test('should confirm delete with Cmd+Enter', async ({ page }) => {
await clickDeleteProfile(page, 'custom-profile-1');
// Press Cmd/Ctrl+Enter to confirm (platform-aware)
await pressModifierEnter(page);
// Should delete and show success toast
await waitForSuccessToast(page, 'Profile deleted');
});
test('should close dialog with Escape key', async ({ page }) => {
// Test add dialog
await clickNewProfileButton(page);
await page.keyboard.press('Escape');
await waitForDialogClose(page);
expect(await isAddProfileDialogOpen(page)).toBe(false);
// Test edit dialog
await clickEditProfile(page, 'custom-profile-1');
await page.keyboard.press('Escape');
await waitForDialogClose(page);
expect(await isEditProfileDialogOpen(page)).toBe(false);
// Test delete dialog
await clickDeleteProfile(page, 'custom-profile-1');
await page.keyboard.press('Escape');
await waitForDialogClose(page);
expect(await isDeleteConfirmDialogOpen(page)).toBe(false);
});
test('should use correct modifier key for platform', async ({ page }) => {
await clickNewProfileButton(page);
await fillProfileForm(page, { name: 'Test', model: 'haiku' });
// Press the platform-specific shortcut (uses utility that handles platform detection)
await pressModifierEnter(page);
// Should work regardless of platform
await waitForSuccessToast(page, 'Profile created');
});
});
// ============================================================================
// Empty States Tests
// ============================================================================
test.describe('Empty States', () => {
test.beforeEach(async ({ page }) => {
// Start with no custom profiles
await setupMockProjectWithProfiles(page, { customProfilesCount: 0 });
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToProfiles(page);
});
test('should show empty state when no custom profiles exist', async ({ page }) => {
// Check for empty state element
const emptyState = page.locator('text="No custom profiles yet. Create one to get started!"');
expect(await emptyState.isVisible()).toBe(true);
});
test('should open add dialog when clicking empty state', async ({ page }) => {
await clickEmptyState(page);
// Dialog should open
expect(await isAddProfileDialogOpen(page)).toBe(true);
});
test('should hide empty state after creating first profile', async ({ page }) => {
// Create a profile
await clickEmptyState(page);
await fillProfileForm(page, { name: 'First Profile', model: 'haiku' });
await saveProfile(page);
await waitForSuccessToast(page, 'Profile created');
// Empty state should no longer be visible
const emptyState = page.locator('text="No custom profiles yet. Create one to get started!"');
expect(await emptyState.isVisible()).toBe(false);
// Profile card should be visible
const customCount = await countCustomProfiles(page);
expect(customCount).toBe(1);
});
});
// ============================================================================
// Built-in vs Custom Profiles Tests
// ============================================================================
test.describe('Built-in vs Custom Profiles', () => {
test.beforeEach(async ({ page }) => {
await setupMockProjectWithProfiles(page, { customProfilesCount: 1 });
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToProfiles(page);
});
test('should show built-in badge on built-in profiles', async ({ page }) => {
// Check Heavy Task profile
const isBuiltIn = await isBuiltInProfile(page, 'profile-heavy-task');
expect(isBuiltIn).toBe(true);
// Verify lock icon is present
const card = await getProfileCard(page, 'profile-heavy-task');
const lockIcon = card.locator('svg[class*="lucide-lock"]');
expect(await lockIcon.isVisible()).toBe(true);
});
test('should not show edit button on built-in profiles', async ({ page }) => {
const isEditVisible = await isEditButtonVisible(page, 'profile-heavy-task');
expect(isEditVisible).toBe(false);
});
test('should not show delete button on built-in profiles', async ({ page }) => {
const isDeleteVisible = await isDeleteButtonVisible(page, 'profile-heavy-task');
expect(isDeleteVisible).toBe(false);
});
test('should show edit and delete buttons on custom profiles', async ({ page }) => {
// Check custom profile
const isEditVisible = await isEditButtonVisible(page, 'custom-profile-1');
const isDeleteVisible = await isDeleteButtonVisible(page, 'custom-profile-1');
expect(isEditVisible).toBe(true);
expect(isDeleteVisible).toBe(true);
});
});
// ============================================================================
// Header Actions Tests
// ============================================================================
test.describe('Header Actions', () => {
test.beforeEach(async ({ page }) => {
await setupMockProjectWithProfiles(page, { customProfilesCount: 2 });
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToProfiles(page);
});
test('should refresh default profiles', async ({ page }) => {
await clickRefreshDefaults(page);
// Should show success toast - message is "Profiles refreshed"
await waitForSuccessToast(page, 'Profiles refreshed');
// Built-in profiles should still be visible
const builtInCount = await countBuiltInProfiles(page);
expect(builtInCount).toBe(3);
// Custom profiles should be preserved
const customCount = await countCustomProfiles(page);
expect(customCount).toBe(2);
});
test('should display correct profile count badges', async ({ page }) => {
// Check for count badges by counting actual profile cards
const customCount = await countCustomProfiles(page);
const builtInCount = await countBuiltInProfiles(page);
expect(customCount).toBe(2);
expect(builtInCount).toBe(3);
// Total profiles should be 5 (2 custom + 3 built-in)
const totalProfiles = customCount + builtInCount;
expect(totalProfiles).toBe(5);
});
});
// ============================================================================
// Data Persistence Tests
// ============================================================================
test.describe('Data Persistence', () => {
test.beforeEach(async ({ page }) => {
await setupMockProjectWithProfiles(page, { customProfilesCount: 0 });
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToProfiles(page);
});
test('should persist created profile after navigation', async ({ page }) => {
// Create a profile
await clickNewProfileButton(page);
await fillProfileForm(page, {
name: 'Persistent Profile',
model: 'haiku',
});
await saveProfile(page);
await waitForSuccessToast(page, 'Profile created');
// Navigate away (within app, not full page reload)
await page.locator('[data-testid="nav-board"]').click();
await waitForNetworkIdle(page);
// Navigate back to profiles
await navigateToProfiles(page);
await waitForNetworkIdle(page);
// Profile should still exist
const customCount = await countCustomProfiles(page);
expect(customCount).toBe(1);
});
test('should show correct count after creating multiple profiles', async ({ page }) => {
// Create multiple profiles
for (let i = 1; i <= 3; i++) {
await clickNewProfileButton(page);
await fillProfileForm(page, { name: `Profile ${i}`, model: 'haiku' });
await saveProfile(page);
await waitForSuccessToast(page, 'Profile created');
// Ensure dialog is fully closed before next iteration
await waitForDialogClose(page);
}
// Verify all profiles exist
const customCount = await countCustomProfiles(page);
expect(customCount).toBe(3);
// Built-in should still be there
const builtInCount = await countBuiltInProfiles(page);
expect(builtInCount).toBe(3);
});
test('should maintain profile order after navigation', async ({ page }) => {
// Create 3 profiles
for (let i = 1; i <= 3; i++) {
await clickNewProfileButton(page);
await fillProfileForm(page, { name: `Profile ${i}`, model: 'haiku' });
await saveProfile(page);
await waitForSuccessToast(page, 'Profile created');
// Ensure dialog is fully closed before next iteration
await waitForDialogClose(page);
}
// Get order after creation
const orderAfterCreate = await getProfileOrder(page);
// Navigate away (within app)
await page.locator('[data-testid="nav-board"]').click();
await waitForNetworkIdle(page);
// Navigate back
await navigateToProfiles(page);
await waitForNetworkIdle(page);
// Verify order is maintained
const orderAfterNavigation = await getProfileOrder(page);
expect(orderAfterNavigation).toEqual(orderAfterCreate);
});
});
// ============================================================================
// Toast Notifications Tests
// ============================================================================
test.describe('Toast Notifications', () => {
test.beforeEach(async ({ page }) => {
await setupMockProjectWithProfiles(page, { customProfilesCount: 1 });
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToProfiles(page);
});
test('should show success toast on profile creation', async ({ page }) => {
await clickNewProfileButton(page);
await fillProfileForm(page, { name: 'New Profile', model: 'haiku' });
await saveProfile(page);
// Verify toast with profile name
await waitForSuccessToast(page, 'Profile created');
});
test('should show success toast on profile update', async ({ page }) => {
await clickEditProfile(page, 'custom-profile-1');
await fillProfileName(page, 'Updated');
await saveProfile(page);
await waitForSuccessToast(page, 'Profile updated');
});
test('should show success toast on profile deletion', async ({ page }) => {
await clickDeleteProfile(page, 'custom-profile-1');
await confirmDeleteProfile(page);
await waitForSuccessToast(page, 'Profile deleted');
});
test('should show error toast on validation failure', async ({ page }) => {
await clickNewProfileButton(page);
// Try to save without a name
await clickElement(page, 'save-profile-button');
// Should show error toast
await waitForErrorToast(page, 'Please enter a profile name');
});
});
});

View File

@@ -0,0 +1,43 @@
/**
* AI Profiles E2E Test
*
* Happy path: Create a new profile
*/
import { test, expect } from '@playwright/test';
import {
setupMockProjectWithProfiles,
waitForNetworkIdle,
navigateToProfiles,
clickNewProfileButton,
fillProfileForm,
saveProfile,
waitForSuccessToast,
countCustomProfiles,
} from '../utils';
test.describe('AI Profiles', () => {
test('should create a new profile', async ({ page }) => {
await setupMockProjectWithProfiles(page, { customProfilesCount: 0 });
await page.goto('/');
await waitForNetworkIdle(page);
await navigateToProfiles(page);
await clickNewProfileButton(page);
await fillProfileForm(page, {
name: 'Test Profile',
description: 'A test profile',
icon: 'Brain',
model: 'sonnet',
thinkingLevel: 'medium',
});
await saveProfile(page);
await waitForSuccessToast(page, 'Profile created');
const customCount = await countCustomProfiles(page);
expect(customCount).toBe(1);
});
});

View File

@@ -1,188 +0,0 @@
/**
* Project Creation End-to-End Tests
*
* Tests the project creation flows:
* 1. Creating a new blank project from the welcome view
* 2. Creating a new project from a GitHub template
*/
import { test, expect } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
import { createTempDirPath, cleanupTempDir, setupWelcomeView } from './utils';
// Create unique temp dir for this test run
const TEST_TEMP_DIR = createTempDirPath('project-creation-test');
test.describe('Project Creation', () => {
test.beforeAll(async () => {
// Create test temp directory
if (!fs.existsSync(TEST_TEMP_DIR)) {
fs.mkdirSync(TEST_TEMP_DIR, { recursive: true });
}
});
test.afterAll(async () => {
// Cleanup temp directory
cleanupTempDir(TEST_TEMP_DIR);
});
test('should create a new blank project from welcome view', async ({ page }) => {
const projectName = `test-project-${Date.now()}`;
// Set up welcome view with workspace directory pre-configured
await setupWelcomeView(page, { workspaceDir: TEST_TEMP_DIR });
// Navigate to the app
await page.goto('/');
await page.waitForLoadState('networkidle');
// Wait for welcome view to be visible
await expect(page.locator('[data-testid="welcome-view"]')).toBeVisible({ timeout: 10000 });
// Click the "Create New Project" dropdown button
const createButton = page.locator('[data-testid="create-new-project"]');
await expect(createButton).toBeVisible();
await createButton.click();
// Click "Quick Setup" option from the dropdown
const quickSetupOption = page.locator('[data-testid="quick-setup-option"]');
await expect(quickSetupOption).toBeVisible();
await quickSetupOption.click();
// Wait for the new project modal to appear
const modal = page.locator('[data-testid="new-project-modal"]');
await expect(modal).toBeVisible({ timeout: 5000 });
// Enter the project name
const projectNameInput = page.locator('[data-testid="project-name-input"]');
await expect(projectNameInput).toBeVisible();
await projectNameInput.fill(projectName);
// Verify the workspace directory is shown (from our pre-configured localStorage)
// Wait for workspace to be loaded (it shows "Will be created at:" when ready)
await expect(page.getByText('Will be created at:')).toBeVisible({ timeout: 5000 });
// Click the Create Project button
const createProjectButton = page.locator('[data-testid="confirm-create-project"]');
await expect(createProjectButton).toBeVisible();
await createProjectButton.click();
// Wait for project creation to complete
// The app may show an init dialog briefly and then navigate to board view
// We just need to verify we end up on the board view with our project
// Wait for the board view - this confirms the project was created and opened
await expect(page.locator('[data-testid="board-view"]')).toBeVisible({ timeout: 15000 });
// Verify the project name appears in the project selector (sidebar)
await expect(
page.locator('[data-testid="project-selector"]').getByText(projectName)
).toBeVisible({ timeout: 5000 });
// Verify the project was created in the filesystem
const projectPath = path.join(TEST_TEMP_DIR, projectName);
expect(fs.existsSync(projectPath)).toBe(true);
// Verify .automaker directory was created
const automakerDir = path.join(projectPath, '.automaker');
expect(fs.existsSync(automakerDir)).toBe(true);
// Verify app_spec.txt was created
const appSpecPath = path.join(automakerDir, 'app_spec.txt');
expect(fs.existsSync(appSpecPath)).toBe(true);
// Verify the app_spec.txt contains the project name
const appSpecContent = fs.readFileSync(appSpecPath, 'utf-8');
expect(appSpecContent).toContain(projectName);
});
test('should create a new project from GitHub template', async ({ page }) => {
// Increase timeout for this test since git clone takes time
test.setTimeout(60000);
const projectName = `template-project-${Date.now()}`;
// Set up welcome view with workspace directory pre-configured
await setupWelcomeView(page, { workspaceDir: TEST_TEMP_DIR });
// Navigate to the app
await page.goto('/');
await page.waitForLoadState('networkidle');
// Wait for welcome view to be visible
await expect(page.locator('[data-testid="welcome-view"]')).toBeVisible({ timeout: 10000 });
// Click the "Create New Project" dropdown button
const createButton = page.locator('[data-testid="create-new-project"]');
await expect(createButton).toBeVisible();
await createButton.click();
// Click "Quick Setup" option from the dropdown
const quickSetupOption = page.locator('[data-testid="quick-setup-option"]');
await expect(quickSetupOption).toBeVisible();
await quickSetupOption.click();
// Wait for the new project modal to appear
const modal = page.locator('[data-testid="new-project-modal"]');
await expect(modal).toBeVisible({ timeout: 5000 });
// Enter the project name first
const projectNameInput = page.locator('[data-testid="project-name-input"]');
await expect(projectNameInput).toBeVisible();
await projectNameInput.fill(projectName);
// Wait for workspace directory to be loaded
await expect(page.getByText('Will be created at:')).toBeVisible({ timeout: 5000 });
// Click on the "Starter Kit" tab
const starterKitTab = modal.getByText('Starter Kit');
await expect(starterKitTab).toBeVisible();
await starterKitTab.click();
// Select the first template (Automaker Starter Kit)
const firstTemplate = page.locator('[data-testid="template-automaker-starter-kit"]');
await expect(firstTemplate).toBeVisible();
await firstTemplate.click();
// Verify the template is selected (check mark should appear)
await expect(firstTemplate.locator('.lucide-check')).toBeVisible();
// Click the Create Project button
const createProjectButton = page.locator('[data-testid="confirm-create-project"]');
await expect(createProjectButton).toBeVisible();
await createProjectButton.click();
// Wait for git clone to complete and board view to appear
// This takes longer due to the git clone operation
await expect(page.locator('[data-testid="board-view"]')).toBeVisible({ timeout: 45000 });
// Verify the project name appears in the project selector (sidebar)
await expect(
page.locator('[data-testid="project-selector"]').getByText(projectName)
).toBeVisible({ timeout: 5000 });
// Verify the project was cloned in the filesystem
const projectPath = path.join(TEST_TEMP_DIR, projectName);
expect(fs.existsSync(projectPath)).toBe(true);
// Verify .automaker directory was created
const automakerDir = path.join(projectPath, '.automaker');
expect(fs.existsSync(automakerDir)).toBe(true);
// Verify app_spec.txt was created with template info
const appSpecPath = path.join(automakerDir, 'app_spec.txt');
expect(fs.existsSync(appSpecPath)).toBe(true);
const appSpecContent = fs.readFileSync(appSpecPath, 'utf-8');
expect(appSpecContent).toContain(projectName);
expect(appSpecContent).toContain('Automaker Starter Kit');
// Verify the template files were cloned (check for package.json which should exist in the template)
const packageJsonPath = path.join(projectPath, 'package.json');
expect(fs.existsSync(packageJsonPath)).toBe(true);
// Verify it's a git repository (cloned from GitHub)
const gitDir = path.join(projectPath, '.git');
expect(fs.existsSync(gitDir)).toBe(true);
});
});

View File

@@ -0,0 +1,53 @@
/**
* Project Creation E2E Test
*
* Happy path: Create a new blank project from welcome view
*/
import { test, expect } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
import { createTempDirPath, cleanupTempDir, setupWelcomeView } from '../utils';
const TEST_TEMP_DIR = createTempDirPath('project-creation-test');
test.describe('Project Creation', () => {
test.beforeAll(async () => {
if (!fs.existsSync(TEST_TEMP_DIR)) {
fs.mkdirSync(TEST_TEMP_DIR, { recursive: true });
}
});
test.afterAll(async () => {
cleanupTempDir(TEST_TEMP_DIR);
});
test('should create a new blank project from welcome view', async ({ page }) => {
const projectName = `test-project-${Date.now()}`;
await setupWelcomeView(page, { workspaceDir: TEST_TEMP_DIR });
await page.goto('/');
await page.waitForLoadState('networkidle');
await expect(page.locator('[data-testid="welcome-view"]')).toBeVisible({ timeout: 10000 });
await page.locator('[data-testid="create-new-project"]').click();
await page.locator('[data-testid="quick-setup-option"]').click();
await expect(page.locator('[data-testid="new-project-modal"]')).toBeVisible({ timeout: 5000 });
await page.locator('[data-testid="project-name-input"]').fill(projectName);
await expect(page.getByText('Will be created at:')).toBeVisible({ timeout: 5000 });
await page.locator('[data-testid="confirm-create-project"]').click();
await expect(page.locator('[data-testid="board-view"]')).toBeVisible({ timeout: 15000 });
await expect(
page.locator('[data-testid="project-selector"]').getByText(projectName)
).toBeVisible({ timeout: 5000 });
const projectPath = path.join(TEST_TEMP_DIR, projectName);
expect(fs.existsSync(projectPath)).toBe(true);
expect(fs.existsSync(path.join(projectPath, '.automaker'))).toBe(true);
});
});

View File

@@ -11,7 +11,7 @@
import { test, expect } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
import { createTempDirPath, cleanupTempDir, setupWelcomeView } from './utils';
import { createTempDirPath, cleanupTempDir, setupWelcomeView } from '../utils';
// Create unique temp dir for this test run
const TEST_TEMP_DIR = createTempDirPath('open-project-test');

View File

@@ -1,362 +0,0 @@
import { test, expect } from '@playwright/test';
import {
resetFixtureSpec,
setupProjectWithFixture,
getFixturePath,
navigateToSpecEditor,
getEditorContent,
setEditorContent,
clickSaveButton,
getByTestId,
clickElement,
fillInput,
waitForNetworkIdle,
waitForElement,
} from './utils';
test.describe('Spec Editor Persistence', () => {
test.beforeEach(async () => {
// Reset the fixture spec file to original content before each test
resetFixtureSpec();
});
test.afterEach(async () => {
// Clean up - reset the spec file after each test
resetFixtureSpec();
});
test('should open project, edit spec, save, and persist changes after refresh', async ({
page,
}) => {
// Use the resolved fixture path
const fixturePath = getFixturePath();
// Step 1: Set up the project in localStorage pointing to our fixture
await setupProjectWithFixture(page, fixturePath);
// Step 2: Navigate to the app
await page.goto('/');
await waitForNetworkIdle(page);
// Step 3: Verify we're on the dashboard with the project loaded
// The sidebar should show the project selector
const sidebar = await getByTestId(page, 'sidebar');
await sidebar.waitFor({ state: 'visible', timeout: 10000 });
// Step 4: Click on the Spec Editor in the sidebar
await navigateToSpecEditor(page);
// Step 5: Wait for the spec view to load (not empty state)
await waitForElement(page, 'spec-view', { timeout: 10000 });
// Step 6: Wait for the spec editor to load
const specEditor = await getByTestId(page, 'spec-editor');
await specEditor.waitFor({ state: 'visible', timeout: 10000 });
// Step 7: Wait for CodeMirror to initialize (it has a .cm-content element)
await specEditor.locator('.cm-content').waitFor({ state: 'visible', timeout: 10000 });
// Step 8: Modify the editor content to "hello world"
await setEditorContent(page, 'hello world');
// Verify content was set before saving
const contentBeforeSave = await getEditorContent(page);
expect(contentBeforeSave.trim()).toBe('hello world');
// Step 9: Click the save button and wait for save to complete
await clickSaveButton(page);
// Step 10: Refresh the page
await page.reload();
await waitForNetworkIdle(page);
// Step 11: Navigate back to the spec editor
// After reload, we need to wait for the app to initialize
await waitForElement(page, 'sidebar', { timeout: 10000 });
// Navigate to spec editor again
await navigateToSpecEditor(page);
// Wait for CodeMirror to be ready
const specEditorAfterReload = await getByTestId(page, 'spec-editor');
await specEditorAfterReload
.locator('.cm-content')
.waitFor({ state: 'visible', timeout: 10000 });
// Wait for CodeMirror content to update with the loaded spec
// The spec might need time to load into the editor after page reload
let contentMatches = false;
let attempts = 0;
const maxAttempts = 30; // Try for up to 30 seconds with 1-second intervals
while (!contentMatches && attempts < maxAttempts) {
try {
const contentElement = page.locator('[data-testid="spec-editor"] .cm-content');
const text = await contentElement.textContent();
if (text && text.trim() === 'hello world') {
contentMatches = true;
break;
}
} catch {
// Element might not be ready yet, continue
}
if (!contentMatches) {
await page.waitForTimeout(1000);
attempts++;
}
}
// If we didn't get the right content with our polling, use the fallback
if (!contentMatches) {
await page.waitForFunction(
(expectedContent) => {
const contentElement = document.querySelector('[data-testid="spec-editor"] .cm-content');
if (!contentElement) return false;
const text = (contentElement.textContent || '').trim();
return text === expectedContent;
},
'hello world',
{ timeout: 10000 }
);
}
// Step 12: Verify the content was persisted
const persistedContent = await getEditorContent(page);
expect(persistedContent.trim()).toBe('hello world');
});
test('should handle opening project via Open Project button and file browser', async ({
page,
}) => {
// This test covers the flow of:
// 1. Clicking Open Project button
// 2. Using the file browser to navigate to the fixture directory
// 3. Opening the project
// 4. Editing the spec
// Set up without a current project to test the open project flow
await page.addInitScript(() => {
const mockState = {
state: {
projects: [],
currentProject: null,
currentView: 'welcome',
theme: 'dark',
sidebarOpen: true,
apiKeys: { anthropic: '', google: '' },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: 3,
},
version: 0,
};
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
// Mark setup as complete
const setupState = {
state: {
isFirstRun: false,
setupComplete: true,
currentStep: 'complete',
skipClaudeSetup: false,
},
version: 0,
};
localStorage.setItem('automaker-setup', JSON.stringify(setupState));
});
// Navigate to the app
await page.goto('/');
await waitForNetworkIdle(page);
// Wait for the sidebar to be visible
const sidebar = await getByTestId(page, 'sidebar');
await sidebar.waitFor({ state: 'visible', timeout: 10000 });
// Click the Open Project button
const openProjectButton = await getByTestId(page, 'open-project-button');
// Check if the button is visible (it might not be in collapsed sidebar)
const isButtonVisible = await openProjectButton.isVisible().catch(() => false);
if (isButtonVisible) {
await clickElement(page, 'open-project-button');
// The file browser dialog should open
// Note: In web mode, this might use the FileBrowserDialog component
// which makes requests to the backend server at /api/fs/browse
// Wait a bit to see if a dialog appears
await page.waitForTimeout(1000);
// Check if a dialog is visible
const dialog = page.locator('[role="dialog"]');
const dialogVisible = await dialog.isVisible().catch(() => false);
if (dialogVisible) {
// If file browser dialog is open, we need to navigate to the fixture path
// This depends on the current directory structure
// For now, let's verify the dialog appeared and close it
// A full test would navigate through directories
console.log('File browser dialog opened successfully');
// Press Escape to close the dialog
await page.keyboard.press('Escape');
}
}
// For a complete e2e test with file browsing, we'd need to:
// 1. Navigate through the directory tree
// 2. Select the projectA directory
// 3. Click "Select Current Folder"
// Since this involves actual file system navigation,
// and depends on the backend server being properly configured,
// we'll verify the basic UI elements are present
expect(sidebar).toBeTruthy();
});
});
test.describe('Spec Editor - Full Open Project Flow', () => {
test.beforeEach(async () => {
// Reset the fixture spec file to original content before each test
resetFixtureSpec();
});
test.afterEach(async () => {
// Clean up - reset the spec file after each test
resetFixtureSpec();
});
// Skip in CI - file browser navigation is flaky in headless environments
test.skip('should open project via file browser, edit spec, and persist', async ({ page }) => {
// Navigate to app first
await page.goto('/');
await waitForNetworkIdle(page);
// Set up localStorage state (without a current project, but mark setup complete)
// Using evaluate instead of addInitScript so it only runs once
// Note: In CI, setup wizard is also skipped via NEXT_PUBLIC_SKIP_SETUP env var
await page.evaluate(() => {
const mockState = {
state: {
projects: [],
currentProject: null,
currentView: 'welcome',
theme: 'dark',
sidebarOpen: true,
apiKeys: { anthropic: '', google: '' },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: 3,
},
version: 0,
};
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
// Mark setup as complete (fallback for when NEXT_PUBLIC_SKIP_SETUP isn't set)
const setupState = {
state: {
isFirstRun: false,
setupComplete: true,
currentStep: 'complete',
skipClaudeSetup: false,
},
version: 0,
};
localStorage.setItem('automaker-setup', JSON.stringify(setupState));
});
// Reload to apply the localStorage state
await page.reload();
await waitForNetworkIdle(page);
// Wait for sidebar
await waitForElement(page, 'sidebar', { timeout: 10000 });
// Click the Open Project button
const openProjectButton = await getByTestId(page, 'open-project-button');
await openProjectButton.waitFor({ state: 'visible', timeout: 10000 });
await clickElement(page, 'open-project-button');
// Wait for the file browser dialog to open
const dialogTitle = page.locator('text="Select Project Directory"');
await dialogTitle.waitFor({ state: 'visible', timeout: 10000 });
// Wait for the dialog to fully load (loading to complete)
await page.waitForFunction(
() => !document.body.textContent?.includes('Loading directories...'),
{ timeout: 10000 }
);
// Use the path input to directly navigate to the fixture directory
const pathInput = await getByTestId(page, 'path-input');
await pathInput.waitFor({ state: 'visible', timeout: 5000 });
// Clear the input and type the full path to the fixture
await fillInput(page, 'path-input', getFixturePath());
// Click the Go button to navigate to the path
await clickElement(page, 'go-to-path-button');
// Wait for loading to complete
await page.waitForFunction(
() => !document.body.textContent?.includes('Loading directories...'),
{ timeout: 10000 }
);
// Verify we're in the right directory by checking the path display
const pathDisplay = page.locator('.font-mono.text-sm.truncate');
await expect(pathDisplay).toContainText('projectA');
// Click "Select Current Folder" button
const selectFolderButton = page.locator('button:has-text("Select Current Folder")');
await selectFolderButton.click();
// Wait for dialog to close and project to load
await page.waitForFunction(() => !document.querySelector('[role="dialog"]'), {
timeout: 10000,
});
await page.waitForTimeout(500);
// Navigate to spec editor
const specNav = await getByTestId(page, 'nav-spec');
await specNav.waitFor({ state: 'visible', timeout: 10000 });
await clickElement(page, 'nav-spec');
// Wait for spec view with the editor (not the empty state)
await waitForElement(page, 'spec-view', { timeout: 10000 });
const specEditorForOpenFlow = await getByTestId(page, 'spec-editor');
await specEditorForOpenFlow
.locator('.cm-content')
.waitFor({ state: 'visible', timeout: 10000 });
await page.waitForTimeout(500);
// Edit the content
await setEditorContent(page, 'hello world');
// Click save button
await clickSaveButton(page);
// Refresh and verify persistence
await page.reload();
await waitForNetworkIdle(page);
// Navigate back to spec editor
await specNav.waitFor({ state: 'visible', timeout: 10000 });
await clickElement(page, 'nav-spec');
const specEditorAfterRefresh = await getByTestId(page, 'spec-editor');
await specEditorAfterRefresh
.locator('.cm-content')
.waitFor({ state: 'visible', timeout: 10000 });
await page.waitForTimeout(500);
// Verify the content persisted
const persistedContent = await getEditorContent(page);
expect(persistedContent.trim()).toBe('hello world');
});
});

View File

@@ -353,7 +353,7 @@ export async function setupProjectWithPath(page: Page, projectPath: string): Pro
currentStep: 'complete',
skipClaudeSetup: false,
},
version: 2, // Must match app-store.ts persist version
version: 0, // setup-store.ts doesn't specify a version, so zustand defaults to 0
};
localStorage.setItem('automaker-setup', JSON.stringify(setupState));
}, projectPath);
@@ -404,7 +404,7 @@ export async function setupProjectWithPathNoWorktrees(
currentStep: 'complete',
skipClaudeSetup: false,
},
version: 2, // Must match app-store.ts persist version
version: 0, // setup-store.ts doesn't specify a version, so zustand defaults to 0
};
localStorage.setItem('automaker-setup', JSON.stringify(setupState));
}, projectPath);
@@ -459,7 +459,7 @@ export async function setupProjectWithStaleWorktree(
currentStep: 'complete',
skipClaudeSetup: false,
},
version: 2, // Must match app-store.ts persist version
version: 0, // setup-store.ts doesn't specify a version, so zustand defaults to 0
};
localStorage.setItem('automaker-setup', JSON.stringify(setupState));
}, projectPath);

View File

@@ -107,7 +107,7 @@ export async function setupProjectWithFixture(
currentStep: 'complete',
skipClaudeSetup: false,
},
version: 2, // Must match app-store.ts persist version
version: 0, // setup-store.ts doesn't specify a version, so zustand defaults to 0
};
localStorage.setItem('automaker-setup', JSON.stringify(setupState));
}, projectPath);

View File

@@ -875,7 +875,7 @@ export async function setupMockProjectWithProfiles(
currentStep: 'complete',
skipClaudeSetup: false,
},
version: 2, // Must match app-store.ts persist version
version: 0, // setup-store.ts doesn't specify a version, so zustand defaults to 0
};
localStorage.setItem('automaker-setup', JSON.stringify(setupState));
}, options);

File diff suppressed because it is too large Load Diff

2
package-lock.json generated
View File

@@ -1212,7 +1212,7 @@
},
"node_modules/@electron/node-gyp": {
"version": "10.2.0-electron.1",
"resolved": "git+ssh://git@github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2",
"resolved": "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2",
"integrity": "sha512-4MSBTT8y07YUDqf69/vSh80Hh791epYqGtWHO3zSKhYFwQg+gx9wi1PqbqP6YqC4WMsNxZ5l9oDmnWdK5pfCKQ==",
"dev": true,
"license": "MIT",