From b996208df2781115db7eadd0bb8f4b7c1f403fec Mon Sep 17 00:00:00 2001 From: Maryia Radchuk Date: Tue, 12 Dec 2023 16:19:32 +0000 Subject: [PATCH] feat(pie-tag): DSW-1520 tag component functionality --- .changeset/good-shoes-burn.md | 5 + .changeset/nervous-apricots-enjoy.md | 5 + .changeset/serious-maps-grow.md | 5 + .changeset/ten-mangos-exist.md | 5 + .../stories/pie-tag-docs/variants.mdx | 40 +++++ apps/pie-storybook/stories/pie-tag.stories.ts | 106 +++++++++++-- .../test/component/pie-button.spec.ts | 33 +--- packages/components/pie-tag/README.md | 30 +++- packages/components/pie-tag/src/defs.ts | 22 ++- packages/components/pie-tag/src/index.ts | 40 ++++- packages/components/pie-tag/src/tag.scss | 148 ++++++++++++++++++ .../test/accessibility/pie-tag.spec.ts | 32 +++- .../pie-tag/test/component/pie-tag.spec.ts | 97 +++++++++++- .../pie-tag/test/visual/pie-tag.spec.ts | 71 ++++++++- .../get-shadow-element-style-prop-values.ts | 32 ++++ .../pie-webc-testing/src/helpers/index.ts | 1 + packages/tools/pie-icons-webc/build.js | 6 + 17 files changed, 604 insertions(+), 74 deletions(-) create mode 100644 .changeset/good-shoes-burn.md create mode 100644 .changeset/nervous-apricots-enjoy.md create mode 100644 .changeset/serious-maps-grow.md create mode 100644 .changeset/ten-mangos-exist.md create mode 100644 apps/pie-storybook/stories/pie-tag-docs/variants.mdx create mode 100644 packages/components/pie-webc-testing/src/helpers/get-shadow-element-style-prop-values.ts diff --git a/.changeset/good-shoes-burn.md b/.changeset/good-shoes-burn.md new file mode 100644 index 0000000000..21aeaec8cd --- /dev/null +++ b/.changeset/good-shoes-burn.md @@ -0,0 +1,5 @@ +--- +"@justeattakeaway/pie-webc-testing": minor +--- + +[Added] getShadowElementStylePropValues helper function diff --git a/.changeset/nervous-apricots-enjoy.md b/.changeset/nervous-apricots-enjoy.md new file mode 100644 index 0000000000..ab96671481 --- /dev/null +++ b/.changeset/nervous-apricots-enjoy.md @@ -0,0 +1,5 @@ +--- +"@justeattakeaway/pie-button": minor +--- + +[Removed] getShadowElementStylePropValues function from button component test to pie-webc-testing helpers diff --git a/.changeset/serious-maps-grow.md b/.changeset/serious-maps-grow.md new file mode 100644 index 0000000000..af9294b1d2 --- /dev/null +++ b/.changeset/serious-maps-grow.md @@ -0,0 +1,5 @@ +--- +"@justeattakeaway/pie-icons-webc": minor +--- + +[Added] :host-context(pie-tag) svg styles diff --git a/.changeset/ten-mangos-exist.md b/.changeset/ten-mangos-exist.md new file mode 100644 index 0000000000..1768271ca8 --- /dev/null +++ b/.changeset/ten-mangos-exist.md @@ -0,0 +1,5 @@ +--- +"@justeattakeaway/pie-tag": minor +--- + +[Added] Tag component functionality diff --git a/apps/pie-storybook/stories/pie-tag-docs/variants.mdx b/apps/pie-storybook/stories/pie-tag-docs/variants.mdx new file mode 100644 index 0000000000..fc9c886a2e --- /dev/null +++ b/apps/pie-storybook/stories/pie-tag-docs/variants.mdx @@ -0,0 +1,40 @@ +import * as TagStories from '../pie-tag.stories.ts' +import { Meta, Canvas } from '@storybook/blocks'; + + + +### Neutral + + + +### Blue + + + +### Green + + + +### Yellow + + + +### Red + + + +### Brand + + + +### Neutral Alternative + + + +### Outline + + + +### Ghost + + \ No newline at end of file diff --git a/apps/pie-storybook/stories/pie-tag.stories.ts b/apps/pie-storybook/stories/pie-tag.stories.ts index 88c985add6..fd0d91843f 100644 --- a/apps/pie-storybook/stories/pie-tag.stories.ts +++ b/apps/pie-storybook/stories/pie-tag.stories.ts @@ -1,36 +1,118 @@ -import { html } from 'lit'; +import { html, nothing } from 'lit'; /* eslint-disable import/no-duplicates */ import '@justeattakeaway/pie-tag'; -import { TagProps } from '@justeattakeaway/pie-tag'; +import { + TagProps as TagBaseProps, variants, sizes, +} from '@justeattakeaway/pie-tag'; /* eslint-enable import/no-duplicates */ +import '@justeattakeaway/pie-icons-webc/IconHeartFilled'; -import { type StoryMeta } from '../types'; -import { createStory } from '../utilities'; +import type { StoryMeta, SlottedComponentProps } from '../types'; +import { createStory, type TemplateFunction } from '../utilities'; +type TagProps = SlottedComponentProps; type TagStoryMeta = StoryMeta; -const defaultArgs: TagProps = {}; +const defaultArgs: TagProps = { + variant: 'neutral', + size: 'large', + isStrong: false, + showIcon: false, + slot: 'Label', +}; const tagStoryMeta: TagStoryMeta = { title: 'Tag', component: 'pie-tag', - argTypes: {}, + argTypes: { + variant: { + description: 'Set the variant of the tag.', + control: 'select', + options: variants, + defaultValue: { + summary: 'neutral', + }, + }, + size: { + description: 'Set the size of the tag.', + control: 'select', + options: sizes, + defaultValue: { + summary: 'large', + }, + }, + isStrong: { + description: 'If `true`, displays strong tag styles for some of the variant', + control: 'boolean', + defaultValue: { + summary: false, + }, + }, + showIcon: { + description: 'Enable to see the example of Tag with icon. Available only for large tag size.', + control: 'boolean', + defaultValue: { + summary: false, + }, + if: { arg: 'size', eq: 'large' }, + }, + }, args: defaultArgs, parameters: { design: { type: 'figma', - url: '', + url: 'https://www.figma.com/file/OOgnT2oNMdGFytj5AanYvt/branch/QGEtmJqZM3OL9QG33L4053/%E2%9D%8C-%5BBETA%5D-%5BCore%5D-Component-Documentation-%5BPIE-3%5D-%E2%9D%8C?type=design&node-id=419-62146&mode=design', }, }, }; export default tagStoryMeta; -// TODO: remove the eslint-disable rule when props are added -// eslint-disable-next-line no-empty-pattern -const Template = ({}: TagProps) => html` - +const Template : TemplateFunction = ({ + variant, + size, + isStrong, + showIcon, + slot, +}) => html` + + ${showIcon ? html`` : nothing} + ${slot} + `; -export const Default = createStory(Template, defaultArgs)(); +const createTagStory = createStory(Template, defaultArgs); + +export const Default = createTagStory(); +export const Neutral = createTagStory({ variant: 'neutral' }); +export const Blue = createTagStory({ variant: 'blue' }); +export const Green = createTagStory({ variant: 'green' }); +export const Yellow = createTagStory({ variant: 'yellow' }); +export const Red = createTagStory({ variant: 'red' }); + +// For the following stories isStrong prop won't have any effect so it is excluded +export const Brand = createTagStory({ variant: 'brand' }, { + controls: { + exclude: ['isStrong'], + }, +}); +export const NeutralAlternative = createTagStory({ variant: 'neutral-alternative' }, { + bgColor: 'dark (container-dark)', + controls: { + exclude: ['isStrong'], + }, +}); +export const Outline = createTagStory({ variant: 'outline' }, { + controls: { + exclude: ['isStrong'], + }, +}); +export const Ghost = createTagStory({ variant: 'ghost' }, { + controls: { + exclude: ['isStrong'], + }, +}); diff --git a/packages/components/pie-button/test/component/pie-button.spec.ts b/packages/components/pie-button/test/component/pie-button.spec.ts index 4e6560835f..0d371773c4 100644 --- a/packages/components/pie-button/test/component/pie-button.spec.ts +++ b/packages/components/pie-button/test/component/pie-button.spec.ts @@ -1,5 +1,5 @@ +import { getShadowElementStylePropValues } from '@justeattakeaway/pie-webc-testing/src/helpers/get-shadow-element-style-prop-values.ts'; import { test, expect } from '@sand4rt/experimental-ct-web'; -import type { Locator } from '@playwright/test'; import { PieButton, ButtonProps } from '@/index'; const props: Partial = { @@ -12,37 +12,6 @@ type SizeResponsiveSize = { responsiveSize: string; }; -/** - * Gets the value of the given style properties from the shadow element - * @param element The custom element instance - * @param selector The selector of the element in the shadow - * @param props The style properties to get the values from - * @returns The values of the given style properties - */ -async function getShadowElementStylePropValues (element:Locator, selector:string, props:Array):Promise> { - const data = { selector, props }; - - const evaluated = await element.evaluate((el, data) => { - const { selector, props } = data; - - if (!el || !el.shadowRoot) { - throw new Error('getShadowElementStylePropValues: evaluate didn\'t return an element'); - } - - const shadowEl = el.shadowRoot.querySelector(selector); - - if (!shadowEl) { - throw new Error('getShadowElementStylePropValues: no shadow element was found'); - } - - const shadowElStyle = getComputedStyle(shadowEl); - - return props.map((prop) => shadowElStyle.getPropertyValue(prop).trim()); - }, data); - - return evaluated; -} - const sizes:Array = [ { sizeName: 'xsmall', responsiveSize: '--btn-height--small' }, { sizeName: 'small-expressive', responsiveSize: '--btn-height--medium' }, diff --git a/packages/components/pie-tag/README.md b/packages/components/pie-tag/README.md index 7db439beae..a771b99c56 100644 --- a/packages/components/pie-tag/README.md +++ b/packages/components/pie-tag/README.md @@ -19,7 +19,7 @@ ## pie-tag -`pie-tag` is a Web Component built using the Lit library. +`pie-tag` is a Web Component built using the Lit library. A tag is a small visual element used to represent and categorize information within a user interface.  This component can be easily integrated into various frontend frameworks and customized through a set of properties. @@ -74,16 +74,38 @@ import { PieTag } from '@justeattakeaway/pie-tag/dist/react'; | Property | Type | Default | Description | |---|---|---|---| -| - | - | - | - | +| size | `String` | `large` | Size of the tag. Can be either `large` or `small` | +| variant | `String` | `neutral` | Variant of the tag, one of `variants` - `neutral-alternative`, `neutral`, `outline`, `ghost`, `blue`, `green`, `yellow`, `red`, `brand` | +| isStrong | `Boolean` | `false` | If `true`, displays strong tag styles for `green`, `yellow`, `red`, `blue` and `neutral` variants'| In your markup or JSX, you can then use these to set the properties for the `pie-tag` component: ```html - +Label - +Label +``` +## Slots + +| Slot | Description | +| Default slot | Used to pass text into the tag component. | +| icon | Used to pass in an icon to the tag component. We recommend using `pie-icons-webc` for defining this icon, but this can also accept an SVG icon. | + +### Using `pie-icons-webc` with `pie-tag` icon slot + +We recommend using `pie-icons-webc` when using the `icon` slot. Here is an example of how you would do this: + +```html + + + + Vegan + ``` ## Contributing diff --git a/packages/components/pie-tag/src/defs.ts b/packages/components/pie-tag/src/defs.ts index d95303e94e..42e2282eb7 100644 --- a/packages/components/pie-tag/src/defs.ts +++ b/packages/components/pie-tag/src/defs.ts @@ -1,3 +1,19 @@ -// TODO - please remove the eslint disable comment below when you add props to this interface -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface TagProps {} +export const variants = ['neutral-alternative', 'neutral', 'outline', 'ghost', 'blue', 'green', 'yellow', 'red', 'brand'] as const; +export const sizes = ['small', 'large'] as const; + +export interface TagProps { + /** + * What style variant the tag should be such as neutral/ghost etc. + */ + variant: typeof variants[number]; + + /** + * When true, the 'green', "yellow", "red", "blue" and "neutral" variants change their styles and become bolder + */ + isStrong: boolean; + + /** + * What size the tag should be. + */ + size: typeof sizes[number]; +} diff --git a/packages/components/pie-tag/src/index.ts b/packages/components/pie-tag/src/index.ts index 4024408c2f..097bff6c64 100644 --- a/packages/components/pie-tag/src/index.ts +++ b/packages/components/pie-tag/src/index.ts @@ -1,8 +1,10 @@ -import { LitElement, html, unsafeCSS } from 'lit'; - -import { defineCustomElement } from '@justeattakeaway/pie-webc-core'; +import { + LitElement, html, unsafeCSS, nothing, +} from 'lit'; +import { property } from 'lit/decorators.js'; +import { validPropertyValues, defineCustomElement } from '@justeattakeaway/pie-webc-core'; import styles from './tag.scss?inline'; -import { TagProps } from './defs'; +import { TagProps, variants, sizes } from './defs'; // Valid values available to consumers export * from './defs'; @@ -11,10 +13,38 @@ const componentSelector = 'pie-tag'; /** * @tagname pie-tag + * @slot icon - The icon slot + * @slot - Default slot */ export class PieTag extends LitElement implements TagProps { + @property({ type: String }) + @validPropertyValues(componentSelector, variants, 'neutral') + public variant: TagProps['variant'] = 'neutral'; + + @property({ type: String }) + @validPropertyValues(componentSelector, sizes, 'large') + public size : TagProps['size'] = 'large'; + + @property({ type: Boolean }) + public isStrong = false; + render () { - return html`

Hello world!

`; + const { + variant, + size, + isStrong, + } = this; + return html` +
+ ${size === 'large' ? html`` : nothing} + +
`; } // Renders a `CSSResult` generated from SCSS by Vite diff --git a/packages/components/pie-tag/src/tag.scss b/packages/components/pie-tag/src/tag.scss index 6ffaedad64..c3c51603df 100644 --- a/packages/components/pie-tag/src/tag.scss +++ b/packages/components/pie-tag/src/tag.scss @@ -1 +1,149 @@ @use '@justeattakeaway/pie-css/scss' as p; + +*, +*:before, +*:after { + box-sizing: border-box; +} + +// Base tag styles +.c-tag { + // Custom Property Declarations + // These are defined here instead of :host to encapsulate them inside Shadow DOM + --tag-font-family: var(--dt-font-body-s-family); + --tag-font-weight: var(--dt-font-body-s-weight); + --tag-icon-size: 16px; + + // The following values set to default background and color + // currently this sets the neutral large tag styles + --tag-bg-color: var(--dt-color-container-inverse); + --tag-color: var(--dt-color-content-inverse); + + // transparent to variable to function properly in component tests + --tag-transparent-bg-color: transparent; + + // Heights for the different tag sizes + --tag-height-large: 24px; + --tag-height-small: 16px; + + display: inline-flex; + vertical-align: middle; + align-items: center; + justify-content: center; + gap: var(--dt-spacing-a); + height: var(--tag-height); + padding: var(--tag-padding); + border: none; + border-radius: var(--tag-border-radius); + background-color: var(--tag-bg-color); + color: var(--tag-color); + font-family: var(--tag-font-family); + font-weight: var(--tag-font-weight); + font-size: var(--tag-font-size); + line-height: var(--tag-line-height); + opacity: var(--tag-opacity, 1); // we don't specify --tag-opacity variable here to let consumers override a default value that we set + + // Size + &[size='small'] { + --tag-height: var(--tag-height-small); + --tag-padding: 0 var(--dt-spacing-a); + --tag-border-radius: var(--dt-radius-rounded-a); + --tag-font-size: #{p.font-size(--dt-font-caption-size)}; + --tag-line-height: #{p.line-height(--dt-font-caption-line-height)}; + } + + &[size='large'] { + --tag-height: var(--tag-height-large); + --tag-padding: 2px var(--dt-spacing-b); + --tag-border-radius: var(--dt-radius-rounded-b); + --tag-font-size: #{p.font-size(--dt-font-body-s-size)}; + --tag-line-height: #{p.line-height(--dt-font-body-s-line-height)}; + } + + // Variant + &[variant='neutral'] { + --tag-bg-color: var(--dt-color-container-strong); + --tag-color: var(--dt-color-content-default); + + &[isStrong] { + --tag-bg-color: var(--dt-color-container-inverse); + --tag-color: var(--dt-color-content-inverse); + } + } + + &[variant='blue'] { + --tag-bg-color: var(--dt-color-support-info-02); + --tag-color: var(--dt-color-content-default); + + &[isStrong] { + --tag-bg-color: var(--dt-color-support-info); + --tag-color: var(--dt-color-content-light); + } + } + + &[variant='green'] { + --tag-bg-color: var(--dt-color-support-positive-02); + --tag-color: var(--dt-color-content-default); + + &[isStrong] { + --tag-bg-color: var(--dt-color-support-positive); + --tag-color: var(--dt-color-content-light); + } + } + + &[variant='yellow'] { + --tag-bg-color: var(--dt-color-support-warning-02); + --tag-color: var(--dt-color-content-dark); + + &[isStrong] { + --tag-bg-color: var(--dt-color-support-warning); + --tag-color: var(--dt-color-content-dark); + } + } + + &[variant='red'] { + --tag-bg-color: var(--dt-color-support-error-02); + --tag-color: var(--dt-color-content-default); + + &[isStrong] { + --tag-bg-color: var(--dt-color-support-error); + --tag-color: var(--dt-color-content-light); + } + } + + &[variant='brand'] { + --tag-bg-color: var(--dt-color-support-brand-02); + --tag-color: var(--dt-color-content-default); + } + + &[variant='neutral-alternative'] { + --tag-bg-color: var(--dt-color-container-default); + --tag-color: var(--dt-color-content-default); + } + + &[variant='outline'] { + --tag-bg-color: var(--tag-transparent-bg-color); + --tag-color: var(--dt-color-content-default); + border: 1px solid var(--dt-color-border-strong); + + &[size='small'] { + --tag-padding: 0 3px; // small tag padding minus 1px of the border + } + + &[size='large'] { + --tag-padding: 1px 7px; // large tag padding minus 1px of the border + } + } + + &[variant='ghost'] { + --tag-bg-color: var(--tag-transparent-bg-color); + --tag-color: var(--dt-color-content-default); + } +} + +// Used to size an SVG if one is passed in (when not using the component icons) +::slotted(svg) { + display: block; + height: var(--tag-icon-size); + width: var(--tag-icon-size); +} diff --git a/packages/components/pie-tag/test/accessibility/pie-tag.spec.ts b/packages/components/pie-tag/test/accessibility/pie-tag.spec.ts index fdaef1c0f1..1096ac692b 100644 --- a/packages/components/pie-tag/test/accessibility/pie-tag.spec.ts +++ b/packages/components/pie-tag/test/accessibility/pie-tag.spec.ts @@ -1,18 +1,34 @@ import { test, expect } from '@justeattakeaway/pie-webc-testing/src/playwright/fixtures.ts'; -import { PieTag, TagProps } from '@/index'; +import { getAllPropCombinations, splitCombinationsByPropertyValue } from '@justeattakeaway/pie-webc-testing/src/helpers/get-all-prop-combos.ts'; +import { PropObject, WebComponentPropValues } from '@justeattakeaway/pie-webc-testing/src/helpers/defs.ts'; +import { PieTag } from '@/index'; +import { sizes, variants } from '@/defs'; -test.describe('PieTag - Accessibility tests', () => { - test('a11y - should test the PieTag component WCAG compliance', async ({ makeAxeBuilder, mount }) => { +const props: PropObject = { + variant: variants, + size: sizes, + isStrong: [true, false], +}; + +const componentPropsMatrix : WebComponentPropValues[] = getAllPropCombinations(props); +const componentPropsMatrixByVariant: Record = splitCombinationsByPropertyValue(componentPropsMatrix, 'variant'); +const componentVariants: string[] = Object.keys(componentPropsMatrixByVariant); + +componentVariants.forEach((variant) => test(`should render all prop variations for Variant: ${variant}`, async ({ makeAxeBuilder, mount }) => { + await Promise.all(componentPropsMatrixByVariant[variant].map(async (combo: WebComponentPropValues) => { await mount( PieTag, { - props: {} as TagProps, + props: { ...combo }, + slots: { + default: 'Hello world', + }, }, ); + })); - const results = await makeAxeBuilder().analyze(); + const results = await makeAxeBuilder().analyze(); - expect(results.violations).toEqual([]); - }); -}); + expect(results.violations).toEqual([]); +})); diff --git a/packages/components/pie-tag/test/component/pie-tag.spec.ts b/packages/components/pie-tag/test/component/pie-tag.spec.ts index 0d401df369..d082a6f6fb 100644 --- a/packages/components/pie-tag/test/component/pie-tag.spec.ts +++ b/packages/components/pie-tag/test/component/pie-tag.spec.ts @@ -1,14 +1,43 @@ - +import { getShadowElementStylePropValues } from '@justeattakeaway/pie-webc-testing/src/helpers/get-shadow-element-style-prop-values.ts'; import { test, expect } from '@sand4rt/experimental-ct-web'; import { PieTag, TagProps } from '@/index'; const componentSelector = '[data-test-id="pie-tag"]'; +const tagIconSelector = '[data-test-id="tag-icon"]'; + +const props: Partial = { + size: 'large', + variant: 'neutral', + isStrong: false, +}; + +type VariantToBgStyle = { + variantName: TagProps['variant']; + bgStyle: string; +}; + +const variantsToIsStrongStyle:Array = [ + { variantName: 'neutral', bgStyle: '--dt-color-container-inverse' }, + { variantName: 'green', bgStyle: '--dt-color-support-positive' }, + { variantName: 'red', bgStyle: '--dt-color-support-error' }, + { variantName: 'yellow', bgStyle: '--dt-color-support-warning' }, + { variantName: 'blue', bgStyle: '--dt-color-support-info' }, + { variantName: 'neutral-alternative', bgStyle: '--dt-color-container-default' }, + { variantName: 'brand', bgStyle: '--dt-color-support-brand-02' }, + { variantName: 'ghost', bgStyle: '--tag-transparent-bg-color' }, + { variantName: 'outline', bgStyle: '--tag-transparent-bg-color' }, +]; + +const icon = ''; test.describe('PieTag - Component tests', () => { test('should render successfully', async ({ mount, page }) => { // Arrange await mount(PieTag, { - props: {} as TagProps, + props, + slots: { + default: 'Label', + }, }); // Act @@ -17,4 +46,68 @@ test.describe('PieTag - Component tests', () => { // Assert expect(tag).toBeVisible(); }); + + test.describe('icon slot', () => { + test.describe('when passed', () => { + test.describe('if the size is large', () => { + test('should render the icon', async ({ mount, page }) => { + // Arrange + await mount(PieTag, { + props, + slots: { + default: 'Label', + icon, + }, + }); + + // Act + const tagIcon = page.locator(tagIconSelector); + + // Assert + expect(tagIcon).toBeVisible(); + }); + }); + }); + + test.describe('if the size is small', () => { + test('should NOT render the icon', async ({ mount, page }) => { + // Arrange + await mount(PieTag, { + props: { + ...props, + size: 'small', + }, + slots: { + default: 'Label', + icon, + }, + }); + + // Act + const tagIcon = page.locator(tagIconSelector); + + // Assert + await expect(tagIcon).not.toBeVisible(); + }); + }); + }); + + variantsToIsStrongStyle.forEach(({ variantName, bgStyle }) => { + test(`a "${variantName}" tag variant bg colour should be equivalent to "${bgStyle}"`, async ({ mount }) => { + const component = await mount(PieTag, { + props: { + ...props, + variant: variantName, + isStrong: true, + }, + slots: { + default: 'Label', + }, + }); + + const [currentBgStyle, expectedBgStyle] = await getShadowElementStylePropValues(component, componentSelector, ['--tag-bg-color', bgStyle]); + + await expect(currentBgStyle).toBe(expectedBgStyle); + }); + }); }); diff --git a/packages/components/pie-tag/test/visual/pie-tag.spec.ts b/packages/components/pie-tag/test/visual/pie-tag.spec.ts index 3e8b9cc0eb..7bd1ee5aa8 100644 --- a/packages/components/pie-tag/test/visual/pie-tag.spec.ts +++ b/packages/components/pie-tag/test/visual/pie-tag.spec.ts @@ -1,14 +1,69 @@ import { test } from '@sand4rt/experimental-ct-web'; import percySnapshot from '@percy/playwright'; -import { PieTag, TagProps } from '@/index'; +import type { + PropObject, WebComponentPropValues, WebComponentTestInput, +} from '@justeattakeaway/pie-webc-testing/src/helpers/defs.ts'; +import { + getAllPropCombinations, splitCombinationsByPropertyValue, +} from '@justeattakeaway/pie-webc-testing/src/helpers/get-all-prop-combos.ts'; +import { + createTestWebComponent, +} from '@justeattakeaway/pie-webc-testing/src/helpers/rendering.ts'; +import { + WebComponentTestWrapper, +} from '@justeattakeaway/pie-webc-testing/src/helpers/components/web-component-test-wrapper/WebComponentTestWrapper.ts'; +import { percyWidths } from '@justeattakeaway/pie-webc-testing/src/percy/breakpoints.ts'; +import { sizes, variants } from '@/defs'; -test.describe('PieTag - Visual tests`', () => { - test('should display the PieTag component successfully', async ({ page, mount }) => { - await mount(PieTag, { - props: {} as TagProps, - }); +// TODO: Currently setting the slot to use a straight up SVG +// This should be updated to use pie-icons-webc, but after some investigation, we think that we'll +// need to convert the webc icons to use Lit, as currently the components don't work well in a Node env like Playwright +// Atm, importing them like `import '@justeattakeaway/pie-icons-webc/icons/IconClose.js';` results in an `HTMLElement is not defined` error +const icon = ''; - await percySnapshot(page, 'PieTag - Visual Test'); - }); +const props: PropObject = { + variant: variants, + size: sizes, + isStrong: [true, false], + iconSlot: ['', icon], +}; + +// Renders a HTML string with the given prop values +const renderTestPieTag = (propVals: WebComponentPropValues) => `${propVals.iconSlot} Hello world`; + +const componentPropsMatrix: WebComponentPropValues[] = getAllPropCombinations(props); +const componentPropsMatrixByVariant: Record = splitCombinationsByPropertyValue(componentPropsMatrix, 'variant'); +const componentVariants: string[] = Object.keys(componentPropsMatrixByVariant); + +// eslint-disable-next-line no-empty-pattern +test.beforeEach(async ({ }, testInfo) => { + testInfo.setTimeout(testInfo.timeout + 40000); }); + +componentVariants.forEach((variant) => test(`should render all prop variations for Variant: ${variant}`, async ({ page, mount }) => { + await Promise.all(componentPropsMatrixByVariant[variant].map(async (combo: WebComponentPropValues) => { + const testComponent: WebComponentTestInput = createTestWebComponent(combo, renderTestPieTag); + const propKeyValues = ` + size: ${testComponent.propValues.size}, + variant: ${testComponent.propValues.variant}, + isStrong: ${testComponent.propValues.isStrong}, + iconSlot: ${testComponent.propValues.iconSlot ? 'with icon' : 'no icon'}`; + const darkMode = ['neutral-alternative'].includes(variant); + + await mount( + WebComponentTestWrapper, + { + props: { propKeyValues, darkMode }, + slots: { + component: testComponent.renderedString.trim(), + }, + }, + ); + })); + + // Follow up to remove in Jan + await page.waitForTimeout(5000); + + await percySnapshot(page, `PIE Tag - Variant: ${variant}`, percyWidths); +})); diff --git a/packages/components/pie-webc-testing/src/helpers/get-shadow-element-style-prop-values.ts b/packages/components/pie-webc-testing/src/helpers/get-shadow-element-style-prop-values.ts new file mode 100644 index 0000000000..2d1b68ba95 --- /dev/null +++ b/packages/components/pie-webc-testing/src/helpers/get-shadow-element-style-prop-values.ts @@ -0,0 +1,32 @@ +import type { Locator } from '@playwright/test'; + +/** + * Gets the value of the given style properties from the shadow element + * @param element The custom element instance + * @param selector The selector of the element in the shadow + * @param props The style properties to get the values from + * @returns The values of the given style properties + */ +export const getShadowElementStylePropValues = async (element:Locator, selector:string, props:Array):Promise> => { + const data = { selector, props }; + + const evaluated = await element.evaluate((el, data) => { + const { selector, props } = data; + + if (!el || !el.shadowRoot) { + throw new Error('getShadowElementStylePropValues: evaluate didn\'t return an element'); + } + + const shadowEl = el.shadowRoot.querySelector(selector); + + if (!shadowEl) { + throw new Error('getShadowElementStylePropValues: no shadow element was found'); + } + + const shadowElStyle = getComputedStyle(shadowEl); + + return props.map((prop) => shadowElStyle.getPropertyValue(prop).trim()); + }, data); + + return evaluated; +}; diff --git a/packages/components/pie-webc-testing/src/helpers/index.ts b/packages/components/pie-webc-testing/src/helpers/index.ts index e0c45fad77..c5f98cda21 100644 --- a/packages/components/pie-webc-testing/src/helpers/index.ts +++ b/packages/components/pie-webc-testing/src/helpers/index.ts @@ -1,3 +1,4 @@ +export * from './get-shadow-element-style-prop-values'; export * from './get-all-prop-combos'; export * from './defs'; export * from './rendering'; diff --git a/packages/tools/pie-icons-webc/build.js b/packages/tools/pie-icons-webc/build.js index 6d66cc5b22..1400780c5f 100644 --- a/packages/tools/pie-icons-webc/build.js +++ b/packages/tools/pie-icons-webc/build.js @@ -44,6 +44,12 @@ export class ${name} extends LitElement implements IconProps { width: var(--btn-icon-size); height: var(--btn-icon-size); } + + :host-context(pie-tag) svg { + display: block; + width: var(--tag-icon-size); + height: var(--tag-icon-size); + } \`; @property({ type: String, reflect: true })