mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-19 10:43:08 +00:00
* Changes from fix/memory-and-context-mobile-friendly * fix: Improve file extension detection and add path traversal protection * refactor: Extract file extension utilities and add path traversal guards Code review improvements: - Extract isMarkdownFilename and isImageFilename to shared image-utils.ts - Remove duplicated code from context-view.tsx and memory-view.tsx - Add path traversal guard for context fixture utilities (matching memory) - Add 7 new tests for context fixture path traversal protection - Total 61 tests pass Addresses code review feedback from PR #813 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: Add e2e tests for profiles crud and board background persistence * Update apps/ui/playwright.config.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * fix: Add robust test navigation handling and file filtering * fix: Format NODE_OPTIONS configuration on single line * test: Update profiles and board background persistence tests * test: Replace iPhone 13 Pro with Pixel 5 for mobile test consistency --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
222 lines
7.9 KiB
TypeScript
222 lines
7.9 KiB
TypeScript
import { Page } from '@playwright/test';
|
|
import { clickElement } from '../core/interactions';
|
|
import { handleLoginScreenIfPresent } from '../core/interactions';
|
|
import { waitForElement, waitForSplashScreenToDisappear } from '../core/waiting';
|
|
import { authenticateForTests } from '../api/client';
|
|
|
|
/**
|
|
* Navigate to the board/kanban view
|
|
* Note: Navigates directly to /board since index route shows WelcomeView
|
|
*/
|
|
export async function navigateToBoard(page: Page): Promise<void> {
|
|
// Authenticate before navigating
|
|
await authenticateForTests(page);
|
|
|
|
// Wait for any pending navigation to complete before starting a new one
|
|
await page.waitForLoadState('domcontentloaded').catch(() => {});
|
|
await page.waitForTimeout(100);
|
|
|
|
// Navigate directly to /board route
|
|
await page.goto('/board', { waitUntil: 'domcontentloaded' });
|
|
|
|
// Wait for splash screen to disappear (safety net)
|
|
await waitForSplashScreenToDisappear(page, 3000);
|
|
|
|
// Handle login redirect if needed
|
|
await handleLoginScreenIfPresent(page);
|
|
|
|
// Wait for the board view to be visible
|
|
await waitForElement(page, 'board-view', { timeout: 10000 });
|
|
}
|
|
|
|
/**
|
|
* Navigate to the context view
|
|
* Note: Navigates directly to /context since index route shows WelcomeView
|
|
*/
|
|
export async function navigateToContext(page: Page): Promise<void> {
|
|
// Authenticate before navigating
|
|
await authenticateForTests(page);
|
|
|
|
// Wait for any pending navigation to complete before starting a new one
|
|
// This prevents race conditions, especially on mobile viewports
|
|
await page.waitForLoadState('domcontentloaded').catch(() => {});
|
|
await page.waitForTimeout(100);
|
|
|
|
// Navigate directly to /context route
|
|
await page.goto('/context', { waitUntil: 'domcontentloaded' });
|
|
|
|
// Wait for splash screen to disappear (safety net)
|
|
await waitForSplashScreenToDisappear(page, 3000);
|
|
|
|
// Handle login redirect if needed
|
|
await handleLoginScreenIfPresent(page);
|
|
|
|
// Wait for loading to complete (if present)
|
|
const loadingElement = page.locator('[data-testid="context-view-loading"]');
|
|
try {
|
|
const loadingVisible = await loadingElement.isVisible({ timeout: 2000 });
|
|
if (loadingVisible) {
|
|
// Wait for loading to disappear (context view will appear)
|
|
await loadingElement.waitFor({ state: 'hidden', timeout: 10000 });
|
|
}
|
|
} catch {
|
|
// Loading element not found or already hidden, continue
|
|
}
|
|
|
|
// Wait for the context view to be visible
|
|
// Increase timeout to handle slower server startup
|
|
await waitForElement(page, 'context-view', { timeout: 15000 });
|
|
|
|
// On mobile, close the sidebar if open so the header actions trigger is clickable (not covered by backdrop)
|
|
// Use JavaScript click to avoid force:true hitting the sidebar (z-30) instead of the backdrop (z-20)
|
|
const backdrop = page.locator('[data-testid="sidebar-backdrop"]');
|
|
if (await backdrop.isVisible().catch(() => false)) {
|
|
await backdrop.evaluate((el) => (el as HTMLElement).click());
|
|
await page.waitForTimeout(200);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Navigate to the spec view
|
|
* Note: Navigates directly to /spec since index route shows WelcomeView
|
|
*/
|
|
export async function navigateToSpec(page: Page): Promise<void> {
|
|
// Authenticate before navigating
|
|
await authenticateForTests(page);
|
|
|
|
// Wait for any pending navigation to complete before starting a new one
|
|
await page.waitForLoadState('domcontentloaded').catch(() => {});
|
|
await page.waitForTimeout(100);
|
|
|
|
// Navigate directly to /spec route
|
|
await page.goto('/spec', { waitUntil: 'domcontentloaded' });
|
|
|
|
// Wait for splash screen to disappear (safety net)
|
|
await waitForSplashScreenToDisappear(page, 3000);
|
|
|
|
// Wait for loading state to complete first (if present)
|
|
const loadingElement = page.locator('[data-testid="spec-view-loading"]');
|
|
try {
|
|
const loadingVisible = await loadingElement.isVisible({ timeout: 2000 });
|
|
if (loadingVisible) {
|
|
// Wait for loading to disappear (spec view or empty state will appear)
|
|
await loadingElement.waitFor({ state: 'hidden', timeout: 10000 });
|
|
}
|
|
} catch {
|
|
// Loading element not found or already hidden, continue
|
|
}
|
|
|
|
// Wait for either the main spec view or empty state to be visible
|
|
// The spec-view element appears when loading is complete and spec exists
|
|
// The spec-view-empty element appears when loading is complete and spec doesn't exist
|
|
await Promise.race([
|
|
waitForElement(page, 'spec-view', { timeout: 10000 }).catch(() => null),
|
|
waitForElement(page, 'spec-view-empty', { timeout: 10000 }).catch(() => null),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Navigate to the agent view
|
|
* Note: Navigates directly to /agent since index route shows WelcomeView
|
|
*/
|
|
export async function navigateToAgent(page: Page): Promise<void> {
|
|
// Authenticate before navigating
|
|
await authenticateForTests(page);
|
|
|
|
// Wait for any pending navigation to complete before starting a new one
|
|
await page.waitForLoadState('domcontentloaded').catch(() => {});
|
|
await page.waitForTimeout(100);
|
|
|
|
// Navigate directly to /agent route
|
|
await page.goto('/agent', { waitUntil: 'domcontentloaded' });
|
|
|
|
// Wait for splash screen to disappear (safety net)
|
|
await waitForSplashScreenToDisappear(page, 3000);
|
|
|
|
// Handle login redirect if needed
|
|
await handleLoginScreenIfPresent(page);
|
|
|
|
// Wait for the agent view to be visible
|
|
await waitForElement(page, 'agent-view', { timeout: 10000 });
|
|
}
|
|
|
|
/**
|
|
* Navigate to the settings view
|
|
* Note: Navigates directly to /settings since index route shows WelcomeView
|
|
*/
|
|
export async function navigateToSettings(page: Page): Promise<void> {
|
|
// Authenticate before navigating
|
|
await authenticateForTests(page);
|
|
|
|
// Navigate directly to /settings route
|
|
await page.goto('/settings');
|
|
await page.waitForLoadState('load');
|
|
|
|
// Wait for splash screen to disappear (safety net)
|
|
await waitForSplashScreenToDisappear(page, 3000);
|
|
|
|
// Wait for the settings view to be visible
|
|
await waitForElement(page, 'settings-view', { timeout: 10000 });
|
|
}
|
|
|
|
/**
|
|
* Navigate to the setup view directly
|
|
* Note: This function uses setupFirstRun from project/setup to avoid circular dependency
|
|
*/
|
|
export async function navigateToSetup(page: Page): Promise<void> {
|
|
// Dynamic import to avoid circular dependency
|
|
const { setupFirstRun } = await import('../project/setup');
|
|
await setupFirstRun(page);
|
|
await page.goto('/');
|
|
await page.waitForLoadState('load');
|
|
await waitForElement(page, 'setup-view', { timeout: 10000 });
|
|
}
|
|
|
|
/**
|
|
* Navigate to the welcome/dashboard view (clear project selection)
|
|
* Note: The app redirects from / to /dashboard when no project is selected
|
|
*/
|
|
export async function navigateToWelcome(page: Page): Promise<void> {
|
|
// Authenticate before navigating
|
|
await authenticateForTests(page);
|
|
|
|
await page.goto('/');
|
|
await page.waitForLoadState('load');
|
|
|
|
// Wait for splash screen to disappear (safety net)
|
|
await waitForSplashScreenToDisappear(page, 3000);
|
|
|
|
// Handle login redirect if needed
|
|
await handleLoginScreenIfPresent(page);
|
|
|
|
// Wait for either welcome-view, dashboard-view, or overview-view (app redirects based on project state)
|
|
await page
|
|
.locator(
|
|
'[data-testid="welcome-view"], [data-testid="dashboard-view"], [data-testid="overview-view"]'
|
|
)
|
|
.first()
|
|
.waitFor({ state: 'visible', timeout: 10000 });
|
|
}
|
|
|
|
/**
|
|
* Navigate to a specific view using the sidebar navigation
|
|
*/
|
|
export async function navigateToView(page: Page, viewId: string): Promise<void> {
|
|
const navSelector = viewId === 'settings' ? 'settings-button' : `nav-${viewId}`;
|
|
await clickElement(page, navSelector);
|
|
await page.waitForTimeout(100);
|
|
}
|
|
|
|
/**
|
|
* Get the current view from the URL or store (checks which view is active)
|
|
*/
|
|
export async function getCurrentView(page: Page): Promise<string | null> {
|
|
// Get the current view from zustand store via localStorage
|
|
const storage = await page.evaluate(() => {
|
|
const item = localStorage.getItem('automaker-storage');
|
|
return item ? JSON.parse(item) : null;
|
|
});
|
|
|
|
return storage?.state?.currentView || null;
|
|
}
|