mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
Merge main into massive-terminal-upgrade
Resolves merge conflicts: - apps/server/src/routes/terminal/common.ts: Keep randomBytes import, use @automaker/utils for createLogger - apps/ui/eslint.config.mjs: Use main's explicit globals list with XMLHttpRequest and MediaQueryListEvent additions - apps/ui/src/components/views/terminal-view.tsx: Keep our terminal improvements (killAllSessions, beforeunload, better error handling) - apps/ui/src/config/terminal-themes.ts: Keep our search highlight colors for all themes - apps/ui/src/store/app-store.ts: Keep our terminal settings persistence improvements (merge function) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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!');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,57 +35,98 @@ 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 editor to load
|
||||
const specEditor = await getByTestId(page, "spec-editor");
|
||||
await specEditor.waitFor({ state: "visible", timeout: 10000 });
|
||||
// Step 5: Wait for the spec view to load (not empty state)
|
||||
await waitForElement(page, 'spec-view', { timeout: 10000 });
|
||||
|
||||
// Step 6: Wait for CodeMirror to initialize (it has a .cm-content element)
|
||||
await specEditor.locator(".cm-content").waitFor({ state: "visible", 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 });
|
||||
|
||||
// Small delay to ensure editor is fully initialized
|
||||
await page.waitForTimeout(500);
|
||||
// Step 7: Wait for CodeMirror to initialize (it has a .cm-content element)
|
||||
await specEditor.locator('.cm-content').waitFor({ state: 'visible', timeout: 10000 });
|
||||
|
||||
// Step 7: Modify the editor content to "hello world"
|
||||
await setEditorContent(page, "hello world");
|
||||
// Step 8: 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 9: Click the save button and wait for save to complete
|
||||
await clickSaveButton(page);
|
||||
|
||||
// Step 9: Refresh the page
|
||||
// Step 10: Refresh the page
|
||||
await page.reload();
|
||||
await waitForNetworkIdle(page);
|
||||
|
||||
// Step 10: Navigate back to the spec editor
|
||||
// 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 });
|
||||
|
||||
// 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
|
||||
|
||||
// Step 11: Verify the content was persisted
|
||||
while (!contentMatches && attempts < maxAttempts) {
|
||||
try {
|
||||
const contentElement = page.locator('[data-testid="spec-editor"] .cm-content');
|
||||
const text = await contentElement.textContent();
|
||||
if (text && text.trim() === 'hello world') {
|
||||
contentMatches = true;
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Element might not be ready yet, continue
|
||||
}
|
||||
|
||||
if (!contentMatches) {
|
||||
await page.waitForTimeout(1000);
|
||||
attempts++;
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't get the right content with our polling, use the fallback
|
||||
if (!contentMatches) {
|
||||
await page.waitForFunction(
|
||||
(expectedContent) => {
|
||||
const contentElement = document.querySelector('[data-testid="spec-editor"] .cm-content');
|
||||
if (!contentElement) return false;
|
||||
const text = (contentElement.textContent || '').trim();
|
||||
return text === expectedContent;
|
||||
},
|
||||
'hello world',
|
||||
{ timeout: 10000 }
|
||||
);
|
||||
}
|
||||
|
||||
// Step 12: Verify the content was persisted
|
||||
const persistedContent = await getEditorContent(page);
|
||||
expect(persistedContent.trim()).toBe("hello world");
|
||||
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:
|
||||
@@ -100,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
|
||||
@@ -161,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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,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();
|
||||
@@ -193,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)
|
||||
@@ -208,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
|
||||
@@ -238,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);
|
||||
@@ -310,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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Page } from "@playwright/test";
|
||||
import { clickElement } from "../core/interactions";
|
||||
import { waitForElement } from "../core/waiting";
|
||||
import { Page } from '@playwright/test';
|
||||
import { clickElement } from '../core/interactions';
|
||||
import { waitForElement } from '../core/waiting';
|
||||
|
||||
/**
|
||||
* Navigate to the board/kanban view
|
||||
@@ -8,11 +8,11 @@ import { waitForElement } from "../core/waiting";
|
||||
*/
|
||||
export async function navigateToBoard(page: Page): Promise<void> {
|
||||
// Navigate directly to /board route
|
||||
await page.goto("/board");
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.goto('/board');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Wait for the board view to be visible
|
||||
await waitForElement(page, "board-view", { timeout: 10000 });
|
||||
await waitForElement(page, 'board-view', { timeout: 10000 });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -21,11 +21,11 @@ export async function navigateToBoard(page: Page): Promise<void> {
|
||||
*/
|
||||
export async function navigateToContext(page: Page): Promise<void> {
|
||||
// Navigate directly to /context route
|
||||
await page.goto("/context");
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.goto('/context');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Wait for the context view to be visible
|
||||
await waitForElement(page, "context-view", { timeout: 10000 });
|
||||
await waitForElement(page, 'context-view', { timeout: 10000 });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -34,11 +34,28 @@ export async function navigateToContext(page: Page): Promise<void> {
|
||||
*/
|
||||
export async function navigateToSpec(page: Page): Promise<void> {
|
||||
// Navigate directly to /spec route
|
||||
await page.goto("/spec");
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.goto('/spec');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Wait for the spec view to be visible
|
||||
await waitForElement(page, "spec-view", { timeout: 10000 });
|
||||
// Wait for loading state to complete first (if present)
|
||||
const loadingElement = page.locator('[data-testid="spec-view-loading"]');
|
||||
try {
|
||||
const loadingVisible = await loadingElement.isVisible({ timeout: 2000 });
|
||||
if (loadingVisible) {
|
||||
// Wait for loading to disappear (spec view or empty state will appear)
|
||||
await loadingElement.waitFor({ state: 'hidden', timeout: 10000 });
|
||||
}
|
||||
} catch {
|
||||
// Loading element not found or already hidden, continue
|
||||
}
|
||||
|
||||
// Wait for either the main spec view or empty state to be visible
|
||||
// The spec-view element appears when loading is complete and spec exists
|
||||
// The spec-view-empty element appears when loading is complete and spec doesn't exist
|
||||
await Promise.race([
|
||||
waitForElement(page, 'spec-view', { timeout: 10000 }).catch(() => null),
|
||||
waitForElement(page, 'spec-view-empty', { timeout: 10000 }).catch(() => null),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,11 +64,11 @@ export async function navigateToSpec(page: Page): Promise<void> {
|
||||
*/
|
||||
export async function navigateToAgent(page: Page): Promise<void> {
|
||||
// Navigate directly to /agent route
|
||||
await page.goto("/agent");
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.goto('/agent');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Wait for the agent view to be visible
|
||||
await waitForElement(page, "agent-view", { timeout: 10000 });
|
||||
await waitForElement(page, 'agent-view', { timeout: 10000 });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,11 +77,11 @@ export async function navigateToAgent(page: Page): Promise<void> {
|
||||
*/
|
||||
export async function navigateToSettings(page: Page): Promise<void> {
|
||||
// Navigate directly to /settings route
|
||||
await page.goto("/settings");
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.goto('/settings');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Wait for the settings view to be visible
|
||||
await waitForElement(page, "settings-view", { timeout: 10000 });
|
||||
await waitForElement(page, 'settings-view', { timeout: 10000 });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,31 +90,27 @@ export async function navigateToSettings(page: Page): Promise<void> {
|
||||
*/
|
||||
export async function navigateToSetup(page: Page): Promise<void> {
|
||||
// Dynamic import to avoid circular dependency
|
||||
const { setupFirstRun } = await import("../project/setup");
|
||||
const { setupFirstRun } = await import('../project/setup');
|
||||
await setupFirstRun(page);
|
||||
await page.goto("/");
|
||||
await page.waitForLoadState("networkidle");
|
||||
await waitForElement(page, "setup-view", { timeout: 10000 });
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await waitForElement(page, 'setup-view', { timeout: 10000 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the welcome view (clear project selection)
|
||||
*/
|
||||
export async function navigateToWelcome(page: Page): Promise<void> {
|
||||
await page.goto("/");
|
||||
await page.waitForLoadState("networkidle");
|
||||
await waitForElement(page, "welcome-view", { timeout: 10000 });
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await waitForElement(page, 'welcome-view', { timeout: 10000 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to a specific view using the sidebar navigation
|
||||
*/
|
||||
export async function navigateToView(
|
||||
page: Page,
|
||||
viewId: string
|
||||
): Promise<void> {
|
||||
const navSelector =
|
||||
viewId === "settings" ? "settings-button" : `nav-${viewId}`;
|
||||
export async function navigateToView(page: Page, viewId: string): Promise<void> {
|
||||
const navSelector = viewId === 'settings' ? 'settings-button' : `nav-${viewId}`;
|
||||
await clickElement(page, navSelector);
|
||||
await page.waitForTimeout(100);
|
||||
}
|
||||
@@ -108,7 +121,7 @@ export async function navigateToView(
|
||||
export async function getCurrentView(page: Page): Promise<string | null> {
|
||||
// Get the current view from zustand store via localStorage
|
||||
const storage = await page.evaluate(() => {
|
||||
const item = localStorage.getItem("automaker-storage");
|
||||
const item = localStorage.getItem('automaker-storage');
|
||||
return item ? JSON.parse(item) : null;
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Page, Locator } from "@playwright/test";
|
||||
import { clickElement, fillInput } from "../core/interactions";
|
||||
import { waitForElement, waitForElementHidden } from "../core/waiting";
|
||||
import { getByTestId } from "../core/elements";
|
||||
import { expect } from "@playwright/test";
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
import { clickElement, fillInput } from '../core/interactions';
|
||||
import { waitForElement, waitForElementHidden } from '../core/waiting';
|
||||
import { getByTestId } from '../core/elements';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Get the context file list element
|
||||
@@ -14,10 +14,7 @@ export async function getContextFileList(page: Page): Promise<Locator> {
|
||||
/**
|
||||
* Click on a context file in the list
|
||||
*/
|
||||
export async function clickContextFile(
|
||||
page: Page,
|
||||
fileName: string
|
||||
): Promise<void> {
|
||||
export async function clickContextFile(page: Page, fileName: string): Promise<void> {
|
||||
const fileButton = page.locator(`[data-testid="context-file-${fileName}"]`);
|
||||
await fileButton.click();
|
||||
}
|
||||
@@ -33,18 +30,15 @@ export async function getContextEditor(page: Page): Promise<Locator> {
|
||||
* Get the context editor content
|
||||
*/
|
||||
export async function getContextEditorContent(page: Page): Promise<string> {
|
||||
const editor = await getByTestId(page, "context-editor");
|
||||
const editor = await getByTestId(page, 'context-editor');
|
||||
return await editor.inputValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the context editor content
|
||||
*/
|
||||
export async function setContextEditorContent(
|
||||
page: Page,
|
||||
content: string
|
||||
): Promise<void> {
|
||||
const editor = await getByTestId(page, "context-editor");
|
||||
export async function setContextEditorContent(page: Page, content: string): Promise<void> {
|
||||
const editor = await getByTestId(page, 'context-editor');
|
||||
await editor.fill(content);
|
||||
}
|
||||
|
||||
@@ -52,8 +46,8 @@ export async function setContextEditorContent(
|
||||
* Open the add context file dialog
|
||||
*/
|
||||
export async function openAddContextFileDialog(page: Page): Promise<void> {
|
||||
await clickElement(page, "add-context-file");
|
||||
await waitForElement(page, "add-context-dialog");
|
||||
await clickElement(page, 'add-context-file');
|
||||
await waitForElement(page, 'add-context-dialog');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,11 +59,11 @@ export async function createContextFile(
|
||||
content: string
|
||||
): Promise<void> {
|
||||
await openAddContextFileDialog(page);
|
||||
await clickElement(page, "add-text-type");
|
||||
await fillInput(page, "new-file-name", filename);
|
||||
await fillInput(page, "new-file-content", content);
|
||||
await clickElement(page, "confirm-add-file");
|
||||
await waitForElementHidden(page, "add-context-dialog");
|
||||
await clickElement(page, 'add-text-type');
|
||||
await fillInput(page, 'new-file-name', filename);
|
||||
await fillInput(page, 'new-file-content', content);
|
||||
await clickElement(page, 'confirm-add-file');
|
||||
await waitForElementHidden(page, 'add-context-dialog');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,34 +75,32 @@ export async function createContextImage(
|
||||
imagePath: string
|
||||
): Promise<void> {
|
||||
await openAddContextFileDialog(page);
|
||||
await clickElement(page, "add-image-type");
|
||||
await fillInput(page, "new-file-name", filename);
|
||||
await clickElement(page, 'add-image-type');
|
||||
await fillInput(page, 'new-file-name', filename);
|
||||
await page.setInputFiles('[data-testid="image-upload-input"]', imagePath);
|
||||
await clickElement(page, "confirm-add-file");
|
||||
await waitForElementHidden(page, "add-context-dialog");
|
||||
await clickElement(page, 'confirm-add-file');
|
||||
await waitForElementHidden(page, 'add-context-dialog');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a context file via the UI (must be selected first)
|
||||
*/
|
||||
export async function deleteSelectedContextFile(page: Page): Promise<void> {
|
||||
await clickElement(page, "delete-context-file");
|
||||
await waitForElement(page, "delete-context-dialog");
|
||||
await clickElement(page, "confirm-delete-file");
|
||||
await waitForElementHidden(page, "delete-context-dialog");
|
||||
await clickElement(page, 'delete-context-file');
|
||||
await waitForElement(page, 'delete-context-dialog');
|
||||
await clickElement(page, 'confirm-delete-file');
|
||||
await waitForElementHidden(page, 'delete-context-dialog');
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the current context file
|
||||
*/
|
||||
export async function saveContextFile(page: Page): Promise<void> {
|
||||
await clickElement(page, "save-context-file");
|
||||
await clickElement(page, 'save-context-file');
|
||||
// Wait for save to complete (button shows "Saved")
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
document
|
||||
.querySelector('[data-testid="save-context-file"]')
|
||||
?.textContent?.includes("Saved"),
|
||||
document.querySelector('[data-testid="save-context-file"]')?.textContent?.includes('Saved'),
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
}
|
||||
@@ -117,7 +109,7 @@ export async function saveContextFile(page: Page): Promise<void> {
|
||||
* Toggle markdown preview mode
|
||||
*/
|
||||
export async function toggleContextPreviewMode(page: Page): Promise<void> {
|
||||
await clickElement(page, "toggle-preview-mode");
|
||||
await clickElement(page, 'toggle-preview-mode');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,8 +120,8 @@ export async function waitForContextFile(
|
||||
filename: string,
|
||||
timeout: number = 10000
|
||||
): Promise<void> {
|
||||
const locator = await getByTestId(page, `context-file-${filename}`);
|
||||
await locator.waitFor({ state: "visible", timeout });
|
||||
const locator = page.locator(`[data-testid="context-file-${filename}"]`);
|
||||
await locator.waitFor({ state: 'visible', timeout });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,13 +134,13 @@ export async function selectContextFile(
|
||||
timeout: number = 10000
|
||||
): Promise<void> {
|
||||
const fileButton = await getByTestId(page, `context-file-${filename}`);
|
||||
await fileButton.waitFor({ state: "visible", timeout });
|
||||
await fileButton.waitFor({ state: 'visible', timeout });
|
||||
|
||||
// Use JavaScript click to ensure React onClick handler fires
|
||||
await fileButton.evaluate((el) => (el as HTMLButtonElement).click());
|
||||
|
||||
// Wait for the file to be selected (toolbar with delete button becomes visible)
|
||||
const deleteButton = await getByTestId(page, "delete-context-file");
|
||||
const deleteButton = await getByTestId(page, 'delete-context-file');
|
||||
await expect(deleteButton).toBeVisible({
|
||||
timeout,
|
||||
});
|
||||
@@ -173,11 +165,11 @@ export async function switchToEditMode(page: Page): Promise<void> {
|
||||
// First wait for content to load
|
||||
await waitForFileContentToLoad(page);
|
||||
|
||||
const markdownPreview = await getByTestId(page, "markdown-preview");
|
||||
const markdownPreview = await getByTestId(page, 'markdown-preview');
|
||||
const isPreview = await markdownPreview.isVisible().catch(() => false);
|
||||
|
||||
if (isPreview) {
|
||||
await clickElement(page, "toggle-preview-mode");
|
||||
await clickElement(page, 'toggle-preview-mode');
|
||||
await page.waitForSelector('[data-testid="context-editor"]', {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Page, Locator } from "@playwright/test";
|
||||
import { clickElement } from "../core/interactions";
|
||||
import { navigateToSpec } from "../navigation/views";
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
import { clickElement } from '../core/interactions';
|
||||
import { navigateToSpec } from '../navigation/views';
|
||||
|
||||
/**
|
||||
* Get the spec editor element
|
||||
@@ -20,10 +20,7 @@ export async function getSpecEditorContent(page: Page): Promise<string> {
|
||||
/**
|
||||
* Set the spec editor content
|
||||
*/
|
||||
export async function setSpecEditorContent(
|
||||
page: Page,
|
||||
content: string
|
||||
): Promise<void> {
|
||||
export async function setSpecEditorContent(page: Page, content: string): Promise<void> {
|
||||
const editor = await getSpecEditor(page);
|
||||
await editor.fill(content);
|
||||
}
|
||||
@@ -32,14 +29,14 @@ export async function setSpecEditorContent(
|
||||
* Click the save spec button
|
||||
*/
|
||||
export async function clickSaveSpec(page: Page): Promise<void> {
|
||||
await clickElement(page, "save-spec");
|
||||
await clickElement(page, 'save-spec');
|
||||
}
|
||||
|
||||
/**
|
||||
* Click the reload spec button
|
||||
*/
|
||||
export async function clickReloadSpec(page: Page): Promise<void> {
|
||||
await clickElement(page, "reload-spec");
|
||||
await clickElement(page, 'reload-spec');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,7 +44,7 @@ export async function clickReloadSpec(page: Page): Promise<void> {
|
||||
*/
|
||||
export async function getDisplayedSpecPath(page: Page): Promise<string | null> {
|
||||
const specView = page.locator('[data-testid="spec-view"]');
|
||||
const pathElement = specView.locator("p.text-muted-foreground").first();
|
||||
const pathElement = specView.locator('p.text-muted-foreground').first();
|
||||
return await pathElement.textContent();
|
||||
}
|
||||
|
||||
@@ -60,13 +57,17 @@ 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();
|
||||
return content || "";
|
||||
// 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 || '';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,14 +82,14 @@ export async function setEditorContent(page: Page, content: string): Promise<voi
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Select all content (Cmd+A on Mac, Ctrl+A on others)
|
||||
const isMac = process.platform === "darwin";
|
||||
await page.keyboard.press(isMac ? "Meta+a" : "Control+a");
|
||||
const isMac = process.platform === 'darwin';
|
||||
await page.keyboard.press(isMac ? 'Meta+a' : 'Control+a');
|
||||
|
||||
// Wait for selection
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
// Delete the selected content first
|
||||
await page.keyboard.press("Backspace");
|
||||
await page.keyboard.press('Backspace');
|
||||
|
||||
// Wait for deletion
|
||||
await page.waitForTimeout(100);
|
||||
@@ -111,7 +112,7 @@ export async function clickSaveButton(page: Page): Promise<void> {
|
||||
await page.waitForFunction(
|
||||
() => {
|
||||
const btn = document.querySelector('[data-testid="save-spec"]');
|
||||
return btn?.textContent?.includes("Saved");
|
||||
return btn?.textContent?.includes('Saved');
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user