mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 20:23:36 +00:00
style: fix formatting with Prettier
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,8 +3,8 @@
|
||||
* Provides type-safe wrappers around common API operations
|
||||
*/
|
||||
|
||||
import { Page, APIResponse } from "@playwright/test";
|
||||
import { API_ENDPOINTS } from "../core/constants";
|
||||
import { Page, APIResponse } from '@playwright/test';
|
||||
import { API_ENDPOINTS } from '../core/constants';
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { Page, Locator } from "@playwright/test";
|
||||
import { waitForElement, waitForElementHidden } from "../core/waiting";
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
import { waitForElement, waitForElementHidden } from '../core/waiting';
|
||||
|
||||
/**
|
||||
* Check if the category autocomplete dropdown is visible
|
||||
*/
|
||||
export async function isCategoryAutocompleteListVisible(
|
||||
page: Page
|
||||
): Promise<boolean> {
|
||||
export async function isCategoryAutocompleteListVisible(page: Page): Promise<boolean> {
|
||||
const list = page.locator('[data-testid="category-autocomplete-list"]');
|
||||
return await list.isVisible();
|
||||
}
|
||||
@@ -18,7 +16,7 @@ export async function waitForCategoryAutocompleteList(
|
||||
page: Page,
|
||||
options?: { timeout?: number }
|
||||
): Promise<Locator> {
|
||||
return await waitForElement(page, "category-autocomplete-list", options);
|
||||
return await waitForElement(page, 'category-autocomplete-list', options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,19 +26,14 @@ export async function waitForCategoryAutocompleteListHidden(
|
||||
page: Page,
|
||||
options?: { timeout?: number }
|
||||
): Promise<void> {
|
||||
await waitForElementHidden(page, "category-autocomplete-list", options);
|
||||
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, "-")}`;
|
||||
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();
|
||||
}
|
||||
@@ -48,22 +41,15 @@ export async function clickCategoryOption(
|
||||
/**
|
||||
* 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, "-")}`;
|
||||
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}"]`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Click the "Create new" option for a category that doesn't exist
|
||||
*/
|
||||
export async function clickCreateNewCategoryOption(
|
||||
page: Page
|
||||
): Promise<void> {
|
||||
export async function clickCreateNewCategoryOption(page: Page): Promise<void> {
|
||||
const option = page.locator('[data-testid="category-option-create-new"]');
|
||||
await option.click();
|
||||
}
|
||||
@@ -71,8 +57,6 @@ export async function clickCreateNewCategoryOption(
|
||||
/**
|
||||
* Get the "Create new" option element for categories
|
||||
*/
|
||||
export async function getCreateNewCategoryOption(
|
||||
page: Page
|
||||
): Promise<Locator> {
|
||||
export async function getCreateNewCategoryOption(page: Page): Promise<Locator> {
|
||||
return page.locator('[data-testid="category-option-create-new"]');
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Page, Locator } from "@playwright/test";
|
||||
import { clickElement } from "../core/interactions";
|
||||
import { waitForElement, waitForElementHidden } from "../core/waiting";
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
import { clickElement } from '../core/interactions';
|
||||
import { waitForElement, waitForElementHidden } from '../core/waiting';
|
||||
|
||||
/**
|
||||
* Check if the add feature dialog is visible
|
||||
@@ -33,36 +33,29 @@ export async function waitForEditFeatureDialog(
|
||||
page: Page,
|
||||
options?: { timeout?: number }
|
||||
): Promise<Locator> {
|
||||
return await waitForElement(page, "edit-feature-dialog", options);
|
||||
return await waitForElement(page, 'edit-feature-dialog', options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the edit feature description input/textarea element
|
||||
*/
|
||||
export async function getEditFeatureDescriptionInput(
|
||||
page: Page
|
||||
): Promise<Locator> {
|
||||
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> {
|
||||
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";
|
||||
return tagName === 'textarea';
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the edit dialog for a specific feature
|
||||
*/
|
||||
export async function openEditFeatureDialog(
|
||||
page: Page,
|
||||
featureId: string
|
||||
): Promise<void> {
|
||||
export async function openEditFeatureDialog(page: Page, featureId: string): Promise<void> {
|
||||
await clickElement(page, `edit-feature-${featureId}`);
|
||||
await waitForEditFeatureDialog(page);
|
||||
}
|
||||
@@ -70,10 +63,7 @@ export async function openEditFeatureDialog(
|
||||
/**
|
||||
* Fill the edit feature description field
|
||||
*/
|
||||
export async function fillEditFeatureDescription(
|
||||
page: Page,
|
||||
value: string
|
||||
): Promise<void> {
|
||||
export async function fillEditFeatureDescription(page: Page, value: string): Promise<void> {
|
||||
const input = await getEditFeatureDescriptionInput(page);
|
||||
await input.fill(value);
|
||||
}
|
||||
@@ -82,24 +72,20 @@ export async function fillEditFeatureDescription(
|
||||
* Click the confirm edit feature button
|
||||
*/
|
||||
export async function confirmEditFeature(page: Page): Promise<void> {
|
||||
await clickElement(page, "confirm-edit-feature");
|
||||
await clickElement(page, 'confirm-edit-feature');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the delete confirmation dialog
|
||||
*/
|
||||
export async function getDeleteConfirmationDialog(
|
||||
page: Page
|
||||
): Promise<Locator> {
|
||||
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> {
|
||||
export async function isDeleteConfirmationDialogVisible(page: Page): Promise<boolean> {
|
||||
const dialog = page.locator('[data-testid="delete-confirmation-dialog"]');
|
||||
return await dialog.isVisible().catch(() => false);
|
||||
}
|
||||
@@ -111,7 +97,7 @@ export async function waitForDeleteConfirmationDialog(
|
||||
page: Page,
|
||||
options?: { timeout?: number }
|
||||
): Promise<Locator> {
|
||||
return await waitForElement(page, "delete-confirmation-dialog", options);
|
||||
return await waitForElement(page, 'delete-confirmation-dialog', options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -121,21 +107,21 @@ export async function waitForDeleteConfirmationDialogHidden(
|
||||
page: Page,
|
||||
options?: { timeout?: number }
|
||||
): Promise<void> {
|
||||
await waitForElementHidden(page, "delete-confirmation-dialog", options);
|
||||
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");
|
||||
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");
|
||||
await clickElement(page, 'cancel-delete-button');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,7 +139,7 @@ export async function waitForFollowUpDialog(
|
||||
page: Page,
|
||||
options?: { timeout?: number }
|
||||
): Promise<Locator> {
|
||||
return await waitForElement(page, "follow-up-dialog", options);
|
||||
return await waitForElement(page, 'follow-up-dialog', options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -163,14 +149,14 @@ export async function waitForFollowUpDialogHidden(
|
||||
page: Page,
|
||||
options?: { timeout?: number }
|
||||
): Promise<void> {
|
||||
await waitForElementHidden(page, "follow-up-dialog", options);
|
||||
await waitForElementHidden(page, 'follow-up-dialog', options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Click the confirm follow-up button in the follow-up dialog
|
||||
*/
|
||||
export async function clickConfirmFollowUp(page: Page): Promise<void> {
|
||||
await clickElement(page, "confirm-follow-up");
|
||||
await clickElement(page, 'confirm-follow-up');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,7 +174,7 @@ export async function waitForProjectInitDialog(
|
||||
page: Page,
|
||||
options?: { timeout?: number }
|
||||
): Promise<Locator> {
|
||||
return await waitForElement(page, "project-init-dialog", options);
|
||||
return await waitForElement(page, 'project-init-dialog', options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Page, Locator } from "@playwright/test";
|
||||
import { waitForElement, waitForElementHidden } from "../core/waiting";
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
import { waitForElement, waitForElementHidden } from '../core/waiting';
|
||||
|
||||
/**
|
||||
* Check if the agent output modal is visible
|
||||
@@ -16,7 +16,7 @@ export async function waitForAgentOutputModal(
|
||||
page: Page,
|
||||
options?: { timeout?: number }
|
||||
): Promise<Locator> {
|
||||
return await waitForElement(page, "agent-output-modal", options);
|
||||
return await waitForElement(page, 'agent-output-modal', options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,28 +26,22 @@ export async function waitForAgentOutputModalHidden(
|
||||
page: Page,
|
||||
options?: { timeout?: number }
|
||||
): Promise<void> {
|
||||
await waitForElementHidden(page, "agent-output-modal", options);
|
||||
await waitForElementHidden(page, 'agent-output-modal', options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the modal title/description text to verify which feature's output is being shown
|
||||
*/
|
||||
export async function getAgentOutputModalDescription(
|
||||
page: Page
|
||||
): Promise<string | null> {
|
||||
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();
|
||||
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> {
|
||||
export async function getOutputModalDescription(page: Page): Promise<string | null> {
|
||||
const modalDescription = page.locator(
|
||||
'[data-testid="agent-output-modal"] [data-slot="dialog-description"]'
|
||||
);
|
||||
@@ -57,18 +51,14 @@ export async function getOutputModalDescription(
|
||||
/**
|
||||
* Get the agent output modal description element
|
||||
*/
|
||||
export async function getAgentOutputModalDescriptionElement(
|
||||
page: Page
|
||||
): Promise<Locator> {
|
||||
export async function getAgentOutputModalDescriptionElement(page: Page): Promise<Locator> {
|
||||
return page.locator('[data-testid="agent-output-description"]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the agent output modal description is scrollable
|
||||
*/
|
||||
export async function isAgentOutputDescriptionScrollable(
|
||||
page: Page
|
||||
): Promise<boolean> {
|
||||
export async function isAgentOutputDescriptionScrollable(page: Page): Promise<boolean> {
|
||||
const description = page.locator('[data-testid="agent-output-description"]');
|
||||
const scrollInfo = await description.evaluate((el) => {
|
||||
return {
|
||||
@@ -83,9 +73,7 @@ export async function isAgentOutputDescriptionScrollable(
|
||||
/**
|
||||
* Get scroll dimensions of the agent output modal description
|
||||
*/
|
||||
export async function getAgentOutputDescriptionScrollDimensions(
|
||||
page: Page
|
||||
): Promise<{
|
||||
export async function getAgentOutputDescriptionScrollDimensions(page: Page): Promise<{
|
||||
scrollHeight: number;
|
||||
clientHeight: number;
|
||||
maxHeight: string;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
/**
|
||||
* Base URL for the API server
|
||||
*/
|
||||
export const API_BASE_URL = "http://localhost:3008";
|
||||
export const API_BASE_URL = 'http://localhost:3008';
|
||||
|
||||
/**
|
||||
* API endpoints for worktree operations
|
||||
@@ -74,55 +74,55 @@ export const TIMEOUTS = {
|
||||
*/
|
||||
export const TEST_IDS = {
|
||||
// Sidebar & Navigation
|
||||
sidebar: "sidebar",
|
||||
navBoard: "nav-board",
|
||||
navSpec: "nav-spec",
|
||||
navContext: "nav-context",
|
||||
navAgent: "nav-agent",
|
||||
navProfiles: "nav-profiles",
|
||||
settingsButton: "settings-button",
|
||||
openProjectButton: "open-project-button",
|
||||
sidebar: 'sidebar',
|
||||
navBoard: 'nav-board',
|
||||
navSpec: 'nav-spec',
|
||||
navContext: 'nav-context',
|
||||
navAgent: 'nav-agent',
|
||||
navProfiles: 'nav-profiles',
|
||||
settingsButton: 'settings-button',
|
||||
openProjectButton: 'open-project-button',
|
||||
|
||||
// Views
|
||||
boardView: "board-view",
|
||||
specView: "spec-view",
|
||||
contextView: "context-view",
|
||||
agentView: "agent-view",
|
||||
profilesView: "profiles-view",
|
||||
settingsView: "settings-view",
|
||||
welcomeView: "welcome-view",
|
||||
setupView: "setup-view",
|
||||
boardView: 'board-view',
|
||||
specView: 'spec-view',
|
||||
contextView: 'context-view',
|
||||
agentView: 'agent-view',
|
||||
profilesView: 'profiles-view',
|
||||
settingsView: 'settings-view',
|
||||
welcomeView: 'welcome-view',
|
||||
setupView: 'setup-view',
|
||||
|
||||
// Board View Components
|
||||
addFeatureButton: "add-feature-button",
|
||||
addFeatureDialog: "add-feature-dialog",
|
||||
confirmAddFeature: "confirm-add-feature",
|
||||
featureBranchInput: "feature-input",
|
||||
featureCategoryInput: "feature-category-input",
|
||||
worktreeSelector: "worktree-selector",
|
||||
addFeatureButton: 'add-feature-button',
|
||||
addFeatureDialog: 'add-feature-dialog',
|
||||
confirmAddFeature: 'confirm-add-feature',
|
||||
featureBranchInput: 'feature-input',
|
||||
featureCategoryInput: 'feature-category-input',
|
||||
worktreeSelector: 'worktree-selector',
|
||||
|
||||
// Spec Editor
|
||||
specEditor: "spec-editor",
|
||||
specEditor: 'spec-editor',
|
||||
|
||||
// File Browser Dialog
|
||||
pathInput: "path-input",
|
||||
goToPathButton: "go-to-path-button",
|
||||
pathInput: 'path-input',
|
||||
goToPathButton: 'go-to-path-button',
|
||||
|
||||
// Profiles View
|
||||
addProfileButton: "add-profile-button",
|
||||
addProfileDialog: "add-profile-dialog",
|
||||
editProfileDialog: "edit-profile-dialog",
|
||||
deleteProfileConfirmDialog: "delete-profile-confirm-dialog",
|
||||
saveProfileButton: "save-profile-button",
|
||||
confirmDeleteProfileButton: "confirm-delete-profile-button",
|
||||
cancelDeleteButton: "cancel-delete-button",
|
||||
profileNameInput: "profile-name-input",
|
||||
profileDescriptionInput: "profile-description-input",
|
||||
refreshProfilesButton: "refresh-profiles-button",
|
||||
addProfileButton: 'add-profile-button',
|
||||
addProfileDialog: 'add-profile-dialog',
|
||||
editProfileDialog: 'edit-profile-dialog',
|
||||
deleteProfileConfirmDialog: 'delete-profile-confirm-dialog',
|
||||
saveProfileButton: 'save-profile-button',
|
||||
confirmDeleteProfileButton: 'confirm-delete-profile-button',
|
||||
cancelDeleteButton: 'cancel-delete-button',
|
||||
profileNameInput: 'profile-name-input',
|
||||
profileDescriptionInput: 'profile-description-input',
|
||||
refreshProfilesButton: 'refresh-profiles-button',
|
||||
|
||||
// Context View
|
||||
contextFileList: "context-file-list",
|
||||
addContextButton: "add-context-button",
|
||||
contextFileList: 'context-file-list',
|
||||
addContextButton: 'add-context-button',
|
||||
} as const;
|
||||
|
||||
// ============================================================================
|
||||
@@ -134,17 +134,17 @@ export const TEST_IDS = {
|
||||
*/
|
||||
export const CSS_SELECTORS = {
|
||||
/** CodeMirror editor content area */
|
||||
codeMirrorContent: ".cm-content",
|
||||
codeMirrorContent: '.cm-content',
|
||||
/** Dialog elements */
|
||||
dialog: '[role="dialog"]',
|
||||
/** Sonner toast notifications */
|
||||
toast: "[data-sonner-toast]",
|
||||
toast: '[data-sonner-toast]',
|
||||
toastError: '[data-sonner-toast][data-type="error"]',
|
||||
toastSuccess: '[data-sonner-toast][data-type="success"]',
|
||||
/** Command/combobox input (shadcn-ui cmdk) */
|
||||
commandInput: "[cmdk-input]",
|
||||
commandInput: '[cmdk-input]',
|
||||
/** Radix dialog overlay */
|
||||
dialogOverlay: "[data-radix-dialog-overlay]",
|
||||
dialogOverlay: '[data-radix-dialog-overlay]',
|
||||
} as const;
|
||||
|
||||
// ============================================================================
|
||||
@@ -155,8 +155,8 @@ export const CSS_SELECTORS = {
|
||||
* localStorage keys used by the application
|
||||
*/
|
||||
export const STORAGE_KEYS = {
|
||||
appStorage: "automaker-storage",
|
||||
setupStorage: "automaker-setup",
|
||||
appStorage: 'automaker-storage',
|
||||
setupStorage: 'automaker-setup',
|
||||
} as const;
|
||||
|
||||
// ============================================================================
|
||||
@@ -169,7 +169,7 @@ export const STORAGE_KEYS = {
|
||||
* @returns Sanitized name suitable for directory paths
|
||||
*/
|
||||
export function sanitizeBranchName(branchName: string): string {
|
||||
return branchName.replace(/[^a-zA-Z0-9_-]/g, "-");
|
||||
return branchName.replace(/[^a-zA-Z0-9_-]/g, '-');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -180,8 +180,8 @@ export function sanitizeBranchName(branchName: string): string {
|
||||
* Default values used in test setup
|
||||
*/
|
||||
export const DEFAULTS = {
|
||||
projectName: "Test Project",
|
||||
projectPath: "/mock/test-project",
|
||||
theme: "dark" as const,
|
||||
projectName: 'Test Project',
|
||||
projectPath: '/mock/test-project',
|
||||
theme: 'dark' as const,
|
||||
maxConcurrency: 3,
|
||||
} as const;
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
import { Page, Locator } from "@playwright/test";
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Get an element by its data-testid attribute
|
||||
*/
|
||||
export async function getByTestId(
|
||||
page: Page,
|
||||
testId: string
|
||||
): Promise<Locator> {
|
||||
export async function getByTestId(page: Page, testId: string): Promise<Locator> {
|
||||
return page.locator(`[data-testid="${testId}"]`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a button by its text content
|
||||
*/
|
||||
export async function getButtonByText(
|
||||
page: Page,
|
||||
text: string
|
||||
): Promise<Locator> {
|
||||
export async function getButtonByText(page: Page, text: string): Promise<Locator> {
|
||||
return page.locator(`button:has-text("${text}")`);
|
||||
}
|
||||
|
||||
@@ -25,7 +19,7 @@ export async function getButtonByText(
|
||||
*/
|
||||
export async function getCategoryAutocompleteInput(
|
||||
page: Page,
|
||||
testId: string = "feature-category-input"
|
||||
testId: string = 'feature-category-input'
|
||||
): Promise<Locator> {
|
||||
return page.locator(`[data-testid="${testId}"]`);
|
||||
}
|
||||
@@ -33,8 +27,6 @@ export async function getCategoryAutocompleteInput(
|
||||
/**
|
||||
* Get the category autocomplete dropdown list
|
||||
*/
|
||||
export async function getCategoryAutocompleteList(
|
||||
page: Page
|
||||
): Promise<Locator> {
|
||||
export async function getCategoryAutocompleteList(page: Page): Promise<Locator> {
|
||||
return page.locator('[data-testid="category-autocomplete-list"]');
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Page } from "@playwright/test";
|
||||
import { getByTestId, getButtonByText } from "./elements";
|
||||
import { Page } from '@playwright/test';
|
||||
import { getByTestId, getButtonByText } from './elements';
|
||||
|
||||
/**
|
||||
* Get the platform-specific modifier key (Meta for Mac, Control for Windows/Linux)
|
||||
* This is used for keyboard shortcuts like Cmd+Enter or Ctrl+Enter
|
||||
*/
|
||||
export function getPlatformModifier(): "Meta" | "Control" {
|
||||
return process.platform === "darwin" ? "Meta" : "Control";
|
||||
export function getPlatformModifier(): 'Meta' | 'Control' {
|
||||
return process.platform === 'darwin' ? 'Meta' : 'Control';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,10 +28,7 @@ export async function clickElement(page: Page, testId: string): Promise<void> {
|
||||
/**
|
||||
* Click a button by its text content
|
||||
*/
|
||||
export async function clickButtonByText(
|
||||
page: Page,
|
||||
text: string
|
||||
): Promise<void> {
|
||||
export async function clickButtonByText(page: Page, text: string): Promise<void> {
|
||||
const button = await getButtonByText(page, text);
|
||||
await button.click();
|
||||
}
|
||||
@@ -39,11 +36,7 @@ export async function clickButtonByText(
|
||||
/**
|
||||
* Fill an input field by its data-testid attribute
|
||||
*/
|
||||
export async function fillInput(
|
||||
page: Page,
|
||||
testId: string,
|
||||
value: string
|
||||
): Promise<void> {
|
||||
export async function fillInput(page: Page, testId: string, value: string): Promise<void> {
|
||||
const input = await getByTestId(page, testId);
|
||||
await input.fill(value);
|
||||
}
|
||||
@@ -75,11 +68,11 @@ export async function focusOnInput(page: Page, testId: string): Promise<void> {
|
||||
* Waits for dialog to be removed from DOM rather than using arbitrary timeout
|
||||
*/
|
||||
export async function closeDialogWithEscape(page: Page): Promise<void> {
|
||||
await page.keyboard.press("Escape");
|
||||
await page.keyboard.press('Escape');
|
||||
// Wait for any dialog overlay to disappear
|
||||
await page
|
||||
.locator('[data-radix-dialog-overlay], [role="dialog"]')
|
||||
.waitFor({ state: "hidden", timeout: 5000 })
|
||||
.waitFor({ state: 'hidden', timeout: 5000 })
|
||||
.catch(() => {
|
||||
// Dialog may have already closed or not exist
|
||||
});
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Page, Locator } from "@playwright/test";
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Wait for the page to reach network idle state
|
||||
* This is commonly used after navigation or page reload to ensure all network requests have completed
|
||||
*/
|
||||
export async function waitForNetworkIdle(page: Page): Promise<void> {
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -14,12 +14,12 @@ export async function waitForNetworkIdle(page: Page): Promise<void> {
|
||||
export async function waitForElement(
|
||||
page: Page,
|
||||
testId: string,
|
||||
options?: { timeout?: number; state?: "attached" | "visible" | "hidden" }
|
||||
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",
|
||||
state: options?.state ?? 'visible',
|
||||
});
|
||||
return element;
|
||||
}
|
||||
@@ -35,6 +35,6 @@ export async function waitForElementHidden(
|
||||
const element = page.locator(`[data-testid="${testId}"]`);
|
||||
await element.waitFor({
|
||||
timeout: options?.timeout ?? 5000,
|
||||
state: "hidden",
|
||||
state: 'hidden',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Page, Locator } from "@playwright/test";
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Perform a drag and drop operation that works with @dnd-kit
|
||||
@@ -13,8 +13,8 @@ export async function dragAndDropWithDndKit(
|
||||
targetLocator: Locator
|
||||
): Promise<void> {
|
||||
// Ensure elements are visible and stable before getting bounding boxes
|
||||
await sourceLocator.waitFor({ state: "visible", timeout: 5000 });
|
||||
await targetLocator.waitFor({ state: "visible", timeout: 5000 });
|
||||
await sourceLocator.waitFor({ state: 'visible', timeout: 5000 });
|
||||
await targetLocator.waitFor({ state: 'visible', timeout: 5000 });
|
||||
|
||||
// Small delay to ensure layout is stable
|
||||
await page.waitForTimeout(100);
|
||||
@@ -23,7 +23,7 @@ export async function dragAndDropWithDndKit(
|
||||
const targetBox = await targetLocator.boundingBox();
|
||||
|
||||
if (!sourceBox || !targetBox) {
|
||||
throw new Error("Could not find source or target element bounds");
|
||||
throw new Error('Could not find source or target element bounds');
|
||||
}
|
||||
|
||||
// Start drag from the center of the source element
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Page, Locator } from "@playwright/test";
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Get the skip tests checkbox element in the add feature dialog
|
||||
@@ -20,8 +20,8 @@ export async function toggleSkipTestsCheckbox(page: Page): Promise<void> {
|
||||
*/
|
||||
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";
|
||||
const state = await checkbox.getAttribute('data-state');
|
||||
return state === 'checked';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,17 +44,14 @@ export async function toggleEditSkipTestsCheckbox(page: Page): Promise<void> {
|
||||
*/
|
||||
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";
|
||||
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> {
|
||||
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);
|
||||
}
|
||||
@@ -62,20 +59,14 @@ export async function isSkipTestsBadgeVisible(
|
||||
/**
|
||||
* Get the skip tests badge element for a kanban card
|
||||
*/
|
||||
export async function getSkipTestsBadge(
|
||||
page: Page,
|
||||
featureId: string
|
||||
): Promise<Locator> {
|
||||
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> {
|
||||
export async function clickManualVerify(page: Page, featureId: string): Promise<void> {
|
||||
const button = page.locator(`[data-testid="manual-verify-${featureId}"]`);
|
||||
await button.click();
|
||||
}
|
||||
@@ -83,10 +74,7 @@ export async function clickManualVerify(
|
||||
/**
|
||||
* Check if the manual verify button is visible for a feature
|
||||
*/
|
||||
export async function isManualVerifyButtonVisible(
|
||||
page: Page,
|
||||
featureId: string
|
||||
): Promise<boolean> {
|
||||
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);
|
||||
}
|
||||
@@ -94,10 +82,7 @@ export async function isManualVerifyButtonVisible(
|
||||
/**
|
||||
* Click the move back button for a verified skipTests feature
|
||||
*/
|
||||
export async function clickMoveBack(
|
||||
page: Page,
|
||||
featureId: string
|
||||
): Promise<void> {
|
||||
export async function clickMoveBack(page: Page, featureId: string): Promise<void> {
|
||||
const button = page.locator(`[data-testid="move-back-${featureId}"]`);
|
||||
await button.click();
|
||||
}
|
||||
@@ -105,10 +90,7 @@ export async function clickMoveBack(
|
||||
/**
|
||||
* Check if the move back button is visible for a feature
|
||||
*/
|
||||
export async function isMoveBackButtonVisible(
|
||||
page: Page,
|
||||
featureId: string
|
||||
): Promise<boolean> {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { Page, Locator } from "@playwright/test";
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Get the count up timer element for a specific feature card
|
||||
*/
|
||||
export async function getTimerForFeature(
|
||||
page: Page,
|
||||
featureId: string
|
||||
): Promise<Locator> {
|
||||
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"]');
|
||||
}
|
||||
@@ -26,10 +23,7 @@ export async function getTimerDisplayForFeature(
|
||||
/**
|
||||
* Check if a timer is visible for a specific feature
|
||||
*/
|
||||
export async function isTimerVisibleForFeature(
|
||||
page: Page,
|
||||
featureId: string
|
||||
): Promise<boolean> {
|
||||
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);
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
import { Page, Locator } from "@playwright/test";
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Get the follow-up button for a waiting_approval feature
|
||||
*/
|
||||
export async function getFollowUpButton(
|
||||
page: Page,
|
||||
featureId: string
|
||||
): Promise<Locator> {
|
||||
export async function getFollowUpButton(page: Page, featureId: string): Promise<Locator> {
|
||||
return page.locator(`[data-testid="follow-up-${featureId}"]`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Click the follow-up button for a waiting_approval feature
|
||||
*/
|
||||
export async function clickFollowUpButton(
|
||||
page: Page,
|
||||
featureId: string
|
||||
): Promise<void> {
|
||||
export async function clickFollowUpButton(page: Page, featureId: string): Promise<void> {
|
||||
const button = page.locator(`[data-testid="follow-up-${featureId}"]`);
|
||||
await button.click();
|
||||
}
|
||||
@@ -24,10 +18,7 @@ export async function clickFollowUpButton(
|
||||
/**
|
||||
* Check if the follow-up button is visible for a feature
|
||||
*/
|
||||
export async function isFollowUpButtonVisible(
|
||||
page: Page,
|
||||
featureId: string
|
||||
): Promise<boolean> {
|
||||
export async function isFollowUpButtonVisible(page: Page, featureId: string): Promise<boolean> {
|
||||
const button = page.locator(`[data-testid="follow-up-${featureId}"]`);
|
||||
return await button.isVisible().catch(() => false);
|
||||
}
|
||||
@@ -35,20 +26,14 @@ export async function isFollowUpButtonVisible(
|
||||
/**
|
||||
* Get the commit button for a waiting_approval feature
|
||||
*/
|
||||
export async function getCommitButton(
|
||||
page: Page,
|
||||
featureId: string
|
||||
): Promise<Locator> {
|
||||
export async function getCommitButton(page: Page, featureId: string): Promise<Locator> {
|
||||
return page.locator(`[data-testid="commit-${featureId}"]`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Click the commit button for a waiting_approval feature
|
||||
*/
|
||||
export async function clickCommitButton(
|
||||
page: Page,
|
||||
featureId: string
|
||||
): Promise<void> {
|
||||
export async function clickCommitButton(page: Page, featureId: string): Promise<void> {
|
||||
const button = page.locator(`[data-testid="commit-${featureId}"]`);
|
||||
await button.click();
|
||||
}
|
||||
@@ -56,10 +41,7 @@ export async function clickCommitButton(
|
||||
/**
|
||||
* Check if the commit button is visible for a feature
|
||||
*/
|
||||
export async function isCommitButtonVisible(
|
||||
page: Page,
|
||||
featureId: string
|
||||
): Promise<boolean> {
|
||||
export async function isCommitButtonVisible(page: Page, featureId: string): Promise<boolean> {
|
||||
const button = page.locator(`[data-testid="commit-${featureId}"]`);
|
||||
return await button.isVisible().catch(() => false);
|
||||
}
|
||||
@@ -74,9 +56,7 @@ export async function getWaitingApprovalColumn(page: Page): Promise<Locator> {
|
||||
/**
|
||||
* Check if the waiting_approval column is visible
|
||||
*/
|
||||
export async function isWaitingApprovalColumnVisible(
|
||||
page: Page
|
||||
): Promise<boolean> {
|
||||
export async function isWaitingApprovalColumnVisible(page: Page): Promise<boolean> {
|
||||
const column = page.locator('[data-testid="kanban-column-waiting_approval"]');
|
||||
return await column.isVisible().catch(() => false);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Page } from "@playwright/test";
|
||||
import { Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Simulate drag and drop of a file onto an element
|
||||
@@ -8,7 +8,7 @@ export async function simulateFileDrop(
|
||||
targetSelector: string,
|
||||
fileName: string,
|
||||
fileContent: string,
|
||||
mimeType: string = "text/plain"
|
||||
mimeType: string = 'text/plain'
|
||||
): Promise<void> {
|
||||
await page.evaluate(
|
||||
({ selector, content, name, mime }) => {
|
||||
@@ -21,13 +21,13 @@ export async function simulateFileDrop(
|
||||
|
||||
// Dispatch drag events
|
||||
target.dispatchEvent(
|
||||
new DragEvent("dragover", {
|
||||
new DragEvent('dragover', {
|
||||
dataTransfer,
|
||||
bubbles: true,
|
||||
})
|
||||
);
|
||||
target.dispatchEvent(
|
||||
new DragEvent("drop", {
|
||||
new DragEvent('drop', {
|
||||
dataTransfer,
|
||||
bubbles: true,
|
||||
})
|
||||
@@ -45,7 +45,7 @@ export async function simulateImagePaste(
|
||||
page: Page,
|
||||
targetSelector: string,
|
||||
imageBase64: string,
|
||||
mimeType: string = "image/png"
|
||||
mimeType: string = 'image/png'
|
||||
): Promise<void> {
|
||||
await page.evaluate(
|
||||
({ selector, base64, mime }) => {
|
||||
@@ -62,14 +62,14 @@ export async function simulateImagePaste(
|
||||
const blob = new Blob([byteArray], { type: mime });
|
||||
|
||||
// Create a File from Blob
|
||||
const file = new File([blob], "pasted-image.png", { type: mime });
|
||||
const file = new File([blob], 'pasted-image.png', { type: mime });
|
||||
|
||||
// Create a DataTransfer with clipboard items
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(file);
|
||||
|
||||
// Create ClipboardEvent with the image data
|
||||
const clipboardEvent = new ClipboardEvent("paste", {
|
||||
const clipboardEvent = new ClipboardEvent('paste', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
clipboardData: dataTransfer,
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { Page, Locator } from "@playwright/test";
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Get the concurrency slider container
|
||||
*/
|
||||
export async function getConcurrencySliderContainer(
|
||||
page: Page
|
||||
): Promise<Locator> {
|
||||
export async function getConcurrencySliderContainer(page: Page): Promise<Locator> {
|
||||
return page.locator('[data-testid="concurrency-slider-container"]');
|
||||
}
|
||||
|
||||
@@ -37,7 +35,7 @@ export async function setConcurrencyValue(
|
||||
const sliderBounds = await slider.boundingBox();
|
||||
|
||||
if (!sliderBounds) {
|
||||
throw new Error("Concurrency slider not found or not visible");
|
||||
throw new Error('Concurrency slider not found or not visible');
|
||||
}
|
||||
|
||||
// Calculate position for target value
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Page, Locator } from "@playwright/test";
|
||||
import { clickElement } from "../core/interactions";
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
import { clickElement } from '../core/interactions';
|
||||
|
||||
/**
|
||||
* Get the log viewer header element (contains type counts and expand/collapse buttons)
|
||||
@@ -26,30 +26,21 @@ export async function getLogEntriesContainer(page: Page): Promise<Locator> {
|
||||
/**
|
||||
* Get a log entry by its type
|
||||
*/
|
||||
export async function getLogEntryByType(
|
||||
page: Page,
|
||||
type: string
|
||||
): Promise<Locator> {
|
||||
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> {
|
||||
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> {
|
||||
export async function countLogEntriesByType(page: Page, type: string): Promise<number> {
|
||||
const entries = page.locator(`[data-testid="log-entry-${type}"]`);
|
||||
return await entries.count();
|
||||
}
|
||||
@@ -57,20 +48,14 @@ export async function countLogEntriesByType(
|
||||
/**
|
||||
* Get the log type count badge by type
|
||||
*/
|
||||
export async function getLogTypeCountBadge(
|
||||
page: Page,
|
||||
type: string
|
||||
): Promise<Locator> {
|
||||
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> {
|
||||
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);
|
||||
}
|
||||
@@ -79,14 +64,14 @@ export async function isLogTypeCountBadgeVisible(
|
||||
* Click the expand all button in the log viewer
|
||||
*/
|
||||
export async function clickLogExpandAll(page: Page): Promise<void> {
|
||||
await clickElement(page, "log-expand-all");
|
||||
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");
|
||||
await clickElement(page, 'log-collapse-all');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,31 +92,22 @@ export async function isLogEntryBadgeVisible(page: Page): Promise<boolean> {
|
||||
/**
|
||||
* Get the view mode toggle button (parsed/raw)
|
||||
*/
|
||||
export async function getViewModeButton(
|
||||
page: Page,
|
||||
mode: "parsed" | "raw"
|
||||
): Promise<Locator> {
|
||||
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> {
|
||||
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> {
|
||||
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;
|
||||
const classes = await button.getAttribute('class');
|
||||
return classes?.includes('text-purple-300') ?? false;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Locator } from "@playwright/test";
|
||||
import { Locator } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Check if an element is scrollable (has scrollable content)
|
||||
|
||||
@@ -1,49 +1,49 @@
|
||||
// Re-export all utilities from their respective modules
|
||||
|
||||
// Core utilities
|
||||
export * from "./core/elements";
|
||||
export * from "./core/interactions";
|
||||
export * from "./core/waiting";
|
||||
export * from "./core/constants";
|
||||
export * from './core/elements';
|
||||
export * from './core/interactions';
|
||||
export * from './core/waiting';
|
||||
export * from './core/constants';
|
||||
|
||||
// API utilities
|
||||
export * from "./api/client";
|
||||
export * from './api/client';
|
||||
|
||||
// Git utilities
|
||||
export * from "./git/worktree";
|
||||
export * from './git/worktree';
|
||||
|
||||
// Project utilities
|
||||
export * from "./project/setup";
|
||||
export * from "./project/fixtures";
|
||||
export * from './project/setup';
|
||||
export * from './project/fixtures';
|
||||
|
||||
// Navigation utilities
|
||||
export * from "./navigation/views";
|
||||
export * from './navigation/views';
|
||||
|
||||
// View-specific utilities
|
||||
export * from "./views/board";
|
||||
export * from "./views/context";
|
||||
export * from "./views/spec-editor";
|
||||
export * from "./views/agent";
|
||||
export * from "./views/settings";
|
||||
export * from "./views/setup";
|
||||
export * from "./views/profiles";
|
||||
export * from './views/board';
|
||||
export * from './views/context';
|
||||
export * from './views/spec-editor';
|
||||
export * from './views/agent';
|
||||
export * from './views/settings';
|
||||
export * from './views/setup';
|
||||
export * from './views/profiles';
|
||||
|
||||
// Component utilities
|
||||
export * from "./components/dialogs";
|
||||
export * from "./components/toasts";
|
||||
export * from "./components/modals";
|
||||
export * from "./components/autocomplete";
|
||||
export * from './components/dialogs';
|
||||
export * from './components/toasts';
|
||||
export * from './components/modals';
|
||||
export * from './components/autocomplete';
|
||||
|
||||
// Feature utilities
|
||||
export * from "./features/kanban";
|
||||
export * from "./features/timers";
|
||||
export * from "./features/skip-tests";
|
||||
export * from "./features/waiting-approval";
|
||||
export * from './features/kanban';
|
||||
export * from './features/timers';
|
||||
export * from './features/skip-tests';
|
||||
export * from './features/waiting-approval';
|
||||
|
||||
// Helper utilities
|
||||
export * from "./helpers/scroll";
|
||||
export * from "./helpers/log-viewer";
|
||||
export * from "./helpers/concurrency";
|
||||
export * from './helpers/scroll';
|
||||
export * from './helpers/log-viewer';
|
||||
export * from './helpers/concurrency';
|
||||
|
||||
// File utilities
|
||||
export * from "./files/drag-drop";
|
||||
export * from './files/drag-drop';
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { Page } from "@playwright/test";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { Page } from '@playwright/test';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* Resolve the workspace root - handle both running from apps/ui and from root
|
||||
*/
|
||||
export 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;
|
||||
}
|
||||
|
||||
const WORKSPACE_ROOT = getWorkspaceRoot();
|
||||
const FIXTURE_PATH = path.join(WORKSPACE_ROOT, "test/fixtures/projectA");
|
||||
const SPEC_FILE_PATH = path.join(FIXTURE_PATH, ".automaker/app_spec.txt");
|
||||
const CONTEXT_PATH = path.join(FIXTURE_PATH, ".automaker/context");
|
||||
const FIXTURE_PATH = path.join(WORKSPACE_ROOT, 'test/fixtures/projectA');
|
||||
const SPEC_FILE_PATH = path.join(FIXTURE_PATH, '.automaker/app_spec.txt');
|
||||
const CONTEXT_PATH = path.join(FIXTURE_PATH, '.automaker/context');
|
||||
|
||||
// Original spec content for resetting between tests
|
||||
const ORIGINAL_SPEC_CONTENT = `<app_spec>
|
||||
@@ -76,8 +76,8 @@ export async function setupProjectWithFixture(
|
||||
): Promise<void> {
|
||||
await page.addInitScript((pathArg: string) => {
|
||||
const mockProject = {
|
||||
id: "test-project-fixture",
|
||||
name: "projectA",
|
||||
id: 'test-project-fixture',
|
||||
name: 'projectA',
|
||||
path: pathArg,
|
||||
lastOpened: new Date().toISOString(),
|
||||
};
|
||||
@@ -86,10 +86,10 @@ export async function setupProjectWithFixture(
|
||||
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,
|
||||
@@ -97,19 +97,19 @@ export async function setupProjectWithFixture(
|
||||
version: 2, // Must match app-store.ts persist version
|
||||
};
|
||||
|
||||
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
|
||||
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
|
||||
|
||||
// Also 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: 2, // Must match app-store.ts persist version
|
||||
};
|
||||
localStorage.setItem("automaker-setup", JSON.stringify(setupState));
|
||||
localStorage.setItem('automaker-setup', JSON.stringify(setupState));
|
||||
}, projectPath);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Page } from "@playwright/test";
|
||||
import { Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Set up a mock project in localStorage to bypass the welcome screen
|
||||
@@ -7,9 +7,9 @@ import { Page } from "@playwright/test";
|
||||
export async function setupMockProject(page: Page): Promise<void> {
|
||||
await page.addInitScript(() => {
|
||||
const mockProject = {
|
||||
id: "test-project-1",
|
||||
name: "Test Project",
|
||||
path: "/mock/test-project",
|
||||
id: 'test-project-1',
|
||||
name: 'Test Project',
|
||||
path: '/mock/test-project',
|
||||
lastOpened: new Date().toISOString(),
|
||||
};
|
||||
|
||||
@@ -17,9 +17,9 @@ export async function setupMockProject(page: Page): Promise<void> {
|
||||
state: {
|
||||
projects: [mockProject],
|
||||
currentProject: mockProject,
|
||||
theme: "dark",
|
||||
theme: 'dark',
|
||||
sidebarOpen: true,
|
||||
apiKeys: { anthropic: "", google: "" },
|
||||
apiKeys: { anthropic: '', google: '' },
|
||||
chatSessions: [],
|
||||
chatHistoryOpen: false,
|
||||
maxConcurrency: 3,
|
||||
@@ -27,7 +27,7 @@ export async function setupMockProject(page: Page): Promise<void> {
|
||||
version: 2, // Must match app-store.ts persist version
|
||||
};
|
||||
|
||||
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
|
||||
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -40,9 +40,9 @@ export async function setupMockProjectWithConcurrency(
|
||||
): Promise<void> {
|
||||
await page.addInitScript((maxConcurrency: number) => {
|
||||
const mockProject = {
|
||||
id: "test-project-1",
|
||||
name: "Test Project",
|
||||
path: "/mock/test-project",
|
||||
id: 'test-project-1',
|
||||
name: 'Test Project',
|
||||
path: '/mock/test-project',
|
||||
lastOpened: new Date().toISOString(),
|
||||
};
|
||||
|
||||
@@ -50,9 +50,9 @@ export async function setupMockProjectWithConcurrency(
|
||||
state: {
|
||||
projects: [mockProject],
|
||||
currentProject: mockProject,
|
||||
theme: "dark",
|
||||
theme: 'dark',
|
||||
sidebarOpen: true,
|
||||
apiKeys: { anthropic: "", google: "" },
|
||||
apiKeys: { anthropic: '', google: '' },
|
||||
chatSessions: [],
|
||||
chatHistoryOpen: false,
|
||||
maxConcurrency: maxConcurrency,
|
||||
@@ -60,7 +60,7 @@ export async function setupMockProjectWithConcurrency(
|
||||
version: 2, // Must match app-store.ts persist version
|
||||
};
|
||||
|
||||
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
|
||||
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
|
||||
}, concurrency);
|
||||
}
|
||||
|
||||
@@ -70,20 +70,14 @@ export async function setupMockProjectWithConcurrency(
|
||||
export async function setupMockProjectAtConcurrencyLimit(
|
||||
page: Page,
|
||||
maxConcurrency: number = 1,
|
||||
runningTasks: string[] = ["running-task-1"]
|
||||
runningTasks: string[] = ['running-task-1']
|
||||
): Promise<void> {
|
||||
await page.addInitScript(
|
||||
({
|
||||
maxConcurrency,
|
||||
runningTasks,
|
||||
}: {
|
||||
maxConcurrency: number;
|
||||
runningTasks: string[];
|
||||
}) => {
|
||||
({ maxConcurrency, runningTasks }: { maxConcurrency: number; runningTasks: string[] }) => {
|
||||
const mockProject = {
|
||||
id: "test-project-1",
|
||||
name: "Test Project",
|
||||
path: "/mock/test-project",
|
||||
id: 'test-project-1',
|
||||
name: 'Test Project',
|
||||
path: '/mock/test-project',
|
||||
lastOpened: new Date().toISOString(),
|
||||
};
|
||||
|
||||
@@ -91,9 +85,9 @@ export async function setupMockProjectAtConcurrencyLimit(
|
||||
state: {
|
||||
projects: [mockProject],
|
||||
currentProject: mockProject,
|
||||
theme: "dark",
|
||||
theme: 'dark',
|
||||
sidebarOpen: true,
|
||||
apiKeys: { anthropic: "", google: "" },
|
||||
apiKeys: { anthropic: '', google: '' },
|
||||
chatSessions: [],
|
||||
chatHistoryOpen: false,
|
||||
maxConcurrency: maxConcurrency,
|
||||
@@ -104,7 +98,7 @@ export async function setupMockProjectAtConcurrencyLimit(
|
||||
version: 2, // Must match app-store.ts persist version
|
||||
};
|
||||
|
||||
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
|
||||
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
|
||||
},
|
||||
{ maxConcurrency, runningTasks }
|
||||
);
|
||||
@@ -122,16 +116,16 @@ export async function setupMockProjectWithFeatures(
|
||||
id: string;
|
||||
category: string;
|
||||
description: string;
|
||||
status: "backlog" | "in_progress" | "verified";
|
||||
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",
|
||||
id: 'test-project-1',
|
||||
name: 'Test Project',
|
||||
path: '/mock/test-project',
|
||||
lastOpened: new Date().toISOString(),
|
||||
};
|
||||
|
||||
@@ -141,9 +135,9 @@ export async function setupMockProjectWithFeatures(
|
||||
state: {
|
||||
projects: [mockProject],
|
||||
currentProject: mockProject,
|
||||
theme: "dark",
|
||||
theme: 'dark',
|
||||
sidebarOpen: true,
|
||||
apiKeys: { anthropic: "", google: "" },
|
||||
apiKeys: { anthropic: '', google: '' },
|
||||
chatSessions: [],
|
||||
chatHistoryOpen: false,
|
||||
maxConcurrency: opts?.maxConcurrency ?? 3,
|
||||
@@ -155,7 +149,7 @@ export async function setupMockProjectWithFeatures(
|
||||
version: 2, // Must match app-store.ts persist version
|
||||
};
|
||||
|
||||
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
|
||||
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
|
||||
@@ -170,20 +164,14 @@ export async function setupMockProjectWithFeatures(
|
||||
export async function setupMockProjectWithContextFile(
|
||||
page: Page,
|
||||
featureId: string,
|
||||
contextContent: string = "# Agent Context\n\nPrevious implementation work..."
|
||||
contextContent: string = '# Agent Context\n\nPrevious implementation work...'
|
||||
): Promise<void> {
|
||||
await page.addInitScript(
|
||||
({
|
||||
featureId,
|
||||
contextContent,
|
||||
}: {
|
||||
featureId: string;
|
||||
contextContent: string;
|
||||
}) => {
|
||||
({ featureId, contextContent }: { featureId: string; contextContent: string }) => {
|
||||
const mockProject = {
|
||||
id: "test-project-1",
|
||||
name: "Test Project",
|
||||
path: "/mock/test-project",
|
||||
id: 'test-project-1',
|
||||
name: 'Test Project',
|
||||
path: '/mock/test-project',
|
||||
lastOpened: new Date().toISOString(),
|
||||
};
|
||||
|
||||
@@ -191,9 +179,9 @@ export async function setupMockProjectWithContextFile(
|
||||
state: {
|
||||
projects: [mockProject],
|
||||
currentProject: mockProject,
|
||||
theme: "dark",
|
||||
theme: 'dark',
|
||||
sidebarOpen: true,
|
||||
apiKeys: { anthropic: "", google: "" },
|
||||
apiKeys: { anthropic: '', google: '' },
|
||||
chatSessions: [],
|
||||
chatHistoryOpen: false,
|
||||
maxConcurrency: 3,
|
||||
@@ -201,7 +189,7 @@ export async function setupMockProjectWithContextFile(
|
||||
version: 2, // Must match app-store.ts persist version
|
||||
};
|
||||
|
||||
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
|
||||
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
|
||||
@@ -228,7 +216,7 @@ export async function setupMockProjectWithInProgressFeatures(
|
||||
id: string;
|
||||
category: string;
|
||||
description: string;
|
||||
status: "backlog" | "in_progress" | "verified";
|
||||
status: 'backlog' | 'in_progress' | 'verified';
|
||||
steps?: string[];
|
||||
startedAt?: string;
|
||||
}>;
|
||||
@@ -236,9 +224,9 @@ export async function setupMockProjectWithInProgressFeatures(
|
||||
): Promise<void> {
|
||||
await page.addInitScript((opts: typeof options) => {
|
||||
const mockProject = {
|
||||
id: "test-project-1",
|
||||
name: "Test Project",
|
||||
path: "/mock/test-project",
|
||||
id: 'test-project-1',
|
||||
name: 'Test Project',
|
||||
path: '/mock/test-project',
|
||||
lastOpened: new Date().toISOString(),
|
||||
};
|
||||
|
||||
@@ -248,9 +236,9 @@ export async function setupMockProjectWithInProgressFeatures(
|
||||
state: {
|
||||
projects: [mockProject],
|
||||
currentProject: mockProject,
|
||||
theme: "dark",
|
||||
theme: 'dark',
|
||||
sidebarOpen: true,
|
||||
apiKeys: { anthropic: "", google: "" },
|
||||
apiKeys: { anthropic: '', google: '' },
|
||||
chatSessions: [],
|
||||
chatHistoryOpen: false,
|
||||
maxConcurrency: opts?.maxConcurrency ?? 3,
|
||||
@@ -262,7 +250,7 @@ export async function setupMockProjectWithInProgressFeatures(
|
||||
version: 2, // Must match app-store.ts persist version
|
||||
};
|
||||
|
||||
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
|
||||
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
|
||||
@@ -273,15 +261,12 @@ export async function setupMockProjectWithInProgressFeatures(
|
||||
/**
|
||||
* Set up a mock project with a specific current view for route persistence testing
|
||||
*/
|
||||
export async function setupMockProjectWithView(
|
||||
page: Page,
|
||||
view: string
|
||||
): Promise<void> {
|
||||
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",
|
||||
id: 'test-project-1',
|
||||
name: 'Test Project',
|
||||
path: '/mock/test-project',
|
||||
lastOpened: new Date().toISOString(),
|
||||
};
|
||||
|
||||
@@ -290,9 +275,9 @@ export async function setupMockProjectWithView(
|
||||
projects: [mockProject],
|
||||
currentProject: mockProject,
|
||||
currentView: currentView,
|
||||
theme: "dark",
|
||||
theme: 'dark',
|
||||
sidebarOpen: true,
|
||||
apiKeys: { anthropic: "", google: "" },
|
||||
apiKeys: { anthropic: '', google: '' },
|
||||
chatSessions: [],
|
||||
chatHistoryOpen: false,
|
||||
maxConcurrency: 3,
|
||||
@@ -300,7 +285,7 @@ export async function setupMockProjectWithView(
|
||||
version: 2, // Must match app-store.ts persist version
|
||||
};
|
||||
|
||||
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
|
||||
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
|
||||
}, view);
|
||||
}
|
||||
|
||||
@@ -313,38 +298,36 @@ export async function setupEmptyLocalStorage(page: Page): Promise<void> {
|
||||
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: 2, // Must match app-store.ts persist version
|
||||
};
|
||||
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
|
||||
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> {
|
||||
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",
|
||||
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",
|
||||
id: 'test-project-2',
|
||||
name: 'Test Project 2',
|
||||
path: '/mock/test-project-2',
|
||||
lastOpened: new Date(Date.now() - 86400000).toISOString(), // 1 day ago
|
||||
},
|
||||
];
|
||||
@@ -353,10 +336,10 @@ export async function setupMockProjectsWithoutCurrent(
|
||||
state: {
|
||||
projects: mockProjects,
|
||||
currentProject: null,
|
||||
currentView: "welcome",
|
||||
theme: "dark",
|
||||
currentView: 'welcome',
|
||||
theme: 'dark',
|
||||
sidebarOpen: true,
|
||||
apiKeys: { anthropic: "", google: "" },
|
||||
apiKeys: { anthropic: '', google: '' },
|
||||
chatSessions: [],
|
||||
chatHistoryOpen: false,
|
||||
maxConcurrency: 3,
|
||||
@@ -364,7 +347,7 @@ export async function setupMockProjectsWithoutCurrent(
|
||||
version: 2, // Must match app-store.ts persist version
|
||||
};
|
||||
|
||||
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
|
||||
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -380,7 +363,7 @@ export async function setupMockProjectWithSkipTestsFeatures(
|
||||
id: string;
|
||||
category: string;
|
||||
description: string;
|
||||
status: "backlog" | "in_progress" | "verified";
|
||||
status: 'backlog' | 'in_progress' | 'verified';
|
||||
steps?: string[];
|
||||
startedAt?: string;
|
||||
skipTests?: boolean;
|
||||
@@ -389,9 +372,9 @@ export async function setupMockProjectWithSkipTestsFeatures(
|
||||
): Promise<void> {
|
||||
await page.addInitScript((opts: typeof options) => {
|
||||
const mockProject = {
|
||||
id: "test-project-1",
|
||||
name: "Test Project",
|
||||
path: "/mock/test-project",
|
||||
id: 'test-project-1',
|
||||
name: 'Test Project',
|
||||
path: '/mock/test-project',
|
||||
lastOpened: new Date().toISOString(),
|
||||
};
|
||||
|
||||
@@ -401,9 +384,9 @@ export async function setupMockProjectWithSkipTestsFeatures(
|
||||
state: {
|
||||
projects: [mockProject],
|
||||
currentProject: mockProject,
|
||||
theme: "dark",
|
||||
theme: 'dark',
|
||||
sidebarOpen: true,
|
||||
apiKeys: { anthropic: "", google: "" },
|
||||
apiKeys: { anthropic: '', google: '' },
|
||||
chatSessions: [],
|
||||
chatHistoryOpen: false,
|
||||
maxConcurrency: opts?.maxConcurrency ?? 3,
|
||||
@@ -415,7 +398,7 @@ export async function setupMockProjectWithSkipTestsFeatures(
|
||||
version: 2, // Must match app-store.ts persist version
|
||||
};
|
||||
|
||||
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
|
||||
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
|
||||
}, options);
|
||||
}
|
||||
|
||||
@@ -441,10 +424,10 @@ export async function setupMockMultipleProjects(
|
||||
state: {
|
||||
projects: mockProjects,
|
||||
currentProject: mockProjects[0],
|
||||
currentView: "board",
|
||||
theme: "dark",
|
||||
currentView: 'board',
|
||||
theme: 'dark',
|
||||
sidebarOpen: true,
|
||||
apiKeys: { anthropic: "", google: "" },
|
||||
apiKeys: { anthropic: '', google: '' },
|
||||
chatSessions: [],
|
||||
chatHistoryOpen: false,
|
||||
maxConcurrency: 3,
|
||||
@@ -452,7 +435,7 @@ export async function setupMockMultipleProjects(
|
||||
version: 2, // Must match app-store.ts persist version
|
||||
};
|
||||
|
||||
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
|
||||
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
|
||||
}, projectCount);
|
||||
}
|
||||
|
||||
@@ -465,17 +448,11 @@ export async function setupMockProjectWithAgentOutput(
|
||||
outputContent: string
|
||||
): Promise<void> {
|
||||
await page.addInitScript(
|
||||
({
|
||||
featureId,
|
||||
outputContent,
|
||||
}: {
|
||||
featureId: string;
|
||||
outputContent: string;
|
||||
}) => {
|
||||
({ featureId, outputContent }: { featureId: string; outputContent: string }) => {
|
||||
const mockProject = {
|
||||
id: "test-project-1",
|
||||
name: "Test Project",
|
||||
path: "/mock/test-project",
|
||||
id: 'test-project-1',
|
||||
name: 'Test Project',
|
||||
path: '/mock/test-project',
|
||||
lastOpened: new Date().toISOString(),
|
||||
};
|
||||
|
||||
@@ -483,9 +460,9 @@ export async function setupMockProjectWithAgentOutput(
|
||||
state: {
|
||||
projects: [mockProject],
|
||||
currentProject: mockProject,
|
||||
theme: "dark",
|
||||
theme: 'dark',
|
||||
sidebarOpen: true,
|
||||
apiKeys: { anthropic: "", google: "" },
|
||||
apiKeys: { anthropic: '', google: '' },
|
||||
chatSessions: [],
|
||||
chatHistoryOpen: false,
|
||||
maxConcurrency: 3,
|
||||
@@ -493,7 +470,7 @@ export async function setupMockProjectWithAgentOutput(
|
||||
version: 2, // Must match app-store.ts persist version
|
||||
};
|
||||
|
||||
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
|
||||
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
|
||||
|
||||
// Set up mock file system with output content for the feature
|
||||
// Now uses features/{id}/agent-output.md path
|
||||
@@ -519,7 +496,7 @@ export async function setupMockProjectWithWaitingApprovalFeatures(
|
||||
id: string;
|
||||
category: string;
|
||||
description: string;
|
||||
status: "backlog" | "in_progress" | "waiting_approval" | "verified";
|
||||
status: 'backlog' | 'in_progress' | 'waiting_approval' | 'verified';
|
||||
steps?: string[];
|
||||
startedAt?: string;
|
||||
skipTests?: boolean;
|
||||
@@ -528,9 +505,9 @@ export async function setupMockProjectWithWaitingApprovalFeatures(
|
||||
): Promise<void> {
|
||||
await page.addInitScript((opts: typeof options) => {
|
||||
const mockProject = {
|
||||
id: "test-project-1",
|
||||
name: "Test Project",
|
||||
path: "/mock/test-project",
|
||||
id: 'test-project-1',
|
||||
name: 'Test Project',
|
||||
path: '/mock/test-project',
|
||||
lastOpened: new Date().toISOString(),
|
||||
};
|
||||
|
||||
@@ -540,9 +517,9 @@ export async function setupMockProjectWithWaitingApprovalFeatures(
|
||||
state: {
|
||||
projects: [mockProject],
|
||||
currentProject: mockProject,
|
||||
theme: "dark",
|
||||
theme: 'dark',
|
||||
sidebarOpen: true,
|
||||
apiKeys: { anthropic: "", google: "" },
|
||||
apiKeys: { anthropic: '', google: '' },
|
||||
chatSessions: [],
|
||||
chatHistoryOpen: false,
|
||||
maxConcurrency: opts?.maxConcurrency ?? 3,
|
||||
@@ -554,7 +531,7 @@ export async function setupMockProjectWithWaitingApprovalFeatures(
|
||||
version: 2, // Must match app-store.ts persist version
|
||||
};
|
||||
|
||||
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
|
||||
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
|
||||
|
||||
// Also store features in a global variable that the mock electron API can use
|
||||
(window as any).__mockFeatures = mockFeatures;
|
||||
@@ -567,20 +544,20 @@ export async function setupMockProjectWithWaitingApprovalFeatures(
|
||||
export async function setupFirstRun(page: Page): Promise<void> {
|
||||
await page.addInitScript(() => {
|
||||
// Clear any existing setup state to simulate first run
|
||||
localStorage.removeItem("automaker-setup");
|
||||
localStorage.removeItem("automaker-storage");
|
||||
localStorage.removeItem('automaker-setup');
|
||||
localStorage.removeItem('automaker-storage');
|
||||
|
||||
// Set up the setup store state for first run
|
||||
const setupState = {
|
||||
state: {
|
||||
isFirstRun: true,
|
||||
setupComplete: false,
|
||||
currentStep: "welcome",
|
||||
currentStep: 'welcome',
|
||||
claudeCliStatus: null,
|
||||
claudeAuthStatus: null,
|
||||
claudeInstallProgress: {
|
||||
isInstalling: false,
|
||||
currentStep: "",
|
||||
currentStep: '',
|
||||
progress: 0,
|
||||
output: [],
|
||||
},
|
||||
@@ -589,28 +566,28 @@ export async function setupFirstRun(page: Page): Promise<void> {
|
||||
version: 2, // Must match app-store.ts persist version
|
||||
};
|
||||
|
||||
localStorage.setItem("automaker-setup", JSON.stringify(setupState));
|
||||
localStorage.setItem('automaker-setup', JSON.stringify(setupState));
|
||||
|
||||
// Also set up app store to show setup view
|
||||
const appState = {
|
||||
state: {
|
||||
projects: [],
|
||||
currentProject: null,
|
||||
theme: "dark",
|
||||
theme: 'dark',
|
||||
sidebarOpen: true,
|
||||
apiKeys: { anthropic: "", google: "" },
|
||||
apiKeys: { anthropic: '', google: '' },
|
||||
chatSessions: [],
|
||||
chatHistoryOpen: false,
|
||||
maxConcurrency: 3,
|
||||
isAutoModeRunning: false,
|
||||
runningAutoTasks: [],
|
||||
autoModeActivityLog: [],
|
||||
currentView: "setup",
|
||||
currentView: 'setup',
|
||||
},
|
||||
version: 2, // Must match app-store.ts persist version
|
||||
};
|
||||
|
||||
localStorage.setItem("automaker-storage", JSON.stringify(appState));
|
||||
localStorage.setItem('automaker-storage', JSON.stringify(appState));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -624,13 +601,13 @@ export async function setupComplete(page: Page): Promise<void> {
|
||||
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));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -647,45 +624,44 @@ export async function setupMockProjectWithProfiles(
|
||||
): Promise<void> {
|
||||
await page.addInitScript((opts: typeof options) => {
|
||||
const mockProject = {
|
||||
id: "test-project-1",
|
||||
name: "Test Project",
|
||||
path: "/mock/test-project",
|
||||
id: 'test-project-1',
|
||||
name: 'Test Project',
|
||||
path: '/mock/test-project',
|
||||
lastOpened: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Default built-in profiles (same as DEFAULT_AI_PROFILES from app-store.ts)
|
||||
const builtInProfiles = [
|
||||
{
|
||||
id: "profile-heavy-task",
|
||||
name: "Heavy Task",
|
||||
id: 'profile-heavy-task',
|
||||
name: 'Heavy Task',
|
||||
description:
|
||||
"Claude Opus with Ultrathink for complex architecture, migrations, or deep debugging.",
|
||||
model: "opus" as const,
|
||||
thinkingLevel: "ultrathink" as const,
|
||||
provider: "claude" as const,
|
||||
'Claude Opus with Ultrathink for complex architecture, migrations, or deep debugging.',
|
||||
model: 'opus' as const,
|
||||
thinkingLevel: 'ultrathink' as const,
|
||||
provider: 'claude' as const,
|
||||
isBuiltIn: true,
|
||||
icon: "Brain",
|
||||
icon: 'Brain',
|
||||
},
|
||||
{
|
||||
id: "profile-balanced",
|
||||
name: "Balanced",
|
||||
description:
|
||||
"Claude Sonnet with medium thinking for typical development tasks.",
|
||||
model: "sonnet" as const,
|
||||
thinkingLevel: "medium" as const,
|
||||
provider: "claude" as const,
|
||||
id: 'profile-balanced',
|
||||
name: 'Balanced',
|
||||
description: 'Claude Sonnet with medium thinking for typical development tasks.',
|
||||
model: 'sonnet' as const,
|
||||
thinkingLevel: 'medium' as const,
|
||||
provider: 'claude' as const,
|
||||
isBuiltIn: true,
|
||||
icon: "Scale",
|
||||
icon: 'Scale',
|
||||
},
|
||||
{
|
||||
id: "profile-quick-edit",
|
||||
name: "Quick Edit",
|
||||
description: "Claude Haiku for fast, simple edits and minor fixes.",
|
||||
model: "haiku" as const,
|
||||
thinkingLevel: "none" as const,
|
||||
provider: "claude" as const,
|
||||
id: 'profile-quick-edit',
|
||||
name: 'Quick Edit',
|
||||
description: 'Claude Haiku for fast, simple edits and minor fixes.',
|
||||
model: 'haiku' as const,
|
||||
thinkingLevel: 'none' as const,
|
||||
provider: 'claude' as const,
|
||||
isBuiltIn: true,
|
||||
icon: "Zap",
|
||||
icon: 'Zap',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -697,56 +673,51 @@ export async function setupMockProjectWithProfiles(
|
||||
id: `custom-profile-${i + 1}`,
|
||||
name: `Custom Profile ${i + 1}`,
|
||||
description: `Test custom profile ${i + 1}`,
|
||||
model: ["haiku", "sonnet", "opus"][i % 3] as
|
||||
| "haiku"
|
||||
| "sonnet"
|
||||
| "opus",
|
||||
thinkingLevel: ["none", "low", "medium", "high"][i % 4] as
|
||||
| "none"
|
||||
| "low"
|
||||
| "medium"
|
||||
| "high",
|
||||
provider: "claude" as const,
|
||||
model: ['haiku', 'sonnet', 'opus'][i % 3] as 'haiku' | 'sonnet' | 'opus',
|
||||
thinkingLevel: ['none', 'low', 'medium', 'high'][i % 4] as
|
||||
| 'none'
|
||||
| 'low'
|
||||
| 'medium'
|
||||
| 'high',
|
||||
provider: 'claude' as const,
|
||||
isBuiltIn: false,
|
||||
icon: ["Brain", "Zap", "Scale", "Cpu", "Rocket", "Sparkles"][i % 6],
|
||||
icon: ['Brain', 'Zap', 'Scale', 'Cpu', 'Rocket', 'Sparkles'][i % 6],
|
||||
});
|
||||
}
|
||||
|
||||
// Combine profiles (built-in first, then custom)
|
||||
const includeBuiltIn = opts?.includeBuiltIn !== false; // Default to true
|
||||
const aiProfiles = includeBuiltIn
|
||||
? [...builtInProfiles, ...customProfiles]
|
||||
: customProfiles;
|
||||
const aiProfiles = includeBuiltIn ? [...builtInProfiles, ...customProfiles] : customProfiles;
|
||||
|
||||
const mockState = {
|
||||
state: {
|
||||
projects: [mockProject],
|
||||
currentProject: mockProject,
|
||||
theme: "dark",
|
||||
theme: 'dark',
|
||||
sidebarOpen: true,
|
||||
apiKeys: { anthropic: "", google: "", openai: "" },
|
||||
apiKeys: { anthropic: '', google: '', openai: '' },
|
||||
chatSessions: [],
|
||||
chatHistoryOpen: false,
|
||||
maxConcurrency: 3,
|
||||
aiProfiles: aiProfiles,
|
||||
features: [],
|
||||
currentView: "board", // Start at board, will navigate to profiles
|
||||
currentView: 'board', // Start at board, will navigate to profiles
|
||||
},
|
||||
version: 2, // Must match app-store.ts persist version
|
||||
};
|
||||
|
||||
localStorage.setItem("automaker-storage", JSON.stringify(mockState));
|
||||
localStorage.setItem('automaker-storage', JSON.stringify(mockState));
|
||||
|
||||
// Also 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));
|
||||
}, options);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Page, Locator } from "@playwright/test";
|
||||
import { waitForElement } from "../core/waiting";
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
import { waitForElement } from '../core/waiting';
|
||||
|
||||
/**
|
||||
* Get the session list element
|
||||
@@ -26,20 +26,14 @@ export async function clickNewSessionButton(page: Page): Promise<void> {
|
||||
/**
|
||||
* Get a session item by its ID
|
||||
*/
|
||||
export async function getSessionItem(
|
||||
page: Page,
|
||||
sessionId: string
|
||||
): Promise<Locator> {
|
||||
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> {
|
||||
export async function clickArchiveSession(page: Page, sessionId: string): Promise<void> {
|
||||
const button = page.locator(`[data-testid="archive-session-${sessionId}"]`);
|
||||
await button.click();
|
||||
}
|
||||
@@ -47,9 +41,7 @@ export async function clickArchiveSession(
|
||||
/**
|
||||
* Check if the no session placeholder is visible
|
||||
*/
|
||||
export async function isNoSessionPlaceholderVisible(
|
||||
page: Page
|
||||
): Promise<boolean> {
|
||||
export async function isNoSessionPlaceholderVisible(page: Page): Promise<boolean> {
|
||||
const placeholder = page.locator('[data-testid="no-session-placeholder"]');
|
||||
return await placeholder.isVisible();
|
||||
}
|
||||
@@ -61,7 +53,7 @@ export async function waitForNoSessionPlaceholder(
|
||||
page: Page,
|
||||
options?: { timeout?: number }
|
||||
): Promise<Locator> {
|
||||
return await waitForElement(page, "no-session-placeholder", options);
|
||||
return await waitForElement(page, 'no-session-placeholder', options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,23 +68,18 @@ export async function isMessageListVisible(page: Page): Promise<boolean> {
|
||||
* 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-"]'
|
||||
);
|
||||
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> {
|
||||
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",
|
||||
state: 'visible',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { Page, Locator } from "@playwright/test";
|
||||
import { clickElement, fillInput } from "../core/interactions";
|
||||
import { waitForElement, waitForElementHidden } from "../core/waiting";
|
||||
import { getByTestId } from "../core/elements";
|
||||
import { navigateToView } from "../navigation/views";
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
import { clickElement, fillInput } from '../core/interactions';
|
||||
import { waitForElement, waitForElementHidden } from '../core/waiting';
|
||||
import { getByTestId } from '../core/elements';
|
||||
import { navigateToView } from '../navigation/views';
|
||||
|
||||
/**
|
||||
* Navigate to the profiles view
|
||||
*/
|
||||
export async function navigateToProfiles(page: Page): Promise<void> {
|
||||
// Click the profiles navigation button
|
||||
await navigateToView(page, "profiles");
|
||||
await navigateToView(page, 'profiles');
|
||||
|
||||
// Wait for profiles view to be visible
|
||||
await page.waitForSelector('[data-testid="profiles-view"]', {
|
||||
state: "visible",
|
||||
state: 'visible',
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
@@ -25,10 +25,7 @@ export async function navigateToProfiles(page: Page): Promise<void> {
|
||||
/**
|
||||
* Get a specific profile card by ID
|
||||
*/
|
||||
export async function getProfileCard(
|
||||
page: Page,
|
||||
profileId: string
|
||||
): Promise<Locator> {
|
||||
export async function getProfileCard(page: Page, profileId: string): Promise<Locator> {
|
||||
return getByTestId(page, `profile-card-${profileId}`);
|
||||
}
|
||||
|
||||
@@ -84,10 +81,10 @@ export async function getCustomProfileIds(page: Page): Promise<string[]> {
|
||||
const builtInText = card.locator('text="Built-in"');
|
||||
const isBuiltIn = (await builtInText.count()) > 0;
|
||||
if (!isBuiltIn) {
|
||||
const testId = await card.getAttribute("data-testid");
|
||||
const testId = await card.getAttribute('data-testid');
|
||||
if (testId) {
|
||||
// Extract ID from "profile-card-{id}"
|
||||
const profileId = testId.replace("profile-card-", "");
|
||||
const profileId = testId.replace('profile-card-', '');
|
||||
customIds.push(profileId);
|
||||
}
|
||||
}
|
||||
@@ -112,8 +109,8 @@ export async function getFirstCustomProfileId(page: Page): Promise<string | null
|
||||
* Click the "New Profile" button in the header
|
||||
*/
|
||||
export async function clickNewProfileButton(page: Page): Promise<void> {
|
||||
await clickElement(page, "add-profile-button");
|
||||
await waitForElement(page, "add-profile-dialog");
|
||||
await clickElement(page, 'add-profile-button');
|
||||
await waitForElement(page, 'add-profile-dialog');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,7 +121,7 @@ export async function clickEmptyState(page: Page): Promise<void> {
|
||||
'.group.rounded-xl.border.border-dashed[class*="cursor-pointer"]'
|
||||
);
|
||||
await emptyState.click();
|
||||
await waitForElement(page, "add-profile-dialog");
|
||||
await waitForElement(page, 'add-profile-dialog');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,10 +158,10 @@ export async function fillProfileForm(
|
||||
* Click the save button to create/update a profile
|
||||
*/
|
||||
export async function saveProfile(page: Page): Promise<void> {
|
||||
await clickElement(page, "save-profile-button");
|
||||
await clickElement(page, 'save-profile-button');
|
||||
// Wait for dialog to close
|
||||
await waitForElementHidden(page, "add-profile-dialog").catch(() => {});
|
||||
await waitForElementHidden(page, "edit-profile-dialog").catch(() => {});
|
||||
await waitForElementHidden(page, 'add-profile-dialog').catch(() => {});
|
||||
await waitForElementHidden(page, 'edit-profile-dialog').catch(() => {});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -175,46 +172,40 @@ export async function cancelProfileDialog(page: Page): Promise<void> {
|
||||
const cancelButton = page.locator('button:has-text("Cancel")');
|
||||
await cancelButton.click();
|
||||
// Wait for dialog to close
|
||||
await waitForElementHidden(page, "add-profile-dialog").catch(() => {});
|
||||
await waitForElementHidden(page, "edit-profile-dialog").catch(() => {});
|
||||
await waitForElementHidden(page, 'add-profile-dialog').catch(() => {});
|
||||
await waitForElementHidden(page, 'edit-profile-dialog').catch(() => {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Click the edit button for a specific profile
|
||||
*/
|
||||
export async function clickEditProfile(
|
||||
page: Page,
|
||||
profileId: string
|
||||
): Promise<void> {
|
||||
export async function clickEditProfile(page: Page, profileId: string): Promise<void> {
|
||||
await clickElement(page, `edit-profile-${profileId}`);
|
||||
await waitForElement(page, "edit-profile-dialog");
|
||||
await waitForElement(page, 'edit-profile-dialog');
|
||||
}
|
||||
|
||||
/**
|
||||
* Click the delete button for a specific profile
|
||||
*/
|
||||
export async function clickDeleteProfile(
|
||||
page: Page,
|
||||
profileId: string
|
||||
): Promise<void> {
|
||||
export async function clickDeleteProfile(page: Page, profileId: string): Promise<void> {
|
||||
await clickElement(page, `delete-profile-${profileId}`);
|
||||
await waitForElement(page, "delete-profile-confirm-dialog");
|
||||
await waitForElement(page, 'delete-profile-confirm-dialog');
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm profile deletion in the dialog
|
||||
*/
|
||||
export async function confirmDeleteProfile(page: Page): Promise<void> {
|
||||
await clickElement(page, "confirm-delete-profile-button");
|
||||
await waitForElementHidden(page, "delete-profile-confirm-dialog");
|
||||
await clickElement(page, 'confirm-delete-profile-button');
|
||||
await waitForElementHidden(page, 'delete-profile-confirm-dialog');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel profile deletion
|
||||
*/
|
||||
export async function cancelDeleteProfile(page: Page): Promise<void> {
|
||||
await clickElement(page, "cancel-delete-button");
|
||||
await waitForElementHidden(page, "delete-profile-confirm-dialog");
|
||||
await clickElement(page, 'cancel-delete-button');
|
||||
await waitForElementHidden(page, 'delete-profile-confirm-dialog');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -224,21 +215,15 @@ export async function cancelDeleteProfile(page: Page): Promise<void> {
|
||||
/**
|
||||
* Fill the profile name field
|
||||
*/
|
||||
export async function fillProfileName(
|
||||
page: Page,
|
||||
name: string
|
||||
): Promise<void> {
|
||||
await fillInput(page, "profile-name-input", name);
|
||||
export async function fillProfileName(page: Page, name: string): Promise<void> {
|
||||
await fillInput(page, 'profile-name-input', name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the profile description field
|
||||
*/
|
||||
export async function fillProfileDescription(
|
||||
page: Page,
|
||||
description: string
|
||||
): Promise<void> {
|
||||
await fillInput(page, "profile-description-input", description);
|
||||
export async function fillProfileDescription(page: Page, description: string): Promise<void> {
|
||||
await fillInput(page, 'profile-description-input', description);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -261,10 +246,7 @@ export async function selectModel(page: Page, modelId: string): Promise<void> {
|
||||
* Select a thinking level for the profile
|
||||
* @param level - Thinking level: none, low, medium, high, ultrathink
|
||||
*/
|
||||
export async function selectThinkingLevel(
|
||||
page: Page,
|
||||
level: string
|
||||
): Promise<void> {
|
||||
export async function selectThinkingLevel(page: Page, level: string): Promise<void> {
|
||||
await clickElement(page, `thinking-select-${level}`);
|
||||
}
|
||||
|
||||
@@ -273,11 +255,9 @@ export async function selectThinkingLevel(
|
||||
*/
|
||||
export async function getSelectedIcon(page: Page): Promise<string | null> {
|
||||
// Find the icon button with primary background
|
||||
const selectedIcon = page.locator(
|
||||
'[data-testid^="icon-select-"][class*="bg-primary"]'
|
||||
);
|
||||
const testId = await selectedIcon.getAttribute("data-testid");
|
||||
return testId ? testId.replace("icon-select-", "") : null;
|
||||
const selectedIcon = page.locator('[data-testid^="icon-select-"][class*="bg-primary"]');
|
||||
const testId = await selectedIcon.getAttribute('data-testid');
|
||||
return testId ? testId.replace('icon-select-', '') : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -285,25 +265,19 @@ export async function getSelectedIcon(page: Page): Promise<string | null> {
|
||||
*/
|
||||
export async function getSelectedModel(page: Page): Promise<string | null> {
|
||||
// Find the model button with primary background
|
||||
const selectedModel = page.locator(
|
||||
'[data-testid^="model-select-"][class*="bg-primary"]'
|
||||
);
|
||||
const testId = await selectedModel.getAttribute("data-testid");
|
||||
return testId ? testId.replace("model-select-", "") : null;
|
||||
const selectedModel = page.locator('[data-testid^="model-select-"][class*="bg-primary"]');
|
||||
const testId = await selectedModel.getAttribute('data-testid');
|
||||
return testId ? testId.replace('model-select-', '') : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently selected thinking level
|
||||
*/
|
||||
export async function getSelectedThinkingLevel(
|
||||
page: Page
|
||||
): Promise<string | null> {
|
||||
export async function getSelectedThinkingLevel(page: Page): Promise<string | null> {
|
||||
// Find the thinking level button with amber background
|
||||
const selectedLevel = page.locator(
|
||||
'[data-testid^="thinking-select-"][class*="bg-amber-500"]'
|
||||
);
|
||||
const testId = await selectedLevel.getAttribute("data-testid");
|
||||
return testId ? testId.replace("thinking-select-", "") : null;
|
||||
const selectedLevel = page.locator('[data-testid^="thinking-select-"][class*="bg-amber-500"]');
|
||||
const testId = await selectedLevel.getAttribute('data-testid');
|
||||
return testId ? testId.replace('thinking-select-', '') : null;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -314,7 +288,7 @@ export async function getSelectedThinkingLevel(
|
||||
* Check if the add profile dialog is open
|
||||
*/
|
||||
export async function isAddProfileDialogOpen(page: Page): Promise<boolean> {
|
||||
const dialog = await getByTestId(page, "add-profile-dialog");
|
||||
const dialog = await getByTestId(page, 'add-profile-dialog');
|
||||
return await dialog.isVisible().catch(() => false);
|
||||
}
|
||||
|
||||
@@ -322,7 +296,7 @@ export async function isAddProfileDialogOpen(page: Page): Promise<boolean> {
|
||||
* Check if the edit profile dialog is open
|
||||
*/
|
||||
export async function isEditProfileDialogOpen(page: Page): Promise<boolean> {
|
||||
const dialog = await getByTestId(page, "edit-profile-dialog");
|
||||
const dialog = await getByTestId(page, 'edit-profile-dialog');
|
||||
return await dialog.isVisible().catch(() => false);
|
||||
}
|
||||
|
||||
@@ -330,7 +304,7 @@ export async function isEditProfileDialogOpen(page: Page): Promise<boolean> {
|
||||
* Check if the delete confirmation dialog is open
|
||||
*/
|
||||
export async function isDeleteConfirmDialogOpen(page: Page): Promise<boolean> {
|
||||
const dialog = await getByTestId(page, "delete-profile-confirm-dialog");
|
||||
const dialog = await getByTestId(page, 'delete-profile-confirm-dialog');
|
||||
return await dialog.isVisible().catch(() => false);
|
||||
}
|
||||
|
||||
@@ -341,17 +315,15 @@ export async function isDeleteConfirmDialogOpen(page: Page): Promise<boolean> {
|
||||
export async function waitForDialogClose(page: Page): Promise<void> {
|
||||
// Wait for all profile dialogs to be hidden
|
||||
await Promise.all([
|
||||
waitForElementHidden(page, "add-profile-dialog").catch(() => {}),
|
||||
waitForElementHidden(page, "edit-profile-dialog").catch(() => {}),
|
||||
waitForElementHidden(page, "delete-profile-confirm-dialog").catch(
|
||||
() => {}
|
||||
),
|
||||
waitForElementHidden(page, 'add-profile-dialog').catch(() => {}),
|
||||
waitForElementHidden(page, 'edit-profile-dialog').catch(() => {}),
|
||||
waitForElementHidden(page, 'delete-profile-confirm-dialog').catch(() => {}),
|
||||
]);
|
||||
|
||||
// Also wait for any Radix dialog overlay to be removed (handles animation)
|
||||
await page
|
||||
.locator('[data-radix-dialog-overlay]')
|
||||
.waitFor({ state: "hidden", timeout: 2000 })
|
||||
.waitFor({ state: 'hidden', timeout: 2000 })
|
||||
.catch(() => {
|
||||
// Overlay may not exist
|
||||
});
|
||||
@@ -364,39 +336,30 @@ export async function waitForDialogClose(page: Page): Promise<void> {
|
||||
/**
|
||||
* Get the profile name from a card
|
||||
*/
|
||||
export async function getProfileName(
|
||||
page: Page,
|
||||
profileId: string
|
||||
): Promise<string> {
|
||||
export async function getProfileName(page: Page, profileId: string): Promise<string> {
|
||||
const card = await getProfileCard(page, profileId);
|
||||
const nameElement = card.locator("h3");
|
||||
return await nameElement.textContent().then((text) => text?.trim() || "");
|
||||
const nameElement = card.locator('h3');
|
||||
return await nameElement.textContent().then((text) => text?.trim() || '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the profile description from a card
|
||||
*/
|
||||
export async function getProfileDescription(
|
||||
page: Page,
|
||||
profileId: string
|
||||
): Promise<string> {
|
||||
export async function getProfileDescription(page: Page, profileId: string): Promise<string> {
|
||||
const card = await getProfileCard(page, profileId);
|
||||
const descElement = card.locator("p").first();
|
||||
return await descElement.textContent().then((text) => text?.trim() || "");
|
||||
const descElement = card.locator('p').first();
|
||||
return await descElement.textContent().then((text) => text?.trim() || '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the profile model badge text from a card
|
||||
*/
|
||||
export async function getProfileModel(
|
||||
page: Page,
|
||||
profileId: string
|
||||
): Promise<string> {
|
||||
export async function getProfileModel(page: Page, profileId: string): Promise<string> {
|
||||
const card = await getProfileCard(page, profileId);
|
||||
const modelBadge = card.locator(
|
||||
'span[class*="border-primary"]:has-text("haiku"), span[class*="border-primary"]:has-text("sonnet"), span[class*="border-primary"]:has-text("opus")'
|
||||
);
|
||||
return await modelBadge.textContent().then((text) => text?.trim() || "");
|
||||
return await modelBadge.textContent().then((text) => text?.trim() || '');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -410,16 +373,13 @@ export async function getProfileThinkingLevel(
|
||||
const thinkingBadge = card.locator('span[class*="border-amber-500"]');
|
||||
const isVisible = await thinkingBadge.isVisible().catch(() => false);
|
||||
if (!isVisible) return null;
|
||||
return await thinkingBadge.textContent().then((text) => text?.trim() || "");
|
||||
return await thinkingBadge.textContent().then((text) => text?.trim() || '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a profile has the built-in badge
|
||||
*/
|
||||
export async function isBuiltInProfile(
|
||||
page: Page,
|
||||
profileId: string
|
||||
): Promise<boolean> {
|
||||
export async function isBuiltInProfile(page: Page, profileId: string): Promise<boolean> {
|
||||
const card = await getProfileCard(page, profileId);
|
||||
const builtInBadge = card.locator('span:has-text("Built-in")');
|
||||
return await builtInBadge.isVisible().catch(() => false);
|
||||
@@ -428,17 +388,14 @@ export async function isBuiltInProfile(
|
||||
/**
|
||||
* Check if the edit button is visible for a profile
|
||||
*/
|
||||
export async function isEditButtonVisible(
|
||||
page: Page,
|
||||
profileId: string
|
||||
): Promise<boolean> {
|
||||
export async function isEditButtonVisible(page: Page, profileId: string): Promise<boolean> {
|
||||
const card = await getProfileCard(page, profileId);
|
||||
// Hover over card to make buttons visible
|
||||
await card.hover();
|
||||
const editButton = await getByTestId(page, `edit-profile-${profileId}`);
|
||||
// Wait for button to become visible after hover (handles CSS transition)
|
||||
try {
|
||||
await editButton.waitFor({ state: "visible", timeout: 2000 });
|
||||
await editButton.waitFor({ state: 'visible', timeout: 2000 });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
@@ -448,17 +405,14 @@ export async function isEditButtonVisible(
|
||||
/**
|
||||
* Check if the delete button is visible for a profile
|
||||
*/
|
||||
export async function isDeleteButtonVisible(
|
||||
page: Page,
|
||||
profileId: string
|
||||
): Promise<boolean> {
|
||||
export async function isDeleteButtonVisible(page: Page, profileId: string): Promise<boolean> {
|
||||
const card = await getProfileCard(page, profileId);
|
||||
// Hover over card to make buttons visible
|
||||
await card.hover();
|
||||
const deleteButton = await getByTestId(page, `delete-profile-${profileId}`);
|
||||
// Wait for button to become visible after hover (handles CSS transition)
|
||||
try {
|
||||
await deleteButton.waitFor({ state: "visible", timeout: 2000 });
|
||||
await deleteButton.waitFor({ state: 'visible', timeout: 2000 });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
@@ -480,11 +434,7 @@ export async function isDeleteButtonVisible(
|
||||
* @param fromIndex - 0-based index of the profile to drag
|
||||
* @param toIndex - 0-based index of the target position
|
||||
*/
|
||||
export async function dragProfile(
|
||||
page: Page,
|
||||
fromIndex: number,
|
||||
toIndex: number
|
||||
): Promise<void> {
|
||||
export async function dragProfile(page: Page, fromIndex: number, toIndex: number): Promise<void> {
|
||||
// Get all profile cards
|
||||
const cards = await page.locator('[data-testid^="profile-card-"]').all();
|
||||
|
||||
@@ -501,14 +451,14 @@ export async function dragProfile(
|
||||
const dragHandle = fromCard.locator('[data-testid^="profile-drag-handle-"]');
|
||||
|
||||
// Ensure drag handle is visible and ready
|
||||
await dragHandle.waitFor({ state: "visible", timeout: 5000 });
|
||||
await dragHandle.waitFor({ state: 'visible', timeout: 5000 });
|
||||
|
||||
// Get bounding boxes
|
||||
const handleBox = await dragHandle.boundingBox();
|
||||
const toBox = await toCard.boundingBox();
|
||||
|
||||
if (!handleBox || !toBox) {
|
||||
throw new Error("Unable to get bounding boxes for drag operation");
|
||||
throw new Error('Unable to get bounding boxes for drag operation');
|
||||
}
|
||||
|
||||
// Start position (center of drag handle)
|
||||
@@ -549,10 +499,10 @@ export async function getProfileOrder(page: Page): Promise<string[]> {
|
||||
const ids: string[] = [];
|
||||
|
||||
for (const card of cards) {
|
||||
const testId = await card.getAttribute("data-testid");
|
||||
const testId = await card.getAttribute('data-testid');
|
||||
if (testId) {
|
||||
// Extract profile ID from data-testid="profile-card-{id}"
|
||||
const profileId = testId.replace("profile-card-", "");
|
||||
const profileId = testId.replace('profile-card-', '');
|
||||
ids.push(profileId);
|
||||
}
|
||||
}
|
||||
@@ -568,5 +518,5 @@ export async function getProfileOrder(page: Page): Promise<string[]> {
|
||||
* Click the "Refresh Defaults" button
|
||||
*/
|
||||
export async function clickRefreshDefaults(page: Page): Promise<void> {
|
||||
await clickElement(page, "refresh-profiles-button");
|
||||
await clickElement(page, 'refresh-profiles-button');
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Page, Locator } from "@playwright/test";
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Get the settings view scrollable content area
|
||||
|
||||
Reference in New Issue
Block a user