From bcedcf7dbf934c529ab99097404df481c122f628 Mon Sep 17 00:00:00 2001 From: Ali Stump Date: Mon, 29 Jul 2024 10:35:50 -0700 Subject: [PATCH] chore(themed): add token CSS variable test helper (#9860) **Related Issue:** #7180 ## Summary Includes helpers which assist developers in testing and theming component tokens as CSS variables. ### EXAMPLE USAGE: #### Demo page ```html Button ``` #### Storybook / Chromatic ```ts export const theming_TestOnly = (): string => html`
Button
`; ``` #### E2E ```ts describe("theme", () => { themed(` Button `, { "--calcite-button-background-color": { shadowSelector: ".button", targetProp: "backgroundColor", }, "--calcite-button-text-color": { shadowSelector: ".button", targetProp: "color", } "--calcite-accordion-border-color": { shadowSelector: ".button", targetProp: "borderColor", }, }); }); ``` --- .../src/demos/_assets/demo-theme.ts | 112 ++++++++++++++++++ .../src/demos/_assets/head.ts | 4 + .../src/tests/commonTests/themed.ts | 21 +--- .../src/tests/utils/cssTokenValues.ts | 47 ++++++++ 4 files changed, 165 insertions(+), 19 deletions(-) create mode 100644 packages/calcite-components/src/demos/_assets/demo-theme.ts create mode 100644 packages/calcite-components/src/tests/utils/cssTokenValues.ts diff --git a/packages/calcite-components/src/demos/_assets/demo-theme.ts b/packages/calcite-components/src/demos/_assets/demo-theme.ts new file mode 100644 index 00000000000..55c02899401 --- /dev/null +++ b/packages/calcite-components/src/demos/_assets/demo-theme.ts @@ -0,0 +1,112 @@ +/** + * + * Sets the value of a CSS variable to a test value. + * This is useful for testing themed components. + * + * @param token - the token as a CSS variable + * @returns string - the new value for the token + */ +export function getTokenValue(token: string): string { + const tokenValueMap = { + background$: "rgb(252, 244, 52)", + "text-color$": "rgb(239,118,39)", + "border-color$": "rgb(156, 89, 209)", + "background-color$": "rgb(44, 44, 44)", + color$: "rgb(0, 191, 255)", + highlighted$: "rgb(255, 105, 180)", + selected$: "rgb(255, 255, 255)", + shadow$: + "rgb(255, 255, 255) 0px 0px 0px 4px, rgb(255, 105, 180) 0px 0px 0px 5px inset, rgb(0, 191, 255) 0px 0px 0px 9px", + "z-index$": "42", + "(size|space)$": "42px", + } as const; + + const match = Object.entries(tokenValueMap).find(([regexStr]) => { + return new RegExp(regexStr, "g").test(token); + }); + + if (!match) { + console.warn("token not found in tokenValueMap", token); + return tokenValueMap["color$"]; + } + + return match[1]; +} + +/* + * @prop tokens - an array of CSS variables + * @returns a string of CSS variables with their new values. + */ +export function setCSSVariables(tokens: string[]): string { + return tokens + .map((token) => { + return `${token}: ${getTokenValue(token)};`; + }) + .join("\n"); +} + +/** + * + * @example + * Button + */ +export class DemoTheme extends HTMLElement { + _slot: HTMLSlotElement; + + _el: HTMLElement; + + static observedAttributes = ["tokens"]; + + constructor() { + super(); + const shadow = this.attachShadow({ mode: "open" }); + const slot = document.createElement("slot"); + shadow.append(slot); + this._slot = slot; + if (this._slot.assignedNodes().length === 1 && this._slot.assignedNodes()[0].nodeName.includes("calcite")) { + this._el = this._slot.assignedNodes()[0] as HTMLElement; + } + } + + attributeChangedCallback(name: string, oldValue: string, newValue: string): void { + if (newValue !== oldValue && name === "tokens") { + this.updateTheme(newValue); + } + } + + updateTheme(newValue: string): void { + if (typeof newValue === "string") { + let tokensList; + + try { + tokensList = JSON.parse(newValue); + } catch (error) { + tokensList = newValue.split(",").map((t) => t.trim()); + } + + if (Array.isArray(tokensList)) { + const stringifiedTheme = setCSSVariables(tokensList); + + if (this._el) { + this._el.style.cssText = stringifiedTheme; + } else { + this.setAttribute("style", stringifiedTheme); + } + } + } + } +} + +customElements.define("demo-theme", DemoTheme); diff --git a/packages/calcite-components/src/demos/_assets/head.ts b/packages/calcite-components/src/demos/_assets/head.ts index eeda4a8ab55..b82226531b6 100644 --- a/packages/calcite-components/src/demos/_assets/head.ts +++ b/packages/calcite-components/src/demos/_assets/head.ts @@ -18,6 +18,10 @@ { src: "demos/_assets/demo-dom-swapper.js", }, + { + src: "demos/_assets/demo-theme.js", + type: "module", + }, ]; const parseTemplate = (text: string): HTMLTemplateElement | null => { diff --git a/packages/calcite-components/src/tests/commonTests/themed.ts b/packages/calcite-components/src/tests/commonTests/themed.ts index 00524d01382..363577eb8f4 100644 --- a/packages/calcite-components/src/tests/commonTests/themed.ts +++ b/packages/calcite-components/src/tests/commonTests/themed.ts @@ -2,6 +2,7 @@ import { E2EElement, E2EPage } from "@stencil/core/testing"; import { toHaveNoViolations } from "jest-axe"; import { ElementHandle } from "puppeteer"; import type { RequireExactlyOne } from "type-fest"; +import { getTokenValue } from "../utils/cssTokenValues"; import type { ComponentTestSetup } from "./interfaces"; import { getTagAndPage } from "./utils"; @@ -101,7 +102,7 @@ export function themed(componentTestSetup: ComponentTestSetup, tokens: Component // Set test values for each token if (!setTokens[token]) { - setTokens[token] = assignTestTokenThemeValues(token); + setTokens[token] = getTokenValue(token); } // Set up styleTargets and testTargets @@ -426,21 +427,3 @@ async function assertThemedProps(page: E2EPage, options: TestTarget): Promise { + return new RegExp(regexStr, "g").test(token); + }); + + if (!match) { + console.warn("token not found in tokenValueMap", token); + return tokenValueMap["color$"]; + } + + return match[1]; +} + +/** + * + * @param tokens - an array of CSS variables + * @returns a string of CSS variables with their new values. + */ +export function setCSSVariables(tokens: string[]): string { + return tokens + .map((token) => { + return `${token}: ${getTokenValue(token)};`; + }) + .join("\n"); +}