mirror of
https://github.com/penpot/penpot.git
synced 2026-09-22 20:06:16 +00:00
* ✨ Auto link tokens when adding external libraries (provisional) * 🔧 Refactor tokens-lib initialization * 🔧 Add separated TokenStatus to store status apart of TokensLib * 🔧 Make all status operations use the new data structure * 🔧 Normalize status helper functions and access token sets by id * 🔧 Rename :tokens-file to :tokens-source * 🎉 Allow the user to choose the tokens-source of a file * 🎉 Make tokens library readonly when it's in an external file * 🎉 Show tokens in library summaries * 🎉 Show source info in sidebar * 🔧 Fix integration tests * 🐛 Propagate changes of token values in external library * 🎉 Layout updates * 🔧 Refactor tokens source calculations * 🔧 Add harder checks for nil or empty values in everything * 🐛 Fix some integration tests * 🔧 Add integration tests for tokens in external libs * 🔧 Validate and repair missing tokens status * 🎉 Make ui changes optional with config flag * 🐛 Propagate tokens after synchronizing components in ext library * 🐛 Propagate tokens after creating new instances * 🐛 Propagate tokens after synchronizing tokens in ext library * 🐛 Add a tokens source icon to libraries section (#11439) * 🐛 Add a tokens source icon to libraries section * 🐛 Fix ellipsis on library names * ♻️ Remove code under flag on legacy component * 🐛 Fix token theme name on inspect tab * 🎉 Add changes notification (#11476) * 🎉 Add changes notification * ♻️ Change fn names * 🐛 Fix tokens source label truncation and missing translations (#11533) * 🐛 Fix tokens source label truncation and missing translations The tokens source file name always showed, even for the current file, and long names wrapped onto a second line instead of truncating because the header used flex-wrap and overflow-wrap: break-word instead of single-line ellipsis. Show the source row unconditionally (it now displays "This file" when the source is the current file, matching the connected-library case), truncate the file name to one line with an ellipsis, and only attach a tooltip with the full name when the text is actually truncated. Replace the hardcoded UI strings with translated ones and add their English and Spanish entries. AI-assisted-by: claude-sonnet-5 * 🐛 Remove redundant effect dependency in tokens source file-name-truncated? was listed as a dependency of the with-effect that checks and observes label truncation, even though it isn't read inside the effect body. Since the effect itself flips that state via check-file-name-truncated, including it as a dependency caused the ResizeObserver to be needlessly disconnected and reconnected on every truncation change. AI-assisted-by: claude-sonnet-5 * 🐛 Fix small visual error * 🐛 Fix problem with plugins * 🐛 Fix playwright tests --------- Co-authored-by: Eva Marco <evamarcod@gmail.com> Co-authored-by: Eva Marco <eva.marco@kaleidos.net> Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
644 lines
21 KiB
JavaScript
644 lines
21 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 clickLayers(clickOptions = {}) {
|
|
await this.sidebar.getByText("Layers").click(clickOptions);
|
|
}
|
|
async clickAssets(clickOptions = {}) {
|
|
await this.sidebar.getByText("Assets").click(clickOptions);
|
|
}
|
|
async clickTokens(clickOptions = {}) {
|
|
await this.sidebar.getByText("Tokens").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;
|