penpot/frontend/playwright/ui/pages/ShortcutsPage.js
Eva Marco 5de06e8f77
🎉 Add customizable shortcuts (#10237)
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-07-24 10:36:32 +02:00

329 lines
8.9 KiB
JavaScript

import { expect } from "@playwright/test";
import { readFile } from "node:fs/promises";
import { BaseWebSocketPage } from "./BaseWebSocketPage";
function decodeKeyword(value) {
if (typeof value === "string" && value.startsWith("~:")) {
return value.slice(2);
}
if (typeof value === "string" && value.startsWith("~$")) {
return value.slice(2);
}
return value;
}
function decodeTransit(data) {
if (Array.isArray(data)) {
if (data[0] === "^ ") {
const result = {};
for (let i = 1; i < data.length; i += 2) {
const key = decodeTransit(data[i]);
const value = decodeTransit(data[i + 1]);
result[key] = value;
}
return result;
}
return data.map(decodeTransit);
}
if (data !== null && typeof data === "object") {
const result = {};
for (const [key, value] of Object.entries(data)) {
result[decodeKeyword(key)] = decodeTransit(value);
}
return result;
}
return decodeKeyword(data);
}
function encodeKeyword(key) {
return `~:${key}`;
}
function encodeTransit(data) {
if (data === null || typeof data === "boolean" || typeof data === "number") {
return data;
}
if (typeof data === "string") {
return data;
}
if (Array.isArray(data)) {
return data.map(encodeTransit);
}
const result = {};
for (const [key, value] of Object.entries(data)) {
result[encodeKeyword(key)] = encodeTransit(value);
}
return result;
}
function decodeRequestBody(body) {
try {
return decodeTransit(JSON.parse(body));
} catch {
return null;
}
}
export class ShortcutsPage extends BaseWebSocketPage {
static async init(page) {
await super.init(page);
const profileText = await readFile(
"playwright/data/logged-in-user/get-profile-logged-in.json",
"utf-8",
);
const baseProfile = decodeTransit(JSON.parse(profileText));
let customShortcuts = null;
await page.route("**/api/main/methods/get-profile", (route) => {
const profile = JSON.parse(JSON.stringify(baseProfile));
if (customShortcuts) {
profile.props["custom-shortcuts"] = customShortcuts;
}
route.fulfill({
status: 200,
contentType: "application/transit+json",
body: JSON.stringify(encodeTransit(profile)),
});
});
await page.route(
"**/api/main/methods/update-profile-props",
async (route, request) => {
const decoded = decodeRequestBody(request.postData() ?? "{}");
if (decoded?.props && "custom-shortcuts" in decoded.props) {
customShortcuts = decoded.props["custom-shortcuts"];
}
route.fulfill({
status: 200,
contentType: "application/transit+json",
body: "{}",
});
},
);
await super.mockRPC(
page,
"get-teams",
"logged-in-user/get-teams-default.json",
);
}
static async initWithShortcuts(page) {
await super.init(page);
await super.mockRPCs(page, {
"get-profile": "logged-in-user/get-profile-logged-in.json",
"get-teams": "logged-in-user/get-teams-default.json",
"update-profile-props":
"logged-in-user/update-profile-with-shortcuts.json",
});
}
static async initWithNoShortcuts(page) {
await super.init(page);
await super.mockRPCs(page, {
"get-profile": "logged-in-user/get-profile-no-shortcuts.json",
"get-teams": "logged-in-user/get-teams-default.json",
"update-profile-props":
"logged-in-user/update-profile-with-shortcuts.json",
});
}
constructor(page) {
super(page);
this.shortcutsSection = page.getByRole("region", { name: /shortcuts/i });
this.searchInput = page.getByRole("textbox", { name: /shortcuts/i });
this.allTab = page.getByRole("tab", { name: "All" });
this.personalizedTab = page.getByRole("tab", { name: "Personalized" });
this.disabledTab = page.getByRole("tab", { name: "Disabled" });
this.restoreAllButton = page.getByRole("button", {
name: /restore all/i,
});
this.importExportButton = page.getByRole("button", {
name: /import\/export/i,
});
this.fileInput = page.locator('input[type="file"]');
}
async goToShortcuts() {
await this.page.goto("#/settings/shortcuts");
await expect(this.shortcutsSection).toBeVisible();
}
async searchForShortcut(term) {
await this.searchInput.fill(term);
}
async clearSearch() {
await this.searchInput.clear();
}
async clickTab(tabName) {
const tab = this.page.getByRole("tab", { name: tabName });
await tab.click();
}
async getShortcutRow(shortcutName) {
return this.page.getByRole("listitem", {
name: new RegExp(`^${shortcutName}$`, "i"),
includeHidden: true,
});
}
async expandSubsection(subsectionName) {
const subsectionButton = this.page.getByRole("button", {
name: subsectionName,
});
await subsectionButton.click();
}
async clickEditShortcut(shortcutName) {
const button = this.page.getByRole("button", {
name: new RegExp(`Edit ${shortcutName}`, "i"),
});
await button.click();
const recordingArea = this.page.getByText("Press the key combination");
await expect(recordingArea).toBeVisible();
await recordingArea.focus();
}
async pressKey(key) {
await this.page.keyboard.press(key);
}
async pressKeyCombo(keys) {
for (const key of keys) {
await this.page.keyboard.down(key);
}
for (const key of [...keys].reverse()) {
await this.page.keyboard.up(key);
}
}
async saveShortcut() {
const saveButton = this.page.getByRole("button", { name: /save/i });
await saveButton.click();
}
async cancelEdit() {
const cancelButton = this.page.getByRole("button", { name: /cancel/i });
await cancelButton.click();
}
async resetShortcut(shortcutName) {
const row = await this.getShortcutRow(shortcutName);
const resetButton = row.getByRole("button", { name: /reset/i });
await resetButton.click();
}
async disableShortcut(shortcutName) {
const row = await this.getShortcutRow(shortcutName);
await row
.getByRole("button", { name: new RegExp(`Edit ${shortcutName}`, "i") })
.click();
const disableButton = this.page.getByRole("button", { name: /disable/i });
await disableButton.click();
}
async expectShortcutCustomized(shortcutName) {
const row = await this.getShortcutRow(shortcutName);
await expect(row).toHaveAttribute("data-customized", "true");
}
async expectShortcutNotCustomized(shortcutName) {
const row = await this.getShortcutRow(shortcutName);
await expect(row).not.toHaveAttribute("data-customized", "true");
}
async expectShortcutHasConflict(shortcutName) {
const row = await this.getShortcutRow(shortcutName);
await expect(row).toHaveAttribute("data-conflict", "true");
}
async expectShortcutVisible(shortcutName) {
const row = await this.getShortcutRow(shortcutName);
await expect(row).toBeVisible();
}
async expectShortcutHidden(shortcutName) {
const row = await this.getShortcutRow(shortcutName);
await expect(row).not.toBeVisible();
}
async restoreAllShortcuts() {
await this.restoreAllButton.click();
const confirmButton = this.page.getByRole("button", {
name: "Restore",
exact: true,
});
await confirmButton.click();
}
async exportShortcuts() {
await this.importExportButton.click();
const exportButton = this.page.getByRole("menuitem", { name: /export/i });
await exportButton.click();
}
async importShortcuts(jsonData) {
await this.importExportButton.click();
const importButton = this.page.getByRole("menuitem", { name: /import$/i });
await importButton.click();
const buffer = Buffer.from(JSON.stringify(jsonData), "utf-8");
await this.fileInput.setInputFiles({
name: "shortcuts.json",
mimeType: "application/json",
buffer,
});
}
async importShortcutsRaw(content) {
await this.importExportButton.click();
const importButton = this.page.getByRole("menuitem", { name: /import$/i });
await importButton.click();
const data =
typeof content === "string" ? content : JSON.stringify(content);
const buffer = Buffer.from(data, "utf-8");
await this.fileInput.setInputFiles({
name: "shortcuts.json",
mimeType: "application/json",
buffer,
});
}
async getExportedJson() {
const [download] = await Promise.all([
this.page.waitForEvent("download"),
this.exportShortcuts(),
]);
expect(download.suggestedFilename()).toBe("penpot-shortcuts.json");
const path = await download.path();
const content = await readFile(path, "utf-8");
return JSON.parse(content);
}
async expectShortcutDisabled(shortcutName) {
const row = await this.getShortcutRow(shortcutName);
await expect(row).toHaveAttribute("data-customized", "true");
await expect(row.locator("use[href*='broken-link']")).toBeVisible();
}
}
export default ShortcutsPage;