Merge remote-tracking branch 'origin/staging-render' into develop

This commit is contained in:
Alejandro Alonso
2026-02-04 13:50:13 +01:00
16 changed files with 316 additions and 130 deletions

View File

@@ -1,4 +1,5 @@
import { defineConfig, devices } from "@playwright/test"; import { defineConfig, devices } from "@playwright/test";
import { platform } from "os";
/** /**
* Read environment variables from file. * Read environment variables from file.
@@ -6,6 +7,10 @@ import { defineConfig, devices } from "@playwright/test";
*/ */
// require('dotenv').config(); // 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" :
undefined;
/** /**
* @see https://playwright.dev/docs/test-configuration * @see https://playwright.dev/docs/test-configuration
*/ */
@@ -43,12 +48,20 @@ export default defineConfig({
projects: [ projects: [
{ {
name: "default", name: "default",
use: { ...devices["Desktop Chrome"] },
testDir: "./playwright/ui/specs", testDir: "./playwright/ui/specs",
use: { use: {
...devices["Desktop Chrome"],
viewport: { width: 1920, height: 1080 }, // Add custom viewport size
video: 'retain-on-failure', video: 'retain-on-failure',
trace: 'retain-on-failure', trace: 'retain-on-failure',
} userAgent,
},
snapshotPathTemplate: "{testDir}/{testFilePath}-snapshots/{arg}.png",
expect: {
toHaveScreenshot: {
maxDiffPixelRatio: 0.001,
},
},
}, },
{ {
name: "ds", name: "ds",

View File

@@ -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"
}

View File

@@ -253,7 +253,7 @@ export class WorkspacePage extends BaseWebSocketPage {
async #waitForWebSocketReadiness() { async #waitForWebSocketReadiness() {
// TODO: find a better event to settle whether the app is ready to receive notifications via ws // 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) { async sendPresenceMessage(fixture) {
@@ -383,19 +383,46 @@ export class WorkspacePage extends BaseWebSocketPage {
await this.page.keyboard.press("T"); await this.page.keyboard.press("T");
await this.page.waitForTimeout(timeToWait); await this.page.waitForTimeout(timeToWait);
await this.clickAndMove(x1, y1, x2, y2); await this.clickAndMove(x1, y1, x2, y2);
await this.page.waitForTimeout(timeToWait); await expect(this.page.getByTestId("text-editor")).toBeVisible();
if (initialText) { if (initialText) {
await this.page.keyboard.type(initialText); await this.page.keyboard.type(initialText);
} }
} }
/** /**
* 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<void>} * @returns {Promise<void>}
*/ */
async copy() { async copy(kind = "keyboard", locator = undefined) {
return this.page.keyboard.press("Control+C"); 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 });
} }
/** /**
@@ -407,9 +434,9 @@ export class WorkspacePage extends BaseWebSocketPage {
async paste(kind = "keyboard") { async paste(kind = "keyboard") {
if (kind === "context-menu") { if (kind === "context-menu") {
await this.viewport.click({ button: "right" }); 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("Control+V"); return this.page.keyboard.press("ControlOrMeta+V");
} }
async panOnViewportAt(x, y, width, height) { async panOnViewportAt(x, y, width, height) {
@@ -448,11 +475,11 @@ export class WorkspacePage extends BaseWebSocketPage {
const layer = this.layers const layer = this.layers
.getByTestId("layer-row") .getByTestId("layer-row")
.filter({ hasText: name }); .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 button.click(clickOptions);
await this.page.waitForTimeout(500); await button.waitFor({ ariaExpanded: true });
} }
async expectSelectedLayer(name) { async expectSelectedLayer(name) {
@@ -495,13 +522,7 @@ export class WorkspacePage extends BaseWebSocketPage {
async clickColorPalette(clickOptions = {}) { async clickColorPalette(clickOptions = {}) {
await this.palette await this.palette
.getByRole("button", { name: "Color Palette (Alt+P)" }) .getByRole("button", { name: /Color Palette/ })
.click(clickOptions);
}
async clickColorPalette(clickOptions = {}) {
await this.palette
.getByRole("button", { name: "Color Palette (Alt+P)" })
.click(clickOptions); .click(clickOptions);
} }

View File

@@ -15,6 +15,8 @@ test("User can complete the onboarding", async ({ page }) => {
const dashboardPage = new DashboardPage(page); const dashboardPage = new DashboardPage(page);
const onboardingPage = new OnboardingPage(page); const onboardingPage = new OnboardingPage(page);
await dashboardPage.mockConfigFlags(["enable-onboarding"]);
await dashboardPage.goToDashboard(); await dashboardPage.goToDashboard();
await expect( await expect(
page.getByRole("heading", { name: "Help us get to know you" }), page.getByRole("heading", { name: "Help us get to know you" }),

View File

@@ -1,5 +1,5 @@
import { test, expect } from "@playwright/test"; import { test, expect } from "@playwright/test";
import { WasmWorkspacePage, WASM_FLAGS } from "../pages/WasmWorkspacePage"; import { WasmWorkspacePage } from "../pages/WasmWorkspacePage";
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await WasmWorkspacePage.init(page); await WasmWorkspacePage.init(page);

View File

@@ -5,7 +5,7 @@ import { WorkspacePage } from "../pages/WorkspacePage";
const timeToWait = 100; const timeToWait = 100;
test.beforeEach(async ({ page, context }) => { 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.init(page);
await WorkspacePage.mockConfigFlags(page, ["enable-feature-text-editor-v2"]); await WorkspacePage.mockConfigFlags(page, ["enable-feature-text-editor-v2"]);

View File

@@ -1,12 +1,19 @@
import { test, expect } from "@playwright/test"; import { test, expect } from "@playwright/test";
import { WorkspacePage } from "../pages/WorkspacePage"; import { WorkspacePage } from "../pages/WorkspacePage";
import { BaseWebSocketPage } from "../pages/BaseWebSocketPage"; 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 WorkspacePage.init(page);
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-variants.json"); await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-variants.json");
}); });
test.afterEach(async ({ context }) => {
context.clearPermissions();
});
const setupVariantsFile = async (workspacePage) => { const setupVariantsFile = async (workspacePage) => {
await workspacePage.setupEmptyFile(); await workspacePage.setupEmptyFile();
await workspacePage.mockRPC( await workspacePage.mockRPC(
@@ -34,9 +41,9 @@ const setupVariantsFileWithVariant = async (workspacePage) => {
await setupVariantsFile(workspacePage); await setupVariantsFile(workspacePage);
await workspacePage.clickLeafLayer("Rectangle"); 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.waitForTimeout(500);
await workspacePage.page.keyboard.press("Control+k"); await workspacePage.page.keyboard.press("ControlOrMeta+k");
await workspacePage.page.waitForTimeout(500); await workspacePage.page.waitForTimeout(500);
// We wait until layer-row starts looking like it an component // 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(); await variant.container.click();
//Duplicate the variant container //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_original = await findVariant(workspacePage, 1); // On duplicate, the new item is the first
const variant_duplicate = await findVariant(workspacePage, 0); const variant_duplicate = await findVariant(workspacePage, 0);
@@ -169,25 +176,27 @@ test("User duplicates a variant container", async ({ page }) => {
await validateVariant(variant_duplicate); 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); const workspacePage = new WorkspacePage(page);
// Access to the read/write clipboard necesary for this functionality
await setupVariantsFileWithVariant(workspacePage); await setupVariantsFileWithVariant(workspacePage);
await workspacePage.mockRPC(
/create-file-object-thumbnail.*/,
"workspace/create-file-object-thumbnail.json",
);
const variant = findVariantNoWait(workspacePage, 0); 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 // Copy the variant container
await workspacePage.page.keyboard.press("Control+c"); await workspacePage.clickLeafLayer("Rectangle");
await workspacePage.copy("keyboard");
// Paste the variant container // Paste the variant container
await workspacePage.clickAt(400, 400); 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 variantDuplicate = findVariantNoWait(workspacePage, 0);
const variantOriginal = findVariantNoWait(workspacePage, 1); const variantOriginal = findVariantNoWait(workspacePage, 1);
@@ -212,18 +221,17 @@ test("User cut paste a variant container", async ({ page }) => {
await variant.container.click(); await variant.container.click();
//Cut the variant container //Cut the variant container
await workspacePage.page.keyboard.press("Control+x"); await workspacePage.cut("keyboard");
await workspacePage.page.waitForTimeout(500);
//Paste the variant container //Paste the variant container
await workspacePage.clickAt(500, 500); await workspacePage.clickAt(500, 500);
await workspacePage.page.keyboard.press("Control+v"); await workspacePage.paste("keyboard");
await workspacePage.page.waitForTimeout(500); await workspacePage.page.waitForTimeout(500);
const variantPasted = await findVariant(workspacePage, 0); const variantPasted = await findVariant(workspacePage, 0);
// Expand the layers // Expand the layers
await variantPasted.container.locator("button").first().click(); await workspacePage.clickToggableLayer("Rectangle");
// The variants are valid // The variants are valid
await validateVariant(variantPasted); await validateVariant(variantPasted);
@@ -239,27 +247,34 @@ test("User cut paste a variant container into a board, and undo twice", async ({
//Create a board //Create a board
await workspacePage.boardButton.click(); 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); await workspacePage.clickAt(495, 495);
const board = await workspacePage.rootShape.locator("Board"); const board = await workspacePage.rootShape.locator("Board");
// Select the variant container // Select the variant container
await variant.container.click(); // await variant.container.click();
await workspacePage.clickLeafLayer("Rectangle");
//Cut the variant container //Cut the variant container
await workspacePage.page.keyboard.press("Control+x"); await workspacePage.cut("keyboard");
await workspacePage.page.waitForTimeout(500); await expect(variant.container).not.toBeVisible();
//Select the board //Select the board
await workspacePage.clickLeafLayer("Board"); await workspacePage.clickLeafLayer("Board");
//Paste the variant container inside the 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 //Undo twice
await workspacePage.page.keyboard.press("Control+z"); await workspacePage.page.keyboard.press("ControlOrMeta+z");
await workspacePage.page.keyboard.press("Control+z");
await workspacePage.page.waitForTimeout(500); await expect(variant.container).not.toBeVisible();
await workspacePage.page.keyboard.press("ControlOrMeta+z");
await expect(variant.container).toBeVisible();
const variantAfterUndo = await findVariant(workspacePage, 0); const variantAfterUndo = await findVariant(workspacePage, 0);
@@ -276,12 +291,12 @@ test("User copy paste a variant", async ({ page }) => {
// Select the variant1 // Select the variant1
await variant.variant1.click(); await variant.variant1.click();
//Cut the variant // Copy the variant
await workspacePage.page.keyboard.press("Control+c"); await workspacePage.copy("keyboard");
//Paste the variant // Paste the variant
await workspacePage.clickAt(500, 500); await workspacePage.clickAt(500, 500);
await workspacePage.page.keyboard.press("Control+v"); await workspacePage.paste("keyboard");
const copy = await workspacePage.layers const copy = await workspacePage.layers
.getByTestId("layer-row") .getByTestId("layer-row")
@@ -302,11 +317,11 @@ test("User cut paste a variant outside the container", async ({ page }) => {
await variant.variant1.click(); await variant.variant1.click();
//Cut the variant //Cut the variant
await workspacePage.page.keyboard.press("Control+x"); await workspacePage.cut("keyboard");
//Paste the variant //Paste the variant
await workspacePage.clickAt(500, 500); await workspacePage.clickAt(500, 500);
await workspacePage.page.keyboard.press("Control+v"); await workspacePage.paste("keyboard");
const component = await workspacePage.layers const component = await workspacePage.layers
.getByTestId("layer-row") .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); const variant = await findVariant(workspacePage, 0);
// Drag and drop the variant // 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 await expect(workspacePage.layers.getByText("Rectangle / Value 1")).toBeVisible();
.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();
}); });
test("User cut paste a component inside a variant", async ({ page }) => { 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.ellipseShapeButton.click();
await workspacePage.clickWithDragViewportAt(500, 500, 20, 20); await workspacePage.clickWithDragViewportAt(500, 500, 20, 20);
await workspacePage.clickLeafLayer("Ellipse"); await workspacePage.clickLeafLayer("Ellipse");
await workspacePage.page.keyboard.press("Control+k"); await workspacePage.page.keyboard.press("ControlOrMeta+k");
//Cut the component //Cut the component
await workspacePage.page.keyboard.press("Control+x"); await workspacePage.cut("keyboard");
//Paste the component inside the variant //Paste the component inside the variant
await variant.container.click(); await variant.container.click();
await workspacePage.page.keyboard.press("Control+v"); await workspacePage.paste("keyboard");
const variant3 = await workspacePage.layers const variant3 = await workspacePage.layers
.getByTestId("layer-row") .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.ellipseShapeButton.click();
await workspacePage.clickWithDragViewportAt(500, 500, 20, 20); await workspacePage.clickWithDragViewportAt(500, 500, 20, 20);
await workspacePage.clickLeafLayer("Ellipse"); await workspacePage.clickLeafLayer("Ellipse");
await workspacePage.page.keyboard.press("Control+k"); await workspacePage.page.keyboard.press("ControlOrMeta+k");
//Rename the component //Rename the component
await workspacePage.layers.getByText("Ellipse").dblclick(); 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"); await workspacePage.page.keyboard.press("Enter");
//Cut the component //Cut the component
await workspacePage.page.keyboard.press("Control+x"); await workspacePage.cut("keyboard");
//Paste the component inside the variant //Paste the component inside the variant
await variant.container.click(); await variant.container.click();
await workspacePage.page.keyboard.press("Control+v"); await workspacePage.paste("keyboard");
const variant3 = await workspacePage.layers const variant3 = await workspacePage.layers
.getByTestId("layer-row") .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.ellipseShapeButton.click();
await workspacePage.clickWithDragViewportAt(500, 500, 20, 20); await workspacePage.clickWithDragViewportAt(500, 500, 20, 20);
await workspacePage.clickLeafLayer("Ellipse"); await workspacePage.clickLeafLayer("Ellipse");
await workspacePage.page.keyboard.press("Control+k"); await workspacePage.page.keyboard.press("ControlOrMeta+k");
//Rename the component //Rename the component
await workspacePage.layers.getByText("Ellipse").dblclick(); 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"); await workspacePage.page.keyboard.press("Enter");
//Drag and drop the component the component //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 const variant3 = await workspacePage.layers
.getByTestId("layer-row") .getByTestId("layer-row")
@@ -446,8 +457,8 @@ test("User cut paste a variant into another container", async ({ page }) => {
await workspacePage.ellipseShapeButton.click(); await workspacePage.ellipseShapeButton.click();
await workspacePage.clickWithDragViewportAt(500, 500, 20, 20); await workspacePage.clickWithDragViewportAt(500, 500, 20, 20);
await workspacePage.clickLeafLayer("Ellipse"); await workspacePage.clickLeafLayer("Ellipse");
await workspacePage.page.keyboard.press("Control+k"); await workspacePage.page.keyboard.press("ControlOrMeta+k");
await workspacePage.page.keyboard.press("Control+k"); await workspacePage.page.keyboard.press("ControlOrMeta+k");
const variantOrigin = await findVariantNoWait(workspacePage, 1); 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(); await variantOrigin.variant1.click();
//Cut the variant //Cut the variant
await workspacePage.page.keyboard.press("Control+x"); await workspacePage.cut("keyboard");
//Paste the variant //Paste the variant
await workspacePage.layers.getByText("Ellipse").first().click(); await workspacePage.layers.getByText("Ellipse").first().click();
await workspacePage.page.keyboard.press("Control+v"); await workspacePage.paste("keyboard");
const variant3 = workspacePage.layers const variant3 = workspacePage.layers
.getByTestId("layer-row") .getByTestId("layer-row")

View File

@@ -1,13 +1,13 @@
import { test, expect } from "@playwright/test"; import { test, expect } from "@playwright/test";
import { WorkspacePage } from "../pages/WorkspacePage"; import { WasmWorkspacePage } from "../pages/WorkspacePage";
import { presenceFixture, joinFixture2, joinFixture3 } from "../../data/workspace/ws-notifications"; import { presenceFixture, joinFixture2, joinFixture3 } from "../../data/workspace/ws-notifications";
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
await WorkspacePage.init(page); await WasmWorkspacePage.init(page);
}); });
test("User loads worskpace with empty file", async ({ 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.setupEmptyFile(page);
await workspacePage.goToWorkspace(); 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 }) => { 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.setupEmptyFile(page);
await workspacePage.goToWorkspace({ 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 ({ test("User receives presence notifications updates in the workspace", async ({
page, page,
}) => { }) => {
const workspacePage = new WorkspacePage(page); const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile(); await workspacePage.setupEmptyFile();
await workspacePage.goToWorkspace(); await workspacePage.goToWorkspace();
@@ -63,7 +63,7 @@ test("BUG 13058 - Presence list shows up to 3 user avatars", async ({
}); });
test("User draws a rect", async ({ page }) => { test("User draws a rect", async ({ page }) => {
const workspacePage = new WorkspacePage(page); const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile(); await workspacePage.setupEmptyFile();
await workspacePage.mockRPC( await workspacePage.mockRPC(
"update-file?id=*", "update-file?id=*",
@@ -74,13 +74,12 @@ test("User draws a rect", async ({ page }) => {
await workspacePage.rectShapeButton.click(); await workspacePage.rectShapeButton.click();
await workspacePage.clickWithDragViewportAt(128, 128, 200, 100); await workspacePage.clickWithDragViewportAt(128, 128, 200, 100);
const shape = await workspacePage.rootShape.locator("rect"); await workspacePage.hideUI();
await expect(shape).toHaveAttribute("width", "200"); await expect(workspacePage.canvas).toHaveScreenshot();
await expect(shape).toHaveAttribute("height", "100");
}); });
test("User makes a group", async ({ page }) => { test("User makes a group", async ({ page }) => {
const workspacePage = new WorkspacePage(page); const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile(); await workspacePage.setupEmptyFile();
await workspacePage.mockRPC( await workspacePage.mockRPC(
/get\-file\?/, /get\-file\?/,
@@ -96,14 +95,14 @@ test("User makes a group", async ({ page }) => {
pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375", pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375",
}); });
await workspacePage.clickLeafLayer("Rectangle"); await workspacePage.clickLeafLayer("Rectangle");
await workspacePage.page.keyboard.press("Control+g"); await workspacePage.page.keyboard.press("ControlOrMeta+g");
await workspacePage.expectSelectedLayer("Group"); await workspacePage.expectSelectedLayer("Group");
}); });
test("Bug 7654 - Toolbar keeps toggling on and off on spacebar press", async ({ test("Bug 7654 - Toolbar keeps toggling on and off on spacebar press", async ({
page, page,
}) => { }) => {
const workspacePage = new WorkspacePage(page); const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile(); await workspacePage.setupEmptyFile();
await workspacePage.goToWorkspace(); await workspacePage.goToWorkspace();
@@ -113,10 +112,10 @@ test("Bug 7654 - Toolbar keeps toggling on and off on spacebar press", async ({
await workspacePage.expectHiddenToolbarOptions(); 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, page,
}) => { }) => {
const workspacePage = new WorkspacePage(page); const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile(); await workspacePage.setupEmptyFile();
await workspacePage.mockRPC( await workspacePage.mockRPC(
/get\-file\?/, /get\-file\?/,
@@ -132,8 +131,8 @@ test("Bug 7525 - User moves a scrollbar and no selciont rectangle appears", asyn
pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375", pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375",
}); });
// Move created rect to a corner, in orther to get scrollbars // Move created rect to a corner, in order to get scrollbars
await workspacePage.panOnViewportAt(128, 128, 300, 300); await workspacePage.panOnViewportAt(128, 128, 600, 600);
// Check scrollbars appear // Check scrollbars appear
const horizontalScrollbar = workspacePage.horizontalScrollbar; const horizontalScrollbar = workspacePage.horizontalScrollbar;
@@ -152,7 +151,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 ({ test("User adds a library and its automatically selected in the color palette", async ({
page, page,
}) => { }) => {
const workspacePage = new WorkspacePage(page); const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile(); await workspacePage.setupEmptyFile();
await workspacePage.mockRPC( await workspacePage.mockRPC(
"link-file-to-library", "link-file-to-library",
@@ -197,7 +196,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 ({ test("Bug 10179 - Drag & drop doesn't add colors to the Recent Colors palette", async ({
page, page,
}) => { }) => {
const workspacePage = new WorkspacePage(page); const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile(); await workspacePage.setupEmptyFile();
await workspacePage.goToWorkspace(); await workspacePage.goToWorkspace();
await workspacePage.moveButton.click(); await workspacePage.moveButton.click();
@@ -240,7 +239,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 ({ test("Bug 7489 - Workspace-palette items stay hidden when opening with keyboard-shortcut", async ({
page, page,
}) => { }) => {
const workspacePage = new WorkspacePage(page); const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile(); await workspacePage.setupEmptyFile();
await workspacePage.goToWorkspace(); await workspacePage.goToWorkspace();
@@ -257,7 +256,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 ({ test("Bug 8784 - Use keyboard arrow to move inside a text input does not change tabs", async ({
page, page,
}) => { }) => {
const workspacePage = new WorkspacePage(page); const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile(); await workspacePage.setupEmptyFile();
await workspacePage.goToWorkspace(); await workspacePage.goToWorkspace();
await workspacePage.pageName.click(); await workspacePage.pageName.click();
@@ -267,7 +266,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 }) => { 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.setupEmptyFile(page);
await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-9066.json"); await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-9066.json");
@@ -295,7 +294,7 @@ test("Bug 9066 - Problem with grid layout", async ({ page }) => {
}); });
test("User have toolbar", async ({ page }) => { test("User have toolbar", async ({ page }) => {
const workspacePage = new WorkspacePage(page); const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile(page); await workspacePage.setupEmptyFile(page);
await workspacePage.goToWorkspace(); await workspacePage.goToWorkspace();
@@ -304,7 +303,7 @@ test("User have toolbar", async ({ page }) => {
}); });
test("User have edition menu entries", 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.setupEmptyFile(page);
await workspacePage.goToWorkspace(); await workspacePage.goToWorkspace();
@@ -320,7 +319,7 @@ test("User have edition menu entries", async ({ page }) => {
}); });
test("Copy/paste properties", async ({ page, context }) => { test("Copy/paste properties", async ({ page, context }) => {
const workspacePage = new WorkspacePage(page); const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile(page); await workspacePage.setupEmptyFile(page);
await workspacePage.mockRPC( await workspacePage.mockRPC(
/get\-file\?/, /get\-file\?/,
@@ -386,23 +385,23 @@ test("Copy/paste properties", async ({ page, context }) => {
}); });
test("[Taiga #9929] Paste text in workspace", 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.setupEmptyFile(page);
await workspacePage.goToWorkspace(); await workspacePage.goToWorkspace();
await context.grantPermissions(["clipboard-read", "clipboard-write"]); await context.grantPermissions(["clipboard-read", "clipboard-write"]);
await page.evaluate(() => navigator.clipboard.writeText("Lorem ipsum dolor")); await page.evaluate(() => navigator.clipboard.writeText("Lorem ipsum dolor"));
await workspacePage.viewport.click({ button: "right" }); await workspacePage.viewport.click({ button: "right" });
await page.getByText("PasteCtrlV").click(); await page.getByText(/^Paste/i).click();
await workspacePage.viewport await workspacePage.viewport
.getByRole("textbox") .getByRole("textbox")
.getByText("Lorem ipsum dolor"); .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, page,
context, context,
}) => { }) => {
const workspacePage = new WorkspacePage(page); const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile(page); await workspacePage.setupEmptyFile(page);
await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-9930.json"); await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-9930.json");
await workspacePage.goToWorkspace({ await workspacePage.goToWorkspace({
@@ -410,16 +409,18 @@ test("[Taiga #9930] Zoom fit all doesn't fits all", async ({
pageId: "fb9798e7-a547-80ae-8005-9ffda4a13e2c", pageId: "fb9798e7-a547-80ae-8005-9ffda4a13e2c",
}); });
const zoom = await page.getByTitle("Zoom"); const zoom = page.getByTitle("Zoom");
await zoom.click(); 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(); await zoomIn.click();
await zoomIn.click(); await zoomIn.click();
// Zoom fit all // Zoom fit all
await page.keyboard.press("Shift+1"); await page.keyboard.press("Shift+1");
// Select all shapes to display selrect
await workspacePage.page.keyboard.press("ControlOrMeta+a");
const ids = [ const ids = [
"shape-165d1e5a-5873-8010-8005-9ffdbeaeec59", "shape-165d1e5a-5873-8010-8005-9ffdbeaeec59",
@@ -441,7 +442,7 @@ test("[Taiga #9930] Zoom fit all doesn't fits all", async ({
const viewportBoundingBox = await workspacePage.viewport.boundingBox(); const viewportBoundingBox = await workspacePage.viewport.boundingBox();
for (const id of ids) { 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(); const shapeBoundingBox = await shape.boundingBox();
expect(contains(viewportBoundingBox, shapeBoundingBox)).toBeTruthy(); expect(contains(viewportBoundingBox, shapeBoundingBox)).toBeTruthy();
} }
@@ -450,7 +451,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 ({ test("Bug 9877, user navigation to dashboard from header goes to blank page", async ({
page, page,
}) => { }) => {
const workspacePage = new WorkspacePage(page); const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile(page); await workspacePage.setupEmptyFile(page);
await workspacePage.goToWorkspace(); await workspacePage.goToWorkspace();
@@ -467,7 +468,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 ({ test("Bug 8371 - Flatten option is not visible in context menu", async ({
page, page,
}) => { }) => {
const workspacePage = new WorkspacePage(page); const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile(page); await workspacePage.setupEmptyFile(page);
await workspacePage.mockGetFile("workspace/get-file-8371.json"); await workspacePage.mockGetFile("workspace/get-file-8371.json");
await workspacePage.goToWorkspace({ await workspacePage.goToWorkspace({

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,90 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<title>WASM + WebGL2 Canvas</title>
<style>
body {
margin: 0;
background: #111;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
overflow: hidden;
}
canvas {
width: 100%;
height: 100%;
position: absolute;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script type="module">
import initWasmModule from '/js/render-wasm.js';
import {
init, addShapeSolidFill, assignCanvas, hexToU32ARGB, getRandomInt, getRandomColor,
getRandomFloat, useShape, setShapeChildren, setupInteraction, addShapeSolidStrokeFill
} from './js/lib.js';
const canvas = document.getElementById("canvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const params = new URLSearchParams(document.location.search);
const shapes = params.get("shapes") || 1000;
initWasmModule().then(Module => {
init(Module);
assignCanvas(canvas);
Module._set_canvas_background(hexToU32ARGB("#FABADA", 1));
Module._init_shapes_pool(shapes + 1);
setupInteraction(canvas);
const children = [];
for (let i = 0; i < shapes; i++) {
const uuid = crypto.randomUUID();
children.push(uuid);
useShape(uuid);
Module._set_parent(0, 0, 0, 0);
Module._set_shape_type(3);
const x1 = getRandomInt(0, canvas.width);
const y1 = getRandomInt(0, canvas.height);
const width = getRandomInt(20, 100);
const height = getRandomInt(20, 100);
Module._set_shape_selrect(x1, y1, x1 + width, y1 + height);
const color = getRandomColor();
const argb = hexToU32ARGB(color, getRandomFloat(0.1, 1.0));
addShapeSolidFill(argb)
Module._add_shape_center_stroke(10, 0, 0, 0);
const argb2 = hexToU32ARGB(color, getRandomFloat(0.1, 1.0));
addShapeSolidStrokeFill(argb2);
// Add shadows
// Shadow 1: drop-shadow, #dedede opacity 0.33, blur 4, spread -2, offsetX 0, offsetY 2
Module._add_shape_shadow(hexToU32ARGB("#dedede", 0.33), 4, -2, 0, 2, 0, false);
// Shadow 2: drop-shadow, #dedede opacity 1, blur 12, spread -8, offsetX 0, offsetY 12
Module._add_shape_shadow(hexToU32ARGB("#dedede", 1), 12, -8, 0, 12, 0, false);
// Shadow 3: inner-shadow, #002046 opacity 0.12, blur 12, spread -8, offsetX 0, offsetY -4
Module._add_shape_shadow(hexToU32ARGB("#002046", 0.12), 12, -8, 0, -4, 1, false);
}
useShape("00000000-0000-0000-0000-000000000000");
setShapeChildren(children);
performance.mark('render:begin');
Module._set_view(1, 0, 0);
Module._render(Date.now());
performance.mark('render:end');
const { duration } = performance.measure('render', 'render:begin', 'render:end');
// alert(`render time: ${duration.toFixed(2)}ms`);
});
</script>
</body>
</html>

View File

@@ -21,7 +21,7 @@
(def ^:private schema:properties-row (def ^:private schema:properties-row
[:map [:map
[:term :string] [:term :string]
[:detail :string] [:detail {:optional true} [:maybe :string]]
[:property {:optional true} :string] ;; CSS valid property [:property {:optional true} :string] ;; CSS valid property
[:token {:optional true} :any] ;; resolved token object [:token {:optional true} :any] ;; resolved token object
[:copiable {:optional true} :boolean]]) [:copiable {:optional true} :boolean]])

View File

@@ -78,13 +78,15 @@
(fn [] (fn []
(close-modals) (close-modals)
;; FIXME: move set-mode to uri? ;; 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)))) (dcm/go-to-dashboard-recent))))
nav-to-project nav-to-project
(mf/use-fn (mf/use-fn
(mf/deps project-id) (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?] (mf/with-effect [editing?]
(when ^boolean editing? (when ^boolean editing?

View File

@@ -401,7 +401,8 @@
(dm/fmt "scale(%)" maybe-zoom))}))] (dm/fmt "scale(%)" maybe-zoom))}))]
[:g.text-editor {:clip-path (dm/fmt "url(#%)" clip-id) [:g.text-editor {:clip-path (dm/fmt "url(#%)" clip-id)
:transform (dm/str transform)} :transform (dm/str transform)
:data-testid "text-editor"}
[:defs [:defs
[:clipPath {:id clip-id} [:clipPath {:id clip-id}
[:rect {:x x :y y :width width :height height}]]] [:rect {:x x :y y :width width :height height}]]]

View File

@@ -119,7 +119,8 @@
[:button {:class (stl/css-case [:button {:class (stl/css-case
:toggle-content true :toggle-content true
:inverse expanded?) :inverse expanded?)
:aria-label "Toggle layer" :data-testid "toggle-content"
:aria-expanded expanded?
:on-click on-toggle-collapse} :on-click on-toggle-collapse}
deprecated-icon/arrow]) deprecated-icon/arrow])

View File

@@ -108,6 +108,7 @@
:on-blur accept-edit :on-blur accept-edit
:on-key-down on-key-down :on-key-down on-key-down
:auto-focus true :auto-focus true
:id (dm/str "layer-name-" shape-id)
:default-value (d/nilv default-value "")}] :default-value (d/nilv default-value "")}]
[:* [:*
[:span [:span
@@ -118,6 +119,7 @@
:hidden is-hidden :hidden is-hidden
:type-comp type-comp :type-comp type-comp
:type-frame type-frame) :type-frame type-frame)
:id (dm/str "layer-name-" shape-id)
:style {"--depth" depth "--parent-size" parent-size} :style {"--depth" depth "--parent-size" parent-size}
:ref ref :ref ref
:on-double-click start-edit} :on-double-click start-edit}

View File

@@ -22,7 +22,7 @@ pub use surfaces::{SurfaceId, Surfaces};
use crate::performance; use crate::performance;
use crate::shapes::{ 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::state::{ShapesPoolMutRef, ShapesPoolRef};
use crate::tiles::{self, PendingTiles, TileRect}; use crate::tiles::{self, PendingTiles, TileRect};
@@ -1512,6 +1512,16 @@ impl RenderState {
Self::combine_blur_values(self.combined_layer_blur(shape.blur), extra_layer_blur); Self::combine_blur_values(self.combined_layer_blur(shape.blur), extra_layer_blur);
let blur_filter = combined_blur let blur_filter = combined_blur
.and_then(|blur| skia::image_filters::blur((blur.value, blur.value), None, None, None)); .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 mut transform_matrix = shape.transform;
let center = shape.center(); let center = shape.center();
@@ -1526,28 +1536,20 @@ impl RenderState {
let world_offset = (mapped.x, mapped.y); let world_offset = (mapped.x, mapped.y);
// The opacity of fills and strokes shouldn't affect the shadow, // The opacity of fills and strokes shouldn't affect the shadow,
// so we paint everything black with the same opacity // so we paint everything black with the same opacity.
plain_shape.to_mut().clear_fills(); let plain_shape_mut = plain_shape.to_mut();
plain_shape_mut.clear_fills();
if shape.has_fills() { if shape.has_fills() {
plain_shape plain_shape_mut.add_fill(Fill::Solid(SolidColor(skia::Color::BLACK)));
.to_mut()
.add_fill(Fill::Solid(SolidColor(skia::Color::BLACK)));
} }
plain_shape.to_mut().clear_strokes(); // Reuse existing strokes and only override their fill color.
for stroke in shape.strokes.iter() { for stroke in plain_shape_mut.strokes.iter_mut() {
plain_shape.to_mut().add_stroke(Stroke { stroke.fill = Fill::Solid(SolidColor(skia::Color::BLACK));
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,
});
} }
plain_shape.to_mut().clear_shadows(); plain_shape_mut.clear_shadows();
plain_shape.to_mut().blur = None; plain_shape_mut.blur = None;
let Some(drop_filter) = transformed_shadow.get_drop_shadow_filter() else { let Some(drop_filter) = transformed_shadow.get_drop_shadow_filter() else {
return; return;
@@ -1556,6 +1558,39 @@ impl RenderState {
let mut bounds = drop_filter.compute_fast_bounds(shape_bounds); let mut bounds = drop_filter.compute_fast_bounds(shape_bounds);
// Account for the shadow offset so the temporary surface fully contains the shifted blur. // Account for the shadow offset so the temporary surface fully contains the shifted blur.
bounds.offset(world_offset); 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 = let filter_result =
filters::render_into_filter_surface(self, bounds, |state, temp_surface| { filters::render_into_filter_surface(self, bounds, |state, temp_surface| {