penpot/frontend/playwright/ui/specs/numeric-input.spec.js
Luis de Dios d800b893bc
🐛 Fix resize cursor when selection has mixed values (#12229)
* 🐛 Fix resize cursor when selection has mixed values

* 📎 PR changes

* ✨ Add test
2026-10-13 18:47:45 +02:00

284 lines
8.9 KiB
JavaScript

import { test, expect } from "@playwright/test";
import { WasmWorkspacePage } from "../pages/WasmWorkspacePage";
test.beforeEach(async ({ page }) => {
await WasmWorkspacePage.init(page);
await WasmWorkspacePage.mockConfigFlags(page, ["enable-feature-token-input"]);
});
test("BUG 14226: Numeric inputs in the design panel reject values with leading whitespace", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile(page);
await workspacePage.mockRPC(
/get\-file\?/,
"workspace/get-file-copy-paste.json",
);
await workspacePage.mockRPC(
"get-file-fragment?file-id=*&fragment-id=*",
"workspace/get-file-copy-paste-fragment.json",
);
await workspacePage.goToWorkspace({
fileId: "870f9f10-87b5-8137-8005-934804124660",
pageId: "870f9f10-87b5-8137-8005-934804124661",
});
// Select first shape
await page.getByTestId("layer-item").getByRole("button").first().click();
await workspacePage.layers.getByTestId("layer-row").nth(0).click();
// Check if measures section is visible
const measuresSection = workspacePage.rightSidebar.getByRole("region", {
name: "shape-measures-section",
});
await expect(measuresSection).toBeVisible();
// Width
const widthInput = measuresSection.getByRole("textbox", {
name: "Width",
exact: true,
});
await expect(widthInput).toHaveValue("360");
await widthInput.fill("100");
await widthInput.press("Enter");
await expect(widthInput).toHaveValue("100");
await widthInput.fill(" 100");
await widthInput.press("Enter");
await expect(widthInput).toHaveValue("100");
await widthInput.fill(" 100 ");
await widthInput.press("Enter");
await expect(widthInput).toHaveValue("100");
await widthInput.fill("100 ");
await widthInput.press("Enter");
await expect(widthInput).toHaveValue("100");
await widthInput.fill("98+2");
await widthInput.press("Enter");
await expect(widthInput).toHaveValue("100");
await widthInput.fill("98 + 2");
await widthInput.press("Enter");
await expect(widthInput).toHaveValue("100");
await widthInput.fill(" 98 + 2 ");
await widthInput.press("Enter");
await expect(widthInput).toHaveValue("100");
await widthInput.fill(" 98+2 ");
await widthInput.press("Enter");
await expect(widthInput).toHaveValue("100");
await widthInput.fill(" asdasdasdasd ");
await widthInput.press("Enter");
await expect(widthInput).toHaveValue("100");
});
test("BUG 10001: Negative margins are allowed on the numeric input", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile(page);
await workspacePage.mockRPC(
/get\-file\?/,
"workspace/get-file-copy-paste.json",
);
await workspacePage.mockRPC(
"get-file-fragment?file-id=*&fragment-id=*",
"workspace/get-file-copy-paste-fragment.json",
);
await workspacePage.goToWorkspace({
fileId: "870f9f10-87b5-8137-8005-934804124660",
pageId: "870f9f10-87b5-8137-8005-934804124661",
});
// Select first shape
await page.getByTestId("layer-item").getByRole("button").first().click();
await workspacePage.layers.getByTestId("layer-row").nth(5).click();
const addLayout = workspacePage.rightSidebar.getByRole("button", {
name: "Add layout",
});
await addLayout.click();
const flexLayout = workspacePage.rightSidebar.getByRole("button", {
name: "Flex layout",
});
await flexLayout.click();
const layoutSection = workspacePage.rightSidebar.getByRole("region", {
name: "Layout container section",
});
await expect(layoutSection).toBeVisible();
await workspacePage.layers.getByTestId("layer-row").nth(6).click();
await page.waitForTimeout(500);
const layoutItemSection = workspacePage.rightSidebar.getByRole("region", {
name: "Layout item section",
});
await expect(layoutItemSection).toBeVisible();
const verticalMarginInput = layoutItemSection.getByRole("textbox", {
name: "Vertical margin",
});
await expect(verticalMarginInput).toBeVisible();
await verticalMarginInput.fill("-10");
await verticalMarginInput.press("Enter");
await expect(verticalMarginInput).toHaveValue("-10");
});
test("BUG 10638 - Invalid padding input on multi-selection must not persist strings", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockRPC(/get\-file\?/, "design/get-file-9543.json");
await workspacePage.mockRPC(
"get-file-fragment?file-id=*&fragment-id=*",
"design/get-file-fragment-10638.json",
);
await workspacePage.mockRPC(
"update-file?id=*",
"design/update-file-9543.json",
);
// Inspect every persisted change: layout padding and gap values must be
// numbers (the backend rejects strings with a Malli validation error).
// In the transit payload, :set operations carry the attr name and its
// value as siblings: {"~:attr": "~:layout-padding", "~:val": {...}}.
const layoutAttrs = ["~:layout-padding", "~:layout-gap"];
const seenLayoutValues = [];
const badLayoutValues = [];
const collectLayoutValues = (node) => {
if (!node || typeof node !== "object") {
return;
}
const attr = node["~:attr"];
const val = node["~:val"];
if (layoutAttrs.includes(attr) && val && typeof val === "object") {
for (const [prop, leaf] of Object.entries(val)) {
seenLayoutValues.push(`${attr} ${prop}`);
if (typeof leaf !== "number") {
badLayoutValues.push(`${attr} ${prop} = ${JSON.stringify(leaf)}`);
}
}
}
for (const value of Object.values(node)) {
collectLayoutValues(value);
}
};
page.on("request", (request) => {
if (request.url().includes("/api/main/methods/update-file")) {
const body = request.postData();
if (body) {
collectLayoutValues(JSON.parse(body));
}
}
});
await workspacePage.goToWorkspace({
fileId: "525a5d8b-028e-80e7-8005-aa6cad42f27d",
pageId: "525a5d8b-028e-80e7-8005-aa6cad42f27e",
});
await workspacePage.clickLeafLayer("Board");
await workspacePage.clickLeafLayer("Second", { modifiers: ["Shift"] });
const toggle = workspacePage.page.getByRole("button", {
name: "Show 4 sided padding options",
});
await toggle.click();
// Paddings differ between the boards (0 vs 33), so the expanded inputs
// have no committed value. Committing invalid text must persist nothing.
const topPaddingInput = workspacePage.page.getByRole("textbox", {
name: "Top padding",
});
await topPaddingInput.click();
await topPaddingInput.fill("abc");
await topPaddingInput.press("Enter");
// A valid change afterwards guarantees at least one persisted update.
const leftPaddingInput = workspacePage.page.getByRole("textbox", {
name: "Left padding",
});
const updateRequest = page.waitForRequest(
"**/api/main/methods/update-file?*",
{
timeout: 15000,
},
);
await leftPaddingInput.fill("5");
await leftPaddingInput.press("Enter");
await updateRequest;
// Guard against a vacuous pass: the valid change above must have
// persisted at least one layout-padding value.
expect(seenLayoutValues).not.toEqual([]);
expect(badLayoutValues).toEqual([]);
});
test("BUG 12227: Numeric inputs only show the scrub cursor when they hold a single value", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile(page);
await workspacePage.mockRPC(
/get\-file\?/,
"workspace/get-file-copy-paste.json",
);
await workspacePage.mockRPC(
"get-file-fragment?file-id=*&fragment-id=*",
"workspace/get-file-copy-paste-fragment.json",
);
await workspacePage.goToWorkspace({
fileId: "870f9f10-87b5-8137-8005-934804124660",
pageId: "870f9f10-87b5-8137-8005-934804124661",
});
await page.getByTestId("layer-item").getByRole("button").first().click();
await workspacePage.layers.getByTestId("layer-row").nth(0).click();
const measuresSection = workspacePage.rightSidebar.getByRole("region", {
name: "shape-measures-section",
});
await expect(measuresSection).toBeVisible();
const widthInput = measuresSection.getByRole("textbox", {
name: "Width",
exact: true,
});
await expect(widthInput).toHaveValue("360");
// The wrapper around the input is the element that carries the cursor.
const widthWrapper = measuresSection
.getByTestId("numeric-input-wrapper")
.filter({ has: page.getByRole("textbox", { name: "Width", exact: true }) });
// A single committed value can be scrubbed.
await expect(widthWrapper).toHaveCSS("cursor", "ew-resize");
// Selecting a second shape with a different width mixes the values.
// The field then has no single value, so it must not look scrubbable.
await workspacePage.layers
.getByTestId("layer-row")
.nth(1)
.click({ modifiers: ["Shift"] });
await expect(widthInput).toHaveAttribute("placeholder", "Mixed");
await expect(widthWrapper).not.toHaveCSS("cursor", "ew-resize");
});