♻️ Reduce and speed up the frontend e2e token suite (#13001)

* ✨ Add unit tests for the token name schema

Cover make-token-name-schema, the schema that rejects a token name whose
path clashes with an existing token or group. The lower-level
token-name-path-exists? already had a test; the schema that composes it
did not.

The cases pin an exact duplicate, a token inside a token, a token name
that collides with a group, an existing child path, valid siblings, the
fail-closed nil tree, invalid syntax and the length limit. This is the
unit-test base for reducing the 15 group-name-clash e2e cases to two.

AI-assisted-by: deepseek-v4.1-flash

* ♻️ Remove duplicate token tests and fix the spread step

The three top-level tests duplicated the ones inside the creation
describe; only the indentation differed. Keep the originals.

The shadow token test labelled "Valid spread" filled the blur field, so
the token under test ended with blur 0 and the spread never changed.
Fill spread instead and expect the referenced token to resolve with
Blur: 4.

AI-assisted-by: deepseek-v4.1-flash

* ♻️ Reduce the token group-name-clash e2e matrix to two tests

The fifteen cases repeated the same setup and only changed the token
type and value. The rule is now covered by the token name schema unit
test, so keep two representatives: thirteen simple types in one test
with a test.step each, and the two composite forms (Typography and
Shadow) in a second test.

The helper no longer re-runs the file setup, so each test loads the
page once: 15 page loads become 2 and the block drops from 433 s to
about 21 s.

AI-assisted-by: deepseek-v4.1-flash

* ♻️ Cover the numeric token creation types in one test

The nine creation tests repeated the same validation ladder and only
changed the token label and placeholder. Run the ladder once in the
border radius step, keep the reference token creation, and create the
other eight types with a test.step each.

The new helpers split the flow: open the form, assert the ladder, save
a token. The e2e drops from nine page loads to one.

AI-assisted-by: deepseek-v4.1-flash

* ♻️ Reduce the remapping e2e spec to three tests

Keep one remapping flow per token shape: box shadow with alias
references, border radius with a value change, and a group rename with
undo. The two cancel paths lose their own page loads and become steps
inside the shadow test: close keeps the original name, and "don't
remap" renames the token and breaks the reference.

The deleted typography and shadow applied-shape cases exercise the same
data path, which the remapping unit tests already cover.

AI-assisted-by: deepseek-v4.1-flash

* ♻️ Shrink the tooltip and numeric-revert e2e tests

The tooltip test asserted five message shapes over eleven layer
selections. Keep the three that have no unit test: a deleted token, a
reference outside the active sets, and a token in an inactive set. Each
is one step with a pill label and its tooltip text.

Three of its five messages were already covered by the token pill unit
test, including both resolved-value cases.

Keep one numeric-revert test and drop the fixed sleeps: await the value
with toHaveValue instead. Division by zero and the min/max clamp are
unit-tested in the numeric input tests.

AI-assisted-by: deepseek-v4.1-flash
This commit is contained in:
Andrey Antukh 2026-11-12 10:58:52 +01:00 committed by GitHub
parent dfc7919f7b
commit 8b9558b380
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 428 additions and 1671 deletions

View File

@ -7,6 +7,7 @@
(ns common-tests.files.tokens-test
(:require
[app.common.files.tokens :as cfo]
[app.common.schema :as sm]
[app.common.test-helpers.files :as thf]
[app.common.test-helpers.ids-map :as thi]
[app.common.test-helpers.tokens :as tht]
@ -1709,3 +1710,42 @@
(t/is (not (contains? (:data detached) :tokens-source)))
(t/is (not (contains? (:data detached) :tokens-status)))
(t/is (= detached (ctf/check-file detached)))))
;; Token name schema
(t/deftest test-make-token-name-schema
(t/testing "rejects a name that already exists as a token"
(let [schema (cfo/make-token-name-schema {"foo" {:name "foo"}})]
(t/is (false? (sm/validate schema "foo")))))
(t/testing "rejects a token inside an existing token"
(let [schema (cfo/make-token-name-schema {"foo" {:name "foo"}})]
(t/is (false? (sm/validate schema "foo.bar")))))
(t/testing "rejects a token name that collides with a group"
(let [schema (cfo/make-token-name-schema {"foo" {"child" {:name "foo.child"}}})]
(t/is (false? (sm/validate schema "foo")))))
(t/testing "rejects a name that exists as a child token"
(let [schema (cfo/make-token-name-schema {"foo" {"bar" {:name "foo.bar"}}})]
(t/is (false? (sm/validate schema "foo.bar")))))
(t/testing "accepts a valid sibling name"
(let [schema (cfo/make-token-name-schema {"foo" {:name "foo"}})]
(t/is (true? (sm/validate schema "bar")))
(t/is (true? (sm/validate schema "bar.baz")))))
(t/testing "rejects every name when there is no tokens tree"
(let [schema (cfo/make-token-name-schema nil)]
(t/is (false? (sm/validate schema "foo")))))
(t/testing "rejects names that do not match the token name format"
(let [schema (cfo/make-token-name-schema {})]
(t/is (false? (sm/validate schema "")))
(t/is (false? (sm/validate schema "$foo")))
(t/is (false? (sm/validate schema "foo.")))
(t/is (false? (sm/validate schema "foo..bar")))))
(t/testing "rejects names longer than 255 characters"
(let [schema (cfo/make-token-name-schema {})]
(t/is (false? (sm/validate schema (apply str (repeat 256 "a"))))))))

View File

@ -58,7 +58,7 @@ test.describe("Tokens: Apply token", () => {
const colorPicker = page.getByTestId("colorpicker");
await expect(colorPicker).toBeVisible();
await colorPicker.getByTestId('opt-token-color').click();
await colorPicker.getByTestId("opt-token-color").click();
await colorPicker.getByRole("button", { name: "black" }).click();
// The applied token badge replaces the plain color swatch/input in
@ -1649,9 +1649,7 @@ test.describe("Numeric Input and Token Integration Tests", () => {
).toBeVisible();
});
test("Invalid formula reverts to previous value in padding inputs", async ({
page,
}) => {
test("Invalid formula reverts to the previous value", async ({ page }) => {
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page, {
flags: ["enable-token-combobox", "enable-feature-token-input"],
@ -1690,120 +1688,14 @@ test.describe("Numeric Input and Token Integration Tests", () => {
// Enter a valid value first
await verticalPaddingInput.fill("23");
await verticalPaddingInput.press("Enter");
// Wait for potential error handling
await page.waitForTimeout(500);
await expect(verticalPaddingInput).toHaveValue("23");
expect(await verticalPaddingInput.inputValue()).toMatch("23");
// Enter invalid expression
// An invalid expression reverts to the previous valid value
await verticalPaddingInput.fill("abc+1");
await verticalPaddingInput.press("Enter");
// Wait for potential error handling
await page.waitForTimeout(500);
// Value should revert to previous valid value
expect(await verticalPaddingInput.inputValue()).toMatch("23");
// Should NOT contain invalid characters
expect(await verticalPaddingInput.inputValue()).not.toContain("abc");
});
test("Division by zero reverts to previous value", async ({ page }) => {
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page, {
flags: ["enable-token-combobox", "enable-feature-token-input"],
});
await page.getByRole("tab", { name: "Layers" }).click();
await workspacePage.layers.getByTestId("layer-row").nth(1).click();
const layoutSection =
workspacePage.rightSidebar.getByTestId("inspect-layout");
const addLayoutButton = layoutSection
.getByRole("button", { name: "Add layout" })
.first();
await addLayoutButton.click();
await page.getByText("Flex layout").click();
// Apply a spacing token to the Column gap property
const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
await tokensTabButton.click();
await unfoldTokenType(tokensSidebar, "spacing");
await tokensSidebar
.getByRole("button", { name: "spacing.lg" })
.click({ button: "right" });
await tokenContextMenuForToken.getByText("Column gap").click();
const verticalPaddingInput = layoutSection.getByRole("textbox", {
name: "Vertical padding",
});
// Enter a valid value first
await verticalPaddingInput.fill("23");
await verticalPaddingInput.press("Enter");
// Wait for potential error handling
await page.waitForTimeout(500);
expect(await verticalPaddingInput.inputValue()).toMatch("23");
// Enter invalid expression
await verticalPaddingInput.fill("10/0");
await verticalPaddingInput.press("Enter");
// Wait for potential error handling
await page.waitForTimeout(500);
// Value should revert to previous valid value
expect(await verticalPaddingInput.inputValue()).toMatch("23");
// Should NOT contain invalid characters
expect(await verticalPaddingInput.inputValue()).not.toContain("10/0");
// Value should revert
expect(await verticalPaddingInput.inputValue()).toMatch(/^(\d+|--)$/);
expect(await verticalPaddingInput.inputValue()).not.toBe("Infinity");
});
test("Negative expression result handled correctly", async ({ page }) => {
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page, {
flags: ["enable-token-combobox", "enable-feature-token-input"],
});
await page.getByRole("tab", { name: "Layers" }).click();
await workspacePage.layers.getByTestId("layer-row").nth(1).click();
const widthInput = workspacePage.rightSidebar.getByRole("textbox", {
name: "Width",
});
await expect(widthInput).toBeVisible();
// Enter a valid value first
await widthInput.fill("23");
await widthInput.press("Enter");
// Wait for potential error handling
await page.waitForTimeout(500);
expect(await widthInput.inputValue()).toMatch("23");
// Enter a negative expression
await widthInput.fill("10-50");
await widthInput.press("Enter");
// Wait for potential error handling
await page.waitForTimeout(500);
expect(await widthInput.inputValue()).toMatch("0.01");
// Should NOT negative values
expect(await widthInput.inputValue()).not.toContain("-40");
await expect(verticalPaddingInput).toHaveValue("23");
await expect(verticalPaddingInput).not.toHaveValue(/abc/);
});
test("Token pill show broken reference when set is not activated", async ({
@ -1903,371 +1795,107 @@ test("BUG: 14234, Numeric input token filtering must be case sensitive", async (
test("BUG: 10471, Correct tooltip on right sidebar tokens", async ({
page,
}) => {
const {
workspacePage,
tokensSidebar,
tokenContextMenuForToken,
tokenThemesSetsSidebar,
tokenSetGroupItems,
} = await setupTokensFileRender(page, {
file: "workspace/get-file-token-tooltip.json",
});
const { workspacePage, tokenThemesSetsSidebar } = await setupTokensFileRender(
page,
{
file: "workspace/get-file-token-tooltip.json",
},
);
await page.getByRole("tab", { name: "Layers" }).click();
const borderRadiusSection = page.getByRole("region", {
name: "Border radius section",
});
const fillSection = workspacePage.rightSidebar.getByRole("region", {
name: "Fill section",
});
const typographySection = workspacePage.rightSidebar.getByRole("region", {
name: "Text section",
});
// ----------------------------------
// Select rectangle with active token
// ----------------------------------
await workspacePage.layers
.getByTestId("layer-row")
.filter({ hasText: "Rectangle with token" })
.click();
await expect(borderRadiusSection).toBeVisible();
const brPill = borderRadiusSection.getByRole("button", { name: "base-50" });
await expect(brPill).toBeVisible();
await brPill.hover();
await expect(page.getByRole("tooltip", { name: "base-50" })).toBeVisible();
await expect(fillSection).toBeVisible();
const fillTokenPill = fillSection.getByLabel("out-ref", {
exact: true,
});
await expect(fillTokenPill).toBeVisible();
await fillTokenPill.hover();
const colorTokenTooltip = page.getByRole("tooltip", {
name: "out-ref",
});
await expect(colorTokenTooltip).toBeVisible();
await expect(colorTokenTooltip).toHaveText(
"Name: out-refResolved value: #da1fea",
);
// ----------------------------------
// Select text with active token
// ----------------------------------
await workspacePage.layers
.getByTestId("layer-row")
.filter({ hasText: "Text with token" })
.click();
await expect(fillSection).toBeVisible();
const textFillTokenPill = fillSection.getByLabel("out-ref", {
exact: true,
});
await expect(textFillTokenPill).toBeVisible();
await textFillTokenPill.hover();
await expect(colorTokenTooltip).toBeVisible();
await expect(colorTokenTooltip).toHaveText(
"Name: out-refResolved value: #da1fea",
);
await expect(typographySection).toBeVisible();
const typographyTokenPill = typographySection.getByLabel("out-typo", {
exact: true,
});
await expect(typographyTokenPill).toBeVisible();
await typographyTokenPill.hover();
const typographyTokenTooltip = page.getByRole("tooltip", {
name: "out-typo",
});
await expect(typographyTokenTooltip).toBeVisible();
await expect(typographyTokenTooltip).toHaveText(
'Name: out-typoResolved value:- font-family: "Arizonia"',
);
// -----------------------------------------
// Select rectangle layer with deleted token
// -----------------------------------------
await workspacePage.layers
.getByTestId("layer-row")
.filter({ hasText: "Deleted token rect" })
.click();
await test.step("Deleted token", async () => {
await workspacePage.layers
.getByTestId("layer-row")
.filter({ hasText: "Deleted token rect" })
.click();
await expect(borderRadiusSection).toBeVisible();
const deletedBrPill = borderRadiusSection.getByRole("button", {
name: "{deleted} token does not exist or",
});
await expect(deletedBrPill).toBeVisible();
await deletedBrPill.hover();
await expect(
page.getByRole("tooltip", { name: "{deleted} token does not exist or" }),
).toBeVisible();
await expect(fillSection).toBeVisible();
const fillBrokenTokenPill = fillSection.getByLabel("blue", {
exact: true,
});
await expect(fillBrokenTokenPill).toBeVisible();
await fillBrokenTokenPill.hover();
const fillBrokenTokenTooltip = page.getByRole("tooltip", {
name: "blue",
});
await expect(fillBrokenTokenTooltip).toBeVisible();
await expect(fillBrokenTokenTooltip).toHaveText(
"{blue} token does not exist or has been deleted.",
);
// ------------------------------------
// Select text layer with deleted token
// ------------------------------------
await workspacePage.layers
.getByTestId("layer-row")
.filter({ hasText: "Text with deleted token" })
.click();
await expect(typographySection).toBeVisible();
const brokenTypographyTokenPill = typographySection.getByLabel(
"deleted-typo",
{ exact: true },
);
await expect(brokenTypographyTokenPill).toBeVisible();
await brokenTypographyTokenPill.hover();
const brokenTypographyTokenTooltip = page.getByRole("tooltip", {
name: "deleted-typo",
});
await expect(brokenTypographyTokenTooltip).toBeVisible();
await expect(brokenTypographyTokenTooltip).toHaveText(
"{deleted-typo} token does not exist or has been deleted.",
);
await expect(fillSection).toBeVisible();
// ---------------------------------------------------
// Select rectangle layer with deleted reference token
// ---------------------------------------------------
await page.getByRole("tab", { name: "Tokens" }).click();
await tokenThemesSetsSidebar.getByRole("button", { name: "Dark" }).click();
const brokenPill = fillSection.getByLabel("blue", { exact: true });
await expect(brokenPill).toBeVisible();
await brokenPill.hover();
await tokenThemesSetsSidebar
.getByRole("button", { name: "Dark" })
.getByRole("checkbox")
.click();
await page.getByRole("tab", { name: "Layers" }).click();
await workspacePage.layers
.getByTestId("layer-row")
.filter({ hasText: "Rectangle with deleted reference" })
.click();
await expect(borderRadiusSection).toBeVisible();
const deletedReferenceBrPill = borderRadiusSection.getByRole("button", {
name: "Reference in {ref-1} is not valid or is not in any active set.",
const tooltip = page.getByRole("tooltip", { name: "blue" });
await expect(tooltip).toBeVisible();
await expect(tooltip).toHaveText(
"{blue} token does not exist or has been deleted.",
);
});
await expect(deletedReferenceBrPill).toBeVisible();
await deletedReferenceBrPill.hover();
await expect(
page.getByRole("tooltip", {
name: "Reference in {ref-1} is not valid or is not in any active set.",
}),
).toBeVisible();
await expect(fillSection).toBeVisible();
const fillDeletedReferenceTokenPill = fillSection.getByLabel("ref-grfeen", {
exact: true,
});
await expect(fillDeletedReferenceTokenPill).toBeVisible();
await fillDeletedReferenceTokenPill.hover();
const fillDeletedReferenceTokenTooltip = page.getByRole("tooltip", {
name: "Reference in {ref-grfeen} is not valid or is not in any active set.",
});
await expect(fillDeletedReferenceTokenTooltip).toBeVisible();
await expect(fillDeletedReferenceTokenTooltip).toHaveText(
"Reference in {ref-grfeen} is not valid or is not in any active set.",
);
// ----------------------------------------------
// Select text layer with deleted reference token
// ----------------------------------------------
await test.step("Reference outside the active sets", async () => {
await page.getByRole("tab", { name: "Tokens" }).click();
await workspacePage.layers
.getByTestId("layer-row")
.filter({ hasText: "Text with deleted reference" })
.click();
await tokenThemesSetsSidebar.getByRole("button", { name: "Dark" }).click();
await tokenThemesSetsSidebar
.getByRole("button", { name: "Dark" })
.getByRole("checkbox")
.click();
await expect(typographySection).toBeVisible();
const deletedRefTypographyTokenPill = typographySection.getByLabel(
"ref-typo",
{ exact: true },
);
await expect(deletedRefTypographyTokenPill).toBeVisible();
await deletedRefTypographyTokenPill.hover();
const deletedRefTypographyTokenTooltip = page.getByRole("tooltip", {
name: "Reference in {ref-typo} is not valid or is",
});
await expect(deletedRefTypographyTokenTooltip).toBeVisible();
await expect(deletedRefTypographyTokenTooltip).toHaveText(
"Reference in {ref-typo} is not valid or is not in any active set.",
);
await expect(fillSection).toBeVisible();
const fillTextDeletedReferenceTokenPill = fillSection.getByLabel(
"ref-grfeen",
{
await page.getByRole("tab", { name: "Layers" }).click();
await workspacePage.layers
.getByTestId("layer-row")
.filter({ hasText: "Rectangle with deleted reference" })
.click();
await expect(fillSection).toBeVisible();
const brokenReferencePill = fillSection.getByLabel("ref-grfeen", {
exact: true,
},
);
await expect(fillTextDeletedReferenceTokenPill).toBeVisible();
await fillTextDeletedReferenceTokenPill.hover();
const fillTextDeletedReferenceTokenTooltip = page.getByRole("tooltip", {
name: "Reference in {ref-grfeen} is not valid or is not in any active set.",
});
await expect(fillTextDeletedReferenceTokenTooltip).toBeVisible();
await expect(fillTextDeletedReferenceTokenTooltip).toHaveText(
"Reference in {ref-grfeen} is not valid or is not in any active set.",
);
});
await expect(brokenReferencePill).toBeVisible();
await brokenReferencePill.hover();
// -----------------------------------------------------------
// Select rectangle layer with reference token on inactive set
// -----------------------------------------------------------
await page.getByRole("tab", { name: "Tokens" }).click();
await tokenThemesSetsSidebar.getByRole("button", { name: "Global" }).click();
const tooltip = page.getByRole("tooltip", {
name: "Reference in {ref-grfeen} is not valid or is not in any active set.",
});
await expect(tooltip).toBeVisible();
await expect(tooltip).toHaveText(
"Reference in {ref-grfeen} is not valid or is not in any active set.",
);
});
await tokenThemesSetsSidebar
.getByRole("button", { name: "Global" })
.getByRole("checkbox")
.click();
await page.getByRole("tab", { name: "Layers" }).click();
await workspacePage.layers
.getByTestId("layer-row")
.filter({ hasText: "Rectangle with not active reference" })
.click();
await expect(borderRadiusSection).toBeVisible();
const nonActiveReferenceBrPill = borderRadiusSection.getByRole("button", {
name: "Reference in {in-br} is not valid or is not in any active set.",
});
await expect(nonActiveReferenceBrPill).toBeVisible();
await nonActiveReferenceBrPill.hover();
await expect(
page.getByRole("tooltip", {
name: "Reference in {in-br} is not valid or is not in any active set.",
}),
).toBeVisible();
await expect(fillSection).toBeVisible();
const fillNonActiveReferenceTokenPill = fillSection.getByLabel("in-color", {
exact: true,
});
await expect(fillNonActiveReferenceTokenPill).toBeVisible();
await fillNonActiveReferenceTokenPill.hover();
const fillNonActiveReferenceTokenTooltip = page.getByRole("tooltip", {
name: "Reference in {in-color} is not valid or is not in any active set.",
});
await expect(fillNonActiveReferenceTokenTooltip).toBeVisible();
await expect(fillNonActiveReferenceTokenTooltip).toHaveText(
"Reference in {in-color} is not valid or is not in any active set.",
);
// ------------------------------------------------------
// Select text layer with reference token on inactive set
// ------------------------------------------------------
await test.step("Token in an inactive set", async () => {
await page.getByRole("tab", { name: "Tokens" }).click();
await workspacePage.layers
.getByTestId("layer-row")
.filter({ hasText: "Text with not active reference" })
.click();
await tokenThemesSetsSidebar
.getByRole("button", { name: "Global" })
.click();
await tokenThemesSetsSidebar
.getByRole("button", { name: "Global" })
.getByRole("checkbox")
.click();
await expect(typographySection).toBeVisible();
const notActiveRefTypographyTokenPill = typographySection.getByLabel(
"in-typo",
{ exact: true },
);
await expect(notActiveRefTypographyTokenPill).toBeVisible();
await notActiveRefTypographyTokenPill.hover();
const notActiveRefTypographyTokenTooltip = page.getByRole("tooltip", {
name: "Reference in {in-typo} is not valid or is",
});
await expect(notActiveRefTypographyTokenTooltip).toBeVisible();
await expect(notActiveRefTypographyTokenTooltip).toHaveText(
"Reference in {in-typo} is not valid or is not in any active set.",
);
await expect(fillSection).toBeVisible();
const fillTextNotActiveReferenceTokenPill = fillSection.getByLabel(
"in-color",
{
exact: true,
},
);
await expect(fillTextNotActiveReferenceTokenPill).toBeVisible();
await fillTextNotActiveReferenceTokenPill.hover();
const fillTextNotActiveReferenceTokenTooltip = page.getByRole("tooltip", {
name: "Reference in {in-color} is not valid or is not in any active set.",
});
await expect(fillTextNotActiveReferenceTokenTooltip).toBeVisible();
await expect(fillTextNotActiveReferenceTokenTooltip).toHaveText(
"Reference in {in-color} is not valid or is not in any active set.",
);
await tokenThemesSetsSidebar.getByRole("button", { name: "Dark" }).click();
await tokenThemesSetsSidebar
.getByRole("button", { name: "Dark" })
.getByRole("checkbox")
.click();
// -------------------------------------------------
// Select rectangle layer with token on inactive set
// -------------------------------------------------
await page.getByRole("tab", { name: "Tokens" }).click();
await tokenThemesSetsSidebar.getByRole("button", { name: "Dark" }).click();
await page.getByRole("tab", { name: "Layers" }).click();
await tokenThemesSetsSidebar
.getByRole("button", { name: "Dark" })
.getByRole("checkbox")
.click();
await page.getByRole("tab", { name: "Layers" }).click();
await workspacePage.layers
.getByTestId("layer-row")
.filter({ hasText: "Rectangle with not active token" })
.click();
await expect(borderRadiusSection).toBeVisible();
const nonActiveBrPill = borderRadiusSection.getByRole("button", {
name: "{border-radius} token is not in any active set",
});
await expect(nonActiveBrPill).toBeVisible();
await nonActiveBrPill.hover();
await expect(
page.getByRole("tooltip", {
name: "{border-radius} token is not in any active set or has an invalid value",
}),
).toBeVisible();
await expect(fillSection).toBeVisible();
const fillNonActiveTokenPill = fillSection.getByLabel("red", {
exact: true,
});
await expect(fillNonActiveTokenPill).toBeVisible();
await fillNonActiveTokenPill.hover();
const fillNonActiveTokenTooltip = page.getByRole("tooltip", {
name: "{red} token is not in any active set or has an invalid value.",
});
await expect(fillNonActiveTokenTooltip).toBeVisible();
await expect(fillNonActiveTokenTooltip).toHaveText(
"{red} token is not in any active set or has an invalid value.",
);
await workspacePage.layers
.getByTestId("layer-row")
.filter({ hasText: "Rectangle with not active token" })
.click();
// --------------------------------------------
// Select text layer with token on inactive set
// --------------------------------------------
await expect(fillSection).toBeVisible();
await workspacePage.layers
.getByTestId("layer-row")
.filter({ hasText: "Text with not active token" })
.click();
const inactivePill = fillSection.getByLabel("red", { exact: true });
await expect(inactivePill).toBeVisible();
await inactivePill.hover();
await expect(typographySection).toBeVisible();
const notActiveTypographyTokenPill = typographySection.getByLabel(
"typo-2",
{ exact: true },
);
await expect(notActiveTypographyTokenPill).toBeVisible();
await notActiveTypographyTokenPill.hover();
const notActiveTypographyTokenTooltip = page.getByRole("tooltip", {
name: "{typo-2} token is not in any active set or has an invalid value.",
const tooltip = page.getByRole("tooltip", {
name: "{red} token is not in any active set or has an invalid value.",
});
await expect(tooltip).toBeVisible();
await expect(tooltip).toHaveText(
"{red} token is not in any active set or has an invalid value.",
);
});
await expect(notActiveTypographyTokenTooltip).toBeVisible();
await expect(notActiveTypographyTokenTooltip).toHaveText(
"{typo-2} token is not in any active set or has an invalid value.",
);
await expect(fillSection).toBeVisible();
const fillTextNotActiveTokenPill = fillSection.getByLabel(
"red",
{
exact: true,
},
);
await expect(fillTextNotActiveTokenPill).toBeVisible();
await fillTextNotActiveTokenPill.hover();
const fillTextNotActiveTokenTooltip = page.getByRole("tooltip", {
name: "{red} token is not in any active set or has an invalid value.",
});
await expect(fillTextNotActiveTokenTooltip).toBeVisible();
await expect(fillTextNotActiveTokenTooltip).toHaveText(
"{red} token is not in any active set or has an invalid value.",
);
});

View File

@ -5,7 +5,10 @@ import {
setupEmptyTokensFileRender,
setupTokensFileRender,
setupTypographyTokensFileRender,
testTokenCreationFlow,
openTokenCreationForm,
assertTokenCreationLadder,
saveToken,
createTokenWithValue,
unfoldTokenType,
createToken,
createSet,
@ -17,19 +20,64 @@ test.beforeEach(async ({ page }) => {
});
test.describe("Tokens - creation", () => {
test("User creates border radius token", async ({ page }) => {
await testTokenCreationFlow(page, {
tokenLabel: "Border Radius",
namePlaceholder: "Enter border radius token name",
valuePlaceholder: "Enter a value or alias with {alias}",
invalidValue: "red",
validValue: "2 + 3",
selfReferenceValue: "{my-token}",
missingReferenceValue: "{missing-token}",
secondValidValue: "{my-token} - 2",
resolvedValueText: "Resolved value: 5",
secondResolvedValueText: "Resolved value: 3",
test("User creates tokens of every numeric type", async ({ page }) => {
const { tokensUpdateCreateModal } = await setupEmptyTokensFileRender(page);
const cases = [
["Border Radius", "border radius", "my-token-border-radius"],
["Dimensions", "dimensions", "my-token-dimensions"],
["Font Size", "font size", "my-token-font-size"],
["Letter spacing", "letter spacing", "my-token-letter-spacing"],
["Number", "number", "my-token-number"],
["Rotation", "rotation", "my-token-rotation"],
["Sizing", "sizing", "my-token-sizing"],
["Spacing", "spacing", "my-token-spacing"],
["Stroke width", "stroke width", "my-token-stroke-width"],
];
const [first, ...rest] = cases;
await test.step(first[0], async () => {
const [tokenLabel, placeholder, name] = first;
const namePlaceholder = `Enter ${placeholder} token name`;
await openTokenCreationForm(page, tokensUpdateCreateModal, {
tokenLabel,
namePlaceholder,
});
await assertTokenCreationLadder(tokensUpdateCreateModal, { name });
await saveToken(page, tokensUpdateCreateModal, {
tokenLabel,
name,
value: "2 + 3",
resolvedText: "Resolved value: 5",
});
// A second token that references the first one
await openTokenCreationForm(page, tokensUpdateCreateModal, {
tokenLabel,
namePlaceholder,
});
await saveToken(page, tokensUpdateCreateModal, {
tokenLabel,
name: `${name}-ref`,
value: `{${name}} - 2`,
resolvedText: "Resolved value: 3",
});
});
for (const [tokenLabel, placeholder, name] of rest) {
await test.step(tokenLabel, async () => {
await createTokenWithValue(page, tokensUpdateCreateModal, {
tokenLabel,
namePlaceholder: `Enter ${placeholder} token name`,
name,
});
});
}
});
test("User creates border radius token with combobox", async ({ page }) => {
@ -187,126 +235,6 @@ test.describe("Tokens - creation", () => {
).toBeEnabled();
});
test("User creates dimensions token", async ({ page }) => {
await testTokenCreationFlow(page, {
tokenLabel: "Dimensions",
namePlaceholder: "Enter dimensions token name",
valuePlaceholder: "Enter a value or alias with {alias}",
invalidValue: "red",
validValue: "2 + 3",
selfReferenceValue: "{my-token}",
missingReferenceValue: "{missing-token}",
secondValidValue: "{my-token} - 2",
resolvedValueText: "Resolved value: 5",
secondResolvedValueText: "Resolved value: 3",
});
});
test("User creates font size token", async ({ page }) => {
await testTokenCreationFlow(page, {
tokenLabel: "Font Size",
namePlaceholder: "Enter font size token name",
valuePlaceholder: "Enter a value or alias with {alias}",
invalidValue: "red",
validValue: "2 + 3",
selfReferenceValue: "{my-token}",
missingReferenceValue: "{missing-token}",
secondValidValue: "{my-token} - 2",
resolvedValueText: "Resolved value: 5",
secondResolvedValueText: "Resolved value: 3",
});
});
test("User creates letter spacing token", async ({ page }) => {
await testTokenCreationFlow(page, {
tokenLabel: "Letter spacing",
namePlaceholder: "Enter letter spacing token name",
valuePlaceholder: "Enter a value or alias with {alias}",
invalidValue: "red",
validValue: "2 + 3",
selfReferenceValue: "{my-token}",
missingReferenceValue: "{missing-token}",
secondValidValue: "{my-token} - 2",
resolvedValueText: "Resolved value: 5",
secondResolvedValueText: "Resolved value: 3",
});
});
test("User creates number token", async ({ page }) => {
await testTokenCreationFlow(page, {
tokenLabel: "Number",
namePlaceholder: "Enter number token name",
valuePlaceholder: "Enter a value or alias with {alias}",
invalidValue: "red",
validValue: "2 + 3",
selfReferenceValue: "{my-token}",
missingReferenceValue: "{missing-token}",
secondValidValue: "{my-token} - 2",
resolvedValueText: "Resolved value: 5",
secondResolvedValueText: "Resolved value: 3",
});
});
test("User creates rotation token", async ({ page }) => {
await testTokenCreationFlow(page, {
tokenLabel: "Rotation",
namePlaceholder: "Enter rotation token name",
valuePlaceholder: "Enter a value or alias with {alias}",
invalidValue: "red",
validValue: "2 + 3",
selfReferenceValue: "{my-token}",
missingReferenceValue: "{missing-token}",
secondValidValue: "{my-token} - 2",
resolvedValueText: "Resolved value: 5",
secondResolvedValueText: "Resolved value: 3",
});
});
test("User creates sizing token", async ({ page }) => {
await testTokenCreationFlow(page, {
tokenLabel: "Sizing",
namePlaceholder: "Enter sizing token name",
valuePlaceholder: "Enter a value or alias with {alias}",
invalidValue: "red",
validValue: "2 + 3",
selfReferenceValue: "{my-token}",
missingReferenceValue: "{missing-token}",
secondValidValue: "{my-token} - 2",
resolvedValueText: "Resolved value: 5",
secondResolvedValueText: "Resolved value: 3",
});
});
test("User creates spacing token", async ({ page }) => {
await testTokenCreationFlow(page, {
tokenLabel: "Spacing",
namePlaceholder: "Enter spacing token name",
valuePlaceholder: "Enter a value or alias with {alias}",
invalidValue: "red",
validValue: "2 + 3",
selfReferenceValue: "{my-token}",
missingReferenceValue: "{missing-token}",
secondValidValue: "{my-token} - 2",
resolvedValueText: "Resolved value: 5",
secondResolvedValueText: "Resolved value: 3",
});
});
test("User creates stroke width token", async ({ page }) => {
await testTokenCreationFlow(page, {
tokenLabel: "Stroke width",
namePlaceholder: "Enter stroke width token name",
valuePlaceholder: "Enter a value or alias with {alias}",
invalidValue: "red",
validValue: "2 + 3",
selfReferenceValue: "{my-token}",
missingReferenceValue: "{missing-token}",
secondValidValue: "{my-token} - 2",
resolvedValueText: "Resolved value: 5",
secondResolvedValueText: "Resolved value: 3",
});
});
test("User creates color token and auto created set show up in the sidebar", async ({
page,
}) => {
@ -569,7 +497,9 @@ test.describe("Tokens - creation", () => {
});
await selectDropdown.click();
const fontOption = tokensUpdateCreateModal.getByRole('img', { name: 'ABeeZee' })
const fontOption = tokensUpdateCreateModal.getByRole("img", {
name: "ABeeZee",
});
await expect(fontOption).toBeVisible();
await fontOption.click();
@ -583,7 +513,9 @@ test.describe("Tokens - creation", () => {
name: "Search font",
});
await searchField.fill("alme");
const fontOption2 = tokensUpdateCreateModal.getByRole('img', {name: "Almendra Display"});
const fontOption2 = tokensUpdateCreateModal.getByRole("img", {
name: "Almendra Display",
});
await expect(fontOption2).toBeVisible();
await fontOption2.click();
@ -1133,7 +1065,7 @@ test.describe("Tokens - creation", () => {
// 8. Valid spread → resolved
await blurField.fill("3 - 3");
await spreadField.fill("3 - 3");
await expect(
tokensUpdateCreateModal.getByText("Resolved value: 0"),
).toBeVisible();
@ -1175,7 +1107,7 @@ test.describe("Tokens - creation", () => {
await referenceField.fill("{my-token}");
await expect(
tokensUpdateCreateModal.getByText(
"Resolved value: - X: 6 - Y: 10 - Blur: 0 - Spread: 0 ",
"Resolved value: - X: 6 - Y: 10 - Blur: 4 - Spread: 0 ",
),
).toBeVisible();
@ -1521,7 +1453,9 @@ test.describe("Tokens - creation", () => {
});
await selectDropdown.click();
const fontOption = tokensUpdateCreateModal.getByRole("img", {name: "ABeeZee"});
const fontOption = tokensUpdateCreateModal.getByRole("img", {
name: "ABeeZee",
});
await expect(fontOption).toBeVisible();
await fontOption.click();
@ -1536,7 +1470,9 @@ test.describe("Tokens - creation", () => {
name: "Search font",
});
await searchField.fill("alme");
const fontOption2 = tokensUpdateCreateModal.getByRole("img", {name: "Almendra Display"});
const fontOption2 = tokensUpdateCreateModal.getByRole("img", {
name: "Almendra Display",
});
await expect(fontOption2).toBeVisible();
await fontOption2.click();
await expect(
@ -1820,85 +1756,6 @@ test.describe("Tokens - creation", () => {
});
});
test("User creates grouped color token", async ({ page }) => {
const { workspacePage, tokensUpdateCreateModal, tokensSidebar } =
await setupEmptyTokensFileRender(page);
await tokensSidebar.getByRole("button", { name: "Add Token: Color" }).click();
// Create grouped color token with mouse
await expect(tokensUpdateCreateModal).toBeVisible();
const nameField = tokensUpdateCreateModal.getByLabel("Name");
const valueField = tokensUpdateCreateModal.getByLabel("Value");
await nameField.click();
await nameField.fill("dark.primary");
await valueField.click();
await valueField.fill("red");
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await expect(submitButton).toBeEnabled();
await submitButton.click();
await unfoldTokenType(tokensSidebar, "color");
await expect(tokensSidebar.getByLabel("primary")).toBeEnabled();
});
test("User can't create regular token with value missing", async ({ page }) => {
const { tokensUpdateCreateModal } = await setupEmptyTokensFileRender(page);
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
await tokensTabPanel
.getByRole("button", { name: "Add Token: Color" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
const nameField = tokensUpdateCreateModal.getByLabel("Name");
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
// Initially submit button should be disabled
await expect(submitButton).toBeDisabled();
// Fill in name but leave value empty
await nameField.click();
await nameField.fill("primary");
// Submit button should remain disabled when value is empty
await expect(submitButton).toBeDisabled();
});
test("User duplicate color token", async ({ page }) => {
const { tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page);
await expect(tokensSidebar).toBeVisible();
await unfoldTokenType(tokensSidebar, "color");
const colorToken = tokensSidebar.getByRole("button", {
name: "colors.blue.100",
});
await colorToken.click({ button: "right" });
await expect(tokenContextMenuForToken).toBeVisible();
await tokenContextMenuForToken.getByText("Duplicate token").click();
await expect(tokenContextMenuForToken).not.toBeVisible();
await expect(
tokensSidebar.getByRole("button", { name: "colors.blue.100-copy" }),
).toBeVisible();
});
test("User disables the current set but token still have resolved values shown in the sidebar", async ({
page,
}) => {
@ -1932,401 +1789,135 @@ test("User disables the current set but token still have resolved values shown i
});
test.describe("User can't create groups that clash with token names", () => {
const createBadToken = async (page, type, name, textFieldName, value, inputType) => {
const clashError = (name) =>
`Group name of ${name} conflicts with a token of the same name in another active set.`;
const setupClashingFile = async (page) => {
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
await setupTokensFileRender(page, {
file: "workspace/get-file-tokens-all-types.json",
flags: ["enable-token-shadow"],
});
await expect(tokensSidebar).toBeVisible();
await createSet(tokenThemesSetsSidebar, "Second set");
return { tokensUpdateCreateModal };
};
const openTokenForm = async (page, tokensUpdateCreateModal, type) => {
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
const { tokensUpdateCreateModal } = await setupTokensFileRender(page, {
// Same file as the outer setup: re-navigating would reload the
// app and wipe the fixture tokens this test clashes against.
file: "workspace/get-file-tokens-all-types.json",
flags: ["enable-token-shadow"],
});
// Add a token of the given type
await tokensTabPanel
.getByRole("button", { name: `Add Token: ${type}` })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
// Fill the bad name
const nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill(name);
// Fill the value
const valueField = tokensUpdateCreateModal.getByRole(inputType, {
name: textFieldName,
});
await valueField.fill(value);
// Check that the value has an error
const errorNode = tokensUpdateCreateModal.getByText(
`Group name of ${name} conflicts with a token of the same name in another active set.`,
);
await expect(errorNode).toBeVisible();
// Check that the form cannot be saved
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await expect(submitButton).toBeDisabled();
};
test("User can't create Border Radius token with group name that clashes with existing token", async ({
page,
}) => {
const { tokenThemesSetsSidebar, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page, {
file: "workspace/get-file-tokens-all-types.json",
flags: ["enable-token-shadow"],
});
await expect(tokensSidebar).toBeVisible();
await createSet(tokenThemesSetsSidebar, "Second set");
await createBadToken(page, "Border Radius", "rad1.bad", "Value", "10", "combobox");
});
test("User can't create Color token with group name that clashes with existing token", async ({
page,
}) => {
const { tokenThemesSetsSidebar, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page, {
file: "workspace/get-file-tokens-all-types.json",
flags: ["enable-token-shadow"],
});
await expect(tokensSidebar).toBeVisible();
await createSet(tokenThemesSetsSidebar, "Second set");
await createBadToken(page, "Color", "col1.bad", "Value", "red", "textbox");
});
test("User can't create Dimensions token with group name that clashes with existing token", async ({
page,
}) => {
const { tokenThemesSetsSidebar, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page, {
file: "workspace/get-file-tokens-all-types.json",
flags: ["enable-token-shadow"],
});
await expect(tokensSidebar).toBeVisible();
await createSet(tokenThemesSetsSidebar, "Second set");
await createBadToken(page, "Dimensions", "dim1.bad", "Value", "100", "combobox");
});
test("User can't create Font Size token with group name that clashes with existing token", async ({
page,
}) => {
const { tokenThemesSetsSidebar, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page, {
file: "workspace/get-file-tokens-all-types.json",
flags: ["enable-token-shadow"],
});
await expect(tokensSidebar).toBeVisible();
await createSet(tokenThemesSetsSidebar, "Second set");
await createBadToken(page, "Font Size", "fsiz1.bad", "Value", "16", "combobox");
});
test("User can't create Font Weight token with group name that clashes with existing token", async ({
page,
}) => {
const { tokenThemesSetsSidebar, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page, {
file: "workspace/get-file-tokens-all-types.json",
flags: ["enable-token-shadow"],
});
await expect(tokensSidebar).toBeVisible();
await createSet(tokenThemesSetsSidebar, "Second set");
await createBadToken(page, "Font Weight", "wei1.bad", "Value", "400", "combobox");
});
test("User can't create Letter Spacing token with group name that clashes with existing token", async ({
page,
}) => {
const { tokenThemesSetsSidebar, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page, {
file: "workspace/get-file-tokens-all-types.json",
flags: ["enable-token-shadow"],
});
await expect(tokensSidebar).toBeVisible();
await createSet(tokenThemesSetsSidebar, "Second set");
await createBadToken(page, "Letter Spacing", "lspa1.bad", "Value", "1", "combobox");
});
test("User can't create Number token with group name that clashes with existing token", async ({
page,
}) => {
const { tokenThemesSetsSidebar, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page, {
file: "workspace/get-file-tokens-all-types.json",
flags: ["enable-token-shadow"],
});
await expect(tokensSidebar).toBeVisible();
await createSet(tokenThemesSetsSidebar, "Second set");
await createBadToken(page, "Number", "num1.bad", "Value", "10", "combobox");
});
test("User can't create Rotation token with group name that clashes with existing token", async ({
page,
}) => {
const { tokenThemesSetsSidebar, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page, {
file: "workspace/get-file-tokens-all-types.json",
flags: ["enable-token-shadow"],
});
await expect(tokensSidebar).toBeVisible();
await createSet(tokenThemesSetsSidebar, "Second set");
await createBadToken(page, "Rotation", "rot1.bad", "Value", "90", "combobox");
});
test("User can't create Sizing token with group name that clashes with existing token", async ({
page,
}) => {
const { tokenThemesSetsSidebar, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page, {
file: "workspace/get-file-tokens-all-types.json",
flags: ["enable-token-shadow"],
});
await expect(tokensSidebar).toBeVisible();
await createSet(tokenThemesSetsSidebar, "Second set");
await createBadToken(page, "Sizing", "siz1.bad", "Value", "100", "combobox");
});
test("User can't create Spacing token with group name that clashes with existing token", async ({
page,
}) => {
const { tokenThemesSetsSidebar, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page, {
file: "workspace/get-file-tokens-all-types.json",
flags: ["enable-token-shadow"],
});
await expect(tokensSidebar).toBeVisible();
await createSet(tokenThemesSetsSidebar, "Second set");
await createBadToken(page, "Spacing", "spa1.bad", "Value", "10", "combobox");
});
test("User can't create Stroke Width token with group name that clashes with existing token", async ({
page,
}) => {
const { tokenThemesSetsSidebar, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page, {
file: "workspace/get-file-tokens-all-types.json",
flags: ["enable-token-shadow"],
});
await expect(tokensSidebar).toBeVisible();
await createSet(tokenThemesSetsSidebar, "Second set");
await createBadToken(page, "Stroke Width", "str1.bad", "Value", "2", "combobox");
});
test("User can't create Text Case token with group name that clashes with existing token", async ({
page,
}) => {
const { tokenThemesSetsSidebar, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page, {
file: "workspace/get-file-tokens-all-types.json",
flags: ["enable-token-shadow"],
});
await expect(tokensSidebar).toBeVisible();
await createSet(tokenThemesSetsSidebar, "Second set");
await createBadToken(page, "Text Case", "cas1.bad", "Value", "uppercase", "combobox");
});
test("User can't create Text Decoration token with group name that clashes with existing token", async ({
page,
}) => {
const { tokenThemesSetsSidebar, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page, {
file: "workspace/get-file-tokens-all-types.json",
flags: ["enable-token-shadow"],
});
await expect(tokensSidebar).toBeVisible();
await createSet(tokenThemesSetsSidebar, "Second set");
await createBadToken(
page,
"Text Decoration",
"dec1.bad",
"Value",
"strike-through",
"combobox"
);
});
test("User can't create Typography token with group name that clashes with existing token", async ({
page,
}) => {
const { tokenThemesSetsSidebar, tokensSidebar } =
await setupTokensFileRender(page, {
file: "workspace/get-file-tokens-all-types.json",
flags: ["enable-token-shadow"],
});
await expect(tokensSidebar).toBeVisible();
await createSet(tokenThemesSetsSidebar, "Second set");
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
const { tokensUpdateCreateModal } = await setupTokensFileRender(page, {
// Same file as the outer setup: re-navigating would reload the
// app and wipe the fixture tokens this test clashes against.
file: "workspace/get-file-tokens-all-types.json",
flags: ["enable-token-shadow"],
});
await tokensTabPanel
.getByRole("button", { name: `Add Token: Typography` })
const closeTokenForm = async (tokensUpdateCreateModal) => {
await tokensUpdateCreateModal
.getByRole("button", { name: "Cancel" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
const nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("typ1.bad");
await expect(tokensUpdateCreateModal).not.toBeVisible();
};
const fontFamilyField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Font family",
});
await fontFamilyField.fill("Arial");
const expectClashError = async (tokensUpdateCreateModal, name) => {
await expect(
tokensUpdateCreateModal.getByText(clashError(name)),
).toHaveCount(1);
const fontSizeField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Font size",
});
await fontSizeField.fill("16");
await expect(
tokensUpdateCreateModal.getByRole("button", { name: "Save" }),
).toBeDisabled();
};
const fontWeightField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Font weight",
});
await fontWeightField.fill("400");
const lineHeightField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Line height",
});
await lineHeightField.fill("1.5");
const letterSpacingField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Letter spacing",
});
await letterSpacingField.fill("0");
const textCaseField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Text case",
});
await textCaseField.fill("none");
const textDecorationField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Text decoration",
});
await textDecorationField.fill("none");
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
const errorNode = tokensUpdateCreateModal.getByText(
"Group name of typ1.bad conflicts with a token of the same name in another active set.",
);
await expect(errorNode).toHaveCount(1);
await expect(submitButton).toBeDisabled();
});
test("User can't create Shadow token with group name that clashes with existing token", async ({
test("User can't create a simple token with group name that clashes with existing token", async ({
page,
}) => {
const { tokenThemesSetsSidebar, tokensSidebar } =
await setupTokensFileRender(page, {
file: "workspace/get-file-tokens-all-types.json",
flags: ["enable-token-shadow"],
const { tokensUpdateCreateModal } = await setupClashingFile(page);
const cases = [
["Border Radius", "rad1.bad", "Value", "10", "combobox"],
["Color", "col1.bad", "Value", "red", "textbox"],
["Dimensions", "dim1.bad", "Value", "100", "combobox"],
["Font Size", "fsiz1.bad", "Value", "16", "combobox"],
["Font Weight", "wei1.bad", "Value", "400", "combobox"],
["Letter Spacing", "lspa1.bad", "Value", "1", "combobox"],
["Number", "num1.bad", "Value", "10", "combobox"],
["Rotation", "rot1.bad", "Value", "90", "combobox"],
["Sizing", "siz1.bad", "Value", "100", "combobox"],
["Spacing", "spa1.bad", "Value", "10", "combobox"],
["Stroke Width", "str1.bad", "Value", "2", "combobox"],
["Text Case", "cas1.bad", "Value", "uppercase", "combobox"],
["Text Decoration", "dec1.bad", "Value", "strike-through", "combobox"],
];
for (const [type, name, field, value, role] of cases) {
await test.step(type, async () => {
await openTokenForm(page, tokensUpdateCreateModal, type);
await tokensUpdateCreateModal.getByLabel("Name").fill(name);
await tokensUpdateCreateModal
.getByRole(role, { name: field })
.fill(value);
await expectClashError(tokensUpdateCreateModal, name);
await closeTokenForm(tokensUpdateCreateModal);
});
}
});
await expect(tokensSidebar).toBeVisible();
test("User can't create a composite token with group name that clashes with existing token", async ({
page,
}) => {
const { tokensUpdateCreateModal } = await setupClashingFile(page);
await createSet(tokenThemesSetsSidebar, "Second set");
await test.step("Typography", async () => {
await openTokenForm(page, tokensUpdateCreateModal, "Typography");
await tokensUpdateCreateModal.getByLabel("Name").fill("typ1.bad");
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
const fields = {
"Font family": "Arial",
"Font size": "16",
"Font weight": "400",
"Line height": "1.5",
"Letter spacing": "0",
"Text case": "none",
"Text decoration": "none",
};
const { tokensUpdateCreateModal } = await setupTokensFileRender(page, {
// Same file as the outer setup: re-navigating would reload the
// app and wipe the fixture tokens this test clashes against.
file: "workspace/get-file-tokens-all-types.json",
flags: ["enable-token-shadow"],
for (const [label, value] of Object.entries(fields)) {
await tokensUpdateCreateModal
.getByRole("textbox", { name: label })
.fill(value);
}
await expectClashError(tokensUpdateCreateModal, "typ1.bad");
await closeTokenForm(tokensUpdateCreateModal);
});
await tokensTabPanel
.getByRole("button", { name: `Add Token: Shadow` })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
await test.step("Shadow", async () => {
await openTokenForm(page, tokensUpdateCreateModal, "Shadow");
await tokensUpdateCreateModal.getByLabel("Name").fill("sha1.bad");
const nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("sha1.bad");
const fields = {
Color: "red",
X: "7",
Y: "8",
Blur: "5",
Spread: "1",
};
const colorField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Color",
for (const [label, value] of Object.entries(fields)) {
await tokensUpdateCreateModal
.getByRole("textbox", { name: label })
.fill(value);
}
await expectClashError(tokensUpdateCreateModal, "sha1.bad");
await closeTokenForm(tokensUpdateCreateModal);
});
await colorField.fill("red");
const offsetXField = tokensUpdateCreateModal.getByRole("textbox", {
name: "X",
});
await offsetXField.fill("7");
const offsetYField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Y",
});
await offsetYField.fill("8");
const blurField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Blur",
});
await blurField.fill("5");
const spreadField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Spread",
});
await spreadField.fill("1");
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
const errorNode = tokensUpdateCreateModal.getByText(
"Group name of sha1.bad conflicts with a token of the same name in another active set.",
);
await expect(errorNode).toHaveCount(1);
await expect(submitButton).toBeDisabled();
});
});

View File

@ -212,130 +212,118 @@ const setupTypographyTokensFileRender = async (page, options = {}) => {
});
};
const testTokenCreationFlow = async (
const tokenValuePlaceholder = "Enter a value or alias with {alias}";
const openTokenCreationForm = async (
page,
{
tokenLabel,
namePlaceholder,
valuePlaceholder,
validValue,
invalidValue,
selfReferenceValue,
missingReferenceValue,
secondValidValue,
resolvedValueText,
secondResolvedValueText,
},
tokensUpdateCreateModal,
{ tokenLabel, namePlaceholder },
) => {
const invalidValueError = "Invalid token value";
const emptyNameError = "Name should be at least 1 character";
const selfReferenceError = "Token has self reference";
const missingReferenceError = "Missing token references";
const { tokensUpdateCreateModal, tokensSidebar } =
await setupEmptyTokensFileRender(page);
// Open modal
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
const addTokenButton = tokensTabPanel.getByRole("button", {
name: `Add Token: ${tokenLabel}`,
});
await tokensTabPanel
.getByRole("button", { name: `Add Token: ${tokenLabel}` })
.click();
await addTokenButton.click();
await expect(tokensUpdateCreateModal).toBeVisible();
// Placeholder checks
await expect(
tokensUpdateCreateModal.getByPlaceholder(namePlaceholder),
).toBeVisible();
await expect(
tokensUpdateCreateModal.getByPlaceholder(valuePlaceholder),
tokensUpdateCreateModal.getByPlaceholder(tokenValuePlaceholder),
).toBeVisible();
};
const assertTokenCreationLadder = async (
tokensUpdateCreateModal,
{ name, invalidValue = "red" },
) => {
const nameField = tokensUpdateCreateModal.getByLabel("Name");
const valueField = tokensUpdateCreateModal.getByLabel("Value");
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
// 1. Name filled + empty value → disabled
await nameField.fill("my-token");
// 1. Name filled + empty value -> disabled
await nameField.fill(name);
await expect(submitButton).toBeDisabled();
// 2. Invalid value → disabled + error message
// 2. Invalid value -> disabled + error message
await valueField.fill(invalidValue);
const invalidValueErrorNode =
tokensUpdateCreateModal.getByText(invalidValueError);
await expect(invalidValueErrorNode).toBeVisible();
await expect(
tokensUpdateCreateModal.getByText("Invalid token value"),
).toBeVisible();
await expect(submitButton).toBeDisabled();
// 3. Empty name → disabled + error message
// 3. Empty name -> disabled + error message
await nameField.fill("");
const emptyNameErrorNode = tokensUpdateCreateModal.getByText(emptyNameError);
await expect(emptyNameErrorNode).toBeVisible();
await expect(submitButton).toBeDisabled();
// 4. Self reference → disabled + error message
await nameField.fill("my-token");
await valueField.fill(selfReferenceValue);
const selfRefErrorNode =
tokensUpdateCreateModal.getByText(selfReferenceError);
await expect(selfRefErrorNode).toBeVisible();
await expect(submitButton).toBeDisabled();
// 5. Missing reference → disabled + error message
await valueField.fill(missingReferenceValue);
const missingRefErrorNode = tokensUpdateCreateModal.getByText(
missingReferenceError,
);
await expect(missingRefErrorNode).toBeVisible();
await expect(submitButton).toBeDisabled();
//
// ------- SUCCESSFUL CREATION -------
//
// 6. Basic valid value → enabled
await valueField.fill(validValue);
await expect(
tokensUpdateCreateModal.getByText(resolvedValueText),
tokensUpdateCreateModal.getByText("Name should be at least 1 character"),
).toBeVisible();
await expect(submitButton).toBeEnabled();
await expect(submitButton).toBeDisabled();
// 4. Self reference -> disabled + error message
await nameField.fill(name);
await valueField.fill(`{${name}}`);
await expect(
tokensUpdateCreateModal.getByText("Token has self reference"),
).toBeVisible();
await expect(submitButton).toBeDisabled();
// 5. Missing reference -> disabled + error message
await valueField.fill("{missing-token}");
await expect(
tokensUpdateCreateModal.getByText("Missing token references"),
).toBeVisible();
await expect(submitButton).toBeDisabled();
};
const saveToken = async (
page,
tokensUpdateCreateModal,
{ tokenLabel, name, value, resolvedText },
) => {
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
await tokensUpdateCreateModal.getByLabel("Name").fill(name);
await tokensUpdateCreateModal.getByLabel("Value").fill(value);
await expect(tokensUpdateCreateModal.getByText(resolvedText)).toBeVisible();
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await expect(submitButton).toBeEnabled();
await submitButton.click();
await expect(
tokensTabPanel.getByRole("button", { name: "my-token" }),
).toBeEnabled();
await unfoldTokenType(tokensTabPanel, tokenLabel.toLocaleLowerCase());
//
// ------- SECOND TOKEN WITH VALID REFERENCE -------
//
await expect(tokensTabPanel.getByRole("button", { name })).toBeEnabled();
};
await addTokenButton.click();
const createTokenWithValue = async (
page,
tokensUpdateCreateModal,
{
tokenLabel,
namePlaceholder,
name,
value = "2 + 3",
resolvedText = "Resolved value: 5",
},
) => {
await openTokenCreationForm(page, tokensUpdateCreateModal, {
tokenLabel,
namePlaceholder,
});
await nameField.fill("my-token-2");
await valueField.fill(secondValidValue);
await expect(
tokensUpdateCreateModal.getByText(secondResolvedValueText),
).toBeVisible();
await expect(submitButton).toBeEnabled();
await submitButton.click();
await expect(
tokensTabPanel.getByRole("button", { name: "my-token-2" }),
).toBeEnabled();
await saveToken(page, tokensUpdateCreateModal, {
tokenLabel,
name,
value,
resolvedText,
});
};
const unfoldTokenType = async (tokensTabPanel, type) => {
@ -427,7 +415,10 @@ export {
setupTokensFileRender,
setupTypographyTokensFile,
setupTypographyTokensFileRender,
testTokenCreationFlow,
openTokenCreationForm,
assertTokenCreationLadder,
saveToken,
createTokenWithValue,
unfoldTokenType,
createToken,
createSet,

View File

@ -1,11 +1,7 @@
import { test, expect } from "@playwright/test";
import { WorkspacePage } from "../../pages/WorkspacePage";
import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage";
import {
createToken,
setupTokensFileRender,
setupTypographyTokensFileRender,
} from "./helpers";
import { createToken, setupTokensFileRender } from "./helpers";
test.beforeEach(async ({ page }) => {
await WorkspacePage.init(page);
@ -123,354 +119,60 @@ test.describe("Remapping a single token", () => {
"{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 test.step("Remap the references", async () => {
await renameToken(page, "base-shadow", "foundation-shadow");
await expect(remappingModal).toBeVisible({ timeout: 5000 });
await expect(remappingModal).toContainText("base-shadow");
await expect(remappingModal).toContainText("foundation-shadow");
await remappingModal
.getByRole("button", { name: "remap tokens" })
.click();
await expect(
tokensSidebar.getByRole("button", { name: "foundation-shadow" }),
).toBeVisible();
await expect(
tokensSidebar.getByRole("button", { name: "derived-shadow" }),
).toBeVisible();
});
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();
});
await test.step("Close the modal and keep the original name", async () => {
await renameToken(page, "foundation-shadow", "shadow-base");
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"] });
await expect(remappingModal).toBeVisible({ timeout: 5000 });
await remappingModal.getByRole("button", { name: "close" }).click();
// Create base shadow token
await createToken(
page,
"Shadow",
"primary-shadow",
"Color",
"textbox",
"#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 expect(
tokensSidebar.getByRole("button", { name: "foundation-shadow" }),
).toBeVisible();
await expect(
tokensSidebar.getByRole("button", { name: "derived-shadow" }),
).toBeVisible();
});
await cardShadowToken.click();
// Rename and update value of base token
const primaryToken = tokensSidebar.getByRole("button", {
name: "primary-shadow",
await test.step("Do not remap and break the reference", async () => {
await renameToken(page, "foundation-shadow", "shadow-base");
await expect(remappingModal).toBeVisible({ timeout: 5000 });
await remappingModal
.getByRole("button", { name: "don't remap" })
.click();
await expect(
tokensSidebar.getByRole("button", { name: "shadow-base" }),
).toBeVisible();
await expect(
tokensSidebar.locator('[aria-label="Missing reference"]'),
).toBeVisible();
});
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",
"textbox",
"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",
"textbox",
"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();
// This proves the remapping worked and the value update propagated through the reference
const tokenPillSidebar = workspacePage.rightSidebar.getByLabel('paragraph-style')
await expect(tokenPillSidebar).toBeVisible();
});
});
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,
}) => {
@ -552,98 +254,6 @@ test.describe("Remapping a single token", () => {
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",
"textbox",
"#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",
"textbox",
"#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", () => {
@ -750,107 +360,4 @@ test.describe("Remapping group of tokens", () => {
fillSection.getByLabel("brand.primary", { exact: true }),
).toBeVisible();
});
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",
"textbox",
"#FFFFFF",
);
await createToken(
page,
"Color",
"light.secondary",
"Value",
"textbox",
"#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();
});
});