penpot/frontend/playwright/ui/specs/workspace.spec.js
Dr. Dominik Jain e20a0d748e
✨ Add OAuth support for MCP server (#12480)
* 🎉 Add OAuth support for remote MCP connections

Add MCP-specific discovery, client registration, and approval endpoints.
Exchange single-use authorization codes with PKCE for the fixed MCP user
token, enabling MCP and creating the token on approval when needed.

Use self-contained client IDs without new database tables or stored client
records. Keep temporary authorization state in Redis/Valkey.

Accept bearer credentials while preserving query-parameter connections.
Enable OAuth through PENPOT_MCP_AUTH_URI in multi-user mode. Document the
environment variable in the README and implementation details in Serena.

Explain protocol and concurrency decisions in the Clojure code. Cover token
provisioning, OAuth state, client registration, HTTP flows, and the approval
page with tests.

AI-assisted-by: gpt-6

* 🐛 Wait for devenv initialization before starting builds

Docker reports a container as Running as soon as its main process starts,
while the entrypoint may still be preparing the development environment.
manage.sh used that status to launch tmux jobs, allowing frontend builds
to race with ownership changes to /opt/rustup and Cargo setup.

On a fresh container, rustup could fail with Permission denied before the
frontend watcher started. Nginx would later serve the existing JavaScript,
leaving source changes absent from the running app despite restarting
devenv.

Wait for nginx's local health endpoint before launching the jobs. Nginx
starts after entrypoint setup, so its response confirms that setup has
finished. Allow up to 120 seconds and report a timeout without starting
the jobs if initialization does not complete.

Validation: shell syntax and whitespace checks pass.

AI-assisted-by: gpt-6

* ♻️ Share the login modal with MCP authorization

Extract the viewer login modal and its styles into a shared auth
component, retaining the viewer wrapper and its close behavior.

Use the modal on the MCP approval page and preserve the pending request
through login. Closing it clears the saved return URL and shows a
cancellation message without further actions.

Cover return URL handling and cancellation with frontend tests.

AI-assisted-by: gpt-6

* ✨ Show client icons in MCP authorization

Preserve HTTPS logo URLs from client registration and published metadata
and show the icon beside the application name on the approval page.
Ignore unusable URLs and hide failed images without blocking approval.

Update the access description to refer to MCP connections established
from Penpot. Cover logo metadata and approval responses in backend tests.

AI-assisted-by: gpt-6

* 🐛 Follow the system theme when no preference is configured

Default to the system theme in the shared resolver used by the app and
plugins. Preserve explicit light and dark choices, including the legacy
default value that means dark.

Reuse the reactive theme hook in progress widgets and show the system
preference in settings for accounts without a configured theme.

Verify preference resolution with regression tests and browser checks.
All 1,474 frontend tests pass.

AI-assisted-by: gpt-6

* 🐛 Replace expired MCP tokens during OAuth approval

Settings can give an MCP token an expiry date. OAuth approval reused
the newest MCP token without checking it, so an expired token reached
the client and never matched a plugin connection.

Approval now treats an expired token as missing and creates a new one.
Token exchange rejects a code whose token expired after approval with
invalid_grant.

AI-assisted-by: claude-opus-5-5

* 🐛 Log unexpected errors on MCP OAuth endpoints

The OAuth routes sit outside the shared HTTP middleware, so the shared
error handler never saw them, and their own handler turned unexpected
failures into a 503 without logging anything. Failures in the session
middleware of the approval endpoints had no handler at all.

Unexpected failures now also go through the shared error handler for
its logging, while clients still get an OAuth error body. The OAuth
error handler also wraps the session middleware of the approval
endpoints.

AI-assisted-by: claude-opus-5-5

* 🔧 Add devenv script to run the MCP server with OAuth

start-mcp-devenv-oauth starts the MCP server in multi-user mode with
PENPOT_MCP_AUTH_URI derived from PENPOT_PUBLIC_URI, to test OAuth in
the devenv. Agentic tooling still uses the single-user
start-mcp-devenv.

AI-assisted-by: claude-opus-5-5

* 🐛 Fix failing test case

---------

Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-10-28 18:40:04 +01:00

735 lines
24 KiB
JavaScript

import { test, expect } from "@playwright/test";
import { WasmWorkspacePage } from "../pages/WasmWorkspacePage";
import { presenceFixture, joinFixture2, joinFixture3 } from "../../data/workspace/ws-notifications";
test.beforeEach(async ({ page }) => {
await WasmWorkspacePage.init(page);
});
test("User loads worskpace with empty file", async ({ page }) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile(page);
await workspacePage.goToWorkspace();
await expect(workspacePage.pageName).toHaveText("Page 1");
});
test("User opens a file with a bad page id", async ({ page }) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile(page);
await workspacePage.goToWorkspace({
pageId: "badpage",
});
await expect(workspacePage.pageName).toHaveText("Page 1");
});
test("User receives presence notifications updates in the workspace", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.goToWorkspace();
await workspacePage.sendPresenceMessage(presenceFixture);
await expect(
page.getByTestId("active-users-list").getByAltText("Princesa Leia"),
).toHaveCount(2);
});
test("BUG 13058 - Presence list shows up to 3 user avatars", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.goToWorkspace();
await workspacePage.sendPresenceMessage(presenceFixture);
await workspacePage.sendPresenceMessage(joinFixture2);
await expect(
page.getByTestId("active-users-list").getByAltText("Princesa Leia"),
).toHaveCount(3);
await workspacePage.sendPresenceMessage(joinFixture3);
await expect(
page.getByTestId("active-users-list").getByAltText("Princesa Leia"),
).toHaveCount(2);
await expect(page.getByTestId("active-users-list").getByText("+2")).toBeVisible();
});
test("User draws a rect", async ({ page }) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockRPC(
"update-file?id=*",
"workspace/update-file-create-rect.json",
);
await workspacePage.goToWorkspace();
await workspacePage.rectShapeButton.click();
await workspacePage.clickWithDragViewportAt(128, 128, 200, 100);
await workspacePage.hideUI();
await expect(workspacePage.canvas).toHaveScreenshot();
});
test("Selection size badge appears on selection and hides on deselect", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockRPC(
/get\-file\?/,
"workspace/get-file-not-empty.json",
);
await workspacePage.goToWorkspace({
fileId: "6191cd35-bb1f-81f7-8004-7cc63d087374",
pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375",
});
const badge = page.locator(".selection-size-badge");
await expect(badge).toHaveCount(0);
await workspacePage.clickLeafLayer("Rectangle");
await expect(badge).toBeVisible();
await workspacePage.page.keyboard.press("Escape");
await expect(badge).toHaveCount(0);
});
test("Selection size badge uses component color for component selection", async ({
page,
}) => {
// The expected component color below is the dark palette value.
await page.emulateMedia({ colorScheme: "dark" });
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockGetFile("components/get-file-13267.json");
await workspacePage.goToWorkspace({
fileId: "e9c84e12-dd29-80fc-8007-86d559dced7f",
pageId: "e9c84e12-dd29-80fc-8007-86d559dced80",
});
await workspacePage.clickLeafLayer("A Component");
const badge = page.locator(".selection-size-badge");
await expect(badge).toBeVisible();
await expect(badge.locator("rect")).toHaveCSS("fill", "rgb(187, 151, 216)");
await expect(badge.locator("text")).toHaveCSS("fill", "rgb(255, 255, 255)");
});
test("Selection size badge shows unrotated dimensions for rotated single selection", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockRPC(
/get\-file\?/,
"workspace/get-file-not-empty.json",
);
await workspacePage.mockRPC(
"update-file?id=*",
"workspace/update-file-create-rect.json",
);
await workspacePage.goToWorkspace({
fileId: "6191cd35-bb1f-81f7-8004-7cc63d087374",
pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375",
});
await workspacePage.clickLeafLayer("Rectangle");
const badgeText = page.locator(".selection-size-badge text");
await expect(badgeText).toHaveText("126 x 134");
const rotationInput = workspacePage.rightSidebar.getByRole("textbox", {
name: "Rotation",
});
await rotationInput.fill("45");
await rotationInput.press("Enter");
await expect(rotationInput).toHaveValue("45");
await expect(badgeText).toHaveText("126 x 134");
});
test("Selection size badge shows dimensions for path shapes", async ({ page }) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockRPC(
"update-file?id=*",
"workspace/update-file-empty.json",
);
await workspacePage.goToWorkspace();
// Workaround: hover viewport first to avoid nil mouse position crash
await workspacePage.viewport.hover();
// Draw a path with two segments; a single straight segment places
// the size badge along the line instead
await workspacePage.pathButton.click();
await workspacePage.clickAt(779, 163);
await workspacePage.clickAt(951, 258);
await workspacePage.clickAt(1050, 163);
// Finish drawing (commits path, path enters edition mode)
await page.keyboard.press("Escape");
// Exit edition mode (path stays selected, badge becomes visible)
await page.keyboard.press("Escape");
const badgeText = page.locator(".selection-size-badge text");
await expect(badgeText).toBeVisible();
await expect(badgeText).toHaveText(/\d+\.?\d* x \d+\.?\d*/);
});
test("Selection size badge runs along straight line paths", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockRPC(
"update-file?id=*",
"workspace/update-file-empty.json",
);
await workspacePage.goToWorkspace();
// Workaround: hover viewport first to avoid nil mouse position crash
await workspacePage.viewport.hover();
// Draw a path with a single straight segment, going down to the right
await workspacePage.pathButton.click();
await workspacePage.clickAt(779, 163);
await workspacePage.clickAt(951, 258);
// Finish drawing (commits path, path enters edition mode)
await page.keyboard.press("Escape");
// Exit edition mode (path stays selected)
await page.keyboard.press("Escape");
await expect(page.locator(".line-controls")).toBeVisible();
// A straight line shows its length, with two decimals at most
const badge = page.locator(".selection-size-badge");
await expect(badge.locator("text")).toHaveText(/^\d+(\.\d{1,2})?$/);
// The badge follows the line angle, about 29 degrees
const transform = await badge.getAttribute("transform");
const rotation = Number(transform.match(/rotate\(([^)]+)\)/)[1]);
expect(rotation).toBeGreaterThan(20);
expect(rotation).toBeLessThan(40);
});
test("User makes a group", async ({ page }) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockRPC(
/get\-file\?/,
"workspace/get-file-not-empty.json",
);
await workspacePage.mockRPC(
"update-file?id=*",
"workspace/update-file-create-rect.json",
);
await workspacePage.goToWorkspace({
fileId: "6191cd35-bb1f-81f7-8004-7cc63d087374",
pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375",
});
await workspacePage.clickLeafLayer("Rectangle");
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 WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.goToWorkspace();
await workspacePage.toggleToolbarButton.click();
await workspacePage.page.keyboard.press("Backspace");
await workspacePage.page.keyboard.press("Enter");
await workspacePage.expectHiddenToolbarOptions();
});
test("Bug 7525 - User moves a scrollbar and no selection rectangle appears", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockRPC(
/get\-file\?/,
"workspace/get-file-not-empty.json",
);
await workspacePage.mockRPC(
"update-file?id=*",
"workspace/update-file-create-rect.json",
);
await workspacePage.goToWorkspace({
fileId: "6191cd35-bb1f-81f7-8004-7cc63d087374",
pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375",
});
// 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;
await expect(horizontalScrollbar).toBeVisible();
// Grab scrollbar and move
const { x, y } = await horizontalScrollbar.boundingBox();
await page.waitForTimeout(100);
await workspacePage.viewport.hover({ position: { x: x, y: y + 5 } });
await page.mouse.down();
await workspacePage.viewport.hover({ position: { x: x - 130, y: y - 95 } });
await expect(workspacePage.selectionRect).not.toBeInViewport();
});
test("User adds a library and its automatically selected in the color palette", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockRPC(
"link-file-to-library",
"workspace/link-file-to-library.json",
);
await workspacePage.mockRPC(
"unlink-file-from-library",
"workspace/unlink-file-from-library.json",
);
await workspacePage.mockRPC(
"get-team-shared-files?team-id=*",
"workspace/get-team-shared-libraries-non-empty.json",
);
await workspacePage.goToWorkspace();
// Add Testing library 1
await workspacePage.clickColorPalette();
await workspacePage.clickAssets();
// Now the get-file call should return a library
await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-library.json");
await workspacePage.openLibrariesModal();
await workspacePage.librariesModal.getByRole("tab", { name: "Libraries" }).click();
await workspacePage.clickLibrary("Testing library 1");
await workspacePage.closeLibrariesModal();
await expect(
workspacePage.palette.getByRole("button", { name: "test-color-187cd5" }),
).toBeVisible();
// Remove Testing library 1
await workspacePage.openLibrariesModal();
await workspacePage.clickLibrary("Testing library 1");
await workspacePage.closeLibrariesModal();
await expect(
workspacePage.palette.getByText(
"There are no color styles in your library yet",
),
).toBeVisible();
});
test("Bug 10179 - Drag & drop doesn't add colors to the Recent Colors palette", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.goToWorkspace();
await workspacePage.moveButton.click();
await workspacePage.page.keyboard.press("Alt+p");
await expect(
workspacePage.palette.getByText(
"There are no color styles in your library yet",
),
).toBeVisible();
await page.getByRole("button", { name: "#E8E9EA" }).click();
await expect(page.getByTestId("colorpicker")).toBeVisible();
const handler = await page.getByTestId("ramp-handler");
await expect(handler).toBeVisible();
const saturation_selection = await page.getByTestId(
"value-saturation-selector",
);
await expect(saturation_selection).toBeVisible();
const saturation_box = await saturation_selection.boundingBox();
const handler_box = await handler.boundingBox();
await page.mouse.move(
handler_box.x + handler_box.width,
handler_box.y + handler_box.height / 2,
);
await page.mouse.down();
await page.mouse.move(
saturation_box.x + saturation_box.width / 2,
saturation_box.y + saturation_box.height / 2,
);
await page.mouse.up();
await expect(
workspacePage.palette.getByText(
"There are no color styles in your library yet",
),
).not.toBeVisible();
});
test("Bug 7489 - Workspace-palette items stay hidden when opening with keyboard-shortcut", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.goToWorkspace();
await workspacePage.clickTogglePalettesVisibility();
await workspacePage.page.keyboard.press("Alt+t");
await expect(
workspacePage.palette.getByText(
"There are no typography styles in your library yet",
),
).toBeVisible();
});
test("Bug 8784 - Use keyboard arrow to move inside a text input does not change tabs", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.goToWorkspace();
await workspacePage.pageName.click();
await page.keyboard.press("ArrowLeft");
await expect(workspacePage.pageName).toHaveText("Page 1");
});
test("Bug 9066 - Problem with grid layout", async ({ page }) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile(page);
await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-9066.json");
await workspacePage.mockRPC(
"update-file?id=*",
"workspace/update-file-create-rect.json",
);
await workspacePage.goToWorkspace({
fileId: "e179d9df-de35-80bf-8005-283bbd5516b0",
pageId: "e179d9df-de35-80bf-8005-283bbd5516b1",
});
await workspacePage.clickToggableLayer("Board");
await workspacePage.clickToggableLayer("Group");
await page.getByText("A", { exact: true }).click();
await workspacePage.rightSidebar.getByTestId("component-pill-button").click();
await page.getByTitle("C", { exact: true }).click();
await expect(
page.getByTestId("children-6ad3e6b9-c5a0-80cf-8005-283bbe378bcb"),
).toHaveText(["CBCDEF"]);
});
test("User have toolbar", async ({ page }) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile(page);
await workspacePage.goToWorkspace();
await expect(page.getByTitle("toggle toolbar")).toBeVisible();
await expect(page.getByTitle("design")).toBeVisible();
});
test("User have edition menu entries", async ({ page }) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile(page);
await workspacePage.goToWorkspace();
await page.getByRole("button", { name: "Main menu" }).click();
await page.getByText("file").last().click();
await expect(page.getByText("Add as Shared Library")).toBeVisible();
await page.getByText("edit").click();
await expect(page.getByText("Undo")).toBeVisible();
await expect(page.getByText("Redo")).toBeVisible();
});
test("Copy/paste properties", async ({ page, context }) => {
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",
});
// Access to the read/write clipboard necesary for this functionality
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
await page.getByTestId("layer-item").getByRole("button").first().click();
await page
.getByTestId("children-0eef4dd0-b39b-807a-8005-934805578f93")
.getByText("Rectangle")
.click({ button: "right" });
await page.getByText("Copy/Paste as").hover();
await page.getByText("Copy properties").click();
await page
.getByTestId("layer-item")
.getByText("Uno dos tres cuatro")
.click({ button: "right" });
await page.getByText("Copy/Paste as").hover();
await page.getByText("Paste properties").click();
await page.getByText("Rectangle").first().click({ button: "right" });
await page.getByText("Copy/Paste as").hover();
await page.getByText("Paste properties").click();
await page.getByText("Board").nth(2).click({ button: "right" });
await page.getByText("Copy/Paste as").hover();
await page.getByText("Paste properties").click();
await page
.getByTestId("layer-item")
.locator("div")
.filter({ hasText: "Path" })
.nth(1)
.click({ button: "right" });
await page.getByText("Copy/Paste as").hover();
await page.getByText("Paste properties").click();
await page.getByText("Ellipse").click({ button: "right" });
await page.getByText("Copy/Paste as").hover();
await page.getByText("Paste properties").click();
});
test("[Taiga #9929] Paste text in workspace", async ({ page, context }) => {
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(/^Paste/i).click();
await workspacePage.viewport
.getByRole("textbox")
.getByText("Lorem ipsum dolor");
});
// I've skipped this test because it doesn't make sense with the new render.
test.skip("[Taiga #9930] Zoom fit all doesn't fit all shapes", async ({
page,
context,
}) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile(page);
await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-9930.json");
await workspacePage.goToWorkspace({
fileId: "8f843b59-7fbb-81ce-8005-aa6d47ae3111",
pageId: "fb9798e7-a547-80ae-8005-9ffda4a13e2c",
});
const zoom = page.getByTitle("Zoom");
await zoom.click();
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",
"shape-165d1e5a-5873-8010-8005-9ffdbeaf8d8a",
"shape-165d1e5a-5873-8010-8005-9ffdbeaf8d9e",
"shape-165d1e5a-5873-8010-8005-9ffdbeb053d9",
"shape-165d1e5a-5873-8010-8005-9ffdbeb09738",
"shape-165d1e5a-5873-8010-8005-9ffdbeb0f3fc",
];
function contains(container, contained) {
return (
container.x <= contained.x &&
container.y <= contained.y &&
container.width >= contained.width &&
container.height >= contained.height
);
}
const viewportBoundingBox = await workspacePage.viewport.boundingBox();
for (const id of ids) {
const shape = page.locator(`.viewport-selrect`);
const shapeBoundingBox = await shape.boundingBox();
expect(contains(viewportBoundingBox, shapeBoundingBox)).toBeTruthy();
}
});
test("Bug 9877, user navigation to dashboard from header goes to blank page", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile(page);
await workspacePage.goToWorkspace();
const popupPromise = page.waitForEvent("popup");
await page.getByText("Drafts").click();
const popup = await popupPromise;
await expect(popup).toHaveURL(
/&project-id=c7ce0794-0992-8105-8004-38e630f7920b/,
);
});
test("Bug 8371 - Flatten option is not visible in context menu", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile(page);
await workspacePage.mockGetFile("workspace/get-file-8371.json");
await workspacePage.goToWorkspace({
fileId: "7ce7750c-3efe-8009-8006-bf390a415df2",
pageId: "7ce7750c-3efe-8009-8006-bf390a415df3",
});
const shape = workspacePage.page.locator(
`[id="shape-40c555bd-1810-809a-8006-bf3912728203"]`,
);
await workspacePage.clickLeafLayer("Union");
await workspacePage.page
.locator(".viewport-selrect")
.click({ button: "right" });
await expect(workspacePage.contextMenuForShape).toBeVisible();
await expect(
workspacePage.contextMenuForShape
.getByText("Flatten")
// there are hidden elements in the context menu (in submenus) with "Flatten" text
.filter({ visible: true }),
).toBeVisible();
});
test("BUG 13415 - Grid layout overlay is not removed when deleting a board", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile(page);
await workspacePage.mockGetFile("workspace/get-file-13415.json");
await workspacePage.mockRPC(
"update-file?id=*",
"workspace/update-file-13415.json",
);
await workspacePage.goToWorkspace();
await workspacePage.clickLeafLayer("Board");
const currentRenderCount = await workspacePage.getRenderCount();
await workspacePage.page.keyboard.press("Delete");
await workspacePage.waitForNextRender(currentRenderCount);
await workspacePage.hideUI();
await expect(workspacePage.canvas).toHaveScreenshot();
});
test("BUG 13822 - Problems with z-index", async ({
page
}) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockGetFile("workspace/get-file-13822.json");
await workspacePage.goToWorkspace({
fileId: "7fd33337-c651-80ae-8007-c37410926e0f",
pageId: "af41758c-e196-8138-8007-c36f805c3f6d",
});
await workspacePage.waitForFirstRenderWithoutUI();
await expect(workspacePage.canvas).toHaveScreenshot();
});
test("BUG 14239 - Fix default path thickness", async ({
page
}) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockRPC("update-file?id=*", "workspace/update-file-empty.json");
await workspacePage.goToWorkspace();
// (Workaround a bug in which mouse position can be nil and path editor crashes
// if we click on the Path tool without hovering over the viewport first)
await workspacePage.viewport.hover();
// 1. Draw a path
await workspacePage.pathButton.click();
await workspacePage.clickAt(779, 163);
await workspacePage.clickAt(951, 258);
// 2. Close it
await page.keyboard.press("Escape");
await expect(workspacePage.rightSidebar.getByRole("textbox", { name: "Stroke width" })).toHaveValue("1");
});
test("Bug 14250 - User with viewer role can select a locked board with a grid", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockRPC("get-teams", "get-teams-role-viewer.json");
await workspacePage.mockRPC(
/get\-file\?/,
"workspace/get-file-14250.json",
);
await workspacePage.goToWorkspace();
// Select the board from the layer tree to reveal its position
// on the canvas via the selection rectangle overlay
await workspacePage.clickLeafLayer("Locked Board with Grid");
await page.waitForSelector(".viewport-selrect");
// Get the selection rectangle bounding box (page coordinates)
// and calculate its center relative to the viewport element
const selrectBox = await page.locator(".viewport-selrect").boundingBox();
const viewportBox = await workspacePage.viewport.boundingBox();
const centerX = selrectBox.x + selrectBox.width / 2 - viewportBox.x;
const centerY = selrectBox.y + selrectBox.height / 2 - viewportBox.y;
// Deselect by pressing Escape
await page.keyboard.press("Escape");
// Click on the canvas at the board's center
await workspacePage.clickAt(centerX, centerY);
// Verify the board is now selected in the layers bar
await workspacePage.expectSelectedLayer("Locked Board with Grid");
});