mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 14:22:02 +00:00
- Updated KanbanCard to conditionally display status badges based on feature attributes, improving visual feedback. - Enhanced WorktreeSelector to conditionally render based on the worktree feature toggle, ensuring a cleaner UI when worktrees are disabled. - Modified AddFeatureDialog and EditFeatureDialog to include branch selection only when worktrees are enabled, streamlining the feature creation process. - Refactored useBoardActions and useBoardDragDrop hooks to create worktrees only when the feature is enabled, optimizing performance. - Introduced comprehensive integration tests for worktree operations, ensuring robust functionality and error handling across various scenarios.
776 lines
29 KiB
TypeScript
776 lines
29 KiB
TypeScript
/**
|
|
* Worktree Integration Tests
|
|
*
|
|
* Tests for git worktree functionality including:
|
|
* - Creating and deleting worktrees
|
|
* - Committing changes
|
|
* - Switching branches
|
|
* - Branch listing
|
|
* - Worktree isolation
|
|
* - Feature filtering by worktree
|
|
*/
|
|
|
|
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 shared utilities
|
|
import {
|
|
waitForNetworkIdle,
|
|
apiCreateWorktree,
|
|
apiDeleteWorktree,
|
|
apiListWorktrees,
|
|
apiCommitWorktree,
|
|
apiSwitchBranch,
|
|
apiListBranches,
|
|
createTestGitRepo,
|
|
cleanupTempDir,
|
|
createTempDirPath,
|
|
getWorktreePath,
|
|
listWorktrees,
|
|
listBranches,
|
|
setupProjectWithPath,
|
|
waitForBoardView,
|
|
clickAddFeature,
|
|
fillAddFeatureDialog,
|
|
confirmAddFeature,
|
|
} from "./utils";
|
|
|
|
const execAsync = promisify(exec);
|
|
|
|
// ============================================================================
|
|
// Test Setup
|
|
// ============================================================================
|
|
|
|
// Create unique temp dir for this test run
|
|
const TEST_TEMP_DIR = createTempDirPath("worktree-tests");
|
|
|
|
interface TestRepo {
|
|
path: string;
|
|
cleanup: () => Promise<void>;
|
|
}
|
|
|
|
// Configure all tests to run serially to prevent interference
|
|
test.describe.configure({ mode: "serial" });
|
|
|
|
// ============================================================================
|
|
// Test Suite: Worktree Integration Tests
|
|
// ============================================================================
|
|
test.describe("Worktree Integration 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);
|
|
});
|
|
|
|
// ==========================================================================
|
|
// Basic Worktree Operations
|
|
// ==========================================================================
|
|
|
|
test("should display worktree selector with main branch", async ({ page }) => {
|
|
await setupProjectWithPath(page, testRepo.path);
|
|
await page.goto("/");
|
|
await waitForNetworkIdle(page);
|
|
await waitForBoardView(page);
|
|
|
|
// Verify the worktree selector is visible
|
|
const branchLabel = page.getByText("Branch:");
|
|
await expect(branchLabel).toBeVisible({ timeout: 10000 });
|
|
|
|
// Verify main branch button is displayed
|
|
const mainBranchButton = page.getByRole("button", { name: "main" });
|
|
await expect(mainBranchButton).toBeVisible({ timeout: 10000 });
|
|
});
|
|
|
|
test("should create a worktree via API and verify filesystem", async ({ page }) => {
|
|
await setupProjectWithPath(page, testRepo.path);
|
|
await page.goto("/");
|
|
await waitForNetworkIdle(page);
|
|
await waitForBoardView(page);
|
|
|
|
const branchName = "feature/test-worktree";
|
|
const expectedWorktreePath = getWorktreePath(testRepo.path, branchName);
|
|
|
|
const { response, data } = await apiCreateWorktree(page, testRepo.path, branchName);
|
|
|
|
expect(response.ok()).toBe(true);
|
|
expect(data.success).toBe(true);
|
|
|
|
// Verify worktree was created on filesystem
|
|
expect(fs.existsSync(expectedWorktreePath)).toBe(true);
|
|
|
|
// Verify branch was created
|
|
const branches = await listBranches(testRepo.path);
|
|
expect(branches).toContain(branchName);
|
|
|
|
// Verify worktree is listed by git
|
|
const worktrees = await listWorktrees(testRepo.path);
|
|
expect(worktrees.length).toBe(1);
|
|
expect(worktrees[0]).toBe(expectedWorktreePath);
|
|
});
|
|
|
|
test("should create two worktrees and list them both", async ({ page }) => {
|
|
await setupProjectWithPath(page, testRepo.path);
|
|
await page.goto("/");
|
|
await waitForNetworkIdle(page);
|
|
await waitForBoardView(page);
|
|
|
|
// Create first worktree
|
|
const { response: response1 } = await apiCreateWorktree(page, testRepo.path, "feature/worktree-one");
|
|
expect(response1.ok()).toBe(true);
|
|
|
|
// Create second worktree
|
|
const { response: response2 } = await apiCreateWorktree(page, testRepo.path, "feature/worktree-two");
|
|
expect(response2.ok()).toBe(true);
|
|
|
|
// Verify both worktrees exist
|
|
const worktrees = await listWorktrees(testRepo.path);
|
|
expect(worktrees.length).toBe(2);
|
|
|
|
// Verify branches were created
|
|
const branches = await listBranches(testRepo.path);
|
|
expect(branches).toContain("feature/worktree-one");
|
|
expect(branches).toContain("feature/worktree-two");
|
|
});
|
|
|
|
test("should delete a worktree via API and verify cleanup", async ({ page }) => {
|
|
await setupProjectWithPath(page, testRepo.path);
|
|
await page.goto("/");
|
|
await waitForNetworkIdle(page);
|
|
await waitForBoardView(page);
|
|
|
|
// Create a worktree
|
|
const branchName = "feature/to-delete";
|
|
const worktreePath = getWorktreePath(testRepo.path, branchName);
|
|
|
|
await apiCreateWorktree(page, testRepo.path, branchName);
|
|
expect(fs.existsSync(worktreePath)).toBe(true);
|
|
|
|
// Delete it
|
|
const { response } = await apiDeleteWorktree(page, testRepo.path, worktreePath, true);
|
|
expect(response.ok()).toBe(true);
|
|
|
|
// Verify worktree directory is removed
|
|
expect(fs.existsSync(worktreePath)).toBe(false);
|
|
|
|
// Verify branch is deleted
|
|
const branches = await listBranches(testRepo.path);
|
|
expect(branches).not.toContain(branchName);
|
|
});
|
|
|
|
test("should delete worktree but keep branch when deleteBranch is false", async ({ page }) => {
|
|
await setupProjectWithPath(page, testRepo.path);
|
|
await page.goto("/");
|
|
await waitForNetworkIdle(page);
|
|
await waitForBoardView(page);
|
|
|
|
const branchName = "feature/keep-branch";
|
|
const worktreePath = getWorktreePath(testRepo.path, branchName);
|
|
|
|
await apiCreateWorktree(page, testRepo.path, branchName);
|
|
expect(fs.existsSync(worktreePath)).toBe(true);
|
|
|
|
// Delete worktree but keep branch
|
|
const { response } = await apiDeleteWorktree(page, testRepo.path, worktreePath, false);
|
|
expect(response.ok()).toBe(true);
|
|
|
|
// Verify worktree is gone but branch remains
|
|
expect(fs.existsSync(worktreePath)).toBe(false);
|
|
const branches = await listBranches(testRepo.path);
|
|
expect(branches).toContain(branchName);
|
|
});
|
|
|
|
test("should list worktrees via API", async ({ page }) => {
|
|
await setupProjectWithPath(page, testRepo.path);
|
|
await page.goto("/");
|
|
await waitForNetworkIdle(page);
|
|
|
|
// Create some worktrees first
|
|
await apiCreateWorktree(page, testRepo.path, "feature/list-test-1");
|
|
await apiCreateWorktree(page, testRepo.path, "feature/list-test-2");
|
|
|
|
// List worktrees via API
|
|
const { response, data } = await apiListWorktrees(page, testRepo.path, true);
|
|
|
|
expect(response.ok()).toBe(true);
|
|
expect(data.success).toBe(true);
|
|
expect(data.worktrees).toHaveLength(3); // main + 2 worktrees
|
|
|
|
// Verify worktree details
|
|
const branches = data.worktrees.map((w) => w.branch);
|
|
expect(branches).toContain("main");
|
|
expect(branches).toContain("feature/list-test-1");
|
|
expect(branches).toContain("feature/list-test-2");
|
|
});
|
|
|
|
// ==========================================================================
|
|
// Commit Operations
|
|
// ==========================================================================
|
|
|
|
test("should commit changes in a worktree via API", async ({ page }) => {
|
|
await setupProjectWithPath(page, testRepo.path);
|
|
await page.goto("/");
|
|
await waitForNetworkIdle(page);
|
|
await waitForBoardView(page);
|
|
|
|
const branchName = "feature/commit-test";
|
|
const worktreePath = getWorktreePath(testRepo.path, branchName);
|
|
|
|
const { response: createResponse } = await apiCreateWorktree(page, testRepo.path, branchName);
|
|
expect(createResponse.ok()).toBe(true);
|
|
|
|
// Create a new file in the worktree
|
|
const testFilePath = path.join(worktreePath, "test-commit.txt");
|
|
fs.writeFileSync(testFilePath, "This is a test file for commit");
|
|
|
|
// Commit the changes via API
|
|
const { response, data } = await apiCommitWorktree(page, worktreePath, "Add test file for commit integration test");
|
|
|
|
expect(response.ok()).toBe(true);
|
|
expect(data.success).toBe(true);
|
|
expect(data.result?.committed).toBe(true);
|
|
expect(data.result?.branch).toBe(branchName);
|
|
expect(data.result?.commitHash).toBeDefined();
|
|
expect(data.result?.commitHash?.length).toBe(8);
|
|
|
|
// Verify the commit exists in git log
|
|
const { stdout: logOutput } = await execAsync("git log --oneline -1", { cwd: worktreePath });
|
|
expect(logOutput).toContain("Add test file for commit integration test");
|
|
});
|
|
|
|
test("should return no changes when committing with no modifications", async ({ page }) => {
|
|
await setupProjectWithPath(page, testRepo.path);
|
|
await page.goto("/");
|
|
await waitForNetworkIdle(page);
|
|
await waitForBoardView(page);
|
|
|
|
const branchName = "feature/no-changes-commit";
|
|
const worktreePath = getWorktreePath(testRepo.path, branchName);
|
|
|
|
await apiCreateWorktree(page, testRepo.path, branchName);
|
|
|
|
// Try to commit without any changes
|
|
const { response, data } = await apiCommitWorktree(page, worktreePath, "Empty commit attempt");
|
|
|
|
expect(response.ok()).toBe(true);
|
|
expect(data.success).toBe(true);
|
|
expect(data.result?.committed).toBe(false);
|
|
expect(data.result?.message).toBe("No changes to commit");
|
|
});
|
|
|
|
test("should handle multiple sequential commits in a worktree", async ({ page }) => {
|
|
await setupProjectWithPath(page, testRepo.path);
|
|
await page.goto("/");
|
|
await waitForNetworkIdle(page);
|
|
await waitForBoardView(page);
|
|
|
|
const branchName = "feature/multi-commit";
|
|
const worktreePath = getWorktreePath(testRepo.path, branchName);
|
|
|
|
await apiCreateWorktree(page, testRepo.path, branchName);
|
|
|
|
// First commit
|
|
fs.writeFileSync(path.join(worktreePath, "file1.txt"), "First file");
|
|
const { data: data1 } = await apiCommitWorktree(page, worktreePath, "First commit");
|
|
expect(data1.result?.committed).toBe(true);
|
|
|
|
// Second commit
|
|
fs.writeFileSync(path.join(worktreePath, "file2.txt"), "Second file");
|
|
const { data: data2 } = await apiCommitWorktree(page, worktreePath, "Second commit");
|
|
expect(data2.result?.committed).toBe(true);
|
|
|
|
// Third commit
|
|
fs.writeFileSync(path.join(worktreePath, "file3.txt"), "Third file");
|
|
const { data: data3 } = await apiCommitWorktree(page, worktreePath, "Third commit");
|
|
expect(data3.result?.committed).toBe(true);
|
|
|
|
// Verify all commits exist in log
|
|
const { stdout: logOutput } = await execAsync("git log --oneline -5", { cwd: worktreePath });
|
|
expect(logOutput).toContain("First commit");
|
|
expect(logOutput).toContain("Second commit");
|
|
expect(logOutput).toContain("Third commit");
|
|
});
|
|
|
|
// ==========================================================================
|
|
// Branch Switching
|
|
// ==========================================================================
|
|
|
|
test("should switch branches within a worktree via API", async ({ page }) => {
|
|
await setupProjectWithPath(page, testRepo.path);
|
|
await page.goto("/");
|
|
await waitForNetworkIdle(page);
|
|
await waitForBoardView(page);
|
|
|
|
// Create a second branch in the main repo for switching
|
|
await execAsync("git branch test-switch-target", { cwd: testRepo.path });
|
|
|
|
// Switch to the new branch via API
|
|
const { response, data } = await apiSwitchBranch(page, testRepo.path, "test-switch-target");
|
|
|
|
expect(response.ok()).toBe(true);
|
|
expect(data.success).toBe(true);
|
|
expect(data.result?.previousBranch).toBe("main");
|
|
expect(data.result?.currentBranch).toBe("test-switch-target");
|
|
expect(data.result?.message).toContain("Switched to branch");
|
|
|
|
// Verify the branch was actually switched
|
|
const { stdout: currentBranch } = await execAsync("git rev-parse --abbrev-ref HEAD", { cwd: testRepo.path });
|
|
expect(currentBranch.trim()).toBe("test-switch-target");
|
|
|
|
// Switch back to main
|
|
await execAsync("git checkout main", { cwd: testRepo.path });
|
|
});
|
|
|
|
test("should prevent branch switch with uncommitted changes", async ({ page }) => {
|
|
await setupProjectWithPath(page, testRepo.path);
|
|
await page.goto("/");
|
|
await waitForNetworkIdle(page);
|
|
await waitForBoardView(page);
|
|
|
|
// Create a branch to switch to
|
|
await execAsync("git branch test-switch-blocked", { cwd: testRepo.path });
|
|
|
|
// Create uncommitted changes
|
|
const testFilePath = path.join(testRepo.path, "uncommitted-change.txt");
|
|
fs.writeFileSync(testFilePath, "This file has uncommitted changes");
|
|
await execAsync("git add uncommitted-change.txt", { cwd: testRepo.path });
|
|
|
|
// Try to switch branches (should fail)
|
|
const { response, data } = await apiSwitchBranch(page, testRepo.path, "test-switch-blocked");
|
|
|
|
expect(response.ok()).toBe(false);
|
|
expect(data.success).toBe(false);
|
|
expect(data.error).toContain("uncommitted changes");
|
|
expect(data.code).toBe("UNCOMMITTED_CHANGES");
|
|
|
|
// Clean up - reset changes
|
|
await execAsync("git reset HEAD", { cwd: testRepo.path });
|
|
fs.unlinkSync(testFilePath);
|
|
});
|
|
|
|
test("should handle switching to non-existent branch", async ({ page }) => {
|
|
await setupProjectWithPath(page, testRepo.path);
|
|
await page.goto("/");
|
|
await waitForNetworkIdle(page);
|
|
await waitForBoardView(page);
|
|
|
|
// Try to switch to a branch that doesn't exist
|
|
const { response, data } = await apiSwitchBranch(page, testRepo.path, "non-existent-branch");
|
|
|
|
expect(response.ok()).toBe(false);
|
|
expect(data.success).toBe(false);
|
|
expect(data.error).toContain("does not exist");
|
|
});
|
|
|
|
test("should handle switching to current branch (no-op)", async ({ page }) => {
|
|
await setupProjectWithPath(page, testRepo.path);
|
|
await page.goto("/");
|
|
await waitForNetworkIdle(page);
|
|
await waitForBoardView(page);
|
|
|
|
// Try to switch to the current branch
|
|
const { response, data } = await apiSwitchBranch(page, testRepo.path, "main");
|
|
|
|
expect(response.ok()).toBe(true);
|
|
expect(data.success).toBe(true);
|
|
expect(data.result?.message).toContain("Already on branch");
|
|
});
|
|
|
|
// ==========================================================================
|
|
// List Branches
|
|
// ==========================================================================
|
|
|
|
test("should list all branches via API", async ({ page }) => {
|
|
await setupProjectWithPath(page, testRepo.path);
|
|
await page.goto("/");
|
|
await waitForNetworkIdle(page);
|
|
await waitForBoardView(page);
|
|
|
|
// Create additional branches
|
|
await execAsync("git branch feature/branch-list-1", { cwd: testRepo.path });
|
|
await execAsync("git branch feature/branch-list-2", { cwd: testRepo.path });
|
|
await execAsync("git branch bugfix/test-branch", { cwd: testRepo.path });
|
|
|
|
// List branches via API
|
|
const { response, data } = await apiListBranches(page, testRepo.path);
|
|
|
|
expect(response.ok()).toBe(true);
|
|
expect(data.success).toBe(true);
|
|
expect(data.result?.currentBranch).toBe("main");
|
|
expect(data.result?.branches.length).toBeGreaterThanOrEqual(4);
|
|
|
|
const branchNames = data.result?.branches.map((b) => b.name) || [];
|
|
expect(branchNames).toContain("main");
|
|
expect(branchNames).toContain("feature/branch-list-1");
|
|
expect(branchNames).toContain("feature/branch-list-2");
|
|
expect(branchNames).toContain("bugfix/test-branch");
|
|
|
|
// Verify current branch is marked correctly
|
|
const currentBranchInfo = data.result?.branches.find((b) => b.name === "main");
|
|
expect(currentBranchInfo?.isCurrent).toBe(true);
|
|
});
|
|
|
|
// ==========================================================================
|
|
// Worktree Isolation
|
|
// ==========================================================================
|
|
|
|
test("should isolate files between worktrees", async ({ page }) => {
|
|
await setupProjectWithPath(page, testRepo.path);
|
|
await page.goto("/");
|
|
await waitForNetworkIdle(page);
|
|
await waitForBoardView(page);
|
|
|
|
// Create two worktrees
|
|
const branch1 = "feature/isolation-1";
|
|
const branch2 = "feature/isolation-2";
|
|
const worktree1Path = getWorktreePath(testRepo.path, branch1);
|
|
const worktree2Path = getWorktreePath(testRepo.path, branch2);
|
|
|
|
await apiCreateWorktree(page, testRepo.path, branch1);
|
|
await apiCreateWorktree(page, testRepo.path, branch2);
|
|
|
|
// Create different files in each worktree
|
|
const file1Path = path.join(worktree1Path, "worktree1-only.txt");
|
|
const file2Path = path.join(worktree2Path, "worktree2-only.txt");
|
|
|
|
fs.writeFileSync(file1Path, "File only in worktree 1");
|
|
fs.writeFileSync(file2Path, "File only in worktree 2");
|
|
|
|
// Verify file1 only exists in worktree1
|
|
expect(fs.existsSync(file1Path)).toBe(true);
|
|
expect(fs.existsSync(path.join(worktree2Path, "worktree1-only.txt"))).toBe(false);
|
|
|
|
// Verify file2 only exists in worktree2
|
|
expect(fs.existsSync(file2Path)).toBe(true);
|
|
expect(fs.existsSync(path.join(worktree1Path, "worktree2-only.txt"))).toBe(false);
|
|
|
|
// Commit in worktree1
|
|
await execAsync("git add worktree1-only.txt", { cwd: worktree1Path });
|
|
await execAsync('git commit -m "Add file in worktree1"', { cwd: worktree1Path });
|
|
|
|
// Commit in worktree2
|
|
await execAsync("git add worktree2-only.txt", { cwd: worktree2Path });
|
|
await execAsync('git commit -m "Add file in worktree2"', { cwd: worktree2Path });
|
|
|
|
// Verify commits are separate
|
|
const { stdout: log1 } = await execAsync("git log --oneline -1", { cwd: worktree1Path });
|
|
const { stdout: log2 } = await execAsync("git log --oneline -1", { cwd: worktree2Path });
|
|
|
|
expect(log1).toContain("Add file in worktree1");
|
|
expect(log2).toContain("Add file in worktree2");
|
|
expect(log1).not.toContain("Add file in worktree2");
|
|
expect(log2).not.toContain("Add file in worktree1");
|
|
});
|
|
|
|
test("should detect modified files count in worktree listing", async ({ page }) => {
|
|
await setupProjectWithPath(page, testRepo.path);
|
|
await page.goto("/");
|
|
await waitForNetworkIdle(page);
|
|
await waitForBoardView(page);
|
|
|
|
const branchName = "feature/changes-detection";
|
|
const worktreePath = getWorktreePath(testRepo.path, branchName);
|
|
|
|
await apiCreateWorktree(page, testRepo.path, branchName);
|
|
|
|
// Create multiple modified files
|
|
fs.writeFileSync(path.join(worktreePath, "change1.txt"), "Change 1");
|
|
fs.writeFileSync(path.join(worktreePath, "change2.txt"), "Change 2");
|
|
fs.writeFileSync(path.join(worktreePath, "change3.txt"), "Change 3");
|
|
|
|
// List worktrees and check for changes
|
|
const { response, data } = await apiListWorktrees(page, testRepo.path, true);
|
|
|
|
expect(response.ok()).toBe(true);
|
|
expect(data.success).toBe(true);
|
|
|
|
// Find the worktree we created
|
|
const changedWorktree = data.worktrees.find((w) => w.branch === branchName);
|
|
expect(changedWorktree).toBeDefined();
|
|
expect(changedWorktree?.hasChanges).toBe(true);
|
|
expect(changedWorktree?.changedFilesCount).toBeGreaterThanOrEqual(3);
|
|
});
|
|
|
|
// ==========================================================================
|
|
// Existing Branch Handling
|
|
// ==========================================================================
|
|
|
|
test("should create worktree from existing branch", async ({ page }) => {
|
|
await setupProjectWithPath(page, testRepo.path);
|
|
await page.goto("/");
|
|
await waitForNetworkIdle(page);
|
|
await waitForBoardView(page);
|
|
|
|
// First, create a branch with some commits (without worktree)
|
|
const branchName = "feature/existing-branch";
|
|
await execAsync(`git branch ${branchName}`, { cwd: testRepo.path });
|
|
await execAsync(`git checkout ${branchName}`, { cwd: testRepo.path });
|
|
fs.writeFileSync(path.join(testRepo.path, "existing-file.txt"), "Content from existing branch");
|
|
await execAsync("git add existing-file.txt", { cwd: testRepo.path });
|
|
await execAsync('git commit -m "Commit on existing branch"', { cwd: testRepo.path });
|
|
await execAsync("git checkout main", { cwd: testRepo.path });
|
|
|
|
// Now create a worktree for that existing branch
|
|
const expectedWorktreePath = getWorktreePath(testRepo.path, branchName);
|
|
|
|
const { response, data } = await apiCreateWorktree(page, testRepo.path, branchName);
|
|
|
|
expect(response.ok()).toBe(true);
|
|
expect(data.success).toBe(true);
|
|
|
|
// Verify the worktree has the file from the existing branch
|
|
const existingFilePath = path.join(expectedWorktreePath, "existing-file.txt");
|
|
expect(fs.existsSync(existingFilePath)).toBe(true);
|
|
const content = fs.readFileSync(existingFilePath, "utf-8");
|
|
expect(content).toBe("Content from existing branch");
|
|
});
|
|
|
|
test("should return existing worktree when creating with same branch name", async ({ page }) => {
|
|
await setupProjectWithPath(page, testRepo.path);
|
|
await page.goto("/");
|
|
await waitForNetworkIdle(page);
|
|
await waitForBoardView(page);
|
|
|
|
// Create first worktree
|
|
const branchName = "feature/duplicate-test";
|
|
const { response: response1, data: data1 } = await apiCreateWorktree(page, testRepo.path, branchName);
|
|
expect(response1.ok()).toBe(true);
|
|
expect(data1.success).toBe(true);
|
|
expect(data1.worktree?.isNew).not.toBe(false); // New branch was created
|
|
|
|
// Try to create another worktree with same branch name
|
|
// This should succeed and return the existing worktree (not an error)
|
|
const { response: response2, data: data2 } = await apiCreateWorktree(page, testRepo.path, branchName);
|
|
|
|
expect(response2.ok()).toBe(true);
|
|
expect(data2.success).toBe(true);
|
|
expect(data2.worktree?.isNew).toBe(false); // Not a new creation, returned existing
|
|
expect(data2.worktree?.branch).toBe(branchName);
|
|
});
|
|
|
|
// ==========================================================================
|
|
// Feature Integration
|
|
// ==========================================================================
|
|
|
|
test("should add a feature to backlog with specific branch", async ({ page }) => {
|
|
await setupProjectWithPath(page, testRepo.path);
|
|
await page.goto("/");
|
|
await waitForNetworkIdle(page);
|
|
await waitForBoardView(page);
|
|
|
|
// Create a worktree first
|
|
const branchName = "feature/test-branch";
|
|
await apiCreateWorktree(page, testRepo.path, branchName);
|
|
|
|
// Click add feature button
|
|
await clickAddFeature(page);
|
|
|
|
// Fill in the feature details
|
|
await fillAddFeatureDialog(page, "Test feature for worktree", {
|
|
branch: branchName,
|
|
category: "Testing",
|
|
});
|
|
|
|
// Confirm
|
|
await confirmAddFeature(page);
|
|
|
|
// Wait for the feature to appear
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Verify feature was created with correct branch by checking the filesystem
|
|
const featuresDir = path.join(testRepo.path, ".automaker", "features");
|
|
const featureDirs = fs.readdirSync(featuresDir);
|
|
expect(featureDirs.length).toBeGreaterThan(0);
|
|
|
|
// Find and read the feature file
|
|
const featureDir = featureDirs[0];
|
|
const featureFilePath = path.join(featuresDir, featureDir, "feature.json");
|
|
const featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
|
|
|
|
expect(featureData.description).toBe("Test feature for worktree");
|
|
expect(featureData.branchName).toBe(branchName);
|
|
expect(featureData.status).toBe("backlog");
|
|
});
|
|
|
|
test("should filter features by selected worktree", async ({ page }) => {
|
|
// Create the worktrees first (using git directly for setup)
|
|
await execAsync(`git worktree add ".worktrees/feature-worktree-a" -b feature/worktree-a`, {
|
|
cwd: testRepo.path,
|
|
});
|
|
await execAsync(`git worktree add ".worktrees/feature-worktree-b" -b feature/worktree-b`, {
|
|
cwd: testRepo.path,
|
|
});
|
|
|
|
await setupProjectWithPath(page, testRepo.path);
|
|
await page.goto("/");
|
|
await waitForNetworkIdle(page);
|
|
await waitForBoardView(page);
|
|
|
|
// First click on main to ensure we're on the main branch
|
|
const mainButton = page.getByRole("button", { name: "main" }).first();
|
|
await mainButton.click();
|
|
await page.waitForTimeout(500);
|
|
|
|
// Create feature for main branch
|
|
await clickAddFeature(page);
|
|
const descriptionInput = page.locator('[data-testid="add-feature-dialog"] textarea').first();
|
|
await descriptionInput.fill("Feature for main branch");
|
|
await confirmAddFeature(page);
|
|
|
|
// Wait for feature to be visible
|
|
const mainFeatureText = page.getByText("Feature for main branch");
|
|
await expect(mainFeatureText).toBeVisible({ timeout: 10000 });
|
|
|
|
// Switch to worktree-a and create a feature there
|
|
const worktreeAButton = page.getByRole("button", { name: /feature\/worktree-a/i });
|
|
await worktreeAButton.click();
|
|
await page.waitForTimeout(500);
|
|
|
|
// Main feature should not be visible now
|
|
await expect(mainFeatureText).not.toBeVisible();
|
|
|
|
// Create feature for worktree-a
|
|
await clickAddFeature(page);
|
|
const descriptionInput2 = page.locator('[data-testid="add-feature-dialog"] textarea').first();
|
|
await descriptionInput2.fill("Feature for worktree A");
|
|
await confirmAddFeature(page);
|
|
|
|
// Wait for feature to be visible
|
|
const worktreeAText = page.getByText("Feature for worktree A");
|
|
await expect(worktreeAText).toBeVisible({ timeout: 10000 });
|
|
|
|
// Switch to worktree-b and create a feature
|
|
const worktreeBButton = page.getByRole("button", { name: /feature\/worktree-b/i });
|
|
await worktreeBButton.click();
|
|
await page.waitForTimeout(500);
|
|
|
|
// worktree-a feature should not be visible
|
|
await expect(worktreeAText).not.toBeVisible();
|
|
|
|
await clickAddFeature(page);
|
|
const descriptionInput3 = page.locator('[data-testid="add-feature-dialog"] textarea').first();
|
|
await descriptionInput3.fill("Feature for worktree B");
|
|
await confirmAddFeature(page);
|
|
|
|
const worktreeBText = page.getByText("Feature for worktree B");
|
|
await expect(worktreeBText).toBeVisible({ timeout: 10000 });
|
|
|
|
// Switch back to main and verify filtering
|
|
await mainButton.click();
|
|
await page.waitForTimeout(500);
|
|
|
|
await expect(mainFeatureText).toBeVisible({ timeout: 10000 });
|
|
await expect(worktreeAText).not.toBeVisible();
|
|
await expect(worktreeBText).not.toBeVisible();
|
|
});
|
|
|
|
test("should pre-fill branch when creating feature from selected worktree", async ({ page }) => {
|
|
// Create a worktree first
|
|
const branchName = "feature/pre-fill-test";
|
|
await execAsync(`git worktree add ".worktrees/feature-pre-fill-test" -b ${branchName}`, {
|
|
cwd: testRepo.path,
|
|
});
|
|
|
|
await setupProjectWithPath(page, testRepo.path);
|
|
await page.goto("/");
|
|
await waitForNetworkIdle(page);
|
|
await waitForBoardView(page);
|
|
|
|
// Wait for worktree selector to load
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Click on the worktree to select it
|
|
const worktreeButton = page.getByRole("button", { name: /feature\/pre-fill-test/i });
|
|
await worktreeButton.click();
|
|
await page.waitForTimeout(500);
|
|
|
|
// Open add feature dialog
|
|
await clickAddFeature(page);
|
|
|
|
// Verify the branch input button shows the selected worktree's branch
|
|
const branchButton = page.locator('[data-testid="feature-branch-input"]');
|
|
await expect(branchButton).toContainText(branchName, { timeout: 5000 });
|
|
|
|
// Close dialog
|
|
await page.keyboard.press("Escape");
|
|
});
|
|
|
|
// ==========================================================================
|
|
// Error Handling
|
|
// ==========================================================================
|
|
|
|
test("should handle commit with missing required fields", async ({ page }) => {
|
|
await setupProjectWithPath(page, testRepo.path);
|
|
await page.goto("/");
|
|
await waitForNetworkIdle(page);
|
|
|
|
// Try to commit without worktreePath
|
|
const response1 = await page.request.post("http://localhost:3008/api/worktree/commit", {
|
|
data: { message: "Missing worktreePath" },
|
|
});
|
|
|
|
expect(response1.ok()).toBe(false);
|
|
const result1 = await response1.json();
|
|
expect(result1.success).toBe(false);
|
|
expect(result1.error).toContain("worktreePath");
|
|
|
|
// Try to commit without message
|
|
const response2 = await page.request.post("http://localhost:3008/api/worktree/commit", {
|
|
data: { worktreePath: testRepo.path },
|
|
});
|
|
|
|
expect(response2.ok()).toBe(false);
|
|
const result2 = await response2.json();
|
|
expect(result2.success).toBe(false);
|
|
expect(result2.error).toContain("message");
|
|
});
|
|
|
|
test("should handle switch-branch with missing required fields", async ({ page }) => {
|
|
await setupProjectWithPath(page, testRepo.path);
|
|
await page.goto("/");
|
|
await waitForNetworkIdle(page);
|
|
|
|
// Try to switch without worktreePath
|
|
const response1 = await page.request.post("http://localhost:3008/api/worktree/switch-branch", {
|
|
data: { branchName: "some-branch" },
|
|
});
|
|
|
|
expect(response1.ok()).toBe(false);
|
|
const result1 = await response1.json();
|
|
expect(result1.success).toBe(false);
|
|
expect(result1.error).toContain("worktreePath");
|
|
|
|
// Try to switch without branchName
|
|
const response2 = await page.request.post("http://localhost:3008/api/worktree/switch-branch", {
|
|
data: { worktreePath: testRepo.path },
|
|
});
|
|
|
|
expect(response2.ok()).toBe(false);
|
|
const result2 = await response2.json();
|
|
expect(result2.success).toBe(false);
|
|
expect(result2.error).toContain("branchName");
|
|
});
|
|
});
|