From e10c73649cb091f0b3f00673d48020d37fb579dc Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 26 Feb 2026 07:36:55 -0800 Subject: [PATCH] Revert "Make memory and context views mobile-friendly (#813)" (#817) This reverts commit 583c3eb4a6fde668d79008d6fcc4860614c3f9a3. --- apps/ui/playwright.config.ts | 4 - .../components/ui/header-actions-panel.tsx | 1 - apps/ui/src/components/views/context-view.tsx | 132 ++--- apps/ui/src/components/views/memory-view.tsx | 107 +--- apps/ui/src/lib/image-utils.ts | 32 -- .../context/desktop-context-view.spec.ts | 237 -------- .../context/file-extension-edge-cases.spec.ts | 193 ------- .../context/mobile-context-operations.spec.ts | 131 ----- .../tests/context/mobile-context-view.spec.ts | 277 ---------- .../tests/memory/desktop-memory-view.spec.ts | 237 -------- .../memory/file-extension-edge-cases.spec.ts | 192 ------- .../memory/mobile-memory-operations.spec.ts | 174 ------ .../tests/memory/mobile-memory-view.spec.ts | 273 --------- apps/ui/tests/profiles/profiles-crud.spec.ts | 62 --- .../board-background-persistence.spec.ts | 491 ---------------- apps/ui/tests/utils/core/interactions.ts | 7 +- apps/ui/tests/utils/core/waiting.ts | 20 +- apps/ui/tests/utils/git/worktree.ts | 6 +- apps/ui/tests/utils/index.ts | 2 - apps/ui/tests/utils/navigation/views.ts | 37 +- apps/ui/tests/utils/project/fixtures.spec.ts | 180 ------ apps/ui/tests/utils/project/fixtures.ts | 89 +-- apps/ui/tests/utils/project/setup.ts | 29 +- apps/ui/tests/utils/views/context.ts | 29 +- apps/ui/tests/utils/views/memory.ts | 238 -------- apps/ui/tests/utils/views/profiles.ts | 522 ------------------ .../test-project-1772086066067/package.json | 4 - .../test-project-1772088506096/README.md | 15 - .../test-feature.js | 62 --- .../test-feature.test.js | 88 --- 30 files changed, 113 insertions(+), 3758 deletions(-) delete mode 100644 apps/ui/tests/context/desktop-context-view.spec.ts delete mode 100644 apps/ui/tests/context/file-extension-edge-cases.spec.ts delete mode 100644 apps/ui/tests/context/mobile-context-operations.spec.ts delete mode 100644 apps/ui/tests/context/mobile-context-view.spec.ts delete mode 100644 apps/ui/tests/memory/desktop-memory-view.spec.ts delete mode 100644 apps/ui/tests/memory/file-extension-edge-cases.spec.ts delete mode 100644 apps/ui/tests/memory/mobile-memory-operations.spec.ts delete mode 100644 apps/ui/tests/memory/mobile-memory-view.spec.ts delete mode 100644 apps/ui/tests/profiles/profiles-crud.spec.ts delete mode 100644 apps/ui/tests/projects/board-background-persistence.spec.ts delete mode 100644 apps/ui/tests/utils/project/fixtures.spec.ts delete mode 100644 apps/ui/tests/utils/views/memory.ts delete mode 100644 apps/ui/tests/utils/views/profiles.ts delete mode 100644 test/opus-thinking-level-none-83449-2bgz7j1/test-project-1772086066067/package.json delete mode 100644 test/running-task-display-test-805-6c4ockc/test-project-1772088506096/README.md delete mode 100644 test/running-task-display-test-805-6c4ockc/test-project-1772088506096/test-feature.js delete mode 100644 test/running-task-display-test-805-6c4ockc/test-project-1772088506096/test-feature.test.js diff --git a/apps/ui/playwright.config.ts b/apps/ui/playwright.config.ts index f530a93f..5a56289f 100644 --- a/apps/ui/playwright.config.ts +++ b/apps/ui/playwright.config.ts @@ -59,10 +59,6 @@ export default defineConfig({ ALLOWED_ROOT_DIRECTORY: '', // Simulate containerized environment to skip sandbox confirmation dialogs IS_CONTAINERIZED: 'true', - // Increase Node.js memory limit to prevent OOM during tests - NODE_OPTIONS: [process.env.NODE_OPTIONS, '--max-old-space-size=4096'] - .filter(Boolean) - .join(' '), }, }, ]), diff --git a/apps/ui/src/components/ui/header-actions-panel.tsx b/apps/ui/src/components/ui/header-actions-panel.tsx index 1795dcb5..708652b6 100644 --- a/apps/ui/src/components/ui/header-actions-panel.tsx +++ b/apps/ui/src/components/ui/header-actions-panel.tsx @@ -98,7 +98,6 @@ export function HeaderActionsPanelTrigger({ onClick={onToggle} className={cn('h-8 w-8 p-0 text-muted-foreground hover:text-foreground lg:hidden', className)} aria-label={isOpen ? 'Close actions menu' : 'Open actions menu'} - data-testid="header-actions-panel-trigger" > {isOpen ? : } diff --git a/apps/ui/src/components/views/context-view.tsx b/apps/ui/src/components/views/context-view.tsx index e41ae4ab..1d88ef94 100644 --- a/apps/ui/src/components/views/context-view.tsx +++ b/apps/ui/src/components/views/context-view.tsx @@ -24,10 +24,8 @@ import { FilePlus, FileUp, MoreVertical, - ArrowLeft, } from 'lucide-react'; import { Spinner } from '@/components/ui/spinner'; -import { useIsMobile } from '@/hooks/use-media-query'; import { useKeyboardShortcuts, useKeyboardShortcutsConfig, @@ -44,7 +42,7 @@ import { import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { cn } from '@/lib/utils'; -import { sanitizeFilename, isMarkdownFilename, isImageFilename } from '@/lib/image-utils'; +import { sanitizeFilename } from '@/lib/image-utils'; import { Markdown } from '../ui/markdown'; import { DropdownMenu, @@ -56,16 +54,6 @@ import { Textarea } from '@/components/ui/textarea'; const logger = createLogger('ContextView'); -// Responsive layout classes -const FILE_LIST_BASE_CLASSES = 'border-r border-border flex flex-col overflow-hidden'; -const FILE_LIST_DESKTOP_CLASSES = 'w-64'; -const FILE_LIST_EXPANDED_CLASSES = 'flex-1'; -const FILE_LIST_MOBILE_NO_SELECTION_CLASSES = 'w-full border-r-0'; -const FILE_LIST_MOBILE_SELECTION_CLASSES = 'hidden'; - -const EDITOR_PANEL_BASE_CLASSES = 'flex-1 flex flex-col overflow-hidden'; -const EDITOR_PANEL_MOBILE_HIDDEN_CLASSES = 'hidden'; - interface ContextFile { name: string; type: 'text' | 'image'; @@ -115,9 +103,6 @@ export function ContextView() { // File input ref for import const fileInputRef = useRef(null); - // Mobile detection - const isMobile = useIsMobile(); - // Keyboard shortcuts for this view const contextShortcuts: KeyboardShortcut[] = useMemo( () => [ @@ -137,6 +122,18 @@ export function ContextView() { return `${currentProject.path}/.automaker/context`; }, [currentProject]); + const isMarkdownFile = (filename: string): boolean => { + const ext = filename.toLowerCase().substring(filename.lastIndexOf('.')); + return ext === '.md' || ext === '.markdown'; + }; + + // Determine if a file is an image based on extension + const isImageFile = (filename: string): boolean => { + const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.bmp']; + const ext = filename.toLowerCase().substring(filename.lastIndexOf('.')); + return imageExtensions.includes(ext); + }; + // Load context metadata const loadMetadata = useCallback(async (): Promise => { const contextPath = getContextPath(); @@ -198,15 +195,10 @@ export function ContextView() { const result = await api.readdir(contextPath); if (result.success && result.entries) { const files: ContextFile[] = result.entries - .filter( - (entry) => - entry.isFile && - entry.name !== 'context-metadata.json' && - (isMarkdownFilename(entry.name) || isImageFilename(entry.name)) - ) + .filter((entry) => entry.isFile && entry.name !== 'context-metadata.json') .map((entry) => ({ name: entry.name, - type: isImageFilename(entry.name) ? 'image' : 'text', + type: isImageFile(entry.name) ? 'image' : 'text', path: `${contextPath}/${entry.name}`, description: metadata.files[entry.name]?.description, })); @@ -240,10 +232,11 @@ export function ContextView() { // Select a file const handleSelectFile = (file: ContextFile) => { - // Note: Unsaved changes warning could be added here in the future - // For now, silently proceed to avoid disrupting mobile UX flow + if (hasChanges) { + // Could add a confirmation dialog here + } loadFileContent(file); - setIsPreviewMode(isMarkdownFilename(file.name)); + setIsPreviewMode(isMarkdownFile(file.name)); }; // Save current file @@ -348,7 +341,7 @@ export function ContextView() { try { const api = getElectronAPI(); - const isImage = isImageFilename(file.name); + const isImage = isImageFile(file.name); let filePath: string; let fileName: string; @@ -589,7 +582,7 @@ export function ContextView() { // Update selected file with new name and path const renamedFile: ContextFile = { name: newName, - type: isImageFilename(newName) ? 'image' : 'text', + type: isImageFile(newName) ? 'image' : 'text', path: newPath, content: result.content, description: metadata.files[newName]?.description, @@ -797,17 +790,7 @@ export function ContextView() { )} {/* Left Panel - File List */} - {/* Mobile: Full width, hidden when file is selected (full-screen editor) */} - {/* Desktop: Fixed width w-64, expands to fill space when no file selected */} -
+

Context Files ({contextFiles.length}) @@ -898,31 +881,12 @@ export function ContextView() {

{/* Right Panel - Editor/Preview */} - {/* Mobile: Hidden when no file selected (file list shows full screen) */} -
+
{selectedFile ? ( <> {/* File toolbar */}
- {/* Mobile-only: Back button to return to file list */} - {isMobile && ( - - )} {selectedFile.type === 'image' ? ( ) : ( @@ -930,26 +894,23 @@ export function ContextView() { )} {selectedFile.name}
-
- {/* Mobile: Icon-only buttons with aria-labels for accessibility */} - {selectedFile.type === 'text' && isMarkdownFilename(selectedFile.name) && ( +
+ {selectedFile.type === 'text' && isMarkdownFile(selectedFile.name) && ( @@ -960,31 +921,20 @@ export function ContextView() { onClick={saveFile} disabled={!hasChanges || isSaving} data-testid="save-context-file" - aria-label="Save" - title="Save" > - - {!isMobile && ( - - {isSaving ? 'Saving...' : hasChanges ? 'Save' : 'Saved'} - - )} - - )} - {/* Desktop-only: Delete button (use dropdown on mobile to save space) */} - {!isMobile && ( - )} +
@@ -1122,7 +1072,7 @@ export function ContextView() { .filter((f): f is globalThis.File => f !== null); } - const mdFile = files.find((f) => isMarkdownFilename(f.name)); + const mdFile = files.find((f) => isMarkdownFile(f.name)); if (mdFile) { const content = await mdFile.text(); setNewMarkdownContent(content); diff --git a/apps/ui/src/components/views/memory-view.tsx b/apps/ui/src/components/views/memory-view.tsx index edac6b9d..b6331602 100644 --- a/apps/ui/src/components/views/memory-view.tsx +++ b/apps/ui/src/components/views/memory-view.tsx @@ -18,9 +18,7 @@ import { Pencil, FilePlus, MoreVertical, - ArrowLeft, } from 'lucide-react'; -import { useIsMobile } from '@/hooks/use-media-query'; import { Spinner } from '@/components/ui/spinner'; import { Dialog, @@ -33,7 +31,6 @@ import { import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { cn } from '@/lib/utils'; -import { isMarkdownFilename } from '@/lib/image-utils'; import { Markdown } from '../ui/markdown'; import { DropdownMenu, @@ -44,16 +41,6 @@ import { const logger = createLogger('MemoryView'); -// Responsive layout classes -const FILE_LIST_BASE_CLASSES = 'border-r border-border flex flex-col overflow-hidden'; -const FILE_LIST_DESKTOP_CLASSES = 'w-64'; -const FILE_LIST_EXPANDED_CLASSES = 'flex-1'; -const FILE_LIST_MOBILE_NO_SELECTION_CLASSES = 'w-full border-r-0'; -const FILE_LIST_MOBILE_SELECTION_CLASSES = 'hidden'; - -const EDITOR_PANEL_BASE_CLASSES = 'flex-1 flex flex-col overflow-hidden'; -const EDITOR_PANEL_MOBILE_HIDDEN_CLASSES = 'hidden'; - interface MemoryFile { name: string; content?: string; @@ -81,15 +68,17 @@ export function MemoryView() { // Actions panel state (for tablet/mobile) const [showActionsPanel, setShowActionsPanel] = useState(false); - // Mobile detection - const isMobile = useIsMobile(); - // Get memory directory path const getMemoryPath = useCallback(() => { if (!currentProject) return null; return `${currentProject.path}/.automaker/memory`; }, [currentProject]); + const isMarkdownFile = (filename: string): boolean => { + const ext = filename.toLowerCase().substring(filename.lastIndexOf('.')); + return ext === '.md' || ext === '.markdown'; + }; + // Load memory files const loadMemoryFiles = useCallback(async () => { const memoryPath = getMemoryPath(); @@ -106,7 +95,7 @@ export function MemoryView() { const result = await api.readdir(memoryPath); if (result.success && result.entries) { const files: MemoryFile[] = result.entries - .filter((entry) => entry.isFile && isMarkdownFilename(entry.name)) + .filter((entry) => entry.isFile && isMarkdownFile(entry.name)) .map((entry) => ({ name: entry.name, path: `${memoryPath}/${entry.name}`, @@ -141,8 +130,9 @@ export function MemoryView() { // Select a file const handleSelectFile = (file: MemoryFile) => { - // Note: Unsaved changes warning could be added here in the future - // For now, silently proceed to avoid disrupting mobile UX flow + if (hasChanges) { + // Could add a confirmation dialog here + } loadFileContent(file); setIsPreviewMode(true); }; @@ -391,17 +381,7 @@ export function MemoryView() { {/* Main content area with file list and editor */}
{/* Left Panel - File List */} - {/* Mobile: Full width, hidden when file is selected (full-screen editor) */} - {/* Desktop: Fixed width w-64, expands to fill space when no file selected */} -
+

Memory Files ({memoryFiles.length}) @@ -475,53 +455,31 @@ export function MemoryView() {

{/* Right Panel - Editor/Preview */} - {/* Mobile: Hidden when no file selected (file list shows full screen) */} -
+
{selectedFile ? ( <> {/* File toolbar */}
- {/* Mobile-only: Back button to return to file list */} - {isMobile && ( - - )} {selectedFile.name}
-
- {/* Mobile: Icon-only buttons with aria-labels for accessibility */} +
@@ -530,30 +488,19 @@ export function MemoryView() { onClick={saveFile} disabled={!hasChanges || isSaving} data-testid="save-memory-file" - aria-label="Save" - title="Save" > - - {!isMobile && ( - - {isSaving ? 'Saving...' : hasChanges ? 'Save' : 'Saved'} - - )} + + {isSaving ? 'Saving...' : hasChanges ? 'Save' : 'Saved'} + + - {/* Desktop-only: Delete button (use dropdown on mobile to save space) */} - {!isMobile && ( - - )}
diff --git a/apps/ui/src/lib/image-utils.ts b/apps/ui/src/lib/image-utils.ts index 64fba3fd..db64b2bc 100644 --- a/apps/ui/src/lib/image-utils.ts +++ b/apps/ui/src/lib/image-utils.ts @@ -17,12 +17,6 @@ export const ACCEPTED_TEXT_TYPES = ['text/plain', 'text/markdown', 'text/x-markd // File extensions for text files (used for validation when MIME type is unreliable) export const ACCEPTED_TEXT_EXTENSIONS = ['.txt', '.md']; -// File extensions for markdown files -export const MARKDOWN_EXTENSIONS = ['.md', '.markdown']; - -// File extensions for image files (used for display filtering) -export const IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.bmp']; - // Default max file size (10MB) export const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024; @@ -240,29 +234,3 @@ export function getTextFileMimeType(filename: string): string { } return 'text/plain'; } - -/** - * Check if a filename has a markdown extension - * - * @param filename - The filename to check - * @returns True if the filename has a .md or .markdown extension - */ -export function isMarkdownFilename(filename: string): boolean { - const dotIndex = filename.lastIndexOf('.'); - if (dotIndex < 0) return false; - const ext = filename.toLowerCase().substring(dotIndex); - return MARKDOWN_EXTENSIONS.includes(ext); -} - -/** - * Check if a filename has an image extension - * - * @param filename - The filename to check - * @returns True if the filename has an image extension - */ -export function isImageFilename(filename: string): boolean { - const dotIndex = filename.lastIndexOf('.'); - if (dotIndex < 0) return false; - const ext = filename.toLowerCase().substring(dotIndex); - return IMAGE_EXTENSIONS.includes(ext); -} diff --git a/apps/ui/tests/context/desktop-context-view.spec.ts b/apps/ui/tests/context/desktop-context-view.spec.ts deleted file mode 100644 index e4163094..00000000 --- a/apps/ui/tests/context/desktop-context-view.spec.ts +++ /dev/null @@ -1,237 +0,0 @@ -/** - * Desktop Context View E2E Tests - * - * Tests for desktop behavior in the context view: - * - File list and editor visible side-by-side - * - Back button is NOT visible on desktop - * - Toolbar buttons show both icon and text - * - Delete button is visible in toolbar (not hidden like on mobile) - */ - -import { test, expect } from '@playwright/test'; -import { - resetContextDirectory, - setupProjectWithFixture, - getFixturePath, - navigateToContext, - waitForContextFile, - selectContextFile, - waitForFileContentToLoad, - clickElement, - fillInput, - waitForNetworkIdle, - authenticateForTests, - waitForElementHidden, -} from '../utils'; - -// Use desktop viewport for desktop tests -test.use({ viewport: { width: 1280, height: 720 } }); - -test.describe('Desktop Context View', () => { - test.beforeEach(async () => { - resetContextDirectory(); - }); - - test.afterEach(async () => { - resetContextDirectory(); - }); - - test('should show file list and editor side-by-side on desktop', async ({ page }) => { - const fileName = 'desktop-test.md'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - - await navigateToContext(page); - - // Create a test file - await clickElement(page, 'create-markdown-button'); - await page.waitForSelector('[data-testid="create-markdown-dialog"]', { timeout: 5000 }); - - await fillInput(page, 'new-markdown-name', fileName); - await fillInput( - page, - 'new-markdown-content', - '# Desktop Test\n\nThis tests desktop view behavior' - ); - - await clickElement(page, 'confirm-create-markdown'); - - await waitForElementHidden(page, 'create-markdown-dialog', { timeout: 5000 }); - - await waitForNetworkIdle(page); - await waitForContextFile(page, fileName); - - // Select the file - await selectContextFile(page, fileName); - await waitForFileContentToLoad(page); - - // On desktop, file list should be visible after selection - const fileList = page.locator('[data-testid="context-file-list"]'); - await expect(fileList).toBeVisible(); - - // Editor panel should also be visible - const editor = page.locator('[data-testid="context-editor"], [data-testid="markdown-preview"]'); - await expect(editor).toBeVisible(); - }); - - test('should NOT show back button in editor toolbar on desktop', async ({ page }) => { - const fileName = 'no-back-button-test.md'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - - await navigateToContext(page); - - // Create a test file - await clickElement(page, 'create-markdown-button'); - await page.waitForSelector('[data-testid="create-markdown-dialog"]', { timeout: 5000 }); - - await fillInput(page, 'new-markdown-name', fileName); - await fillInput(page, 'new-markdown-content', '# No Back Button Test'); - - await clickElement(page, 'confirm-create-markdown'); - - await waitForElementHidden(page, 'create-markdown-dialog', { timeout: 5000 }); - - await waitForNetworkIdle(page); - await waitForContextFile(page, fileName); - - // Select the file - await selectContextFile(page, fileName); - await waitForFileContentToLoad(page); - - // Back button should NOT be visible on desktop - const backButton = page.locator('button[aria-label="Back"]'); - await expect(backButton).not.toBeVisible(); - }); - - test('should show buttons with text labels on desktop', async ({ page }) => { - const fileName = 'text-labels-test.md'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - - await navigateToContext(page); - - // Create a test file - await clickElement(page, 'create-markdown-button'); - await page.waitForSelector('[data-testid="create-markdown-dialog"]', { timeout: 5000 }); - - await fillInput(page, 'new-markdown-name', fileName); - await fillInput( - page, - 'new-markdown-content', - '# Text Labels Test\n\nTesting button text labels on desktop' - ); - - await clickElement(page, 'confirm-create-markdown'); - - await waitForElementHidden(page, 'create-markdown-dialog', { timeout: 5000 }); - - await waitForNetworkIdle(page); - await waitForContextFile(page, fileName); - - // Select the file - await selectContextFile(page, fileName); - await waitForFileContentToLoad(page); - - // Get the toggle preview mode button - const toggleButton = page.locator('[data-testid="toggle-preview-mode"]'); - await expect(toggleButton).toBeVisible(); - - // Button should have text label on desktop - const buttonText = await toggleButton.textContent(); - // On desktop, button should have visible text (Edit or Preview) - expect(buttonText?.trim()).not.toBe(''); - expect(buttonText?.toLowerCase()).toMatch(/(edit|preview)/); - }); - - test('should show delete button in toolbar on desktop', async ({ page }) => { - const fileName = 'delete-button-desktop-test.md'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - - await navigateToContext(page); - - // Create a test file - await clickElement(page, 'create-markdown-button'); - await page.waitForSelector('[data-testid="create-markdown-dialog"]', { timeout: 5000 }); - - await fillInput(page, 'new-markdown-name', fileName); - await fillInput(page, 'new-markdown-content', '# Delete Button Desktop Test'); - - await clickElement(page, 'confirm-create-markdown'); - - await waitForElementHidden(page, 'create-markdown-dialog', { timeout: 5000 }); - - await waitForNetworkIdle(page); - await waitForContextFile(page, fileName); - - // Select the file - await selectContextFile(page, fileName); - await waitForFileContentToLoad(page); - - // Delete button in toolbar should be visible on desktop - const deleteButton = page.locator('[data-testid="delete-context-file"]'); - await expect(deleteButton).toBeVisible(); - }); - - test('should show file list at fixed width on desktop when file is selected', async ({ - page, - }) => { - const fileName = 'fixed-width-test.md'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - - await navigateToContext(page); - - // Create a test file - await clickElement(page, 'create-markdown-button'); - await page.waitForSelector('[data-testid="create-markdown-dialog"]', { timeout: 5000 }); - - await fillInput(page, 'new-markdown-name', fileName); - await fillInput(page, 'new-markdown-content', '# Fixed Width Test'); - - await clickElement(page, 'confirm-create-markdown'); - - await waitForElementHidden(page, 'create-markdown-dialog', { timeout: 5000 }); - - await waitForNetworkIdle(page); - await waitForContextFile(page, fileName); - - // Select the file - await selectContextFile(page, fileName); - await waitForFileContentToLoad(page); - - // File list should be visible - const fileList = page.locator('[data-testid="context-file-list"]'); - await expect(fileList).toBeVisible(); - - // On desktop with file selected, the file list should be at fixed width (w-64 = 256px) - const fileListBox = await fileList.boundingBox(); - expect(fileListBox).not.toBeNull(); - - if (fileListBox) { - // Desktop file list is w-64 = 256px, allow some tolerance for borders - expect(fileListBox.width).toBeLessThanOrEqual(300); - expect(fileListBox.width).toBeGreaterThanOrEqual(200); - } - }); - - test('should show action buttons inline in header on desktop', async ({ page }) => { - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - - await navigateToContext(page); - - // On desktop, inline buttons should be visible - const createButton = page.locator('[data-testid="create-markdown-button"]'); - await expect(createButton).toBeVisible(); - - const importButton = page.locator('[data-testid="import-file-button"]'); - await expect(importButton).toBeVisible(); - }); -}); diff --git a/apps/ui/tests/context/file-extension-edge-cases.spec.ts b/apps/ui/tests/context/file-extension-edge-cases.spec.ts deleted file mode 100644 index 1c7af128..00000000 --- a/apps/ui/tests/context/file-extension-edge-cases.spec.ts +++ /dev/null @@ -1,193 +0,0 @@ -/** - * Context View File Extension Edge Cases E2E Tests - * - * Tests for file extension handling in the context view: - * - Files with valid markdown extensions (.md, .markdown) - * - Files without extensions (edge case for isMarkdownFile/isImageFile) - * - Image files with various extensions - * - Files with multiple dots in name - */ - -import { test, expect } from '@playwright/test'; -import { - resetContextDirectory, - setupProjectWithFixture, - getFixturePath, - navigateToContext, - waitForContextFile, - selectContextFile, - waitForFileContentToLoad, - clickElement, - fillInput, - waitForNetworkIdle, - authenticateForTests, - waitForElementHidden, - createContextFileOnDisk, -} from '../utils'; - -// Use desktop viewport for these tests -test.use({ viewport: { width: 1280, height: 720 } }); - -test.describe('Context View File Extension Edge Cases', () => { - test.beforeEach(async () => { - resetContextDirectory(); - }); - - test.afterEach(async () => { - resetContextDirectory(); - }); - - test('should handle file with .md extension', async ({ page }) => { - const fileName = 'standard-file.md'; - const content = '# Standard Markdown'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - await navigateToContext(page); - - // Create file via API - createContextFileOnDisk(fileName, content); - await waitForNetworkIdle(page); - - // Refresh to load the file - await page.reload(); - await waitForContextFile(page, fileName); - - // Select and verify it opens as markdown - await selectContextFile(page, fileName); - await waitForFileContentToLoad(page); - - // Should show markdown preview - const markdownPreview = page.locator('[data-testid="markdown-preview"]'); - await expect(markdownPreview).toBeVisible(); - - // Verify content rendered - const h1 = markdownPreview.locator('h1'); - await expect(h1).toHaveText('Standard Markdown'); - }); - - test('should handle file with .markdown extension', async ({ page }) => { - const fileName = 'extended-extension.markdown'; - const content = '# Extended Extension Test'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - await navigateToContext(page); - - // Create file via API - createContextFileOnDisk(fileName, content); - await waitForNetworkIdle(page); - - // Refresh to load the file - await page.reload(); - await waitForContextFile(page, fileName); - - // Select and verify - await selectContextFile(page, fileName); - await waitForFileContentToLoad(page); - - const markdownPreview = page.locator('[data-testid="markdown-preview"]'); - await expect(markdownPreview).toBeVisible(); - }); - - test('should handle file with multiple dots in name', async ({ page }) => { - const fileName = 'my.detailed.notes.md'; - const content = '# Multiple Dots Test'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - await navigateToContext(page); - - // Create file via API - createContextFileOnDisk(fileName, content); - await waitForNetworkIdle(page); - - // Refresh to load the file - await page.reload(); - await waitForContextFile(page, fileName); - - // Select and verify - should still recognize as markdown - await selectContextFile(page, fileName); - await waitForFileContentToLoad(page); - - const markdownPreview = page.locator('[data-testid="markdown-preview"]'); - await expect(markdownPreview).toBeVisible(); - }); - - test('should NOT show file without extension in file list', async ({ page }) => { - const fileName = 'README'; - const content = '# File Without Extension'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - await navigateToContext(page); - - // Create file via API (without extension) - createContextFileOnDisk(fileName, content); - await waitForNetworkIdle(page); - - // Refresh to load the file - await page.reload(); - - // Wait a moment for files to load - await page.waitForTimeout(1000); - - // File should NOT appear in list because isMarkdownFile returns false for no extension - const fileButton = page.locator(`[data-testid="context-file-${fileName}"]`); - await expect(fileButton).not.toBeVisible(); - }); - - test('should NOT create file without .md extension via UI', async ({ page }) => { - const fileName = 'NOTES'; - const content = '# Notes without extension'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - await navigateToContext(page); - - // Create file via UI without extension - await clickElement(page, 'create-markdown-button'); - await page.waitForSelector('[data-testid="create-markdown-dialog"]', { timeout: 5000 }); - - await fillInput(page, 'new-markdown-name', fileName); - await fillInput(page, 'new-markdown-content', content); - - await clickElement(page, 'confirm-create-markdown'); - await waitForElementHidden(page, 'create-markdown-dialog', { timeout: 5000 }); - - await waitForNetworkIdle(page); - - // File should NOT appear in list because UI enforces .md extension - // (The UI may add .md automatically or show validation error) - const fileButton = page.locator(`[data-testid="context-file-${fileName}"]`); - await expect(fileButton) - .not.toBeVisible({ timeout: 3000 }) - .catch(() => { - // It's OK if it doesn't appear - that's expected behavior - }); - }); - - test('should handle uppercase extensions', async ({ page }) => { - const fileName = 'uppercase.MD'; - const content = '# Uppercase Extension'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - await navigateToContext(page); - - // Create file via API with uppercase extension - createContextFileOnDisk(fileName, content); - await waitForNetworkIdle(page); - - // Refresh to load the file - await page.reload(); - await waitForContextFile(page, fileName); - - // Select and verify - should recognize .MD as markdown (case-insensitive) - await selectContextFile(page, fileName); - await waitForFileContentToLoad(page); - - const markdownPreview = page.locator('[data-testid="markdown-preview"]'); - await expect(markdownPreview).toBeVisible(); - }); -}); diff --git a/apps/ui/tests/context/mobile-context-operations.spec.ts b/apps/ui/tests/context/mobile-context-operations.spec.ts deleted file mode 100644 index 3b187983..00000000 --- a/apps/ui/tests/context/mobile-context-operations.spec.ts +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Mobile Context View Operations E2E Tests - * - * Tests for file operations on mobile in the context view: - * - Deleting files via dropdown menu on mobile - * - Creating files via mobile actions panel - */ - -import { test, expect, devices } from '@playwright/test'; -import { - resetContextDirectory, - setupProjectWithFixture, - getFixturePath, - navigateToContext, - waitForContextFile, - clickElement, - fillInput, - waitForNetworkIdle, - authenticateForTests, - contextFileExistsOnDisk, - waitForElementHidden, -} from '../utils'; - -// Use mobile viewport for mobile tests in Chromium CI -test.use({ ...devices['Pixel 5'] }); - -test.describe('Mobile Context View Operations', () => { - test.beforeEach(async () => { - resetContextDirectory(); - }); - - test.afterEach(async () => { - resetContextDirectory(); - }); - - test('should create a file via mobile actions panel', async ({ page }) => { - const fileName = 'mobile-created.md'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - - await navigateToContext(page); - - // Create a test file via mobile actions panel - await clickElement(page, 'header-actions-panel-trigger'); - await clickElement(page, 'create-markdown-button-mobile'); - await page.waitForSelector('[data-testid="create-markdown-dialog"]', { timeout: 5000 }); - - await fillInput(page, 'new-markdown-name', fileName); - await fillInput(page, 'new-markdown-content', '# Created on Mobile'); - - await clickElement(page, 'confirm-create-markdown'); - - await waitForElementHidden(page, 'create-markdown-dialog', { timeout: 5000 }); - - await waitForNetworkIdle(page); - await waitForContextFile(page, fileName); - - // Verify file appears in list - const fileButton = page.locator(`[data-testid="context-file-${fileName}"]`); - await expect(fileButton).toBeVisible(); - - // Verify file exists on disk - expect(contextFileExistsOnDisk(fileName)).toBe(true); - }); - - test('should delete a file via dropdown menu on mobile', async ({ page }) => { - const fileName = 'delete-via-menu-test.md'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - - await navigateToContext(page); - - // Create a test file - await clickElement(page, 'header-actions-panel-trigger'); - await clickElement(page, 'create-markdown-button-mobile'); - await page.waitForSelector('[data-testid="create-markdown-dialog"]', { timeout: 5000 }); - - await fillInput(page, 'new-markdown-name', fileName); - await fillInput(page, 'new-markdown-content', '# File to Delete'); - - await clickElement(page, 'confirm-create-markdown'); - - await waitForElementHidden(page, 'create-markdown-dialog', { timeout: 5000 }); - - await waitForNetworkIdle(page); - await waitForContextFile(page, fileName); - - // Verify file exists - expect(contextFileExistsOnDisk(fileName)).toBe(true); - - // Close actions panel if still open - await page.keyboard.press('Escape'); - await page.waitForTimeout(300); - - // Click on the file menu dropdown - hover first to make it visible - const fileRow = page.locator(`[data-testid="context-file-${fileName}"]`); - await fileRow.hover(); - - const fileMenuButton = page.locator(`[data-testid="context-file-menu-${fileName}"]`); - await fileMenuButton.click({ force: true }); - - // Wait for dropdown - await page.waitForTimeout(300); - - // Click delete in dropdown - const deleteMenuItem = page.locator(`[data-testid="delete-context-file-${fileName}"]`); - await deleteMenuItem.click(); - - // Wait for file to be removed from list - await waitForElementHidden(page, `context-file-${fileName}`, { timeout: 5000 }); - - // Verify file no longer exists on disk - expect(contextFileExistsOnDisk(fileName)).toBe(false); - }); - - test('should import file button be available in actions panel', async ({ page }) => { - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - - await navigateToContext(page); - - // Open actions panel - await clickElement(page, 'header-actions-panel-trigger'); - - // Verify import button is visible in actions panel - const importButton = page.locator('[data-testid="import-file-button-mobile"]'); - await expect(importButton).toBeVisible(); - }); -}); diff --git a/apps/ui/tests/context/mobile-context-view.spec.ts b/apps/ui/tests/context/mobile-context-view.spec.ts deleted file mode 100644 index 43cf65dc..00000000 --- a/apps/ui/tests/context/mobile-context-view.spec.ts +++ /dev/null @@ -1,277 +0,0 @@ -/** - * Mobile Context View E2E Tests - * - * Tests for mobile-friendly behavior in the context view: - * - File list hides when file is selected on mobile - * - Back button appears on mobile to return to file list - * - Toolbar buttons are icon-only on mobile - * - Delete button is hidden on mobile (use dropdown menu instead) - */ - -import { test, expect, devices } from '@playwright/test'; -import { - resetContextDirectory, - setupProjectWithFixture, - getFixturePath, - navigateToContext, - waitForContextFile, - selectContextFile, - waitForFileContentToLoad, - clickElement, - fillInput, - waitForNetworkIdle, - authenticateForTests, - waitForElementHidden, -} from '../utils'; - -// Use mobile viewport for mobile tests in Chromium CI -test.use({ ...devices['Pixel 5'] }); - -test.describe('Mobile Context View', () => { - test.beforeEach(async () => { - resetContextDirectory(); - }); - - test.afterEach(async () => { - resetContextDirectory(); - }); - - test('should hide file list when a file is selected on mobile', async ({ page }) => { - const fileName = 'mobile-test.md'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - - await navigateToContext(page); - - // Create a test file - on mobile, open the actions panel first - await clickElement(page, 'header-actions-panel-trigger'); - await clickElement(page, 'create-markdown-button-mobile'); - await page.waitForSelector('[data-testid="create-markdown-dialog"]', { timeout: 5000 }); - - await fillInput(page, 'new-markdown-name', fileName); - await fillInput( - page, - 'new-markdown-content', - '# Mobile Test\n\nThis tests mobile view behavior' - ); - - await clickElement(page, 'confirm-create-markdown'); - - await waitForElementHidden(page, 'create-markdown-dialog', { timeout: 5000 }); - - await waitForNetworkIdle(page); - await waitForContextFile(page, fileName); - - // File list should be visible before selection - const fileListBefore = page.locator('[data-testid="context-file-list"]'); - await expect(fileListBefore).toBeVisible(); - - // Select the file - await selectContextFile(page, fileName); - await waitForFileContentToLoad(page); - - // On mobile, file list should be hidden after selection (full-screen editor) - const fileListAfter = page.locator('[data-testid="context-file-list"]'); - await expect(fileListAfter).toBeHidden(); - }); - - test('should show back button in editor toolbar on mobile', async ({ page }) => { - const fileName = 'back-button-test.md'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - - await navigateToContext(page); - - // Create a test file - on mobile, open the actions panel first - await clickElement(page, 'header-actions-panel-trigger'); - await clickElement(page, 'create-markdown-button-mobile'); - await page.waitForSelector('[data-testid="create-markdown-dialog"]', { timeout: 5000 }); - - await fillInput(page, 'new-markdown-name', fileName); - await fillInput( - page, - 'new-markdown-content', - '# Back Button Test\n\nTesting back button on mobile' - ); - - await clickElement(page, 'confirm-create-markdown'); - - await waitForElementHidden(page, 'create-markdown-dialog', { timeout: 5000 }); - - await waitForNetworkIdle(page); - await waitForContextFile(page, fileName); - - // Select the file - await selectContextFile(page, fileName); - await waitForFileContentToLoad(page); - - // Back button should be visible on mobile - const backButton = page.locator('button[aria-label="Back"]'); - await expect(backButton).toBeVisible(); - - // Back button should have ArrowLeft icon - const arrowIcon = backButton.locator('svg.lucide-arrow-left'); - await expect(arrowIcon).toBeVisible(); - }); - - test('should return to file list when back button is clicked on mobile', async ({ page }) => { - const fileName = 'back-navigation-test.md'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - - await navigateToContext(page); - - // Create a test file - on mobile, open the actions panel first - await clickElement(page, 'header-actions-panel-trigger'); - await clickElement(page, 'create-markdown-button-mobile'); - await page.waitForSelector('[data-testid="create-markdown-dialog"]', { timeout: 5000 }); - - await fillInput(page, 'new-markdown-name', fileName); - await fillInput(page, 'new-markdown-content', '# Back Navigation Test'); - - await clickElement(page, 'confirm-create-markdown'); - - await waitForElementHidden(page, 'create-markdown-dialog', { timeout: 5000 }); - - await waitForNetworkIdle(page); - await waitForContextFile(page, fileName); - - // Select the file - await selectContextFile(page, fileName); - await waitForFileContentToLoad(page); - - // File list should be hidden after selection - const fileListHidden = page.locator('[data-testid="context-file-list"]'); - await expect(fileListHidden).toBeHidden(); - - // Click back button - const backButton = page.locator('button[aria-label="Back"]'); - await backButton.click(); - - // File list should be visible again - const fileListVisible = page.locator('[data-testid="context-file-list"]'); - await expect(fileListVisible).toBeVisible(); - - // Editor should no longer be visible - const editor = page.locator('[data-testid="context-editor"]'); - await expect(editor).not.toBeVisible(); - }); - - test('should show icon-only buttons in toolbar on mobile', async ({ page }) => { - const fileName = 'icon-buttons-test.md'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - - await navigateToContext(page); - - // Create a test file - on mobile, open the actions panel first - await clickElement(page, 'header-actions-panel-trigger'); - await clickElement(page, 'create-markdown-button-mobile'); - await page.waitForSelector('[data-testid="create-markdown-dialog"]', { timeout: 5000 }); - - await fillInput(page, 'new-markdown-name', fileName); - await fillInput( - page, - 'new-markdown-content', - '# Icon Buttons Test\n\nTesting icon-only buttons on mobile' - ); - - await clickElement(page, 'confirm-create-markdown'); - - await waitForElementHidden(page, 'create-markdown-dialog', { timeout: 5000 }); - - await waitForNetworkIdle(page); - await waitForContextFile(page, fileName); - - // Select the file - await selectContextFile(page, fileName); - await waitForFileContentToLoad(page); - - // Get the toggle preview mode button - const toggleButton = page.locator('[data-testid="toggle-preview-mode"]'); - await expect(toggleButton).toBeVisible(); - - // Button should have icon (Eye or Pencil) - const eyeIcon = toggleButton.locator('svg.lucide-eye'); - const pencilIcon = toggleButton.locator('svg.lucide-pencil'); - - // One of the icons should be present - const hasIcon = await (async () => { - const eyeVisible = await eyeIcon.isVisible().catch(() => false); - const pencilVisible = await pencilIcon.isVisible().catch(() => false); - return eyeVisible || pencilVisible; - })(); - - expect(hasIcon).toBe(true); - - // Text label should not be present (or minimal space on mobile) - const buttonText = await toggleButton.textContent(); - // On mobile, button should have icon only (no "Edit" or "Preview" text visible) - // The text is wrapped in {!isMobile && }, so it shouldn't render - expect(buttonText?.trim()).toBe(''); - }); - - test('should hide delete button in toolbar on mobile', async ({ page }) => { - const fileName = 'delete-button-test.md'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - - await navigateToContext(page); - - // Create a test file - on mobile, open the actions panel first - await clickElement(page, 'header-actions-panel-trigger'); - await clickElement(page, 'create-markdown-button-mobile'); - await page.waitForSelector('[data-testid="create-markdown-dialog"]', { timeout: 5000 }); - - await fillInput(page, 'new-markdown-name', fileName); - await fillInput(page, 'new-markdown-content', '# Delete Button Test'); - - await clickElement(page, 'confirm-create-markdown'); - - await waitForElementHidden(page, 'create-markdown-dialog', { timeout: 5000 }); - - await waitForNetworkIdle(page); - await waitForContextFile(page, fileName); - - // Select the file - await selectContextFile(page, fileName); - await waitForFileContentToLoad(page); - - // Delete button in toolbar should be hidden on mobile - const deleteButton = page.locator('[data-testid="delete-context-file"]'); - await expect(deleteButton).not.toBeVisible(); - }); - - test('should show file list at full width on mobile when no file is selected', async ({ - page, - }) => { - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - - await navigateToContext(page); - - // File list should be visible - const fileList = page.locator('[data-testid="context-file-list"]'); - await expect(fileList).toBeVisible(); - - // On mobile with no file selected, the file list should take full width - // Check that the file list container has the w-full class (mobile behavior) - const fileListBox = await fileList.boundingBox(); - expect(fileListBox).not.toBeNull(); - - if (fileListBox) { - // On mobile (Pixel 5 has width 393), the file list should take most of the width - // We check that it's significantly wider than the desktop w-64 (256px) - expect(fileListBox.width).toBeGreaterThan(300); - } - - // Editor panel should be hidden on mobile when no file is selected - const editor = page.locator('[data-testid="context-editor"]'); - await expect(editor).not.toBeVisible(); - }); -}); diff --git a/apps/ui/tests/memory/desktop-memory-view.spec.ts b/apps/ui/tests/memory/desktop-memory-view.spec.ts deleted file mode 100644 index 61dfaff7..00000000 --- a/apps/ui/tests/memory/desktop-memory-view.spec.ts +++ /dev/null @@ -1,237 +0,0 @@ -/** - * Desktop Memory View E2E Tests - * - * Tests for desktop behavior in the memory view: - * - File list and editor visible side-by-side - * - Back button is NOT visible on desktop - * - Toolbar buttons show both icon and text - * - Delete button is visible in toolbar (not hidden like on mobile) - */ - -import { test, expect } from '@playwright/test'; -import { - resetMemoryDirectory, - setupProjectWithFixture, - getFixturePath, - navigateToMemory, - waitForMemoryFile, - selectMemoryFile, - waitForMemoryContentToLoad, - clickElement, - fillInput, - waitForNetworkIdle, - authenticateForTests, - waitForElementHidden, -} from '../utils'; - -// Use desktop viewport for desktop tests -test.use({ viewport: { width: 1280, height: 720 } }); - -test.describe('Desktop Memory View', () => { - test.beforeEach(async () => { - resetMemoryDirectory(); - }); - - test.afterEach(async () => { - resetMemoryDirectory(); - }); - - test('should show file list and editor side-by-side on desktop', async ({ page }) => { - const fileName = 'desktop-test.md'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - - await navigateToMemory(page); - - // Create a test file - await clickElement(page, 'create-memory-button'); - await page.waitForSelector('[data-testid="create-memory-dialog"]', { timeout: 5000 }); - - await fillInput(page, 'new-memory-name', fileName); - await fillInput( - page, - 'new-memory-content', - '# Desktop Test\n\nThis tests desktop view behavior' - ); - - await clickElement(page, 'confirm-create-memory'); - - await waitForElementHidden(page, 'create-memory-dialog', { timeout: 5000 }); - - await waitForNetworkIdle(page); - await waitForMemoryFile(page, fileName); - - // Select the file - await selectMemoryFile(page, fileName); - await waitForMemoryContentToLoad(page); - - // On desktop, file list should be visible after selection - const fileList = page.locator('[data-testid="memory-file-list"]'); - await expect(fileList).toBeVisible(); - - // Editor panel should also be visible (either editor or preview) - const editor = page.locator('[data-testid="memory-editor"], [data-testid="markdown-preview"]'); - await expect(editor).toBeVisible(); - }); - - test('should NOT show back button in editor toolbar on desktop', async ({ page }) => { - const fileName = 'no-back-button-test.md'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - - await navigateToMemory(page); - - // Create a test file - await clickElement(page, 'create-memory-button'); - await page.waitForSelector('[data-testid="create-memory-dialog"]', { timeout: 5000 }); - - await fillInput(page, 'new-memory-name', fileName); - await fillInput(page, 'new-memory-content', '# No Back Button Test'); - - await clickElement(page, 'confirm-create-memory'); - - await waitForElementHidden(page, 'create-memory-dialog', { timeout: 5000 }); - - await waitForNetworkIdle(page); - await waitForMemoryFile(page, fileName); - - // Select the file - await selectMemoryFile(page, fileName); - await waitForMemoryContentToLoad(page); - - // Back button should NOT be visible on desktop - const backButton = page.locator('button[aria-label="Back"]'); - await expect(backButton).not.toBeVisible(); - }); - - test('should show buttons with text labels on desktop', async ({ page }) => { - const fileName = 'text-labels-test.md'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - - await navigateToMemory(page); - - // Create a test file - await clickElement(page, 'create-memory-button'); - await page.waitForSelector('[data-testid="create-memory-dialog"]', { timeout: 5000 }); - - await fillInput(page, 'new-memory-name', fileName); - await fillInput( - page, - 'new-memory-content', - '# Text Labels Test\n\nTesting button text labels on desktop' - ); - - await clickElement(page, 'confirm-create-memory'); - - await waitForElementHidden(page, 'create-memory-dialog', { timeout: 5000 }); - - await waitForNetworkIdle(page); - await waitForMemoryFile(page, fileName); - - // Select the file - await selectMemoryFile(page, fileName); - await waitForMemoryContentToLoad(page); - - // Get the toggle preview mode button - const toggleButton = page.locator('[data-testid="toggle-preview-mode"]'); - await expect(toggleButton).toBeVisible(); - - // Button should have text label on desktop - const buttonText = await toggleButton.textContent(); - // On desktop, button should have visible text (Edit or Preview) - expect(buttonText?.trim()).not.toBe(''); - expect(buttonText?.toLowerCase()).toMatch(/(edit|preview)/); - }); - - test('should show delete button in toolbar on desktop', async ({ page }) => { - const fileName = 'delete-button-desktop-test.md'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - - await navigateToMemory(page); - - // Create a test file - await clickElement(page, 'create-memory-button'); - await page.waitForSelector('[data-testid="create-memory-dialog"]', { timeout: 5000 }); - - await fillInput(page, 'new-memory-name', fileName); - await fillInput(page, 'new-memory-content', '# Delete Button Desktop Test'); - - await clickElement(page, 'confirm-create-memory'); - - await waitForElementHidden(page, 'create-memory-dialog', { timeout: 5000 }); - - await waitForNetworkIdle(page); - await waitForMemoryFile(page, fileName); - - // Select the file - await selectMemoryFile(page, fileName); - await waitForMemoryContentToLoad(page); - - // Delete button in toolbar should be visible on desktop - const deleteButton = page.locator('[data-testid="delete-memory-file"]'); - await expect(deleteButton).toBeVisible(); - }); - - test('should show file list at fixed width on desktop when file is selected', async ({ - page, - }) => { - const fileName = 'fixed-width-test.md'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - - await navigateToMemory(page); - - // Create a test file - await clickElement(page, 'create-memory-button'); - await page.waitForSelector('[data-testid="create-memory-dialog"]', { timeout: 5000 }); - - await fillInput(page, 'new-memory-name', fileName); - await fillInput(page, 'new-memory-content', '# Fixed Width Test'); - - await clickElement(page, 'confirm-create-memory'); - - await waitForElementHidden(page, 'create-memory-dialog', { timeout: 5000 }); - - await waitForNetworkIdle(page); - await waitForMemoryFile(page, fileName); - - // Select the file - await selectMemoryFile(page, fileName); - await waitForMemoryContentToLoad(page); - - // File list should be visible - const fileList = page.locator('[data-testid="memory-file-list"]'); - await expect(fileList).toBeVisible(); - - // On desktop with file selected, the file list should be at fixed width (w-64 = 256px) - const fileListBox = await fileList.boundingBox(); - expect(fileListBox).not.toBeNull(); - - if (fileListBox) { - // Desktop file list is w-64 = 256px, allow some tolerance for borders - expect(fileListBox.width).toBeLessThanOrEqual(300); - expect(fileListBox.width).toBeGreaterThanOrEqual(200); - } - }); - - test('should show action buttons inline in header on desktop', async ({ page }) => { - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - - await navigateToMemory(page); - - // On desktop, inline buttons should be visible - const createButton = page.locator('[data-testid="create-memory-button"]'); - await expect(createButton).toBeVisible(); - - const refreshButton = page.locator('[data-testid="refresh-memory-button"]'); - await expect(refreshButton).toBeVisible(); - }); -}); diff --git a/apps/ui/tests/memory/file-extension-edge-cases.spec.ts b/apps/ui/tests/memory/file-extension-edge-cases.spec.ts deleted file mode 100644 index 6bc592f6..00000000 --- a/apps/ui/tests/memory/file-extension-edge-cases.spec.ts +++ /dev/null @@ -1,192 +0,0 @@ -/** - * Memory View File Extension Edge Cases E2E Tests - * - * Tests for file extension handling in the memory view: - * - Files with valid markdown extensions (.md, .markdown) - * - Files without extensions (edge case for isMarkdownFile) - * - Files with multiple dots in name - */ - -import { test, expect } from '@playwright/test'; -import { - resetMemoryDirectory, - setupProjectWithFixture, - getFixturePath, - navigateToMemory, - waitForMemoryFile, - selectMemoryFile, - waitForMemoryContentToLoad, - clickElement, - fillInput, - waitForNetworkIdle, - authenticateForTests, - waitForElementHidden, - createMemoryFileOnDisk, -} from '../utils'; - -// Use desktop viewport for these tests -test.use({ viewport: { width: 1280, height: 720 } }); - -test.describe('Memory View File Extension Edge Cases', () => { - test.beforeEach(async () => { - resetMemoryDirectory(); - }); - - test.afterEach(async () => { - resetMemoryDirectory(); - }); - - test('should handle file with .md extension', async ({ page }) => { - const fileName = 'standard-file.md'; - const content = '# Standard Markdown'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - await navigateToMemory(page); - - // Create file via API - createMemoryFileOnDisk(fileName, content); - await waitForNetworkIdle(page); - - // Refresh to load the file - await page.reload(); - await waitForMemoryFile(page, fileName); - - // Select and verify it opens as markdown - await selectMemoryFile(page, fileName); - await waitForMemoryContentToLoad(page); - - // Should show markdown preview - const markdownPreview = page.locator('[data-testid="markdown-preview"]'); - await expect(markdownPreview).toBeVisible(); - - // Verify content rendered - const h1 = markdownPreview.locator('h1'); - await expect(h1).toHaveText('Standard Markdown'); - }); - - test('should handle file with .markdown extension', async ({ page }) => { - const fileName = 'extended-extension.markdown'; - const content = '# Extended Extension Test'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - await navigateToMemory(page); - - // Create file via API - createMemoryFileOnDisk(fileName, content); - await waitForNetworkIdle(page); - - // Refresh to load the file - await page.reload(); - await waitForMemoryFile(page, fileName); - - // Select and verify - await selectMemoryFile(page, fileName); - await waitForMemoryContentToLoad(page); - - const markdownPreview = page.locator('[data-testid="markdown-preview"]'); - await expect(markdownPreview).toBeVisible(); - }); - - test('should handle file with multiple dots in name', async ({ page }) => { - const fileName = 'my.detailed.notes.md'; - const content = '# Multiple Dots Test'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - await navigateToMemory(page); - - // Create file via API - createMemoryFileOnDisk(fileName, content); - await waitForNetworkIdle(page); - - // Refresh to load the file - await page.reload(); - await waitForMemoryFile(page, fileName); - - // Select and verify - should still recognize as markdown - await selectMemoryFile(page, fileName); - await waitForMemoryContentToLoad(page); - - const markdownPreview = page.locator('[data-testid="markdown-preview"]'); - await expect(markdownPreview).toBeVisible(); - }); - - test('should NOT show file without extension in file list', async ({ page }) => { - const fileName = 'README'; - const content = '# File Without Extension'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - await navigateToMemory(page); - - // Create file via API (without extension) - createMemoryFileOnDisk(fileName, content); - await waitForNetworkIdle(page); - - // Refresh to load the file - await page.reload(); - - // Wait a moment for files to load - await page.waitForTimeout(1000); - - // File should NOT appear in list because isMarkdownFile returns false for no extension - const fileButton = page.locator(`[data-testid="memory-file-${fileName}"]`); - await expect(fileButton).not.toBeVisible(); - }); - - test('should NOT create file without .md extension via UI', async ({ page }) => { - const fileName = 'NOTES'; - const content = '# Notes without extension'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - await navigateToMemory(page); - - // Create file via UI without extension - await clickElement(page, 'create-memory-button'); - await page.waitForSelector('[data-testid="create-memory-dialog"]', { timeout: 5000 }); - - await fillInput(page, 'new-memory-name', fileName); - await fillInput(page, 'new-memory-content', content); - - await clickElement(page, 'confirm-create-memory'); - await waitForElementHidden(page, 'create-memory-dialog', { timeout: 5000 }); - - await waitForNetworkIdle(page); - - // File should NOT appear in list because UI enforces .md extension - // (The UI may add .md automatically or show validation error) - const fileButton = page.locator(`[data-testid="memory-file-${fileName}"]`); - await expect(fileButton) - .not.toBeVisible({ timeout: 3000 }) - .catch(() => { - // It's OK if it doesn't appear - that's expected behavior - }); - }); - - test('should handle uppercase extensions', async ({ page }) => { - const fileName = 'uppercase.MD'; - const content = '# Uppercase Extension'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - await navigateToMemory(page); - - // Create file via API with uppercase extension - createMemoryFileOnDisk(fileName, content); - await waitForNetworkIdle(page); - - // Refresh to load the file - await page.reload(); - await waitForMemoryFile(page, fileName); - - // Select and verify - should recognize .MD as markdown (case-insensitive) - await selectMemoryFile(page, fileName); - await waitForMemoryContentToLoad(page); - - const markdownPreview = page.locator('[data-testid="markdown-preview"]'); - await expect(markdownPreview).toBeVisible(); - }); -}); diff --git a/apps/ui/tests/memory/mobile-memory-operations.spec.ts b/apps/ui/tests/memory/mobile-memory-operations.spec.ts deleted file mode 100644 index e35047f4..00000000 --- a/apps/ui/tests/memory/mobile-memory-operations.spec.ts +++ /dev/null @@ -1,174 +0,0 @@ -/** - * Mobile Memory View Operations E2E Tests - * - * Tests for file operations on mobile in the memory view: - * - Deleting files via dropdown menu on mobile - * - Creating files via mobile actions panel - */ - -import { test, expect, devices } from '@playwright/test'; -import { - resetMemoryDirectory, - setupProjectWithFixture, - getFixturePath, - navigateToMemory, - waitForMemoryFile, - clickElement, - fillInput, - waitForNetworkIdle, - authenticateForTests, - memoryFileExistsOnDisk, - waitForElementHidden, -} from '../utils'; - -// Use mobile viewport for mobile tests in Chromium CI -test.use({ ...devices['Pixel 5'] }); - -test.describe('Mobile Memory View Operations', () => { - test.beforeEach(async () => { - resetMemoryDirectory(); - }); - - test.afterEach(async () => { - resetMemoryDirectory(); - }); - - test('should create a file via mobile actions panel', async ({ page }) => { - const fileName = 'mobile-created.md'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - - await navigateToMemory(page); - - // Create a test file via mobile actions panel - await clickElement(page, 'header-actions-panel-trigger'); - await clickElement(page, 'create-memory-button-mobile'); - await page.waitForSelector('[data-testid="create-memory-dialog"]', { timeout: 5000 }); - - await fillInput(page, 'new-memory-name', fileName); - await fillInput(page, 'new-memory-content', '# Created on Mobile'); - - await clickElement(page, 'confirm-create-memory'); - - await waitForElementHidden(page, 'create-memory-dialog', { timeout: 5000 }); - - await waitForNetworkIdle(page); - await waitForMemoryFile(page, fileName); - - // Verify file appears in list - const fileButton = page.locator(`[data-testid="memory-file-${fileName}"]`); - await expect(fileButton).toBeVisible(); - - // Verify file exists on disk - expect(memoryFileExistsOnDisk(fileName)).toBe(true); - }); - - test('should delete a file via dropdown menu on mobile', async ({ page }) => { - const fileName = 'delete-via-menu-test.md'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - - await navigateToMemory(page); - - // Create a test file - await clickElement(page, 'header-actions-panel-trigger'); - await clickElement(page, 'create-memory-button-mobile'); - await page.waitForSelector('[data-testid="create-memory-dialog"]', { timeout: 5000 }); - - await fillInput(page, 'new-memory-name', fileName); - await fillInput(page, 'new-memory-content', '# File to Delete'); - - await clickElement(page, 'confirm-create-memory'); - - await waitForElementHidden(page, 'create-memory-dialog', { timeout: 5000 }); - - await waitForNetworkIdle(page); - await waitForMemoryFile(page, fileName); - - // Verify file exists - expect(memoryFileExistsOnDisk(fileName)).toBe(true); - - // Close actions panel if still open - await page.keyboard.press('Escape'); - await page.waitForTimeout(300); - - // Click on the file menu dropdown - hover first to make it visible - const fileRow = page.locator(`[data-testid="memory-file-${fileName}"]`); - await fileRow.hover(); - - const fileMenuButton = page.locator(`[data-testid="memory-file-menu-${fileName}"]`); - await fileMenuButton.click({ force: true }); - - // Wait for dropdown - await page.waitForTimeout(300); - - // Click delete in dropdown - const deleteMenuItem = page.locator(`[data-testid="delete-memory-file-${fileName}"]`); - await deleteMenuItem.click(); - - // Wait for file to be removed from list - await waitForElementHidden(page, `memory-file-${fileName}`, { timeout: 5000 }); - - // Verify file no longer exists on disk - expect(memoryFileExistsOnDisk(fileName)).toBe(false); - }); - - test('should refresh button be available in actions panel', async ({ page }) => { - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - - await navigateToMemory(page); - - // Open actions panel - await clickElement(page, 'header-actions-panel-trigger'); - - // Verify refresh button is visible in actions panel - const refreshButton = page.locator('[data-testid="refresh-memory-button-mobile"]'); - await expect(refreshButton).toBeVisible(); - }); - - test('should preview markdown content on mobile', async ({ page }) => { - const fileName = 'preview-test.md'; - const markdownContent = - '# Preview Test\n\n**Bold text** and *italic text*\n\n- List item 1\n- List item 2'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - - await navigateToMemory(page); - - // Create a test file - await clickElement(page, 'header-actions-panel-trigger'); - await clickElement(page, 'create-memory-button-mobile'); - await page.waitForSelector('[data-testid="create-memory-dialog"]', { timeout: 5000 }); - - await fillInput(page, 'new-memory-name', fileName); - await fillInput(page, 'new-memory-content', markdownContent); - - await clickElement(page, 'confirm-create-memory'); - - await waitForElementHidden(page, 'create-memory-dialog', { timeout: 5000 }); - - await waitForNetworkIdle(page); - await waitForMemoryFile(page, fileName); - - // Select the file by clicking on it - const fileButton = page.locator(`[data-testid="memory-file-${fileName}"]`); - await fileButton.click(); - - // Wait for content to load (preview or editor) - await page.waitForSelector('[data-testid="markdown-preview"], [data-testid="memory-editor"]', { - timeout: 5000, - }); - - // Memory files open in preview mode by default - const markdownPreview = page.locator('[data-testid="markdown-preview"]'); - await expect(markdownPreview).toBeVisible(); - - // Verify the preview rendered the markdown (check for h1) - const h1 = markdownPreview.locator('h1'); - await expect(h1).toHaveText('Preview Test'); - }); -}); diff --git a/apps/ui/tests/memory/mobile-memory-view.spec.ts b/apps/ui/tests/memory/mobile-memory-view.spec.ts deleted file mode 100644 index 3e135df4..00000000 --- a/apps/ui/tests/memory/mobile-memory-view.spec.ts +++ /dev/null @@ -1,273 +0,0 @@ -/** - * Mobile Memory View E2E Tests - * - * Tests for mobile-friendly behavior in the memory view: - * - File list hides when file is selected on mobile - * - Back button appears on mobile to return to file list - * - Toolbar buttons are icon-only on mobile - * - Delete button is hidden on mobile (use dropdown menu instead) - */ - -import { test, expect, devices } from '@playwright/test'; -import { - resetMemoryDirectory, - setupProjectWithFixture, - getFixturePath, - navigateToMemory, - waitForMemoryFile, - selectMemoryFile, - waitForMemoryContentToLoad, - clickElement, - fillInput, - waitForNetworkIdle, - authenticateForTests, - waitForElementHidden, -} from '../utils'; - -// Use mobile viewport for mobile tests in Chromium CI -test.use({ ...devices['Pixel 5'] }); - -test.describe('Mobile Memory View', () => { - test.beforeEach(async () => { - resetMemoryDirectory(); - }); - - test.afterEach(async () => { - resetMemoryDirectory(); - }); - - test('should hide file list when a file is selected on mobile', async ({ page }) => { - const fileName = 'mobile-test.md'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - - await navigateToMemory(page); - - // Create a test file - on mobile, open the actions panel first - await clickElement(page, 'header-actions-panel-trigger'); - await clickElement(page, 'create-memory-button-mobile'); - await page.waitForSelector('[data-testid="create-memory-dialog"]', { timeout: 5000 }); - - await fillInput(page, 'new-memory-name', fileName); - await fillInput(page, 'new-memory-content', '# Mobile Test\n\nThis tests mobile view behavior'); - - await clickElement(page, 'confirm-create-memory'); - - await waitForElementHidden(page, 'create-memory-dialog', { timeout: 5000 }); - - await waitForNetworkIdle(page); - await waitForMemoryFile(page, fileName); - - // File list should be visible before selection - const fileListBefore = page.locator('[data-testid="memory-file-list"]'); - await expect(fileListBefore).toBeVisible(); - - // Select the file - await selectMemoryFile(page, fileName); - await waitForMemoryContentToLoad(page); - - // On mobile, file list should be hidden after selection (full-screen editor) - const fileListAfter = page.locator('[data-testid="memory-file-list"]'); - await expect(fileListAfter).toBeHidden(); - }); - - test('should show back button in editor toolbar on mobile', async ({ page }) => { - const fileName = 'back-button-test.md'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - - await navigateToMemory(page); - - // Create a test file - on mobile, open the actions panel first - await clickElement(page, 'header-actions-panel-trigger'); - await clickElement(page, 'create-memory-button-mobile'); - await page.waitForSelector('[data-testid="create-memory-dialog"]', { timeout: 5000 }); - - await fillInput(page, 'new-memory-name', fileName); - await fillInput( - page, - 'new-memory-content', - '# Back Button Test\n\nTesting back button on mobile' - ); - - await clickElement(page, 'confirm-create-memory'); - - await waitForElementHidden(page, 'create-memory-dialog', { timeout: 5000 }); - - await waitForNetworkIdle(page); - await waitForMemoryFile(page, fileName); - - // Select the file - await selectMemoryFile(page, fileName); - await waitForMemoryContentToLoad(page); - - // Back button should be visible on mobile - const backButton = page.locator('button[aria-label="Back"]'); - await expect(backButton).toBeVisible(); - - // Back button should have ArrowLeft icon - const arrowIcon = backButton.locator('svg.lucide-arrow-left'); - await expect(arrowIcon).toBeVisible(); - }); - - test('should return to file list when back button is clicked on mobile', async ({ page }) => { - const fileName = 'back-navigation-test.md'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - - await navigateToMemory(page); - - // Create a test file - on mobile, open the actions panel first - await clickElement(page, 'header-actions-panel-trigger'); - await clickElement(page, 'create-memory-button-mobile'); - await page.waitForSelector('[data-testid="create-memory-dialog"]', { timeout: 5000 }); - - await fillInput(page, 'new-memory-name', fileName); - await fillInput(page, 'new-memory-content', '# Back Navigation Test'); - - await clickElement(page, 'confirm-create-memory'); - - await waitForElementHidden(page, 'create-memory-dialog', { timeout: 5000 }); - - await waitForNetworkIdle(page); - await waitForMemoryFile(page, fileName); - - // Select the file - await selectMemoryFile(page, fileName); - await waitForMemoryContentToLoad(page); - - // File list should be hidden after selection - const fileListHidden = page.locator('[data-testid="memory-file-list"]'); - await expect(fileListHidden).toBeHidden(); - - // Click back button - const backButton = page.locator('button[aria-label="Back"]'); - await backButton.click(); - - // File list should be visible again - const fileListVisible = page.locator('[data-testid="memory-file-list"]'); - await expect(fileListVisible).toBeVisible(); - - // Editor should no longer be visible - const editor = page.locator('[data-testid="memory-editor"]'); - await expect(editor).not.toBeVisible(); - }); - - test('should show icon-only buttons in toolbar on mobile', async ({ page }) => { - const fileName = 'icon-buttons-test.md'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - - await navigateToMemory(page); - - // Create a test file - on mobile, open the actions panel first - await clickElement(page, 'header-actions-panel-trigger'); - await clickElement(page, 'create-memory-button-mobile'); - await page.waitForSelector('[data-testid="create-memory-dialog"]', { timeout: 5000 }); - - await fillInput(page, 'new-memory-name', fileName); - await fillInput( - page, - 'new-memory-content', - '# Icon Buttons Test\n\nTesting icon-only buttons on mobile' - ); - - await clickElement(page, 'confirm-create-memory'); - - await waitForElementHidden(page, 'create-memory-dialog', { timeout: 5000 }); - - await waitForNetworkIdle(page); - await waitForMemoryFile(page, fileName); - - // Select the file - await selectMemoryFile(page, fileName); - await waitForMemoryContentToLoad(page); - - // Get the toggle preview mode button - const toggleButton = page.locator('[data-testid="toggle-preview-mode"]'); - await expect(toggleButton).toBeVisible(); - - // Button should have icon (Eye or Pencil) - const eyeIcon = toggleButton.locator('svg.lucide-eye'); - const pencilIcon = toggleButton.locator('svg.lucide-pencil'); - - // One of the icons should be present - const hasIcon = await (async () => { - const eyeVisible = await eyeIcon.isVisible().catch(() => false); - const pencilVisible = await pencilIcon.isVisible().catch(() => false); - return eyeVisible || pencilVisible; - })(); - - expect(hasIcon).toBe(true); - - // Text label should not be present (or minimal space on mobile) - const buttonText = await toggleButton.textContent(); - // On mobile, button should have icon only (no "Edit" or "Preview" text visible) - // The text is wrapped in {!isMobile && }, so it shouldn't render - expect(buttonText?.trim()).toBe(''); - }); - - test('should hide delete button in toolbar on mobile', async ({ page }) => { - const fileName = 'delete-button-test.md'; - - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - - await navigateToMemory(page); - - // Create a test file - on mobile, open the actions panel first - await clickElement(page, 'header-actions-panel-trigger'); - await clickElement(page, 'create-memory-button-mobile'); - await page.waitForSelector('[data-testid="create-memory-dialog"]', { timeout: 5000 }); - - await fillInput(page, 'new-memory-name', fileName); - await fillInput(page, 'new-memory-content', '# Delete Button Test'); - - await clickElement(page, 'confirm-create-memory'); - - await waitForElementHidden(page, 'create-memory-dialog', { timeout: 5000 }); - - await waitForNetworkIdle(page); - await waitForMemoryFile(page, fileName); - - // Select the file - await selectMemoryFile(page, fileName); - await waitForMemoryContentToLoad(page); - - // Delete button in toolbar should be hidden on mobile - const deleteButton = page.locator('[data-testid="delete-memory-file"]'); - await expect(deleteButton).not.toBeVisible(); - }); - - test('should show file list at full width on mobile when no file is selected', async ({ - page, - }) => { - await setupProjectWithFixture(page, getFixturePath()); - await authenticateForTests(page); - - await navigateToMemory(page); - - // File list should be visible - const fileList = page.locator('[data-testid="memory-file-list"]'); - await expect(fileList).toBeVisible(); - - // On mobile with no file selected, the file list should take full width - // Check that the file list container has the w-full class (mobile behavior) - const fileListBox = await fileList.boundingBox(); - expect(fileListBox).not.toBeNull(); - - if (fileListBox) { - // On mobile (Pixel 5 has width 393), the file list should take most of the width - // We check that it's significantly wider than the desktop w-64 (256px) - expect(fileListBox.width).toBeGreaterThan(300); - } - - // Editor panel should be hidden on mobile when no file is selected - const editor = page.locator('[data-testid="memory-editor"]'); - await expect(editor).not.toBeVisible(); - }); -}); diff --git a/apps/ui/tests/profiles/profiles-crud.spec.ts b/apps/ui/tests/profiles/profiles-crud.spec.ts deleted file mode 100644 index 7b174de7..00000000 --- a/apps/ui/tests/profiles/profiles-crud.spec.ts +++ /dev/null @@ -1,62 +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', () => { - // Skip: The profiles UI (standalone nav item, profile cards, add/edit dialogs) - // has not been implemented yet. The test references data-testid values that - // do not exist in the current codebase. - test.skip('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); - }); -}); diff --git a/apps/ui/tests/projects/board-background-persistence.spec.ts b/apps/ui/tests/projects/board-background-persistence.spec.ts deleted file mode 100644 index b336903d..00000000 --- a/apps/ui/tests/projects/board-background-persistence.spec.ts +++ /dev/null @@ -1,491 +0,0 @@ -/** - * Board Background Persistence End-to-End Test - * - * Tests that board background settings are properly saved and loaded when switching projects. - * This verifies that: - * 1. Background settings are saved to .automaker-local/settings.json - * 2. Settings are loaded when switching back to a project - * 3. Background image, opacity, and other settings are correctly restored - * 4. Settings persist across app restarts (new page loads) - * - * This test prevents regression of the board background loading bug where - * settings were saved but never loaded when switching projects. - */ - -import { test, expect } from '@playwright/test'; -import * as fs from 'fs'; -import * as path from 'path'; -import { - createTempDirPath, - cleanupTempDir, - authenticateForTests, - handleLoginScreenIfPresent, -} from '../utils'; - -// Create unique temp dirs for this test run -const TEST_TEMP_DIR = createTempDirPath('board-bg-test'); - -test.describe('Board Background Persistence', () => { - test.beforeAll(async () => { - // Create test temp directory - if (!fs.existsSync(TEST_TEMP_DIR)) { - fs.mkdirSync(TEST_TEMP_DIR, { recursive: true }); - } - }); - - test.afterAll(async () => { - // Cleanup temp directory - cleanupTempDir(TEST_TEMP_DIR); - }); - - test('should load board background settings when switching projects', async ({ page }) => { - const projectAName = `project-a-${Date.now()}`; - const projectBName = `project-b-${Date.now()}`; - const projectAPath = path.join(TEST_TEMP_DIR, projectAName); - const projectBPath = path.join(TEST_TEMP_DIR, projectBName); - const projectAId = `project-a-${Date.now()}`; - const projectBId = `project-b-${Date.now()}`; - - // Create both project directories - fs.mkdirSync(projectAPath, { recursive: true }); - fs.mkdirSync(projectBPath, { recursive: true }); - - // Create basic files for both projects - for (const [name, projectPath] of [ - [projectAName, projectAPath], - [projectBName, projectBPath], - ]) { - fs.writeFileSync( - path.join(projectPath, 'package.json'), - JSON.stringify({ name, version: '1.0.0' }, null, 2) - ); - fs.writeFileSync(path.join(projectPath, 'README.md'), `# ${name}\n`); - } - - // Create .automaker-local directory for project A with background settings - const automakerDirA = path.join(projectAPath, '.automaker-local'); - fs.mkdirSync(automakerDirA, { recursive: true }); - fs.mkdirSync(path.join(automakerDirA, 'board'), { recursive: true }); - fs.mkdirSync(path.join(automakerDirA, 'features'), { recursive: true }); - fs.mkdirSync(path.join(automakerDirA, 'context'), { recursive: true }); - - // Copy actual background image from test fixtures - const backgroundPath = path.join(automakerDirA, 'board', 'background.jpg'); - const testImagePath = path.join(__dirname, '..', 'img', 'background.jpg'); - fs.copyFileSync(testImagePath, backgroundPath); - - // Create settings.json with board background configuration - const settingsPath = path.join(automakerDirA, 'settings.json'); - const backgroundSettings = { - version: 1, - boardBackground: { - imagePath: backgroundPath, - cardOpacity: 85, - columnOpacity: 60, - columnBorderEnabled: true, - cardGlassmorphism: true, - cardBorderEnabled: false, - cardBorderOpacity: 50, - hideScrollbar: true, - imageVersion: Date.now(), - }, - }; - fs.writeFileSync(settingsPath, JSON.stringify(backgroundSettings, null, 2)); - - // Create minimal automaker-local directory for project B (no background) - const automakerDirB = path.join(projectBPath, '.automaker-local'); - fs.mkdirSync(automakerDirB, { recursive: true }); - fs.mkdirSync(path.join(automakerDirB, 'features'), { recursive: true }); - fs.mkdirSync(path.join(automakerDirB, 'context'), { recursive: true }); - fs.writeFileSync( - path.join(automakerDirB, 'settings.json'), - JSON.stringify({ version: 1 }, null, 2) - ); - - // Set up project A as the current project directly (skip welcome view). - // The auto-open logic in __root.tsx always opens the most recent project when - // navigating to /, so we cannot reliably show the welcome view with projects. - const projectA = { - id: projectAId, - name: projectAName, - path: projectAPath, - lastOpened: new Date().toISOString(), - }; - const projectB = { - id: projectBId, - name: projectBName, - path: projectBPath, - lastOpened: new Date(Date.now() - 86400000).toISOString(), - }; - - await page.addInitScript( - ({ - projects, - versions, - }: { - projects: Array<{ id: string; name: string; path: string; lastOpened: string }>; - versions: { APP_STORE: number; SETUP_STORE: number }; - }) => { - const appState = { - state: { - projects: projects, - currentProject: projects[0], - currentView: 'board', - theme: 'dark', - sidebarOpen: true, - skipSandboxWarning: true, - apiKeys: { anthropic: '', google: '' }, - chatSessions: [], - chatHistoryOpen: false, - maxConcurrency: 3, - boardBackgroundByProject: {}, - }, - version: versions.APP_STORE, - }; - localStorage.setItem('automaker-storage', JSON.stringify(appState)); - - const setupState = { - state: { - isFirstRun: false, - setupComplete: true, - skipClaudeSetup: false, - }, - version: versions.SETUP_STORE, - }; - localStorage.setItem('automaker-setup', JSON.stringify(setupState)); - - const settingsCache = { - setupComplete: true, - isFirstRun: false, - projects: projects.map((p) => ({ - id: p.id, - name: p.name, - path: p.path, - lastOpened: p.lastOpened, - })), - currentProjectId: projects[0].id, - theme: 'dark', - sidebarOpen: true, - maxConcurrency: 3, - }; - localStorage.setItem('automaker-settings-cache', JSON.stringify(settingsCache)); - - localStorage.setItem('automaker-disable-splash', 'true'); - }, - { projects: [projectA, projectB], versions: { APP_STORE: 2, SETUP_STORE: 1 } } - ); - - // Intercept settings API BEFORE authentication to ensure our test projects - // are consistently returned by the server. Only intercept GET requests - - // let PUT requests (settings saves) pass through unmodified. - await page.route('**/api/settings/global', async (route) => { - if (route.request().method() !== 'GET') { - await route.continue(); - return; - } - const response = await route.fetch(); - const json = await response.json(); - if (json.settings) { - json.settings.currentProjectId = projectAId; - json.settings.projects = [projectA, projectB]; - } - await route.fulfill({ response, json }); - }); - - // Track API calls to /api/settings/project to verify settings are being loaded - const settingsApiCalls: Array<{ url: string; method: string; body: string }> = []; - page.on('request', (request) => { - if (request.url().includes('/api/settings/project') && request.method() === 'POST') { - settingsApiCalls.push({ - url: request.url(), - method: request.method(), - body: request.postData() || '', - }); - } - }); - - await authenticateForTests(page); - - // Navigate to the board directly with project A - await page.goto('/board'); - await page.waitForLoadState('load'); - await handleLoginScreenIfPresent(page); - - // Wait for board view - await expect(page.locator('[data-testid="board-view"]')).toBeVisible({ timeout: 15000 }); - - // CRITICAL: Wait for settings to be loaded (useProjectSettingsLoader hook) - // This ensures the background settings are fetched from the server - await page.waitForTimeout(2000); - - // Check if background settings were applied by checking the store - // We can't directly access React state, so we'll verify via DOM/CSS - const boardView = page.locator('[data-testid="board-view"]'); - await expect(boardView).toBeVisible(); - - // Wait for initial project load to stabilize - await page.waitForTimeout(500); - - // Ensure sidebar is expanded before interacting with project selector - const expandSidebarButton = page.locator('button:has-text("Expand sidebar")'); - if (await expandSidebarButton.isVisible()) { - await expandSidebarButton.click(); - await page.waitForTimeout(300); - } - - // Switch to project B (no background) - const projectSelector = page.locator('[data-testid="project-dropdown-trigger"]'); - await expect(projectSelector).toBeVisible({ timeout: 5000 }); - await projectSelector.click(); - - // Wait for dropdown to be visible - await expect(page.locator('[data-testid="project-dropdown-content"]')).toBeVisible({ - timeout: 5000, - }); - - const projectPickerB = page.locator(`[data-testid="project-item-${projectBId}"]`); - await expect(projectPickerB).toBeVisible({ timeout: 5000 }); - await projectPickerB.click(); - - // Wait for project B to load - await expect( - page.locator('[data-testid="project-dropdown-trigger"]').getByText(projectBName) - ).toBeVisible({ timeout: 5000 }); - - // Wait a bit for project B to fully load before switching - await page.waitForTimeout(500); - - // Switch back to project A - await projectSelector.click(); - - // Wait for dropdown to be visible - await expect(page.locator('[data-testid="project-dropdown-content"]')).toBeVisible({ - timeout: 5000, - }); - - const projectPickerA = page.locator(`[data-testid="project-item-${projectAId}"]`); - await expect(projectPickerA).toBeVisible({ timeout: 5000 }); - await projectPickerA.click(); - - // Verify we're back on project A - await expect( - page.locator('[data-testid="project-dropdown-trigger"]').getByText(projectAName) - ).toBeVisible({ timeout: 5000 }); - - // CRITICAL: Wait for settings to be loaded again - await page.waitForTimeout(2000); - - // Verify that the settings API was called for project A at least once (initial load). - // Note: When switching back, the app may use cached settings and skip re-fetching. - const projectASettingsCalls = settingsApiCalls.filter((call) => - call.body.includes(projectAPath) - ); - - // Debug: log all API calls if test fails - if (projectASettingsCalls.length < 1) { - console.log('Total settings API calls:', settingsApiCalls.length); - console.log('API calls:', JSON.stringify(settingsApiCalls, null, 2)); - console.log('Looking for path:', projectAPath); - } - - expect(projectASettingsCalls.length).toBeGreaterThanOrEqual(1); - - // Verify settings file still exists with correct data - const loadedSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')); - expect(loadedSettings.boardBackground).toBeDefined(); - expect(loadedSettings.boardBackground.imagePath).toBe(backgroundPath); - expect(loadedSettings.boardBackground.cardOpacity).toBe(85); - expect(loadedSettings.boardBackground.columnOpacity).toBe(60); - expect(loadedSettings.boardBackground.hideScrollbar).toBe(true); - - // Clean up route handlers to avoid "route in flight" errors during teardown - await page.unrouteAll({ behavior: 'ignoreErrors' }); - - // The test passing means: - // 1. The useProjectSettingsLoader hook is working - // 2. Settings are loaded when switching projects - // 3. The API call to /api/settings/project is made correctly - }); - - test('should load background settings on app restart', async ({ page }) => { - const projectName = `restart-test-${Date.now()}`; - const projectPath = path.join(TEST_TEMP_DIR, projectName); - const projectId = `project-${Date.now()}`; - - // Create project directory - fs.mkdirSync(projectPath, { recursive: true }); - fs.writeFileSync( - path.join(projectPath, 'package.json'), - JSON.stringify({ name: projectName, version: '1.0.0' }, null, 2) - ); - - // Create .automaker-local with background settings - const automakerDir = path.join(projectPath, '.automaker-local'); - fs.mkdirSync(automakerDir, { recursive: true }); - fs.mkdirSync(path.join(automakerDir, 'board'), { recursive: true }); - fs.mkdirSync(path.join(automakerDir, 'features'), { recursive: true }); - fs.mkdirSync(path.join(automakerDir, 'context'), { recursive: true }); - - // Copy actual background image from test fixtures - const backgroundPath = path.join(automakerDir, 'board', 'background.jpg'); - const testImagePath = path.join(__dirname, '..', 'img', 'background.jpg'); - fs.copyFileSync(testImagePath, backgroundPath); - - const settingsPath = path.join(automakerDir, 'settings.json'); - fs.writeFileSync( - settingsPath, - JSON.stringify( - { - version: 1, - boardBackground: { - imagePath: backgroundPath, - cardOpacity: 90, - columnOpacity: 70, - imageVersion: Date.now(), - }, - }, - null, - 2 - ) - ); - - // Set up with project as current using direct localStorage - await page.addInitScript( - ({ project }: { project: string[] }) => { - const projectObj = { - id: project[0], - name: project[1], - path: project[2], - lastOpened: new Date().toISOString(), - }; - - const appState = { - state: { - projects: [projectObj], - currentProject: projectObj, - currentView: 'board', - theme: 'dark', - sidebarOpen: true, - skipSandboxWarning: true, - apiKeys: { anthropic: '', google: '' }, - chatSessions: [], - chatHistoryOpen: false, - maxConcurrency: 3, - boardBackgroundByProject: {}, - }, - version: 2, - }; - localStorage.setItem('automaker-storage', JSON.stringify(appState)); - - // Setup complete - use correct key name - const setupState = { - state: { - isFirstRun: false, - setupComplete: true, - skipClaudeSetup: false, - }, - version: 1, - }; - localStorage.setItem('automaker-setup', JSON.stringify(setupState)); - - const settingsCache = { - setupComplete: true, - isFirstRun: false, - projects: [ - { - id: projectObj.id, - name: projectObj.name, - path: projectObj.path, - lastOpened: projectObj.lastOpened, - }, - ], - currentProjectId: projectObj.id, - theme: 'dark', - sidebarOpen: true, - maxConcurrency: 3, - }; - localStorage.setItem('automaker-settings-cache', JSON.stringify(settingsCache)); - - // Disable splash screen in tests - localStorage.setItem('automaker-disable-splash', 'true'); - }, - { project: [projectId, projectName, projectPath] } - ); - - // Intercept settings API to use our test project instead of the E2E fixture. - // Only intercept GET requests - let PUT requests pass through unmodified. - await page.route('**/api/settings/global', async (route) => { - if (route.request().method() !== 'GET') { - await route.continue(); - return; - } - const response = await route.fetch(); - const json = await response.json(); - // Override to use our test project - if (json.settings) { - json.settings.currentProjectId = projectId; - json.settings.projects = [ - { - id: projectId, - name: projectName, - path: projectPath, - lastOpened: new Date().toISOString(), - }, - ]; - } - await route.fulfill({ response, json }); - }); - - // Track API calls to /api/settings/project to verify settings are being loaded - const settingsApiCalls: Array<{ url: string; method: string; body: string }> = []; - page.on('request', (request) => { - if (request.url().includes('/api/settings/project') && request.method() === 'POST') { - settingsApiCalls.push({ - url: request.url(), - method: request.method(), - body: request.postData() || '', - }); - } - }); - - await authenticateForTests(page); - - // Navigate to the app - await page.goto('/'); - await page.waitForLoadState('load'); - await handleLoginScreenIfPresent(page); - - // Should go straight to board view (not welcome) since we have currentProject - await expect(page.locator('[data-testid="board-view"]')).toBeVisible({ timeout: 15000 }); - - // Wait for settings to load - await page.waitForTimeout(2000); - - // Verify that the settings API was called for this project - const projectSettingsCalls = settingsApiCalls.filter((call) => call.body.includes(projectPath)); - - // Debug: log all API calls if test fails - if (projectSettingsCalls.length < 1) { - console.log('Total settings API calls:', settingsApiCalls.length); - console.log('API calls:', JSON.stringify(settingsApiCalls, null, 2)); - console.log('Looking for path:', projectPath); - } - - expect(projectSettingsCalls.length).toBeGreaterThanOrEqual(1); - - // Verify settings file exists with correct data - const loadedSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')); - expect(loadedSettings.boardBackground).toBeDefined(); - expect(loadedSettings.boardBackground.imagePath).toBe(backgroundPath); - expect(loadedSettings.boardBackground.cardOpacity).toBe(90); - expect(loadedSettings.boardBackground.columnOpacity).toBe(70); - - // Clean up route handlers to avoid "route in flight" errors during teardown - await page.unrouteAll({ behavior: 'ignoreErrors' }); - - // The test passing means: - // 1. The useProjectSettingsLoader hook is working - // 2. Settings are loaded when app starts with a currentProject - // 3. The API call to /api/settings/project is made correctly - }); -}); diff --git a/apps/ui/tests/utils/core/interactions.ts b/apps/ui/tests/utils/core/interactions.ts index ab743963..e4e82a92 100644 --- a/apps/ui/tests/utils/core/interactions.ts +++ b/apps/ui/tests/utils/core/interactions.ts @@ -1,5 +1,6 @@ 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) @@ -22,10 +23,10 @@ export async function pressModifierEnter(page: Page): Promise { * Waits for the element to be visible before clicking to avoid flaky tests */ export async function clickElement(page: Page, testId: string): Promise { - // Splash screen waits are handled by navigation helpers (navigateToContext, navigateToMemory, etc.) - // before any clickElement calls, so we skip the splash check here to avoid blocking when - // other fixed overlays (e.g. HeaderActionsPanel backdrop at z-[60]) are present on the page. + // 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(); } diff --git a/apps/ui/tests/utils/core/waiting.ts b/apps/ui/tests/utils/core/waiting.ts index 5dc142bd..4ec50c74 100644 --- a/apps/ui/tests/utils/core/waiting.ts +++ b/apps/ui/tests/utils/core/waiting.ts @@ -54,16 +54,13 @@ export async function waitForElementHidden( */ export async function waitForSplashScreenToDisappear(page: Page, timeout = 5000): Promise { try { - // Check if splash screen is disabled or already shown (fastest check) - const splashDisabled = await page.evaluate(() => { - return ( - localStorage.getItem('automaker-disable-splash') === 'true' || - localStorage.getItem('automaker-splash-shown-session') === 'true' - ); + // Check if splash screen is shown via sessionStorage first (fastest check) + const splashShown = await page.evaluate(() => { + return sessionStorage.getItem('automaker-splash-shown') === 'true'; }); - // If splash is disabled or already shown, it won't appear, so we're done - if (splashDisabled) { + // If splash is already marked as shown, it won't appear, so we're done + if (splashShown) { return; } @@ -72,11 +69,8 @@ export async function waitForSplashScreenToDisappear(page: Page, timeout = 5000) // We check for elements that match the splash screen pattern await page.waitForFunction( () => { - // Check if splash is disabled or already shown - if ( - localStorage.getItem('automaker-disable-splash') === 'true' || - localStorage.getItem('automaker-splash-shown-session') === 'true' - ) { + // Check if splash is marked as shown in sessionStorage + if (sessionStorage.getItem('automaker-splash-shown') === 'true') { return true; } diff --git a/apps/ui/tests/utils/git/worktree.ts b/apps/ui/tests/utils/git/worktree.ts index 81c52597..ec1dac9e 100644 --- a/apps/ui/tests/utils/git/worktree.ts +++ b/apps/ui/tests/utils/git/worktree.ts @@ -381,7 +381,7 @@ export async function setupProjectWithPath(page: Page, projectPath: string): Pro localStorage.setItem('automaker-setup', JSON.stringify(setupState)); // Disable splash screen in tests - localStorage.setItem('automaker-disable-splash', 'true'); + sessionStorage.setItem('automaker-splash-shown', 'true'); }, projectPath); } @@ -435,7 +435,7 @@ export async function setupProjectWithPathNoWorktrees( localStorage.setItem('automaker-setup', JSON.stringify(setupState)); // Disable splash screen in tests - localStorage.setItem('automaker-disable-splash', 'true'); + sessionStorage.setItem('automaker-splash-shown', 'true'); }, projectPath); } @@ -493,7 +493,7 @@ export async function setupProjectWithStaleWorktree( localStorage.setItem('automaker-setup', JSON.stringify(setupState)); // Disable splash screen in tests - localStorage.setItem('automaker-disable-splash', 'true'); + sessionStorage.setItem('automaker-splash-shown', 'true'); }, projectPath); } diff --git a/apps/ui/tests/utils/index.ts b/apps/ui/tests/utils/index.ts index e81cb7ca..276ae313 100644 --- a/apps/ui/tests/utils/index.ts +++ b/apps/ui/tests/utils/index.ts @@ -22,12 +22,10 @@ export * from './navigation/views'; // View-specific utilities export * from './views/board'; export * from './views/context'; -export * from './views/memory'; 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'; diff --git a/apps/ui/tests/utils/navigation/views.ts b/apps/ui/tests/utils/navigation/views.ts index 0d64a375..28bb07cf 100644 --- a/apps/ui/tests/utils/navigation/views.ts +++ b/apps/ui/tests/utils/navigation/views.ts @@ -12,12 +12,9 @@ export async function navigateToBoard(page: Page): Promise { // 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' }); + await page.goto('/board'); + await page.waitForLoadState('load'); // Wait for splash screen to disappear (safety net) await waitForSplashScreenToDisappear(page, 3000); @@ -37,13 +34,9 @@ export async function navigateToContext(page: Page): Promise { // 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' }); + await page.goto('/context'); + await page.waitForLoadState('load'); // Wait for splash screen to disappear (safety net) await waitForSplashScreenToDisappear(page, 3000); @@ -66,14 +59,6 @@ export async function navigateToContext(page: Page): Promise { // 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); - } } /** @@ -84,12 +69,9 @@ export async function navigateToSpec(page: Page): Promise { // 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' }); + await page.goto('/spec'); + await page.waitForLoadState('load'); // Wait for splash screen to disappear (safety net) await waitForSplashScreenToDisappear(page, 3000); @@ -123,12 +105,9 @@ export async function navigateToAgent(page: Page): Promise { // 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' }); + await page.goto('/agent'); + await page.waitForLoadState('load'); // Wait for splash screen to disappear (safety net) await waitForSplashScreenToDisappear(page, 3000); diff --git a/apps/ui/tests/utils/project/fixtures.spec.ts b/apps/ui/tests/utils/project/fixtures.spec.ts deleted file mode 100644 index 552ada47..00000000 --- a/apps/ui/tests/utils/project/fixtures.spec.ts +++ /dev/null @@ -1,180 +0,0 @@ -/** - * Tests for project fixture utilities - * - * Tests for path traversal guard and file operations in test fixtures - */ - -import { test, expect } from '@playwright/test'; -import { - createMemoryFileOnDisk, - memoryFileExistsOnDisk, - resetMemoryDirectory, - createContextFileOnDisk, - contextFileExistsOnDisk, - resetContextDirectory, -} from './fixtures'; - -test.describe('Memory Fixture Utilities', () => { - test.beforeEach(() => { - resetMemoryDirectory(); - }); - - test.afterEach(() => { - resetMemoryDirectory(); - }); - - test('should create and detect a valid memory file', () => { - const filename = 'test-file.md'; - const content = '# Test Content'; - - createMemoryFileOnDisk(filename, content); - - expect(memoryFileExistsOnDisk(filename)).toBe(true); - }); - - test('should return false for non-existent file', () => { - expect(memoryFileExistsOnDisk('non-existent.md')).toBe(false); - }); - - test('should reject path traversal attempt with ../', () => { - const maliciousFilename = '../../../etc/passwd'; - - expect(() => { - createMemoryFileOnDisk(maliciousFilename, 'malicious content'); - }).toThrow('Invalid memory filename'); - - expect(() => { - memoryFileExistsOnDisk(maliciousFilename); - }).toThrow('Invalid memory filename'); - }); - - test('should handle Windows-style path traversal attempt ..\\ (platform-dependent)', () => { - const maliciousFilename = '..\\..\\..\\windows\\system32\\config'; - - // On Unix/macOS, backslash is treated as a literal character in filenames, - // not as a path separator, so path.resolve doesn't traverse directories. - // This test documents that behavior - the guard works for Unix paths, - // but Windows-style backslashes are handled differently per platform. - // On macOS/Linux: backslash is a valid filename character - // On Windows: would need additional normalization to prevent traversal - expect(() => { - memoryFileExistsOnDisk(maliciousFilename); - }).not.toThrow(); - - // The file gets created with backslashes in the name (which is valid on Unix) - // but won't escape the directory - }); - - test('should reject absolute path attempt', () => { - const maliciousFilename = '/etc/passwd'; - - expect(() => { - createMemoryFileOnDisk(maliciousFilename, 'malicious content'); - }).toThrow('Invalid memory filename'); - - expect(() => { - memoryFileExistsOnDisk(maliciousFilename); - }).toThrow('Invalid memory filename'); - }); - - test('should accept nested paths within memory directory', () => { - // Note: This tests the boundary - if subdirectories are supported, - // this should pass; if not, it should throw - const nestedFilename = 'subfolder/nested-file.md'; - - // Currently, the implementation doesn't create subdirectories, - // so this would fail when trying to write. But the path itself - // is valid (doesn't escape the memory directory) - expect(() => { - memoryFileExistsOnDisk(nestedFilename); - }).not.toThrow(); - }); - - test('should handle filenames without extensions', () => { - const filename = 'README'; - - createMemoryFileOnDisk(filename, 'content without extension'); - - expect(memoryFileExistsOnDisk(filename)).toBe(true); - }); - - test('should handle filenames with multiple dots', () => { - const filename = 'my.file.name.md'; - - createMemoryFileOnDisk(filename, '# Multiple dots'); - - expect(memoryFileExistsOnDisk(filename)).toBe(true); - }); -}); - -test.describe('Context Fixture Utilities', () => { - test.beforeEach(() => { - resetContextDirectory(); - }); - - test.afterEach(() => { - resetContextDirectory(); - }); - - test('should create and detect a valid context file', () => { - const filename = 'test-context.md'; - const content = '# Test Context Content'; - - createContextFileOnDisk(filename, content); - - expect(contextFileExistsOnDisk(filename)).toBe(true); - }); - - test('should return false for non-existent context file', () => { - expect(contextFileExistsOnDisk('non-existent.md')).toBe(false); - }); - - test('should reject path traversal attempt with ../ for context files', () => { - const maliciousFilename = '../../../etc/passwd'; - - expect(() => { - createContextFileOnDisk(maliciousFilename, 'malicious content'); - }).toThrow('Invalid context filename'); - - expect(() => { - contextFileExistsOnDisk(maliciousFilename); - }).toThrow('Invalid context filename'); - }); - - test('should reject absolute path attempt for context files', () => { - const maliciousFilename = '/etc/passwd'; - - expect(() => { - createContextFileOnDisk(maliciousFilename, 'malicious content'); - }).toThrow('Invalid context filename'); - - expect(() => { - contextFileExistsOnDisk(maliciousFilename); - }).toThrow('Invalid context filename'); - }); - - test('should accept nested paths within context directory', () => { - const nestedFilename = 'subfolder/nested-file.md'; - - // The path itself is valid (doesn't escape the context directory) - expect(() => { - contextFileExistsOnDisk(nestedFilename); - }).not.toThrow(); - }); - - test('should handle filenames without extensions for context', () => { - const filename = 'README'; - - createContextFileOnDisk(filename, 'content without extension'); - - expect(contextFileExistsOnDisk(filename)).toBe(true); - }); - - test('should handle filenames with multiple dots for context', () => { - const filename = 'my.context.file.md'; - - createContextFileOnDisk(filename, '# Multiple dots'); - - expect(contextFileExistsOnDisk(filename)).toBe(true); - }); -}); diff --git a/apps/ui/tests/utils/project/fixtures.ts b/apps/ui/tests/utils/project/fixtures.ts index 5e00f6fe..f39d4817 100644 --- a/apps/ui/tests/utils/project/fixtures.ts +++ b/apps/ui/tests/utils/project/fixtures.ts @@ -17,7 +17,6 @@ const WORKSPACE_ROOT = getWorkspaceRoot(); const FIXTURE_PATH = path.join(WORKSPACE_ROOT, 'test/fixtures/projectA'); const SPEC_FILE_PATH = path.join(FIXTURE_PATH, '.automaker/app_spec.txt'); const CONTEXT_PATH = path.join(FIXTURE_PATH, '.automaker/context'); -const MEMORY_PATH = path.join(FIXTURE_PATH, '.automaker/memory'); // Original spec content for resetting between tests const ORIGINAL_SPEC_CONTENT = ` @@ -51,53 +50,11 @@ export function resetContextDirectory(): void { fs.mkdirSync(CONTEXT_PATH, { recursive: true }); } -/** - * Reset the memory directory to empty state - */ -export function resetMemoryDirectory(): void { - if (fs.existsSync(MEMORY_PATH)) { - fs.rmSync(MEMORY_PATH, { recursive: true }); - } - fs.mkdirSync(MEMORY_PATH, { recursive: true }); -} - -/** - * Resolve and validate a context fixture path to prevent path traversal - */ -function resolveContextFixturePath(filename: string): string { - const resolved = path.resolve(CONTEXT_PATH, filename); - const base = path.resolve(CONTEXT_PATH) + path.sep; - if (!resolved.startsWith(base)) { - throw new Error(`Invalid context filename: ${filename}`); - } - return resolved; -} - /** * Create a context file directly on disk (for test setup) */ export function createContextFileOnDisk(filename: string, content: string): void { - const filePath = resolveContextFixturePath(filename); - fs.writeFileSync(filePath, content); -} - -/** - * Resolve and validate a memory fixture path to prevent path traversal - */ -function resolveMemoryFixturePath(filename: string): string { - const resolved = path.resolve(MEMORY_PATH, filename); - const base = path.resolve(MEMORY_PATH) + path.sep; - if (!resolved.startsWith(base)) { - throw new Error(`Invalid memory filename: ${filename}`); - } - return resolved; -} - -/** - * Create a memory file directly on disk (for test setup) - */ -export function createMemoryFileOnDisk(filename: string, content: string): void { - const filePath = resolveMemoryFixturePath(filename); + const filePath = path.join(CONTEXT_PATH, filename); fs.writeFileSync(filePath, content); } @@ -105,15 +62,7 @@ export function createMemoryFileOnDisk(filename: string, content: string): void * Check if a context file exists on disk */ export function contextFileExistsOnDisk(filename: string): boolean { - const filePath = resolveContextFixturePath(filename); - return fs.existsSync(filePath); -} - -/** - * Check if a memory file exists on disk - */ -export function memoryFileExistsOnDisk(filename: string): boolean { - const filePath = resolveMemoryFixturePath(filename); + const filePath = path.join(CONTEXT_PATH, filename); return fs.existsSync(filePath); } @@ -163,29 +112,8 @@ export async function setupProjectWithFixture( }; localStorage.setItem('automaker-setup', JSON.stringify(setupState)); - // Set settings cache so the fast-hydrate path uses our fixture project. - // Without this, a stale settings cache from a previous test can override - // the project we just set in automaker-storage. - const settingsCache = { - setupComplete: true, - isFirstRun: false, - projects: [ - { - id: mockProject.id, - name: mockProject.name, - path: mockProject.path, - lastOpened: mockProject.lastOpened, - }, - ], - currentProjectId: mockProject.id, - theme: 'dark', - sidebarOpen: true, - maxConcurrency: 3, - }; - localStorage.setItem('automaker-settings-cache', JSON.stringify(settingsCache)); - // Disable splash screen in tests - localStorage.setItem('automaker-disable-splash', 'true'); + sessionStorage.setItem('automaker-splash-shown', 'true'); }, projectPath); } @@ -195,14 +123,3 @@ export async function setupProjectWithFixture( export function getFixturePath(): string { return FIXTURE_PATH; } - -/** - * Set up a mock project with the fixture path (for profile/settings tests that need a project). - * Options such as customProfilesCount are reserved for future use (e.g. mocking server profile state). - */ -export async function setupMockProjectWithProfiles( - page: Page, - _options?: { customProfilesCount?: number } -): Promise { - await setupProjectWithFixture(page, FIXTURE_PATH); -} diff --git a/apps/ui/tests/utils/project/setup.ts b/apps/ui/tests/utils/project/setup.ts index 526db47b..cd350bbf 100644 --- a/apps/ui/tests/utils/project/setup.ts +++ b/apps/ui/tests/utils/project/setup.ts @@ -84,9 +84,6 @@ export async function setupWelcomeView( setupComplete: true, isFirstRun: false, projects: opts?.recentProjects || [], - // Explicitly set currentProjectId to null so the fast-hydrate path - // does not restore a stale project from a previous test. - currentProjectId: null, theme: 'dark', sidebarOpen: true, maxConcurrency: 3, @@ -106,7 +103,7 @@ export async function setupWelcomeView( } // Disable splash screen in tests - localStorage.setItem('automaker-disable-splash', 'true'); + sessionStorage.setItem('automaker-splash-shown', 'true'); // Set up a mechanism to keep currentProject null even after settings hydration // Settings API might restore a project, so we override it after hydration @@ -229,7 +226,7 @@ export async function setupRealProject( localStorage.setItem('automaker-settings-cache', JSON.stringify(settingsCache)); // Disable splash screen in tests - localStorage.setItem('automaker-disable-splash', 'true'); + sessionStorage.setItem('automaker-splash-shown', 'true'); }, { path: projectPath, name: projectName, opts: options, versions: STORE_VERSIONS } ); @@ -294,7 +291,7 @@ export async function setupMockProject(page: Page): Promise { localStorage.setItem('automaker-settings-cache', JSON.stringify(settingsCache)); // Disable splash screen in tests - localStorage.setItem('automaker-disable-splash', 'true'); + sessionStorage.setItem('automaker-splash-shown', 'true'); }, STORE_VERSIONS); } @@ -426,7 +423,7 @@ export async function setupMockProjectAtConcurrencyLimit( localStorage.setItem('automaker-settings-cache', JSON.stringify(settingsCache)); // Disable splash screen in tests - localStorage.setItem('automaker-disable-splash', 'true'); + sessionStorage.setItem('automaker-splash-shown', 'true'); }, { maxConcurrency, runningTasks, versions: STORE_VERSIONS } ); @@ -508,7 +505,7 @@ export async function setupMockProjectWithFeatures( (window as { __mockFeatures?: unknown[] }).__mockFeatures = mockFeatures; // Disable splash screen in tests - localStorage.setItem('automaker-disable-splash', 'true'); + sessionStorage.setItem('automaker-splash-shown', 'true'); }, { opts: options, versions: STORE_VERSIONS } ); @@ -580,7 +577,7 @@ export async function setupMockProjectWithContextFile( localStorage.setItem('automaker-settings-cache', JSON.stringify(settingsCache)); // Disable splash screen in tests - localStorage.setItem('automaker-disable-splash', 'true'); + sessionStorage.setItem('automaker-splash-shown', 'true'); // Set up mock file system with a context file for the feature // This will be used by the mock electron API @@ -772,7 +769,7 @@ export async function setupEmptyLocalStorage(page: Page): Promise { localStorage.setItem('automaker-settings-cache', JSON.stringify(settingsCache)); // Disable splash screen in tests - localStorage.setItem('automaker-disable-splash', 'true'); + sessionStorage.setItem('automaker-splash-shown', 'true'); }, STORE_VERSIONS); } @@ -835,7 +832,7 @@ export async function setupMockProjectsWithoutCurrent(page: Page): Promise localStorage.setItem('automaker-settings-cache', JSON.stringify(settingsCache)); // Disable splash screen in tests - localStorage.setItem('automaker-disable-splash', 'true'); + sessionStorage.setItem('automaker-splash-shown', 'true'); }, STORE_VERSIONS); } @@ -913,7 +910,7 @@ export async function setupMockProjectWithSkipTestsFeatures( localStorage.setItem('automaker-settings-cache', JSON.stringify(settingsCache)); // Disable splash screen in tests - localStorage.setItem('automaker-disable-splash', 'true'); + sessionStorage.setItem('automaker-splash-shown', 'true'); }, { opts: options, versions: STORE_VERSIONS } ); @@ -988,7 +985,7 @@ export async function setupMockMultipleProjects( localStorage.setItem('automaker-settings-cache', JSON.stringify(settingsCache)); // Disable splash screen in tests - localStorage.setItem('automaker-disable-splash', 'true'); + sessionStorage.setItem('automaker-splash-shown', 'true'); }, { count: projectCount, versions: STORE_VERSIONS } ); @@ -1059,7 +1056,7 @@ export async function setupMockProjectWithAgentOutput( localStorage.setItem('automaker-settings-cache', JSON.stringify(settingsCache)); // Disable splash screen in tests - localStorage.setItem('automaker-disable-splash', 'true'); + sessionStorage.setItem('automaker-splash-shown', 'true'); // Set up mock file system with output content for the feature // Now uses features/{id}/agent-output.md path @@ -1218,7 +1215,7 @@ export async function setupFirstRun(page: Page): Promise { localStorage.setItem('automaker-settings-cache', JSON.stringify(settingsCache)); // Disable splash screen in tests - localStorage.setItem('automaker-disable-splash', 'true'); + sessionStorage.setItem('automaker-splash-shown', 'true'); }, STORE_VERSIONS); } @@ -1241,6 +1238,6 @@ export async function setupComplete(page: Page): Promise { localStorage.setItem('automaker-setup', JSON.stringify(setupState)); // Disable splash screen in tests - localStorage.setItem('automaker-disable-splash', 'true'); + sessionStorage.setItem('automaker-splash-shown', 'true'); }, STORE_VERSIONS); } diff --git a/apps/ui/tests/utils/views/context.ts b/apps/ui/tests/utils/views/context.ts index ad40553e..6032e947 100644 --- a/apps/ui/tests/utils/views/context.ts +++ b/apps/ui/tests/utils/views/context.ts @@ -97,22 +97,10 @@ export async function deleteSelectedContextFile(page: Page): Promise { */ export async function saveContextFile(page: Page): Promise { await clickElement(page, 'save-context-file'); - // Wait for save to complete across desktop/mobile variants - // On desktop: button text shows "Saved" - // On mobile: icon-only button uses aria-label or title + // Wait for save to complete (button shows "Saved") await page.waitForFunction( - () => { - const btn = document.querySelector('[data-testid="save-context-file"]'); - if (!btn) return false; - const stateText = [ - btn.textContent ?? '', - btn.getAttribute('aria-label') ?? '', - btn.getAttribute('title') ?? '', - ] - .join(' ') - .toLowerCase(); - return stateText.includes('saved'); - }, + () => + document.querySelector('[data-testid="save-context-file"]')?.textContent?.includes('Saved'), { timeout: 5000 } ); } @@ -150,16 +138,13 @@ export async function selectContextFile( ): Promise { const fileButton = await getByTestId(page, `context-file-${filename}`); - // Retry click + wait for content panel to handle timing issues - // Note: On mobile, delete button is hidden, so we wait for content panel instead + // Retry click + wait for delete button to handle timing issues await expect(async () => { // Use JavaScript click to ensure React onClick handler fires await fileButton.evaluate((el) => (el as HTMLButtonElement).click()); - // Wait for content to appear (editor, preview, or image) - const contentLocator = page.locator( - '[data-testid="context-editor"], [data-testid="markdown-preview"], [data-testid="image-preview"]' - ); - await expect(contentLocator).toBeVisible(); + // Wait for the file to be selected (toolbar with delete button becomes visible) + const deleteButton = await getByTestId(page, 'delete-context-file'); + await expect(deleteButton).toBeVisible(); }).toPass({ timeout, intervals: [500, 1000, 2000] }); } diff --git a/apps/ui/tests/utils/views/memory.ts b/apps/ui/tests/utils/views/memory.ts deleted file mode 100644 index 170f6a7e..00000000 --- a/apps/ui/tests/utils/views/memory.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { Page, Locator } from '@playwright/test'; -import { clickElement, fillInput, handleLoginScreenIfPresent } from '../core/interactions'; -import { - waitForElement, - waitForElementHidden, - waitForSplashScreenToDisappear, -} from '../core/waiting'; -import { getByTestId } from '../core/elements'; -import { expect } from '@playwright/test'; -import { authenticateForTests } from '../api/client'; - -/** - * Get the memory file list element - */ -export async function getMemoryFileList(page: Page): Promise { - return page.locator('[data-testid="memory-file-list"]'); -} - -/** - * Click on a memory file in the list - */ -export async function clickMemoryFile(page: Page, fileName: string): Promise { - const fileButton = page.locator(`[data-testid="memory-file-${fileName}"]`); - await fileButton.click(); -} - -/** - * Get the memory editor element - */ -export async function getMemoryEditor(page: Page): Promise { - return page.locator('[data-testid="memory-editor"]'); -} - -/** - * Get the memory editor content - */ -export async function getMemoryEditorContent(page: Page): Promise { - const editor = await getByTestId(page, 'memory-editor'); - return await editor.inputValue(); -} - -/** - * Set the memory editor content - */ -export async function setMemoryEditorContent(page: Page, content: string): Promise { - const editor = await getByTestId(page, 'memory-editor'); - await editor.fill(content); -} - -/** - * Open the create memory file dialog - */ -export async function openCreateMemoryDialog(page: Page): Promise { - await clickElement(page, 'create-memory-button'); - await waitForElement(page, 'create-memory-dialog'); -} - -/** - * Create a memory file via the UI - */ -export async function createMemoryFile( - page: Page, - filename: string, - content: string -): Promise { - await openCreateMemoryDialog(page); - await fillInput(page, 'new-memory-name', filename); - await fillInput(page, 'new-memory-content', content); - await clickElement(page, 'confirm-create-memory'); - await waitForElementHidden(page, 'create-memory-dialog'); -} - -/** - * Delete a memory file via the UI (must be selected first) - */ -export async function deleteSelectedMemoryFile(page: Page): Promise { - await clickElement(page, 'delete-memory-file'); - await waitForElement(page, 'delete-memory-dialog'); - await clickElement(page, 'confirm-delete-memory'); - await waitForElementHidden(page, 'delete-memory-dialog'); -} - -/** - * Save the current memory file - */ -export async function saveMemoryFile(page: Page): Promise { - await clickElement(page, 'save-memory-file'); - // Wait for save to complete across desktop/mobile variants - // On desktop: button text shows "Saved" - // On mobile: icon-only button uses aria-label or title - await page.waitForFunction( - () => { - const btn = document.querySelector('[data-testid="save-memory-file"]'); - if (!btn) return false; - const stateText = [ - btn.textContent ?? '', - btn.getAttribute('aria-label') ?? '', - btn.getAttribute('title') ?? '', - ] - .join(' ') - .toLowerCase(); - return stateText.includes('saved'); - }, - { timeout: 5000 } - ); -} - -/** - * Toggle markdown preview mode - */ -export async function toggleMemoryPreviewMode(page: Page): Promise { - await clickElement(page, 'toggle-preview-mode'); -} - -/** - * Wait for a specific file to appear in the memory file list - * Uses retry mechanism to handle race conditions with API/UI updates - */ -export async function waitForMemoryFile( - page: Page, - filename: string, - timeout: number = 15000 -): Promise { - await expect(async () => { - const locator = page.locator(`[data-testid="memory-file-${filename}"]`); - await expect(locator).toBeVisible(); - }).toPass({ timeout, intervals: [500, 1000, 2000] }); -} - -/** - * Click a file in the list and wait for it to be selected (toolbar visible) - * Uses retry mechanism to handle race conditions where element is visible but not yet interactive - */ -export async function selectMemoryFile( - page: Page, - filename: string, - timeout: number = 15000 -): Promise { - const fileButton = await getByTestId(page, `memory-file-${filename}`); - - // Retry click + wait for content panel to handle timing issues - // Note: On mobile, delete button is hidden, so we wait for content panel instead - await expect(async () => { - // Use JavaScript click to ensure React onClick handler fires - await fileButton.evaluate((el) => (el as HTMLButtonElement).click()); - // Wait for content to appear (editor or preview) - const contentLocator = page.locator( - '[data-testid="memory-editor"], [data-testid="markdown-preview"]' - ); - await expect(contentLocator).toBeVisible(); - }).toPass({ timeout, intervals: [500, 1000, 2000] }); -} - -/** - * Wait for file content panel to load (either editor or preview) - * Uses retry mechanism to handle race conditions with file selection - */ -export async function waitForMemoryContentToLoad( - page: Page, - timeout: number = 15000 -): Promise { - await expect(async () => { - const contentLocator = page.locator( - '[data-testid="memory-editor"], [data-testid="markdown-preview"]' - ); - await expect(contentLocator).toBeVisible(); - }).toPass({ timeout, intervals: [500, 1000, 2000] }); -} - -/** - * Switch from preview mode to edit mode for memory files - * Memory files open in preview mode by default, this helper switches to edit mode - */ -export async function switchMemoryToEditMode(page: Page): Promise { - // First wait for content to load - await waitForMemoryContentToLoad(page); - - const markdownPreview = await getByTestId(page, 'markdown-preview'); - const isPreview = await markdownPreview.isVisible().catch(() => false); - - if (isPreview) { - await clickElement(page, 'toggle-preview-mode'); - await page.waitForSelector('[data-testid="memory-editor"]', { - timeout: 5000, - }); - } -} - -/** - * Navigate to the memory view - * Note: Navigates directly to /memory since index route shows WelcomeView - */ -export async function navigateToMemory(page: Page): Promise { - // Authenticate before navigating (same pattern as navigateToContext / navigateToBoard) - 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 /memory route - await page.goto('/memory', { waitUntil: 'domcontentloaded' }); - - // Wait for splash screen to disappear (safety net) - await waitForSplashScreenToDisappear(page, 3000); - - // Handle login redirect if needed (e.g. when redirected to /logged-out) - await handleLoginScreenIfPresent(page); - - // Wait for loading to complete (if present) - const loadingElement = page.locator('[data-testid="memory-view-loading"]'); - try { - const loadingVisible = await loadingElement.isVisible({ timeout: 2000 }); - if (loadingVisible) { - // Wait for loading to disappear (memory view will appear) - await loadingElement.waitFor({ state: 'hidden', timeout: 10000 }); - } - } catch { - // Loading element not found or already hidden, continue - } - - // Wait for the memory view to be visible - await waitForElement(page, 'memory-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); - } - - // Ensure the header (and actions panel trigger on mobile) is interactive - await page - .locator('[data-testid="header-actions-panel-trigger"]') - .waitFor({ state: 'visible', timeout: 5000 }) - .catch(() => {}); -} diff --git a/apps/ui/tests/utils/views/profiles.ts b/apps/ui/tests/utils/views/profiles.ts deleted file mode 100644 index d03e1c34..00000000 --- a/apps/ui/tests/utils/views/profiles.ts +++ /dev/null @@ -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 { - // 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'); -} diff --git a/test/opus-thinking-level-none-83449-2bgz7j1/test-project-1772086066067/package.json b/test/opus-thinking-level-none-83449-2bgz7j1/test-project-1772086066067/package.json deleted file mode 100644 index 6602b030..00000000 --- a/test/opus-thinking-level-none-83449-2bgz7j1/test-project-1772086066067/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "test-project-1772086066067", - "version": "1.0.0" -} diff --git a/test/running-task-display-test-805-6c4ockc/test-project-1772088506096/README.md b/test/running-task-display-test-805-6c4ockc/test-project-1772088506096/README.md deleted file mode 100644 index a57f3357..00000000 --- a/test/running-task-display-test-805-6c4ockc/test-project-1772088506096/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Test Feature Project - -This is a test project for demonstrating the Automaker system's feature implementation capabilities. - -## Feature Implementation - -The test feature has been successfully implemented to demonstrate: - -1. Code creation and modification -2. File system operations -3. Agent workflow verification - -## Status - -✅ Test feature implementation completed diff --git a/test/running-task-display-test-805-6c4ockc/test-project-1772088506096/test-feature.js b/test/running-task-display-test-805-6c4ockc/test-project-1772088506096/test-feature.js deleted file mode 100644 index 30286f4a..00000000 --- a/test/running-task-display-test-805-6c4ockc/test-project-1772088506096/test-feature.js +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Test Feature Implementation - * - * This file demonstrates a simple test feature implementation - * for validating the Automaker system workflow. - */ - -class TestFeature { - constructor(name = 'Test Feature') { - this.name = name; - this.status = 'running'; - this.createdAt = new Date().toISOString(); - } - - /** - * Execute the test feature - * @returns {Object} Execution result - */ - execute() { - console.log(`Executing ${this.name}...`); - - const result = { - success: true, - message: 'Test feature executed successfully', - timestamp: new Date().toISOString(), - feature: this.name, - }; - - this.status = 'completed'; - return result; - } - - /** - * Get feature status - * @returns {string} Current status - */ - getStatus() { - return this.status; - } - - /** - * Get feature info - * @returns {Object} Feature information - */ - getInfo() { - return { - name: this.name, - status: this.status, - createdAt: this.createdAt, - }; - } -} - -// Export for use in tests -module.exports = TestFeature; - -// Example usage -if (require.main === module) { - const feature = new TestFeature(); - const result = feature.execute(); - console.log(JSON.stringify(result, null, 2)); -} diff --git a/test/running-task-display-test-805-6c4ockc/test-project-1772088506096/test-feature.test.js b/test/running-task-display-test-805-6c4ockc/test-project-1772088506096/test-feature.test.js deleted file mode 100644 index 169ea75e..00000000 --- a/test/running-task-display-test-805-6c4ockc/test-project-1772088506096/test-feature.test.js +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Test Feature Unit Tests - * - * Simple tests to verify the test feature implementation - */ - -const TestFeature = require('./test-feature'); - -function runTests() { - let passed = 0; - let failed = 0; - - console.log('Running Test Feature Tests...\n'); - - // Test 1: Feature creation - try { - const feature = new TestFeature('Test Feature'); - if (feature.name === 'Test Feature' && feature.status === 'running') { - console.log('✓ Test 1: Feature creation - PASSED'); - passed++; - } else { - console.log('✗ Test 1: Feature creation - FAILED'); - failed++; - } - } catch (error) { - console.log('✗ Test 1: Feature creation - FAILED:', error.message); - failed++; - } - - // Test 2: Feature execution - try { - const feature = new TestFeature(); - const result = feature.execute(); - if (result.success === true && feature.status === 'completed') { - console.log('✓ Test 2: Feature execution - PASSED'); - passed++; - } else { - console.log('✗ Test 2: Feature execution - FAILED'); - failed++; - } - } catch (error) { - console.log('✗ Test 2: Feature execution - FAILED:', error.message); - failed++; - } - - // Test 3: Get status - try { - const feature = new TestFeature(); - const status = feature.getStatus(); - if (status === 'running') { - console.log('✓ Test 3: Get status - PASSED'); - passed++; - } else { - console.log('✗ Test 3: Get status - FAILED'); - failed++; - } - } catch (error) { - console.log('✗ Test 3: Get status - FAILED:', error.message); - failed++; - } - - // Test 4: Get info - try { - const feature = new TestFeature('My Test Feature'); - const info = feature.getInfo(); - if (info.name === 'My Test Feature' && info.status === 'running' && info.createdAt) { - console.log('✓ Test 4: Get info - PASSED'); - passed++; - } else { - console.log('✗ Test 4: Get info - FAILED'); - failed++; - } - } catch (error) { - console.log('✗ Test 4: Get info - FAILED:', error.message); - failed++; - } - - console.log(`\nTest Results: ${passed} passed, ${failed} failed`); - return failed === 0; -} - -// Run tests -if (require.main === module) { - const success = runTests(); - process.exit(success ? 0 : 1); -} - -module.exports = { runTests };