diff --git a/.automaker/feature_list.json b/.automaker/feature_list.json index 6c0b68c5..e4b8f903 100644 --- a/.automaker/feature_list.json +++ b/.automaker/feature_list.json @@ -4,7 +4,7 @@ "category": "Kanban", "description": "Remove drag icon from cards when in in progress or verified. also add a timer that tracks how long it has been since the agent started, a count up timer basically formatted 00:00", "steps": [], - "status": "in_progress" + "status": "verified" }, { "id": "feature-1765261027396-b78maajg7", @@ -32,7 +32,7 @@ "category": "Core", "description": "When the electron ui refreshes, it often redirects me back to the overview, remeber my last route and restore on app load", "steps": [], - "status": "in_progress" + "status": "verified" }, { "id": "feature-1765263521367-vse4n57j8", @@ -54,5 +54,13 @@ "description": "remove claude-progress from anywhere in code or prompts as it's no longer needed", "steps": [], "status": "verified" + }, + { + "id": "feature-1765264341539-km1238av9", + "category": "Core", + "description": "remove the code view link", + "steps": [], + "status": "in_progress", + "startedAt": "2025-12-09T07:12:24.361Z" } ] \ No newline at end of file diff --git a/app/src/components/views/kanban-card.tsx b/app/src/components/views/kanban-card.tsx index dd261053..fa523fe7 100644 --- a/app/src/components/views/kanban-card.tsx +++ b/app/src/components/views/kanban-card.tsx @@ -76,16 +76,15 @@ export function KanbanCard({ feature, onEdit, onDelete, onViewOutput, onVerify, )}
-
- -
+ {isDraggable && ( +
+ +
+ )}
{feature.description} diff --git a/app/src/store/app-store.ts b/app/src/store/app-store.ts index 6b3a3b64..08604d2d 100644 --- a/app/src/store/app-store.ts +++ b/app/src/store/app-store.ts @@ -2,7 +2,7 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; import type { Project } from "@/lib/electron"; -export type ViewMode = "welcome" | "spec" | "board" | "code" | "agent" | "settings" | "tools" | "interview" | "context"; +export type ViewMode = "welcome" | "spec" | "board" | "agent" | "settings" | "tools" | "interview" | "context"; export type ThemeMode = "light" | "dark" | "system"; export interface ApiKeys { @@ -398,6 +398,7 @@ export const useAppStore = create()( partialize: (state) => ({ projects: state.projects, currentProject: state.currentProject, + currentView: state.currentView, theme: state.theme, sidebarOpen: state.sidebarOpen, apiKeys: state.apiKeys, diff --git a/app/tests/utils.ts b/app/tests/utils.ts index 6f216be7..68be558a 100644 --- a/app/tests/utils.ts +++ b/app/tests/utils.ts @@ -894,3 +894,352 @@ export async function setupMockProjectWithInProgressFeatures( options ); } + +/** + * Navigate to the spec view + */ +export async function navigateToSpec(page: Page): Promise { + await page.goto("/"); + + // Wait for the page to load + await page.waitForLoadState("networkidle"); + + // Click on the Spec nav button + const specNav = page.locator('[data-testid="nav-spec"]'); + if (await specNav.isVisible().catch(() => false)) { + await specNav.click(); + } + + // Wait for the spec view to be visible + await waitForElement(page, "spec-view", { timeout: 10000 }); +} + +/** + * Get the spec editor element + */ +export async function getSpecEditor(page: Page): Promise { + return page.locator('[data-testid="spec-editor"]'); +} + +/** + * Get the spec editor content + */ +export async function getSpecEditorContent(page: Page): Promise { + const editor = await getSpecEditor(page); + return await editor.inputValue(); +} + +/** + * Set the spec editor content + */ +export async function setSpecEditorContent( + page: Page, + content: string +): Promise { + const editor = await getSpecEditor(page); + await editor.fill(content); +} + +/** + * Click the save spec button + */ +export async function clickSaveSpec(page: Page): Promise { + await clickElement(page, "save-spec"); +} + +/** + * Click the reload spec button + */ +export async function clickReloadSpec(page: Page): Promise { + await clickElement(page, "reload-spec"); +} + +/** + * Check if the spec view path display shows the correct .automaker path + */ +export async function getDisplayedSpecPath(page: Page): Promise { + const specView = page.locator('[data-testid="spec-view"]'); + const pathElement = specView.locator("p.text-muted-foreground").first(); + return await pathElement.textContent(); +} + +/** + * Get a kanban column by its ID + */ +export async function getKanbanColumn( + page: Page, + columnId: string +): Promise { + return page.locator(`[data-testid="kanban-column-${columnId}"]`); +} + +/** + * Get the width of a kanban column + */ +export async function getKanbanColumnWidth( + page: Page, + columnId: string +): Promise { + const column = page.locator(`[data-testid="kanban-column-${columnId}"]`); + const box = await column.boundingBox(); + return box?.width ?? 0; +} + +/** + * Check if a kanban column has CSS columns (masonry) layout + */ +export async function hasKanbanColumnMasonryLayout( + page: Page, + columnId: string +): Promise { + const column = page.locator(`[data-testid="kanban-column-${columnId}"]`); + 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"; +} + +/** + * Set up a mock project with a specific current view for route persistence testing + */ +export async function setupMockProjectWithView( + page: Page, + view: string +): Promise { + await page.addInitScript((currentView: string) => { + const mockProject = { + id: "test-project-1", + name: "Test Project", + path: "/mock/test-project", + lastOpened: new Date().toISOString(), + }; + + const mockState = { + state: { + projects: [mockProject], + currentProject: mockProject, + currentView: currentView, + theme: "dark", + sidebarOpen: true, + apiKeys: { anthropic: "", google: "" }, + chatSessions: [], + chatHistoryOpen: false, + maxConcurrency: 3, + }, + version: 0, + }; + + localStorage.setItem("automaker-storage", JSON.stringify(mockState)); + }, view); +} + +/** + * Navigate to a specific view using the sidebar navigation + */ +export async function navigateToView( + page: Page, + viewId: string +): Promise { + const navSelector = viewId === "settings" ? "settings-button" : `nav-${viewId}`; + await clickElement(page, navSelector); + await page.waitForTimeout(100); +} + +/** + * Get the current view from the URL or store (checks which view is active) + */ +export async function getCurrentView(page: Page): Promise { + // Get the current view from zustand store via localStorage + const storage = await page.evaluate(() => { + const item = localStorage.getItem("automaker-storage"); + return item ? JSON.parse(item) : null; + }); + + return storage?.state?.currentView || null; +} + +/** + * Check if the drag handle is visible for a specific feature card + */ +export async function isDragHandleVisibleForFeature( + page: Page, + featureId: string +): Promise { + const dragHandle = page.locator(`[data-testid="drag-handle-${featureId}"]`); + return await dragHandle.isVisible().catch(() => false); +} + +/** + * Get the drag handle element for a specific feature card + */ +export async function getDragHandleForFeature( + page: Page, + featureId: string +): Promise { + return page.locator(`[data-testid="drag-handle-${featureId}"]`); +} + +/** + * Navigate to the welcome view (clear project selection) + */ +export async function navigateToWelcome(page: Page): Promise { + await page.goto("/"); + await page.waitForLoadState("networkidle"); + await waitForElement(page, "welcome-view", { timeout: 10000 }); +} + +/** + * Set up an empty localStorage (no projects) to show welcome screen + */ +export async function setupEmptyLocalStorage(page: Page): Promise { + 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)); + }); +} + +/** + * Set up mock projects in localStorage but with no current project (for recent projects list) + */ +export async function setupMockProjectsWithoutCurrent(page: Page): Promise { + await page.addInitScript(() => { + const mockProjects = [ + { + id: "test-project-1", + name: "Test Project 1", + path: "/mock/test-project-1", + lastOpened: new Date().toISOString(), + }, + { + id: "test-project-2", + name: "Test Project 2", + path: "/mock/test-project-2", + lastOpened: new Date(Date.now() - 86400000).toISOString(), // 1 day ago + }, + ]; + + const mockState = { + state: { + projects: mockProjects, + 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)); + }); +} + +/** + * Check if the project initialization dialog is visible + */ +export async function isProjectInitDialogVisible(page: Page): Promise { + const dialog = page.locator('[data-testid="project-init-dialog"]'); + return await dialog.isVisible(); +} + +/** + * Wait for the project initialization dialog to appear + */ +export async function waitForProjectInitDialog( + page: Page, + options?: { timeout?: number } +): Promise { + return await waitForElement(page, "project-init-dialog", options); +} + +/** + * Close the project initialization dialog + */ +export async function closeProjectInitDialog(page: Page): Promise { + const closeButton = page.locator('[data-testid="close-init-dialog"]'); + await closeButton.click(); +} + +/** + * Check if the project opening overlay is visible + */ +export async function isProjectOpeningOverlayVisible(page: Page): Promise { + const overlay = page.locator('[data-testid="project-opening-overlay"]'); + return await overlay.isVisible(); +} + +/** + * Wait for the project opening overlay to disappear + */ +export async function waitForProjectOpeningOverlayHidden( + page: Page, + options?: { timeout?: number } +): Promise { + await waitForElementHidden(page, "project-opening-overlay", options); +} + +/** + * Click on a recent project in the welcome view + */ +export async function clickRecentProject( + page: Page, + projectId: string +): Promise { + await clickElement(page, `recent-project-${projectId}`); +} + +/** + * Click the open project card in the welcome view + */ +export async function clickOpenProjectCard(page: Page): Promise { + await clickElement(page, "open-project-card"); +} + +/** + * Check if a navigation item exists in the sidebar + */ +export async function isNavItemVisible( + page: Page, + navId: string +): Promise { + const navItem = page.locator(`[data-testid="nav-${navId}"]`); + return await navItem.isVisible().catch(() => false); +} + +/** + * Get all visible navigation items in the sidebar + */ +export async function getVisibleNavItems(page: Page): Promise { + const navItems = page.locator('[data-testid^="nav-"]'); + const count = await navItems.count(); + const items: string[] = []; + + for (let i = 0; i < count; i++) { + const testId = await navItems.nth(i).getAttribute("data-testid"); + if (testId) { + items.push(testId.replace("nav-", "")); + } + } + + return items; +}