import { Page, Locator } from '@playwright/test'; /** * Get a kanban card by feature ID */ export async function getKanbanCard(page: Page, featureId: string): Promise { return page.locator(`[data-testid="kanban-card-${featureId}"]`); } /** * Get a kanban column by its ID */ export async function getKanbanColumn(page: Page, columnId: string): Promise { return page.locator(`[data-testid="kanban-column-${columnId}"]`); } /** * Get the width of a kanban column */ export async function getKanbanColumnWidth(page: Page, columnId: string): Promise { const column = page.locator(`[data-testid="kanban-column-${columnId}"]`); const box = await column.boundingBox(); return box?.width ?? 0; } /** * Check if a kanban column has CSS columns (masonry) layout */ export async function hasKanbanColumnMasonryLayout(page: Page, columnId: string): Promise { const column = page.locator(`[data-testid="kanban-column-${columnId}"]`); const contentDiv = column.locator('> div').nth(1); // Second child is the content area const columnCount = await contentDiv.evaluate((el) => { const style = window.getComputedStyle(el); return style.columnCount; }); return columnCount === '2'; } /** * Drag a kanban card from one column to another */ export async function dragKanbanCard( page: Page, featureId: string, targetColumnId: string ): Promise { const dragHandle = page.locator(`[data-testid="drag-handle-${featureId}"]`); const targetColumn = page.locator(`[data-testid="kanban-column-${targetColumnId}"]`); // Perform drag and drop await dragHandle.dragTo(targetColumn); } /** * Click the view output button on a kanban card */ export async function clickViewOutput(page: Page, featureId: string): Promise { // Try the running version first, then the in-progress version const runningBtn = page.locator(`[data-testid="view-output-${featureId}"]`); const inProgressBtn = page.locator(`[data-testid="view-output-inprogress-${featureId}"]`); if (await runningBtn.isVisible()) { await runningBtn.click(); } else if (await inProgressBtn.isVisible()) { await inProgressBtn.click(); } else { throw new Error(`View output button not found for feature ${featureId}`); } } /** * Check if the drag handle is visible for a specific feature card */ export async function isDragHandleVisibleForFeature( page: Page, featureId: string ): Promise { const dragHandle = page.locator(`[data-testid="drag-handle-${featureId}"]`); return await dragHandle.isVisible().catch(() => false); } /** * Get the drag handle element for a specific feature card */ export async function getDragHandleForFeature(page: Page, featureId: string): Promise { return page.locator(`[data-testid="drag-handle-${featureId}"]`); } // ============================================================================ // Add Feature Dialog // ============================================================================ /** * Click the add feature button */ export async function clickAddFeature(page: Page): Promise { await page.click('[data-testid="add-feature-button"]'); await page.waitForSelector('[data-testid="add-feature-dialog"]', { timeout: 5000, }); } /** * Fill in the add feature dialog */ export async function fillAddFeatureDialog( page: Page, description: string, options?: { branch?: string; category?: string } ): Promise { // Fill description (using the dropzone textarea) const descriptionInput = page.locator('[data-testid="add-feature-dialog"] textarea').first(); await descriptionInput.fill(description); // Fill branch if provided (it's a combobox autocomplete) if (options?.branch) { // First, select "Other branch" radio option if not already selected const otherBranchRadio = page .locator('[data-testid="feature-radio-group"]') .locator('[id="feature-other"]'); await otherBranchRadio.waitFor({ state: 'visible', timeout: 5000 }); await otherBranchRadio.click(); // Wait for the branch input to appear await page.waitForTimeout(300); // Now click on the branch input (autocomplete) const branchInput = page.locator('[data-testid="feature-input"]'); await branchInput.waitFor({ state: 'visible', timeout: 5000 }); await branchInput.click(); // Wait for the popover to open await page.waitForTimeout(300); // Type in the command input const commandInput = page.locator('[cmdk-input]'); await commandInput.fill(options.branch); // Press Enter to select/create the branch await commandInput.press('Enter'); // Wait for popover to close await page.waitForTimeout(200); } // Fill category if provided (it's also a combobox autocomplete) if (options?.category) { const categoryButton = page.locator('[data-testid="feature-category-input"]'); await categoryButton.click(); await page.waitForTimeout(300); const commandInput = page.locator('[cmdk-input]'); await commandInput.fill(options.category); await commandInput.press('Enter'); await page.waitForTimeout(200); } } /** * Confirm the add feature dialog */ export async function confirmAddFeature(page: Page): Promise { await page.click('[data-testid="confirm-add-feature"]'); // Wait for dialog to close await page.waitForFunction(() => !document.querySelector('[data-testid="add-feature-dialog"]'), { timeout: 5000, }); } /** * Add a feature with all steps in one call */ export async function addFeature( page: Page, description: string, options?: { branch?: string; category?: string } ): Promise { await clickAddFeature(page); await fillAddFeatureDialog(page, description, options); await confirmAddFeature(page); } // ============================================================================ // Worktree Selector // ============================================================================ /** * Get the worktree selector element */ export async function getWorktreeSelector(page: Page): Promise { return page.locator('[data-testid="worktree-selector"]'); } /** * Click on a branch button in the worktree selector */ export async function selectWorktreeBranch(page: Page, branchName: string): Promise { const branchButton = page.getByRole('button', { name: new RegExp(branchName, 'i'), }); await branchButton.click(); await page.waitForTimeout(500); // Wait for UI to update } /** * Get the currently selected branch in the worktree selector */ export async function getSelectedWorktreeBranch(page: Page): Promise { // The main branch button has aria-pressed="true" when selected const selectedButton = page.locator( '[data-testid="worktree-selector"] button[aria-pressed="true"]' ); const text = await selectedButton.textContent().catch(() => null); return text?.trim() || null; } /** * Check if a branch button is visible in the worktree selector */ export async function isWorktreeBranchVisible(page: Page, branchName: string): Promise { const branchButton = page.getByRole('button', { name: new RegExp(branchName, 'i'), }); return await branchButton.isVisible().catch(() => false); }