Files
automaker/apps/ui/tests/utils/core/interactions.ts
gsxdsm 7df2182818 Improve pull request flow, add branch selection for worktree creation, fix auto-mode concurrency count (#787)
* Changes from fix/fetch-before-pull-fetch

* feat: Improve pull request flow, add branch selection for worktree creation, fix for automode concurrency count

* feat: Add validation for remote names and improve error handling

* Address PR comments and mobile layout fixes

* ```
refactor: Extract PR target resolution logic into dedicated service
```

* feat: Add app shell UI and improve service imports. Address PR comments

* fix: Improve security validation and cache handling in git operations

* feat: Add GET /list endpoint and improve parameter handling

* chore: Improve validation, accessibility, and error handling across apps

* chore: Format vite server port configuration

* fix: Add error handling for gh pr list command and improve offline fallbacks

* fix: Preserve existing PR creation time and improve remote handling
2026-02-19 21:55:12 -08:00

176 lines
6.0 KiB
TypeScript

import { Page, expect } from '@playwright/test';
import { getByTestId, getButtonByText } from './elements';
import { waitForSplashScreenToDisappear } from './waiting';
/**
* 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';
}
/**
* Press the platform-specific modifier + a key (e.g., Cmd+Enter or Ctrl+Enter)
*/
export async function pressModifierEnter(page: Page): Promise<void> {
const modifier = getPlatformModifier();
await page.keyboard.press(`${modifier}+Enter`);
}
/**
* Click an element by its data-testid attribute
* Waits for the element to be visible before clicking to avoid flaky tests
*/
export async function clickElement(page: Page, testId: string): Promise<void> {
// Wait for splash screen to disappear first (safety net)
await waitForSplashScreenToDisappear(page, 5000);
const element = page.locator(`[data-testid="${testId}"]`);
// Wait for element to be visible and stable before clicking
await element.waitFor({ state: 'visible', timeout: 10000 });
await element.click();
}
/**
* Click a button by its text content
*/
export async function clickButtonByText(page: Page, text: string): Promise<void> {
const button = await getButtonByText(page, text);
await button.click();
}
/**
* Fill an input field by its data-testid attribute
*/
export async function fillInput(page: Page, testId: string, value: string): Promise<void> {
const input = await getByTestId(page, testId);
await input.fill(value);
}
/**
* Press a keyboard shortcut key
*/
export async function pressShortcut(page: Page, key: string): Promise<void> {
await page.keyboard.press(key);
}
/**
* Navigate to a URL with authentication
* This wrapper ensures authentication happens before navigation
*/
export async function gotoWithAuth(page: Page, url: string): Promise<void> {
const { authenticateForTests } = await import('../api/client');
await authenticateForTests(page);
await page.goto(url);
}
/** Selector matching any top-level app view by data-testid, used to detect that the app has loaded. */
const APP_CONTENT_SELECTOR =
'[data-testid="welcome-view"], [data-testid="dashboard-view"], [data-testid="board-view"], [data-testid="context-view"], [data-testid="agent-view"], [data-testid="overview-view"]';
/**
* Handle login screen if it appears after navigation
* Returns true if login was handled, false if no login screen was found
*/
export async function handleLoginScreenIfPresent(page: Page): Promise<boolean> {
// Check for login screen by waiting for either login input or app-container to be visible
// Use data-testid selector (preferred) with fallback to the old selector
const loginInput = page
.locator('[data-testid="login-api-key-input"], input[type="password"][placeholder*="API key"]')
.first();
const appContent = page.locator(APP_CONTENT_SELECTOR);
const loggedOutPage = page.getByRole('heading', { name: /logged out/i });
const goToLoginButton = page.locator('button:has-text("Go to login")');
const maxWaitMs = 15000;
// Race between login screen, logged-out page, a delayed redirect to /login, and actual content
const result = await Promise.race([
page
.waitForURL((url) => url.pathname.includes('/login'), { timeout: maxWaitMs })
.then(() => 'login-redirect' as const)
.catch(() => null),
loginInput
.waitFor({ state: 'visible', timeout: maxWaitMs })
.then(() => 'login-input' as const)
.catch(() => null),
loggedOutPage
.waitFor({ state: 'visible', timeout: maxWaitMs })
.then(() => 'logged-out' as const)
.catch(() => null),
appContent
.first()
.waitFor({ state: 'visible', timeout: maxWaitMs })
.then(() => 'app-content' as const)
.catch(() => null),
]);
// Handle logged-out page - click "Go to login" button and then login
if (result === 'logged-out') {
await goToLoginButton.click();
await page.waitForLoadState('load');
// Now handle the login screen
return handleLoginScreenIfPresent(page);
}
const loginVisible = result === 'login-redirect' || result === 'login-input';
if (loginVisible) {
const apiKey = process.env.AUTOMAKER_API_KEY || 'test-api-key-for-e2e-tests';
await loginInput.fill(apiKey);
// Wait a moment for the button to become enabled
await page.waitForTimeout(100);
// Wait for button to be enabled (it's disabled when input is empty)
const loginButton = page
.locator('[data-testid="login-submit-button"], button:has-text("Login")')
.first();
await expect(loginButton).toBeEnabled({ timeout: 5000 });
await loginButton.click();
// Wait for navigation away from login - either to content or URL change
await Promise.race([
page.waitForURL((url) => !url.pathname.includes('/login'), { timeout: 15000 }),
appContent.first().waitFor({ state: 'visible', timeout: 15000 }),
]).catch(() => {});
// Wait for page to load
await page.waitForLoadState('load');
return true;
}
return false;
}
/**
* Press a number key (0-9) on the keyboard
*/
export async function pressNumberKey(page: Page, num: number): Promise<void> {
await page.keyboard.press(num.toString());
}
/**
* Focus on an input element to test that shortcuts don't fire when typing
*/
export async function focusOnInput(page: Page, testId: string): Promise<void> {
const input = page.locator(`[data-testid="${testId}"]`);
await input.focus();
}
/**
* Close any open dialog by pressing Escape
* 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');
// Wait for any dialog overlay to disappear
await page
.locator('[data-radix-dialog-overlay], [role="dialog"]')
.waitFor({ state: 'hidden', timeout: 5000 })
.catch(() => {
// Dialog may have already closed or not exist
});
}