import { test, expect } from "@playwright/test"; import { WorkspacePage } from "../pages/WorkspacePage"; test.beforeEach(async ({ page }) => { await WorkspacePage.init(page); }); const setupFileWithAssets = async (workspace) => { const fileId = "015fda4f-caa6-8103-8004-862a00dd4f31"; const pageId = "015fda4f-caa6-8103-8004-862a00ddbe94"; const fragments = { "015fda4f-caa6-8103-8004-862a9e4b4d4b": "assets/get-file-fragment-with-assets-components.json", "015fda4f-caa6-8103-8004-862a9e4ad279": "assets/get-file-fragmnet-with-assets-page.json", }; await workspace.setupEmptyFile(); await workspace.mockRPC(/get\-file\?/, "assets/get-file-with-assets.json"); for (const [id, fixture] of Object.entries(fragments)) { await workspace.mockRPC( `get-file-fragment?file-id=*&fragment-id=${id}`, fixture, ); } return { fileId, pageId }; }; test.describe("Color palette", () => { test("Shows the scroll arrows and the palette menu", async ({ page }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.mockRPC( /get\-file\?/, "workspace/get-file-many-colors.json", ); await workspace.goToWorkspace({ fileId: "c1249a66-fce0-8175-8004-7433fe4be8bc", pageId: "c1249a66-fce0-8175-8004-7433fe4be8bd", }); await workspace.clickColorPalette(); await workspace.palette.locator('[class*="palette-actions"]').click(); await page.getByText("File library").click(); await expect( workspace.palette.getByRole("button", { name: "color-01" }), ).toBeVisible(); await page.mouse.move(640, 300); // The arrows are a small part of the palette: compare every pixel. await expect(workspace.palette).toHaveScreenshot("color-palette.png", { maxDiffPixelRatio: 0, threshold: 0, }); await workspace.palette.locator('[class*="right-arrow"]').click(); await page.mouse.move(640, 300); await expect(workspace.palette).toHaveScreenshot( "color-palette-scrolled.png", { maxDiffPixelRatio: 0, threshold: 0 }, ); await workspace.palette.locator('[class*="palette-actions"]').click(); await expect(page.getByText("Recent colors")).toBeVisible(); await page.mouse.move(640, 300); await expect(page).toHaveScreenshot("color-palette-menu.png", { clip: { x: 300, y: 400, width: 700, height: 320 }, maxDiffPixelRatio: 0, threshold: 0, }); }); }); test.describe("Viewport", () => { test("Shows the workspace correctly for a blank file", async ({ page }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.goToWorkspace(); await expect(workspace.page).toHaveScreenshot(); }); test("User creates a rectangle and locks it", async ({ page }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(page); await workspace.goToWorkspace(); await workspace.rectShapeButton.click(); await workspace.clickWithDragViewportAt(128, 128, 200, 100); await workspace.clickLeafLayer("Rectangle"); await page.keyboard.press("Shift+Control+L"); await expect(workspace.page).toHaveScreenshot(); }); }); test.describe("Grid layout", () => { test("Shows the grid layout options and the grid editor", async ({ page, }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.mockRPC(/get\-file\?/, "workspace/get-file-9042.json"); await workspace.mockRPC( "get-file-fragment?file-id=*&fragment-id=*", "workspace/get-file-fragment-9042.json", ); await workspace.goToWorkspace({ fileId: "af2494d0-39ba-8184-8005-230696f6df5c", pageId: "af2494d0-39ba-8184-8005-230696f6df5d", }); await workspace.clickLeafLayer("Board"); const section = page.getByTestId("inspect-layout"); await expect( section.getByRole("button", { name: "Edit grid" }), ).toBeVisible(); // The buttons are a small part of the section: compare every pixel. await expect(section).toHaveScreenshot("grid-layout-options.png", { maxDiffPixelRatio: 0, threshold: 0, }); await section.getByRole("button", { name: "Edit grid" }).click(); await expect(workspace.rightSidebar.getByText("GRID LAYOUT")).toBeVisible(); // Expand the column tracks so their rows show too. await workspace.rightSidebar.getByText(/^Columns? \d/).click(); await page.mouse.move(0, 0); await expect(workspace.rightSidebar).toHaveScreenshot( "grid-layout-editor.png", { maxDiffPixelRatio: 0, threshold: 0 }, ); }); }); test.describe("Grid editor", () => { test("Shows the track options button on the canvas", async ({ page }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.mockRPC(/get\-file\?/, "workspace/get-file-9042.json"); await workspace.mockRPC( "get-file-fragment?file-id=*&fragment-id=*", "workspace/get-file-fragment-9042.json", ); await workspace.goToWorkspace({ fileId: "af2494d0-39ba-8184-8005-230696f6df5c", pageId: "af2494d0-39ba-8184-8005-230696f6df5d", }); await workspace.clickLeafLayer("Board"); // Tracks need 110px or more on screen to show their options button. await page.keyboard.press("Control+="); await page.keyboard.press("Control+="); await page .getByTestId("inspect-layout") .getByRole("button", { name: "Edit grid" }) .click(); const shotTrack = async () => { const track = page.getByTestId("grid-track-editor-wrapper").first(); await track.hover(); await expect( page.getByTestId("grid-track-options-btn").first(), ).toBeVisible(); // Wait until the zoom animation leaves the track in place. let box = await track.boundingBox(); await expect .poll(async () => { const prev = box; box = await track.boundingBox(); return prev.x === box.x && prev.width === box.width; }) .toBe(true); await expect(page).toHaveScreenshot({ clip: { x: Math.floor(box.x) - 8, y: Math.floor(box.y) - 8, width: Math.ceil(box.width) + 16, height: Math.ceil(box.height) + 16, }, maxDiffPixelRatio: 0, threshold: 0, }); }; // The track editors draw at a fixed screen size at any zoom. await shotTrack(); await page.keyboard.press("Control+="); await shotTrack(); }); }); test.describe("Design tab", () => { test("Shows the boolean options when selecting two shapes", async ({ page, }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.goToWorkspace(); await workspace.rectShapeButton.click(); await workspace.clickWithDragViewportAt(128, 128, 200, 100); await workspace.clickLeafLayer("Rectangle"); await page.keyboard.press("Control+D"); await expect(workspace.layers.getByText("Rectangle")).toHaveCount(2); await page.keyboard.press("Control+A"); await expect( workspace.rightSidebar.getByRole("button", { name: "Flatten" }), ).toBeVisible(); // The boolean options are a small part of the sidebar: compare every // pixel. await expect(workspace.rightSidebar).toHaveScreenshot({ maxDiffPixelRatio: 0, threshold: 0, }); }); test("Shows the design tab when selecting a shape", async ({ page }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.mockRPC(/get\-file\?/, "workspace/get-file-not-empty.json"); await workspace.goToWorkspace({ fileId: "6191cd35-bb1f-81f7-8004-7cc63d087374", pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375", }); await workspace.clickLeafLayer("Rectangle"); await expect(workspace.page).toHaveScreenshot(); }); test("Shows expanded sections of the design tab", async ({ page }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.mockRPC(/get\-file\?/, "workspace/get-file-not-empty.json"); await workspace.goToWorkspace({ fileId: "6191cd35-bb1f-81f7-8004-7cc63d087374", pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375", }); await workspace.clickLeafLayer("Rectangle"); await workspace.rightSidebar.getByTestId("add-stroke").click(); await expect(workspace.page).toHaveScreenshot(); }); test("Shows the wrap and padding modes of a flex layout", async ({ page, }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.mockRPC(/get\-file\?/, "workspace/get-file-7760.json"); await workspace.mockRPC( "get-file-fragment?file-id=*&fragment-id=*", "workspace/get-file-fragment-7760.json", ); await workspace.mockRPC( "update-file?id=*", "workspace/update-file-create-rect.json", ); await workspace.goToWorkspace({ fileId: "cd90e028-326a-80b4-8004-7cdec16ffad5", pageId: "cd90e028-326a-80b4-8004-7cdec16ffad6", }); await workspace.clickLeafLayer("Flex Board"); const section = workspace.rightSidebar.getByRole("region", { name: "Layout container section", }); const wrap = section.getByRole("button", { name: /^(No w|W)rap$/ }); const padding = section.getByRole("button", { name: "Show 4 sided padding options", }); await expect(wrap).toBeVisible(); // The buttons are a small part of the section: compare every pixel. await expect(section).toHaveScreenshot("flex-layout-rest.png", { maxDiffPixelRatio: 0, threshold: 0, }); await wrap.click(); await padding.click(); await padding.blur(); await page.mouse.move(0, 0); await expect( section.getByRole("textbox", { name: /top padding/i }), ).toBeVisible(); await expect(section).toHaveScreenshot("flex-layout-pressed.png", { maxDiffPixelRatio: 0, threshold: 0, }); }); test("Shows the align options when selecting two shapes", async ({ page, }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.goToWorkspace(); await workspace.rectShapeButton.click(); await workspace.clickWithDragViewportAt(128, 128, 200, 100); await workspace.clickLeafLayer("Rectangle"); await page.keyboard.press("Control+D"); await expect(workspace.layers.getByText("Rectangle")).toHaveCount(2); await page.keyboard.press("Control+A"); await expect( workspace.rightSidebar.getByRole("button", { name: /^Align left/ }), ).toBeVisible(); // The align options are a small part of the sidebar: compare every pixel. await expect(workspace.rightSidebar).toHaveScreenshot({ maxDiffPixelRatio: 0, threshold: 0, }); }); test("Shows the margin modes of a flex layout element", async ({ page }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.mockRPC(/get\-file\?/, "workspace/get-file-7760.json"); await workspace.mockRPC( "get-file-fragment?file-id=*&fragment-id=*", "workspace/get-file-fragment-7760.json", ); await workspace.mockRPC( "update-file?id=*", "workspace/update-file-create-rect.json", ); await workspace.goToWorkspace({ fileId: "cd90e028-326a-80b4-8004-7cdec16ffad5", pageId: "cd90e028-326a-80b4-8004-7cdec16ffad6", }); await workspace.clickToggableLayer("Flex Board"); await workspace.clickLeafLayer("Rectangle"); const section = workspace.rightSidebar.getByRole("region", { name: "Layout item section", }); const marginMode = section.getByRole("button", { name: "Margin - multiple", }); await expect(marginMode).toBeVisible(); // The margin button is a small part of the section: compare every pixel. await expect(section).toHaveScreenshot("margin-simple.png", { maxDiffPixelRatio: 0, threshold: 0, }); await marginMode.click(); await marginMode.blur(); await page.mouse.move(0, 0); await expect( section.getByRole("textbox", { name: "Top margin" }), ).toBeVisible(); await expect(section).toHaveScreenshot("margin-multiple.png", { maxDiffPixelRatio: 0, threshold: 0, }); }); }); test.describe("Component options", () => { test("Shows the component pill of a copy", async ({ page }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.mockRPC(/get\-file\?/, "workspace/get-file-9066.json"); await workspace.goToWorkspace({ fileId: "e179d9df-de35-80bf-8005-283bbd5516b0", pageId: "e179d9df-de35-80bf-8005-283bbd5516b1", }); await workspace.clickToggableLayer("Board"); await workspace.clickToggableLayer("Group"); await workspace.layers.getByText("A", { exact: true }).click(); await expect( workspace.rightSidebar.getByTestId("component-pill-button"), ).toBeVisible(); // The component pill is a small part of the sidebar: compare every // pixel. await expect(workspace.rightSidebar).toHaveScreenshot({ maxDiffPixelRatio: 0, threshold: 0, }); }); test("Shows the groups of the swap panel", async ({ page }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.mockRPC( /get\-file\?/, "workspace/get-file-9066-swap-groups.json", ); await workspace.goToWorkspace({ fileId: "e179d9df-de35-80bf-8005-283bbd5516b0", pageId: "e179d9df-de35-80bf-8005-283bbd5516b1", }); await workspace.clickToggableLayer("Board"); await workspace.clickToggableLayer("Group"); await workspace.layers.getByText("A", { exact: true }).click(); await workspace.rightSidebar.getByRole("button", { name: /^A / }).click(); await expect(workspace.rightSidebar.getByText("Letters")).toBeVisible(); await workspace.rightSidebar.getByText("Letters").hover(); await expect(workspace.rightSidebar).toHaveScreenshot({ maxDiffPixelRatio: 0, threshold: 0, }); }); test("Shows the annotation actions of a main component", async ({ page }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.mockRPC(/get\-file\?/, "workspace/get-file-9066.json"); await workspace.goToWorkspace({ fileId: "e179d9df-de35-80bf-8005-283bbd5516b0", pageId: "e179d9df-de35-80bf-8005-283bbd5516b1", }); await workspace.clickToggableLayer("Board"); await workspace.clickToggableLayer("Group"); await workspace.layers.getByText("B", { exact: true }).click(); await workspace.rightSidebar .getByRole("button", { name: "Options" }) .click(); await workspace.rightSidebar.getByText("Create annotation").click(); // Creating: save (disabled while the text is empty) and discard. const annotation = workspace.rightSidebar.locator("#annotation-textarea"); await expect(annotation).toBeFocused(); await expect(workspace.rightSidebar).toHaveScreenshot( "annotation-creating.png", { maxDiffPixelRatio: 0, threshold: 0 }, ); // Saved: edit and delete show when hovering the title. await annotation.fill("An annotation"); await workspace.rightSidebar .getByRole("button", { name: "Create", exact: true }) .click(); const title = workspace.rightSidebar.getByText("Annotation", { exact: true, }); await title.hover(); await expect(workspace.rightSidebar).toHaveScreenshot( "annotation-saved.png", { maxDiffPixelRatio: 0, threshold: 0 }, ); // Collapsed by clicking its title. await title.click(); await expect( workspace.rightSidebar.getByText("An annotation"), ).toBeHidden(); await expect(workspace.rightSidebar).toHaveScreenshot( "annotation-collapsed.png", { maxDiffPixelRatio: 0, threshold: 0 }, ); }); }); test.describe("Assets tab", () => { test("Shows the libraries modal correctly", async ({ page }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.mockRPC( "link-file-to-library", "workspace/link-file-to-library.json", ); await workspace.mockRPC( "get-team-shared-files?team-id=*", "workspace/get-team-shared-libraries-non-empty.json", ); await workspace.mockRPC( "push-audit-events", "workspace/audit-event-empty.json", ); await workspace.goToWorkspace(); await workspace.clickAssets(); await workspace.openLibrariesModal(); await expect(workspace.page).toHaveScreenshot(); await workspace.librariesModal .getByRole("tab", { name: "Libraries" }) .click(); await expect(workspace.page).toHaveScreenshot(); // Now the get-file call should return a library await workspace.mockRPC(/get\-file\?/, "workspace/get-file-library.json"); await workspace.clickLibrary("Testing library 1"); await expect(workspace.librariesModal.getByText("Added")).toBeVisible(); await expect(workspace.page).toHaveScreenshot(); }); test("Shows the assets correctly", async ({ page }) => { const workspace = new WorkspacePage(page); const { fileId, pageId } = await setupFileWithAssets(workspace); await workspace.goToWorkspace({ fileId, pageId }); await workspace.clickAssets(); await workspace.sidebar.getByRole("button", { name: "Components" }).click(); await workspace.sidebar.getByRole("button", { name: "Colors" }).click(); await workspace.sidebar .getByRole("button", { name: "Typographies" }) .click(); await expect(workspace.page).toHaveScreenshot(); await workspace.sidebar.getByTitle("List view").click(); await expect(workspace.page).toHaveScreenshot(); }); }); test.describe("Debug shape panel", () => { test("Shows the debug info of the selected shape", async ({ page }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.mockRPC(/get\-file\?/, "workspace/get-file-9066.json"); await workspace.goToWorkspace({ fileId: "e179d9df-de35-80bf-8005-283bbd5516b0", pageId: "e179d9df-de35-80bf-8005-283bbd5516b1", }); await workspace.clickLeafLayer("Board"); await page.getByRole("button", { name: "Debugging tool" }).click(); await page.getByText("shape-panel", { exact: true }).click(); await expect( workspace.rightSidebar.getByText("Board", { exact: true }), ).toBeVisible(); await page.mouse.move(640, 600); // Only the title row with the dump buttons: the attributes list below // holds ids that change on every run. const title = workspace.rightSidebar .getByText("Board", { exact: true }) .locator(".."); await expect(title).toHaveScreenshot({ maxDiffPixelRatio: 0, threshold: 0, }); }); }); test.describe("Layers panel", () => { test("Shows the focus mode title and the layer filters", async ({ page }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.mockRPC(/get\-file\?/, "workspace/get-file-9066.json"); await workspace.goToWorkspace({ fileId: "e179d9df-de35-80bf-8005-283bbd5516b0", pageId: "e179d9df-de35-80bf-8005-283bbd5516b1", }); await workspace.clickLeafLayer("Board"); await page.keyboard.press("f"); await expect(workspace.sidebar.getByText("Focus mode")).toBeVisible(); await page.mouse.move(640, 600); await expect(workspace.sidebar).toHaveScreenshot({ maxDiffPixelRatio: 0, threshold: 0, }); await page.keyboard.press("f"); await expect(workspace.sidebar.getByText("Focus mode")).toBeHidden(); await workspace.sidebar.getByRole("button", { name: "Search" }).click(); await workspace.sidebar.getByRole("button", { name: "Filter" }).click(); await workspace.sidebar.getByText("Boards", { exact: true }).click(); await page.mouse.move(640, 600); // The "Boards" filter pill. The clip leaves out the pages divider and // the search bar, whose transitions may still be running. const filtersClip = { x: 0, y: 340, width: 318, height: 380 }; await expect(page).toHaveScreenshot({ clip: filtersClip, maxDiffPixelRatio: 0, threshold: 0, }); await workspace.sidebar.getByRole("button", { name: "Filter" }).click(); await page.mouse.move(640, 600); // The filter menu open, with its option ticked. await expect(page).toHaveScreenshot({ clip: filtersClip, maxDiffPixelRatio: 0, threshold: 0, }); }); }); test.describe("Layers", () => { test("Shows the buttons of the layer rows", async ({ page }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.mockRPC(/get\-file\?/, "workspace/get-file-9066.json"); await workspace.mockRPC( "update-file?id=*", "workspace/update-file-empty.json", ); await workspace.goToWorkspace({ fileId: "e179d9df-de35-80bf-8005-283bbd5516b0", pageId: "e179d9df-de35-80bf-8005-283bbd5516b1", }); await workspace.clickToggableLayer("Board"); await workspace.clickToggableLayer("Group"); const layer = workspace.layers.getByText("A", { exact: true }); await layer.click(); await layer.hover(); await workspace.layers.getByRole("button", { name: "Hide" }).click(); await workspace.layers.getByRole("button", { name: "Lock" }).click(); await expect( workspace.layers.getByRole("button", { name: "Show" }), ).toBeVisible(); await expect( workspace.layers.getByRole("button", { name: "Unlock" }), ).toBeVisible(); // Selected, hidden and blocked layer, with the pointer on its row. await expect(workspace.layers).toHaveScreenshot({ maxDiffPixelRatio: 0, threshold: 0, }); // The pointer on a collapsed group, away from the selected layer. await workspace.layers.getByText("Board", { exact: true }).hover(); await expect(workspace.layers).toHaveScreenshot({ maxDiffPixelRatio: 0, threshold: 0, }); }); test("Shows the rename input of a layer", async ({ page }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.mockRPC(/get\-file\?/, "workspace/get-file-9066.json"); await workspace.goToWorkspace({ fileId: "e179d9df-de35-80bf-8005-283bbd5516b0", pageId: "e179d9df-de35-80bf-8005-283bbd5516b1", }); await workspace.layers.getByText("Board", { exact: true }).dblclick(); const input = workspace.layers.getByRole("textbox"); await expect(input).toBeVisible(); await page.mouse.move(0, 0); await expect(workspace.layers).toHaveScreenshot({ maxDiffPixelRatio: 0, threshold: 0, }); }); }); test.describe("Guides", () => { test("Shows the advanced options of a square guide", async ({ page }) => { // A taller window keeps the whole Design tab visible: a scrolled // sidebar can land on a fractional offset and change the antialiasing. await page.setViewportSize({ width: 1280, height: 900 }); const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.goToWorkspace(); await workspace.boardButton.click(); await workspace.clickWithDragViewportAt(128, 128, 200, 200); await workspace.rightSidebar .getByRole("button", { name: "Add guide" }) .click(); // The advanced options toggle has no accessible name yet. await workspace.rightSidebar .locator("button[class*='frame_grid__show-options']") .click(); await expect(workspace.rightSidebar.getByText("Color")).toBeVisible(); // The guide options are a small part of the sidebar: compare every // pixel. The color rows antialias their rounded corners one color step // differently between runs, so allow that much and nothing more. await expect(workspace.rightSidebar).toHaveScreenshot({ maxDiffPixelRatio: 0, threshold: 0.02, }); }); }); test.describe("Path actions", () => { test("Shows the path editing bar and its menus", async ({ page }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.goToWorkspace(); // A path with a curve node, so the handler menu shows. await page.keyboard.press("p"); await workspace.clickAt(200, 200); await workspace.clickAndMove(400, 200, 450, 300); await workspace.clickAt(300, 400); await page.keyboard.press("Enter"); const bar = page.locator("[class*='sub-actions']").first(); await expect(bar).toBeVisible(); await page.mouse.move(640, 600); await expect(bar).toHaveScreenshot("path-actions-draw.png", { maxDiffPixelRatio: 0, threshold: 0, }); // Select every node so all the node actions show. await bar.getByRole("button", { name: /Move nodes/ }).click(); await workspace.clickWithDragViewportAt(150, 150, 350, 300); await page.mouse.move(640, 600); await expect(bar).toHaveScreenshot("path-actions.png", { maxDiffPixelRatio: 0, threshold: 0, }); const box = await bar.boundingBox(); const clip = { x: box.x - 40, y: box.y, width: box.width + 80, height: box.height + 46, }; await bar.getByRole("button", { name: /Merge nodes/ }).hover(); // Let the tooltip show. await page.waitForTimeout(1000); await expect(page).toHaveScreenshot("path-actions-hover.png", { clip, maxDiffPixelRatio: 0, threshold: 0, }); await bar.getByRole("button", { name: /Handler/ }).click(); await expect( bar.getByRole("button", { name: "Independent" }), ).toBeVisible(); // Let the menu fade in. await page.waitForTimeout(500); await expect(page).toHaveScreenshot("path-actions-handlers.png", { clip, maxDiffPixelRatio: 0, threshold: 0, }); }); }); test.describe("Viewport top bar", () => { test("Shows the view only bar of the inspect tab", async ({ page }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.mockRPC(/get\-file\?/, "workspace/get-file-9066.json"); await workspace.goToWorkspace({ fileId: "e179d9df-de35-80bf-8005-283bbd5516b0", pageId: "e179d9df-de35-80bf-8005-283bbd5516b1", }); await workspace.clickLeafLayer("Board"); await workspace.rightSidebar.getByRole("tab", { name: "Inspect" }).click(); const bar = page.getByText("Inspecting code").locator("../../.."); await expect(bar).toBeVisible(); await page.mouse.move(640, 600); await expect(bar).toHaveScreenshot({ maxDiffPixelRatio: 0, threshold: 0, }); }); }); test.describe("Left header", () => { test("Shows the shared badge and the save status", async ({ page }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.mockRPC(/get\-file\?/, "workspace/get-file-library.json"); await workspace.mockRPC( "update-file?id=*", "workspace/update-file-create-rect.json", ); await workspace.goToWorkspace({ fileId: "c1249a66-fce0-8175-8004-7433fe4be8bc", pageId: "c1249a66-fce0-8175-8004-7433fe4be8bd", }); const header = page.locator("header", { hasText: "Testing library 1" }); await expect(header).toBeVisible(); // The badge and the status dot are tiny: compare every pixel. await expect(header).toHaveScreenshot("left-header-shared.png", { maxDiffPixelRatio: 0, threshold: 0, }); await workspace.rectShapeButton.click(); await workspace.clickWithDragViewportAt(128, 128, 200, 100); await expect(header.getByTitle("Saved")).toBeVisible(); await page.mouse.move(640, 600); await expect(header).toHaveScreenshot("left-header-saved.png", { maxDiffPixelRatio: 0, threshold: 0, }); }); test("Shows the Penpot logo link", async ({ page }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.goToWorkspace(); const logo = page.locator("header a[class*='main-icon']"); await expect(logo).toBeVisible(); await page.mouse.move(640, 600); await expect(logo).toHaveScreenshot("left-header-logo.png", { maxDiffPixelRatio: 0, threshold: 0, }); await logo.hover(); await expect(logo).toHaveScreenshot("left-header-logo-hover.png", { maxDiffPixelRatio: 0, threshold: 0, }); }); }); test.describe("Right header", () => { test("Shows the header actions and the zoom menu", async ({ page }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.goToWorkspace(); const header = page.locator('[class*="workspace-header-right"]'); await expect(header).toBeVisible(); await page.mouse.move(640, 300); // The buttons are a small part of the header: compare every pixel. await expect(header).toHaveScreenshot("right-header.png", { maxDiffPixelRatio: 0, threshold: 0, }); await header.getByRole("button", { name: /Comments/ }).click(); await page.mouse.move(640, 300); await expect(header).toHaveScreenshot("right-header-comments.png", { maxDiffPixelRatio: 0, threshold: 0, }); await page.keyboard.press("Escape"); await header.getByTitle("Zoom").click(); await expect(page.getByText("Reset")).toBeVisible(); await page.mouse.move(640, 300); await expect(page).toHaveScreenshot("right-header-zoom.png", { clip: { x: 900, y: 0, width: 380, height: 260 }, maxDiffPixelRatio: 0, threshold: 0, }); }); }); test.describe("Share button", () => { const shot = { maxDiffPixelRatio: 0, threshold: 0 }; test("Share button in the right header", async ({ page }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); // A shared team where the user is admin, so the header shows Share. await workspace.mockRPC("get-teams", "workspace/get-teams-shared.json"); await workspace.goToWorkspace(); const header = page.locator('[class*="workspace-header-right"]'); const share = header .locator("[class*='share-btn']") .or(header.getByRole("button", { name: "Share" })); await expect(share).toBeVisible(); await page.mouse.move(640, 300); await expect(header).toHaveScreenshot(shot); await share.hover(); await page.waitForTimeout(1000); await expect(header).toHaveScreenshot(shot); }); }); test.describe("Undo history", () => { test("Shows the actions list with an entry expanded", async ({ page }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.mockRPC( "update-file?id=*", "workspace/update-file-empty.json", ); await workspace.mockRPC( "get-file-snapshots?file-id=*", "workspace/versions-snapshot-1.json", ); await workspace.goToWorkspace(); await page.keyboard.press("r"); await workspace.clickWithDragViewportAt(128, 128, 200, 100); await page.keyboard.press("Control+Alt+h"); await page.getByRole("tab", { name: "Actions" }).click(); const panel = page.getByRole("tabpanel", { name: "Actions" }); const entry = panel.getByText("New rectangle"); // The short id and the relative time change on every run. const mask = [page.locator('[class*="history-entry-meta"]')]; // The expand arrow only shows while the entry is hovered. await entry.hover(); await expect(panel).toHaveScreenshot({ mask }); // The data attribute exists before and after the DS migration. await panel.locator('[data-has-entry="true"]').click(); await expect(panel.getByText("Rectangle", { exact: true })).toBeVisible(); await entry.hover(); await expect(panel).toHaveScreenshot({ mask }); }); }); test.describe("Palette", () => { test("Shows the bottom palette expanded and collapsed", async ({ page }) => { const workspace = new WorkspacePage(page); const { fileId, pageId } = await setupFileWithAssets(workspace); await workspace.goToWorkspace({ fileId, pageId }); await expect(workspace.page).toHaveScreenshot(); await workspace.palette .getByRole("button", { name: "Typographies" }) .click(); await expect( workspace.palette.getByText("Source Sans Pro Regular"), ).toBeVisible(); await expect(workspace.page).toHaveScreenshot(); await workspace.palette .getByRole("button", { name: "Color Palette" }) .click(); await expect( workspace.palette.getByText( "There are no color styles in your library yet", ), ).toBeVisible(); }); test("Shows the palette toggles selected", async ({ page }) => { const workspace = new WorkspacePage(page); const { fileId, pageId } = await setupFileWithAssets(workspace); await workspace.goToWorkspace({ fileId, pageId }); await workspace.palette .getByRole("button", { name: "Typographies" }) .click(); await expect( workspace.palette.getByText("Source Sans Pro Regular"), ).toBeVisible(); await page.mouse.move(640, 300); // The toggles are a small part of the palette: compare every pixel. await expect(workspace.palette).toHaveScreenshot("palette-text.png", { maxDiffPixelRatio: 0, threshold: 0, }); await workspace.palette .getByRole("button", { name: "Color Palette" }) .click(); await expect( workspace.palette.getByText( "There are no color styles in your library yet", ), ).toBeVisible(); await page.mouse.move(640, 300); await expect(workspace.palette).toHaveScreenshot("palette-color.png", { maxDiffPixelRatio: 0, threshold: 0, }); }); }); test.describe("Plugins", () => { const setupPlugins = async (workspace, profile) => { await workspace.setupEmptyFile(); await workspace.mockRPC("get-team?id=*", "workspace/get-team-plugins.json"); if (profile) { await workspace.mockRPC("get-profile", profile); } await workspace.goToWorkspace(); await workspace.page.keyboard.press("Control+Alt+P"); }; test("Shows the plugins manager without plugins", async ({ page }) => { const workspace = new WorkspacePage(page); await setupPlugins(workspace); await expect(page.getByText("No plugins installed yet")).toBeVisible(); await expect(page.getByRole("button", { name: "Close" })).toBeVisible(); await expect(page).toHaveScreenshot(); }); test("Shows the plugins manager and the permissions dialog", async ({ page, }) => { const workspace = new WorkspacePage(page); await page.route("https://plugin.example.com/manifest.json", (route) => route.fulfill({ json: { name: "Contrast checker", description: "Check the contrast of the selected layers.", code: "plugin.js", permissions: ["content:write", "user:read", "library:write"], }, }), ); await setupPlugins( workspace, "logged-in-user/get-profile-with-plugins.json", ); await expect(page.getByText("Lorem Ipsum generator")).toBeVisible(); await expect(page).toHaveScreenshot(); await page .getByRole("textbox") .fill("https://plugin.example.com/manifest.json"); await page.getByRole("button", { name: "Install" }).click(); await expect(page.getByText("CONTRAST CHECKER")).toBeVisible(); await expect(page).toHaveScreenshot(); }); }); test.describe("Nudge modal", () => { test("Shows the nudge amount modal", async ({ page }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.goToWorkspace(); await page.getByRole("button", { name: "Main menu" }).click(); await page.locator("#file-menu-preferences").click(); await page.locator("#file-menu-nudge").click(); await expect(page.getByText("Nudge amount")).toBeVisible(); await page.mouse.move(0, 0); await expect(workspace.page).toHaveScreenshot("nudge-modal.png", { maxDiffPixelRatio: 0, threshold: 0, }); }); }); test.describe("Text palette", () => { test("Shows the scroll arrows and the palette menu", async ({ page }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.mockRPC( /get\-file\?/, "workspace/get-file-many-typographies.json", ); await workspace.goToWorkspace({ fileId: "c1249a66-fce0-8175-8004-7433fe4be8bc", pageId: "c1249a66-fce0-8175-8004-7433fe4be8bd", }); await workspace.palette .getByRole("button", { name: "Typographies" }) .click(); await expect( workspace.palette.getByText("Type 01 Regular 12"), ).toBeVisible(); await page.mouse.move(640, 300); // The arrows are a small part of the palette: compare every pixel. await expect(workspace.palette).toHaveScreenshot("text-palette.png", { maxDiffPixelRatio: 0, threshold: 0, }); const rightArrow = workspace.palette.locator('[class*="right-arrow"]'); await rightArrow.hover(); await expect(workspace.palette).toHaveScreenshot( "text-palette-arrow-hover.png", { maxDiffPixelRatio: 0, threshold: 0 }, ); await rightArrow.click(); await page.mouse.move(640, 300); await expect(workspace.palette).toHaveScreenshot( "text-palette-scrolled.png", { maxDiffPixelRatio: 0, threshold: 0 }, ); await workspace.palette.locator('[class*="palette-actions"]').click(); await expect(page.getByText("File library")).toBeVisible(); await page.mouse.move(640, 300); await expect(page).toHaveScreenshot("text-palette-menu.png", { clip: { x: 300, y: 450, width: 700, height: 270 }, maxDiffPixelRatio: 0, threshold: 0, }); }); }); test.describe("Selected colors", () => { test("Shows the more colors button", async ({ page }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.mockRPC(/get\-file\?/, "workspace/get-file-9066.json"); await workspace.goToWorkspace({ fileId: "e179d9df-de35-80bf-8005-283bbd5516b0", pageId: "e179d9df-de35-80bf-8005-283bbd5516b1", }); await workspace.clickToggableLayer("Board"); await page.keyboard.press("Control+A"); await expect( workspace.rightSidebar.getByRole("button", { name: "More colors" }), ).toBeVisible(); // The button is a small part of the sidebar: compare every pixel. await expect(workspace.rightSidebar).toHaveScreenshot({ maxDiffPixelRatio: 0, threshold: 0, }); }); }); test.describe("Imported SVG attributes", () => { test("Shows the attributes of an imported SVG shape", async ({ page }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.mockRPC(/get\-file\?/, "workspace/get-file-9930.json"); await workspace.goToWorkspace({ fileId: "8f843b59-7fbb-81ce-8005-aa6d47ae3111", pageId: "fb9798e7-a547-80ae-8005-9ffda4a13e2c", }); // The fourth "workspace" board holds the imported SVG groups. await workspace.clickToggableLayer("workspace", {}, 3); await workspace.clickToggableLayer("Canvas"); await workspace.clickLeafLayer("Rectangle 760 (Stroke)"); const section = workspace.rightSidebar.getByText("Imported SVG Attributes"); await section.scrollIntoViewIfNeeded(); await expect(section).toBeVisible(); // The attributes are a small part of the sidebar: compare every pixel. await expect(workspace.rightSidebar).toHaveScreenshot({ maxDiffPixelRatio: 0, threshold: 0, }); }); }); test.describe("Grid cell options", () => { test("Shows the grid cell options when selecting a cell", async ({ page, }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.mockRPC(/get\-file\?/, "workspace/get-file-9042.json"); await workspace.mockRPC( "get-file-fragment?file-id=*&fragment-id=*", "workspace/get-file-fragment-9042.json", ); await workspace.goToWorkspace({ fileId: "af2494d0-39ba-8184-8005-230696f6df5c", pageId: "af2494d0-39ba-8184-8005-230696f6df5d", }); await workspace.clickLeafLayer("Board"); await page .getByTestId("inspect-layout") .getByRole("button", { name: "Edit grid" }) .click(); // Click the first cell of the grid editor. const cell = page.locator("g.cell-editor > rect").first(); const box = await cell.boundingBox(); await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2); await expect(workspace.rightSidebar.getByText("Grid cell")).toBeVisible(); // The cell options are a small part of the sidebar: compare every pixel. await expect(workspace.rightSidebar).toHaveScreenshot({ maxDiffPixelRatio: 0, threshold: 0, }); }); }); test.describe("Export modal", () => { test("Shows the export frames modal with every selection state", async ({ page, }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.mockRPC( /get\-file\?/, "workspace/get-file-export-frames.json", ); await workspace.mockRPC( "get-file-object-thumbnails?file-id=*", "workspace/get-file-object-thumbnails-blank.json", ); await workspace.mockRPC( "get-file-fragment?file-id=*", "workspace/get-file-fragment-blank.json", ); await workspace.goToWorkspace({ fileId: "8d102c33-b98f-81f6-8006-fdd4ea9780d7", pageId: "8d102c33-b98f-81f6-8006-fdd4ea9780d8", }); await page.getByRole("button", { name: "Main menu" }).click(); await page.getByText("file").last().click(); await page.locator("#file-menu-export-frames").click(); const modal = page.getByText("Export as PDF").locator("../.."); await expect(page.getByText("2 of 2 elements selected")).toBeVisible(); await page.mouse.move(0, 0); // Every row checked. await expect(modal).toHaveScreenshot({ maxDiffPixelRatio: 0, threshold: 0, }); await page.getByRole("button", { name: "Board 1" }).click(); await expect(page.getByText("1 of 2 elements selected")).toBeVisible(); await page.mouse.move(0, 0); // Mixed: the header checkbox shows the "some selected" mark. await expect(modal).toHaveScreenshot({ maxDiffPixelRatio: 0, threshold: 0, }); await page.getByRole("button", { name: "Board 2" }).click(); await expect(page.getByText("0 of 2 elements selected")).toBeVisible(); await page.mouse.move(0, 0); // Nothing checked: the export button is disabled. await expect(modal).toHaveScreenshot({ maxDiffPixelRatio: 0, threshold: 0, }); }); }); test.describe("Unpublish library modal", () => { test("Shows the close, cancel and unpublish buttons", async ({ page }) => { const shot = { maxDiffPixelRatio: 0, threshold: 0 }; const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.mockRPC(/get\-file\?/, "workspace/get-file-library.json"); await workspace.mockRPC( "get-library-file-references?file-id=*", "workspace/get-library-file-references-empty.json", ); await workspace.goToWorkspace({ fileId: "c1249a66-fce0-8175-8004-7433fe4be8bc", pageId: "c1249a66-fce0-8175-8004-7433fe4be8bd", }); await page.getByRole("button", { name: "Main menu" }).click(); await page.locator("#file-menu-file").click(); await page.locator("#file-menu-remove-shared").click(); const modal = page.locator("[class*='delete_shared__modal-container']"); await expect(modal).toBeVisible(); await page.mouse.move(10, 700); await expect(modal).toHaveScreenshot(shot); await modal.locator("[class*='modal-header'] button").hover(); await expect(modal).toHaveScreenshot(shot); }); }); test.describe("Assets tab controls", () => { const shot = { maxDiffPixelRatio: 0, threshold: 0 }; test("Add library button", async ({ page }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.goToWorkspace(); await workspace.clickAssets(); const add = workspace.sidebar.getByTestId("libraries"); await expect(add).toBeVisible(); await page.mouse.move(640, 600); await expect(add).toHaveScreenshot(shot); await add.hover(); await expect(add).toHaveScreenshot(shot); }); test("Libraries, filter and group modal", async ({ page }) => { const workspace = new WorkspacePage(page); const { fileId, pageId } = await setupFileWithAssets(workspace); await workspace.goToWorkspace({ fileId, pageId }); await workspace.clickAssets(); const header = workspace.sidebar .getByTestId("libraries") .locator("xpath=.."); await expect(workspace.sidebar.getByTestId("libraries")).toBeVisible(); await page.mouse.move(640, 600); await expect(header).toHaveScreenshot(shot); await workspace.sidebar.getByTestId("libraries").hover(); await expect(header).toHaveScreenshot(shot); const filter = workspace.sidebar.getByTitle("Filter"); await filter.hover(); await expect(header).toHaveScreenshot(shot); await filter.click(); await page.mouse.move(640, 600); await expect(header).toHaveScreenshot(shot); await page.keyboard.press("Escape"); await workspace.sidebar.getByRole("button", { name: "Colors" }).click(); await workspace.sidebar .locator("[class*='colors'] [class*='asset-list-item']") .first() .click({ button: "right" }); await page.getByText("Group", { exact: true }).click(); const modal = page.locator("[class*='groups__modal-container']"); await expect(modal).toBeVisible(); await page.mouse.move(10, 700); await expect(modal).toHaveScreenshot(shot); }); }); test.describe("Comments panel filter", () => { test("Shows the filter trigger and its menu", async ({ page }) => { const shot = { maxDiffPixelRatio: 0, threshold: 0 }; const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.goToWorkspace(); await workspace.showComments(); const trigger = workspace.rightSidebar.getByRole("button", { name: "Show all comments", }); await expect(trigger).toBeVisible(); await page.mouse.move(640, 600); await expect(trigger).toHaveScreenshot(shot); await trigger.hover(); await expect(trigger).toHaveScreenshot(shot); await trigger.click(); const menu = page.locator("[class*='comment-mode-dropdown']"); await expect(menu).toBeVisible(); await page.mouse.move(640, 600); await expect(menu).toHaveScreenshot(shot); }); }); test.describe("Gradient type select", () => { test("Shows the gradient type select closed, open and changed", async ({ page, }) => { const shot = { maxDiffPixelRatio: 0, threshold: 0 }; const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.mockRPC(/get\-file\?/, "workspace/get-file-not-empty.json"); await workspace.goToWorkspace({ fileId: "6191cd35-bb1f-81f7-8004-7cc63d087374", pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375", }); await workspace.clickLeafLayer("Rectangle"); await page.getByRole("button", { name: "#B1B2B5" }).click(); await page.getByText("Solid").click(); await page.getByText("Gradient").click(); const typeSelect = workspace.colorpicker .getByRole("combobox") .filter({ hasText: "Linear" }); await expect(typeSelect).toBeVisible(); const pickerBox = await workspace.colorpicker.boundingBox(); const selectBox = await typeSelect.boundingBox(); // The gradient bar above the select and room below it for the open list. const clip = { x: Math.floor(pickerBox.x), y: Math.floor(selectBox.y) - 60, width: Math.ceil(pickerBox.width), height: 200, }; await page.mouse.move(0, 0); await expect(page).toHaveScreenshot({ clip, ...shot }); await typeSelect.hover(); await expect(page).toHaveScreenshot({ clip, ...shot }); await typeSelect.click(); const radial = page.getByRole("option", { name: "Radial" }); await expect(radial).toBeVisible(); await page.mouse.move(0, 0); await expect(page).toHaveScreenshot({ clip, ...shot }); await radial.click(); await expect( workspace.colorpicker.getByRole("combobox").filter({ hasText: "Radial" }), ).toBeVisible(); await page.mouse.move(0, 0); await expect(page).toHaveScreenshot({ clip, ...shot }); // The whole gradient panel: preview bar, stops and the stop list. const panel = workspace.colorpicker.locator("[class*='gradient-panel']"); await expect(panel).toHaveScreenshot(shot); }); }); test.describe("Shortcuts panel", () => { const shot = { maxDiffPixelRatio: 0, threshold: 0 }; test("Wraps long key lists between combinations", async ({ page }) => { const workspace = new WorkspacePage(page); await workspace.setupEmptyFile(); await workspace.goToWorkspace(); await page.keyboard.press("Shift+?"); const list = page.locator("[class*='shortcuts-list']"); await expect(list).toBeVisible(); await page.getByRole("textbox").first().fill("board"); const next = page.getByRole("listitem", { name: "Next board" }); const prev = page.getByRole("listitem", { name: "Previous board" }); await prev.scrollIntoViewIfNeeded(); await page.mouse.move(640, 300); await expect(next).toHaveScreenshot(shot); await expect(prev).toHaveScreenshot(shot); }); });