From e3b97638b421e7282f73ecbda8e73e69512cfb8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Mon, 26 Jan 2026 16:46:31 +0100 Subject: [PATCH] :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}