mirror of
https://github.com/penpot/penpot.git
synced 2026-11-01 23:50:38 +00:00
* 🎉 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>
735 lines
24 KiB
JavaScript
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");
|
|
});
|