Compare commits

...

17 Commits

Author SHA1 Message Date
Alejandro Alonso
86cb6b6f51 WIP 2026-02-04 14:16:26 +01:00
Alejandro Alonso
f3e0c2409e WIP 2026-02-04 14:11:12 +01:00
Alejandro Alonso
8584e070f9 WIP 2026-02-04 14:06:03 +01:00
Alejandro Alonso
49ea9a30a9 🐛 Fix pdf export 2026-02-04 13:58:03 +01:00
Alejandro Alonso
24c8fc484f 🐛 Fix Internal Error when adding a new text layer and trying to go back to Dashboard without saving 2026-02-04 10:01:10 +01:00
Alejandro Alonso
bc16b8ddc3 Merge pull request #8198 from penpot/ladybenko-13176-playwright-wasm
🔧 Migrate workspace tests to user the wasm viewport
2026-02-03 13:00:10 +01:00
Alejandro Alonso
b07c98faa5 Merge pull request #8259 from penpot/superalex-improve-shadow-rendering
🎉 Improving shadow rendering performance
2026-02-03 12:59:49 +01:00
Alejandro Alonso
25aff100cf 🎉 Add shadows playground for render wasm 2026-02-03 12:44:43 +01:00
Alejandro Alonso
5be887f10b 🎉 Improve plain shape calculation 2026-02-03 12:44:43 +01:00
Alejandro Alonso
f7403935c8 🎉 Improve shadows rendering performance 2026-02-03 12:33:05 +01:00
Belén Albeza
79be3ab7df 🔧 Fix text editor flaky tests 2026-02-03 10:39:38 +01:00
Belén Albeza
629649aca6 🔧 Fix config playwright syntax 2026-02-02 16:25:16 +01:00
Belén Albeza
cc326f23cf 🔧 Adjust timeout of websocket readiness (playwright) 2026-02-02 16:16:59 +01:00
Belén Albeza
2c4efc6b53 🔧 Fix onboarding test 2026-02-02 16:16:58 +01:00
Belén Albeza
4d5c874b91 🔧 Fix typography token test 2026-02-02 16:16:58 +01:00
Belén Albeza
e3b97638b4 🔧 Fix broken / flaky tests 2026-02-02 16:16:58 +01:00
Belén Albeza
daedc660b9 🔧 Migrate workspace tests to user the wasm viewport 2026-02-02 16:16:58 +01:00
17 changed files with 338 additions and 129 deletions

View File

@@ -38,6 +38,26 @@
(assoc :path "/render.html")
(assoc :query (u/map->query-string params)))))
(sync-page-size! [dom]
(bw/eval! dom
(fn [elem]
(let [rect (.getBoundingClientRect ^js elem)
width (js/Math.max (or (some-> (.getAttribute ^js elem "width") js/parseFloat) 0)
(.-width rect))
height (js/Math.max (or (some-> (.getAttribute ^js elem "height") js/parseFloat) 0)
(.-height rect))
width-px (str width "px")
height-px (str height "px")
style-node (or (.getElementById js/document "penpot-pdf-page-size")
(let [node (.createElement js/document "style")]
(set! (.-id node) "penpot-pdf-page-size")
(.appendChild (.-head js/document) node)
node))]
(set! (.-textContent style-node)
(str "@page { size: " width-px " " height-px "; margin: 0; }\n"
"html, body, #app { margin: 0; padding: 0;"
"width: " width-px "; height: " height-px "; }"))))))
(render-object [page base-uri {:keys [id] :as object}]
(p/let [uri (prepare-uri base-uri id)
path (sh/tempfile :prefix "penpot.tmp.pdf." :suffix (mime/get-extension type))]
@@ -45,6 +65,7 @@
(bw/nav! page uri)
(p/let [dom (bw/select page (dm/str "#screenshot-" id))]
(bw/wait-for dom)
(sync-page-size! dom)
(bw/screenshot dom {:full-page? true})
(bw/sleep page 2000) ; the good old fix with sleep
(bw/pdf page {:path path})

View File

@@ -1,4 +1,5 @@
import { defineConfig, devices } from "@playwright/test";
import { platform } from "os";
/**
* Read environment variables from file.
@@ -6,6 +7,10 @@ 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" :
undefined;
/**
* @see https://playwright.dev/docs/test-configuration
*/
@@ -43,12 +48,20 @@ 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',
}
userAgent,
},
snapshotPathTemplate: "{testDir}/{testFilePath}-snapshots/{arg}.png",
expect: {
toHaveScreenshot: {
maxDiffPixelRatio: 0.001,
},
},
},
{
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() {
// 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) {
@@ -383,19 +383,46 @@ 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);
}
}
/**
* 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>}
*/
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 });
}
/**
@@ -407,9 +434,9 @@ 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("Control+V");
return this.page.keyboard.press("ControlOrMeta+V");
}
async panOnViewportAt(x, y, width, height) {
@@ -448,11 +475,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) {
@@ -495,13 +522,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);
}

View File

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

View File

@@ -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);

View File

@@ -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"]);

View File

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

View File

@@ -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();
@@ -91,10 +90,10 @@ 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 WorkspacePage(page);
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockRPC(
/get\-file\?/,
@@ -110,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;
@@ -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({

View File

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
[: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]])

View File

@@ -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?

View File

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

View File

@@ -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])

View File

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

View File

@@ -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};
@@ -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();
@@ -1526,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;
@@ -1556,6 +1558,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| {