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 { // 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 { return getByTestId(page, `profile-card-${profileId}`); } /** * Get all profile cards (both built-in and custom) */ export async function getProfileCards(page: Page): Promise { return page.locator('[data-testid^="profile-card-"]'); } /** * Get only custom profile cards */ export async function getCustomProfiles(page: Page): Promise { // 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 { // 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 { const customProfiles = await getCustomProfiles(page); return customProfiles.count(); } /** * Count the number of built-in profiles */ export async function countBuiltInProfiles(page: Page): Promise { const builtInProfiles = await getBuiltInProfiles(page); return await builtInProfiles.count(); } /** * Get all custom profile IDs */ export async function getCustomProfileIds(page: Page): Promise { 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 { 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 { 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 { 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 { 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 { 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 { // 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 { 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 { 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 { await clickElement(page, "confirm-delete-profile-button"); await waitForElementHidden(page, "delete-profile-confirm-dialog"); } /** * Cancel profile deletion */ export async function cancelDeleteProfile(page: Page): Promise { 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 { await fillInput(page, "profile-name-input", name); } /** * Fill the profile description field */ export async function fillProfileDescription( page: Page, description: string ): Promise { 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 { 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 { 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 { await clickElement(page, `thinking-select-${level}`); } /** * Get the currently selected icon */ export async function getSelectedIcon(page: Page): Promise { // 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 { // 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 { // 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 { 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 { 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 { 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 { // 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { // 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 { 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 { await clickElement(page, "refresh-profiles-button"); }