import { test, expect } from "@playwright/test"; import * as fs from "fs"; import * as path from "path"; import { resetContextDirectory, createContextFileOnDisk, contextFileExistsOnDisk, setupProjectWithFixture, getFixturePath, navigateToContext, waitForFileContentToLoad, switchToEditMode, waitForContextFile, selectContextFile, simulateFileDrop, setContextEditorContent, getContextEditorContent, clickElement, fillInput, getByTestId, waitForNetworkIdle, } from "./utils"; const WORKSPACE_ROOT = path.resolve(process.cwd(), "../.."); const TEST_IMAGE_SRC = path.join(WORKSPACE_ROOT, "apps/ui/public/logo.png"); // Configure all tests to run serially to prevent interference with shared context directory test.describe.configure({ mode: "serial" }); // ============================================================================ // Test Suite 1: Context View - File Management // ============================================================================ test.describe("Context View - File Management", () => { test.beforeEach(async () => { resetContextDirectory(); }); test.afterEach(async () => { resetContextDirectory(); }); test("should create a new MD context file", async ({ page }) => { await setupProjectWithFixture(page, getFixturePath()); await page.goto("/"); await waitForNetworkIdle(page); await navigateToContext(page); // Click Add File button await clickElement(page, "add-context-file"); await page.waitForSelector('[data-testid="add-context-dialog"]', { timeout: 5000, }); // Select text type (should be default) await clickElement(page, "add-text-type"); // Enter filename await fillInput(page, "new-file-name", "test-context.md"); // Enter content const testContent = "# Test Context\n\nThis is test content"; await fillInput(page, "new-file-content", testContent); // Click confirm await clickElement(page, "confirm-add-file"); // Wait for dialog to close await page.waitForFunction( () => !document.querySelector('[data-testid="add-context-dialog"]'), { timeout: 5000 } ); // Wait for file list to refresh (file should appear) await waitForContextFile(page, "test-context.md", 10000); // Verify file appears in list const fileButton = await getByTestId(page, "context-file-test-context.md"); await expect(fileButton).toBeVisible(); // Click on the file and wait for it to be selected await selectContextFile(page, "test-context.md"); // Wait for content to load await waitForFileContentToLoad(page); // Switch to edit mode if in preview mode (markdown files default to preview) await switchToEditMode(page); // Wait for editor to be visible await page.waitForSelector('[data-testid="context-editor"]', { timeout: 5000, }); // Verify content in editor const editorContent = await getContextEditorContent(page); expect(editorContent).toBe(testContent); }); test("should edit an existing MD context file", async ({ page }) => { // Create a test file on disk first const originalContent = "# Original Content\n\nThis will be edited."; createContextFileOnDisk("edit-test.md", originalContent); await setupProjectWithFixture(page, getFixturePath()); await page.goto("/"); await waitForNetworkIdle(page); await navigateToContext(page); // Click on the existing file and wait for it to be selected await selectContextFile(page, "edit-test.md"); // Wait for file content to load await waitForFileContentToLoad(page); // Switch to edit mode (markdown files open in preview mode by default) await switchToEditMode(page); // Wait for editor await page.waitForSelector('[data-testid="context-editor"]', { timeout: 5000, }); // Modify content const newContent = "# Modified Content\n\nThis has been edited."; await setContextEditorContent(page, newContent); // Click save await clickElement(page, "save-context-file"); // Wait for save to complete await page.waitForFunction( () => document .querySelector('[data-testid="save-context-file"]') ?.textContent?.includes("Saved"), { timeout: 5000 } ); // Reload page await page.reload(); await waitForNetworkIdle(page); // Navigate back to context view await navigateToContext(page); // Wait for file to appear after reload and select it await selectContextFile(page, "edit-test.md"); // Wait for content to load await waitForFileContentToLoad(page); // Switch to edit mode (markdown files open in preview mode) await switchToEditMode(page); await page.waitForSelector('[data-testid="context-editor"]', { timeout: 5000, }); // Verify content persisted const persistedContent = await getContextEditorContent(page); expect(persistedContent).toBe(newContent); }); test("should remove an MD context file", async ({ page }) => { // Create a test file on disk first createContextFileOnDisk("delete-test.md", "# Delete Me"); await setupProjectWithFixture(page, getFixturePath()); await page.goto("/"); await waitForNetworkIdle(page); await navigateToContext(page); // Click on the file to select it const fileButton = await getByTestId(page, "context-file-delete-test.md"); await fileButton.waitFor({ state: "visible", timeout: 5000 }); await fileButton.click(); // Click delete button await clickElement(page, "delete-context-file"); // Wait for delete dialog await page.waitForSelector('[data-testid="delete-context-dialog"]', { timeout: 5000, }); // Confirm deletion await clickElement(page, "confirm-delete-file"); // Wait for dialog to close await page.waitForFunction( () => !document.querySelector('[data-testid="delete-context-dialog"]'), { timeout: 5000 } ); // Verify file is removed from list const deletedFile = await getByTestId(page, "context-file-delete-test.md"); await expect(deletedFile).not.toBeVisible(); // Verify file is removed from disk expect(contextFileExistsOnDisk("delete-test.md")).toBe(false); }); test("should upload an image context file", async ({ page }) => { await setupProjectWithFixture(page, getFixturePath()); await page.goto("/"); await waitForNetworkIdle(page); await navigateToContext(page); // Click Add File button await clickElement(page, "add-context-file"); await page.waitForSelector('[data-testid="add-context-dialog"]', { timeout: 5000, }); // Select image type await clickElement(page, "add-image-type"); // Enter filename await fillInput(page, "new-file-name", "test-image.png"); // Upload image using file input await page.setInputFiles( '[data-testid="image-upload-input"]', TEST_IMAGE_SRC ); // Wait for image preview to appear (indicates upload success) const addDialog = await getByTestId(page, "add-context-dialog"); await addDialog.locator("img").waitFor({ state: "visible" }); // Click confirm await clickElement(page, "confirm-add-file"); // Wait for dialog to close await page.waitForFunction( () => !document.querySelector('[data-testid="add-context-dialog"]'), { timeout: 5000 } ); // Verify file appears in list const fileButton = await getByTestId(page, "context-file-test-image.png"); await expect(fileButton).toBeVisible(); // Click on the image to view it await fileButton.click(); // Verify image preview is displayed await page.waitForSelector('[data-testid="image-preview"]', { timeout: 5000, }); const imagePreview = await getByTestId(page, "image-preview"); await expect(imagePreview).toBeVisible(); }); test("should remove an image context file", async ({ page }) => { // Create a test image file on disk as base64 data URL (matching app's storage format) const imageContent = fs.readFileSync(TEST_IMAGE_SRC); const base64DataUrl = `data:image/png;base64,${imageContent.toString("base64")}`; const contextPath = path.join(getFixturePath(), ".automaker/context"); fs.writeFileSync(path.join(contextPath, "delete-image.png"), base64DataUrl); await setupProjectWithFixture(page, getFixturePath()); await page.goto("/"); await waitForNetworkIdle(page); await navigateToContext(page); // Wait for the image file and select it await selectContextFile(page, "delete-image.png"); // Wait for file content (image preview) to load await waitForFileContentToLoad(page); // Click delete button await clickElement(page, "delete-context-file"); // Wait for delete dialog await page.waitForSelector('[data-testid="delete-context-dialog"]', { timeout: 5000, }); // Confirm deletion await clickElement(page, "confirm-delete-file"); // Wait for dialog to close await page.waitForFunction( () => !document.querySelector('[data-testid="delete-context-dialog"]'), { timeout: 5000 } ); // Verify file is removed from list const deletedImageFile = await getByTestId(page, "context-file-delete-image.png"); await expect(deletedImageFile).not.toBeVisible(); }); test("should toggle markdown preview mode", async ({ page }) => { // Create a markdown file with content const mdContent = "# Heading\n\n**Bold text** and *italic text*\n\n- List item 1\n- List item 2"; createContextFileOnDisk("preview-test.md", mdContent); await setupProjectWithFixture(page, getFixturePath()); await page.goto("/"); await waitForNetworkIdle(page); await navigateToContext(page); // Click on the markdown file const fileButton = await getByTestId(page, "context-file-preview-test.md"); await fileButton.waitFor({ state: "visible", timeout: 5000 }); await fileButton.click(); // Wait for content to load (markdown files open in preview mode by default) await waitForFileContentToLoad(page); // Check if preview button is visible (indicates it's a markdown file) const previewToggle = await getByTestId(page, "toggle-preview-mode"); await expect(previewToggle).toBeVisible(); // Markdown files always open in preview mode by default (see context-view.tsx:163) // Verify we're in preview mode const markdownPreview = await getByTestId(page, "markdown-preview"); await expect(markdownPreview).toBeVisible(); // Click to switch to edit mode await previewToggle.click(); await page.waitForSelector('[data-testid="context-editor"]', { timeout: 5000, }); // Verify editor is shown const editor = await getByTestId(page, "context-editor"); await expect(editor).toBeVisible(); await expect(markdownPreview).not.toBeVisible(); // Click to switch back to preview mode await previewToggle.click(); await page.waitForSelector('[data-testid="markdown-preview"]', { timeout: 5000, }); // Verify preview is shown await expect(markdownPreview).toBeVisible(); }); }); // ============================================================================ // Test Suite 2: Context View - Drag and Drop // ============================================================================ test.describe("Context View - Drag and Drop", () => { test.beforeEach(async () => { resetContextDirectory(); }); test.afterEach(async () => { resetContextDirectory(); }); test("should handle drag and drop of MD file onto textarea in add dialog", async ({ page, }) => { await setupProjectWithFixture(page, getFixturePath()); await page.goto("/"); await waitForNetworkIdle(page); await navigateToContext(page); // Open add file dialog await clickElement(page, "add-context-file"); await page.waitForSelector('[data-testid="add-context-dialog"]', { timeout: 5000, }); // Ensure text type is selected await clickElement(page, "add-text-type"); // Simulate drag and drop of a .md file onto the textarea const droppedContent = "# Dropped Content\n\nThis was dragged and dropped."; await simulateFileDrop( page, '[data-testid="new-file-content"]', "dropped-file.md", droppedContent ); // Wait for content to be populated in textarea const textarea = await getByTestId(page, "new-file-content"); await textarea.waitFor({ state: "visible" }); await expect(textarea).toHaveValue(droppedContent); // Verify content is populated in textarea const textareaContent = await textarea.inputValue(); expect(textareaContent).toBe(droppedContent); // Verify filename is auto-filled const filenameValue = await page .locator('[data-testid="new-file-name"]') .inputValue(); expect(filenameValue).toBe("dropped-file.md"); // Confirm and create the file await clickElement(page, "confirm-add-file"); // Wait for dialog to close await page.waitForFunction( () => !document.querySelector('[data-testid="add-context-dialog"]'), { timeout: 5000 } ); // Verify file was created const droppedFile = await getByTestId(page, "context-file-dropped-file.md"); await expect(droppedFile).toBeVisible(); }); test("should handle drag and drop of file onto main view", async ({ page, }) => { await setupProjectWithFixture(page, getFixturePath()); await page.goto("/"); await waitForNetworkIdle(page); await navigateToContext(page); // Wait for the context view to be fully loaded await page.waitForSelector('[data-testid="context-file-list"]', { timeout: 5000, }); // Simulate drag and drop onto the drop zone const droppedContent = "This is a text file dropped onto the main view."; await simulateFileDrop( page, '[data-testid="context-drop-zone"]', "main-drop.txt", droppedContent ); // Wait for file to appear in the list (drag-drop triggers file creation) await waitForContextFile(page, "main-drop.txt", 15000); // Verify file appears in the file list const fileButton = await getByTestId(page, "context-file-main-drop.txt"); await expect(fileButton).toBeVisible(); // Select file and verify content await fileButton.click(); await page.waitForSelector('[data-testid="context-editor"]', { timeout: 5000, }); const editorContent = await getContextEditorContent(page); expect(editorContent).toBe(droppedContent); }); }); // ============================================================================ // Test Suite 3: Context View - Edge Cases // ============================================================================ test.describe("Context View - Edge Cases", () => { test.beforeEach(async () => { resetContextDirectory(); }); test.afterEach(async () => { resetContextDirectory(); }); test("should handle duplicate filename (overwrite behavior)", async ({ page, }) => { // Create an existing file createContextFileOnDisk("test.md", "# Original Content"); await setupProjectWithFixture(page, getFixturePath()); await page.goto("/"); await waitForNetworkIdle(page); await navigateToContext(page); // Verify the original file exists const originalFile = await getByTestId(page, "context-file-test.md"); await expect(originalFile).toBeVisible(); // Try to create another file with the same name await clickElement(page, "add-context-file"); await page.waitForSelector('[data-testid="add-context-dialog"]', { timeout: 5000, }); await clickElement(page, "add-text-type"); await fillInput(page, "new-file-name", "test.md"); await fillInput(page, "new-file-content", "# New Content - Overwritten"); await clickElement(page, "confirm-add-file"); // Wait for dialog to close await page.waitForFunction( () => !document.querySelector('[data-testid="add-context-dialog"]'), { timeout: 5000 } ); // File should still exist (was overwritten) await expect(originalFile).toBeVisible(); // Select the file and verify the new content await originalFile.click(); // Wait for content to load await waitForFileContentToLoad(page); // Switch to edit mode (markdown files open in preview mode) await switchToEditMode(page); await page.waitForSelector('[data-testid="context-editor"]', { timeout: 5000, }); const editorContent = await getContextEditorContent(page); expect(editorContent).toBe("# New Content - Overwritten"); }); test("should handle special characters in filename", async ({ page }) => { await setupProjectWithFixture(page, getFixturePath()); await page.goto("/"); await waitForNetworkIdle(page); await navigateToContext(page); // Test file with parentheses await clickElement(page, "add-context-file"); await page.waitForSelector('[data-testid="add-context-dialog"]', { timeout: 5000, }); await clickElement(page, "add-text-type"); await fillInput(page, "new-file-name", "context (1).md"); await fillInput(page, "new-file-content", "Content with parentheses in filename"); await clickElement(page, "confirm-add-file"); await page.waitForFunction( () => !document.querySelector('[data-testid="add-context-dialog"]'), { timeout: 5000 } ); // Verify file is created - use CSS escape for special characters const fileWithParens = await getByTestId(page, "context-file-context (1).md"); await expect(fileWithParens).toBeVisible(); // Test file with hyphens and underscores await clickElement(page, "add-context-file"); await page.waitForSelector('[data-testid="add-context-dialog"]', { timeout: 5000, }); await clickElement(page, "add-text-type"); await fillInput(page, "new-file-name", "test-file_v2.md"); await fillInput(page, "new-file-content", "Content with hyphens and underscores"); await clickElement(page, "confirm-add-file"); await page.waitForFunction( () => !document.querySelector('[data-testid="add-context-dialog"]'), { timeout: 5000 } ); // Verify file is created const fileWithHyphens = await getByTestId(page, "context-file-test-file_v2.md"); await expect(fileWithHyphens).toBeVisible(); // Verify both files are accessible await fileWithHyphens.click(); // Wait for content to load await waitForFileContentToLoad(page); // Switch to edit mode (markdown files open in preview mode) await switchToEditMode(page); await page.waitForSelector('[data-testid="context-editor"]', { timeout: 5000, }); const content = await getContextEditorContent(page); expect(content).toBe("Content with hyphens and underscores"); }); test("should handle empty content", async ({ page }) => { await setupProjectWithFixture(page, getFixturePath()); await page.goto("/"); await waitForNetworkIdle(page); await navigateToContext(page); // Create file with empty content await clickElement(page, "add-context-file"); await page.waitForSelector('[data-testid="add-context-dialog"]', { timeout: 5000, }); await clickElement(page, "add-text-type"); await fillInput(page, "new-file-name", "empty-file.md"); // Don't fill any content - leave it empty await clickElement(page, "confirm-add-file"); await page.waitForFunction( () => !document.querySelector('[data-testid="add-context-dialog"]'), { timeout: 5000 } ); // Verify file is created const emptyFile = await getByTestId(page, "context-file-empty-file.md"); await expect(emptyFile).toBeVisible(); // Select file and verify editor shows empty content await emptyFile.click(); // Wait for content to load await waitForFileContentToLoad(page); // Switch to edit mode (markdown files open in preview mode) await switchToEditMode(page); await page.waitForSelector('[data-testid="context-editor"]', { timeout: 5000, }); const editorContent = await getContextEditorContent(page); expect(editorContent).toBe(""); // Verify save works with empty content // The save button should be disabled when there are no changes // Let's add some content first, then clear it and save await setContextEditorContent(page, "temporary"); await setContextEditorContent(page, ""); // Save should work await clickElement(page, "save-context-file"); await page.waitForFunction( () => document .querySelector('[data-testid="save-context-file"]') ?.textContent?.includes("Saved"), { timeout: 5000 } ); }); test("should verify persistence across page refresh", async ({ page }) => { // Create a file directly on disk to ensure it persists across refreshes const testContent = "# Persistence Test\n\nThis content should persist."; createContextFileOnDisk("persist-test.md", testContent); await setupProjectWithFixture(page, getFixturePath()); await page.goto("/"); await waitForNetworkIdle(page); await navigateToContext(page); // Verify file exists before refresh await waitForContextFile(page, "persist-test.md", 10000); // Refresh the page await page.reload(); await waitForNetworkIdle(page); // Navigate back to context view await navigateToContext(page); // Select the file after refresh (uses robust clicking mechanism) await selectContextFile(page, "persist-test.md"); // Wait for file content to load await waitForFileContentToLoad(page); // Switch to edit mode (markdown files open in preview mode) await switchToEditMode(page); await page.waitForSelector('[data-testid="context-editor"]', { timeout: 5000, }); const persistedContent = await getContextEditorContent(page); expect(persistedContent).toBe(testContent); }); });