penpot/frontend/playwright/ui/specs/tokens/remapping.spec.js
2026-04-15 09:16:16 +02:00

699 lines
22 KiB
JavaScript

import { test, expect } from "@playwright/test";
import { WorkspacePage } from "../../pages/WorkspacePage";
import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage";
import {
createToken,
setupTokensFileRender,
setupTypographyTokensFileRender,
} from "./helpers";
test.beforeEach(async ({ page }) => {
await WorkspacePage.init(page);
await WasmWorkspacePage.mockConfigFlags(page, [
"enable-feature-design-tokens-v1",
]);
await WasmWorkspacePage.mockRPC(page, "get-teams", "get-teams-tokens.json");
});
const createTokenCombobox = async (page, type, name, textFieldName, value) => {
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
const { tokensUpdateCreateModal } = await setupTokensFileRender(page, {
flags: ["enable-token-shadow"],
});
// Create base token
await tokensTabPanel
.getByRole("button", { name: `Add Token: ${type}` })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
const nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill(name);
const valueFill = tokensUpdateCreateModal.getByRole("combobox", {
name: textFieldName,
});
await valueFill.fill(value);
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
};
const renameToken = async (page, oldName, newName) => {
const { tokensUpdateCreateModal, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page, { flags: ["enable-token-shadow"] });
const baseToken = tokensSidebar.getByRole("button", {
name: oldName,
});
await baseToken.click({ button: "right" });
await tokenContextMenuForToken.getByText("Edit token").click();
await expect(tokensUpdateCreateModal).toBeVisible();
const nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill(newName);
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
};
const createCompositeDerivedToken = async (page, type, name, reference) => {
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
const { tokensUpdateCreateModal } = await setupTokensFileRender(page, {
flags: ["enable-token-shadow"],
});
await tokensTabPanel
.getByRole("button", { name: `Add Token: ${type}` })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
const nameField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Name",
});
await nameField.fill(name);
const referenceToggle = tokensUpdateCreateModal.getByTestId("reference-opt");
await referenceToggle.click();
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Reference",
});
await referenceField.fill(reference);
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
};
test.describe("Remapping a single token", () => {
test.describe("Box Shadow Token Remapping", () => {
test("User renames box shadow token with alias references", async ({
page,
}) => {
const { tokensSidebar } = await setupTokensFileRender(page, {
flags: ["enable-token-shadow"],
});
// Create base shadow token
await createToken(page, "Shadow", "base-shadow", "Color", "#000000");
// Create derived shadow token that references base-shadow
await createCompositeDerivedToken(
page,
"Shadow",
"derived-shadow",
"{base-shadow}",
);
// Rename base-shadow token
await renameToken(page, "base-shadow", "foundation-shadow");
// Check for remapping modal
const remappingModal = page.getByTestId("token-remapping-modal");
await expect(remappingModal).toBeVisible({ timeout: 5000 });
await expect(remappingModal).toContainText("base-shadow");
await expect(remappingModal).toContainText("foundation-shadow");
const confirmButton = remappingModal.getByRole("button", {
name: "remap tokens",
});
await confirmButton.click();
// Verify token was renamed
await expect(
tokensSidebar.getByRole("button", { name: "foundation-shadow" }),
).toBeVisible();
await expect(
tokensSidebar.getByRole("button", { name: "derived-shadow" }),
).toBeVisible();
});
test("User renames and updates shadow token - referenced token and applied shapes update", async ({
page,
}) => {
const {
tokensUpdateCreateModal,
tokensSidebar,
tokenContextMenuForToken,
workspacePage,
} = await setupTokensFileRender(page, { flags: ["enable-token-shadow"] });
// Create base shadow token
await createToken(page, "Shadow", "primary-shadow", "Color", "#000000");
// Create derived shadow token that references base
await createCompositeDerivedToken(
page,
"Shadow",
"card-shadow",
"{primary-shadow}",
);
// Apply the referenced token to a shape
await page.getByRole("tab", { name: "Layers" }).click();
await workspacePage.layers
.getByTestId("layer-row")
.filter({ hasText: "Button" })
.click();
await page.getByRole("tab", { name: "Tokens" }).click();
const cardShadowToken = tokensSidebar.getByRole("button", {
name: "card-shadow",
});
await cardShadowToken.click();
// Rename and update value of base token
const primaryToken = tokensSidebar.getByRole("button", {
name: "primary-shadow",
});
await primaryToken.click({ button: "right" });
await tokenContextMenuForToken.getByText("Edit token").click();
await expect(tokensUpdateCreateModal).toBeVisible();
const nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("main-shadow");
// Update the color value
const colorField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Color",
});
await colorField.fill("#FF0000");
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
// Confirm remapping
const remappingModal = page.getByTestId("token-remapping-modal");
await expect(remappingModal).toBeVisible({ timeout: 5000 });
const confirmButton = remappingModal.getByRole("button", {
name: "remap tokens",
});
await confirmButton.click();
// Verify base token was renamed
await expect(
tokensSidebar.getByRole("button", { name: "main-shadow" }),
).toBeVisible();
// Verify referenced token still exists
await expect(
tokensSidebar.getByRole("button", { name: "card-shadow" }),
).toBeVisible();
// Verify the shape still has the token applied with the NEW name
await page.getByRole("tab", { name: "Layers" }).click();
await workspacePage.layers
.getByTestId("layer-row")
.filter({ hasText: "Button" })
.click();
// Verify the shape still has the shadow applied with the UPDATED color value
// Expand the shadow section to access the color field
const shadowSection =
workspacePage.rightSidebar.getByTestId("shadow-section");
await expect(shadowSection).toBeVisible();
// Click to expand the shadow options (the menu button)
const shadowMenuButton = shadowSection
.getByRole("button", { name: "options" })
.first();
await shadowMenuButton.click();
// Wait for the advanced options to appear
await page.waitForTimeout(500);
// Verify the color value has updated from #000000 to #FF0000
const colorInput = shadowSection.getByRole("textbox", { name: "Color" });
expect(colorInput).not.toBeNull();
const colorValue = await colorInput.inputValue();
expect(colorValue.toUpperCase()).toBe("FF0000");
});
});
test.describe("Typography Token Remapping", () => {
test("User renames typography token with alias references", async ({
page,
}) => {
const {
tokensUpdateCreateModal,
tokensSidebar,
tokenContextMenuForToken,
} = await setupTypographyTokensFileRender(page);
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
// Create base typography token
await createToken(page, "Typography", "base-text", "Font size", "16");
// Create derived typography token
await createCompositeDerivedToken(
page,
"Typography",
"body-text",
"{base-text}",
);
// Rename base token
await renameToken(page, "base-text", "default-text");
// Check for remapping modal
const remappingModal = page.getByTestId("token-remapping-modal");
await expect(remappingModal).toBeVisible({ timeout: 5000 });
const confirmButton = remappingModal.getByRole("button", {
name: "remap tokens",
});
await confirmButton.click();
// Verify token was renamed
await expect(
tokensSidebar.getByRole("button", { name: "default-text" }),
).toBeVisible();
await expect(
tokensSidebar.getByRole("button", { name: "body-text" }),
).toBeVisible();
});
test("User renames and updates typography token - referenced token and applied shapes update", async ({
page,
}) => {
const {
tokensUpdateCreateModal,
tokensSidebar,
tokenContextMenuForToken,
workspacePage,
} = await setupTypographyTokensFileRender(page);
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
// Create base typography token
await createToken(page, "Typography", "body-style", "Font size", "16");
// Create derived typography token
await tokensTabPanel
.getByRole("button", { name: "Add Token: Typography" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
let nameField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Name",
});
await nameField.fill("paragraph-style");
const referenceToggle =
tokensUpdateCreateModal.getByTestId("reference-opt");
await referenceToggle.click();
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Reference",
});
await referenceField.fill("{body-style}");
let submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Apply the referenced token to a text shape
await page.getByRole("tab", { name: "Layers" }).click();
await workspacePage.layers
.getByTestId("layer-row")
.filter({ hasText: "Some Text" })
.click();
await page.getByRole("tab", { name: "Tokens" }).click();
const paragraphToken = tokensSidebar.getByRole("button", {
name: "paragraph-style",
});
await paragraphToken.click();
// Rename and update value of base token
const bodyToken = tokensSidebar.getByRole("button", {
name: "body-style",
});
await bodyToken.click({ button: "right" });
await tokenContextMenuForToken.getByText("Edit token").click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("text-base");
// Update the font size value
const fontSizeField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Font size",
});
await fontSizeField.fill("18");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
// Confirm remapping
const remappingModal = page.getByTestId("token-remapping-modal");
await expect(remappingModal).toBeVisible({ timeout: 5000 });
const confirmButton = remappingModal.getByRole("button", {
name: "remap tokens",
});
await confirmButton.click();
// Verify base token was renamed
await expect(
tokensSidebar.getByRole("button", { name: "text-base" }),
).toBeVisible();
// Verify referenced token still exists
await expect(
tokensSidebar.getByRole("button", { name: "paragraph-style" }),
).toBeVisible();
// Verify the text shape still has the token applied with NEW name and value
await page.getByRole("tab", { name: "Layers" }).click();
await workspacePage.layers
.getByTestId("layer-row")
.filter({ hasText: "Some Text" })
.click();
// Verify the shape shows the updated font size value (18)
// This proves the remapping worked and the value update propagated through the reference
const fontSizeInput = workspacePage.rightSidebar.getByRole("textbox", {
name: "Font Size",
});
await expect(fontSizeInput).toBeVisible();
await expect(fontSizeInput).toHaveValue("18");
});
});
test.describe("Border Radius Token Remapping", () => {
test("User renames border radius token with alias references", async ({
page,
}) => {
const { tokensSidebar } = await setupTokensFileRender(page, {
flags: ["enable-token-combobox", "enable-feature-token-input"],
});
// Create base border radius token
await createTokenCombobox(
page,
"Border Radius",
"base-radius",
"Value",
"4",
);
// Create derived border radius token
await createTokenCombobox(
page,
"Border Radius",
"card-radius",
"Value",
"{base-radius}",
);
// Rename base token
await renameToken(page, "base-radius", "primary-radius");
// Check for remapping modal
const remappingModal = page.getByTestId("token-remapping-modal");
await expect(remappingModal).toBeVisible({ timeout: 5000 });
const confirmButton = remappingModal.getByRole("button", {
name: "remap tokens",
});
await confirmButton.click();
// Verify token was renamed
await expect(
tokensSidebar.getByRole("button", { name: "primary-radius" }),
).toBeVisible();
await expect(
tokensSidebar.getByRole("button", { name: "card-radius" }),
).toBeVisible();
});
test("User renames and updates border radius token - referenced token updates", async ({
page,
}) => {
const {
tokensUpdateCreateModal,
tokensSidebar,
tokenContextMenuForToken,
} = await setupTokensFileRender(page, {
flags: ["enable-token-combobox", "enable-feature-token-input"],
});
// Create base border radius token
await createTokenCombobox(
page,
"Border Radius",
"radius-sm",
"Value",
"4",
);
// Create derived border radius token
await createTokenCombobox(
page,
"Border Radius",
"button-radius",
"Value",
"{radius-sm}",
);
// Rename and update value of base token
const radiusToken = tokensSidebar.getByRole("button", {
name: "radius-sm",
});
await radiusToken.click({ button: "right" });
await tokenContextMenuForToken.getByText("Edit token").click();
await expect(tokensUpdateCreateModal).toBeVisible();
const nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("radius-base");
// Update the value
const valueField = tokensUpdateCreateModal.getByLabel("Value");
await valueField.fill("8");
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
// Confirm remapping
const remappingModal = page.getByTestId("token-remapping-modal");
await expect(remappingModal).toBeVisible({ timeout: 5000 });
const confirmButton = remappingModal.getByRole("button", {
name: "remap tokens",
});
await confirmButton.click();
// Verify base token was renamed
await expect(
tokensSidebar.getByRole("button", { name: "radius-base" }),
).toBeVisible();
// Verify referenced token still exists
await expect(
tokensSidebar.getByRole("button", { name: "button-radius" }),
).toBeVisible();
// Verify the referenced token now points to the renamed token
// by opening it and checking the reference
const buttonRadiusToken = tokensSidebar.getByRole("button", {
name: "button-radius",
});
await buttonRadiusToken.click({ button: "right" });
await tokenContextMenuForToken.getByText("Edit token").click();
await expect(tokensUpdateCreateModal).toBeVisible();
const currentValue = tokensUpdateCreateModal.getByLabel("Value");
await expect(currentValue).toHaveValue("{radius-base}");
});
});
test.describe("Cancel remap", () => {
test("Only rename - breaks reference", async ({ page }) => {
const { tokensSidebar } = await setupTokensFileRender(page, {
flags: ["enable-token-shadow"],
});
// Create base shadow token
await createToken(page, "Shadow", "base-shadow", "Color", "#000000");
// Create derived shadow token that references base-shadow
await createCompositeDerivedToken(
page,
"Shadow",
"derived-shadow",
"{base-shadow}",
);
// Rename base-shadow token
await renameToken(page, "base-shadow", "foundation-shadow");
// Check for remapping modal
const remappingModal = page.getByTestId("token-remapping-modal");
await expect(remappingModal).toBeVisible({ timeout: 5000 });
const cancelButton = remappingModal.getByRole("button", {
name: "don't remap",
});
await cancelButton.click();
// Verify token was renamed
await expect(
tokensSidebar.getByRole("button", {
name: "foundation-shadow",
}),
).toBeVisible();
await expect(
tokensSidebar.locator('[aria-label="Missing reference"]'),
).toBeVisible();
});
test("Cancel process - no changes applied", async ({ page }) => {
const { tokensSidebar } = await setupTokensFileRender(page, {
flags: ["enable-token-shadow"],
});
// Create base shadow token
await createToken(page, "Shadow", "base-shadow", "Color", "#000000");
// Create derived shadow token that references base-shadow
await createCompositeDerivedToken(
page,
"Shadow",
"derived-shadow",
"{base-shadow}",
);
// Rename base-shadow token
await renameToken(page, "base-shadow", "foundation-shadow");
// Check for remapping modal
const remappingModal = page.getByTestId("token-remapping-modal");
await expect(remappingModal).toBeVisible({ timeout: 5000 });
const closeButton = remappingModal.getByRole("button", {
name: "close",
});
await closeButton.click();
// Verify original token name still exists
await expect(
tokensSidebar.getByRole("button", { name: "base-shadow" }),
).toBeVisible();
await expect(
tokensSidebar.getByRole("button", { name: "derived-shadow" }),
).toBeVisible();
});
});
});
test.describe("Remapping group of tokens", () => {
test("User renames a group - and remaps", async ({ page }) => {
const { tokensSidebar } = await setupTokensFileRender(page);
const workspacePage = new WasmWorkspacePage(page);
const rightSidebar = workspacePage.rightSidebar;
// Create multiple tokens in a group
await createToken(page, "Color", "light.primary", "Value", "#FFFFFF");
await createToken(page, "Color", "light.secondary", "Value", "#EEEEEE");
// Verify that the node and child token are visible before deletion
const lightNode = tokensSidebar.getByRole("button", {
name: "light",
exact: true,
});
const lightNodeToken = tokensSidebar.getByRole("button", {
name: "primary",
});
// Select a node and right click on it to open context menu
await expect(lightNode).toBeVisible();
await expect(lightNodeToken).toBeVisible();
// Apply token to a shape to ensure remapping modal appears with applied token reference
await page.getByRole("tab", { name: "Layers" }).click();
await page
.getByTestId("layer-row")
.filter({ hasText: "Rectangle" })
.first()
.click();
await page.getByRole("tab", { name: "Tokens" }).click();
const lightPrimaryToken = tokensSidebar.getByRole("button", {
name: "primary",
});
await lightPrimaryToken.click();
// Right click on the node to rename
await lightNode.click({ button: "right" });
const renameNodeButton = page.getByRole("button", {
name: "Rename",
exact: true,
});
await expect(renameNodeButton).toBeVisible();
await renameNodeButton.click();
// Expect the rename modal to be visible, fill in the new name and submit
const tokenRenameNodeModal = page.getByTestId("token-rename-node-modal");
await expect(tokenRenameNodeModal).toBeVisible();
const nameField = tokenRenameNodeModal.getByRole("textbox", {
name: "Name",
});
await nameField.fill("lighter");
const submitButton = tokenRenameNodeModal.getByRole("button", {
name: "Rename",
});
await submitButton.click();
// Ensure that the remapping modal appears and confirm remap
const remappingModal = page.getByTestId("token-remapping-modal");
await expect(remappingModal).toBeVisible({ timeout: 5000 });
const confirmButton = remappingModal.getByRole("button", {
name: "remap tokens",
});
await confirmButton.click();
// Verify that the node has been renamed and tokens are still visible
const lighterNode = tokensSidebar.getByRole("button", {
name: "lighter",
exact: true,
});
await expect(lighterNode).toBeVisible();
// Verify that the applied token reference has been updated in the right sidebar for the selected shape
const fillSection = rightSidebar.getByRole("region", { name: "Fill section" });
await expect(fillSection).toBeVisible();
const tokenReference = fillSection.getByLabel("lighter.primary", {
exact: true,
});
await expect(tokenReference).toBeVisible();
});
});