Files
automaker/apps/ui/tests/spec-editor-persistence.spec.ts
Kacper ad4da23743 Merge main into refactor/frontend
- Resolved conflicts from apps/app to apps/ui migration
- Moved worktree-panel component to apps/ui
- Moved dependency-resolver.ts to apps/ui
- Removed worktree-selector.tsx (replaced by worktree-panel)
- Merged theme updates, file browser improvements, and Gemini fixes
- Merged server dependency resolver and auto-mode-service updates

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 20:14:19 +01:00

325 lines
11 KiB
TypeScript

import { test, expect } from "@playwright/test";
import {
resetFixtureSpec,
setupProjectWithFixture,
getFixturePath,
navigateToSpecEditor,
getEditorContent,
setEditorContent,
clickSaveButton,
getByTestId,
clickElement,
fillInput,
waitForNetworkIdle,
waitForElement,
} from "./utils";
test.describe("Spec Editor Persistence", () => {
test.beforeEach(async () => {
// Reset the fixture spec file to original content before each test
resetFixtureSpec();
});
test.afterEach(async () => {
// Clean up - reset the spec file after each test
resetFixtureSpec();
});
test("should open project, edit spec, save, and persist changes after refresh", async ({
page,
}) => {
// Use the resolved fixture path
const fixturePath = getFixturePath();
// Step 1: Set up the project in localStorage pointing to our fixture
await setupProjectWithFixture(page, fixturePath);
// Step 2: Navigate to the app
await page.goto("/");
await waitForNetworkIdle(page);
// Step 3: Verify we're on the dashboard with the project loaded
// The sidebar should show the project selector
const sidebar = await getByTestId(page, "sidebar");
await sidebar.waitFor({ state: "visible", timeout: 10000 });
// Step 4: Click on the Spec Editor in the sidebar
await navigateToSpecEditor(page);
// Step 5: Wait for the spec editor to load
const specEditor = await getByTestId(page, "spec-editor");
await specEditor.waitFor({ state: "visible", 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 });
// Small delay to ensure editor is fully initialized
await page.waitForTimeout(500);
// Step 7: Modify the editor content to "hello world"
await setEditorContent(page, "hello world");
// Step 8: Click the save button
await clickSaveButton(page);
// Step 9: Refresh the page
await page.reload();
await waitForNetworkIdle(page);
// Step 10: Navigate back to the spec editor
// After reload, we need to wait for the app to initialize
await waitForElement(page, "sidebar", { timeout: 10000 });
// Navigate to spec editor again
await navigateToSpecEditor(page);
// Wait for CodeMirror to be ready
const specEditorAfterReload = await getByTestId(page, "spec-editor");
await specEditorAfterReload.locator(".cm-content").waitFor({ state: "visible", timeout: 10000 });
// Small delay to ensure editor content is loaded
await page.waitForTimeout(500);
// Step 11: Verify the content was persisted
const persistedContent = await getEditorContent(page);
expect(persistedContent.trim()).toBe("hello world");
});
test("should handle opening project via Open Project button and file browser", async ({
page,
}) => {
// This test covers the flow of:
// 1. Clicking Open Project button
// 2. Using the file browser to navigate to the fixture directory
// 3. Opening the project
// 4. Editing the spec
// Set up without a current project to test the open project flow
await page.addInitScript(() => {
const mockState = {
state: {
projects: [],
currentProject: null,
currentView: "welcome",
theme: "dark",
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: 3,
},
version: 0,
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
// Mark setup as complete
const setupState = {
state: {
isFirstRun: false,
setupComplete: true,
currentStep: "complete",
skipClaudeSetup: false,
},
version: 0,
};
localStorage.setItem("automaker-setup", JSON.stringify(setupState));
});
// Navigate to the app
await page.goto("/");
await waitForNetworkIdle(page);
// Wait for the sidebar to be visible
const sidebar = await getByTestId(page, "sidebar");
await sidebar.waitFor({ state: "visible", timeout: 10000 });
// Click the Open Project button
const openProjectButton = await getByTestId(page, "open-project-button");
// Check if the button is visible (it might not be in collapsed sidebar)
const isButtonVisible = await openProjectButton
.isVisible()
.catch(() => false);
if (isButtonVisible) {
await clickElement(page, "open-project-button");
// The file browser dialog should open
// Note: In web mode, this might use the FileBrowserDialog component
// which makes requests to the backend server at /api/fs/browse
// Wait a bit to see if a dialog appears
await page.waitForTimeout(1000);
// Check if a dialog is visible
const dialog = page.locator('[role="dialog"]');
const dialogVisible = await dialog.isVisible().catch(() => false);
if (dialogVisible) {
// If file browser dialog is open, we need to navigate to the fixture path
// This depends on the current directory structure
// For now, let's verify the dialog appeared and close it
// A full test would navigate through directories
console.log("File browser dialog opened successfully");
// Press Escape to close the dialog
await page.keyboard.press("Escape");
}
}
// For a complete e2e test with file browsing, we'd need to:
// 1. Navigate through the directory tree
// 2. Select the projectA directory
// 3. Click "Select Current Folder"
// Since this involves actual file system navigation,
// and depends on the backend server being properly configured,
// we'll verify the basic UI elements are present
expect(sidebar).toBeTruthy();
});
});
test.describe("Spec Editor - Full Open Project Flow", () => {
test.beforeEach(async () => {
// Reset the fixture spec file to original content before each test
resetFixtureSpec();
});
test.afterEach(async () => {
// Clean up - reset the spec file after each test
resetFixtureSpec();
});
// Skip in CI - file browser navigation is flaky in headless environments
test.skip("should open project via file browser, edit spec, and persist", async ({
page,
}) => {
// Navigate to app first
await page.goto("/");
await waitForNetworkIdle(page);
// Set up localStorage state (without a current project, but mark setup complete)
// Using evaluate instead of addInitScript so it only runs once
// Note: In CI, setup wizard is also skipped via NEXT_PUBLIC_SKIP_SETUP env var
await page.evaluate(() => {
const mockState = {
state: {
projects: [],
currentProject: null,
currentView: "welcome",
theme: "dark",
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: 3,
},
version: 0,
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
// Mark setup as complete (fallback for when NEXT_PUBLIC_SKIP_SETUP isn't set)
const setupState = {
state: {
isFirstRun: false,
setupComplete: true,
currentStep: "complete",
skipClaudeSetup: false,
},
version: 0,
};
localStorage.setItem("automaker-setup", JSON.stringify(setupState));
});
// Reload to apply the localStorage state
await page.reload();
await waitForNetworkIdle(page);
// Wait for sidebar
await waitForElement(page, "sidebar", { timeout: 10000 });
// Click the Open Project button
const openProjectButton = await getByTestId(page, "open-project-button");
await openProjectButton.waitFor({ state: "visible", timeout: 10000 });
await clickElement(page, "open-project-button");
// Wait for the file browser dialog to open
const dialogTitle = page.locator('text="Select Project Directory"');
await dialogTitle.waitFor({ state: "visible", timeout: 10000 });
// Wait for the dialog to fully load (loading to complete)
await page.waitForFunction(
() => !document.body.textContent?.includes("Loading directories..."),
{ timeout: 10000 }
);
// Use the path input to directly navigate to the fixture directory
const pathInput = await getByTestId(page, "path-input");
await pathInput.waitFor({ state: "visible", timeout: 5000 });
// Clear the input and type the full path to the fixture
await fillInput(page, "path-input", getFixturePath());
// Click the Go button to navigate to the path
await clickElement(page, "go-to-path-button");
// Wait for loading to complete
await page.waitForFunction(
() => !document.body.textContent?.includes("Loading directories..."),
{ timeout: 10000 }
);
// Verify we're in the right directory by checking the path display
const pathDisplay = page.locator(".font-mono.text-sm.truncate");
await expect(pathDisplay).toContainText("projectA");
// Click "Select Current Folder" button
const selectFolderButton = page.locator(
'button:has-text("Select Current Folder")'
);
await selectFolderButton.click();
// Wait for dialog to close and project to load
await page.waitForFunction(
() => !document.querySelector('[role="dialog"]'),
{ timeout: 10000 }
);
await page.waitForTimeout(500);
// Navigate to spec editor
const specNav = await getByTestId(page, "nav-spec");
await specNav.waitFor({ state: "visible", timeout: 10000 });
await clickElement(page, "nav-spec");
// Wait for spec view with the editor (not the empty state)
await waitForElement(page, "spec-view", { timeout: 10000 });
const specEditorForOpenFlow = await getByTestId(page, "spec-editor");
await specEditorForOpenFlow.locator(".cm-content").waitFor({ state: "visible", timeout: 10000 });
await page.waitForTimeout(500);
// Edit the content
await setEditorContent(page, "hello world");
// Click save button
await clickSaveButton(page);
// Refresh and verify persistence
await page.reload();
await waitForNetworkIdle(page);
// Navigate back to spec editor
await specNav.waitFor({ state: "visible", timeout: 10000 });
await clickElement(page, "nav-spec");
const specEditorAfterRefresh = await getByTestId(page, "spec-editor");
await specEditorAfterRefresh.locator(".cm-content").waitFor({ state: "visible", timeout: 10000 });
await page.waitForTimeout(500);
// Verify the content persisted
const persistedContent = await getEditorContent(page);
expect(persistedContent.trim()).toBe("hello world");
});
});