mirror of
https://github.com/penpot/penpot.git
synced 2026-08-01 19:06:18 +00:00
Combined fixes from PR #10656 (numeric-input redesign) and PR #10696 (global finite? guard) for issue #10638. - Add (number? v) guard to cljs mth/finite? so strings are rejected - Redesign numeric-input last-value* to store number, not formatted string - Invalid-input fallback restores display without emitting on-change - Esc now fully discards typed text (resets raw-value* + dirty flag) - Token dedup by name instead of resolved value - Defense-in-depth: d/parse-double at 4 padding/gap handlers - Math tests (common), unit tests, Storybook play tests, Playwright E2E AI-assisted-by: deepseek-v4-flash Co-authored-by: Akshit Nassa <nassaakshit@gmail.com> Co-authored-by: Ulises Millán <ulises.millanguerrero@gmail.com>
233 lines
7.2 KiB
JavaScript
233 lines
7.2 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([]);
|
|
});
|