mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 20:43:36 +00:00
refactor: remove AI profile functionality and related components
- Deleted the AI profile management feature, including all associated views, hooks, and types. - Updated settings and navigation components to remove references to AI profiles. - Adjusted local storage and settings synchronization logic to reflect the removal of AI profiles. - Cleaned up tests and utility functions that were dependent on the AI profile feature. These changes streamline the application by eliminating unused functionality, improving maintainability and reducing complexity.
This commit is contained in:
@@ -1,59 +0,0 @@
|
||||
/**
|
||||
* AI Profiles E2E Test
|
||||
*
|
||||
* Happy path: Create a new profile
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
import {
|
||||
setupMockProjectWithProfiles,
|
||||
waitForNetworkIdle,
|
||||
navigateToProfiles,
|
||||
clickNewProfileButton,
|
||||
fillProfileForm,
|
||||
saveProfile,
|
||||
waitForSuccessToast,
|
||||
countCustomProfiles,
|
||||
authenticateForTests,
|
||||
handleLoginScreenIfPresent,
|
||||
} from '../utils';
|
||||
|
||||
test.describe('AI Profiles', () => {
|
||||
test('should create a new profile', async ({ page }) => {
|
||||
await setupMockProjectWithProfiles(page, { customProfilesCount: 0 });
|
||||
await authenticateForTests(page);
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('load');
|
||||
await handleLoginScreenIfPresent(page);
|
||||
await waitForNetworkIdle(page);
|
||||
await navigateToProfiles(page);
|
||||
|
||||
// Get initial custom profile count (may be 0 or more due to server settings hydration)
|
||||
const initialCount = await countCustomProfiles(page);
|
||||
|
||||
await clickNewProfileButton(page);
|
||||
|
||||
await fillProfileForm(page, {
|
||||
name: 'Test Profile',
|
||||
description: 'A test profile',
|
||||
icon: 'Brain',
|
||||
model: 'sonnet',
|
||||
thinkingLevel: 'medium',
|
||||
});
|
||||
|
||||
await saveProfile(page);
|
||||
|
||||
await waitForSuccessToast(page, 'Profile created');
|
||||
|
||||
// Wait for the new profile to appear in the list (replaces arbitrary timeout)
|
||||
// The count should increase by 1 from the initial count
|
||||
await expect(async () => {
|
||||
const customCount = await countCustomProfiles(page);
|
||||
expect(customCount).toBe(initialCount + 1);
|
||||
}).toPass({ timeout: 5000 });
|
||||
|
||||
// Verify the count is correct (final assertion)
|
||||
const finalCount = await countCustomProfiles(page);
|
||||
expect(finalCount).toBe(initialCount + 1);
|
||||
});
|
||||
});
|
||||
@@ -79,7 +79,6 @@ export const TEST_IDS = {
|
||||
navSpec: 'nav-spec',
|
||||
navContext: 'nav-context',
|
||||
navAgent: 'nav-agent',
|
||||
navProfiles: 'nav-profiles',
|
||||
settingsButton: 'settings-button',
|
||||
openProjectButton: 'open-project-button',
|
||||
|
||||
@@ -88,7 +87,6 @@ export const TEST_IDS = {
|
||||
specView: 'spec-view',
|
||||
contextView: 'context-view',
|
||||
agentView: 'agent-view',
|
||||
profilesView: 'profiles-view',
|
||||
settingsView: 'settings-view',
|
||||
welcomeView: 'welcome-view',
|
||||
setupView: 'setup-view',
|
||||
@@ -108,18 +106,6 @@ export const TEST_IDS = {
|
||||
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',
|
||||
|
||||
// Context View
|
||||
contextFileList: 'context-file-list',
|
||||
addContextButton: 'add-context-button',
|
||||
|
||||
@@ -357,7 +357,6 @@ export async function setupProjectWithPath(page: Page, projectPath: string): Pro
|
||||
chatSessions: [],
|
||||
chatHistoryOpen: false,
|
||||
maxConcurrency: 3,
|
||||
aiProfiles: [],
|
||||
useWorktrees: true, // Enable worktree feature for tests
|
||||
currentWorktreeByProject: {
|
||||
[pathArg]: { path: null, branch: 'main' }, // Initialize to main branch
|
||||
@@ -414,7 +413,6 @@ export async function setupProjectWithPathNoWorktrees(
|
||||
chatSessions: [],
|
||||
chatHistoryOpen: false,
|
||||
maxConcurrency: 3,
|
||||
aiProfiles: [],
|
||||
useWorktrees: false, // Worktree feature DISABLED
|
||||
currentWorktreeByProject: {},
|
||||
worktreesByProject: {},
|
||||
@@ -470,7 +468,6 @@ export async function setupProjectWithStaleWorktree(
|
||||
chatSessions: [],
|
||||
chatHistoryOpen: false,
|
||||
maxConcurrency: 3,
|
||||
aiProfiles: [],
|
||||
useWorktrees: true, // Enable worktree feature for tests
|
||||
currentWorktreeByProject: {
|
||||
// This is STALE data - pointing to a worktree path that doesn't exist
|
||||
|
||||
@@ -26,7 +26,6 @@ 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';
|
||||
|
||||
@@ -829,127 +829,3 @@ export async function setupComplete(page: Page): Promise<void> {
|
||||
sessionStorage.setItem('automaker-splash-shown', 'true');
|
||||
}, STORE_VERSIONS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up a mock project with AI profiles for testing the profiles view
|
||||
* Includes default built-in profiles and optionally custom profiles
|
||||
*/
|
||||
export async function setupMockProjectWithProfiles(
|
||||
page: Page,
|
||||
options?: {
|
||||
customProfilesCount?: number;
|
||||
includeBuiltIn?: boolean;
|
||||
}
|
||||
): Promise<void> {
|
||||
await page.addInitScript((opts: typeof options) => {
|
||||
const mockProject = {
|
||||
id: 'test-project-1',
|
||||
name: 'Test Project',
|
||||
path: '/mock/test-project',
|
||||
lastOpened: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Default built-in profiles (same as DEFAULT_AI_PROFILES from app-store.ts)
|
||||
// Include all 4 default profiles to match the actual store initialization
|
||||
const builtInProfiles = [
|
||||
{
|
||||
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,
|
||||
isBuiltIn: true,
|
||||
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,
|
||||
isBuiltIn: true,
|
||||
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,
|
||||
isBuiltIn: true,
|
||||
icon: 'Zap',
|
||||
},
|
||||
{
|
||||
id: 'profile-cursor-refactoring',
|
||||
name: 'Cursor Refactoring',
|
||||
description: 'Cursor Composer 1 for refactoring tasks.',
|
||||
provider: 'cursor' as const,
|
||||
cursorModel: 'composer-1' as const,
|
||||
isBuiltIn: true,
|
||||
icon: 'Sparkles',
|
||||
},
|
||||
];
|
||||
|
||||
// Generate custom profiles if requested
|
||||
const customProfiles = [];
|
||||
const customCount = opts?.customProfilesCount ?? 0;
|
||||
for (let i = 0; i < customCount; i++) {
|
||||
customProfiles.push({
|
||||
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,
|
||||
isBuiltIn: false,
|
||||
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 mockState = {
|
||||
state: {
|
||||
projects: [mockProject],
|
||||
currentProject: mockProject,
|
||||
theme: 'dark',
|
||||
sidebarOpen: true,
|
||||
apiKeys: { anthropic: '', google: '', openai: '' },
|
||||
chatSessions: [],
|
||||
chatHistoryOpen: false,
|
||||
maxConcurrency: 3,
|
||||
aiProfiles: aiProfiles,
|
||||
features: [],
|
||||
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));
|
||||
|
||||
// Also mark setup as complete to skip the setup wizard
|
||||
const setupState = {
|
||||
state: {
|
||||
isFirstRun: false,
|
||||
setupComplete: true,
|
||||
currentStep: 'complete',
|
||||
skipClaudeSetup: false,
|
||||
},
|
||||
version: 0, // setup-store.ts doesn't specify a version, so zustand defaults to 0
|
||||
};
|
||||
localStorage.setItem('automaker-setup', JSON.stringify(setupState));
|
||||
|
||||
// Disable splash screen in tests
|
||||
sessionStorage.setItem('automaker-splash-shown', 'true');
|
||||
}, options);
|
||||
}
|
||||
|
||||
@@ -1,522 +0,0 @@
|
||||
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');
|
||||
|
||||
// Wait for profiles view to be visible
|
||||
await page.waitForSelector('[data-testid="profiles-view"]', {
|
||||
state: 'visible',
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Profile List Operations
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get a specific profile card by ID
|
||||
*/
|
||||
export async function getProfileCard(page: Page, profileId: string): Promise<Locator> {
|
||||
return getByTestId(page, `profile-card-${profileId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all profile cards (both built-in and custom)
|
||||
*/
|
||||
export async function getProfileCards(page: Page): Promise<Locator> {
|
||||
return page.locator('[data-testid^="profile-card-"]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get only custom profile cards
|
||||
*/
|
||||
export async function getCustomProfiles(page: Page): Promise<Locator> {
|
||||
// Custom profiles don't have the "Built-in" badge
|
||||
return page.locator('[data-testid^="profile-card-"]').filter({
|
||||
hasNot: page.locator('text="Built-in"'),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get only built-in profile cards
|
||||
*/
|
||||
export async function getBuiltInProfiles(page: Page): Promise<Locator> {
|
||||
// Built-in profiles have the lock icon and "Built-in" text
|
||||
return page.locator('[data-testid^="profile-card-"]:has-text("Built-in")');
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of custom profiles
|
||||
*/
|
||||
export async function countCustomProfiles(page: Page): Promise<number> {
|
||||
const customProfiles = await getCustomProfiles(page);
|
||||
return customProfiles.count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of built-in profiles
|
||||
*/
|
||||
export async function countBuiltInProfiles(page: Page): Promise<number> {
|
||||
const builtInProfiles = await getBuiltInProfiles(page);
|
||||
return await builtInProfiles.count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all custom profile IDs
|
||||
*/
|
||||
export async function getCustomProfileIds(page: Page): Promise<string[]> {
|
||||
const allCards = await page.locator('[data-testid^="profile-card-"]').all();
|
||||
const customIds: string[] = [];
|
||||
|
||||
for (const card of allCards) {
|
||||
const builtInText = card.locator('text="Built-in"');
|
||||
const isBuiltIn = (await builtInText.count()) > 0;
|
||||
if (!isBuiltIn) {
|
||||
const testId = await card.getAttribute('data-testid');
|
||||
if (testId) {
|
||||
// Extract ID from "profile-card-{id}"
|
||||
const profileId = testId.replace('profile-card-', '');
|
||||
customIds.push(profileId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return customIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first custom profile ID (useful after creating a profile)
|
||||
*/
|
||||
export async function getFirstCustomProfileId(page: Page): Promise<string | null> {
|
||||
const ids = await getCustomProfileIds(page);
|
||||
return ids.length > 0 ? ids[0] : null;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CRUD Operations
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 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');
|
||||
}
|
||||
|
||||
/**
|
||||
* Click the empty state card to create a new profile
|
||||
*/
|
||||
export async function clickEmptyState(page: Page): Promise<void> {
|
||||
const emptyState = page.locator(
|
||||
'.group.rounded-xl.border.border-dashed[class*="cursor-pointer"]'
|
||||
);
|
||||
await emptyState.click();
|
||||
await waitForElement(page, 'add-profile-dialog');
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the profile form with data
|
||||
*/
|
||||
export async function fillProfileForm(
|
||||
page: Page,
|
||||
data: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
model?: string;
|
||||
thinkingLevel?: string;
|
||||
}
|
||||
): Promise<void> {
|
||||
if (data.name !== undefined) {
|
||||
await fillProfileName(page, data.name);
|
||||
}
|
||||
if (data.description !== undefined) {
|
||||
await fillProfileDescription(page, data.description);
|
||||
}
|
||||
if (data.icon !== undefined) {
|
||||
await selectIcon(page, data.icon);
|
||||
}
|
||||
if (data.model !== undefined) {
|
||||
await selectModel(page, data.model);
|
||||
}
|
||||
if (data.thinkingLevel !== undefined) {
|
||||
await selectThinkingLevel(page, data.thinkingLevel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Click the save button to create/update a profile
|
||||
*/
|
||||
export async function saveProfile(page: Page): Promise<void> {
|
||||
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(() => {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Click the cancel button in the profile dialog
|
||||
*/
|
||||
export async function cancelProfileDialog(page: Page): Promise<void> {
|
||||
// Look for cancel button in dialog footer
|
||||
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(() => {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Click the edit button for a specific profile
|
||||
*/
|
||||
export async function clickEditProfile(page: Page, profileId: string): Promise<void> {
|
||||
await clickElement(page, `edit-profile-${profileId}`);
|
||||
await waitForElement(page, 'edit-profile-dialog');
|
||||
}
|
||||
|
||||
/**
|
||||
* Click the delete button for a specific profile
|
||||
*/
|
||||
export async function clickDeleteProfile(page: Page, profileId: string): Promise<void> {
|
||||
await clickElement(page, `delete-profile-${profileId}`);
|
||||
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');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel profile deletion
|
||||
*/
|
||||
export async function cancelDeleteProfile(page: Page): Promise<void> {
|
||||
await clickElement(page, 'cancel-delete-button');
|
||||
await waitForElementHidden(page, 'delete-profile-confirm-dialog');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Form Field Operations
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Fill the profile name field
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select an icon for the profile
|
||||
* @param iconName - Name of the icon: Brain, Zap, Scale, Cpu, Rocket, Sparkles
|
||||
*/
|
||||
export async function selectIcon(page: Page, iconName: string): Promise<void> {
|
||||
await clickElement(page, `icon-select-${iconName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a model for the profile
|
||||
* @param modelId - Model ID: haiku, sonnet, opus
|
||||
*/
|
||||
export async function selectModel(page: Page, modelId: string): Promise<void> {
|
||||
await clickElement(page, `model-select-${modelId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
await clickElement(page, `thinking-select-${level}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently selected icon
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently selected model
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently selected thinking level
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Dialog Operations
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Check if the add profile dialog is open
|
||||
*/
|
||||
export async function isAddProfileDialogOpen(page: Page): Promise<boolean> {
|
||||
const dialog = await getByTestId(page, 'add-profile-dialog');
|
||||
return await dialog.isVisible().catch(() => false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the edit profile dialog is open
|
||||
*/
|
||||
export async function isEditProfileDialogOpen(page: Page): Promise<boolean> {
|
||||
const dialog = await getByTestId(page, 'edit-profile-dialog');
|
||||
return await dialog.isVisible().catch(() => false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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');
|
||||
return await dialog.isVisible().catch(() => false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for any profile dialog to close
|
||||
* This ensures all dialog animations complete before proceeding
|
||||
*/
|
||||
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(() => {}),
|
||||
]);
|
||||
|
||||
// Also wait for any Radix dialog overlay to be removed (handles animation)
|
||||
await page
|
||||
.locator('[data-radix-dialog-overlay]')
|
||||
.waitFor({ state: 'hidden', timeout: 2000 })
|
||||
.catch(() => {
|
||||
// Overlay may not exist
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Profile Card Inspection
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get the profile name from a card
|
||||
*/
|
||||
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() || '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the profile description from a card
|
||||
*/
|
||||
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() || '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the profile model badge text from a card
|
||||
*/
|
||||
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() || '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the profile thinking level badge text from a card
|
||||
*/
|
||||
export async function getProfileThinkingLevel(
|
||||
page: Page,
|
||||
profileId: string
|
||||
): Promise<string | null> {
|
||||
const card = await getProfileCard(page, profileId);
|
||||
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() || '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a profile has the built-in badge
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the edit button is visible for a profile
|
||||
*/
|
||||
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 });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the delete button is visible for a profile
|
||||
*/
|
||||
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 });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Drag & Drop
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Drag a profile from one position to another
|
||||
* Uses the drag handle and dnd-kit library pattern
|
||||
*
|
||||
* Note: dnd-kit requires pointer events with specific timing for drag recognition.
|
||||
* Manual mouse operations are needed because Playwright's dragTo doesn't work
|
||||
* reliably with dnd-kit's pointer-based drag detection.
|
||||
*
|
||||
* @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> {
|
||||
// Get all profile cards
|
||||
const cards = await page.locator('[data-testid^="profile-card-"]').all();
|
||||
|
||||
if (fromIndex >= cards.length || toIndex >= cards.length) {
|
||||
throw new Error(
|
||||
`Invalid drag indices: fromIndex=${fromIndex}, toIndex=${toIndex}, total=${cards.length}`
|
||||
);
|
||||
}
|
||||
|
||||
const fromCard = cards[fromIndex];
|
||||
const toCard = cards[toIndex];
|
||||
|
||||
// Get the drag handle within the source card
|
||||
const dragHandle = fromCard.locator('[data-testid^="profile-drag-handle-"]');
|
||||
|
||||
// Ensure drag handle is visible and ready
|
||||
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');
|
||||
}
|
||||
|
||||
// Start position (center of drag handle)
|
||||
const startX = handleBox.x + handleBox.width / 2;
|
||||
const startY = handleBox.y + handleBox.height / 2;
|
||||
|
||||
// End position (center of target card)
|
||||
const endX = toBox.x + toBox.width / 2;
|
||||
const endY = toBox.y + toBox.height / 2;
|
||||
|
||||
// Perform manual drag operation
|
||||
// dnd-kit needs pointer events in a specific sequence
|
||||
await page.mouse.move(startX, startY);
|
||||
await page.mouse.down();
|
||||
|
||||
// dnd-kit requires a brief hold before recognizing the drag gesture
|
||||
// This is a library requirement, not an arbitrary timeout
|
||||
await page.waitForTimeout(150);
|
||||
|
||||
// Move to target in steps for smoother drag recognition
|
||||
await page.mouse.move(endX, endY, { steps: 10 });
|
||||
|
||||
// Brief pause before drop
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
await page.mouse.up();
|
||||
|
||||
// Wait for reorder animation to complete
|
||||
await page.waitForTimeout(200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current order of all profile IDs
|
||||
* Returns array of profile IDs in display order
|
||||
*/
|
||||
export async function getProfileOrder(page: Page): Promise<string[]> {
|
||||
const cards = await page.locator('[data-testid^="profile-card-"]').all();
|
||||
const ids: string[] = [];
|
||||
|
||||
for (const card of cards) {
|
||||
const testId = await card.getAttribute('data-testid');
|
||||
if (testId) {
|
||||
// Extract profile ID from data-testid="profile-card-{id}"
|
||||
const profileId = testId.replace('profile-card-', '');
|
||||
ids.push(profileId);
|
||||
}
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Header Actions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Click the "Refresh Defaults" button
|
||||
*/
|
||||
export async function clickRefreshDefaults(page: Page): Promise<void> {
|
||||
await clickElement(page, 'refresh-profiles-button');
|
||||
}
|
||||
Reference in New Issue
Block a user