mirror of
https://github.com/penpot/penpot.git
synced 2026-09-19 10:26:14 +00:00
Move SPA routing out of the URL fragment into the normal query string. The screen travels in a reserved `screen` key holding the route name (`?screen=workspace&team-id=…`); every other param keeps its name. `rt/nav` and `rt/resolve` keep their signatures. This deletes the fragment-mirroring URL surgery, simplifies link-preview (the server sees everything) and nginx (single path, no SPA fallback rules needed), and migrates OIDC redirects, email links, e2e helpers and plugin test utils to the new format. Legacy `#/…` URLs translate client-side for one Penpot version (`legacy-routes`, marked TODO(next-version)); non-SPA paths are untouched. AI-assisted-by: muse-spark-1.3-contributor
641 lines
20 KiB
JavaScript
641 lines
20 KiB
JavaScript
import { expect } from "@playwright/test";
|
|
import { readFile } from "node:fs/promises";
|
|
import { MockWebSocketHelper } from "../../helpers/MockWebSocketHelper";
|
|
import { BaseWebSocketPage } from "./BaseWebSocketPage";
|
|
import { Transit } from "../../helpers/Transit";
|
|
|
|
export class WorkspacePage extends BaseWebSocketPage {
|
|
static TextEditor = class TextEditor {
|
|
constructor(workspacePage) {
|
|
this.workspacePage = workspacePage;
|
|
|
|
// locators.
|
|
this.fontSize = this.workspacePage.rightSidebar.getByRole("textbox", {
|
|
name: "Font Size",
|
|
});
|
|
this.lineHeight = this.workspacePage.rightSidebar.getByRole("textbox", {
|
|
name: "Line Height",
|
|
});
|
|
this.letterSpacing = this.workspacePage.rightSidebar.getByRole(
|
|
"textbox",
|
|
{
|
|
name: "Letter Spacing",
|
|
},
|
|
);
|
|
}
|
|
|
|
get page() {
|
|
return this.workspacePage.page;
|
|
}
|
|
|
|
async waitForStyle(locator, styleName) {
|
|
return locator.evaluate(
|
|
(element, styleName) => element.style.getPropertyValue(styleName),
|
|
styleName,
|
|
);
|
|
}
|
|
|
|
async waitForEditor() {
|
|
const typographyInput =
|
|
this.workspacePage.rightSidebar.getByLabel("Font Size");
|
|
await expect(typographyInput).toBeVisible();
|
|
}
|
|
|
|
async startEditing() {
|
|
await this.page.keyboard.press("Enter");
|
|
return this.waitForEditor();
|
|
}
|
|
|
|
async stopEditing() {
|
|
await this.page.keyboard.press("Escape");
|
|
}
|
|
|
|
async moveToLeft(amount = 0) {
|
|
for (let i = 0; i < amount; i++) {
|
|
await this.page.keyboard.press("ArrowLeft");
|
|
}
|
|
await this.waitForIdle({ timeout: 100 });
|
|
}
|
|
|
|
async moveToRight(amount = 0) {
|
|
for (let i = 0; i < amount; i++) {
|
|
await this.page.keyboard.press("ArrowRight");
|
|
}
|
|
await this.waitForIdle({ timeout: 100 });
|
|
}
|
|
|
|
async moveFromStart(offset = 0) {
|
|
await this.page.keyboard.press("Home");
|
|
await this.moveToRight(offset);
|
|
}
|
|
|
|
async moveFromEnd(offset = 0) {
|
|
await this.page.keyboard.press("ArrowRight");
|
|
await this.moveToLeft(offset);
|
|
}
|
|
|
|
async selectFromStart(length, offset = 0) {
|
|
await this.moveFromStart(offset);
|
|
await this.page.keyboard.down("Shift");
|
|
await this.moveToRight(length);
|
|
await this.page.keyboard.up("Shift");
|
|
}
|
|
|
|
async selectFromEnd(length, offset = 0) {
|
|
await this.moveFromEnd(offset);
|
|
await this.page.keyboard.down("Shift");
|
|
await this.moveToLeft(length);
|
|
await this.page.keyboard.up("Shift");
|
|
}
|
|
|
|
async changeNumericInput(locator, newValue) {
|
|
await expect(locator).toBeVisible();
|
|
await locator.focus();
|
|
await locator.fill(`${newValue}`);
|
|
await this.page.keyboard.press("Enter");
|
|
}
|
|
|
|
changeFontSize(newValue) {
|
|
return this.changeNumericInput(this.fontSize, newValue);
|
|
}
|
|
|
|
changeLineHeight(newValue) {
|
|
return this.changeNumericInput(this.lineHeight, newValue);
|
|
}
|
|
|
|
changeLetterSpacing(newValue) {
|
|
return this.changeNumericInput(this.letterSpacing, newValue);
|
|
}
|
|
|
|
async waitForIdle(options) {
|
|
await this.page.evaluate(
|
|
(options) => new Promise(
|
|
(resolve) => globalThis.requestIdleCallback(resolve, options)), options);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This should be called on `test.beforeEach`.
|
|
*
|
|
* @param {Page} page
|
|
* @returns
|
|
*/
|
|
static async init(page) {
|
|
await super.init(page);
|
|
|
|
await super.mockRPCs(page, {
|
|
"get-profile": "logged-in-user/get-profile-logged-in.json",
|
|
"get-team-users?file-id=*":
|
|
"logged-in-user/get-team-users-single-user.json",
|
|
"get-comment-threads?file-id=*":
|
|
"workspace/get-comment-threads-empty.json",
|
|
"get-project?id=*": "workspace/get-project-default.json",
|
|
"get-team?id=*": "workspace/get-team-default.json",
|
|
"get-teams": "get-teams.json",
|
|
"get-team-members?team-id=*":
|
|
"logged-in-user/get-team-members-your-penpot.json",
|
|
"get-profiles-for-file-comments?file-id=*":
|
|
"workspace/get-profile-for-file-comments.json",
|
|
"update-profile-props": "workspace/update-profile-empty.json",
|
|
});
|
|
}
|
|
|
|
static anyTeamId = "c7ce0794-0992-8105-8004-38e630f7920a";
|
|
static anyProjectId = "c7ce0794-0992-8105-8004-38e630f7920b";
|
|
static anyFileId = "c7ce0794-0992-8105-8004-38f280443849";
|
|
static anyPageId = "c7ce0794-0992-8105-8004-38f28044384a";
|
|
|
|
/**
|
|
* WebSocket mock
|
|
*
|
|
* @type {MockWebSocketHelper}
|
|
*/
|
|
#ws = null;
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param {Page} page
|
|
* @param {} [options]
|
|
*/
|
|
constructor(page, options) {
|
|
super(page);
|
|
this.pageName = page.getByTestId("page-name");
|
|
|
|
this.presentUserListItems = page
|
|
.getByTestId("active-users-list")
|
|
.getByAltText("Princesa Leia");
|
|
|
|
this.viewport = page.getByTestId("viewport");
|
|
this.rootShape = page.locator(
|
|
`[id="shape-00000000-0000-0000-0000-000000000000"]`,
|
|
);
|
|
this.toolbarOptions = page.getByTestId("toolbar-options");
|
|
this.rectShapeButton = page.getByRole("button", { name: "Rectangle (R)" });
|
|
this.ellipseShapeButton = page.getByRole("button", { name: "Ellipse (E)" });
|
|
this.textShapeButton = page.getByRole("button", { name: "Text (T)" });
|
|
this.moveButton = page.getByRole("button", { name: "Move (V)" });
|
|
this.boardButton = page.getByRole("button", { name: "Board (B)" });
|
|
this.pathButton = page.getByRole("button", { name: "Path (P)" });
|
|
this.toggleToolbarButton = page.getByRole("button", {
|
|
name: "Toggle toolbar",
|
|
});
|
|
this.colorpicker = page.getByTestId("colorpicker");
|
|
this.layers = page.getByTestId("layer-tree");
|
|
this.palette = page.getByTestId("palette");
|
|
this.sidebar = page.getByTestId("left-sidebar");
|
|
this.rightSidebar = page.getByTestId("right-sidebar");
|
|
this.selectionRect = page.getByTestId("workspace-selection-rect");
|
|
this.horizontalScrollbar = page.getByTestId("horizontal-scrollbar");
|
|
this.librariesModal = page.getByTestId("libraries-modal");
|
|
this.togglePalettesVisibility = page.getByTestId(
|
|
"toggle-palettes-visibility",
|
|
);
|
|
this.tokensUpdateCreateModal = page.getByTestId(
|
|
"token-update-create-modal",
|
|
);
|
|
this.tokenRenameNodeModal = page.getByTestId("token-rename-node-modal");
|
|
this.tokenThemeUpdateCreateModal = page.getByTestId(
|
|
"token-theme-update-create-modal",
|
|
);
|
|
this.tokenThemesSetsSidebar = page.getByTestId("token-management-sidebar");
|
|
this.tokensSidebar = page.getByTestId("tokens-sidebar");
|
|
this.tokenSetItems = page.getByTestId("tokens-set-item");
|
|
this.tokenSetGroupItems = page.getByTestId("tokens-set-group-item");
|
|
this.tokenContextMenuForToken = page.getByTestId(
|
|
"tokens-context-menu-for-token",
|
|
);
|
|
this.tokenContextMenuForSet = page.getByTestId(
|
|
"tokens-context-menu-for-set",
|
|
);
|
|
this.contextMenuForShape = page.getByTestId("context-menu");
|
|
if (options?.textEditor) {
|
|
this.textEditor = new WorkspacePage.TextEditor(this);
|
|
}
|
|
}
|
|
|
|
async goToWorkspace({
|
|
fileId = this.fileId ?? WorkspacePage.anyFileId,
|
|
pageId = this.pageId ?? WorkspacePage.anyPageId,
|
|
pageName = "Page 1",
|
|
} = {}) {
|
|
// Helpers often call setup (and this) several times per test with the
|
|
// same file. Re-navigating would reload the document and wipe the
|
|
// in-memory file state (e.g. tokens created by previous steps), so
|
|
// only navigate when the target file actually changes. Extra query
|
|
// params the app adds itself (e.g. layout=tokens) are ignored, and
|
|
// navigating away and back still reloads as before.
|
|
const currentParams = new URL(this.page.url()).searchParams;
|
|
const sameFile =
|
|
currentParams.get("screen") === "workspace" &&
|
|
currentParams.get("team-id") === WorkspacePage.anyTeamId &&
|
|
currentParams.get("file-id") === fileId &&
|
|
currentParams.get("page-id") === pageId;
|
|
if (!sameFile) {
|
|
// Drop mocks from any previous document: page.goto reloads the app,
|
|
// so entries registered by the old document would otherwise resolve
|
|
// waitForNotificationsWebSocket immediately with a stale mock that
|
|
// no longer exists in the new document.
|
|
MockWebSocketHelper.clear();
|
|
await this.page.goto(
|
|
`/?screen=workspace&team-id=${WorkspacePage.anyTeamId}&file-id=${fileId}&page-id=${pageId}`,
|
|
);
|
|
}
|
|
|
|
this.#ws = await this.waitForNotificationsWebSocket();
|
|
await this.#ws.mockOpen();
|
|
if (!sameFile) {
|
|
await this.#waitForWebSocketReadiness(pageName);
|
|
} else {
|
|
// Already on the target file (e.g. Tokens tab open, where the
|
|
// sitemap page name is not rendered): just ensure the canvas is
|
|
// present instead of waiting for the page name.
|
|
await expect(this.viewport).toBeVisible({ timeout: 30000 });
|
|
}
|
|
}
|
|
|
|
async #waitForWebSocketReadiness(pageName) {
|
|
// TODO: find a better event to settle whether the app is ready to receive notifications via ws
|
|
await expect(this.pageName).toHaveText(pageName, { timeout: 30000 })
|
|
}
|
|
|
|
async sendPresenceMessage(fixture) {
|
|
await this.#ws.mockMessage(JSON.stringify(fixture));
|
|
}
|
|
|
|
async cleanUp() {
|
|
await this.#ws.mockClose();
|
|
}
|
|
|
|
async setupEmptyFile() {
|
|
await this.mockRPCs({
|
|
"get-profile": "logged-in-user/get-profile-logged-in.json",
|
|
"get-team-users?file-id=*":
|
|
"logged-in-user/get-team-users-single-user.json ",
|
|
"get-comment-threads?file-id=*":
|
|
"workspace/get-comment-threads-empty.json",
|
|
"get-project?id=*": "workspace/get-project-default.json",
|
|
"get-team?id=*": "workspace/get-team-default.json",
|
|
"get-profiles-for-file-comments?file-id=*":
|
|
"workspace/get-profile-for-file-comments.json",
|
|
"get-file-object-thumbnails?file-id=*":
|
|
"workspace/get-file-object-thumbnails-blank.json",
|
|
"get-font-variants?team-id=*": "workspace/get-font-variants-empty.json",
|
|
"get-file-fragment?file-id=*": "workspace/get-file-fragment-blank.json",
|
|
"get-file-libraries?file-id=*": "workspace/get-file-libraries-empty.json",
|
|
// Any shape mutation schedules a persistence flush. An unmocked
|
|
// update-file answers 404, which the persistence task rethrows as an
|
|
// unhandled error and the workspace is replaced by the Internal Error
|
|
// page. Tests that need a specific response mock this again afterwards;
|
|
// the last matching route wins.
|
|
"update-file?id=*": "workspace/update-file-empty.json",
|
|
});
|
|
|
|
// by default we mock the blank file.
|
|
await this.mockGetFile("workspace/get-file-blank.json");
|
|
}
|
|
|
|
async mockGetFile(jsonFilename, options) {
|
|
const page = this.page;
|
|
const jsonPath = `playwright/data/${jsonFilename}`;
|
|
const body = await readFile(jsonPath, "utf-8");
|
|
const payload = JSON.parse(body);
|
|
|
|
const fileId = Transit.get(payload, "id");
|
|
const pageId = Transit.get(payload, "data", "pages", 0);
|
|
const teamId = Transit.get(payload, "team-id");
|
|
|
|
this.fileId = fileId ?? this.anyFileId;
|
|
this.pageId = pageId ?? this.anyPageId;
|
|
this.teamId = teamId ?? this.anyTeamId;
|
|
|
|
const path = /get\-file\?/;
|
|
const url = typeof path === "string" ? `**/api/main/methods/${path}` : path;
|
|
const interceptConfig = {
|
|
status: 200,
|
|
contentType: "application/transit+json",
|
|
...options,
|
|
};
|
|
return page.route(url, (route) =>
|
|
route.fulfill({
|
|
...interceptConfig,
|
|
body,
|
|
}),
|
|
);
|
|
}
|
|
|
|
async mockGetAsset(regex, asset) {
|
|
await this.mockRPC(new RegExp(regex), asset);
|
|
}
|
|
|
|
async setupFileWithComments() {
|
|
await this.mockRPCs({
|
|
"get-comment-threads?file-id=*":
|
|
"workspace/get-comment-threads-unread.json",
|
|
"get-file-fragment?file-id=*&fragment-id=*":
|
|
"viewer/get-file-fragment-single-board.json",
|
|
"get-comments?thread-id=*": "workspace/get-thread-comments.json",
|
|
"update-comment-thread-status":
|
|
"workspace/update-comment-thread-status.json",
|
|
});
|
|
}
|
|
|
|
async clickWithDragViewportAt(x, y, width, height) {
|
|
await this.page.waitForTimeout(100);
|
|
const box = await this.viewport.boundingBox();
|
|
if (!box) throw new Error("Viewport not visible");
|
|
|
|
const startX = box.x + x;
|
|
const startY = box.y + y;
|
|
const endX = startX + width;
|
|
const endY = startY + height;
|
|
|
|
await this.page.mouse.move(startX, startY);
|
|
await this.page.mouse.down();
|
|
// Use steps so mouseup is properly processed (see Playwright issue #20254)
|
|
await this.page.mouse.move(endX, endY, { steps: 10 });
|
|
await this.page.mouse.up();
|
|
}
|
|
|
|
async clickAt(x, y) {
|
|
await this.page.waitForTimeout(100);
|
|
await this.viewport.hover({ position: { x, y } });
|
|
await this.page.mouse.down();
|
|
await this.page.mouse.up();
|
|
}
|
|
|
|
/**
|
|
* Clicks and moves from the coordinates x1,y1 to x2,y2
|
|
*
|
|
* @param {number} x1
|
|
* @param {number} y1
|
|
* @param {number} x2
|
|
* @param {number} y2
|
|
*/
|
|
async clickAndMove(x1, y1, x2, y2) {
|
|
await this.page.waitForTimeout(100);
|
|
await this.viewport.hover({ position: { x: x1, y: y1 } });
|
|
await this.page.mouse.down();
|
|
await this.viewport.hover({ position: { x: x2, y: y2 } });
|
|
await this.page.mouse.up();
|
|
}
|
|
|
|
/**
|
|
* Creates a new Text Shape in the specified coordinates
|
|
* with an initial text.
|
|
*
|
|
* @param {number} x1
|
|
* @param {number} y1
|
|
* @param {number} x2
|
|
* @param {number} y2
|
|
* @param {string} initialText
|
|
* @param {*} [options]
|
|
*/
|
|
async createTextShape(x1, y1, x2, y2, initialText, options) {
|
|
const timeToWait = options?.timeToWait ?? 100;
|
|
await this.page.keyboard.press("T");
|
|
await this.page.waitForTimeout(timeToWait);
|
|
|
|
const layersCountBefore = await this.layers
|
|
.getByTestId("layer-row")
|
|
.count();
|
|
await this.clickAndMove(x1, y1, x2, y2);
|
|
|
|
if (initialText) {
|
|
await this.waitForSelectedShapeName("Text");
|
|
await this.page.keyboard.type(initialText);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a new auto-width Text Shape by single-clicking at the given
|
|
* coordinates (as opposed to dragging a fixed-size box) and, optionally,
|
|
* types an initial text.
|
|
*
|
|
* @param {number} x
|
|
* @param {number} y
|
|
* @param {string} [initialText]
|
|
* @param {*} [options]
|
|
*/
|
|
async createAutoWidthTextShape(x, y, initialText, options) {
|
|
const timeToWait = options?.timeToWait ?? 100;
|
|
await this.page.keyboard.press("T");
|
|
await this.page.waitForTimeout(timeToWait);
|
|
|
|
await this.clickAt(x, y);
|
|
|
|
if (initialText) {
|
|
await this.waitForSelectedShapeName("Text");
|
|
await this.page.keyboard.type(initialText);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Copies the selected element into the clipboard, or copy the
|
|
* content of the locator into the clipboard.
|
|
*
|
|
* @returns {Promise<void>}
|
|
*/
|
|
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 },
|
|
);
|
|
|
|
await this.page.waitForTimeout(3000);
|
|
}
|
|
|
|
/**
|
|
* Pastes something from the clipboard.
|
|
*
|
|
* @param {"keyboard"|"context-menu"} [kind="keyboard"]
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async paste(kind = "keyboard") {
|
|
if (kind === "context-menu") {
|
|
await this.viewport.click({ button: "right" });
|
|
return this.page.getByText("Paste", { exact: true }).click();
|
|
}
|
|
await this.page.keyboard.press("ControlOrMeta+V");
|
|
await this.page.waitForTimeout(3000);
|
|
}
|
|
|
|
async panOnViewportAt(x, y, width, height) {
|
|
await this.page.waitForTimeout(100);
|
|
await this.viewport.hover({ position: { x, y } });
|
|
await this.page.mouse.down({ button: "middle" });
|
|
await this.viewport.hover({ position: { x: x + width, y: y + height } });
|
|
await this.page.mouse.up({ button: "middle" });
|
|
}
|
|
|
|
async togglePages() {
|
|
const pagesToggle = this.page.getByText("Pages");
|
|
await pagesToggle.click();
|
|
}
|
|
|
|
async selectToolbarTool(workspacePage, toolName) {
|
|
await workspacePage.page
|
|
.getByRole("button", { name: toolName })
|
|
.first()
|
|
.click();
|
|
}
|
|
|
|
async selectToolFromFlyout(
|
|
workspacePage,
|
|
{ triggerToolName, targetToolName },
|
|
) {
|
|
const trigger = workspacePage.page
|
|
.getByRole("button", { name: triggerToolName })
|
|
.first();
|
|
|
|
const option = workspacePage.page
|
|
.getByRole("menuitemradio", { name: targetToolName })
|
|
.first();
|
|
|
|
await trigger.hover();
|
|
// Flyout opening is delayed by 350ms in the toolbar component.
|
|
await workspacePage.page.waitForTimeout(450);
|
|
await expect(trigger).toHaveAttribute("aria-expanded", "true");
|
|
await option.waitFor({ state: "visible" });
|
|
await option.click();
|
|
}
|
|
|
|
async moveSelectionToShape(name) {
|
|
await this.page.locator("rect.viewport-selrect").hover();
|
|
await this.page.mouse.down();
|
|
await this.viewport.getByText(name).first().hover({ force: true });
|
|
await this.page.mouse.up();
|
|
}
|
|
|
|
async clickLeafLayer(name, clickOptions = {}, index = 0) {
|
|
const layer = this.layers.getByText(name).nth(index);
|
|
await layer.waitFor();
|
|
await layer.click(clickOptions);
|
|
await this.page.waitForTimeout(500);
|
|
}
|
|
|
|
async doubleClickLeafLayer(name, clickOptions = {}) {
|
|
await this.clickLeafLayer(name, clickOptions);
|
|
await this.clickLeafLayer(name, clickOptions);
|
|
}
|
|
|
|
async clickToggableLayer(name, clickOptions = {}, index = 0) {
|
|
const layer = this.layers
|
|
.getByTestId("layer-row")
|
|
.filter({ hasText: name })
|
|
.nth(index);
|
|
const button = layer.getByTestId("toggle-content");
|
|
|
|
await expect(button).toBeVisible();
|
|
await button.click(clickOptions);
|
|
await button.waitFor({ ariaExpanded: true });
|
|
}
|
|
|
|
async expectSelectedLayer(name) {
|
|
await expect(
|
|
this.layers.getByRole("checkbox", { name, checked: true }),
|
|
).toBeVisible();
|
|
}
|
|
|
|
async getSelectedShapeName() {
|
|
const selectedLayer = this.layers
|
|
.getByRole("checkbox", { checked: true })
|
|
.first();
|
|
await selectedLayer.waitFor({ state: "visible" });
|
|
return (await selectedLayer.innerText()).trim();
|
|
}
|
|
|
|
async waitForSelectedShapeName(expectedName) {
|
|
const selectedLayer = this.layers
|
|
.getByRole("checkbox", { checked: true })
|
|
.first();
|
|
await expect(selectedLayer).toHaveText(expectedName);
|
|
}
|
|
|
|
async expectHiddenToolbarOptions() {
|
|
await expect(this.toolbarOptions).toHaveCSS("opacity", "0");
|
|
}
|
|
|
|
async clickAssets(clickOptions = {}) {
|
|
await this.sidebar.getByText("Assets").click(clickOptions);
|
|
}
|
|
async clickLayers(clickOptions = {}) {
|
|
await this.sidebar.getByText("Layers").click(clickOptions);
|
|
}
|
|
|
|
async openLibrariesModal(clickOptions = {}) {
|
|
await this.sidebar.getByTestId("libraries").click(clickOptions);
|
|
await expect(this.librariesModal).toBeVisible();
|
|
}
|
|
|
|
async clickLibrary(name, clickOptions = {}) {
|
|
await this.page
|
|
.getByTestId("library-item")
|
|
.filter({ hasText: name })
|
|
.getByRole("button")
|
|
.click(clickOptions);
|
|
}
|
|
|
|
async closeLibrariesModal(clickOptions = {}) {
|
|
await this.librariesModal
|
|
.getByRole("button", { name: "Close" })
|
|
.click(clickOptions);
|
|
}
|
|
|
|
async clickColorPalette(clickOptions = {}) {
|
|
await this.palette
|
|
.getByRole("button", { name: /Color Palette/ })
|
|
.click(clickOptions);
|
|
}
|
|
|
|
async clickTogglePalettesVisibility(clickOptions = {}) {
|
|
await this.togglePalettesVisibility.click(clickOptions);
|
|
}
|
|
|
|
async openTokenThemesModal(clickOptions = {}) {
|
|
await this.tokenThemesSetsSidebar.getByText("Edit").click(clickOptions);
|
|
await expect(this.tokenThemeUpdateCreateModal).toBeVisible();
|
|
}
|
|
|
|
async showComments(clickOptions = {}) {
|
|
await this.page
|
|
.getByRole("button", { name: "Comments (C)" })
|
|
.click(clickOptions);
|
|
}
|
|
|
|
async toggleCommentsVisibilityFromMenu(clickOptions = {}) {
|
|
await this.page.getByRole("button", { name: "Main menu" }).click();
|
|
await this.page.getByText("view").last().click();
|
|
await this.page.locator("#file-menu-comments").click(clickOptions);
|
|
}
|
|
}
|
|
|
|
export default WorkspacePage;
|