mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 08:33:36 +00:00
simplify the e2e tests
This commit is contained in:
89
apps/ui/tests/agent/start-new-chat-session.spec.ts
Normal file
89
apps/ui/tests/agent/start-new-chat-session.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
146
apps/ui/tests/context/add-context-image.spec.ts
Normal file
146
apps/ui/tests/context/add-context-image.spec.ts
Normal 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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
67
apps/ui/tests/context/context-file-management.spec.ts
Normal file
67
apps/ui/tests/context/context-file-management.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
75
apps/ui/tests/context/delete-context-file.spec.ts
Normal file
75
apps/ui/tests/context/delete-context-file.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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!');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
85
apps/ui/tests/features/add-feature-to-backlog.spec.ts
Normal file
85
apps/ui/tests/features/add-feature-to-backlog.spec.ts
Normal 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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
139
apps/ui/tests/features/edit-feature.spec.ts
Normal file
139
apps/ui/tests/features/edit-feature.spec.ts
Normal 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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
119
apps/ui/tests/features/feature-manual-review-flow.spec.ts
Normal file
119
apps/ui/tests/features/feature-manual-review-flow.spec.ts
Normal 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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
107
apps/ui/tests/features/feature-skip-tests-toggle.spec.ts
Normal file
107
apps/ui/tests/features/feature-skip-tests-toggle.spec.ts
Normal 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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
60
apps/ui/tests/git/worktree-integration.spec.ts
Normal file
60
apps/ui/tests/git/worktree-integration.spec.ts
Normal 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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
43
apps/ui/tests/profiles/profiles-crud.spec.ts
Normal file
43
apps/ui/tests/profiles/profiles-crud.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
53
apps/ui/tests/projects/new-project-creation.spec.ts
Normal file
53
apps/ui/tests/projects/new-project-creation.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
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
|
// Create unique temp dir for this test run
|
||||||
const TEST_TEMP_DIR = createTempDirPath('open-project-test');
|
const TEST_TEMP_DIR = createTempDirPath('open-project-test');
|
||||||
@@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -353,7 +353,7 @@ export async function setupProjectWithPath(page: Page, projectPath: string): Pro
|
|||||||
currentStep: 'complete',
|
currentStep: 'complete',
|
||||||
skipClaudeSetup: false,
|
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));
|
localStorage.setItem('automaker-setup', JSON.stringify(setupState));
|
||||||
}, projectPath);
|
}, projectPath);
|
||||||
@@ -404,7 +404,7 @@ export async function setupProjectWithPathNoWorktrees(
|
|||||||
currentStep: 'complete',
|
currentStep: 'complete',
|
||||||
skipClaudeSetup: false,
|
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));
|
localStorage.setItem('automaker-setup', JSON.stringify(setupState));
|
||||||
}, projectPath);
|
}, projectPath);
|
||||||
@@ -459,7 +459,7 @@ export async function setupProjectWithStaleWorktree(
|
|||||||
currentStep: 'complete',
|
currentStep: 'complete',
|
||||||
skipClaudeSetup: false,
|
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));
|
localStorage.setItem('automaker-setup', JSON.stringify(setupState));
|
||||||
}, projectPath);
|
}, projectPath);
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export async function setupProjectWithFixture(
|
|||||||
currentStep: 'complete',
|
currentStep: 'complete',
|
||||||
skipClaudeSetup: false,
|
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));
|
localStorage.setItem('automaker-setup', JSON.stringify(setupState));
|
||||||
}, projectPath);
|
}, projectPath);
|
||||||
|
|||||||
@@ -875,7 +875,7 @@ export async function setupMockProjectWithProfiles(
|
|||||||
currentStep: 'complete',
|
currentStep: 'complete',
|
||||||
skipClaudeSetup: false,
|
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));
|
localStorage.setItem('automaker-setup', JSON.stringify(setupState));
|
||||||
}, options);
|
}, options);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user