test: add editor E2E test specs

This commit is contained in:
SnapOtter
2026-05-07 09:29:01 +08:00
parent 4a96d4b217
commit e11e3660d9
10 changed files with 872 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./tests/e2e-editor",
timeout: 60_000,
expect: { timeout: 15_000 },
fullyParallel: false,
workers: 1,
retries: 0,
reporter: "html",
use: {
baseURL: "http://localhost:1351",
trace: "retain-on-failure",
screenshot: "only-on-failure",
viewport: { width: 1440, height: 900 },
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
});
+96
View File
@@ -0,0 +1,96 @@
import { createNewDocument, expect, test } from "./helpers";
test.describe("Editor Colors", () => {
test.beforeEach(async ({ editorPage: page }) => {
await createNewDocument(page);
});
test("foreground and background swatches are visible", async ({ editorPage: page }) => {
const fgSwatch = page.locator("[data-testid='fg-color-swatch']");
const bgSwatch = page.locator("[data-testid='bg-color-swatch']");
await expect(fgSwatch).toBeVisible();
await expect(bgSwatch).toBeVisible();
});
test("click foreground swatch opens color picker", async ({ editorPage: page }) => {
const fgSwatch = page.locator("[data-testid='fg-color-swatch']");
await fgSwatch.click();
await page.waitForTimeout(300);
const picker = page.locator("[data-testid='color-picker-popover']");
await expect(picker).toBeVisible();
// Picker should contain the react-colorful component
await expect(picker.locator(".react-colorful")).toBeVisible();
});
test("click background swatch opens color picker", async ({ editorPage: page }) => {
const bgSwatch = page.locator("[data-testid='bg-color-swatch']");
await bgSwatch.click();
await page.waitForTimeout(300);
const picker = page.locator("[data-testid='color-picker-popover']");
await expect(picker).toBeVisible();
});
test("D key resets colors to black and white", async ({ editorPage: page }) => {
// First change the foreground color via hex input
const hexInput = page.locator("[data-testid='foreground-hex-input']");
await hexInput.fill("#FF0000");
await page.waitForTimeout(300);
// Press D to reset
// Click canvas area first to ensure no input is focused
await page
.locator("canvas")
.first()
.click({ position: { x: 5, y: 5 } });
await page.waitForTimeout(200);
await page.keyboard.press("d");
await page.waitForTimeout(300);
// Foreground should be black (#000000)
const fgSwatch = page.locator("[data-testid='fg-color-swatch']");
const fgColor = await fgSwatch.evaluate((el) => (el as HTMLElement).style.backgroundColor);
// rgb(0, 0, 0) is black
expect(fgColor).toContain("rgb(0, 0, 0)");
});
test("swap button exchanges foreground and background", async ({ editorPage: page }) => {
const swapBtn = page.locator("[data-testid='swap-colors']");
await expect(swapBtn).toBeVisible();
// Get initial colors
const fgBefore = await page
.locator("[data-testid='fg-color-swatch']")
.evaluate((el) => (el as HTMLElement).style.backgroundColor);
const bgBefore = await page
.locator("[data-testid='bg-color-swatch']")
.evaluate((el) => (el as HTMLElement).style.backgroundColor);
await swapBtn.click();
await page.waitForTimeout(300);
const fgAfter = await page
.locator("[data-testid='fg-color-swatch']")
.evaluate((el) => (el as HTMLElement).style.backgroundColor);
const bgAfter = await page
.locator("[data-testid='bg-color-swatch']")
.evaluate((el) => (el as HTMLElement).style.backgroundColor);
expect(fgAfter).toBe(bgBefore);
expect(bgAfter).toBe(fgBefore);
});
test("color picker has hex, rgb, and hsl mode tabs", async ({ editorPage: page }) => {
// Open the color picker
await page.locator("[data-testid='fg-color-swatch']").click();
await page.waitForTimeout(300);
const picker = page.locator("[data-testid='color-picker-popover']");
await expect(picker.getByText("HEX", { exact: true })).toBeVisible();
await expect(picker.getByText("RGB", { exact: true })).toBeVisible();
await expect(picker.getByText("HSL", { exact: true })).toBeVisible();
});
});
+49
View File
@@ -0,0 +1,49 @@
import { createNewDocument, expect, test } from "./helpers";
test.describe("Editor Export", () => {
test.beforeEach(async ({ editorPage: page }) => {
await createNewDocument(page);
});
test("export dialog opens via Ctrl+Shift+S", async ({ editorPage: page }) => {
await page.keyboard.press("Control+Shift+s");
await page.waitForTimeout(500);
// Export dialog should appear with "Export Image" heading
await expect(page.getByText("Export Image")).toBeVisible();
});
test("export dialog has format options PNG, JPEG, WebP", async ({ editorPage: page }) => {
await page.keyboard.press("Control+Shift+s");
await page.waitForTimeout(500);
await expect(page.getByText("PNG", { exact: true })).toBeVisible();
await expect(page.getByText("JPEG", { exact: true })).toBeVisible();
await expect(page.getByText("WebP", { exact: true })).toBeVisible();
});
test("export dialog has dimension inputs and aspect lock", async ({ editorPage: page }) => {
await page.keyboard.press("Control+Shift+s");
await page.waitForTimeout(500);
await expect(page.getByText("Dimensions")).toBeVisible();
await expect(page.getByText("Width")).toBeVisible();
await expect(page.getByText("Height")).toBeVisible();
// Aspect lock button
const lockBtn = page.locator(
"button[aria-label='Unlock aspect ratio'], button[aria-label='Lock aspect ratio']",
);
await expect(lockBtn).toBeVisible();
});
test("export dialog has export, copy, save, and load buttons", async ({ editorPage: page }) => {
await page.keyboard.press("Control+Shift+s");
await page.waitForTimeout(500);
await expect(page.getByText("Export", { exact: true })).toBeVisible();
await expect(page.getByText("Copy", { exact: false }).first()).toBeVisible();
await expect(page.getByText("Save Project")).toBeVisible();
await expect(page.getByText("Load Project")).toBeVisible();
});
});
+75
View File
@@ -0,0 +1,75 @@
import { createNewDocument, drawOnCanvas, expect, selectTool, test } from "./helpers";
test.describe("Editor History Panel", () => {
test.beforeEach(async ({ editorPage: page }) => {
await createNewDocument(page);
});
test("history tab shows action list", async ({ editorPage: page }) => {
// Switch to history tab
await page.locator("[data-testid='tab-history']").click();
await page.waitForTimeout(300);
// There should be at least one history entry (the initial load)
const entries = page.locator(".flex-1.overflow-y-auto button");
const count = await entries.count();
expect(count).toBeGreaterThanOrEqual(1);
});
test("history panel shows undo and redo buttons", async ({ editorPage: page }) => {
await page.locator("[data-testid='tab-history']").click();
await page.waitForTimeout(300);
const undoBtn = page.locator("button[aria-label='Undo']");
const redoBtn = page.locator("button[aria-label='Redo']");
await expect(undoBtn).toBeVisible();
await expect(redoBtn).toBeVisible();
});
test("drawing creates a history entry", async ({ editorPage: page }) => {
// Switch to history tab first to count entries
await page.locator("[data-testid='tab-history']").click();
await page.waitForTimeout(300);
const entriesLocator = page.locator(".flex-1.overflow-y-auto button");
const countBefore = await entriesLocator.count();
// Draw something with the brush
await selectTool(page, "brush");
await drawOnCanvas(page, 100, 100, 300, 200);
await page.waitForTimeout(500);
// Switch back to history tab to see new entry
await page.locator("[data-testid='tab-history']").click();
await page.waitForTimeout(300);
const countAfter = await entriesLocator.count();
expect(countAfter).toBeGreaterThan(countBefore);
});
test("history shows step count", async ({ editorPage: page }) => {
await page.locator("[data-testid='tab-history']").click();
await page.waitForTimeout(300);
// The history panel shows "N / 50" counter
await expect(page.getByText(/\d+\s*\/\s*50/)).toBeVisible();
});
test("undo button is disabled with no history", async ({ editorPage: page }) => {
await page.locator("[data-testid='tab-history']").click();
await page.waitForTimeout(300);
// On a fresh document with no actions, undo should be disabled
// (the initial Load Image creates one past state, so let's just
// check the button exists and is a proper control)
const undoBtn = page.locator("button[aria-label='Undo']");
await expect(undoBtn).toBeVisible();
// Redo should be disabled since we haven't undone anything
const redoBtn = page.locator("button[aria-label='Redo']");
await expect(redoBtn).toBeVisible();
// Redo should visually appear disabled (no future states)
await expect(redoBtn).toHaveCSS("cursor", "not-allowed");
});
});
@@ -0,0 +1,95 @@
import { createNewDocument, expect, selectTool, test } from "./helpers";
test.describe("Editor Keyboard Shortcuts", () => {
test.beforeEach(async ({ editorPage: page }) => {
await createNewDocument(page);
});
test("V activates move tool", async ({ editorPage: page }) => {
await selectTool(page, "brush"); // start elsewhere
await page.keyboard.press("v");
await page.waitForTimeout(300);
await expect(page.locator("[data-tool='move']")).toHaveAttribute("data-tool-active", "true");
});
test("B activates brush tool", async ({ editorPage: page }) => {
await page.keyboard.press("b");
await page.waitForTimeout(300);
await expect(page.locator("[data-tool='brush']")).toHaveAttribute("data-tool-active", "true");
});
test("E activates eraser tool", async ({ editorPage: page }) => {
await page.keyboard.press("e");
await page.waitForTimeout(300);
await expect(page.locator("[data-tool='eraser']")).toHaveAttribute("data-tool-active", "true");
});
test("C activates crop tool", async ({ editorPage: page }) => {
await page.keyboard.press("c");
await page.waitForTimeout(300);
await expect(page.locator("[data-tool='crop']")).toHaveAttribute("data-tool-active", "true");
});
test("M activates marquee tool", async ({ editorPage: page }) => {
await page.keyboard.press("m");
await page.waitForTimeout(300);
await expect(page.locator("[data-tool='marquee-rect']")).toHaveAttribute(
"data-tool-active",
"true",
);
});
test("I activates eyedropper tool", async ({ editorPage: page }) => {
await page.keyboard.press("i");
await page.waitForTimeout(300);
await expect(page.locator("[data-tool='eyedropper']")).toHaveAttribute(
"data-tool-active",
"true",
);
});
test("G activates fill tool", async ({ editorPage: page }) => {
await page.keyboard.press("g");
await page.waitForTimeout(300);
await expect(page.locator("[data-tool='fill']")).toHaveAttribute("data-tool-active", "true");
});
test("H activates hand tool", async ({ editorPage: page }) => {
await page.keyboard.press("h");
await page.waitForTimeout(300);
await expect(page.locator("[data-tool='hand']")).toHaveAttribute("data-tool-active", "true");
});
test("Z activates zoom tool", async ({ editorPage: page }) => {
await page.keyboard.press("z");
await page.waitForTimeout(300);
await expect(page.locator("[data-tool='zoom']")).toHaveAttribute("data-tool-active", "true");
});
test("shortcuts are disabled when typing in text input", async ({ editorPage: page }) => {
// Focus the zoom input in the status bar (a number input)
const zoomInput = page.locator("[data-testid='status-zoom'] input[type='number']");
await zoomInput.click();
await zoomInput.fill("");
// Pressing B while focused in input should NOT switch tool
const activeBefore = await page.locator("[data-tool-active='true']").getAttribute("data-tool");
await page.keyboard.press("b");
await page.waitForTimeout(300);
const activeAfter = await page.locator("[data-tool-active='true']").getAttribute("data-tool");
// Tool should not have changed to brush
expect(activeAfter).toBe(activeBefore);
});
});
+143
View File
@@ -0,0 +1,143 @@
import { createNewDocument, expect, test } from "./helpers";
test.describe("Editor Layers Panel", () => {
test.beforeEach(async ({ editorPage: page }) => {
await createNewDocument(page);
// Ensure layers tab is active
await page.locator("[data-testid='tab-layers']").click();
await page.waitForTimeout(300);
});
test("layers panel shows default Layer 1", async ({ editorPage: page }) => {
const layersPanel = page.locator("[data-testid='layers-panel']");
await expect(layersPanel).toBeVisible();
// Should have at least one layer with "Layer 1" text
const layerRows = layersPanel.locator("[role='option']");
await expect(layerRows).toHaveCount(1);
await expect(layersPanel.getByText("Layer 1")).toBeVisible();
});
test("add layer button creates new layer", async ({ editorPage: page }) => {
const addBtn = page.locator("[data-testid='add-layer-btn']");
await expect(addBtn).toBeVisible();
await addBtn.click();
await page.waitForTimeout(300);
// Now there should be 2 layers
const layersPanel = page.locator("[data-testid='layers-panel']");
const layerRows = layersPanel.locator("[role='option']");
await expect(layerRows).toHaveCount(2);
});
test("cannot delete last layer", async ({ editorPage: page }) => {
const deleteBtn = page.locator("[data-testid='delete-layer-btn']");
await expect(deleteBtn).toBeVisible();
// With only one layer, delete button should be disabled
await expect(deleteBtn).toBeDisabled();
});
test("delete button enabled with multiple layers", async ({ editorPage: page }) => {
// Add a second layer
await page.locator("[data-testid='add-layer-btn']").click();
await page.waitForTimeout(300);
const deleteBtn = page.locator("[data-testid='delete-layer-btn']");
await expect(deleteBtn).toBeEnabled();
});
test("eye icon toggles visibility", async ({ editorPage: page }) => {
const layersPanel = page.locator("[data-testid='layers-panel']");
const layerRow = layersPanel.locator("[role='option']").first();
// Initially visible (Eye icon, aria-label "Hide layer")
const hideBtn = layerRow.locator("button[aria-label='Hide layer']");
await expect(hideBtn).toBeVisible();
// Click to hide
await hideBtn.click();
await page.waitForTimeout(300);
// Now should show "Show layer" button
const showBtn = layerRow.locator("button[aria-label='Show layer']");
await expect(showBtn).toBeVisible();
// Click again to show
await showBtn.click();
await page.waitForTimeout(300);
await expect(layerRow.locator("button[aria-label='Hide layer']")).toBeVisible();
});
test("lock icon toggles lock state", async ({ editorPage: page }) => {
const layersPanel = page.locator("[data-testid='layers-panel']");
const layerRow = layersPanel.locator("[role='option']").first();
// Initially unlocked
const lockBtn = layerRow.locator("button[aria-label='Lock layer']");
await expect(lockBtn).toBeVisible();
await lockBtn.click();
await page.waitForTimeout(300);
// Now should show "Unlock layer"
const unlockBtn = layerRow.locator("button[aria-label='Unlock layer']");
await expect(unlockBtn).toBeVisible();
});
test("blend mode dropdown shows modes", async ({ editorPage: page }) => {
const blendSelect = page.locator("[data-testid='blend-mode-select']");
await expect(blendSelect).toBeVisible();
// Check that it has the expected blend mode options
const options = blendSelect.locator("option");
const texts = await options.allTextContents();
expect(texts).toContain("Normal");
expect(texts).toContain("Multiply");
expect(texts).toContain("Screen");
expect(texts).toContain("Overlay");
expect(texts).toContain("Darken");
expect(texts).toContain("Lighten");
});
test("opacity slider works", async ({ editorPage: page }) => {
const slider = page.locator("[data-testid='layer-opacity-slider']");
await expect(slider).toBeVisible();
// Default opacity should be 100
await expect(slider).toHaveValue("100");
// Change opacity
await slider.fill("50");
await page.waitForTimeout(300);
await expect(slider).toHaveValue("50");
});
test("double-click layer name enters rename mode", async ({ editorPage: page }) => {
const layersPanel = page.locator("[data-testid='layers-panel']");
// Double-click the layer name button
const nameBtn = layersPanel
.locator("[role='option']")
.first()
.locator("button")
.filter({ hasText: "Layer 1" });
await nameBtn.dblclick();
await page.waitForTimeout(300);
// An input for renaming should appear
const renameInput = layersPanel.locator("input[type='text'][class*='bg-muted']");
await expect(renameInput).toBeVisible();
});
test("layer row highlights active layer", async ({ editorPage: page }) => {
const layersPanel = page.locator("[data-testid='layers-panel']");
const layerRow = layersPanel.locator("[role='option']").first();
await expect(layerRow).toHaveAttribute("aria-selected", "true");
});
});
+117
View File
@@ -0,0 +1,117 @@
import { expect, test } from "./helpers";
test.describe("Editor Navigation", () => {
test("sidebar shows Editor between Automate and Files", async ({ editorPage: page }) => {
await page.goto("/");
await page.waitForTimeout(1000);
const sidebarLinks = page.locator("nav a, aside a, [class*='sidebar'] a");
const labels = await sidebarLinks.allTextContents();
const flat = labels.join("|");
expect(flat).toContain("Automate");
expect(flat).toContain("Editor");
expect(flat).toContain("Files");
const automateIdx = flat.indexOf("Automate");
const editorIdx = flat.indexOf("Editor");
const filesIdx = flat.indexOf("Files");
expect(editorIdx).toBeGreaterThan(automateIdx);
expect(filesIdx).toBeGreaterThan(editorIdx);
});
test("clicking Editor navigates to /editor", async ({ editorPage: page }) => {
await page.goto("/");
await page.waitForTimeout(1000);
await page.locator('a[href="/editor"]').click();
await page.waitForTimeout(1000);
expect(page.url()).toContain("/editor");
});
test("editor page renders four-zone layout", async ({ editorPage: page }) => {
// Options bar at the top (contains tool name)
const optionsBar = page.locator("text=move").first();
await expect(optionsBar).toBeVisible();
// Toolbar on the left (contains tool buttons with data-tool)
const toolbar = page.locator("[data-tool='move']");
await expect(toolbar).toBeVisible();
// Canvas area in the middle
const canvasArea = page.locator(".bg-muted\\/30").first();
await expect(canvasArea).toBeVisible();
// Right panel with tabs
const rightPanel = page.locator("[data-testid='tab-layers']");
await expect(rightPanel).toBeVisible();
});
test("welcome screen shows when no image loaded", async ({ editorPage: page }) => {
await expect(page.getByText("Image Editor")).toBeVisible();
await expect(page.getByText("Drop an image here to get started")).toBeVisible();
await expect(page.getByText("Open Image")).toBeVisible();
await expect(page.getByText("New Document")).toBeVisible();
await expect(page.getByText("paste from clipboard")).toBeVisible();
});
test("right panel has three tabs", async ({ editorPage: page }) => {
const layersTab = page.locator("[data-testid='tab-layers']");
const adjustmentsTab = page.locator("[data-testid='tab-adjustments']");
const historyTab = page.locator("[data-testid='tab-history']");
await expect(layersTab).toBeVisible();
await expect(adjustmentsTab).toBeVisible();
await expect(historyTab).toBeVisible();
await expect(layersTab).toHaveText("Layers");
await expect(adjustmentsTab).toHaveText("Adjustments");
await expect(historyTab).toHaveText("History");
});
test("right panel collapses and expands", async ({ editorPage: page }) => {
// Panel starts visible with tabs
await expect(page.locator("[data-testid='tab-layers']")).toBeVisible();
// Click the collapse button (ChevronRight at end of tab row)
await page.locator("button[aria-label='Collapse panel']").click();
await page.waitForTimeout(300);
// Tabs should no longer be visible
await expect(page.locator("[data-testid='tab-layers']")).not.toBeVisible();
// Expand button should appear
const expandBtn = page.locator("button[aria-label='Expand panel']");
await expect(expandBtn).toBeVisible();
// Click expand
await expandBtn.click();
await page.waitForTimeout(300);
// Tabs visible again
await expect(page.locator("[data-testid='tab-layers']")).toBeVisible();
});
test("status bar shows zoom level", async ({ editorPage: page }) => {
const statusZoom = page.locator("[data-testid='status-zoom']");
await expect(statusZoom).toBeVisible();
// Should contain a number input and % sign
const zoomInput = statusZoom.locator("input[type='number']");
await expect(zoomInput).toBeVisible();
await expect(statusZoom.locator("text=%")).toBeVisible();
});
test("status bar shows cursor position after loading document", async ({ editorPage: page }) => {
const { createNewDocument } = await import("./helpers");
await createNewDocument(page);
const statusCursor = page.locator("[data-testid='status-cursor']");
await expect(statusCursor).toBeVisible();
// Should show X and Y values
await expect(page.locator("[data-testid='status-cursor-x']")).toBeVisible();
await expect(page.locator("[data-testid='status-cursor-y']")).toBeVisible();
});
});
@@ -0,0 +1,131 @@
import { createNewDocument, drawOnCanvas, expect, selectTool, test } from "./helpers";
test.describe("Editor Drawing Tools", () => {
test.beforeEach(async ({ editorPage: page }) => {
await createNewDocument(page);
});
test("brush activates via toolbar click", async ({ editorPage: page }) => {
await selectTool(page, "brush");
const brushBtn = page.locator("[data-tool='brush']");
await expect(brushBtn).toHaveAttribute("data-tool-active", "true");
});
test("brush activates via B shortcut", async ({ editorPage: page }) => {
// Start with a different tool
await selectTool(page, "move");
await expect(page.locator("[data-tool='move']")).toHaveAttribute("data-tool-active", "true");
// Press B to switch to brush
await page.keyboard.press("b");
await page.waitForTimeout(300);
await expect(page.locator("[data-tool='brush']")).toHaveAttribute("data-tool-active", "true");
});
test("brush options bar shows size, opacity, and hardness", async ({ editorPage: page }) => {
await selectTool(page, "brush");
// Options bar should display Size, Opacity, and Hardness labels
const optionsBar = page.locator(".flex.items-center.h-10");
await expect(optionsBar.getByText("Size")).toBeVisible();
await expect(optionsBar.getByText("Opacity")).toBeVisible();
await expect(optionsBar.getByText("Hardness")).toBeVisible();
});
test("brush drawing changes canvas content", async ({ editorPage: page }) => {
await selectTool(page, "brush");
const canvas = page.locator("canvas").first();
const before = await canvas.screenshot();
await drawOnCanvas(page, 100, 100, 300, 300);
const after = await canvas.screenshot();
expect(Buffer.compare(before, after)).not.toBe(0);
});
test("shape tool draws rectangle on canvas", async ({ editorPage: page }) => {
await selectTool(page, "shape-rect");
const canvas = page.locator("canvas").first();
const before = await canvas.screenshot();
await drawOnCanvas(page, 100, 100, 250, 200);
const after = await canvas.screenshot();
expect(Buffer.compare(before, after)).not.toBe(0);
});
test("shape tool draws ellipse on canvas", async ({ editorPage: page }) => {
await selectTool(page, "shape-rect");
// Switch to ellipse via the shape type dropdown in options bar
const shapeSelect = page.locator("select").filter({ hasText: "Rectangle" });
await shapeSelect.selectOption("shape-ellipse");
await page.waitForTimeout(300);
const canvas = page.locator("canvas").first();
const before = await canvas.screenshot();
await drawOnCanvas(page, 150, 150, 350, 300);
const after = await canvas.screenshot();
expect(Buffer.compare(before, after)).not.toBe(0);
});
test("eraser activates via E shortcut", async ({ editorPage: page }) => {
await page.keyboard.press("e");
await page.waitForTimeout(300);
await expect(page.locator("[data-tool='eraser']")).toHaveAttribute("data-tool-active", "true");
});
test("eraser options bar shows size and opacity", async ({ editorPage: page }) => {
await selectTool(page, "eraser");
const optionsBar = page.locator(".flex.items-center.h-10");
await expect(optionsBar.getByText("Size")).toBeVisible();
await expect(optionsBar.getByText("Opacity")).toBeVisible();
});
test("pencil tool activates via N shortcut", async ({ editorPage: page }) => {
await page.keyboard.press("n");
await page.waitForTimeout(300);
await expect(page.locator("[data-tool='pencil']")).toHaveAttribute("data-tool-active", "true");
});
test("pencil options bar hides hardness", async ({ editorPage: page }) => {
await selectTool(page, "pencil");
const optionsBar = page.locator(".flex.items-center.h-10");
await expect(optionsBar.getByText("Size")).toBeVisible();
await expect(optionsBar.getByText("Opacity")).toBeVisible();
// Pencil is always hard, so hardness should not appear
await expect(optionsBar.getByText("Hardness")).not.toBeVisible();
});
test("shape fill color applies", async ({ editorPage: page }) => {
await selectTool(page, "shape-rect");
// The fill color input should be visible in options bar
const fillLabel = page.locator("label").filter({ hasText: "Fill" });
await expect(fillLabel).toBeVisible();
// A color input for fill should be present
const fillColorInput = fillLabel.locator("input[type='color']");
await expect(fillColorInput).toBeVisible();
});
test("shape stroke width control is visible", async ({ editorPage: page }) => {
await selectTool(page, "shape-rect");
const widthLabel = page.locator("label").filter({ hasText: /^Width/ });
await expect(widthLabel).toBeVisible();
const widthSlider = widthLabel.locator("input[type='range']");
await expect(widthSlider).toBeVisible();
});
});
@@ -0,0 +1,90 @@
import { createNewDocument, expect, selectTool, test } from "./helpers";
test.describe("Editor Selection Tools", () => {
test.beforeEach(async ({ editorPage: page }) => {
await createNewDocument(page);
});
test("move tool activates via V shortcut", async ({ editorPage: page }) => {
// Start with a different tool
await selectTool(page, "brush");
await page.keyboard.press("v");
await page.waitForTimeout(300);
await expect(page.locator("[data-tool='move']")).toHaveAttribute("data-tool-active", "true");
});
test("move tool shows options bar label", async ({ editorPage: page }) => {
await selectTool(page, "move");
// Options bar should show "move" label
const optionsBar = page.locator(".flex.items-center.h-10");
await expect(optionsBar.getByText("move", { exact: false })).toBeVisible();
});
test("marquee activates via M shortcut", async ({ editorPage: page }) => {
await page.keyboard.press("m");
await page.waitForTimeout(300);
await expect(page.locator("[data-tool='marquee-rect']")).toHaveAttribute(
"data-tool-active",
"true",
);
});
test("marquee cycles subtypes on repeated M press", async ({ editorPage: page }) => {
await page.keyboard.press("m");
await page.waitForTimeout(300);
await expect(page.locator("[data-tool='marquee-rect']")).toHaveAttribute(
"data-tool-active",
"true",
);
// Press M again to cycle to ellipse marquee
await page.keyboard.press("m");
await page.waitForTimeout(300);
await expect(page.locator("[data-tool='marquee-rect']")).toHaveAttribute(
"data-tool-active",
"false",
);
});
test("crop activates via C shortcut", async ({ editorPage: page }) => {
await page.keyboard.press("c");
await page.waitForTimeout(300);
await expect(page.locator("[data-tool='crop']")).toHaveAttribute("data-tool-active", "true");
});
test("crop options show aspect ratio dropdown", async ({ editorPage: page }) => {
await selectTool(page, "crop");
// Crop options bar should have aspect ratio dropdown
const ratioLabel = page.getByText("Ratio:");
await expect(ratioLabel).toBeVisible();
const ratioSelect = page.locator("#crop-aspect");
await expect(ratioSelect).toBeVisible();
// The dropdown should have a "Free" option
const freeOption = ratioSelect.locator("option", { hasText: "Free" });
await expect(freeOption).toBeAttached();
});
test("crop options show width and height inputs", async ({ editorPage: page }) => {
await selectTool(page, "crop");
await expect(page.locator("#crop-width")).toBeVisible();
await expect(page.locator("#crop-height")).toBeVisible();
});
test("crop options show apply and cancel buttons", async ({ editorPage: page }) => {
await selectTool(page, "crop");
await expect(page.locator("button[aria-label='Apply Crop']")).toBeVisible();
await expect(page.locator("button[aria-label='Cancel Crop']")).toBeVisible();
});
});
+53
View File
@@ -0,0 +1,53 @@
import { test as base, expect, type Page } from "@playwright/test";
export const test = base.extend<{ editorPage: Page }>({
editorPage: async ({ page }, use) => {
// Login first
await page.goto("/login");
await page.waitForTimeout(1500);
await page.fill('input[placeholder*="username" i]', "admin");
await page.fill('input[placeholder*="password" i]', "admin");
await page.click('button:has-text("Login")');
await page.waitForURL("**/", { timeout: 10000 }).catch(() => {});
await page.waitForTimeout(1500);
// Navigate to editor
await page.goto("/editor");
await page.waitForTimeout(2000);
await use(page);
},
});
export { expect };
export async function createNewDocument(page: Page, _width = 1920, _height = 1080): Promise<void> {
await page.getByText("New Document").click();
await page.waitForTimeout(500);
await page.locator('button:has-text("Create")').click();
await page.waitForTimeout(2000);
}
export async function waitForCanvas(page: Page): Promise<void> {
await page.locator("canvas").first().waitFor({ state: "visible", timeout: 10000 });
}
export async function selectTool(page: Page, toolName: string): Promise<void> {
await page.locator(`[data-tool="${toolName}"]`).click();
await page.waitForTimeout(300);
}
export async function drawOnCanvas(
page: Page,
x1: number,
y1: number,
x2: number,
y2: number,
): Promise<void> {
const canvas = page.locator("canvas").first();
const box = await canvas.boundingBox();
if (!box) throw new Error("Canvas not found");
await page.mouse.move(box.x + x1, box.y + y1);
await page.mouse.down();
await page.mouse.move(box.x + x2, box.y + y2, { steps: 10 });
await page.mouse.up();
await page.waitForTimeout(300);
}