From 2a9472c0a997cc86558d6117555f330ec375282c Mon Sep 17 00:00:00 2001 From: Ali Stump Date: Mon, 25 Mar 2024 15:27:59 -0700 Subject: [PATCH 01/11] chore(common): add themed common test --- .../src/tests/commonTests.ts | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) diff --git a/packages/calcite-components/src/tests/commonTests.ts b/packages/calcite-components/src/tests/commonTests.ts index 02e658744f7..46500e3dd24 100644 --- a/packages/calcite-components/src/tests/commonTests.ts +++ b/packages/calcite-components/src/tests/commonTests.ts @@ -1839,3 +1839,158 @@ export function openClose(componentTagOrHTML: TagOrHTML, options?: OpenCloseOpti }); } } + +export async function themed( + componentTagOrHTML: TagOrHTML, + tokens: string[], + componentName?: string, +): Promise<{ page: E2EPage; component: E2EElement; themedTokens: Record }> { + let colorList: string[] = [ + rgb(252, 88, 159), + rgb(193, 54, 91), + rgb(252, 42, 5), + rgb(252, 50, 121), + rgb(206, 35, 75), + rgb(153, 37, 29), + rgb(234, 105, 103), + rgb(219, 80, 52), + rgb(153, 7, 60), + rgb(224, 74, 134), + rgb(168, 43, 62), + rgb(252, 20, 24), + rgb(186, 1, 35), + rgb(255, 108, 63), + rgb(204, 63, 51), + rgb(216, 108, 41), + rgb(249, 192, 4), + rgb(252, 164, 22), + rgb(244, 150, 95), + rgb(226, 164, 93), + rgb(232, 192, 83), + rgb(237, 189, 106), + rgb(244, 147, 90), + rgb(204, 121, 32), + rgb(216, 131, 88), + rgb(221, 148, 93), + rgb(255, 144, 96), + rgb(242, 124, 33), + rgb(196, 69, 5), + rgb(237, 137, 87), + rgb(229, 194, 41), + rgb(252, 244, 95), + rgb(241, 244, 36), + rgb(216, 189, 54), + rgb(229, 218, 64), + rgb(252, 235, 106), + rgb(226, 220, 102), + rgb(234, 227, 98), + rgb(244, 244, 4), + rgb(229, 212, 100), + rgb(226, 216, 63), + rgb(247, 214, 81), + rgb(247, 232, 64), + rgb(224, 195, 80), + rgb(242, 230, 106), + rgb(13, 232, 199), + rgb(102, 160, 9), + rgb(4, 158, 45), + rgb(29, 193, 97), + rgb(6, 232, 127), + rgb(88, 181, 30), + rgb(115, 175, 31), + rgb(43, 229, 114), + rgb(106, 252, 95), + rgb(66, 255, 166), + rgb(101, 221, 95), + rgb(114, 255, 240), + rgb(188, 221, 88), + rgb(185, 219, 15), + rgb(64, 209, 187), + rgb(76, 119, 173), + rgb(74, 124, 181), + rgb(4, 54, 204), + rgb(11, 170, 188), + rgb(128, 98, 219), + rgb(3, 135, 150), + rgb(92, 214, 212), + rgb(46, 136, 232), + rgb(86, 70, 168), + rgb(32, 75, 173), + rgb(28, 131, 165), + rgb(99, 99, 221), + rgb(105, 177, 244), + rgb(27, 112, 119), + rgb(78, 197, 252), + rgb(57, 10, 168), + rgb(172, 54, 226), + rgb(152, 75, 252), + rgb(158, 15, 224), + rgb(117, 0, 196), + rgb(56, 10, 119), + rgb(139, 90, 237), + rgb(116, 65, 198), + rgb(90, 11, 130), + rgb(98, 18, 135), + rgb(135, 38, 181), + rgb(113, 61, 211), + rgb(163, 29, 247), + rgb(74, 27, 145), + rgb(128, 79, 188), + rgb(216, 0, 255), + rgb(221, 88, 175), + rgb(249, 29, 187), + rgb(244, 78, 172), + rgb(242, 77, 168), + rgb(249, 49, 136), + rgb(239, 57, 239), + rgb(234, 42, 212), + rgb(242, 94, 215), + rgb(211, 74, 177), + rgb(206, 24, 219), + rgb(207, 41, 244), + rgb(242, 107, 249), + rgb(226, 6, 190), + rgb(214, 40, 237), + ]; + + function shuffle(array) { + let currentIndex = array.length; + let temporaryValue; + let randomIndex; + + // While there remain elements to shuffle... + while (0 !== currentIndex) { + // Pick a remaining element... + randomIndex = Math.floor(Math.random() * currentIndex); + currentIndex -= 1; + + // And swap it with the current element. + temporaryValue = array[currentIndex]; + array[currentIndex] = array[randomIndex]; + array[randomIndex] = temporaryValue; + } + + return array; + } + + colorList = shuffle(colorList); + + const page = await simplePageSetup(componentTagOrHTML); + const tag = getTag(isHTML(componentTagOrHTML) ? componentName : componentTagOrHTML); + const component = await page.find(tag); + const themedTokens = {}; + + tokens.forEach((token, i) => { + themedTokens[token] = colorList[i]; + }); + + component.setAttribute( + "style", + Object.entries(themedTokens) + .map((k, v) => `${k}: ${v}`) + .join("; "), + ); + await page.waitForChanges(); + + return { page, component, themedTokens }; +} From e1d204fee307b7beff30e94d3279cb83810b6b2b Mon Sep 17 00:00:00 2001 From: Ali Stump Date: Mon, 25 Mar 2024 15:32:01 -0700 Subject: [PATCH 02/11] chore(common): add non-color values for tokenList in themed --- packages/calcite-components/src/tests/commonTests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/calcite-components/src/tests/commonTests.ts b/packages/calcite-components/src/tests/commonTests.ts index 46500e3dd24..9104a160aa3 100644 --- a/packages/calcite-components/src/tests/commonTests.ts +++ b/packages/calcite-components/src/tests/commonTests.ts @@ -1981,7 +1981,7 @@ export async function themed( const themedTokens = {}; tokens.forEach((token, i) => { - themedTokens[token] = colorList[i]; + themedTokens[token] = token.includes("color") ? colorList[i] : `${i * 10}${token.includes("z-index") ? "" : "px"}`; }); component.setAttribute( From 2e0e1d8411f171ec3f7ea2484108bbc1e5fe054c Mon Sep 17 00:00:00 2001 From: Ali Stump Date: Tue, 26 Mar 2024 16:52:00 -0700 Subject: [PATCH 03/11] chore: update common theme-ing tests --- .../src/tests/commonTests.ts | 200 +++++------------- .../calcite-components/src/tests/utils.ts | 30 +++ 2 files changed, 84 insertions(+), 146 deletions(-) diff --git a/packages/calcite-components/src/tests/commonTests.ts b/packages/calcite-components/src/tests/commonTests.ts index 9104a160aa3..77b62ee17ab 100644 --- a/packages/calcite-components/src/tests/commonTests.ts +++ b/packages/calcite-components/src/tests/commonTests.ts @@ -11,6 +11,8 @@ import { MessageBundle } from "../utils/t9n"; import { GlobalTestProps, IntrinsicElementsWithProp, + assertThemedProps, + assignTestTokenThemeValues, isElementFocused, newProgrammaticE2EPage, skipAnimations, @@ -1840,157 +1842,63 @@ export function openClose(componentTagOrHTML: TagOrHTML, options?: OpenCloseOpti } } -export async function themed( +/** + * + * @param componentTagOrHTML - + * @param tokens + */ +export function themed( componentTagOrHTML: TagOrHTML, - tokens: string[], - componentName?: string, -): Promise<{ page: E2EPage; component: E2EElement; themedTokens: Record }> { - let colorList: string[] = [ - rgb(252, 88, 159), - rgb(193, 54, 91), - rgb(252, 42, 5), - rgb(252, 50, 121), - rgb(206, 35, 75), - rgb(153, 37, 29), - rgb(234, 105, 103), - rgb(219, 80, 52), - rgb(153, 7, 60), - rgb(224, 74, 134), - rgb(168, 43, 62), - rgb(252, 20, 24), - rgb(186, 1, 35), - rgb(255, 108, 63), - rgb(204, 63, 51), - rgb(216, 108, 41), - rgb(249, 192, 4), - rgb(252, 164, 22), - rgb(244, 150, 95), - rgb(226, 164, 93), - rgb(232, 192, 83), - rgb(237, 189, 106), - rgb(244, 147, 90), - rgb(204, 121, 32), - rgb(216, 131, 88), - rgb(221, 148, 93), - rgb(255, 144, 96), - rgb(242, 124, 33), - rgb(196, 69, 5), - rgb(237, 137, 87), - rgb(229, 194, 41), - rgb(252, 244, 95), - rgb(241, 244, 36), - rgb(216, 189, 54), - rgb(229, 218, 64), - rgb(252, 235, 106), - rgb(226, 220, 102), - rgb(234, 227, 98), - rgb(244, 244, 4), - rgb(229, 212, 100), - rgb(226, 216, 63), - rgb(247, 214, 81), - rgb(247, 232, 64), - rgb(224, 195, 80), - rgb(242, 230, 106), - rgb(13, 232, 199), - rgb(102, 160, 9), - rgb(4, 158, 45), - rgb(29, 193, 97), - rgb(6, 232, 127), - rgb(88, 181, 30), - rgb(115, 175, 31), - rgb(43, 229, 114), - rgb(106, 252, 95), - rgb(66, 255, 166), - rgb(101, 221, 95), - rgb(114, 255, 240), - rgb(188, 221, 88), - rgb(185, 219, 15), - rgb(64, 209, 187), - rgb(76, 119, 173), - rgb(74, 124, 181), - rgb(4, 54, 204), - rgb(11, 170, 188), - rgb(128, 98, 219), - rgb(3, 135, 150), - rgb(92, 214, 212), - rgb(46, 136, 232), - rgb(86, 70, 168), - rgb(32, 75, 173), - rgb(28, 131, 165), - rgb(99, 99, 221), - rgb(105, 177, 244), - rgb(27, 112, 119), - rgb(78, 197, 252), - rgb(57, 10, 168), - rgb(172, 54, 226), - rgb(152, 75, 252), - rgb(158, 15, 224), - rgb(117, 0, 196), - rgb(56, 10, 119), - rgb(139, 90, 237), - rgb(116, 65, 198), - rgb(90, 11, 130), - rgb(98, 18, 135), - rgb(135, 38, 181), - rgb(113, 61, 211), - rgb(163, 29, 247), - rgb(74, 27, 145), - rgb(128, 79, 188), - rgb(216, 0, 255), - rgb(221, 88, 175), - rgb(249, 29, 187), - rgb(244, 78, 172), - rgb(242, 77, 168), - rgb(249, 49, 136), - rgb(239, 57, 239), - rgb(234, 42, 212), - rgb(242, 94, 215), - rgb(211, 74, 177), - rgb(206, 24, 219), - rgb(207, 41, 244), - rgb(242, 107, 249), - rgb(226, 6, 190), - rgb(214, 40, 237), - ]; - - function shuffle(array) { - let currentIndex = array.length; - let temporaryValue; - let randomIndex; - - // While there remain elements to shuffle... - while (0 !== currentIndex) { - // Pick a remaining element... - randomIndex = Math.floor(Math.random() * currentIndex); - currentIndex -= 1; - - // And swap it with the current element. - temporaryValue = array[currentIndex]; - array[currentIndex] = array[randomIndex]; - array[randomIndex] = temporaryValue; - } + tokens: Record, +): void { + it("is theme-able", async () => { + const page = await simplePageSetup(componentTagOrHTML); + const expectChecklist: Record< + string, + [E2EElement, Record]>] + > = {}; + + for (const token in tokens) { + const { selector, shadowSelector, targetProp } = tokens[token]; + const themedTokenValue = assignTestTokenThemeValues(token); + if (!expectChecklist[selector]) { + const el = await page.find(selector); + expectChecklist[selector] = [el, {}]; + } - return array; - } + const [el, selectors] = expectChecklist[selector]; + + if (!selectors[shadowSelector || selector]) { + const target = shadowSelector ? await page.find(`${selector} >>> ${shadowSelector}`) : el; + selectors[shadowSelector || selector] = [target, { [token]: [targetProp, themedTokenValue] }]; + } else { + const [, targetProps] = selectors[shadowSelector || selector]; + targetProps[token] = [targetProp, themedTokenValue]; + } + } - colorList = shuffle(colorList); + // let all page.find calls resolve + await page.waitForChanges(); - const page = await simplePageSetup(componentTagOrHTML); - const tag = getTag(isHTML(componentTagOrHTML) ? componentName : componentTagOrHTML); - const component = await page.find(tag); - const themedTokens = {}; + for (const selector in expectChecklist) { + const [el, selectors] = expectChecklist[selector]; + const style = Object.entries(selectors).flatMap(([, targetProps]) => + Object.entries(targetProps[1]).map(([token, [, themedTokenValue]]) => `${token}: ${themedTokenValue}`), + ); + // Sets the style of each element to a string of CSS token props with themed token values + el.setAttribute("style", style.join("; ")); + } - tokens.forEach((token, i) => { - themedTokens[token] = token.includes("color") ? colorList[i] : `${i * 10}${token.includes("z-index") ? "" : "px"}`; - }); + // let all el.setAttribute calls resolve + await page.waitForChanges(); - component.setAttribute( - "style", - Object.entries(themedTokens) - .map((k, v) => `${k}: ${v}`) - .join("; "), - ); - await page.waitForChanges(); + for (const elSelector in expectChecklist) { + const [, selectors] = expectChecklist[elSelector]; - return { page, component, themedTokens }; + for (const targetSelector in selectors) { + const [target, targetProps] = selectors[targetSelector]; + await assertThemedProps(target, Object.values(targetProps)); + } + } + }); } diff --git a/packages/calcite-components/src/tests/utils.ts b/packages/calcite-components/src/tests/utils.ts index 3c15a156143..b8b70bf8f88 100644 --- a/packages/calcite-components/src/tests/utils.ts +++ b/packages/calcite-components/src/tests/utils.ts @@ -425,3 +425,33 @@ export function toBeNumber(): any { }, }; } + +/** + * Get the computed style of an element and assert that it matches the expected themed token value. + * This is useful for testing themed components. + * + * @param target - the element to get the computed style from + * @param props - an array of tuples where the first value is the CSS property to check and the second value is the expected themed token value + */ +export async function assertThemedProps(target: E2EElement, props: [string, string][]): Promise { + const styles = await target.getComputedStyle(); + for (const [targetProp, themedTokenValue] of props) { + expect(styles[targetProp]).toBe(themedTokenValue); + } +} + +/** + * + * 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 assignTestTokenThemeValues(token: string): string { + return token.includes("color") + ? "rgb(0, 191, 255)" + : token.includes("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" + : `42${token.includes("z-index") ? "" : "px"}`; +} From ec2ec647171f710d33a5141c38c4078b00190d16 Mon Sep 17 00:00:00 2001 From: Ali Stump Date: Wed, 27 Mar 2024 12:26:56 -0700 Subject: [PATCH 04/11] test: allow theme tests to penetrate subcomponent shadow dom --- .../src/tests/commonTests.ts | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/packages/calcite-components/src/tests/commonTests.ts b/packages/calcite-components/src/tests/commonTests.ts index 77b62ee17ab..c1a1d418fdb 100644 --- a/packages/calcite-components/src/tests/commonTests.ts +++ b/packages/calcite-components/src/tests/commonTests.ts @@ -1869,12 +1869,32 @@ export function themed( const [el, selectors] = expectChecklist[selector]; if (!selectors[shadowSelector || selector]) { - const target = shadowSelector ? await page.find(`${selector} >>> ${shadowSelector}`) : el; - selectors[shadowSelector || selector] = [target, { [token]: [targetProp, themedTokenValue] }]; - } else { - const [, targetProps] = selectors[shadowSelector || selector]; - targetProps[token] = [targetProp, themedTokenValue]; + let target: E2EElement; + + if (shadowSelector && shadowSelector.includes(">>>")) { + const shadowSelectors = shadowSelector.split(" "); + + for (let i = 0; i < shadowSelectors.length; i++) { + const s = shadowSelectors[i]; + + if (i === 0) { + target = await page.find(`${selector} >>> ${s}`); + } else if (shadowSelectors[i + 1] === ">>>") { + target = await target.find(`${s} >>> ${shadowSelectors[i + 2]}`); + i += 2; + } else { + target = await target.find(s); + } + } + } else { + target = shadowSelector ? await page.find(`${selector} >>> ${shadowSelector}`) : el; + } + + selectors[shadowSelector || selector] = [target, {}]; } + + const [, targetProps] = selectors[shadowSelector || selector]; + targetProps[token] = [targetProp, themedTokenValue]; } // let all page.find calls resolve From c2244dafd25f49526000150dd1c469935565c509 Mon Sep 17 00:00:00 2001 From: Ali Stump Date: Mon, 1 Apr 2024 12:03:21 -0700 Subject: [PATCH 05/11] tests: test theme-ing for stateful tokens --- .../src/tests/commonTests.ts | 27 ++++-- .../calcite-components/src/tests/utils.ts | 90 ++++++++++++++++++- 2 files changed, 109 insertions(+), 8 deletions(-) diff --git a/packages/calcite-components/src/tests/commonTests.ts b/packages/calcite-components/src/tests/commonTests.ts index c1a1d418fdb..48e78eb6e92 100644 --- a/packages/calcite-components/src/tests/commonTests.ts +++ b/packages/calcite-components/src/tests/commonTests.ts @@ -1849,17 +1849,34 @@ export function openClose(componentTagOrHTML: TagOrHTML, options?: OpenCloseOpti */ export function themed( componentTagOrHTML: TagOrHTML, - tokens: Record, + tokens: Record< + string, + { + selector: string; + shadowSelector?: string; + targetProp: string; + state?: string | Record; + } + >, ): void { it("is theme-able", async () => { const page = await simplePageSetup(componentTagOrHTML); const expectChecklist: Record< string, - [E2EElement, Record]>] + [ + E2EElement, + Record< + string, + [ + E2EElement, + Record | undefined]>, + ] + >, + ] > = {}; for (const token in tokens) { - const { selector, shadowSelector, targetProp } = tokens[token]; + const { selector, shadowSelector, targetProp, state } = tokens[token]; const themedTokenValue = assignTestTokenThemeValues(token); if (!expectChecklist[selector]) { const el = await page.find(selector); @@ -1894,7 +1911,7 @@ export function themed( } const [, targetProps] = selectors[shadowSelector || selector]; - targetProps[token] = [targetProp, themedTokenValue]; + targetProps[token] = [targetProp, themedTokenValue, state]; } // let all page.find calls resolve @@ -1917,7 +1934,7 @@ export function themed( for (const targetSelector in selectors) { const [target, targetProps] = selectors[targetSelector]; - await assertThemedProps(target, Object.values(targetProps)); + await assertThemedProps(page, target, Object.values(targetProps)); } } }); diff --git a/packages/calcite-components/src/tests/utils.ts b/packages/calcite-components/src/tests/utils.ts index b8b70bf8f88..7e57d76f8da 100644 --- a/packages/calcite-components/src/tests/utils.ts +++ b/packages/calcite-components/src/tests/utils.ts @@ -430,13 +430,97 @@ export function toBeNumber(): any { * Get the computed style of an element and assert that it matches the expected themed token value. * This is useful for testing themed components. * + * @param page - the e2e page * @param target - the element to get the computed style from + * @param selector - the selector of the target element * @param props - an array of tuples where the first value is the CSS property to check and the second value is the expected themed token value */ -export async function assertThemedProps(target: E2EElement, props: [string, string][]): Promise { +export async function assertThemedProps( + page: E2EPage, + target: E2EElement, + props: [string, string, string | Record | undefined][], +): Promise { const styles = await target.getComputedStyle(); - for (const [targetProp, themedTokenValue] of props) { - expect(styles[targetProp]).toBe(themedTokenValue); + for (const [targetProp, themedTokenValue, state] of props) { + if (state) { + if (typeof state === "string") { + await target[state](); + await page.waitForChanges(); + } else { + const [stateName, { attribute, value }] = Object.entries(state)[0]; + const rect = (await page.evaluate( + (attribute: string, value: string | RegExp) => { + const searchInShadowDom = (node: Node): T | undefined => { + if (node.nodeType === 1) { + const attr = (node as Element).getAttribute(attribute); + if (typeof value === "string" && attr === value) { + return node; + } + if (value instanceof RegExp && attr && value.test(attr)) { + return node ?? undefined; + } + if ((node as Element).getAttribute(attribute) === value) { + return node; + } + + if ((node as Element) && !attribute && !value) { + return node; + } + } + + if (node.nodeType === 1 && (node as Element).shadowRoot) { + for (const child of ((node as Element).shadowRoot as ShadowRoot).children) { + const result = searchInShadowDom(child); + if (result) { + return result; + } + } + } + + for (const child of node.childNodes) { + const result = searchInShadowDom(child); + if (result) { + return result; + } + } + }; + return new Promise<{ width: number; height: number; left: number; top: number } | undefined>((resolve) => { + requestAnimationFrame(() => { + const foundNode = searchInShadowDom(document); + if (foundNode && foundNode.getBoundingClientRect) { + const { width, height, left, top } = foundNode.getBoundingClientRect(); + resolve({ width, height, left, top }); + } else { + resolve(undefined); + } + }); + }); + }, + attribute, + value, + )) as { width: number; height: number; left: number; top: number } | undefined; + + const box = { + x: rect.left + rect.width / 2, + y: rect.top + rect.height / 2, + }; + await page.mouse.move(box.x, box.y); + + if (stateName === "press") { + await page.mouse.down(); + } + if (stateName === "focus") { + await page.mouse.up(); + } + await page.waitForChanges(); + } + + const statefulStyles = await target.getComputedStyle(); + expect(statefulStyles[targetProp]).toBe(themedTokenValue); + } else { + expect(styles[targetProp]).toBe(themedTokenValue); + } + page.mouse.reset(); } } From e777ea3a11643837f26927350f81408118254d53 Mon Sep 17 00:00:00 2001 From: Ali Stump Date: Mon, 1 Apr 2024 12:17:32 -0700 Subject: [PATCH 06/11] docs: add jsdocs to common test utils --- .../src/tests/commonTests.ts | 36 +++++++++++++++++-- .../calcite-components/src/tests/utils.ts | 1 + 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/calcite-components/src/tests/commonTests.ts b/packages/calcite-components/src/tests/commonTests.ts index 48e78eb6e92..da95cbb51b1 100644 --- a/packages/calcite-components/src/tests/commonTests.ts +++ b/packages/calcite-components/src/tests/commonTests.ts @@ -1844,8 +1844,40 @@ export function openClose(componentTagOrHTML: TagOrHTML, options?: OpenCloseOpti /** * - * @param componentTagOrHTML - - * @param tokens + * Helper to test custom theming of a component's associated tokens. + * + * @example + * describe("default", () => { + * const tokens = { + * "--calcite-action-bar-trigger-background-color": { + * selector: "calcite-action-bar", + * shadowSelector: "calcite-action-group calcite-action >>> .button", + * targetProp: "backgroundColor", + * }, + * "--calcite-action-bar-trigger-background-color-active": { + * selector: "calcite-action-bar", + * shadowSelector: "calcite-action-group calcite-action >>> .button", + * targetProp: "backgroundColor", + * state: { press: { attribute: "class", value: CSS.expandToggle } }, + * }, + * "--calcite-action-bar-trigger-background-color-focus": { + * selector: "calcite-action-bar", + * shadowSelector: "calcite-action-group calcite-action >>> .button", + * targetProp: "backgroundColor", + * state: "focus", + * }, + * "--calcite-action-bar-trigger-background-color-hover": { + * selector: "calcite-action-bar", + * shadowSelector: "calcite-action-group calcite-action >>> .button", + * targetProp: "backgroundColor", + * state: "hover", + * }, + * }; + * themed(`calcite-action-bar`, tokens); + * }); + * + * @param componentTagOrHTML - The component tag or HTML markup to test against. + * @param tokens - A record of token names and their associated selectors, shadow selectors, target props, and states. */ export function themed( componentTagOrHTML: TagOrHTML, diff --git a/packages/calcite-components/src/tests/utils.ts b/packages/calcite-components/src/tests/utils.ts index 7e57d76f8da..83fabfbc0ab 100644 --- a/packages/calcite-components/src/tests/utils.ts +++ b/packages/calcite-components/src/tests/utils.ts @@ -520,6 +520,7 @@ export async function assertThemedProps( } else { expect(styles[targetProp]).toBe(themedTokenValue); } + // reset the mouse state to ensure each "state" starts with a clean slate page.mouse.reset(); } } From 6b78dfbf1e521ebb93a3837db14b9dc0efae7816 Mon Sep 17 00:00:00 2001 From: Ali Stump Date: Mon, 1 Apr 2024 15:10:11 -0700 Subject: [PATCH 07/11] refactor: test utils to simplify assertThemedProps --- packages/calcite-components/src/tests/utils.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/calcite-components/src/tests/utils.ts b/packages/calcite-components/src/tests/utils.ts index 83fabfbc0ab..71944090194 100644 --- a/packages/calcite-components/src/tests/utils.ts +++ b/packages/calcite-components/src/tests/utils.ts @@ -440,7 +440,7 @@ export async function assertThemedProps( target: E2EElement, props: [string, string, string | Record | undefined][], ): Promise { - const styles = await target.getComputedStyle(); + let styles = await target.getComputedStyle(); for (const [targetProp, themedTokenValue, state] of props) { if (state) { if (typeof state === "string") { @@ -515,13 +515,13 @@ export async function assertThemedProps( await page.waitForChanges(); } - const statefulStyles = await target.getComputedStyle(); - expect(statefulStyles[targetProp]).toBe(themedTokenValue); - } else { - expect(styles[targetProp]).toBe(themedTokenValue); + styles = await target.getComputedStyle(); + // reset the mouse state to ensure each "state" starts with a clean slate + page.mouse.reset(); } - // reset the mouse state to ensure each "state" starts with a clean slate - page.mouse.reset(); + + await page.waitForChanges(); + expect(Object.is(styles[targetProp], themedTokenValue)).toBe(true); } } From 377dc68ade2e5a6508a1bc6a1c6fdcf91f1bf407 Mon Sep 17 00:00:00 2001 From: Ali Stump Date: Wed, 3 Apr 2024 15:34:57 -0700 Subject: [PATCH 08/11] refactor(common): simplify theme tests to be more readable --- .../src/tests/commonTests.ts | 265 +++++++++++++----- .../calcite-components/src/tests/utils.ts | 112 +------- 2 files changed, 210 insertions(+), 167 deletions(-) diff --git a/packages/calcite-components/src/tests/commonTests.ts b/packages/calcite-components/src/tests/commonTests.ts index da95cbb51b1..82d762e1694 100644 --- a/packages/calcite-components/src/tests/commonTests.ts +++ b/packages/calcite-components/src/tests/commonTests.ts @@ -11,8 +11,8 @@ import { MessageBundle } from "../utils/t9n"; import { GlobalTestProps, IntrinsicElementsWithProp, - assertThemedProps, assignTestTokenThemeValues, + isArray, isElementFocused, newProgrammaticE2EPage, skipAnimations, @@ -1847,13 +1847,16 @@ export function openClose(componentTagOrHTML: TagOrHTML, options?: OpenCloseOpti * Helper to test custom theming of a component's associated tokens. * * @example - * describe("default", () => { + * describe("theme", () => { * const tokens = { - * "--calcite-action-bar-trigger-background-color": { + * "--calcite-action-bar-trigger-background-color": [{ + * selector: "calcite-action-bar", + * targetProp: "backgroundColor", + * }, { * selector: "calcite-action-bar", * shadowSelector: "calcite-action-group calcite-action >>> .button", * targetProp: "backgroundColor", - * }, + * }], * "--calcite-action-bar-trigger-background-color-active": { * selector: "calcite-action-bar", * shadowSelector: "calcite-action-group calcite-action >>> .button", @@ -1881,93 +1884,219 @@ export function openClose(componentTagOrHTML: TagOrHTML, options?: OpenCloseOpti */ export function themed( componentTagOrHTML: TagOrHTML, - tokens: Record< - string, - { - selector: string; - shadowSelector?: string; - targetProp: string; - state?: string | Record; - } - >, + tokens: Record, ): void { - it("is theme-able", async () => { + it("is themeable", async () => { const page = await simplePageSetup(componentTagOrHTML); - const expectChecklist: Record< - string, - [ - E2EElement, - Record< - string, - [ - E2EElement, - Record | undefined]>, - ] - >, - ] - > = {}; + const setTokens: Record = {}; + const styleTargets: Record = {}; + const testTargets: TestTarget[] = []; + // Parse test config for tokens and selectors for (const token in tokens) { - const { selector, shadowSelector, targetProp, state } = tokens[token]; - const themedTokenValue = assignTestTokenThemeValues(token); - if (!expectChecklist[selector]) { - const el = await page.find(selector); - expectChecklist[selector] = [el, {}]; - } + let selectors = tokens[token]; - const [el, selectors] = expectChecklist[selector]; + if (!isArray(selectors)) { + selectors = [selectors]; + } - if (!selectors[shadowSelector || selector]) { - let target: E2EElement; + // Set test values for each token + if (!setTokens[token]) { + setTokens[token] = assignTestTokenThemeValues(token); + } - if (shadowSelector && shadowSelector.includes(">>>")) { - const shadowSelectors = shadowSelector.split(" "); + // Set up styleTargets and testTargets + for (let i = 0; i < selectors.length; i++) { + const { selector, shadowSelector, targetProp, state } = selectors[i]; + const el = await page.find(selector); + const tokenStyle = `${token}: ${setTokens[token]}`; + let target = el; + let contextSelector = undefined; + let stateName = undefined; - for (let i = 0; i < shadowSelectors.length; i++) { - const s = shadowSelectors[i]; + if (state) { + stateName = typeof state === "string" ? state : Object.keys(state)[0]; + } - if (i === 0) { - target = await page.find(`${selector} >>> ${s}`); - } else if (shadowSelectors[i + 1] === ">>>") { - target = await target.find(`${s} >>> ${shadowSelectors[i + 2]}`); - i += 2; - } else { - target = await target.find(s); + if (!styleTargets[selector]) { + styleTargets[selector] = [el, []]; + } + if (styleTargets[selector][1].indexOf(tokenStyle) === -1) { + styleTargets[selector][1].push(tokenStyle); + } + if (shadowSelector) { + if (shadowSelector.includes(">>>")) { + const shadowSelectors = shadowSelector.split(" "); + + for (let i = 0; i < shadowSelectors.length; i++) { + const s = shadowSelectors[i]; + + if (i === 0) { + target = await page.find(`${selector} >>> ${s}`); + } else if (target && shadowSelectors[i + 1] === ">>>") { + target = await target.find(`${s} >>> ${shadowSelectors[i + 2]}`); + i += 2; + } else if (target) { + target = await target.find(s); + } } + } else { + target = shadowSelector ? await page.find(`${selector} >>> ${shadowSelector}`) : target; } - } else { - target = shadowSelector ? await page.find(`${selector} >>> ${shadowSelector}`) : el; + } + if (state && typeof state !== "string") { + contextSelector = Object.values(state)[0]; } - selectors[shadowSelector || selector] = [target, {}]; + testTargets.push({ target, targetProp, contextSelector, state: stateName, expectedValue: setTokens[token] }); } - - const [, targetProps] = selectors[shadowSelector || selector]; - targetProps[token] = [targetProp, themedTokenValue, state]; } - // let all page.find calls resolve - await page.waitForChanges(); + // set style attribute on styleTargets with the assigned token values + for (const selector in styleTargets) { + const [el, assignedCSSVars] = styleTargets[selector]; - for (const selector in expectChecklist) { - const [el, selectors] = expectChecklist[selector]; - const style = Object.entries(selectors).flatMap(([, targetProps]) => - Object.entries(targetProps[1]).map(([token, [, themedTokenValue]]) => `${token}: ${themedTokenValue}`), - ); // Sets the style of each element to a string of CSS token props with themed token values - el.setAttribute("style", style.join("; ")); + el.setAttribute("style", assignedCSSVars.join("; ")); } - // let all el.setAttribute calls resolve await page.waitForChanges(); - for (const elSelector in expectChecklist) { - const [, selectors] = expectChecklist[elSelector]; + // Assert target computedStyle targetProp matches test theme token color + for (let i = 0; i < testTargets.length; i++) { + await assertThemedProps(page, { ...testTargets[i] }); + } + }); +} + +export type ContextSelectByAttr = { attribute: string; value: string | RegExp }; - for (const targetSelector in selectors) { - const [target, targetProps] = selectors[targetSelector]; - await assertThemedProps(page, target, Object.values(targetProps)); +/** + * Custom type describing a test target for themed components. Use with themed and assertThemedProps. + */ +export type TestTarget = { + target: E2EElement; + contextSelector?: string | ContextSelectByAttr; + targetProp: keyof CSSStyleDeclaration; + state?: string; + expectedValue: string; +}; + +/** + * Custom type describing a test selector for themed components. Use with themed assertThemedProps. + */ +export type TestSelectToken = { + selector: string; + shadowSelector?: string; + targetProp: keyof CSSStyleDeclaration; + state?: string | Record; +}; + +/** + * Get the computed style of an element and assert that it matches the expected themed token value. + * This is useful for testing themed components. + * + * @param page - the e2e page + * @param options - the options to pass to the utility + * @param options.target - the element to get the computed style from + * @param options.contextSelector - the selector of the target element + * @param options.targetProp - the CSSStyleDeclaration property to check + * @param options.state - the state to apply to the target element + * @param options.expectedValue - the expected value of the targetProp + */ +export async function assertThemedProps(page: E2EPage, options: TestTarget): Promise { + const { target, contextSelector, targetProp, state, expectedValue } = options; + let styles = await target.getComputedStyle(); + + if (state) { + if (contextSelector) { + const rect = (await page.evaluate( + ( + context: + | string + | { + attribute: string; + value: string | RegExp; + }, + ) => { + const searchInShadowDom = (node: Node): HTMLElement | SVGElement | Node | undefined => { + const { attribute, value } = context as { + attribute: string; + value: string | RegExp; + }; + if (node.nodeType === 1) { + const attr = (node as Element).getAttribute(attribute); + if (typeof value === "string" && attr === value) { + return node; + } + if (value instanceof RegExp && attr && value.test(attr)) { + return node ?? undefined; + } + if (attr === value) { + return node; + } + + if ((node as Element) && !attribute && !value) { + return node; + } + } + + if (node.nodeType === 1 && (node as Element).shadowRoot) { + for (const child of ((node as Element).shadowRoot as ShadowRoot).children) { + const result = searchInShadowDom(child); + if (result) { + return result; + } + } + } + + for (const child of node.childNodes) { + const result = searchInShadowDom(child); + if (result) { + return result; + } + } + }; + return new Promise<{ width: number; height: number; left: number; top: number } | undefined>((resolve) => { + requestAnimationFrame(() => { + const foundNode = + typeof context === "string" + ? document.querySelector(context) + : (searchInShadowDom(document) as HTMLElement | SVGElement | undefined); + + if (foundNode?.getBoundingClientRect) { + const { width, height, left, top } = foundNode.getBoundingClientRect(); + resolve({ width, height, left, top }); + } else { + resolve(undefined); + } + }); + }); + }, + contextSelector, + )) as { width: number; height: number; left: number; top: number } | undefined; + + const box = { + x: rect.left + rect.width / 2, + y: rect.top + rect.height / 2, + }; + + // hover state + await page.mouse.move(box.x, box.y); + + if (state === "press") { + await page.mouse.down(); + } else if (state === "focus") { + await page.mouse.down(); + await page.mouse.up(); } + } else { + await target[state](); } - }); + await page.waitForChanges(); + styles = await target.getComputedStyle(); + await page.mouse.reset(); + } + await page.waitForChanges(); + expect(Object.is(styles[targetProp], expectedValue)).toBe(true); } diff --git a/packages/calcite-components/src/tests/utils.ts b/packages/calcite-components/src/tests/utils.ts index 71944090194..9b16854cb89 100644 --- a/packages/calcite-components/src/tests/utils.ts +++ b/packages/calcite-components/src/tests/utils.ts @@ -426,105 +426,6 @@ export function toBeNumber(): any { }; } -/** - * Get the computed style of an element and assert that it matches the expected themed token value. - * This is useful for testing themed components. - * - * @param page - the e2e page - * @param target - the element to get the computed style from - * @param selector - the selector of the target element - * @param props - an array of tuples where the first value is the CSS property to check and the second value is the expected themed token value - */ -export async function assertThemedProps( - page: E2EPage, - target: E2EElement, - props: [string, string, string | Record | undefined][], -): Promise { - let styles = await target.getComputedStyle(); - for (const [targetProp, themedTokenValue, state] of props) { - if (state) { - if (typeof state === "string") { - await target[state](); - await page.waitForChanges(); - } else { - const [stateName, { attribute, value }] = Object.entries(state)[0]; - const rect = (await page.evaluate( - (attribute: string, value: string | RegExp) => { - const searchInShadowDom = (node: Node): T | undefined => { - if (node.nodeType === 1) { - const attr = (node as Element).getAttribute(attribute); - if (typeof value === "string" && attr === value) { - return node; - } - if (value instanceof RegExp && attr && value.test(attr)) { - return node ?? undefined; - } - if ((node as Element).getAttribute(attribute) === value) { - return node; - } - - if ((node as Element) && !attribute && !value) { - return node; - } - } - - if (node.nodeType === 1 && (node as Element).shadowRoot) { - for (const child of ((node as Element).shadowRoot as ShadowRoot).children) { - const result = searchInShadowDom(child); - if (result) { - return result; - } - } - } - - for (const child of node.childNodes) { - const result = searchInShadowDom(child); - if (result) { - return result; - } - } - }; - return new Promise<{ width: number; height: number; left: number; top: number } | undefined>((resolve) => { - requestAnimationFrame(() => { - const foundNode = searchInShadowDom(document); - if (foundNode && foundNode.getBoundingClientRect) { - const { width, height, left, top } = foundNode.getBoundingClientRect(); - resolve({ width, height, left, top }); - } else { - resolve(undefined); - } - }); - }); - }, - attribute, - value, - )) as { width: number; height: number; left: number; top: number } | undefined; - - const box = { - x: rect.left + rect.width / 2, - y: rect.top + rect.height / 2, - }; - await page.mouse.move(box.x, box.y); - - if (stateName === "press") { - await page.mouse.down(); - } - if (stateName === "focus") { - await page.mouse.up(); - } - await page.waitForChanges(); - } - - styles = await target.getComputedStyle(); - // reset the mouse state to ensure each "state" starts with a clean slate - page.mouse.reset(); - } - - await page.waitForChanges(); - expect(Object.is(styles[targetProp], themedTokenValue)).toBe(true); - } -} - /** * * Sets the value of a CSS variable to a test value. @@ -540,3 +441,16 @@ export function assignTestTokenThemeValues(token: string): string { ? "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" : `42${token.includes("z-index") ? "" : "px"}`; } + +/** + * Evaluate a passed value to determine if it is an array. + * + * @param value - the value to check + * @returns - a type guard to check if the value is an array + */ +export const isArray = (value: unknown): value is T[] => { + if (value instanceof Array) { + return true; + } + return false; +}; From d8dbc9a533c88df4e36e3ce6616ece81dce025e9 Mon Sep 17 00:00:00 2001 From: Ali Stump Date: Thu, 4 Apr 2024 13:37:01 -0700 Subject: [PATCH 09/11] =?UTF-8?q?docs(common):=20add=20required=20?= =?UTF-8?q?=E2=80=9Cas=20const=E2=80=9D=20to=20example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/calcite-components/src/tests/commonTests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/calcite-components/src/tests/commonTests.ts b/packages/calcite-components/src/tests/commonTests.ts index 82d762e1694..c2d358398a7 100644 --- a/packages/calcite-components/src/tests/commonTests.ts +++ b/packages/calcite-components/src/tests/commonTests.ts @@ -1875,7 +1875,7 @@ export function openClose(componentTagOrHTML: TagOrHTML, options?: OpenCloseOpti * targetProp: "backgroundColor", * state: "hover", * }, - * }; + * } as const; * themed(`calcite-action-bar`, tokens); * }); * From 3e3e2fe20bfc4abfa4e855eac17dbf04534bca2a Mon Sep 17 00:00:00 2001 From: Ali Stump Date: Thu, 4 Apr 2024 13:41:58 -0700 Subject: [PATCH 10/11] test(common): move assignTestTokenThemeValues to commonTests --- .../calcite-components/src/tests/commonTests.ts | 17 ++++++++++++++++- packages/calcite-components/src/tests/utils.ts | 16 ---------------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/calcite-components/src/tests/commonTests.ts b/packages/calcite-components/src/tests/commonTests.ts index c2d358398a7..d63c266c728 100644 --- a/packages/calcite-components/src/tests/commonTests.ts +++ b/packages/calcite-components/src/tests/commonTests.ts @@ -11,7 +11,6 @@ import { MessageBundle } from "../utils/t9n"; import { GlobalTestProps, IntrinsicElementsWithProp, - assignTestTokenThemeValues, isArray, isElementFocused, newProgrammaticE2EPage, @@ -2100,3 +2099,19 @@ export async function assertThemedProps(page: E2EPage, options: TestTarget): Pro await page.waitForChanges(); expect(Object.is(styles[targetProp], expectedValue)).toBe(true); } + +/** + * + * 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 assignTestTokenThemeValues(token: string): string { + return token.includes("color") + ? "rgb(0, 191, 255)" + : token.includes("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" + : `42${token.includes("z-index") ? "" : "px"}`; +} diff --git a/packages/calcite-components/src/tests/utils.ts b/packages/calcite-components/src/tests/utils.ts index 9b16854cb89..847e6a5322a 100644 --- a/packages/calcite-components/src/tests/utils.ts +++ b/packages/calcite-components/src/tests/utils.ts @@ -426,22 +426,6 @@ export function toBeNumber(): any { }; } -/** - * - * 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 assignTestTokenThemeValues(token: string): string { - return token.includes("color") - ? "rgb(0, 191, 255)" - : token.includes("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" - : `42${token.includes("z-index") ? "" : "px"}`; -} - /** * Evaluate a passed value to determine if it is an array. * From 0318e55a0a3c494f5ae4f3b9fd1aa72f1e479619 Mon Sep 17 00:00:00 2001 From: Ali Stump Date: Fri, 5 Apr 2024 17:16:57 -0700 Subject: [PATCH 11/11] =?UTF-8?q?chore(common):=20don=E2=80=99t=20export?= =?UTF-8?q?=20helper=20functions=20for=20common=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/calcite-components/src/tests/commonTests.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/calcite-components/src/tests/commonTests.ts b/packages/calcite-components/src/tests/commonTests.ts index d63c266c728..2d425596fae 100644 --- a/packages/calcite-components/src/tests/commonTests.ts +++ b/packages/calcite-components/src/tests/commonTests.ts @@ -2003,7 +2003,7 @@ export type TestSelectToken = { * @param options.state - the state to apply to the target element * @param options.expectedValue - the expected value of the targetProp */ -export async function assertThemedProps(page: E2EPage, options: TestTarget): Promise { +async function assertThemedProps(page: E2EPage, options: TestTarget): Promise { const { target, contextSelector, targetProp, state, expectedValue } = options; let styles = await target.getComputedStyle(); @@ -2108,7 +2108,7 @@ export async function assertThemedProps(page: E2EPage, options: TestTarget): Pro * @param token - the token as a CSS variable * @returns string - the new value for the token */ -export function assignTestTokenThemeValues(token: string): string { +function assignTestTokenThemeValues(token: string): string { return token.includes("color") ? "rgb(0, 191, 255)" : token.includes("shadow")