feat: enhance ESLint configuration and improve component error handling

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

These changes aim to enhance the development experience by improving linting support and simplifying error handling across components.
This commit is contained in:
Kacper
2025-12-21 23:08:08 +01:00
parent 43c93fe19a
commit 26236d3d5b
40 changed files with 2013 additions and 2587 deletions

View File

@@ -15,11 +15,11 @@
* 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 { 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,
@@ -29,15 +29,14 @@ import {
setupProjectWithPathNoWorktrees,
waitForBoardView,
clickAddFeature,
fillAddFeatureDialog,
confirmAddFeature,
dragAndDropWithDndKit,
} from "./utils";
} from './utils';
const execAsync = promisify(exec);
// Create unique temp dir for this test run
const TEST_TEMP_DIR = createTempDirPath("feature-lifecycle-tests");
const TEST_TEMP_DIR = createTempDirPath('feature-lifecycle-tests');
interface TestRepo {
path: string;
@@ -45,9 +44,9 @@ interface TestRepo {
}
// Configure all tests to run serially
test.describe.configure({ mode: "serial" });
test.describe.configure({ mode: 'serial' });
test.describe("Feature Lifecycle Tests", () => {
test.describe('Feature Lifecycle Tests', () => {
let testRepo: TestRepo;
let featureId: string;
@@ -76,7 +75,7 @@ test.describe("Feature Lifecycle Tests", () => {
});
// 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 ({
test.skip('complete feature lifecycle: create -> in_progress -> waiting_approval -> commit -> verified -> archive -> restore -> delete', async ({
page,
}) => {
// Increase timeout for this comprehensive test
@@ -87,7 +86,7 @@ test.describe("Feature Lifecycle Tests", () => {
// ==========================================================================
// Use no-worktrees setup to avoid worktree-related filtering/initialization issues
await setupProjectWithPathNoWorktrees(page, testRepo.path);
await page.goto("/");
await page.goto('/');
await waitForNetworkIdle(page);
await waitForBoardView(page);
@@ -98,18 +97,15 @@ test.describe("Feature Lifecycle Tests", () => {
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();
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");
const featuresDir = path.join(testRepo.path, '.automaker', 'features');
// Wait for the feature to be created in the filesystem
await expect(async () => {
@@ -131,18 +127,14 @@ test.describe("Feature Lifecycle Tests", () => {
featureId = featureDirs[0];
// Now get the actual card element by testid
const featureCardByTestId = page.locator(
`[data-testid="kanban-card-${featureId}"]`
);
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"]'
);
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);
@@ -151,13 +143,10 @@ test.describe("Feature Lifecycle Tests", () => {
// 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"
)
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);
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)
@@ -165,12 +154,9 @@ test.describe("Feature Lifecycle Tests", () => {
// 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"
)
fs.readFileSync(path.join(featuresDir, featureId, 'feature.json'), 'utf-8')
);
expect(featureData.status).toBe("waiting_approval");
expect(featureData.status).toBe('waiting_approval');
}).toPass({ timeout: 30000 });
// Refresh page to ensure UI reflects the status change
@@ -181,19 +167,17 @@ test.describe("Feature Lifecycle Tests", () => {
// ==========================================================================
// Step 3: Verify feature is in waiting_approval (manual review) column
// ==========================================================================
const waitingApprovalColumn = page.locator(
'[data-testid="kanban-column-waiting_approval"]'
);
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");
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");
const yellowContent = fs.readFileSync(yellowFilePath, 'utf-8');
expect(yellowContent).toBe('yellow');
// ==========================================================================
// Step 4: Click commit and verify git status shows committed changes
@@ -207,18 +191,18 @@ test.describe("Feature Lifecycle Tests", () => {
await page.waitForTimeout(2000);
// Verify git status shows clean (changes committed)
const { stdout: gitStatus } = await execAsync("git status --porcelain", {
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);
expect(gitStatus.includes('yellow.txt')).toBe(false);
// Verify the commit exists in git log
const { stdout: gitLog } = await execAsync("git log --oneline -1", {
const { stdout: gitLog } = await execAsync('git log --oneline -1', {
cwd: testRepo.path,
});
expect(gitLog.toLowerCase()).toContain("yellow");
expect(gitLog.toLowerCase()).toContain('yellow');
// ==========================================================================
// Step 5: Verify feature moved to verified column after commit
@@ -228,21 +212,15 @@ test.describe("Feature Lifecycle Tests", () => {
await waitForNetworkIdle(page);
await waitForBoardView(page);
const verifiedColumn = page.locator(
'[data-testid="kanban-column-verified"]'
);
const cardInVerified = verifiedColumn.locator(
`[data-testid="kanban-card-${featureId}"]`
);
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}"]`
);
const completeButton = page.locator(`[data-testid="complete-${featureId}"]`);
await expect(completeButton).toBeVisible({ timeout: 5000 });
await completeButton.click();
@@ -254,39 +232,28 @@ test.describe("Feature Lifecycle Tests", () => {
// Verify feature status is completed in filesystem
const featureData = JSON.parse(
fs.readFileSync(
path.join(featuresDir, featureId, "feature.json"),
"utf-8"
)
fs.readFileSync(path.join(featuresDir, featureId, 'feature.json'), 'utf-8')
);
expect(featureData.status).toBe("completed");
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"]'
);
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"]'
);
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}"]`
);
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}"]`
);
const restoreButton = page.locator(`[data-testid="unarchive-${featureId}"]`);
await expect(restoreButton).toBeVisible({ timeout: 5000 });
await restoreButton.click();
@@ -294,47 +261,34 @@ test.describe("Feature Lifecycle Tests", () => {
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();
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}"]`
);
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"
)
fs.readFileSync(path.join(featuresDir, featureId, 'feature.json'), 'utf-8')
);
expect(restoredFeatureData.status).toBe("verified");
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}"]`
);
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"]'
);
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"]'
);
const confirmDeleteButton = page.locator('[data-testid="confirm-delete-button"]');
await confirmDeleteButton.click();
// Wait for the delete action to complete
@@ -361,7 +315,7 @@ test.describe("Feature Lifecycle Tests", () => {
// Step 1: Setup and create a feature in backlog
// ==========================================================================
await setupProjectWithPathNoWorktrees(page, testRepo.path);
await page.goto("/");
await page.goto('/');
await waitForNetworkIdle(page);
await waitForBoardView(page);
await page.waitForTimeout(1000);
@@ -370,17 +324,15 @@ test.describe("Feature Lifecycle Tests", () => {
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();
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");
const featuresDir = path.join(testRepo.path, '.automaker', 'features');
await expect(async () => {
const dirs = fs.readdirSync(featuresDir);
expect(dirs.length).toBeGreaterThan(0);
@@ -396,36 +348,26 @@ test.describe("Feature Lifecycle Tests", () => {
await waitForBoardView(page);
// Wait for the feature card to appear
const featureCard = page.locator(
`[data-testid="kanban-card-${testFeatureId}"]`
);
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"]'
);
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"
);
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"));
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);
expect(['in_progress', 'waiting_approval']).toContain(featureData.status);
}).toPass({ timeout: 15000 });
// ==========================================================================
@@ -433,19 +375,14 @@ test.describe("Feature Lifecycle Tests", () => {
// ==========================================================================
// 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");
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
);
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();
@@ -457,12 +394,8 @@ test.describe("Feature Lifecycle Tests", () => {
// ==========================================================================
// 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}"]`
);
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);
@@ -470,8 +403,8 @@ test.describe("Feature Lifecycle Tests", () => {
// Verify feature is in backlog
await expect(async () => {
const data = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
expect(data.status).toBe("backlog");
const data = JSON.parse(fs.readFileSync(featureFilePath, 'utf-8'));
expect(data.status).toBe('backlog');
}).toPass({ timeout: 10000 });
// Reload to ensure clean state
@@ -482,55 +415,45 @@ test.describe("Feature Lifecycle Tests", () => {
// ==========================================================================
// Step 5: Restart the feature (drag to in_progress again)
// ==========================================================================
const restartCard = page.locator(
`[data-testid="kanban-card-${testFeatureId}"]`
);
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"]'
);
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") {
page.on('console', (msg) => {
if (msg.type() === 'error') {
consoleErrors.push(msg.text());
}
});
// Drag to in_progress to restart
await dragAndDropWithDndKit(
page,
restartDragHandle,
inProgressColumnRestart
);
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"));
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);
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")
(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");
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!");
console.log('Feature successfully restarted after stop!');
});
});

View File

@@ -1,4 +1,4 @@
import { test, expect } from "@playwright/test";
import { test, expect } from '@playwright/test';
import {
resetFixtureSpec,
setupProjectWithFixture,
@@ -12,9 +12,9 @@ import {
fillInput,
waitForNetworkIdle,
waitForElement,
} from "./utils";
} from './utils';
test.describe("Spec Editor Persistence", () => {
test.describe('Spec Editor Persistence', () => {
test.beforeEach(async () => {
// Reset the fixture spec file to original content before each test
resetFixtureSpec();
@@ -25,7 +25,7 @@ test.describe("Spec Editor Persistence", () => {
resetFixtureSpec();
});
test("should open project, edit spec, save, and persist changes after refresh", async ({
test('should open project, edit spec, save, and persist changes after refresh', async ({
page,
}) => {
// Use the resolved fixture path
@@ -35,33 +35,33 @@ test.describe("Spec Editor Persistence", () => {
await setupProjectWithFixture(page, fixturePath);
// Step 2: Navigate to the app
await page.goto("/");
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 });
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 });
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 });
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 });
await specEditor.locator('.cm-content').waitFor({ state: 'visible', timeout: 10000 });
// Step 8: Modify the editor content to "hello world"
await setEditorContent(page, "hello world");
await setEditorContent(page, 'hello world');
// Verify content was set before saving
const contentBeforeSave = await getEditorContent(page);
expect(contentBeforeSave.trim()).toBe("hello world");
expect(contentBeforeSave.trim()).toBe('hello world');
// Step 9: Click the save button and wait for save to complete
await clickSaveButton(page);
@@ -72,14 +72,16 @@ test.describe("Spec Editor Persistence", () => {
// 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 });
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 });
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
@@ -91,11 +93,11 @@ test.describe("Spec Editor Persistence", () => {
try {
const contentElement = page.locator('[data-testid="spec-editor"] .cm-content');
const text = await contentElement.textContent();
if (text && text.trim() === "hello world") {
if (text && text.trim() === 'hello world') {
contentMatches = true;
break;
}
} catch (e) {
} catch {
// Element might not be ready yet, continue
}
@@ -111,20 +113,20 @@ test.describe("Spec Editor Persistence", () => {
(expectedContent) => {
const contentElement = document.querySelector('[data-testid="spec-editor"] .cm-content');
if (!contentElement) return false;
const text = (contentElement.textContent || "").trim();
const text = (contentElement.textContent || '').trim();
return text === expectedContent;
},
"hello world",
'hello world',
{ timeout: 10000 }
);
}
// Step 12: Verify the content was persisted
const persistedContent = await getEditorContent(page);
expect(persistedContent.trim()).toBe("hello world");
expect(persistedContent.trim()).toBe('hello world');
});
test("should handle opening project via Open Project button and file browser", async ({
test('should handle opening project via Open Project button and file browser', async ({
page,
}) => {
// This test covers the flow of:
@@ -139,49 +141,47 @@ test.describe("Spec Editor Persistence", () => {
state: {
projects: [],
currentProject: null,
currentView: "welcome",
theme: "dark",
currentView: 'welcome',
theme: 'dark',
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
apiKeys: { anthropic: '', google: '' },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: 3,
},
version: 0,
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
// Mark setup as complete
const setupState = {
state: {
isFirstRun: false,
setupComplete: true,
currentStep: "complete",
currentStep: 'complete',
skipClaudeSetup: false,
},
version: 0,
};
localStorage.setItem("automaker-setup", JSON.stringify(setupState));
localStorage.setItem('automaker-setup', JSON.stringify(setupState));
});
// Navigate to the app
await page.goto("/");
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 });
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");
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);
const isButtonVisible = await openProjectButton.isVisible().catch(() => false);
if (isButtonVisible) {
await clickElement(page, "open-project-button");
await clickElement(page, 'open-project-button');
// The file browser dialog should open
// Note: In web mode, this might use the FileBrowserDialog component
@@ -200,10 +200,10 @@ test.describe("Spec Editor Persistence", () => {
// 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");
console.log('File browser dialog opened successfully');
// Press Escape to close the dialog
await page.keyboard.press("Escape");
await page.keyboard.press('Escape');
}
}
@@ -220,7 +220,7 @@ test.describe("Spec Editor Persistence", () => {
});
});
test.describe("Spec Editor - Full Open Project Flow", () => {
test.describe('Spec Editor - Full Open Project Flow', () => {
test.beforeEach(async () => {
// Reset the fixture spec file to original content before each test
resetFixtureSpec();
@@ -232,11 +232,9 @@ test.describe("Spec Editor - Full Open Project Flow", () => {
});
// 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,
}) => {
test.skip('should open project via file browser, edit spec, and persist', async ({ page }) => {
// Navigate to app first
await page.goto("/");
await page.goto('/');
await waitForNetworkIdle(page);
// Set up localStorage state (without a current project, but mark setup complete)
@@ -247,29 +245,29 @@ test.describe("Spec Editor - Full Open Project Flow", () => {
state: {
projects: [],
currentProject: null,
currentView: "welcome",
theme: "dark",
currentView: 'welcome',
theme: 'dark',
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
apiKeys: { anthropic: '', google: '' },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: 3,
},
version: 0,
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
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",
currentStep: 'complete',
skipClaudeSetup: false,
},
version: 0,
};
localStorage.setItem("automaker-setup", JSON.stringify(setupState));
localStorage.setItem('automaker-setup', JSON.stringify(setupState));
});
// Reload to apply the localStorage state
@@ -277,69 +275,68 @@ test.describe("Spec Editor - Full Open Project Flow", () => {
await waitForNetworkIdle(page);
// Wait for sidebar
await waitForElement(page, "sidebar", { timeout: 10000 });
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");
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 });
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..."),
() => !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 });
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());
await fillInput(page, 'path-input', getFixturePath());
// Click the Go button to navigate to the path
await clickElement(page, "go-to-path-button");
await clickElement(page, 'go-to-path-button');
// Wait for loading to complete
await page.waitForFunction(
() => !document.body.textContent?.includes("Loading directories..."),
() => !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");
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")'
);
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.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");
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 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");
await setEditorContent(page, 'hello world');
// Click save button
await clickSaveButton(page);
@@ -349,15 +346,17 @@ test.describe("Spec Editor - Full Open Project Flow", () => {
await waitForNetworkIdle(page);
// Navigate back to spec editor
await specNav.waitFor({ state: "visible", timeout: 10000 });
await clickElement(page, "nav-spec");
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 });
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");
expect(persistedContent.trim()).toBe('hello world');
});
});

View File

@@ -1,5 +1,4 @@
import { Page, Locator } from "@playwright/test";
import { waitForElement } from "../core/waiting";
import { Page, Locator } from '@playwright/test';
/**
* Wait for a toast notification with specific text to appear
@@ -12,7 +11,7 @@ export async function waitForToast(
const toast = page.locator(`[data-sonner-toast]:has-text("${text}")`).first();
await toast.waitFor({
timeout: options?.timeout ?? 5000,
state: "visible",
state: 'visible',
});
return toast;
}
@@ -32,19 +31,21 @@ export async function waitForErrorToast(
if (titleText) {
// First try specific error type, then fallback to any toast with text
const errorToast = page.locator(
`[data-sonner-toast][data-type="error"]:has-text("${titleText}"), [data-sonner-toast]:has-text("${titleText}")`
).first();
const errorToast = page
.locator(
`[data-sonner-toast][data-type="error"]:has-text("${titleText}"), [data-sonner-toast]:has-text("${titleText}")`
)
.first();
await errorToast.waitFor({
timeout,
state: "visible",
state: 'visible',
});
return errorToast;
} else {
const errorToast = page.locator('[data-sonner-toast][data-type="error"]').first();
await errorToast.waitFor({
timeout,
state: "visible",
state: 'visible',
});
return errorToast;
}
@@ -53,10 +54,7 @@ export async function waitForErrorToast(
/**
* Check if an error toast is visible
*/
export async function isErrorToastVisible(
page: Page,
titleText?: string
): Promise<boolean> {
export async function isErrorToastVisible(page: Page, titleText?: string): Promise<boolean> {
const toastSelector = titleText
? `[data-sonner-toast][data-type="error"]:has-text("${titleText}")`
: '[data-sonner-toast][data-type="error"]';
@@ -81,7 +79,7 @@ export async function waitForSuccessToast(
const toast = page.locator(toastSelector).first();
await toast.waitFor({
timeout: options?.timeout ?? 5000,
state: "visible",
state: 'visible',
});
return toast;
}

View File

@@ -3,12 +3,12 @@
* Provides helpers for creating test git repos and managing worktrees
*/
import * as fs from "fs";
import * as path from "path";
import { exec } from "child_process";
import { promisify } from "util";
import { Page } from "@playwright/test";
import { sanitizeBranchName, TIMEOUTS } from "../core/constants";
import * as fs from 'fs';
import * as path from 'path';
import { exec } from 'child_process';
import { promisify } from 'util';
import { Page } from '@playwright/test';
import { sanitizeBranchName, TIMEOUTS } from '../core/constants';
const execAsync = promisify(exec);
@@ -40,8 +40,8 @@ export interface FeatureData {
*/
function getWorkspaceRoot(): string {
const cwd = process.cwd();
if (cwd.includes("apps/ui")) {
return path.resolve(cwd, "../..");
if (cwd.includes('apps/ui')) {
return path.resolve(cwd, '../..');
}
return cwd;
}
@@ -49,9 +49,9 @@ function getWorkspaceRoot(): string {
/**
* Create a unique temp directory path for tests
*/
export function createTempDirPath(prefix: string = "temp-worktree-tests"): string {
export function createTempDirPath(prefix: string = 'temp-worktree-tests'): string {
const uniqueId = `${process.pid}-${Math.random().toString(36).substring(2, 9)}`;
return path.join(getWorkspaceRoot(), "test", `${prefix}-${uniqueId}`);
return path.join(getWorkspaceRoot(), 'test', `${prefix}-${uniqueId}`);
}
/**
@@ -59,7 +59,7 @@ export function createTempDirPath(prefix: string = "temp-worktree-tests"): strin
*/
export function getWorktreePath(projectPath: string, branchName: string): string {
const sanitizedName = sanitizeBranchName(branchName);
return path.join(projectPath, ".worktrees", sanitizedName);
return path.join(projectPath, '.worktrees', sanitizedName);
}
// ============================================================================
@@ -79,25 +79,25 @@ export async function createTestGitRepo(tempDir: string): Promise<TestRepo> {
fs.mkdirSync(tmpDir, { recursive: true });
// Initialize git repo
await execAsync("git init", { cwd: tmpDir });
await execAsync('git init', { cwd: tmpDir });
await execAsync('git config user.email "test@example.com"', { cwd: tmpDir });
await execAsync('git config user.name "Test User"', { cwd: tmpDir });
// Create initial commit
fs.writeFileSync(path.join(tmpDir, "README.md"), "# Test Project\n");
await execAsync("git add .", { cwd: tmpDir });
fs.writeFileSync(path.join(tmpDir, 'README.md'), '# Test Project\n');
await execAsync('git add .', { cwd: tmpDir });
await execAsync('git commit -m "Initial commit"', { cwd: tmpDir });
// Create main branch explicitly
await execAsync("git branch -M main", { cwd: tmpDir });
await execAsync('git branch -M main', { cwd: tmpDir });
// Create .automaker directories
const automakerDir = path.join(tmpDir, ".automaker");
const featuresDir = path.join(automakerDir, "features");
const automakerDir = path.join(tmpDir, '.automaker');
const featuresDir = path.join(automakerDir, 'features');
fs.mkdirSync(featuresDir, { recursive: true });
// Create empty categories.json to avoid ENOENT errors in tests
fs.writeFileSync(path.join(automakerDir, "categories.json"), "[]");
fs.writeFileSync(path.join(automakerDir, 'categories.json'), '[]');
return {
path: tmpDir,
@@ -113,16 +113,16 @@ export async function createTestGitRepo(tempDir: string): Promise<TestRepo> {
export async function cleanupTestRepo(repoPath: string): Promise<void> {
try {
// Remove all worktrees first
const { stdout } = await execAsync("git worktree list --porcelain", {
const { stdout } = await execAsync('git worktree list --porcelain', {
cwd: repoPath,
}).catch(() => ({ stdout: "" }));
}).catch(() => ({ stdout: '' }));
const worktrees = stdout
.split("\n\n")
.split('\n\n')
.slice(1) // Skip main worktree
.map((block) => {
const pathLine = block.split("\n").find((line) => line.startsWith("worktree "));
return pathLine ? pathLine.replace("worktree ", "") : null;
const pathLine = block.split('\n').find((line) => line.startsWith('worktree '));
return pathLine ? pathLine.replace('worktree ', '') : null;
})
.filter(Boolean);
@@ -139,7 +139,7 @@ export async function cleanupTestRepo(repoPath: string): Promise<void> {
// Remove the repository
fs.rmSync(repoPath, { recursive: true, force: true });
} catch (error) {
console.error("Failed to cleanup test repo:", error);
console.error('Failed to cleanup test repo:', error);
}
}
@@ -171,18 +171,18 @@ export async function gitExec(
*/
export async function listWorktrees(repoPath: string): Promise<string[]> {
try {
const { stdout } = await execAsync("git worktree list --porcelain", {
const { stdout } = await execAsync('git worktree list --porcelain', {
cwd: repoPath,
});
return stdout
.split("\n\n")
.split('\n\n')
.slice(1) // Skip main worktree
.map((block) => {
const pathLine = block.split("\n").find((line) => line.startsWith("worktree "));
const pathLine = block.split('\n').find((line) => line.startsWith('worktree '));
if (!pathLine) return null;
// Normalize path separators to OS native (git on Windows returns forward slashes)
const worktreePath = pathLine.replace("worktree ", "");
const worktreePath = pathLine.replace('worktree ', '');
return path.normalize(worktreePath);
})
.filter(Boolean) as string[];
@@ -195,10 +195,10 @@ export async function listWorktrees(repoPath: string): Promise<string[]> {
* Get list of git branches
*/
export async function listBranches(repoPath: string): Promise<string[]> {
const { stdout } = await execAsync("git branch --list", { cwd: repoPath });
const { stdout } = await execAsync('git branch --list', { cwd: repoPath });
return stdout
.split("\n")
.map((line) => line.trim().replace(/^[*+]\s*/, ""))
.split('\n')
.map((line) => line.trim().replace(/^[*+]\s*/, ''))
.filter(Boolean);
}
@@ -206,7 +206,7 @@ export async function listBranches(repoPath: string): Promise<string[]> {
* Get the current branch name
*/
export async function getCurrentBranch(repoPath: string): Promise<string> {
const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", { cwd: repoPath });
const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd: repoPath });
return stdout.trim();
}
@@ -233,7 +233,7 @@ export async function createWorktreeDirectly(
worktreePath?: string
): Promise<string> {
const sanitizedName = sanitizeBranchName(branchName);
const targetPath = worktreePath || path.join(repoPath, ".worktrees", sanitizedName);
const targetPath = worktreePath || path.join(repoPath, '.worktrees', sanitizedName);
await execAsync(`git worktree add "${targetPath}" -b ${branchName}`, { cwd: repoPath });
return targetPath;
@@ -257,7 +257,7 @@ export async function commitFile(
* Get the latest commit message
*/
export async function getLatestCommitMessage(repoPath: string): Promise<string> {
const { stdout } = await execAsync("git log --oneline -1", { cwd: repoPath });
const { stdout } = await execAsync('git log --oneline -1', { cwd: repoPath });
return stdout.trim();
}
@@ -268,32 +268,36 @@ export async function getLatestCommitMessage(repoPath: string): Promise<string>
/**
* Create a feature file in the test repo
*/
export function createTestFeature(repoPath: string, featureId: string, featureData: FeatureData): void {
const featuresDir = path.join(repoPath, ".automaker", "features");
export function createTestFeature(
repoPath: string,
featureId: string,
featureData: FeatureData
): void {
const featuresDir = path.join(repoPath, '.automaker', 'features');
const featureDir = path.join(featuresDir, featureId);
fs.mkdirSync(featureDir, { recursive: true });
fs.writeFileSync(path.join(featureDir, "feature.json"), JSON.stringify(featureData, null, 2));
fs.writeFileSync(path.join(featureDir, 'feature.json'), JSON.stringify(featureData, null, 2));
}
/**
* Read a feature file from the test repo
*/
export function readTestFeature(repoPath: string, featureId: string): FeatureData | null {
const featureFilePath = path.join(repoPath, ".automaker", "features", featureId, "feature.json");
const featureFilePath = path.join(repoPath, '.automaker', 'features', featureId, 'feature.json');
if (!fs.existsSync(featureFilePath)) {
return null;
}
return JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
return JSON.parse(fs.readFileSync(featureFilePath, 'utf-8'));
}
/**
* List all feature directories in the test repo
*/
export function listTestFeatures(repoPath: string): string[] {
const featuresDir = path.join(repoPath, ".automaker", "features");
const featuresDir = path.join(repoPath, '.automaker', 'features');
if (!fs.existsSync(featuresDir)) {
return [];
@@ -312,8 +316,8 @@ export function listTestFeatures(repoPath: string): string[] {
export async function setupProjectWithPath(page: Page, projectPath: string): Promise<void> {
await page.addInitScript((pathArg: string) => {
const mockProject = {
id: "test-project-worktree",
name: "Worktree Test Project",
id: 'test-project-worktree',
name: 'Worktree Test Project',
path: pathArg,
lastOpened: new Date().toISOString(),
};
@@ -322,36 +326,36 @@ export async function setupProjectWithPath(page: Page, projectPath: string): Pro
state: {
projects: [mockProject],
currentProject: mockProject,
currentView: "board",
theme: "dark",
currentView: 'board',
theme: 'dark',
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
apiKeys: { anthropic: '', google: '' },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: 3,
aiProfiles: [],
useWorktrees: true, // Enable worktree feature for tests
currentWorktreeByProject: {
[pathArg]: { path: null, branch: "main" }, // Initialize to main branch
[pathArg]: { path: null, branch: 'main' }, // Initialize to main branch
},
worktreesByProject: {},
},
version: 2, // Must match app-store.ts persist version
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
// Mark setup as complete to skip the setup wizard
const setupState = {
state: {
isFirstRun: false,
setupComplete: true,
currentStep: "complete",
currentStep: 'complete',
skipClaudeSetup: false,
},
version: 2, // Must match app-store.ts persist version
};
localStorage.setItem("automaker-setup", JSON.stringify(setupState));
localStorage.setItem('automaker-setup', JSON.stringify(setupState));
}, projectPath);
}
@@ -359,11 +363,14 @@ export async function setupProjectWithPath(page: Page, projectPath: string): Pro
* Set up localStorage with a project pointing to a test repo with worktrees DISABLED
* Use this to test scenarios where the worktree feature flag is off
*/
export async function setupProjectWithPathNoWorktrees(page: Page, projectPath: string): Promise<void> {
export async function setupProjectWithPathNoWorktrees(
page: Page,
projectPath: string
): Promise<void> {
await page.addInitScript((pathArg: string) => {
const mockProject = {
id: "test-project-no-worktree",
name: "Test Project (No Worktrees)",
id: 'test-project-no-worktree',
name: 'Test Project (No Worktrees)',
path: pathArg,
lastOpened: new Date().toISOString(),
};
@@ -372,10 +379,10 @@ export async function setupProjectWithPathNoWorktrees(page: Page, projectPath: s
state: {
projects: [mockProject],
currentProject: mockProject,
currentView: "board",
theme: "dark",
currentView: 'board',
theme: 'dark',
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
apiKeys: { anthropic: '', google: '' },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: 3,
@@ -387,19 +394,19 @@ export async function setupProjectWithPathNoWorktrees(page: Page, projectPath: s
version: 2, // Must match app-store.ts persist version
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
// Mark setup as complete to skip the setup wizard
const setupState = {
state: {
isFirstRun: false,
setupComplete: true,
currentStep: "complete",
currentStep: 'complete',
skipClaudeSetup: false,
},
version: 2, // Must match app-store.ts persist version
};
localStorage.setItem("automaker-setup", JSON.stringify(setupState));
localStorage.setItem('automaker-setup', JSON.stringify(setupState));
}, projectPath);
}
@@ -408,11 +415,14 @@ export async function setupProjectWithPathNoWorktrees(page: Page, projectPath: s
* The currentWorktreeByProject points to a worktree path that no longer exists
* This simulates the scenario where a user previously selected a worktree that was later deleted
*/
export async function setupProjectWithStaleWorktree(page: Page, projectPath: string): Promise<void> {
export async function setupProjectWithStaleWorktree(
page: Page,
projectPath: string
): Promise<void> {
await page.addInitScript((pathArg: string) => {
const mockProject = {
id: "test-project-stale-worktree",
name: "Stale Worktree Test Project",
id: 'test-project-stale-worktree',
name: 'Stale Worktree Test Project',
path: pathArg,
lastOpened: new Date().toISOString(),
};
@@ -421,10 +431,10 @@ export async function setupProjectWithStaleWorktree(page: Page, projectPath: str
state: {
projects: [mockProject],
currentProject: mockProject,
currentView: "board",
theme: "dark",
currentView: 'board',
theme: 'dark',
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
apiKeys: { anthropic: '', google: '' },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: 3,
@@ -432,26 +442,26 @@ export async function setupProjectWithStaleWorktree(page: Page, projectPath: str
useWorktrees: true, // Enable worktree feature for tests
currentWorktreeByProject: {
// This is STALE data - pointing to a worktree path that doesn't exist
[pathArg]: { path: "/non/existent/worktree/path", branch: "feature/deleted-branch" },
[pathArg]: { path: '/non/existent/worktree/path', branch: 'feature/deleted-branch' },
},
worktreesByProject: {},
},
version: 2, // Must match app-store.ts persist version
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
// Mark setup as complete to skip the setup wizard
const setupState = {
state: {
isFirstRun: false,
setupComplete: true,
currentStep: "complete",
currentStep: 'complete',
skipClaudeSetup: false,
},
version: 2, // Must match app-store.ts persist version
};
localStorage.setItem("automaker-setup", JSON.stringify(setupState));
localStorage.setItem('automaker-setup', JSON.stringify(setupState));
}, projectPath);
}
@@ -477,8 +487,6 @@ export async function waitForBoardView(page: Page): Promise<void> {
await page.waitForFunction(
() => {
const boardView = document.querySelector('[data-testid="board-view"]');
const noProject = document.querySelector('[data-testid="board-view-no-project"]');
const loading = document.querySelector('[data-testid="board-view-loading"]');
// Return true only when board-view is visible (store hydrated with project)
return boardView !== null;
},
@@ -490,8 +498,10 @@ export async function waitForBoardView(page: Page): Promise<void> {
* Wait for the worktree selector to be visible
*/
export async function waitForWorktreeSelector(page: Page): Promise<void> {
await page.waitForSelector('[data-testid="worktree-selector"]', { timeout: TIMEOUTS.medium }).catch(() => {
// Fallback: wait for "Branch:" text
return page.getByText("Branch:").waitFor({ timeout: TIMEOUTS.medium });
});
await page
.waitForSelector('[data-testid="worktree-selector"]', { timeout: TIMEOUTS.medium })
.catch(() => {
// Fallback: wait for "Branch:" text
return page.getByText('Branch:').waitFor({ timeout: TIMEOUTS.medium });
});
}

View File

@@ -1,32 +1,23 @@
import { Page, Locator } from "@playwright/test";
import { Page, Locator } from '@playwright/test';
/**
* Get a kanban card by feature ID
*/
export async function getKanbanCard(
page: Page,
featureId: string
): Promise<Locator> {
export async function getKanbanCard(page: Page, featureId: string): Promise<Locator> {
return page.locator(`[data-testid="kanban-card-${featureId}"]`);
}
/**
* Get a kanban column by its ID
*/
export async function getKanbanColumn(
page: Page,
columnId: string
): Promise<Locator> {
export async function getKanbanColumn(page: Page, columnId: string): Promise<Locator> {
return page.locator(`[data-testid="kanban-column-${columnId}"]`);
}
/**
* Get the width of a kanban column
*/
export async function getKanbanColumnWidth(
page: Page,
columnId: string
): Promise<number> {
export async function getKanbanColumnWidth(page: Page, columnId: string): Promise<number> {
const column = page.locator(`[data-testid="kanban-column-${columnId}"]`);
const box = await column.boundingBox();
return box?.width ?? 0;
@@ -35,19 +26,16 @@ export async function getKanbanColumnWidth(
/**
* Check if a kanban column has CSS columns (masonry) layout
*/
export async function hasKanbanColumnMasonryLayout(
page: Page,
columnId: string
): Promise<boolean> {
export async function hasKanbanColumnMasonryLayout(page: Page, columnId: string): Promise<boolean> {
const column = page.locator(`[data-testid="kanban-column-${columnId}"]`);
const contentDiv = column.locator("> div").nth(1); // Second child is the content area
const contentDiv = column.locator('> div').nth(1); // Second child is the content area
const columnCount = await contentDiv.evaluate((el) => {
const style = window.getComputedStyle(el);
return style.columnCount;
});
return columnCount === "2";
return columnCount === '2';
}
/**
@@ -58,11 +46,8 @@ export async function dragKanbanCard(
featureId: string,
targetColumnId: string
): Promise<void> {
const card = page.locator(`[data-testid="kanban-card-${featureId}"]`);
const dragHandle = page.locator(`[data-testid="drag-handle-${featureId}"]`);
const targetColumn = page.locator(
`[data-testid="kanban-column-${targetColumnId}"]`
);
const targetColumn = page.locator(`[data-testid="kanban-column-${targetColumnId}"]`);
// Perform drag and drop
await dragHandle.dragTo(targetColumn);
@@ -71,15 +56,10 @@ export async function dragKanbanCard(
/**
* Click the view output button on a kanban card
*/
export async function clickViewOutput(
page: Page,
featureId: string
): Promise<void> {
export async function clickViewOutput(page: Page, featureId: string): Promise<void> {
// Try the running version first, then the in-progress version
const runningBtn = page.locator(`[data-testid="view-output-${featureId}"]`);
const inProgressBtn = page.locator(
`[data-testid="view-output-inprogress-${featureId}"]`
);
const inProgressBtn = page.locator(`[data-testid="view-output-inprogress-${featureId}"]`);
if (await runningBtn.isVisible()) {
await runningBtn.click();
@@ -104,10 +84,7 @@ export async function isDragHandleVisibleForFeature(
/**
* Get the drag handle element for a specific feature card
*/
export async function getDragHandleForFeature(
page: Page,
featureId: string
): Promise<Locator> {
export async function getDragHandleForFeature(page: Page, featureId: string): Promise<Locator> {
return page.locator(`[data-testid="drag-handle-${featureId}"]`);
}
@@ -134,9 +111,7 @@ export async function fillAddFeatureDialog(
options?: { branch?: string; category?: string }
): Promise<void> {
// Fill description (using the dropzone textarea)
const descriptionInput = page
.locator('[data-testid="add-feature-dialog"] textarea')
.first();
const descriptionInput = page.locator('[data-testid="add-feature-dialog"] textarea').first();
await descriptionInput.fill(description);
// Fill branch if provided (it's a combobox autocomplete)
@@ -145,36 +120,34 @@ export async function fillAddFeatureDialog(
const otherBranchRadio = page
.locator('[data-testid="feature-radio-group"]')
.locator('[id="feature-other"]');
await otherBranchRadio.waitFor({ state: "visible", timeout: 5000 });
await otherBranchRadio.waitFor({ state: 'visible', timeout: 5000 });
await otherBranchRadio.click();
// Wait for the branch input to appear
await page.waitForTimeout(300);
// Now click on the branch input (autocomplete)
const branchInput = page.locator('[data-testid="feature-input"]');
await branchInput.waitFor({ state: "visible", timeout: 5000 });
await branchInput.waitFor({ state: 'visible', timeout: 5000 });
await branchInput.click();
// Wait for the popover to open
await page.waitForTimeout(300);
// Type in the command input
const commandInput = page.locator("[cmdk-input]");
const commandInput = page.locator('[cmdk-input]');
await commandInput.fill(options.branch);
// Press Enter to select/create the branch
await commandInput.press("Enter");
await commandInput.press('Enter');
// Wait for popover to close
await page.waitForTimeout(200);
}
// Fill category if provided (it's also a combobox autocomplete)
if (options?.category) {
const categoryButton = page.locator(
'[data-testid="feature-category-input"]'
);
const categoryButton = page.locator('[data-testid="feature-category-input"]');
await categoryButton.click();
await page.waitForTimeout(300);
const commandInput = page.locator("[cmdk-input]");
const commandInput = page.locator('[cmdk-input]');
await commandInput.fill(options.category);
await commandInput.press("Enter");
await commandInput.press('Enter');
await page.waitForTimeout(200);
}
}
@@ -185,10 +158,9 @@ export async function fillAddFeatureDialog(
export async function confirmAddFeature(page: Page): Promise<void> {
await page.click('[data-testid="confirm-add-feature"]');
// Wait for dialog to close
await page.waitForFunction(
() => !document.querySelector('[data-testid="add-feature-dialog"]'),
{ timeout: 5000 }
);
await page.waitForFunction(() => !document.querySelector('[data-testid="add-feature-dialog"]'), {
timeout: 5000,
});
}
/**
@@ -218,12 +190,9 @@ export async function getWorktreeSelector(page: Page): Promise<Locator> {
/**
* Click on a branch button in the worktree selector
*/
export async function selectWorktreeBranch(
page: Page,
branchName: string
): Promise<void> {
const branchButton = page.getByRole("button", {
name: new RegExp(branchName, "i"),
export async function selectWorktreeBranch(page: Page, branchName: string): Promise<void> {
const branchButton = page.getByRole('button', {
name: new RegExp(branchName, 'i'),
});
await branchButton.click();
await page.waitForTimeout(500); // Wait for UI to update
@@ -232,9 +201,7 @@ export async function selectWorktreeBranch(
/**
* Get the currently selected branch in the worktree selector
*/
export async function getSelectedWorktreeBranch(
page: Page
): Promise<string | null> {
export async function getSelectedWorktreeBranch(page: Page): Promise<string | null> {
// The main branch button has aria-pressed="true" when selected
const selectedButton = page.locator(
'[data-testid="worktree-selector"] button[aria-pressed="true"]'
@@ -246,12 +213,9 @@ export async function getSelectedWorktreeBranch(
/**
* Check if a branch button is visible in the worktree selector
*/
export async function isWorktreeBranchVisible(
page: Page,
branchName: string
): Promise<boolean> {
const branchButton = page.getByRole("button", {
name: new RegExp(branchName, "i"),
export async function isWorktreeBranchVisible(page: Page, branchName: string): Promise<boolean> {
const branchButton = page.getByRole('button', {
name: new RegExp(branchName, 'i'),
});
return await branchButton.isVisible().catch(() => false);
}

View File

@@ -1,20 +1,19 @@
import { Page, Locator } from "@playwright/test";
import { getByTestId } from "../core/elements";
import { waitForElement } from "../core/waiting";
import { setupFirstRun } from "../project/setup";
import { Page, Locator } from '@playwright/test';
import { getByTestId } from '../core/elements';
import { waitForElement } from '../core/waiting';
/**
* Wait for setup view to be visible
*/
export async function waitForSetupView(page: Page): Promise<Locator> {
return waitForElement(page, "setup-view", { timeout: 10000 });
return waitForElement(page, 'setup-view', { timeout: 10000 });
}
/**
* Click "Get Started" button on setup welcome step
*/
export async function clickSetupGetStarted(page: Page): Promise<void> {
const button = await getByTestId(page, "setup-start-button");
const button = await getByTestId(page, 'setup-start-button');
await button.click();
}
@@ -22,7 +21,7 @@ export async function clickSetupGetStarted(page: Page): Promise<void> {
* Click continue on Claude setup step
*/
export async function clickClaudeContinue(page: Page): Promise<void> {
const button = await getByTestId(page, "claude-next-button");
const button = await getByTestId(page, 'claude-next-button');
await button.click();
}
@@ -30,46 +29,40 @@ export async function clickClaudeContinue(page: Page): Promise<void> {
* Click finish on setup complete step
*/
export async function clickSetupFinish(page: Page): Promise<void> {
const button = await getByTestId(page, "setup-finish-button");
const button = await getByTestId(page, 'setup-finish-button');
await button.click();
}
/**
* Enter Anthropic API key in setup
*/
export async function enterAnthropicApiKey(
page: Page,
apiKey: string
): Promise<void> {
export async function enterAnthropicApiKey(page: Page, apiKey: string): Promise<void> {
// Click "Use Anthropic API Key Instead" button
const useApiKeyButton = await getByTestId(page, "use-api-key-button");
const useApiKeyButton = await getByTestId(page, 'use-api-key-button');
await useApiKeyButton.click();
// Enter the API key
const input = await getByTestId(page, "anthropic-api-key-input");
const input = await getByTestId(page, 'anthropic-api-key-input');
await input.fill(apiKey);
// Click save button
const saveButton = await getByTestId(page, "save-anthropic-key-button");
const saveButton = await getByTestId(page, 'save-anthropic-key-button');
await saveButton.click();
}
/**
* Enter OpenAI API key in setup
*/
export async function enterOpenAIApiKey(
page: Page,
apiKey: string
): Promise<void> {
export async function enterOpenAIApiKey(page: Page, apiKey: string): Promise<void> {
// Click "Enter OpenAI API Key" button
const useApiKeyButton = await getByTestId(page, "use-openai-key-button");
const useApiKeyButton = await getByTestId(page, 'use-openai-key-button');
await useApiKeyButton.click();
// Enter the API key
const input = await getByTestId(page, "openai-api-key-input");
const input = await getByTestId(page, 'openai-api-key-input');
await input.fill(apiKey);
// Click save button
const saveButton = await getByTestId(page, "save-openai-key-button");
const saveButton = await getByTestId(page, 'save-openai-key-button');
await saveButton.click();
}