Files
automaker/apps/app/tests/context-view.spec.ts
Kacper caae869501 refactor: improve context view tests by utilizing utility functions
- Replaced direct element locators with utility functions for better readability and maintainability in context view tests.
- Removed unnecessary wait statements and replaced them with appropriate utility functions to enhance test reliability.
- Streamlined the verification process for file visibility and content loading, ensuring tests are more efficient and easier to understand.
2025-12-15 02:49:08 +01:00

689 lines
22 KiB
TypeScript

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/app/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);
});
});