Files
automaker/app/tests/utils.ts
Kacper 2c01587180 feat: improve agent output modal with parsed log viewer
Add a parsed log viewer to the agent output modal that groups and colors
log entries by type (tool calls, phases, errors, success messages, etc.)
similar to Coolify logs. Includes:

- New log-parser.ts utility for parsing agent output into structured entries
- New LogViewer component with collapsible entries and colored badges
- Toggle between Parsed and Raw view modes in the modal header
- Type-specific colors (amber for tools, cyan for phases, red for errors)
- Expand/Collapse all buttons for better navigation
- JSON content detection and formatting within entries

Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-09 21:20:25 +01:00

2125 lines
57 KiB
TypeScript

import { Page, Locator, expect } from "@playwright/test";
/**
* Get an element by its data-testid attribute
*/
export async function getByTestId(
page: Page,
testId: string
): Promise<Locator> {
return page.locator(`[data-testid="${testId}"]`);
}
/**
* Set up a mock project in localStorage to bypass the welcome screen
* This simulates having opened a project before
*/
export async function setupMockProject(page: Page): Promise<void> {
await page.addInitScript(() => {
const mockProject = {
id: "test-project-1",
name: "Test Project",
path: "/mock/test-project",
lastOpened: new Date().toISOString(),
};
const mockState = {
state: {
projects: [mockProject],
currentProject: mockProject,
theme: "dark",
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: 3,
},
version: 0,
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
});
}
/**
* Click an element by its data-testid attribute
*/
export async function clickElement(page: Page, testId: string): Promise<void> {
const element = await getByTestId(page, testId);
await element.click();
}
/**
* Wait for an element with a specific data-testid to appear
*/
export async function waitForElement(
page: Page,
testId: string,
options?: { timeout?: number; state?: "attached" | "visible" | "hidden" }
): Promise<Locator> {
const element = page.locator(`[data-testid="${testId}"]`);
await element.waitFor({
timeout: options?.timeout ?? 5000,
state: options?.state ?? "visible",
});
return element;
}
/**
* Wait for an element with a specific data-testid to be hidden
*/
export async function waitForElementHidden(
page: Page,
testId: string,
options?: { timeout?: number }
): Promise<void> {
const element = page.locator(`[data-testid="${testId}"]`);
await element.waitFor({
timeout: options?.timeout ?? 5000,
state: "hidden",
});
}
/**
* Get a button by its text content
*/
export async function getButtonByText(
page: Page,
text: string
): Promise<Locator> {
return page.locator(`button:has-text("${text}")`);
}
/**
* Click a button by its text content
*/
export async function clickButtonByText(
page: Page,
text: string
): Promise<void> {
const button = await getButtonByText(page, text);
await button.click();
}
/**
* Fill an input field by its data-testid attribute
*/
export async function fillInput(
page: Page,
testId: string,
value: string
): Promise<void> {
const input = await getByTestId(page, testId);
await input.fill(value);
}
/**
* Navigate to the board/kanban view
*/
export async function navigateToBoard(page: Page): Promise<void> {
await page.goto("/");
// Wait for the page to load
await page.waitForLoadState("networkidle");
// Check if we're on the board view already
const boardView = page.locator('[data-testid="board-view"]');
const isOnBoard = await boardView.isVisible().catch(() => false);
if (!isOnBoard) {
// Try to click on a recent project first (from welcome screen)
const recentProject = page.locator('p:has-text("Test Project")').first();
if (await recentProject.isVisible().catch(() => false)) {
await recentProject.click();
await page.waitForTimeout(200);
}
// Then click on Kanban Board nav button to ensure we're on the board
const kanbanNav = page.locator('[data-testid="nav-board"]');
if (await kanbanNav.isVisible().catch(() => false)) {
await kanbanNav.click();
}
}
// Wait for the board view to be visible
await waitForElement(page, "board-view", { timeout: 10000 });
}
/**
* Check if the agent output modal is visible
*/
export async function isAgentOutputModalVisible(page: Page): Promise<boolean> {
const modal = page.locator('[data-testid="agent-output-modal"]');
return await modal.isVisible();
}
/**
* Wait for the agent output modal to be visible
*/
export async function waitForAgentOutputModal(
page: Page,
options?: { timeout?: number }
): Promise<Locator> {
return await waitForElement(page, "agent-output-modal", options);
}
/**
* Wait for the agent output modal to be hidden
*/
export async function waitForAgentOutputModalHidden(
page: Page,
options?: { timeout?: number }
): Promise<void> {
await waitForElementHidden(page, "agent-output-modal", options);
}
/**
* Drag a kanban card from one column to another
*/
export async function dragKanbanCard(
page: Page,
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}"]`);
// Perform drag and drop
await dragHandle.dragTo(targetColumn);
}
/**
* Get a kanban card by feature ID
*/
export async function getKanbanCard(
page: Page,
featureId: string
): Promise<Locator> {
return page.locator(`[data-testid="kanban-card-${featureId}"]`);
}
/**
* Click the view output button on a kanban card
*/
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}"]`
);
if (await runningBtn.isVisible()) {
await runningBtn.click();
} else if (await inProgressBtn.isVisible()) {
await inProgressBtn.click();
} else {
throw new Error(`View output button not found for feature ${featureId}`);
}
}
/**
* Perform a drag and drop operation that works with @dnd-kit
* This uses explicit mouse movements with pointer events
*/
export async function dragAndDropWithDndKit(
page: Page,
sourceLocator: Locator,
targetLocator: Locator
): Promise<void> {
const sourceBox = await sourceLocator.boundingBox();
const targetBox = await targetLocator.boundingBox();
if (!sourceBox || !targetBox) {
throw new Error("Could not find source or target element bounds");
}
// Start drag from the center of the source element
const startX = sourceBox.x + sourceBox.width / 2;
const startY = sourceBox.y + sourceBox.height / 2;
// End drag at the center of the target element
const endX = targetBox.x + targetBox.width / 2;
const endY = targetBox.y + targetBox.height / 2;
// Perform the drag and drop with pointer events
await page.mouse.move(startX, startY);
await page.mouse.down();
await page.waitForTimeout(150); // Give dnd-kit time to recognize the drag
await page.mouse.move(endX, endY, { steps: 15 });
await page.waitForTimeout(100); // Allow time for drop detection
await page.mouse.up();
}
/**
* Get the concurrency slider container
*/
export async function getConcurrencySliderContainer(
page: Page
): Promise<Locator> {
return page.locator('[data-testid="concurrency-slider-container"]');
}
/**
* Get the concurrency slider
*/
export async function getConcurrencySlider(page: Page): Promise<Locator> {
return page.locator('[data-testid="concurrency-slider"]');
}
/**
* Get the displayed concurrency value
*/
export async function getConcurrencyValue(page: Page): Promise<string | null> {
const valueElement = page.locator('[data-testid="concurrency-value"]');
return await valueElement.textContent();
}
/**
* Change the concurrency slider value by clicking on the slider track
*/
export async function setConcurrencyValue(
page: Page,
targetValue: number,
min: number = 1,
max: number = 10
): Promise<void> {
const slider = page.locator('[data-testid="concurrency-slider"]');
const sliderBounds = await slider.boundingBox();
if (!sliderBounds) {
throw new Error("Concurrency slider not found or not visible");
}
// Calculate position for target value
const percentage = (targetValue - min) / (max - min);
const targetX = sliderBounds.x + sliderBounds.width * percentage;
const centerY = sliderBounds.y + sliderBounds.height / 2;
// Click at the target position to set the value
await page.mouse.click(targetX, centerY);
}
/**
* Set up a mock project with custom concurrency value
*/
export async function setupMockProjectWithConcurrency(
page: Page,
concurrency: number
): Promise<void> {
await page.addInitScript((maxConcurrency: number) => {
const mockProject = {
id: "test-project-1",
name: "Test Project",
path: "/mock/test-project",
lastOpened: new Date().toISOString(),
};
const mockState = {
state: {
projects: [mockProject],
currentProject: mockProject,
theme: "dark",
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: maxConcurrency,
},
version: 0,
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
}, concurrency);
}
/**
* Navigate to the context view
*/
export async function navigateToContext(page: Page): Promise<void> {
await page.goto("/");
// Wait for the page to load
await page.waitForLoadState("networkidle");
// Click on the Context nav button
const contextNav = page.locator('[data-testid="nav-context"]');
if (await contextNav.isVisible().catch(() => false)) {
await contextNav.click();
}
// Wait for the context view to be visible
await waitForElement(page, "context-view", { timeout: 10000 });
}
/**
* Get the context file list element
*/
export async function getContextFileList(page: Page): Promise<Locator> {
return page.locator('[data-testid="context-file-list"]');
}
/**
* Click on a context file in the list
*/
export async function clickContextFile(
page: Page,
fileName: string
): Promise<void> {
const fileButton = page.locator(`[data-testid="context-file-${fileName}"]`);
await fileButton.click();
}
/**
* Get the context editor element
*/
export async function getContextEditor(page: Page): Promise<Locator> {
return page.locator('[data-testid="context-editor"]');
}
/**
* 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");
}
/**
* Wait for an error toast to appear with specific text
*/
export async function waitForErrorToast(
page: Page,
titleText?: string,
options?: { timeout?: number }
): Promise<Locator> {
// Sonner toasts use data-sonner-toast and data-type="error" for error toasts
const toastSelector = titleText
? `[data-sonner-toast][data-type="error"]:has-text("${titleText}")`
: '[data-sonner-toast][data-type="error"]';
const toast = page.locator(toastSelector).first();
await toast.waitFor({
timeout: options?.timeout ?? 5000,
state: "visible",
});
return toast;
}
/**
* Check if an error toast is visible
*/
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"]';
const toast = page.locator(toastSelector).first();
return await toast.isVisible();
}
/**
* Set up a mock project with specific running tasks to simulate concurrency limit
*/
export async function setupMockProjectAtConcurrencyLimit(
page: Page,
maxConcurrency: number = 1,
runningTasks: string[] = ["running-task-1"]
): Promise<void> {
await page.addInitScript(
({ maxConcurrency, runningTasks }: { maxConcurrency: number; runningTasks: 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,
theme: "dark",
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: maxConcurrency,
isAutoModeRunning: false,
runningAutoTasks: runningTasks,
autoModeActivityLog: [],
},
version: 0,
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
},
{ maxConcurrency, runningTasks }
);
}
/**
* Get the force stop button for a specific feature
*/
export async function getForceStopButton(
page: Page,
featureId: string
): Promise<Locator> {
return page.locator(`[data-testid="force-stop-${featureId}"]`);
}
/**
* Click the force stop button for a specific feature
*/
export async function clickForceStop(
page: Page,
featureId: string
): Promise<void> {
const button = page.locator(`[data-testid="force-stop-${featureId}"]`);
await button.click();
}
/**
* Check if the force stop button is visible for a feature
*/
export async function isForceStopButtonVisible(
page: Page,
featureId: string
): Promise<boolean> {
const button = page.locator(`[data-testid="force-stop-${featureId}"]`);
return await button.isVisible();
}
/**
* Wait for a success toast to appear with specific text
*/
export async function waitForSuccessToast(
page: Page,
titleText?: string,
options?: { timeout?: number }
): Promise<Locator> {
// Sonner toasts use data-sonner-toast and data-type="success" for success toasts
const toastSelector = titleText
? `[data-sonner-toast][data-type="success"]:has-text("${titleText}")`
: '[data-sonner-toast][data-type="success"]';
const toast = page.locator(toastSelector).first();
await toast.waitFor({
timeout: options?.timeout ?? 5000,
state: "visible",
});
return toast;
}
/**
* Get the delete button for an in_progress feature
*/
export async function getDeleteInProgressButton(
page: Page,
featureId: string
): Promise<Locator> {
return page.locator(`[data-testid="delete-inprogress-feature-${featureId}"]`);
}
/**
* Click the delete button for an in_progress feature
*/
export async function clickDeleteInProgressFeature(
page: Page,
featureId: string
): Promise<void> {
const button = page.locator(
`[data-testid="delete-inprogress-feature-${featureId}"]`
);
await button.click();
}
/**
* Check if the delete button is visible for an in_progress feature
*/
export async function isDeleteInProgressButtonVisible(
page: Page,
featureId: string
): Promise<boolean> {
const button = page.locator(
`[data-testid="delete-inprogress-feature-${featureId}"]`
);
return await button.isVisible();
}
/**
* Set up a mock project with features in different states
*/
export async function setupMockProjectWithFeatures(
page: Page,
options?: {
maxConcurrency?: number;
runningTasks?: string[];
features?: Array<{
id: string;
category: string;
description: string;
status: "backlog" | "in_progress" | "verified";
steps?: string[];
}>;
}
): Promise<void> {
await page.addInitScript(
(opts: typeof options) => {
const mockProject = {
id: "test-project-1",
name: "Test Project",
path: "/mock/test-project",
lastOpened: new Date().toISOString(),
};
const mockFeatures = opts?.features || [];
const mockState = {
state: {
projects: [mockProject],
currentProject: mockProject,
theme: "dark",
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: opts?.maxConcurrency ?? 3,
isAutoModeRunning: false,
runningAutoTasks: opts?.runningTasks ?? [],
autoModeActivityLog: [],
features: mockFeatures,
},
version: 0,
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
// Also store features in a global variable that the mock electron API can use
// This is needed because the board-view loads features from the file system
(window as any).__mockFeatures = mockFeatures;
},
options
);
}
/**
* Set up a mock project with a feature context file
* This simulates an agent having created context for a feature
*/
export async function setupMockProjectWithContextFile(
page: Page,
featureId: string,
contextContent: string = "# Agent Context\n\nPrevious implementation work..."
): Promise<void> {
await page.addInitScript(
({ featureId, contextContent }: { featureId: string; contextContent: 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,
theme: "dark",
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: 3,
},
version: 0,
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
// Set up mock file system with a context file for the feature
// This will be used by the mock electron API
(window as any).__mockContextFile = {
featureId,
path: `/mock/test-project/.automaker/agents-context/${featureId}.md`,
content: contextContent,
};
},
{ featureId, contextContent }
);
}
/**
* Get the category autocomplete input element
*/
export async function getCategoryAutocompleteInput(
page: Page,
testId: string = "feature-category-input"
): Promise<Locator> {
return page.locator(`[data-testid="${testId}"]`);
}
/**
* Get the category autocomplete dropdown list
*/
export async function getCategoryAutocompleteList(page: Page): Promise<Locator> {
return page.locator('[data-testid="category-autocomplete-list"]');
}
/**
* Check if the category autocomplete dropdown is visible
*/
export async function isCategoryAutocompleteListVisible(page: Page): Promise<boolean> {
const list = page.locator('[data-testid="category-autocomplete-list"]');
return await list.isVisible();
}
/**
* Wait for the category autocomplete dropdown to be visible
*/
export async function waitForCategoryAutocompleteList(
page: Page,
options?: { timeout?: number }
): Promise<Locator> {
return await waitForElement(page, "category-autocomplete-list", options);
}
/**
* Wait for the category autocomplete dropdown to be hidden
*/
export async function waitForCategoryAutocompleteListHidden(
page: Page,
options?: { timeout?: number }
): Promise<void> {
await waitForElementHidden(page, "category-autocomplete-list", options);
}
/**
* Click a category option in the autocomplete dropdown
*/
export async function clickCategoryOption(
page: Page,
categoryName: string
): Promise<void> {
const optionTestId = `category-option-${categoryName.toLowerCase().replace(/\s+/g, "-")}`;
const option = page.locator(`[data-testid="${optionTestId}"]`);
await option.click();
}
/**
* Get a category option element by name
*/
export async function getCategoryOption(
page: Page,
categoryName: string
): Promise<Locator> {
const optionTestId = `category-option-${categoryName.toLowerCase().replace(/\s+/g, "-")}`;
return page.locator(`[data-testid="${optionTestId}"]`);
}
/**
* Navigate to the agent view
*/
export async function navigateToAgent(page: Page): Promise<void> {
await page.goto("/");
// Wait for the page to load
await page.waitForLoadState("networkidle");
// Click on the Agent nav button
const agentNav = page.locator('[data-testid="nav-agent"]');
if (await agentNav.isVisible().catch(() => false)) {
await agentNav.click();
}
// Wait for the agent view to be visible
await waitForElement(page, "agent-view", { timeout: 10000 });
}
/**
* Get the session list element
*/
export async function getSessionList(page: Page): Promise<Locator> {
return page.locator('[data-testid="session-list"]');
}
/**
* Get the new session button
*/
export async function getNewSessionButton(page: Page): Promise<Locator> {
return page.locator('[data-testid="new-session-button"]');
}
/**
* Click the new session button
*/
export async function clickNewSessionButton(page: Page): Promise<void> {
const button = await getNewSessionButton(page);
await button.click();
}
/**
* Get a session item by its ID
*/
export async function getSessionItem(
page: Page,
sessionId: string
): Promise<Locator> {
return page.locator(`[data-testid="session-item-${sessionId}"]`);
}
/**
* Click the archive button for a session
*/
export async function clickArchiveSession(
page: Page,
sessionId: string
): Promise<void> {
const button = page.locator(`[data-testid="archive-session-${sessionId}"]`);
await button.click();
}
/**
* Check if the no session placeholder is visible
*/
export async function isNoSessionPlaceholderVisible(page: Page): Promise<boolean> {
const placeholder = page.locator('[data-testid="no-session-placeholder"]');
return await placeholder.isVisible();
}
/**
* Wait for the no session placeholder to be visible
*/
export async function waitForNoSessionPlaceholder(
page: Page,
options?: { timeout?: number }
): Promise<Locator> {
return await waitForElement(page, "no-session-placeholder", options);
}
/**
* Check if the message list is visible (indicates a session is selected)
*/
export async function isMessageListVisible(page: Page): Promise<boolean> {
const messageList = page.locator('[data-testid="message-list"]');
return await messageList.isVisible();
}
/**
* Get the count up timer element for a specific feature card
*/
export async function getTimerForFeature(
page: Page,
featureId: string
): Promise<Locator> {
const card = page.locator(`[data-testid="kanban-card-${featureId}"]`);
return card.locator('[data-testid="count-up-timer"]');
}
/**
* Get the timer display text for a specific feature card
*/
export async function getTimerDisplayForFeature(
page: Page,
featureId: string
): Promise<string | null> {
const card = page.locator(`[data-testid="kanban-card-${featureId}"]`);
const timerDisplay = card.locator('[data-testid="timer-display"]');
return await timerDisplay.textContent();
}
/**
* Check if a timer is visible for a specific feature
*/
export async function isTimerVisibleForFeature(
page: Page,
featureId: string
): Promise<boolean> {
const card = page.locator(`[data-testid="kanban-card-${featureId}"]`);
const timer = card.locator('[data-testid="count-up-timer"]');
return await timer.isVisible().catch(() => false);
}
/**
* Set up a mock project with features that have startedAt timestamps
*/
export async function setupMockProjectWithInProgressFeatures(
page: Page,
options?: {
maxConcurrency?: number;
runningTasks?: string[];
features?: Array<{
id: string;
category: string;
description: string;
status: "backlog" | "in_progress" | "verified";
steps?: string[];
startedAt?: string;
}>;
}
): Promise<void> {
await page.addInitScript(
(opts: typeof options) => {
const mockProject = {
id: "test-project-1",
name: "Test Project",
path: "/mock/test-project",
lastOpened: new Date().toISOString(),
};
const mockFeatures = opts?.features || [];
const mockState = {
state: {
projects: [mockProject],
currentProject: mockProject,
theme: "dark",
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: opts?.maxConcurrency ?? 3,
isAutoModeRunning: false,
runningAutoTasks: opts?.runningTasks ?? [],
autoModeActivityLog: [],
features: mockFeatures,
},
version: 0,
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
// Also store features in a global variable that the mock electron API can use
// This is needed because the board-view loads features from the file system
(window as any).__mockFeatures = mockFeatures;
},
options
);
}
/**
* Navigate to the spec view
*/
export async function navigateToSpec(page: Page): Promise<void> {
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<Locator> {
return page.locator('[data-testid="spec-editor"]');
}
/**
* Get the spec editor content
*/
export async function getSpecEditorContent(page: Page): Promise<string> {
const editor = await getSpecEditor(page);
return await editor.inputValue();
}
/**
* Set the spec editor content
*/
export async function setSpecEditorContent(
page: Page,
content: string
): Promise<void> {
const editor = await getSpecEditor(page);
await editor.fill(content);
}
/**
* Click the save spec button
*/
export async function clickSaveSpec(page: Page): Promise<void> {
await clickElement(page, "save-spec");
}
/**
* Click the reload spec button
*/
export async function clickReloadSpec(page: Page): Promise<void> {
await clickElement(page, "reload-spec");
}
/**
* Check if the spec view path display shows the correct .automaker path
*/
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();
return await pathElement.textContent();
}
/**
* Get a kanban column by its ID
*/
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> {
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<boolean> {
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<void> {
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<void> {
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<string | null> {
// 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<boolean> {
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<Locator> {
return page.locator(`[data-testid="drag-handle-${featureId}"]`);
}
/**
* 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 });
}
/**
* Set up an empty localStorage (no projects) to show welcome screen
*/
export async function setupEmptyLocalStorage(page: Page): Promise<void> {
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<void> {
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<boolean> {
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<Locator> {
return await waitForElement(page, "project-init-dialog", options);
}
/**
* Close the project initialization dialog
*/
export async function closeProjectInitDialog(page: Page): Promise<void> {
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<boolean> {
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<void> {
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<void> {
await clickElement(page, `recent-project-${projectId}`);
}
/**
* Click the open project card in the welcome view
*/
export async function clickOpenProjectCard(page: Page): Promise<void> {
await clickElement(page, "open-project-card");
}
/**
* Check if a navigation item exists in the sidebar
*/
export async function isNavItemVisible(
page: Page,
navId: string
): Promise<boolean> {
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<string[]> {
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;
}
/**
* Press a keyboard shortcut key
*/
export async function pressShortcut(page: Page, key: string): Promise<void> {
await page.keyboard.press(key);
}
/**
* Count the number of session items in the session list
*/
export async function countSessionItems(page: Page): Promise<number> {
const sessionList = page.locator('[data-testid="session-list"] [data-testid^="session-item-"]');
return await sessionList.count();
}
/**
* Wait for a new session to be created (by checking if a session item appears)
*/
export async function waitForNewSession(
page: Page,
options?: { timeout?: number }
): Promise<void> {
// Wait for any session item to appear
const sessionItem = page.locator('[data-testid^="session-item-"]').first();
await sessionItem.waitFor({
timeout: options?.timeout ?? 5000,
state: "visible",
});
}
/**
* Check if a shortcut key indicator is visible for a navigation item
*/
export async function isShortcutIndicatorVisible(
page: Page,
navId: string
): Promise<boolean> {
const shortcut = page.locator(`[data-testid="shortcut-${navId}"]`);
return await shortcut.isVisible().catch(() => false);
}
/**
* Get the shortcut key text for a navigation item
*/
export async function getShortcutKeyText(
page: Page,
navId: string
): Promise<string | null> {
const shortcut = page.locator(`[data-testid="shortcut-${navId}"]`);
return await shortcut.textContent();
}
/**
* Focus on an input element to test that shortcuts don't fire when typing
*/
export async function focusOnInput(page: Page, testId: string): Promise<void> {
const input = page.locator(`[data-testid="${testId}"]`);
await input.focus();
}
/**
* Check if the add feature dialog is visible
*/
export async function isAddFeatureDialogVisible(page: Page): Promise<boolean> {
const dialog = page.locator('[data-testid="add-feature-dialog"]');
return await dialog.isVisible().catch(() => false);
}
/**
* Check if the add context file dialog is visible
*/
export async function isAddContextDialogVisible(page: Page): Promise<boolean> {
const dialog = page.locator('[data-testid="add-context-dialog"]');
return await dialog.isVisible().catch(() => false);
}
/**
* Close any open dialog by pressing Escape
*/
export async function closeDialogWithEscape(page: Page): Promise<void> {
await page.keyboard.press("Escape");
await page.waitForTimeout(100); // Give dialog time to close
}
/**
* Wait for a toast notification with specific text to appear
*/
export async function waitForToast(
page: Page,
text: string,
options?: { timeout?: number }
): Promise<Locator> {
const toast = page.locator(`[data-sonner-toast]:has-text("${text}")`).first();
await toast.waitFor({
timeout: options?.timeout ?? 5000,
state: "visible",
});
return toast;
}
/**
* Check if project analysis is in progress (analyzing spinner is visible)
*/
export async function isProjectAnalyzingVisible(page: Page): Promise<boolean> {
const analyzingText = page.locator('p:has-text("AI agent is analyzing")');
return await analyzingText.isVisible().catch(() => false);
}
/**
* Wait for project analysis to complete (no longer analyzing)
*/
export async function waitForProjectAnalysisComplete(
page: Page,
options?: { timeout?: number }
): Promise<void> {
// Wait for the analyzing text to disappear
const analyzingText = page.locator('p:has-text("AI agent is analyzing")');
await analyzingText.waitFor({
timeout: options?.timeout ?? 10000,
state: "hidden",
}).catch(() => {
// It may never have been visible, that's ok
});
}
/**
* Get the delete confirmation dialog
*/
export async function getDeleteConfirmationDialog(page: Page): Promise<Locator> {
return page.locator('[data-testid="delete-confirmation-dialog"]');
}
/**
* Check if the delete confirmation dialog is visible
*/
export async function isDeleteConfirmationDialogVisible(page: Page): Promise<boolean> {
const dialog = page.locator('[data-testid="delete-confirmation-dialog"]');
return await dialog.isVisible().catch(() => false);
}
/**
* Wait for the delete confirmation dialog to appear
*/
export async function waitForDeleteConfirmationDialog(
page: Page,
options?: { timeout?: number }
): Promise<Locator> {
return await waitForElement(page, "delete-confirmation-dialog", options);
}
/**
* Wait for the delete confirmation dialog to be hidden
*/
export async function waitForDeleteConfirmationDialogHidden(
page: Page,
options?: { timeout?: number }
): Promise<void> {
await waitForElementHidden(page, "delete-confirmation-dialog", options);
}
/**
* Click the confirm delete button in the delete confirmation dialog
*/
export async function clickConfirmDeleteButton(page: Page): Promise<void> {
await clickElement(page, "confirm-delete-button");
}
/**
* Click the cancel delete button in the delete confirmation dialog
*/
export async function clickCancelDeleteButton(page: Page): Promise<void> {
await clickElement(page, "cancel-delete-button");
}
/**
* Click the delete button for a backlog feature card
*/
export async function clickDeleteFeatureButton(
page: Page,
featureId: string
): Promise<void> {
const button = page.locator(`[data-testid="delete-feature-${featureId}"]`);
await button.click();
}
/**
* Check if the delete button is visible for a backlog feature
*/
export async function isDeleteFeatureButtonVisible(
page: Page,
featureId: string
): Promise<boolean> {
const button = page.locator(`[data-testid="delete-feature-${featureId}"]`);
return await button.isVisible().catch(() => false);
}
/**
* Check if the edit feature dialog is visible
*/
export async function isEditFeatureDialogVisible(page: Page): Promise<boolean> {
const dialog = page.locator('[data-testid="edit-feature-dialog"]');
return await dialog.isVisible().catch(() => false);
}
/**
* Wait for the edit feature dialog to be visible
*/
export async function waitForEditFeatureDialog(
page: Page,
options?: { timeout?: number }
): Promise<Locator> {
return await waitForElement(page, "edit-feature-dialog", options);
}
/**
* Get the edit feature description input/textarea element
*/
export async function getEditFeatureDescriptionInput(page: Page): Promise<Locator> {
return page.locator('[data-testid="edit-feature-description"]');
}
/**
* Check if the edit feature description field is a textarea
*/
export async function isEditFeatureDescriptionTextarea(page: Page): Promise<boolean> {
const element = page.locator('[data-testid="edit-feature-description"]');
const tagName = await element.evaluate((el) => el.tagName.toLowerCase());
return tagName === "textarea";
}
/**
* Open the edit dialog for a specific feature
*/
export async function openEditFeatureDialog(
page: Page,
featureId: string
): Promise<void> {
await clickElement(page, `edit-feature-${featureId}`);
await waitForEditFeatureDialog(page);
}
/**
* Fill the edit feature description field
*/
export async function fillEditFeatureDescription(
page: Page,
value: string
): Promise<void> {
const input = await getEditFeatureDescriptionInput(page);
await input.fill(value);
}
/**
* Click the confirm edit feature button
*/
export async function confirmEditFeature(page: Page): Promise<void> {
await clickElement(page, "confirm-edit-feature");
}
/**
* Get the skip tests checkbox element in the add feature dialog
*/
export async function getSkipTestsCheckbox(page: Page): Promise<Locator> {
return page.locator('[data-testid="skip-tests-checkbox"]');
}
/**
* Toggle the skip tests checkbox in the add feature dialog
*/
export async function toggleSkipTestsCheckbox(page: Page): Promise<void> {
const checkbox = page.locator('[data-testid="skip-tests-checkbox"]');
await checkbox.click();
}
/**
* Check if the skip tests checkbox is checked in the add feature dialog
*/
export async function isSkipTestsChecked(page: Page): Promise<boolean> {
const checkbox = page.locator('[data-testid="skip-tests-checkbox"]');
const state = await checkbox.getAttribute("data-state");
return state === "checked";
}
/**
* Get the edit skip tests checkbox element in the edit feature dialog
*/
export async function getEditSkipTestsCheckbox(page: Page): Promise<Locator> {
return page.locator('[data-testid="edit-skip-tests-checkbox"]');
}
/**
* Toggle the skip tests checkbox in the edit feature dialog
*/
export async function toggleEditSkipTestsCheckbox(page: Page): Promise<void> {
const checkbox = page.locator('[data-testid="edit-skip-tests-checkbox"]');
await checkbox.click();
}
/**
* Check if the skip tests checkbox is checked in the edit feature dialog
*/
export async function isEditSkipTestsChecked(page: Page): Promise<boolean> {
const checkbox = page.locator('[data-testid="edit-skip-tests-checkbox"]');
const state = await checkbox.getAttribute("data-state");
return state === "checked";
}
/**
* Check if the skip tests badge is visible on a kanban card
*/
export async function isSkipTestsBadgeVisible(
page: Page,
featureId: string
): Promise<boolean> {
const badge = page.locator(`[data-testid="skip-tests-badge-${featureId}"]`);
return await badge.isVisible().catch(() => false);
}
/**
* Get the skip tests badge element for a kanban card
*/
export async function getSkipTestsBadge(
page: Page,
featureId: string
): Promise<Locator> {
return page.locator(`[data-testid="skip-tests-badge-${featureId}"]`);
}
/**
* Click the manual verify button for a skipTests feature
*/
export async function clickManualVerify(
page: Page,
featureId: string
): Promise<void> {
const button = page.locator(`[data-testid="manual-verify-${featureId}"]`);
await button.click();
}
/**
* Check if the manual verify button is visible for a feature
*/
export async function isManualVerifyButtonVisible(
page: Page,
featureId: string
): Promise<boolean> {
const button = page.locator(`[data-testid="manual-verify-${featureId}"]`);
return await button.isVisible().catch(() => false);
}
/**
* Click the move back button for a verified skipTests feature
*/
export async function clickMoveBack(
page: Page,
featureId: string
): Promise<void> {
const button = page.locator(`[data-testid="move-back-${featureId}"]`);
await button.click();
}
/**
* Check if the move back button is visible for a feature
*/
export async function isMoveBackButtonVisible(
page: Page,
featureId: string
): Promise<boolean> {
const button = page.locator(`[data-testid="move-back-${featureId}"]`);
return await button.isVisible().catch(() => false);
}
/**
* Set up a mock project with features that have skipTests enabled
*/
export async function setupMockProjectWithSkipTestsFeatures(
page: Page,
options?: {
maxConcurrency?: number;
runningTasks?: string[];
features?: Array<{
id: string;
category: string;
description: string;
status: "backlog" | "in_progress" | "verified";
steps?: string[];
startedAt?: string;
skipTests?: boolean;
}>;
}
): Promise<void> {
await page.addInitScript(
(opts: typeof options) => {
const mockProject = {
id: "test-project-1",
name: "Test Project",
path: "/mock/test-project",
lastOpened: new Date().toISOString(),
};
const mockFeatures = opts?.features || [];
const mockState = {
state: {
projects: [mockProject],
currentProject: mockProject,
theme: "dark",
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: opts?.maxConcurrency ?? 3,
isAutoModeRunning: false,
runningAutoTasks: opts?.runningTasks ?? [],
autoModeActivityLog: [],
features: mockFeatures,
},
version: 0,
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
},
options
);
}
/**
* Press a number key (0-9) on the keyboard
*/
export async function pressNumberKey(page: Page, num: number): Promise<void> {
await page.keyboard.press(num.toString());
}
/**
* Get the modal title/description text to verify which feature's output is being shown
*/
export async function getAgentOutputModalDescription(page: Page): Promise<string | null> {
const modal = page.locator('[data-testid="agent-output-modal"]');
const description = modal.locator('[id="radix-\\:r.+\\:-description"]').first();
return await description.textContent().catch(() => null);
}
/**
* Check the dialog description content in the agent output modal
*/
export async function getOutputModalDescription(page: Page): Promise<string | null> {
const modalDescription = page.locator('[data-testid="agent-output-modal"] [data-slot="dialog-description"]');
return await modalDescription.textContent().catch(() => null);
}
/**
* Check if the project picker dropdown is open
*/
export async function isProjectPickerDropdownOpen(page: Page): Promise<boolean> {
const dropdown = page.locator('[data-testid="project-picker-dropdown"]');
return await dropdown.isVisible().catch(() => false);
}
/**
* Wait for the project picker dropdown to be visible
*/
export async function waitForProjectPickerDropdown(
page: Page,
options?: { timeout?: number }
): Promise<Locator> {
return await waitForElement(page, "project-picker-dropdown", options);
}
/**
* Wait for the project picker dropdown to be hidden
*/
export async function waitForProjectPickerDropdownHidden(
page: Page,
options?: { timeout?: number }
): Promise<void> {
await waitForElementHidden(page, "project-picker-dropdown", options);
}
/**
* Get a project hotkey indicator element by number (1-5)
*/
export async function getProjectHotkey(page: Page, num: number): Promise<Locator> {
return page.locator(`[data-testid="project-hotkey-${num}"]`);
}
/**
* Check if a project hotkey indicator is visible
*/
export async function isProjectHotkeyVisible(page: Page, num: number): Promise<boolean> {
const hotkey = page.locator(`[data-testid="project-hotkey-${num}"]`);
return await hotkey.isVisible().catch(() => false);
}
/**
* Get the project picker shortcut indicator (P key)
*/
export async function getProjectPickerShortcut(page: Page): Promise<Locator> {
return page.locator('[data-testid="project-picker-shortcut"]');
}
/**
* Set up a mock state with multiple projects
*/
export async function setupMockMultipleProjects(
page: Page,
projectCount: number = 3
): Promise<void> {
await page.addInitScript((count: number) => {
const mockProjects = [];
for (let i = 0; i < count; i++) {
mockProjects.push({
id: `test-project-${i + 1}`,
name: `Test Project ${i + 1}`,
path: `/mock/test-project-${i + 1}`,
lastOpened: new Date(Date.now() - i * 86400000).toISOString(),
});
}
const mockState = {
state: {
projects: mockProjects,
currentProject: mockProjects[0],
currentView: "board",
theme: "dark",
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: 3,
},
version: 0,
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
}, projectCount);
}
/**
* Get the description image dropzone element
*/
export async function getDescriptionImageDropzone(page: Page): Promise<Locator> {
return page.locator('[data-testid="feature-description-input"]');
}
/**
* Get the description image hidden input element
*/
export async function getDescriptionImageInput(page: Page): Promise<Locator> {
return page.locator('[data-testid="description-image-input"]');
}
/**
* Check if the description image previews section is visible
*/
export async function isDescriptionImagePreviewsVisible(page: Page): Promise<boolean> {
const previews = page.locator('[data-testid="description-image-previews"]');
return await previews.isVisible().catch(() => false);
}
/**
* Get the number of description image previews
*/
export async function getDescriptionImagePreviewCount(page: Page): Promise<number> {
const previews = page.locator('[data-testid^="description-image-preview-"]');
return await previews.count();
}
/**
* Upload an image to the description dropzone via the file input
*/
export async function uploadDescriptionImage(
page: Page,
imagePath: string
): Promise<void> {
const input = page.locator('[data-testid="description-image-input"]');
await input.setInputFiles(imagePath);
}
/**
* Create a test PNG image as a data URL
*/
export function createTestImageDataUrl(): string {
// A tiny 1x1 transparent PNG as base64
return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
}
/**
* Wait for description image preview to appear
*/
export async function waitForDescriptionImagePreview(
page: Page,
options?: { timeout?: number }
): Promise<Locator> {
const preview = page.locator('[data-testid^="description-image-preview-"]').first();
await preview.waitFor({
timeout: options?.timeout ?? 5000,
state: "visible",
});
return preview;
}
/**
* Check if the drop overlay is visible on the description area
*/
export async function isDropOverlayVisible(page: Page): Promise<boolean> {
const overlay = page.locator('[data-testid="drop-overlay"]');
return await overlay.isVisible().catch(() => false);
}
/**
* Navigate to the settings view
*/
export async function navigateToSettings(page: Page): Promise<void> {
await page.goto("/");
// Wait for the page to load
await page.waitForLoadState("networkidle");
// Click on the Settings button in the sidebar
const settingsButton = page.locator('[data-testid="settings-button"]');
if (await settingsButton.isVisible().catch(() => false)) {
await settingsButton.click();
}
// Wait for the settings view to be visible
await waitForElement(page, "settings-view", { timeout: 10000 });
}
/**
* Get the settings view scrollable content area
*/
export async function getSettingsContentArea(page: Page): Promise<Locator> {
return page.locator('[data-testid="settings-view"] .overflow-y-auto');
}
/**
* Check if an element is scrollable (has scrollable content)
*/
export async function isElementScrollable(locator: Locator): Promise<boolean> {
const scrollInfo = await locator.evaluate((el) => {
return {
scrollHeight: el.scrollHeight,
clientHeight: el.clientHeight,
isScrollable: el.scrollHeight > el.clientHeight,
};
});
return scrollInfo.isScrollable;
}
/**
* Scroll an element to the bottom
*/
export async function scrollToBottom(locator: Locator): Promise<void> {
await locator.evaluate((el) => {
el.scrollTop = el.scrollHeight;
});
}
/**
* Get the scroll position of an element
*/
export async function getScrollPosition(locator: Locator): Promise<{ scrollTop: number; scrollHeight: number; clientHeight: number }> {
return await locator.evaluate((el) => ({
scrollTop: el.scrollTop,
scrollHeight: el.scrollHeight,
clientHeight: el.clientHeight,
}));
}
/**
* Check if an element is visible within a scrollable container
*/
export async function isElementVisibleInScrollContainer(
element: Locator,
container: Locator
): Promise<boolean> {
const elementBox = await element.boundingBox();
const containerBox = await container.boundingBox();
if (!elementBox || !containerBox) {
return false;
}
// Check if element is within the visible area of the container
return (
elementBox.y >= containerBox.y &&
elementBox.y + elementBox.height <= containerBox.y + containerBox.height
);
}
// ============ Log Viewer Utilities ============
/**
* Get the log viewer header element (contains type counts and expand/collapse buttons)
*/
export async function getLogViewerHeader(page: Page): Promise<Locator> {
return page.locator('[data-testid="log-viewer-header"]');
}
/**
* Check if the log viewer header is visible
*/
export async function isLogViewerHeaderVisible(page: Page): Promise<boolean> {
const header = page.locator('[data-testid="log-viewer-header"]');
return await header.isVisible().catch(() => false);
}
/**
* Get the log entries container element
*/
export async function getLogEntriesContainer(page: Page): Promise<Locator> {
return page.locator('[data-testid="log-entries-container"]');
}
/**
* Get a log entry by its type
*/
export async function getLogEntryByType(
page: Page,
type: string
): Promise<Locator> {
return page.locator(`[data-testid="log-entry-${type}"]`).first();
}
/**
* Get all log entries of a specific type
*/
export async function getAllLogEntriesByType(
page: Page,
type: string
): Promise<Locator> {
return page.locator(`[data-testid="log-entry-${type}"]`);
}
/**
* Count log entries of a specific type
*/
export async function countLogEntriesByType(
page: Page,
type: string
): Promise<number> {
const entries = page.locator(`[data-testid="log-entry-${type}"]`);
return await entries.count();
}
/**
* Get the log type count badge by type
*/
export async function getLogTypeCountBadge(
page: Page,
type: string
): Promise<Locator> {
return page.locator(`[data-testid="log-type-count-${type}"]`);
}
/**
* Check if a log type count badge is visible
*/
export async function isLogTypeCountBadgeVisible(
page: Page,
type: string
): Promise<boolean> {
const badge = page.locator(`[data-testid="log-type-count-${type}"]`);
return await badge.isVisible().catch(() => false);
}
/**
* Click the expand all button in the log viewer
*/
export async function clickLogExpandAll(page: Page): Promise<void> {
await clickElement(page, "log-expand-all");
}
/**
* Click the collapse all button in the log viewer
*/
export async function clickLogCollapseAll(page: Page): Promise<void> {
await clickElement(page, "log-collapse-all");
}
/**
* Get a log entry badge element
*/
export async function getLogEntryBadge(page: Page): Promise<Locator> {
return page.locator('[data-testid="log-entry-badge"]').first();
}
/**
* Check if any log entry badge is visible
*/
export async function isLogEntryBadgeVisible(page: Page): Promise<boolean> {
const badge = page.locator('[data-testid="log-entry-badge"]').first();
return await badge.isVisible().catch(() => false);
}
/**
* Get the view mode toggle button (parsed/raw)
*/
export async function getViewModeButton(
page: Page,
mode: "parsed" | "raw"
): Promise<Locator> {
return page.locator(`[data-testid="view-mode-${mode}"]`);
}
/**
* Click a view mode toggle button
*/
export async function clickViewModeButton(
page: Page,
mode: "parsed" | "raw"
): Promise<void> {
await clickElement(page, `view-mode-${mode}`);
}
/**
* Check if a view mode button is active (selected)
*/
export async function isViewModeActive(
page: Page,
mode: "parsed" | "raw"
): Promise<boolean> {
const button = page.locator(`[data-testid="view-mode-${mode}"]`);
const classes = await button.getAttribute("class");
return classes?.includes("text-purple-300") ?? false;
}
/**
* Set up a mock project with agent output content in the context file
*/
export async function setupMockProjectWithAgentOutput(
page: Page,
featureId: string,
outputContent: string
): Promise<void> {
await page.addInitScript(
({ featureId, outputContent }: { featureId: string; outputContent: 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,
theme: "dark",
sidebarOpen: true,
apiKeys: { anthropic: "", google: "" },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: 3,
},
version: 0,
};
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
// Set up mock file system with output content for the feature
(window as any).__mockContextFile = {
featureId,
path: `/mock/test-project/.automaker/agents-context/${featureId}.md`,
content: outputContent,
};
},
{ featureId, outputContent }
);
}