From daedc660b9ba4a0466552cb25f4d0e96dc3745bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Mon, 26 Jan 2026 15:38:29 +0100 Subject: [PATCH 01/11] :wrench: Migrate workspace tests to user the wasm viewport --- frontend/playwright.config.js | 11 +++- frontend/playwright/ui/pages/WorkspacePage.js | 10 +-- .../playwright/ui/specs/render-wasm.spec.js | 2 +- .../playwright/ui/specs/workspace.spec.js | 61 +++++++++--------- .../User-draws-a-rect-1.png | Bin 0 -> 10572 bytes 5 files changed, 43 insertions(+), 41 deletions(-) create mode 100644 frontend/playwright/ui/specs/workspace.spec.js-snapshots/User-draws-a-rect-1.png diff --git a/frontend/playwright.config.js b/frontend/playwright.config.js index 09f33d68d1..4cf0af9a3c 100644 --- a/frontend/playwright.config.js +++ b/frontend/playwright.config.js @@ -43,12 +43,19 @@ export default defineConfig({ projects: [ { name: "default", - use: { ...devices["Desktop Chrome"] }, testDir: "./playwright/ui/specs", use: { + ...devices["Desktop Chrome"], + viewport: { width: 1920, height: 1080 }, // Add custom viewport size video: 'retain-on-failure', trace: 'retain-on-failure', - } + }, + snapshotPathTemplate: "{testDir}/{testFilePath}-snapshots/{arg}.png", + expect: { + toHaveScreenshot: { + maxDiffPixelRatio: 0.001, + }, + }, }, { name: "ds", diff --git a/frontend/playwright/ui/pages/WorkspacePage.js b/frontend/playwright/ui/pages/WorkspacePage.js index 7947fb4368..4809749dfc 100644 --- a/frontend/playwright/ui/pages/WorkspacePage.js +++ b/frontend/playwright/ui/pages/WorkspacePage.js @@ -409,7 +409,7 @@ export class WorkspacePage extends BaseWebSocketPage { await this.viewport.click({ button: "right" }); return this.page.getByText("PasteCtrlV").click(); } - return this.page.keyboard.press("Control+V"); + return this.page.keyboard.press("ControlOrMeta+V"); } async panOnViewportAt(x, y, width, height) { @@ -495,13 +495,7 @@ export class WorkspacePage extends BaseWebSocketPage { async clickColorPalette(clickOptions = {}) { await this.palette - .getByRole("button", { name: "Color Palette (Alt+P)" }) - .click(clickOptions); - } - - async clickColorPalette(clickOptions = {}) { - await this.palette - .getByRole("button", { name: "Color Palette (Alt+P)" }) + .getByRole("button", { name: /Color Palette/ }) .click(clickOptions); } diff --git a/frontend/playwright/ui/specs/render-wasm.spec.js b/frontend/playwright/ui/specs/render-wasm.spec.js index d8b72d13be..1c336bf6a8 100644 --- a/frontend/playwright/ui/specs/render-wasm.spec.js +++ b/frontend/playwright/ui/specs/render-wasm.spec.js @@ -1,5 +1,5 @@ import { test, expect } from "@playwright/test"; -import { WasmWorkspacePage, WASM_FLAGS } from "../pages/WasmWorkspacePage"; +import { WasmWorkspacePage } from "../pages/WasmWorkspacePage"; test.beforeEach(async ({ page }) => { await WasmWorkspacePage.init(page); diff --git a/frontend/playwright/ui/specs/workspace.spec.js b/frontend/playwright/ui/specs/workspace.spec.js index 8489cf122f..7813406720 100644 --- a/frontend/playwright/ui/specs/workspace.spec.js +++ b/frontend/playwright/ui/specs/workspace.spec.js @@ -1,13 +1,13 @@ import { test, expect } from "@playwright/test"; -import { WorkspacePage } from "../pages/WorkspacePage"; +import { WasmWorkspacePage } from "../pages/WasmWorkspacePage"; import { presenceFixture } from "../../data/workspace/ws-notifications"; test.beforeEach(async ({ page }) => { - await WorkspacePage.init(page); + await WasmWorkspacePage.init(page); }); test("User loads worskpace with empty file", async ({ page }) => { - const workspacePage = new WorkspacePage(page); + const workspacePage = new WasmWorkspacePage(page); await workspacePage.setupEmptyFile(page); await workspacePage.goToWorkspace(); @@ -16,7 +16,7 @@ test("User loads worskpace with empty file", async ({ page }) => { }); test("User opens a file with a bad page id", async ({ page }) => { - const workspacePage = new WorkspacePage(page); + const workspacePage = new WasmWorkspacePage(page); await workspacePage.setupEmptyFile(page); await workspacePage.goToWorkspace({ @@ -29,7 +29,7 @@ test("User opens a file with a bad page id", async ({ page }) => { test("User receives presence notifications updates in the workspace", async ({ page, }) => { - const workspacePage = new WorkspacePage(page); + const workspacePage = new WasmWorkspacePage(page); await workspacePage.setupEmptyFile(); await workspacePage.goToWorkspace(); @@ -41,7 +41,7 @@ test("User receives presence notifications updates in the workspace", async ({ }); test("User draws a rect", async ({ page }) => { - const workspacePage = new WorkspacePage(page); + const workspacePage = new WasmWorkspacePage(page); await workspacePage.setupEmptyFile(); await workspacePage.mockRPC( "update-file?id=*", @@ -52,13 +52,12 @@ test("User draws a rect", async ({ page }) => { await workspacePage.rectShapeButton.click(); await workspacePage.clickWithDragViewportAt(128, 128, 200, 100); - const shape = await workspacePage.rootShape.locator("rect"); - await expect(shape).toHaveAttribute("width", "200"); - await expect(shape).toHaveAttribute("height", "100"); + await workspacePage.hideUI(); + await expect(workspacePage.canvas).toHaveScreenshot(); }); test("User makes a group", async ({ page }) => { - const workspacePage = new WorkspacePage(page); + const workspacePage = new WasmWorkspacePage(page); await workspacePage.setupEmptyFile(); await workspacePage.mockRPC( /get\-file\?/, @@ -74,14 +73,14 @@ test("User makes a group", async ({ page }) => { pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375", }); await workspacePage.clickLeafLayer("Rectangle"); - await workspacePage.page.keyboard.press("Control+g"); + await workspacePage.page.keyboard.press("ControlOrMeta+g"); await workspacePage.expectSelectedLayer("Group"); }); test("Bug 7654 - Toolbar keeps toggling on and off on spacebar press", async ({ page, }) => { - const workspacePage = new WorkspacePage(page); + const workspacePage = new WasmWorkspacePage(page); await workspacePage.setupEmptyFile(); await workspacePage.goToWorkspace(); @@ -94,7 +93,7 @@ test("Bug 7654 - Toolbar keeps toggling on and off on spacebar press", async ({ test("Bug 7525 - User moves a scrollbar and no selciont rectangle appears", async ({ page, }) => { - const workspacePage = new WorkspacePage(page); + const workspacePage = new WasmWorkspacePage(page); await workspacePage.setupEmptyFile(); await workspacePage.mockRPC( /get\-file\?/, @@ -130,7 +129,7 @@ test("Bug 7525 - User moves a scrollbar and no selciont rectangle appears", asyn test("User adds a library and its automatically selected in the color palette", async ({ page, }) => { - const workspacePage = new WorkspacePage(page); + const workspacePage = new WasmWorkspacePage(page); await workspacePage.setupEmptyFile(); await workspacePage.mockRPC( "link-file-to-library", @@ -175,7 +174,7 @@ test("User adds a library and its automatically selected in the color palette", test("Bug 10179 - Drag & drop doesn't add colors to the Recent Colors palette", async ({ page, }) => { - const workspacePage = new WorkspacePage(page); + const workspacePage = new WasmWorkspacePage(page); await workspacePage.setupEmptyFile(); await workspacePage.goToWorkspace(); await workspacePage.moveButton.click(); @@ -218,7 +217,7 @@ test("Bug 10179 - Drag & drop doesn't add colors to the Recent Colors palette", test("Bug 7489 - Workspace-palette items stay hidden when opening with keyboard-shortcut", async ({ page, }) => { - const workspacePage = new WorkspacePage(page); + const workspacePage = new WasmWorkspacePage(page); await workspacePage.setupEmptyFile(); await workspacePage.goToWorkspace(); @@ -235,7 +234,7 @@ test("Bug 7489 - Workspace-palette items stay hidden when opening with keyboard- test("Bug 8784 - Use keyboard arrow to move inside a text input does not change tabs", async ({ page, }) => { - const workspacePage = new WorkspacePage(page); + const workspacePage = new WasmWorkspacePage(page); await workspacePage.setupEmptyFile(); await workspacePage.goToWorkspace(); await workspacePage.pageName.click(); @@ -245,7 +244,7 @@ test("Bug 8784 - Use keyboard arrow to move inside a text input does not change }); test("Bug 9066 - Problem with grid layout", async ({ page }) => { - const workspacePage = new WorkspacePage(page); + const workspacePage = new WasmWorkspacePage(page); await workspacePage.setupEmptyFile(page); await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-9066.json"); @@ -273,7 +272,7 @@ test("Bug 9066 - Problem with grid layout", async ({ page }) => { }); test("User have toolbar", async ({ page }) => { - const workspacePage = new WorkspacePage(page); + const workspacePage = new WasmWorkspacePage(page); await workspacePage.setupEmptyFile(page); await workspacePage.goToWorkspace(); @@ -282,7 +281,7 @@ test("User have toolbar", async ({ page }) => { }); test("User have edition menu entries", async ({ page }) => { - const workspacePage = new WorkspacePage(page); + const workspacePage = new WasmWorkspacePage(page); await workspacePage.setupEmptyFile(page); await workspacePage.goToWorkspace(); @@ -298,7 +297,7 @@ test("User have edition menu entries", async ({ page }) => { }); test("Copy/paste properties", async ({ page, context }) => { - const workspacePage = new WorkspacePage(page); + const workspacePage = new WasmWorkspacePage(page); await workspacePage.setupEmptyFile(page); await workspacePage.mockRPC( /get\-file\?/, @@ -355,23 +354,23 @@ test("Copy/paste properties", async ({ page, context }) => { }); test("[Taiga #9929] Paste text in workspace", async ({ page, context }) => { - const workspacePage = new WorkspacePage(page); + const workspacePage = new WasmWorkspacePage(page); await workspacePage.setupEmptyFile(page); await workspacePage.goToWorkspace(); await context.grantPermissions(["clipboard-read", "clipboard-write"]); await page.evaluate(() => navigator.clipboard.writeText("Lorem ipsum dolor")); await workspacePage.viewport.click({ button: "right" }); - await page.getByText("PasteCtrlV").click(); + await page.getByText(/^Paste/i).click(); await workspacePage.viewport .getByRole("textbox") .getByText("Lorem ipsum dolor"); }); -test("[Taiga #9930] Zoom fit all doesn't fits all", async ({ +test("[Taiga #9930] Zoom fit all doesn't fit all shapes", async ({ page, context, }) => { - const workspacePage = new WorkspacePage(page); + const workspacePage = new WasmWorkspacePage(page); await workspacePage.setupEmptyFile(page); await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-9930.json"); await workspacePage.goToWorkspace({ @@ -379,16 +378,18 @@ test("[Taiga #9930] Zoom fit all doesn't fits all", async ({ pageId: "fb9798e7-a547-80ae-8005-9ffda4a13e2c", }); - const zoom = await page.getByTitle("Zoom"); + const zoom = page.getByTitle("Zoom"); await zoom.click(); - const zoomIn = await page.getByRole("button", { name: "Zoom in" }); + const zoomIn = page.getByRole("button", { name: "Zoom in" }); await zoomIn.click(); await zoomIn.click(); await zoomIn.click(); // Zoom fit all await page.keyboard.press("Shift+1"); + // Select all shapes to display selrect + await workspacePage.page.keyboard.press("ControlOrMeta+a"); const ids = [ "shape-165d1e5a-5873-8010-8005-9ffdbeaeec59", @@ -410,7 +411,7 @@ test("[Taiga #9930] Zoom fit all doesn't fits all", async ({ const viewportBoundingBox = await workspacePage.viewport.boundingBox(); for (const id of ids) { - const shape = await page.locator(`.ws-shape-wrapper > g#${id}`); + const shape = page.locator(`.viewport-selrect`); const shapeBoundingBox = await shape.boundingBox(); expect(contains(viewportBoundingBox, shapeBoundingBox)).toBeTruthy(); } @@ -419,7 +420,7 @@ test("[Taiga #9930] Zoom fit all doesn't fits all", async ({ test("Bug 9877, user navigation to dashboard from header goes to blank page", async ({ page, }) => { - const workspacePage = new WorkspacePage(page); + const workspacePage = new WasmWorkspacePage(page); await workspacePage.setupEmptyFile(page); await workspacePage.goToWorkspace(); @@ -436,7 +437,7 @@ test("Bug 9877, user navigation to dashboard from header goes to blank page", as test("Bug 8371 - Flatten option is not visible in context menu", async ({ page, }) => { - const workspacePage = new WorkspacePage(page); + const workspacePage = new WasmWorkspacePage(page); await workspacePage.setupEmptyFile(page); await workspacePage.mockGetFile("workspace/get-file-8371.json"); await workspacePage.goToWorkspace({ diff --git a/frontend/playwright/ui/specs/workspace.spec.js-snapshots/User-draws-a-rect-1.png b/frontend/playwright/ui/specs/workspace.spec.js-snapshots/User-draws-a-rect-1.png new file mode 100644 index 0000000000000000000000000000000000000000..fa630d516499976fbb2b324df5c7284b963b444a GIT binary patch literal 10572 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 Date: Mon, 26 Jan 2026 16:46:31 +0100 Subject: [PATCH 02/11] :wrench: Fix broken / flaky tests --- frontend/playwright.config.js | 5 + .../create-file-object-thumbnail.json | 7 ++ frontend/playwright/ui/pages/WorkspacePage.js | 38 +++++- frontend/playwright/ui/specs/variants.spec.js | 117 ++++++++++-------- .../playwright/ui/specs/workspace.spec.js | 6 +- .../main/ui/workspace/sidebar/layer_item.cljs | 3 + .../main/ui/workspace/sidebar/layer_name.cljs | 2 + 7 files changed, 116 insertions(+), 62 deletions(-) create mode 100644 frontend/playwright/data/workspace/create-file-object-thumbnail.json diff --git a/frontend/playwright.config.js b/frontend/playwright.config.js index 4cf0af9a3c..5474f58360 100644 --- a/frontend/playwright.config.js +++ b/frontend/playwright.config.js @@ -1,4 +1,5 @@ import { defineConfig, devices } from "@playwright/test"; +import { platform } from "os"; /** * Read environment variables from file. @@ -6,6 +7,9 @@ import { defineConfig, devices } from "@playwright/test"; */ // require('dotenv').config(); +const userAgent = platform === 'darwin' ? + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" : undefined; + /** * @see https://playwright.dev/docs/test-configuration */ @@ -49,6 +53,7 @@ export default defineConfig({ viewport: { width: 1920, height: 1080 }, // Add custom viewport size video: 'retain-on-failure', trace: 'retain-on-failure', + userAgent, }, snapshotPathTemplate: "{testDir}/{testFilePath}-snapshots/{arg}.png", expect: { diff --git a/frontend/playwright/data/workspace/create-file-object-thumbnail.json b/frontend/playwright/data/workspace/create-file-object-thumbnail.json new file mode 100644 index 0000000000..ffe22d334d --- /dev/null +++ b/frontend/playwright/data/workspace/create-file-object-thumbnail.json @@ -0,0 +1,7 @@ +{ + "~:file-id": "~u8d38942d-b01f-800e-8007-79ee6a9bac45", + "~:tag": "component", + "~:object-id": "8d38942d-b01f-800e-8007-79ee6a9bac45/8d38942d-b01f-800e-8007-79ee6a9bac46/6b68aedd-4c5b-80b9-8007-7b38c1d34ce4/component", + "~:media-id": "~ube2dc82e-615b-486b-a193-8768bdb51d7a", + "~:created-at": "~m1769523563389" +} \ No newline at end of file diff --git a/frontend/playwright/ui/pages/WorkspacePage.js b/frontend/playwright/ui/pages/WorkspacePage.js index 4809749dfc..290f81b550 100644 --- a/frontend/playwright/ui/pages/WorkspacePage.js +++ b/frontend/playwright/ui/pages/WorkspacePage.js @@ -390,12 +390,38 @@ export class WorkspacePage extends BaseWebSocketPage { } /** - * Copies the selected element into the clipboard. + * Copies the selected element into the clipboard, or copy the + * content of the locator into the clipboard. * * @returns {Promise} */ - async copy() { - return this.page.keyboard.press("Control+C"); + async copy(kind = "keyboard", locator = undefined) { + if (kind === "context-menu" && locator) { + await locator.click({ button: "right" }); + await this.page.getByText("Copy", { exact: true }).click(); + } else { + await this.page.keyboard.press("ControlOrMeta+C"); + } + // wait for the clipboard to be updated + await this.page.waitForFunction(async () => { + const content = await navigator.clipboard.readText() + return content !== ""; + }, { timeout: 1000 }); + } + + async cut(kind = "keyboard", locator = undefined) { + if (kind === "context-menu" && locator) { + await locator.click({ button: "right" }); + await this.page.getByText("Cut", { exact: true }).click(); + } else { + await this.page.keyboard.press("ControlOrMeta+X"); + } + // wait for the clipboard to be updated + await this.page.waitForFunction(async () => { + const content = await navigator.clipboard.readText() + return content !== ""; + }, { timeout: 1000 }); + } /** @@ -448,11 +474,11 @@ export class WorkspacePage extends BaseWebSocketPage { const layer = this.layers .getByTestId("layer-row") .filter({ hasText: name }); - const button = layer.getByRole("button"); + const button = layer.getByTestId("toggle-content"); - await button.waitFor(); + await expect(button).toBeVisible(); await button.click(clickOptions); - await this.page.waitForTimeout(500); + await button.waitFor({ ariaExpanded: true }); } async expectSelectedLayer(name) { diff --git a/frontend/playwright/ui/specs/variants.spec.js b/frontend/playwright/ui/specs/variants.spec.js index 26c083239d..51eafa2156 100644 --- a/frontend/playwright/ui/specs/variants.spec.js +++ b/frontend/playwright/ui/specs/variants.spec.js @@ -1,12 +1,19 @@ import { test, expect } from "@playwright/test"; -import { WorkspacePage } from "../pages/WorkspacePage"; +import { WorkspacePage } from "../pages/WorkspacePage"; import { BaseWebSocketPage } from "../pages/BaseWebSocketPage"; +import { Clipboard } from "../../helpers/Clipboard"; + +test.beforeEach(async ({ page, context }) => { + await Clipboard.enable(context, Clipboard.Permission.ALL); -test.beforeEach(async ({ page }) => { await WorkspacePage.init(page); await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-variants.json"); }); +test.afterEach(async ({ context }) => { + context.clearPermissions(); +}); + const setupVariantsFile = async (workspacePage) => { await workspacePage.setupEmptyFile(); await workspacePage.mockRPC( @@ -34,9 +41,9 @@ const setupVariantsFileWithVariant = async (workspacePage) => { await setupVariantsFile(workspacePage); await workspacePage.clickLeafLayer("Rectangle"); - await workspacePage.page.keyboard.press("Control+k"); + await workspacePage.page.keyboard.press("ControlOrMeta+k"); await workspacePage.page.waitForTimeout(500); - await workspacePage.page.keyboard.press("Control+k"); + await workspacePage.page.keyboard.press("ControlOrMeta+k"); await workspacePage.page.waitForTimeout(500); // We wait until layer-row starts looking like it an component @@ -156,7 +163,7 @@ test("User duplicates a variant container", async ({ page }) => { await variant.container.click(); //Duplicate the variant container - await workspacePage.page.keyboard.press("Control+d"); + await workspacePage.page.keyboard.press("ControlOrMeta+d"); const variant_original = await findVariant(workspacePage, 1); // On duplicate, the new item is the first const variant_duplicate = await findVariant(workspacePage, 0); @@ -169,25 +176,27 @@ test("User duplicates a variant container", async ({ page }) => { await validateVariant(variant_duplicate); }); -test("User copy paste a variant container", async ({ page }) => { +test("User copy paste a variant container", async ({ page, context }) => { const workspacePage = new WorkspacePage(page); + // Access to the read/write clipboard necesary for this functionality await setupVariantsFileWithVariant(workspacePage); + await workspacePage.mockRPC( + /create-file-object-thumbnail.*/, + "workspace/create-file-object-thumbnail.json", + ); const variant = findVariantNoWait(workspacePage, 0); - // await variant.container.waitFor(); - - // Select the variant container - await variant.container.click(); - - await workspacePage.page.waitForTimeout(1000); - // Copy the variant container - await workspacePage.page.keyboard.press("Control+c"); + await workspacePage.clickLeafLayer("Rectangle"); + await workspacePage.copy("keyboard"); // Paste the variant container await workspacePage.clickAt(400, 400); - await workspacePage.page.keyboard.press("Control+v"); + await workspacePage.paste("keyboard"); + + const variants = workspacePage.layers.getByText("Rectangle"); + await expect(variants).toHaveCount(2); const variantDuplicate = findVariantNoWait(workspacePage, 0); const variantOriginal = findVariantNoWait(workspacePage, 1); @@ -212,18 +221,17 @@ test("User cut paste a variant container", async ({ page }) => { await variant.container.click(); //Cut the variant container - await workspacePage.page.keyboard.press("Control+x"); - await workspacePage.page.waitForTimeout(500); + await workspacePage.cut("keyboard"); //Paste the variant container await workspacePage.clickAt(500, 500); - await workspacePage.page.keyboard.press("Control+v"); + await workspacePage.paste("keyboard"); await workspacePage.page.waitForTimeout(500); const variantPasted = await findVariant(workspacePage, 0); // Expand the layers - await variantPasted.container.locator("button").first().click(); + await workspacePage.clickToggableLayer("Rectangle"); // The variants are valid await validateVariant(variantPasted); @@ -239,27 +247,34 @@ test("User cut paste a variant container into a board, and undo twice", async ({ //Create a board await workspacePage.boardButton.click(); - await workspacePage.clickWithDragViewportAt(500, 500, 100, 100); + // NOTE: this board should not intersect the existing variants, otherwise + // this test is flaky + await workspacePage.clickWithDragViewportAt(200, 200, 100, 100); await workspacePage.clickAt(495, 495); const board = await workspacePage.rootShape.locator("Board"); // Select the variant container - await variant.container.click(); + // await variant.container.click(); + await workspacePage.clickLeafLayer("Rectangle"); //Cut the variant container - await workspacePage.page.keyboard.press("Control+x"); - await workspacePage.page.waitForTimeout(500); + await workspacePage.cut("keyboard"); + await expect(variant.container).not.toBeVisible(); //Select the board await workspacePage.clickLeafLayer("Board"); //Paste the variant container inside the board - await workspacePage.page.keyboard.press("Control+v"); + await workspacePage.paste("keyboard"); + await expect(variant.container).toBeVisible(); //Undo twice - await workspacePage.page.keyboard.press("Control+z"); - await workspacePage.page.keyboard.press("Control+z"); - await workspacePage.page.waitForTimeout(500); + await workspacePage.page.keyboard.press("ControlOrMeta+z"); + + await expect(variant.container).not.toBeVisible(); + + await workspacePage.page.keyboard.press("ControlOrMeta+z"); + await expect(variant.container).toBeVisible(); const variantAfterUndo = await findVariant(workspacePage, 0); @@ -276,12 +291,12 @@ test("User copy paste a variant", async ({ page }) => { // Select the variant1 await variant.variant1.click(); - //Cut the variant - await workspacePage.page.keyboard.press("Control+c"); + // Copy the variant + await workspacePage.copy("keyboard"); - //Paste the variant + // Paste the variant await workspacePage.clickAt(500, 500); - await workspacePage.page.keyboard.press("Control+v"); + await workspacePage.paste("keyboard"); const copy = await workspacePage.layers .getByTestId("layer-row") @@ -302,11 +317,11 @@ test("User cut paste a variant outside the container", async ({ page }) => { await variant.variant1.click(); //Cut the variant - await workspacePage.page.keyboard.press("Control+x"); + await workspacePage.cut("keyboard"); //Paste the variant await workspacePage.clickAt(500, 500); - await workspacePage.page.keyboard.press("Control+v"); + await workspacePage.paste("keyboard"); const component = await workspacePage.layers .getByTestId("layer-row") @@ -324,15 +339,11 @@ test("User drag and drop a variant outside the container", async ({ page }) => { const variant = await findVariant(workspacePage, 0); // Drag and drop the variant - await workspacePage.clickWithDragViewportAt(350, 400, 0, 200); + // FIXME: to make this test more resilient, we should get the bounding box of the Value 1 variant + // and use it to calculate the target position + await workspacePage.clickWithDragViewportAt(600, 500, 0, 300); - const component = await workspacePage.layers - .getByTestId("layer-row") - .filter({ has: workspacePage.page.getByText("Rectangle / Value 1") }) - .filter({ has: workspacePage.page.getByTestId("icon-component") }); - - //The component exists and is visible - await expect(component).toBeVisible(); + await expect(workspacePage.layers.getByText("Rectangle / Value 1")).toBeVisible(); }); test("User cut paste a component inside a variant", async ({ page }) => { @@ -345,14 +356,14 @@ test("User cut paste a component inside a variant", async ({ page }) => { await workspacePage.ellipseShapeButton.click(); await workspacePage.clickWithDragViewportAt(500, 500, 20, 20); await workspacePage.clickLeafLayer("Ellipse"); - await workspacePage.page.keyboard.press("Control+k"); + await workspacePage.page.keyboard.press("ControlOrMeta+k"); //Cut the component - await workspacePage.page.keyboard.press("Control+x"); + await workspacePage.cut("keyboard"); //Paste the component inside the variant await variant.container.click(); - await workspacePage.page.keyboard.press("Control+v"); + await workspacePage.paste("keyboard"); const variant3 = await workspacePage.layers .getByTestId("layer-row") @@ -376,7 +387,7 @@ test("User cut paste a component with path inside a variant", async ({ await workspacePage.ellipseShapeButton.click(); await workspacePage.clickWithDragViewportAt(500, 500, 20, 20); await workspacePage.clickLeafLayer("Ellipse"); - await workspacePage.page.keyboard.press("Control+k"); + await workspacePage.page.keyboard.press("ControlOrMeta+k"); //Rename the component await workspacePage.layers.getByText("Ellipse").dblclick(); @@ -387,11 +398,11 @@ test("User cut paste a component with path inside a variant", async ({ await workspacePage.page.keyboard.press("Enter"); //Cut the component - await workspacePage.page.keyboard.press("Control+x"); + await workspacePage.cut("keyboard"); //Paste the component inside the variant await variant.container.click(); - await workspacePage.page.keyboard.press("Control+v"); + await workspacePage.paste("keyboard"); const variant3 = await workspacePage.layers .getByTestId("layer-row") @@ -415,7 +426,7 @@ test("User drag and drop a component with path inside a variant", async ({ await workspacePage.ellipseShapeButton.click(); await workspacePage.clickWithDragViewportAt(500, 500, 20, 20); await workspacePage.clickLeafLayer("Ellipse"); - await workspacePage.page.keyboard.press("Control+k"); + await workspacePage.page.keyboard.press("ControlOrMeta+k"); //Rename the component await workspacePage.layers.getByText("Ellipse").dblclick(); @@ -426,7 +437,7 @@ test("User drag and drop a component with path inside a variant", async ({ await workspacePage.page.keyboard.press("Enter"); //Drag and drop the component the component - await workspacePage.clickWithDragViewportAt(510, 510, 0, -200); + await workspacePage.clickWithDragViewportAt(510, 510, 200, 0); const variant3 = await workspacePage.layers .getByTestId("layer-row") @@ -446,8 +457,8 @@ test("User cut paste a variant into another container", async ({ page }) => { await workspacePage.ellipseShapeButton.click(); await workspacePage.clickWithDragViewportAt(500, 500, 20, 20); await workspacePage.clickLeafLayer("Ellipse"); - await workspacePage.page.keyboard.press("Control+k"); - await workspacePage.page.keyboard.press("Control+k"); + await workspacePage.page.keyboard.press("ControlOrMeta+k"); + await workspacePage.page.keyboard.press("ControlOrMeta+k"); const variantOrigin = await findVariantNoWait(workspacePage, 1); @@ -457,11 +468,11 @@ test("User cut paste a variant into another container", async ({ page }) => { await variantOrigin.variant1.click(); //Cut the variant - await workspacePage.page.keyboard.press("Control+x"); + await workspacePage.cut("keyboard"); //Paste the variant await workspacePage.layers.getByText("Ellipse").first().click(); - await workspacePage.page.keyboard.press("Control+v"); + await workspacePage.paste("keyboard"); const variant3 = workspacePage.layers .getByTestId("layer-row") diff --git a/frontend/playwright/ui/specs/workspace.spec.js b/frontend/playwright/ui/specs/workspace.spec.js index 7813406720..8d13802a97 100644 --- a/frontend/playwright/ui/specs/workspace.spec.js +++ b/frontend/playwright/ui/specs/workspace.spec.js @@ -90,7 +90,7 @@ test("Bug 7654 - Toolbar keeps toggling on and off on spacebar press", async ({ await workspacePage.expectHiddenToolbarOptions(); }); -test("Bug 7525 - User moves a scrollbar and no selciont rectangle appears", async ({ +test("Bug 7525 - User moves a scrollbar and no selection rectangle appears", async ({ page, }) => { const workspacePage = new WasmWorkspacePage(page); @@ -109,8 +109,8 @@ test("Bug 7525 - User moves a scrollbar and no selciont rectangle appears", asyn pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375", }); - // Move created rect to a corner, in orther to get scrollbars - await workspacePage.panOnViewportAt(128, 128, 300, 300); + // Move created rect to a corner, in order to get scrollbars + await workspacePage.panOnViewportAt(128, 128, 600, 600); // Check scrollbars appear const horizontalScrollbar = workspacePage.horizontalScrollbar; diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs b/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs index 5b1d42c50e..d65b1cbfc8 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs @@ -119,6 +119,9 @@ [:button {:class (stl/css-case :toggle-content true :inverse expanded?) + :data-testid "toggle-content" + :aria-expanded expanded? + :aria-labelledby (dm/str "layer-name-" id) :on-click on-toggle-collapse} deprecated-icon/arrow]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs b/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs index 642b3d27f5..ffe019638d 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs @@ -108,6 +108,7 @@ :on-blur accept-edit :on-key-down on-key-down :auto-focus true + :id (dm/str "layer-name-" shape-id) :default-value (d/nilv default-value "")}] [:* [:span @@ -118,6 +119,7 @@ :hidden is-hidden :type-comp type-comp :type-frame type-frame) + :id (dm/str "layer-name-" shape-id) :style {"--depth" depth "--parent-size" parent-size} :ref ref :on-double-click start-edit} From 4d5c874b914a132081bf18a5d488c95dd6111611 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Mon, 2 Feb 2026 14:18:38 +0100 Subject: [PATCH 03/11] :wrench: Fix typography token test --- .../src/app/main/ui/inspect/styles/rows/properties_row.cljs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/main/ui/inspect/styles/rows/properties_row.cljs b/frontend/src/app/main/ui/inspect/styles/rows/properties_row.cljs index e3b3104fa8..0a317d7339 100644 --- a/frontend/src/app/main/ui/inspect/styles/rows/properties_row.cljs +++ b/frontend/src/app/main/ui/inspect/styles/rows/properties_row.cljs @@ -21,7 +21,7 @@ (def ^:private schema:properties-row [:map [:term :string] - [:detail :string] + [:detail {:optional true} [:maybe :string]] [:property {:optional true} :string] ;; CSS valid property [:token {:optional true} :any] ;; resolved token object [:copiable {:optional true} :boolean]]) From 2c4efc6b5306ca6aa9e35b67b9d520cd21b1794c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Mon, 2 Feb 2026 15:39:43 +0100 Subject: [PATCH 04/11] :wrench: Fix onboarding test --- frontend/playwright/ui/specs/onboarding.spec.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/playwright/ui/specs/onboarding.spec.js b/frontend/playwright/ui/specs/onboarding.spec.js index b39c3b958b..a14b0abe42 100644 --- a/frontend/playwright/ui/specs/onboarding.spec.js +++ b/frontend/playwright/ui/specs/onboarding.spec.js @@ -15,6 +15,8 @@ test("User can complete the onboarding", async ({ page }) => { const dashboardPage = new DashboardPage(page); const onboardingPage = new OnboardingPage(page); + await dashboardPage.mockConfigFlags(["enable-onboarding"]); + await dashboardPage.goToDashboard(); await expect( page.getByRole("heading", { name: "Help us get to know you" }), From cc326f23cf36e4c5f941b05d3683ea8aa12b447f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Mon, 2 Feb 2026 16:15:08 +0100 Subject: [PATCH 05/11] :wrench: Adjust timeout of websocket readiness (playwright) --- frontend/playwright/ui/pages/WorkspacePage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/playwright/ui/pages/WorkspacePage.js b/frontend/playwright/ui/pages/WorkspacePage.js index 290f81b550..741c892111 100644 --- a/frontend/playwright/ui/pages/WorkspacePage.js +++ b/frontend/playwright/ui/pages/WorkspacePage.js @@ -253,7 +253,7 @@ export class WorkspacePage extends BaseWebSocketPage { async #waitForWebSocketReadiness() { // TODO: find a better event to settle whether the app is ready to receive notifications via ws - await expect(this.pageName).toHaveText("Page 1"); + await expect(this.pageName).toHaveText("Page 1", { timeout: 30000 }) } async sendPresenceMessage(fixture) { From 629649aca67a0c9395ec3ca266af80f76dffa3ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Mon, 2 Feb 2026 16:25:16 +0100 Subject: [PATCH 06/11] :wrench: Fix config playwright syntax --- frontend/playwright.config.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/playwright.config.js b/frontend/playwright.config.js index 5474f58360..e673f1da8f 100644 --- a/frontend/playwright.config.js +++ b/frontend/playwright.config.js @@ -8,7 +8,8 @@ import { platform } from "os"; // require('dotenv').config(); const userAgent = platform === 'darwin' ? - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" : undefined; + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" : + undefined; /** * @see https://playwright.dev/docs/test-configuration From 79be3ab7dfbe5a9938c63ae64e87c973648be807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Tue, 3 Feb 2026 10:39:38 +0100 Subject: [PATCH 07/11] :wrench: Fix text editor flaky tests --- frontend/playwright/ui/pages/WorkspacePage.js | 5 +++-- frontend/playwright/ui/specs/text-editor-v2.spec.js | 2 +- .../src/app/main/ui/workspace/shapes/text/v2_editor.cljs | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/playwright/ui/pages/WorkspacePage.js b/frontend/playwright/ui/pages/WorkspacePage.js index 741c892111..a9d6e1d939 100644 --- a/frontend/playwright/ui/pages/WorkspacePage.js +++ b/frontend/playwright/ui/pages/WorkspacePage.js @@ -383,7 +383,8 @@ export class WorkspacePage extends BaseWebSocketPage { await this.page.keyboard.press("T"); await this.page.waitForTimeout(timeToWait); await this.clickAndMove(x1, y1, x2, y2); - await this.page.waitForTimeout(timeToWait); + await expect(this.page.getByTestId("text-editor")).toBeVisible(); + if (initialText) { await this.page.keyboard.type(initialText); } @@ -433,7 +434,7 @@ export class WorkspacePage extends BaseWebSocketPage { async paste(kind = "keyboard") { if (kind === "context-menu") { await this.viewport.click({ button: "right" }); - return this.page.getByText("PasteCtrlV").click(); + return this.page.getByText("Paste", { exact: true }).click(); } return this.page.keyboard.press("ControlOrMeta+V"); } diff --git a/frontend/playwright/ui/specs/text-editor-v2.spec.js b/frontend/playwright/ui/specs/text-editor-v2.spec.js index cc6061f192..26c62a6825 100644 --- a/frontend/playwright/ui/specs/text-editor-v2.spec.js +++ b/frontend/playwright/ui/specs/text-editor-v2.spec.js @@ -5,7 +5,7 @@ import { WorkspacePage } from "../pages/WorkspacePage"; const timeToWait = 100; test.beforeEach(async ({ page, context }) => { - await Clipboard.enable(context, Clipboard.Permission.ONLY_WRITE); + await Clipboard.enable(context, Clipboard.Permission.ALL); await WorkspacePage.init(page); await WorkspacePage.mockConfigFlags(page, ["enable-feature-text-editor-v2"]); diff --git a/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.cljs index 909ba2eca7..98e82162e2 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.cljs @@ -401,7 +401,8 @@ (dm/fmt "scale(%)" maybe-zoom))}))] [:g.text-editor {:clip-path (dm/fmt "url(#%)" clip-id) - :transform (dm/str transform)} + :transform (dm/str transform) + :data-testid "text-editor"} [:defs [:clipPath {:id clip-id} [:rect {:x x :y y :width width :height height}]]] From f7403935c817c60556ec51c39df3ca37dc64c5c2 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 3 Feb 2026 08:47:43 +0100 Subject: [PATCH 08/11] :tada: Improve shadows rendering performance --- render-wasm/src/render.rs | 43 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 76eedf0288..9b783e76b8 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -1512,6 +1512,16 @@ impl RenderState { Self::combine_blur_values(self.combined_layer_blur(shape.blur), extra_layer_blur); let blur_filter = combined_blur .and_then(|blur| skia::image_filters::blur((blur.value, blur.value), None, None, None)); + // Legacy path is only stable up to 1.0 zoom: the canvas is scaled and the shadow + // filter is evaluated in that scaled space, so for scale > 1 it over-inflates blur/spread. + // We also disable it when combined layer blur is present to avoid incorrect composition. + let use_low_zoom_path = scale <= 1.0 && combined_blur.is_none(); + + if use_low_zoom_path { + // Match pre-commit behavior: scale blur/spread with zoom for low zoom levels. + transformed_shadow.to_mut().blur = shadow.blur * scale; + transformed_shadow.to_mut().spread = shadow.spread * scale; + } let mut transform_matrix = shape.transform; let center = shape.center(); @@ -1556,6 +1566,39 @@ impl RenderState { let mut bounds = drop_filter.compute_fast_bounds(shape_bounds); // Account for the shadow offset so the temporary surface fully contains the shifted blur. bounds.offset(world_offset); + // Early cull if the shadow bounds are outside the render area. + if !bounds.intersects(self.render_area) { + return; + } + + if use_low_zoom_path { + let mut shadow_paint = skia::Paint::default(); + shadow_paint.set_image_filter(drop_filter); + shadow_paint.set_blend_mode(skia::BlendMode::SrcOver); + + let layer_rec = skia::canvas::SaveLayerRec::default().paint(&shadow_paint); + let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows); + drop_canvas.save_layer(&layer_rec); + drop_canvas.scale((scale, scale)); + drop_canvas.translate(translation); + + self.with_nested_blurs_suppressed(|state| { + state.render_shape( + &plain_shape, + clip_bounds, + SurfaceId::DropShadows, + SurfaceId::DropShadows, + SurfaceId::DropShadows, + SurfaceId::DropShadows, + false, + Some(shadow.offset), + None, + ); + }); + + self.surfaces.canvas(SurfaceId::DropShadows).restore(); + return; + } let filter_result = filters::render_into_filter_surface(self, bounds, |state, temp_surface| { From 5be887f10bb755deb2e8b5605bd18227fb838560 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 3 Feb 2026 11:21:01 +0100 Subject: [PATCH 09/11] :tada: Improve plain shape calculation --- render-wasm/src/render.rs | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 9b783e76b8..b1968e7e99 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -22,7 +22,7 @@ pub use surfaces::{SurfaceId, Surfaces}; use crate::performance; use crate::shapes::{ - all_with_ancestors, Blur, BlurType, Corners, Fill, Shadow, Shape, SolidColor, Stroke, Type, + all_with_ancestors, Blur, BlurType, Corners, Fill, Shadow, Shape, SolidColor, Type, }; use crate::state::{ShapesPoolMutRef, ShapesPoolRef}; use crate::tiles::{self, PendingTiles, TileRect}; @@ -1536,28 +1536,20 @@ impl RenderState { let world_offset = (mapped.x, mapped.y); // The opacity of fills and strokes shouldn't affect the shadow, - // so we paint everything black with the same opacity - plain_shape.to_mut().clear_fills(); + // so we paint everything black with the same opacity. + let plain_shape_mut = plain_shape.to_mut(); + plain_shape_mut.clear_fills(); if shape.has_fills() { - plain_shape - .to_mut() - .add_fill(Fill::Solid(SolidColor(skia::Color::BLACK))); + plain_shape_mut.add_fill(Fill::Solid(SolidColor(skia::Color::BLACK))); } - plain_shape.to_mut().clear_strokes(); - for stroke in shape.strokes.iter() { - plain_shape.to_mut().add_stroke(Stroke { - fill: Fill::Solid(SolidColor(skia::Color::BLACK)), - width: stroke.width, - style: stroke.style, - cap_end: stroke.cap_end, - cap_start: stroke.cap_start, - kind: stroke.kind, - }); + // Reuse existing strokes and only override their fill color. + for stroke in plain_shape_mut.strokes.iter_mut() { + stroke.fill = Fill::Solid(SolidColor(skia::Color::BLACK)); } - plain_shape.to_mut().clear_shadows(); - plain_shape.to_mut().blur = None; + plain_shape_mut.clear_shadows(); + plain_shape_mut.blur = None; let Some(drop_filter) = transformed_shadow.get_drop_shadow_filter() else { return; From 25aff100cfc96fcbd28ef69a044b7841f4119706 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 3 Feb 2026 12:42:24 +0100 Subject: [PATCH 10/11] :tada: Add shadows playground for render wasm --- .../resources/wasm-playground/shadows.html | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 frontend/resources/wasm-playground/shadows.html diff --git a/frontend/resources/wasm-playground/shadows.html b/frontend/resources/wasm-playground/shadows.html new file mode 100644 index 0000000000..ea6b206633 --- /dev/null +++ b/frontend/resources/wasm-playground/shadows.html @@ -0,0 +1,90 @@ + + + + + WASM + WebGL2 Canvas + + + + + + + From 24c8fc484faf5aad99f7d2dfb7fe733eb97a4f82 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 4 Feb 2026 09:45:37 +0100 Subject: [PATCH 11/11] :bug: Fix Internal Error when adding a new text layer and trying to go back to Dashboard without saving --- frontend/src/app/main/ui/workspace/left_header.cljs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/left_header.cljs b/frontend/src/app/main/ui/workspace/left_header.cljs index 914d4ef1a8..046a4aee58 100644 --- a/frontend/src/app/main/ui/workspace/left_header.cljs +++ b/frontend/src/app/main/ui/workspace/left_header.cljs @@ -78,13 +78,15 @@ (fn [] (close-modals) ;; FIXME: move set-mode to uri? - (st/emit! (dw/set-options-mode :design) + (st/emit! :interrupt + (dw/set-options-mode :design) (dcm/go-to-dashboard-recent)))) nav-to-project (mf/use-fn (mf/deps project-id) - #(st/emit! (dcm/go-to-dashboard-files ::rt/new-window true :project-id project-id)))] + #(st/emit! :interrupt + (dcm/go-to-dashboard-files ::rt/new-window true :project-id project-id)))] (mf/with-effect [editing?] (when ^boolean editing?