import path from "node:path"; import { expect, getTestImagePath, test, waitForProcessing } from "./helpers"; // --------------------------------------------------------------------------- // Helper: resolve fixture image paths // --------------------------------------------------------------------------- function getFixturePath(name: string): string { // Fixtures live under tests/fixtures/image/valid/ in 2.0. return path.join(process.cwd(), "tests", "fixtures", "image", "valid", name); } const FIXTURE_JPG = getFixturePath("test-100x100.jpg"); const FIXTURE_PNG = getFixturePath("test-200x150.png"); const FIXTURE_WEBP = getFixturePath("test-50x50.webp"); // --------------------------------------------------------------------------- // Helper: navigate to /automate with retry logic for blank-page flakes // --------------------------------------------------------------------------- async function gotoAutomate(page: import("@playwright/test").Page) { const heading = page.getByRole("heading", { name: /pipeline builder|automate/i, }); for (let attempt = 0; attempt < 3; attempt++) { await page.goto("/automate", { waitUntil: "load" }); try { await expect(heading).toBeVisible({ timeout: 8_000 }); return; } catch { await page.waitForTimeout(500); } } // Final attempt - let it throw if it fails await page.goto("/automate", { waitUntil: "load" }); await expect(heading).toBeVisible({ timeout: 10_000 }); } /** Wait for pipeline steps to render by counting Remove buttons. */ async function waitForSteps(page: import("@playwright/test").Page, count: number) { await expect(page.getByTitle("Remove")).toHaveCount(count, { timeout: 5_000, }); } /** Search for a tool by name in the palette and click it. */ async function addToolStep( page: import("@playwright/test").Page, name: string, expectedCount: number, ) { await page.getByPlaceholder("Search tools...").fill(name); await page .getByRole("button", { name: new RegExp(name, "i") }) .first() .click(); await waitForSteps(page, expectedCount); } /** Upload the test image via the Dropzone file chooser. */ async function uploadTestFile(page: import("@playwright/test").Page) { const testImagePath = getTestImagePath(); const fileChooserPromise = page.waitForEvent("filechooser"); await page.getByRole("button", { name: /upload from computer/i }).click(); const fileChooser = await fileChooserPromise; await fileChooser.setFiles(testImagePath); await page.waitForTimeout(500); } // --------------------------------------------------------------------------- // Empty state tests // --------------------------------------------------------------------------- test.describe("Pipeline Builder - Empty state", () => { test("navigate to /automate shows empty state message", async ({ loggedInPage: page }) => { await gotoAutomate(page); await expect(page.getByText("No steps yet")).toBeVisible(); // The prompt renders in two places (canvas header + builder empty state), // so scope to the first match to avoid a strict-mode violation. await expect(page.getByText("Add tools from the palette to get started").first()).toBeVisible(); }); test("tool palette with search is visible", async ({ loggedInPage: page }) => { await gotoAutomate(page); await expect(page.getByText("Tool Palette")).toBeVisible(); await expect(page.getByPlaceholder("Search tools...")).toBeVisible(); }); test("tool palette search filters results", async ({ loggedInPage: page }) => { await gotoAutomate(page); const searchInput = page.getByPlaceholder("Search tools..."); await expect(searchInput).toBeVisible(); // Type "resize" to filter the palette await searchInput.fill("resize"); await page.waitForTimeout(300); // A Resize button should be visible in the filtered results await expect(page.getByRole("button", { name: /resize/i }).first()).toBeVisible(); // Tools unrelated to "resize" should not be visible (e.g. "Compress") await expect(page.getByRole("button", { name: /^compress$/i }).first()).not.toBeVisible({ timeout: 1_000, }); // Clear search and verify palette restores full list await searchInput.clear(); await page.waitForTimeout(300); // Compress should now be visible again in the full palette await expect(page.getByRole("button", { name: /compress/i }).first()).toBeVisible({ timeout: 3_000, }); }); test("process button is disabled when no steps or file", async ({ loggedInPage: page }) => { await gotoAutomate(page); const processBtn = page.getByRole("button", { name: "Process", exact: true }); await expect(processBtn).toBeVisible(); await expect(processBtn).toBeDisabled(); }); test("dropzone is visible when no file uploaded", async ({ loggedInPage: page }) => { await gotoAutomate(page); await expect(page.locator("section[aria-label='File drop zone']")).toBeVisible(); await expect(page.getByRole("button", { name: /upload from computer/i })).toBeVisible(); }); }); // --------------------------------------------------------------------------- // Adding steps tests // --------------------------------------------------------------------------- test.describe("Pipeline Builder - Adding steps", () => { test("search 'resize' in palette, click, and step 1 appears", async ({ loggedInPage: page }) => { await gotoAutomate(page); await addToolStep(page, "Resize", 1); // Empty state should be gone await expect(page.getByText("No steps yet")).not.toBeVisible(); // Step should display the tool name await expect(page.getByText("Resize").first()).toBeVisible(); }); test("step shows name, expand/collapse toggle, and remove button", async ({ loggedInPage: page, }) => { await gotoAutomate(page); await addToolStep(page, "Resize", 1); // Tool name visible in the step await expect(page.getByText("Resize").first()).toBeVisible(); // Remove button visible await expect(page.getByTitle("Remove")).toBeVisible(); // Step number badge "1" visible await expect(page.locator("span").filter({ hasText: /^1$/ }).first()).toBeVisible(); }); test("expand step shows settings form", async ({ loggedInPage: page }) => { await gotoAutomate(page); await addToolStep(page, "Resize", 1); // A newly added step auto-expands, so collapse it first to test expanding. // The step header is a div[role=button] (palette items are real