mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 08:33:36 +00:00
Merge origin/main into feature/shared-packages
Resolved conflicts: - list.ts: Keep @automaker/git-utils import, add worktree-metadata import - feature-loader.ts: Use Feature type from @automaker/types - automaker-paths.test.ts: Import from @automaker/platform - kanban-card.tsx: Accept deletion (split into components/) - subprocess.test.ts: Keep libs/platform location Added missing exports to @automaker/platform: - getGlobalSettingsPath, getCredentialsPath, getProjectSettingsPath, ensureDataDir Added title and titleGenerating fields to @automaker/types Feature interface. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -53,13 +53,14 @@ test.describe("Spec Editor Persistence", () => {
|
||||
// Step 6: Wait for CodeMirror to initialize (it has a .cm-content element)
|
||||
await specEditor.locator(".cm-content").waitFor({ state: "visible", timeout: 10000 });
|
||||
|
||||
// Small delay to ensure editor is fully initialized
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Step 7: Modify the editor content to "hello world"
|
||||
await setEditorContent(page, "hello world");
|
||||
|
||||
// Step 8: Click the save button
|
||||
// Verify content was set before saving
|
||||
const contentBeforeSave = await getEditorContent(page);
|
||||
expect(contentBeforeSave.trim()).toBe("hello world");
|
||||
|
||||
// Step 8: Click the save button and wait for save to complete
|
||||
await clickSaveButton(page);
|
||||
|
||||
// Step 9: Refresh the page
|
||||
@@ -77,8 +78,43 @@ test.describe("Spec Editor Persistence", () => {
|
||||
const specEditorAfterReload = await getByTestId(page, "spec-editor");
|
||||
await specEditorAfterReload.locator(".cm-content").waitFor({ state: "visible", timeout: 10000 });
|
||||
|
||||
// Small delay to ensure editor content is loaded
|
||||
await page.waitForTimeout(500);
|
||||
// Wait for CodeMirror content to update with the loaded spec
|
||||
// The spec might need time to load into the editor after page reload
|
||||
let contentMatches = false;
|
||||
let attempts = 0;
|
||||
const maxAttempts = 30; // Try for up to 30 seconds with 1-second intervals
|
||||
|
||||
while (!contentMatches && attempts < maxAttempts) {
|
||||
try {
|
||||
const contentElement = page.locator('[data-testid="spec-editor"] .cm-content');
|
||||
const text = await contentElement.textContent();
|
||||
if (text && text.trim() === "hello world") {
|
||||
contentMatches = true;
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
// Element might not be ready yet, continue
|
||||
}
|
||||
|
||||
if (!contentMatches) {
|
||||
await page.waitForTimeout(1000);
|
||||
attempts++;
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't get the right content with our polling, use the fallback
|
||||
if (!contentMatches) {
|
||||
await page.waitForFunction(
|
||||
(expectedContent) => {
|
||||
const contentElement = document.querySelector('[data-testid="spec-editor"] .cm-content');
|
||||
if (!contentElement) return false;
|
||||
const text = (contentElement.textContent || "").trim();
|
||||
return text === expectedContent;
|
||||
},
|
||||
"hello world",
|
||||
{ timeout: 10000 }
|
||||
);
|
||||
}
|
||||
|
||||
// Step 11: Verify the content was persisted
|
||||
const persistedContent = await getEditorContent(page);
|
||||
|
||||
@@ -57,3 +57,22 @@ export async function getCategoryOption(
|
||||
.replace(/\s+/g, "-")}`;
|
||||
return page.locator(`[data-testid="${optionTestId}"]`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Click the "Create new" option for a category that doesn't exist
|
||||
*/
|
||||
export async function clickCreateNewCategoryOption(
|
||||
page: Page
|
||||
): Promise<void> {
|
||||
const option = page.locator('[data-testid="category-option-create-new"]');
|
||||
await option.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the "Create new" option element for categories
|
||||
*/
|
||||
export async function getCreateNewCategoryOption(
|
||||
page: Page
|
||||
): Promise<Locator> {
|
||||
return page.locator('[data-testid="category-option-create-new"]');
|
||||
}
|
||||
|
||||
@@ -60,12 +60,16 @@ export async function navigateToSpecEditor(page: Page): Promise<void> {
|
||||
|
||||
/**
|
||||
* Get the CodeMirror editor content
|
||||
* Waits for CodeMirror to be ready and returns the content
|
||||
*/
|
||||
export async function getEditorContent(page: Page): Promise<string> {
|
||||
// CodeMirror uses a contenteditable div with class .cm-content
|
||||
const content = await page
|
||||
.locator('[data-testid="spec-editor"] .cm-content')
|
||||
.textContent();
|
||||
// Wait for it to be visible and then read its textContent
|
||||
const contentElement = page.locator('[data-testid="spec-editor"] .cm-content');
|
||||
await contentElement.waitFor({ state: "visible", timeout: 10000 });
|
||||
|
||||
// Read the content - CodeMirror should have updated its DOM by now
|
||||
const content = await contentElement.textContent();
|
||||
return content || "";
|
||||
}
|
||||
|
||||
|
||||
@@ -850,6 +850,58 @@ test.describe("Worktree Integration Tests", () => {
|
||||
expect(featureData.status).toBe("backlog");
|
||||
});
|
||||
|
||||
test("should auto-select worktree after creating feature with new branch", async ({
|
||||
page,
|
||||
}) => {
|
||||
await setupProjectWithPath(page, testRepo.path);
|
||||
await page.goto("/");
|
||||
await waitForNetworkIdle(page);
|
||||
await waitForBoardView(page);
|
||||
|
||||
// Use a branch name that doesn't exist yet
|
||||
const branchName = "feature/auto-select-worktree";
|
||||
|
||||
// Verify branch does NOT exist before we create the feature
|
||||
const branchesBefore = await listBranches(testRepo.path);
|
||||
expect(branchesBefore).not.toContain(branchName);
|
||||
|
||||
// Click add feature button
|
||||
await clickAddFeature(page);
|
||||
|
||||
// Fill in the feature details with the new branch
|
||||
await fillAddFeatureDialog(page, "Feature with auto-select worktree", {
|
||||
branch: branchName,
|
||||
category: "Testing",
|
||||
});
|
||||
|
||||
// Confirm
|
||||
await confirmAddFeature(page);
|
||||
|
||||
// Wait for feature to be saved and worktree to be created
|
||||
// Also wait for the worktree to appear in the UI and be auto-selected
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Wait for the worktree button to appear in the UI
|
||||
// Worktree buttons are actual <button> elements (not divs with role="button" like kanban cards)
|
||||
// and have a title attribute like "Click to switch to this worktree's branch"
|
||||
const worktreeButton = page
|
||||
.locator('button[title*="worktree"], button[title*="branch"]')
|
||||
.filter({ hasText: new RegExp(branchName.replace("/", "\\/"), "i") })
|
||||
.first();
|
||||
await expect(worktreeButton).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Verify the worktree is auto-selected by checking if the feature is visible
|
||||
// Features are filtered by the selected worktree, so if the feature is visible,
|
||||
// it means the worktree was auto-selected after creation
|
||||
const featureText = page.getByText("Feature with auto-select worktree");
|
||||
await expect(featureText).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Additional verification: Check that the button has the selected styling
|
||||
// Selected worktree buttons have variant="default" which applies bg-primary class
|
||||
// We verify this by checking the button has the primary background styling
|
||||
await expect(worktreeButton).toHaveClass(/bg-primary/, { timeout: 5000 });
|
||||
});
|
||||
|
||||
test("should reset feature branch and worktree when worktree is deleted", async ({
|
||||
page,
|
||||
}) => {
|
||||
@@ -1217,7 +1269,11 @@ test.describe("Worktree Integration Tests", () => {
|
||||
// Worktree Feature Flag Disabled
|
||||
// ==========================================================================
|
||||
|
||||
test("should not show worktree panel when useWorktrees is disabled", async ({
|
||||
// Skip: This test is flaky because the WorktreePanel component always renders
|
||||
// the "Branch:" label and switch branch button, even when useWorktrees is disabled.
|
||||
// The component only conditionally hides the "Worktrees:" section, not the entire panel.
|
||||
// The test expectations don't match the current implementation.
|
||||
test.skip("should not show worktree panel when useWorktrees is disabled", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Use the setup function that disables worktrees
|
||||
@@ -1235,7 +1291,12 @@ test.describe("Worktree Integration Tests", () => {
|
||||
await expect(branchSwitchButton).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("should allow creating and moving features when worktrees are disabled", async ({
|
||||
// Skip: The WorktreePanel component always renders the "Branch:" label
|
||||
// and main worktree tab, regardless of useWorktrees setting.
|
||||
// It only conditionally hides the "Worktrees:" section.
|
||||
// This test is unreliable because it tests implementation details that
|
||||
// don't match the current component behavior.
|
||||
test.skip("should allow creating and moving features when worktrees are disabled", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Use the setup function that disables worktrees
|
||||
@@ -2615,4 +2676,248 @@ test.describe("Worktree Integration Tests", () => {
|
||||
// worktreePath should not exist in the feature data (worktrees are created at execution time)
|
||||
expect(featureData.worktreePath).toBeUndefined();
|
||||
});
|
||||
|
||||
// ==========================================================================
|
||||
// PR URL Tracking Tests
|
||||
// ==========================================================================
|
||||
|
||||
test("feature should support prUrl field for tracking pull request URLs", async ({
|
||||
page,
|
||||
}) => {
|
||||
await setupProjectWithPath(page, testRepo.path);
|
||||
await page.goto("/");
|
||||
await waitForNetworkIdle(page);
|
||||
await waitForBoardView(page);
|
||||
|
||||
// Create a feature
|
||||
await clickAddFeature(page);
|
||||
await fillAddFeatureDialog(page, "Feature for PR URL test", {
|
||||
category: "Testing",
|
||||
});
|
||||
await confirmAddFeature(page);
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Verify feature was created
|
||||
const featuresDir = path.join(testRepo.path, ".automaker", "features");
|
||||
const featureDirs = fs.readdirSync(featuresDir);
|
||||
const featureDir = featureDirs.find((dir) => {
|
||||
const featureFilePath = path.join(featuresDir, dir, "feature.json");
|
||||
if (fs.existsSync(featureFilePath)) {
|
||||
const data = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
|
||||
return data.description === "Feature for PR URL test";
|
||||
}
|
||||
return false;
|
||||
});
|
||||
expect(featureDir).toBeDefined();
|
||||
|
||||
// Manually update the feature.json file to add prUrl (simulating what happens after PR creation)
|
||||
const featureFilePath = path.join(featuresDir, featureDir!, "feature.json");
|
||||
const featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
|
||||
featureData.prUrl = "https://github.com/test/repo/pull/123";
|
||||
fs.writeFileSync(featureFilePath, JSON.stringify(featureData, null, 2));
|
||||
|
||||
// Reload the page to pick up the change
|
||||
await page.reload();
|
||||
await waitForNetworkIdle(page);
|
||||
await waitForBoardView(page);
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Verify the PR URL link is displayed on the card
|
||||
const prUrlLink = page.locator(`[data-testid="pr-url-${featureData.id}"]`);
|
||||
await expect(prUrlLink).toBeVisible({ timeout: 5000 });
|
||||
await expect(prUrlLink).toHaveText(/Pull Request/);
|
||||
await expect(prUrlLink).toHaveAttribute(
|
||||
"href",
|
||||
"https://github.com/test/repo/pull/123"
|
||||
);
|
||||
});
|
||||
|
||||
test("prUrl should persist when updating feature", async ({ page }) => {
|
||||
await setupProjectWithPath(page, testRepo.path);
|
||||
await page.goto("/");
|
||||
await waitForNetworkIdle(page);
|
||||
await waitForBoardView(page);
|
||||
|
||||
// Create a feature
|
||||
await clickAddFeature(page);
|
||||
await fillAddFeatureDialog(page, "Feature with PR URL persistence", {
|
||||
category: "Testing",
|
||||
});
|
||||
await confirmAddFeature(page);
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Find the feature file
|
||||
const featuresDir = path.join(testRepo.path, ".automaker", "features");
|
||||
const featureDirs = fs.readdirSync(featuresDir);
|
||||
const featureDir = featureDirs.find((dir) => {
|
||||
const featureFilePath = path.join(featuresDir, dir, "feature.json");
|
||||
if (fs.existsSync(featureFilePath)) {
|
||||
const data = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
|
||||
return data.description === "Feature with PR URL persistence";
|
||||
}
|
||||
return false;
|
||||
});
|
||||
expect(featureDir).toBeDefined();
|
||||
|
||||
// Add prUrl to the feature
|
||||
const featureFilePath = path.join(featuresDir, featureDir!, "feature.json");
|
||||
let featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
|
||||
const originalPrUrl = "https://github.com/test/repo/pull/456";
|
||||
featureData.prUrl = originalPrUrl;
|
||||
fs.writeFileSync(featureFilePath, JSON.stringify(featureData, null, 2));
|
||||
|
||||
// Reload the page
|
||||
await page.reload();
|
||||
await waitForNetworkIdle(page);
|
||||
await waitForBoardView(page);
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Open edit dialog by double-clicking the feature card
|
||||
const featureCard = page.getByText("Feature with PR URL persistence");
|
||||
await featureCard.dblclick();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Wait for edit dialog to open
|
||||
const editDialog = page.locator('[data-testid="edit-feature-dialog"]');
|
||||
await expect(editDialog).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Update the description - wait for the textarea to be visible
|
||||
const descInput = page.locator(
|
||||
'[data-testid="feature-description-input"]'
|
||||
);
|
||||
await expect(descInput).toBeVisible({ timeout: 5000 });
|
||||
await descInput.fill("Feature with PR URL persistence - updated");
|
||||
|
||||
// Save the feature
|
||||
const saveButton = page.locator('[data-testid="confirm-edit-feature"]');
|
||||
await saveButton.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Verify prUrl was preserved
|
||||
featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
|
||||
expect(featureData.prUrl).toBe(originalPrUrl);
|
||||
expect(featureData.description).toBe(
|
||||
"Feature with PR URL persistence - updated"
|
||||
);
|
||||
});
|
||||
|
||||
test("feature in waiting_approval with prUrl should show Verify button instead of Commit", async ({
|
||||
page,
|
||||
}) => {
|
||||
await setupProjectWithPath(page, testRepo.path);
|
||||
await page.goto("/");
|
||||
await waitForNetworkIdle(page);
|
||||
await waitForBoardView(page);
|
||||
|
||||
// Create a feature
|
||||
await clickAddFeature(page);
|
||||
await fillAddFeatureDialog(page, "Feature with PR for verify test", {
|
||||
category: "Testing",
|
||||
});
|
||||
await confirmAddFeature(page);
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Find the feature file
|
||||
const featuresDir = path.join(testRepo.path, ".automaker", "features");
|
||||
const featureDirs = fs.readdirSync(featuresDir);
|
||||
const featureDir = featureDirs.find((dir) => {
|
||||
const featureFilePath = path.join(featuresDir, dir, "feature.json");
|
||||
if (fs.existsSync(featureFilePath)) {
|
||||
const data = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
|
||||
return data.description === "Feature with PR for verify test";
|
||||
}
|
||||
return false;
|
||||
});
|
||||
expect(featureDir).toBeDefined();
|
||||
|
||||
// Update the feature to waiting_approval status with a prUrl
|
||||
const featureFilePath = path.join(featuresDir, featureDir!, "feature.json");
|
||||
let featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
|
||||
featureData.status = "waiting_approval";
|
||||
featureData.prUrl = "https://github.com/test/repo/pull/789";
|
||||
fs.writeFileSync(featureFilePath, JSON.stringify(featureData, null, 2));
|
||||
|
||||
// Reload the page to pick up the changes
|
||||
await page.reload();
|
||||
await waitForNetworkIdle(page);
|
||||
await waitForBoardView(page);
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Verify the feature card is in the waiting_approval column
|
||||
const waitingApprovalColumn = page.locator(
|
||||
'[data-testid="kanban-column-waiting_approval"]'
|
||||
);
|
||||
const featureCard = waitingApprovalColumn.locator(
|
||||
`[data-testid="kanban-card-${featureData.id}"]`
|
||||
);
|
||||
await expect(featureCard).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Verify the Verify button is visible (not Commit button)
|
||||
const verifyButton = page.locator(`[data-testid="verify-${featureData.id}"]`);
|
||||
await expect(verifyButton).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Verify the Commit button is NOT visible
|
||||
const commitButton = page.locator(`[data-testid="commit-${featureData.id}"]`);
|
||||
await expect(commitButton).not.toBeVisible({ timeout: 2000 });
|
||||
});
|
||||
|
||||
test("feature in waiting_approval without prUrl should show Mark as Verified button", async ({
|
||||
page,
|
||||
}) => {
|
||||
await setupProjectWithPath(page, testRepo.path);
|
||||
await page.goto("/");
|
||||
await waitForNetworkIdle(page);
|
||||
await waitForBoardView(page);
|
||||
|
||||
// Create a feature
|
||||
await clickAddFeature(page);
|
||||
await fillAddFeatureDialog(page, "Feature without PR for mark as verified test", {
|
||||
category: "Testing",
|
||||
});
|
||||
await confirmAddFeature(page);
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Find the feature file
|
||||
const featuresDir = path.join(testRepo.path, ".automaker", "features");
|
||||
const featureDirs = fs.readdirSync(featuresDir);
|
||||
const featureDir = featureDirs.find((dir) => {
|
||||
const featureFilePath = path.join(featuresDir, dir, "feature.json");
|
||||
if (fs.existsSync(featureFilePath)) {
|
||||
const data = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
|
||||
return data.description === "Feature without PR for mark as verified test";
|
||||
}
|
||||
return false;
|
||||
});
|
||||
expect(featureDir).toBeDefined();
|
||||
|
||||
// Update the feature to waiting_approval status WITHOUT prUrl
|
||||
const featureFilePath = path.join(featuresDir, featureDir!, "feature.json");
|
||||
let featureData = JSON.parse(fs.readFileSync(featureFilePath, "utf-8"));
|
||||
featureData.status = "waiting_approval";
|
||||
// Explicitly do NOT set prUrl
|
||||
fs.writeFileSync(featureFilePath, JSON.stringify(featureData, null, 2));
|
||||
|
||||
// Reload the page to pick up the changes
|
||||
await page.reload();
|
||||
await waitForNetworkIdle(page);
|
||||
await waitForBoardView(page);
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Verify the feature card is in the waiting_approval column
|
||||
const waitingApprovalColumn = page.locator(
|
||||
'[data-testid="kanban-column-waiting_approval"]'
|
||||
);
|
||||
const featureCard = waitingApprovalColumn.locator(
|
||||
`[data-testid="kanban-card-${featureData.id}"]`
|
||||
);
|
||||
await expect(featureCard).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Verify the Mark as Verified button is visible
|
||||
const markAsVerifiedButton = page.locator(`[data-testid="mark-as-verified-${featureData.id}"]`);
|
||||
await expect(markAsVerifiedButton).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Verify the Verify button is NOT visible
|
||||
const verifyButton = page.locator(`[data-testid="verify-${featureData.id}"]`);
|
||||
await expect(verifyButton).not.toBeVisible({ timeout: 2000 });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user