From 00398c9ae56052fd60ee9df355dbbfbbe0e75e1a Mon Sep 17 00:00:00 2001 From: Ben Siggery <14013357+siggerzz@users.noreply.github.com> Date: Thu, 6 Feb 2025 16:00:07 +0000 Subject: [PATCH] refactor(pie-radio): DSW-2770 tests to use `@playwright/test` (#2188) * refactor(pie-radio): DSW-2770 tests to use `@playwright/test` * remove uneeded test * improve prop labelling * improve prop labelling --------- Co-authored-by: Ben Siggery --- .../stories/testing/pie-radio.test.stories.ts | 201 ++++++ .../pie-radio/playwright-lit-visual.config.ts | 6 +- .../pie-radio/playwright-lit.config.ts | 6 +- packages/components/pie-radio/src/index.ts | 2 +- .../test/accessibility/pie-radio.spec.ts | 16 +- .../test/component/pie-radio.spec.ts | 597 ++++++++---------- .../test/helpers/page-object/selectors.ts | 13 + .../pie-radio/test/visual/pie-radio.spec.ts | 120 +--- packages/components/pie-radio/turbo.json | 40 ++ 9 files changed, 540 insertions(+), 461 deletions(-) create mode 100644 apps/pie-storybook/stories/testing/pie-radio.test.stories.ts create mode 100644 packages/components/pie-radio/test/helpers/page-object/selectors.ts create mode 100644 packages/components/pie-radio/turbo.json diff --git a/apps/pie-storybook/stories/testing/pie-radio.test.stories.ts b/apps/pie-storybook/stories/testing/pie-radio.test.stories.ts new file mode 100644 index 0000000000..0b3424409e --- /dev/null +++ b/apps/pie-storybook/stories/testing/pie-radio.test.stories.ts @@ -0,0 +1,201 @@ +import { html } from 'lit'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { useArgs as UseArgs } from '@storybook/preview-api'; +import { type Meta } from '@storybook/web-components'; + +import '@justeattakeaway/pie-radio'; +import '@justeattakeaway/pie-button'; +import { + defaultProps, + statusTypes, + type RadioProps as RadioBaseProps, +} from '@justeattakeaway/pie-radio'; + +import { type SlottedComponentProps } from '../../types'; +import { + createStory, createVariantStory, sanitizeAndRenderHTML, type PropDisplayOptions, type TemplateFunction, +} from '../../utilities'; + +type RadioProps = SlottedComponentProps; +type RadioStoryMeta = Meta; + +const defaultArgs: RadioProps = { + ...defaultProps, + slot: 'Label', + value: 'value', +}; + +const radioStoryMeta: RadioStoryMeta = { + title: 'Radio', + component: 'pie-radio', + argTypes: { + checked: { + control: 'boolean', + defaultValue: { + summary: defaultArgs.checked, + }, + }, + + defaultChecked: { + control: 'boolean', + defaultValue: { + summary: defaultArgs.defaultChecked, + }, + }, + + disabled: { + control: 'boolean', + defaultValue: { + summary: defaultArgs.disabled, + }, + }, + + name: { + control: 'text', + defaultValue: { + summary: defaultArgs.name, + }, + }, + + required: { + control: 'boolean', + defaultValue: { + summary: defaultArgs.required, + }, + }, + + slot: { + control: 'text', + }, + value: { + control: 'text', + defaultValue: { + summary: defaultArgs.value, + }, + }, + status: { + control: 'select', + options: statusTypes, + defaultValue: { + summary: defaultProps.status, + }, + }, + }, + args: defaultArgs, +}; + +export default radioStoryMeta; + +const onSubmit = (event: Event) => { + event.preventDefault(); + const form = document.querySelector('#testForm') as HTMLFormElement; + const output = document.querySelector('#formDataOutput') as HTMLDivElement; + + console.log('form', form); + const formData = new FormData(form); + const formDataObj: { [key: string]: FormDataEntryValue } = {}; + formData.forEach((value, key) => { + formDataObj[key] = value; + }); + + output.innerText = JSON.stringify(formDataObj); +}; + +const Template = ({ + checked, + disabled, + defaultChecked, + name, + required, + slot, + value, + status, +}: RadioProps) => { + const [, updateArgs] = UseArgs(); + + const onChange = (event: InputEvent) => { + const radioElement = event.target as HTMLInputElement; + updateArgs({ checked: radioElement.checked }); + console.info(JSON.stringify(event)); + }; + + return html` + + ${sanitizeAndRenderHTML(slot)} + `; +}; + +const ExampleFormTemplate: TemplateFunction = ({ + value, + name, + checked, + defaultChecked, + disabled, + required, + slot, +}: RadioProps) => { + const [, updateArgs] = UseArgs(); + + const onChange = (event: InputEvent) => { + const radioElement = event.target as HTMLInputElement; + updateArgs({ checked: radioElement.checked }); + console.info('change event fired'); + }; + + return html` + +
+ + ${sanitizeAndRenderHTML(slot)} + + Reset + Submit +
+
`; +}; + +export const Default = createStory(Template, defaultArgs)(); +export const ExampleForm = createStory(ExampleFormTemplate, defaultArgs)(); + +const shortLabel = 'Short label'; +const longLabel = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi pretium quam eget dolor imperdiet placerat. Aliquam sollicitudin erat sed est lobortis sollicitudin. Nam vulputate, mi vel finibus convallis, mi dolor molestie arcu, vel pulvinar urna neque et sapien. Aenean euismod faucibus turpis et efficitur. Sed porttitor dui at justo cursus pulvinar. Sed scelerisque aliquet diam sed feugiat. Fusce id lorem finibus, tempor nulla tempor, tincidunt odio. Mauris consequat lectus ex, eget lacinia dui finibus sit amet. Phasellus maximus posuere sapien eget condimentum. Nunc viverra pharetra blandit.'; + +const radioPropsMatrix: Partial> = { + checked: [true, false], + disabled: [true, false], + slot: ['Label', longLabel], + status: ['default', 'error'], +}; + +const variantPropDisplayOptions: PropDisplayOptions = { + propLabels: { + slot: { + [longLabel]: 'With long content', + [shortLabel]: 'With short content', + }, + }, +}; + +export const Variations = createVariantStory(Template, radioPropsMatrix, { ... variantPropDisplayOptions, multiColumn: true }); diff --git a/packages/components/pie-radio/playwright-lit-visual.config.ts b/packages/components/pie-radio/playwright-lit-visual.config.ts index fb0f14c480..2fd82d7d5f 100644 --- a/packages/components/pie-radio/playwright-lit-visual.config.ts +++ b/packages/components/pie-radio/playwright-lit-visual.config.ts @@ -1,4 +1,4 @@ -import { defineConfig } from '@sand4rt/experimental-ct-web'; -import { getPlaywrightVisualConfig } from '@justeattakeaway/pie-components-config'; +import { defineConfig } from '@playwright/test'; +import { getPlaywrightNativeVisualConfig } from '@justeattakeaway/pie-components-config'; -export default defineConfig(getPlaywrightVisualConfig()); +export default defineConfig(getPlaywrightNativeVisualConfig()); diff --git a/packages/components/pie-radio/playwright-lit.config.ts b/packages/components/pie-radio/playwright-lit.config.ts index e50b9373b3..6dcc0f833d 100644 --- a/packages/components/pie-radio/playwright-lit.config.ts +++ b/packages/components/pie-radio/playwright-lit.config.ts @@ -1,4 +1,4 @@ -import { defineConfig } from '@sand4rt/experimental-ct-web'; -import { getPlaywrightConfig } from '@justeattakeaway/pie-components-config'; +import { defineConfig } from '@playwright/test'; +import { getPlaywrightNativeConfig } from '@justeattakeaway/pie-components-config'; -export default defineConfig(getPlaywrightConfig()); +export default defineConfig(getPlaywrightNativeConfig()); diff --git a/packages/components/pie-radio/src/index.ts b/packages/components/pie-radio/src/index.ts index df03f0990f..12b3ed40fe 100644 --- a/packages/components/pie-radio/src/index.ts +++ b/packages/components/pie-radio/src/index.ts @@ -154,7 +154,7 @@ export class PieRadio extends FormControlMixin(RtlMixin(LitElement)) implements class="c-radio-input" type="radio" id="radioId" - data-test-id="pie-radio" + data-test-id="pie-radio-input" .checked="${live(checked)}" .value="${value}" name="${ifDefined(name)}" diff --git a/packages/components/pie-radio/test/accessibility/pie-radio.spec.ts b/packages/components/pie-radio/test/accessibility/pie-radio.spec.ts index 8679c2e938..a19c928be4 100644 --- a/packages/components/pie-radio/test/accessibility/pie-radio.spec.ts +++ b/packages/components/pie-radio/test/accessibility/pie-radio.spec.ts @@ -1,16 +1,10 @@ -import { test, expect } from '@justeattakeaway/pie-webc-testing/src/playwright/webc-fixtures.ts'; -import { PieRadio, type RadioProps } from '../../src/index.ts'; +import { test, expect } from '@justeattakeaway/pie-webc-testing/src/playwright/playwright-fixtures.ts'; +import { BasePage } from '@justeattakeaway/pie-webc-testing/src/helpers/page-object/base-page.ts'; test.describe('PieRadio - Accessibility tests', () => { - test('a11y - should test the PieRadio component WCAG compliance', async ({ makeAxeBuilder, mount }) => { - await mount(PieRadio, { - props: { - name: 'option-1', - } as RadioProps, - slots: { - default: 'Label', - }, - }); + test('a11y - should test the PieRadio component WCAG compliance', async ({ makeAxeBuilder, page }) => { + const radioDefaultPage = await new BasePage(page, 'radio--default'); + await radioDefaultPage.load(); const results = await makeAxeBuilder().analyze(); diff --git a/packages/components/pie-radio/test/component/pie-radio.spec.ts b/packages/components/pie-radio/test/component/pie-radio.spec.ts index 91453bf99f..2bc52ae035 100644 --- a/packages/components/pie-radio/test/component/pie-radio.spec.ts +++ b/packages/components/pie-radio/test/component/pie-radio.spec.ts @@ -1,322 +1,300 @@ -import { test, expect } from '@sand4rt/experimental-ct-web'; - -import { setupFormDataExtraction, getFormDataObject } from '@justeattakeaway/pie-webc-testing/src/helpers/form-helpers.ts'; - -import { PieRadio } from '../../src/index.ts'; -import { type RadioProps } from '../../src/defs.ts'; - -const componentSelector = '[data-test-id="pie-radio"]'; -const inputSelector = 'input[type="radio"]'; - -const slots = { - default: 'Label', -}; +import { test, expect } from '@playwright/test'; +import { BasePage } from '@justeattakeaway/pie-webc-testing/src/helpers/page-object/base-page.ts'; +import type { RadioProps } from '../../src/defs.ts'; +import type { PieRadio } from '../../src/index.ts'; +import { radio } from '../helpers/page-object/selectors.ts'; test.describe('PieRadio - Component tests', () => { - test.beforeEach(async ({ mount }) => { - const component = await mount(PieRadio); - await component.unmount(); - }); - - test('should render successfully', async ({ mount, page }) => { + test('should render successfully', async ({ page }) => { // Arrange - await mount(PieRadio, { - props: { - value: 'testValue', - } as RadioProps, - slots, - }); + const radioDefaultPage = new BasePage(page, 'radio--default'); + await radioDefaultPage.load(); // Act - const radio = page.locator(componentSelector); + const radioComponent = page.getByTestId(radio.selectors.container.dataTestId); // Assert - expect(radio).toBeVisible(); + await expect(radioComponent).toBeVisible(); }); test.describe('props', () => { test.describe('value', () => { - test('should apply the value attribute to the input', async ({ mount }) => { + test('should apply the value attribute to the input', async ({ page }) => { // Arrange - const component = await mount(PieRadio, { - props: { - value: 'testValue', - } as RadioProps, - }); + const props : RadioProps = { + value: 'testValue', + }; + const radioDefaultPage = new BasePage(page, 'radio--default'); + await radioDefaultPage.load({ ...props }); // Act - const radio = component.locator(inputSelector); + const radioInput = page.getByTestId(radio.selectors.input.dataTestId); // Assert - expect((await radio.inputValue())).toBe('testValue'); + await expect(radioInput).toHaveValue('testValue'); }); }); test.describe('name', () => { - test('should not render a name attr on the radio element if no name provided', async ({ mount }) => { + test('should not render a name attr on the radio element if no name provided', async ({ page }) => { // Arrange - const component = await mount(PieRadio, { - props: { - value: 'testValue', - }, - }); + const radioDefaultPage = new BasePage(page, 'radio--default'); + await radioDefaultPage.load({ value: 'testValue' }); // Act - const radio = component.locator(inputSelector); + const radioInput = page.getByTestId(radio.selectors.input.dataTestId); // Assert - expect((await radio.getAttribute('name'))).toBe(null); + await expect(radioInput).not.toHaveAttribute('name'); }); - test('should apply the name attr to the radio', async ({ mount }) => { + test('should apply the name attr to the radio', async ({ page }) => { // Arrange - const component = await mount(PieRadio, { - props: { - name: 'test', - value: 'testValue', - } as RadioProps, - }); + const props : RadioProps = { + name: 'test', + value: 'testValue', + }; + + const radioDefaultPage = new BasePage(page, 'radio--default'); + await radioDefaultPage.load({ ...props }); // Act - const radio = component.locator(inputSelector); + const radioInput = page.getByTestId(radio.selectors.input.dataTestId); // Assert - expect((await radio.getAttribute('name'))).toBe('test'); + await expect(radioInput).toHaveAttribute('name', 'test'); }); }); test.describe('checked', () => { - test('should check the radio when true', async ({ page, mount }) => { + test('should check the radio when true', async ({ page }) => { // Arrange - await mount(PieRadio, { - props: { - checked: true, - value: 'testValue', - } as RadioProps, - slots, - }); + const props : RadioProps = { + checked: true, + value: 'testValue', + }; + + const radioDefaultPage = new BasePage(page, 'radio--default'); + await radioDefaultPage.load({ ...props }); // Act - const radio = page.locator(inputSelector); + const radioInput = page.getByTestId(radio.selectors.input.dataTestId); // Assert - await expect(radio).toBeChecked(); + await expect(radioInput).toBeChecked(); }); - test('should not check the radio when false', async ({ page, mount }) => { + test('should not check the radio when false', async ({ page }) => { // Arrange - await mount(PieRadio, { - props: { - checked: false, - value: 'testValue', - } as RadioProps, - slots, - }); + const props : RadioProps = { + checked: false, + value: 'testValue', + }; + + const radioDefaultPage = new BasePage(page, 'radio--default'); + await radioDefaultPage.load({ ...props }); // Act - const radio = page.locator(inputSelector); + const radioInput = page.getByTestId(radio.selectors.input.dataTestId); // Assert - await expect(radio).not.toBeChecked(); + await expect(radioInput).not.toBeChecked(); }); - test('should keep aria-checked in sync with the checked prop', async ({ page, mount }) => { + test('should keep aria-checked in sync with the checked prop', async ({ page }) => { // Arrange - await mount(PieRadio, { - props: { - checked: false, - value: 'testValue', - } as RadioProps, - slots, - }); + const props : RadioProps = { + checked: false, + value: 'testValue', + }; + + const radioDefaultPage = new BasePage(page, 'radio--default'); + await radioDefaultPage.load({ ...props }); // Act - const radio = page.locator('pie-radio'); + const radioComponent = page.getByTestId(radio.selectors.container.dataTestId); // Assert initial state - await expect(radio).toHaveAttribute('aria-checked', 'false'); + await expect(radioComponent).toHaveAttribute('aria-checked', 'false'); // Update checked prop to true - await page.evaluate(() => { - const radioComponent = document.querySelector('pie-radio'); - if (radioComponent) radioComponent.checked = true; - }); + await page.getByTestId(radio.selectors.input.dataTestId).check(); // Assert updated state - await expect(radio).toHaveAttribute('aria-checked', 'true'); - - // Update checked prop back to false - await page.evaluate(() => { - const radioComponent = document.querySelector('pie-radio'); - if (radioComponent) radioComponent.checked = false; - }); - - // Assert reverted state - await expect(radio).toHaveAttribute('aria-checked', 'false'); + await expect(radioComponent).toHaveAttribute('aria-checked', 'true'); }); }); test.describe('disabled', () => { - test('should enable the radio when false', async ({ mount, page }) => { + test('should enable the radio when false', async ({ page }) => { // Arrange - await mount(PieRadio, { - props: { - disabled: false, - value: 'testValue', - } as RadioProps, - slots, - }); + const props : RadioProps = { + disabled: false, + value: 'testValue', + }; + + const radioDefaultPage = new BasePage(page, 'radio--default'); + await radioDefaultPage.load({ ...props }); // Act - const radio = page.locator(inputSelector); + const radioInput = page.getByTestId(radio.selectors.input.dataTestId); // Assert - await expect(radio).not.toBeDisabled(); + await expect(radioInput).not.toBeDisabled(); }); - test('should disable the radio when true', async ({ mount, page }) => { + test('should disable the radio when true', async ({ page }) => { // Arrange - await mount(PieRadio, { - props: { - disabled: true, - value: 'testValue', - } as RadioProps, - slots, - }); + const props : RadioProps = { + disabled: true, + value: 'testValue', + }; + + const radioDefaultPage = new BasePage(page, 'radio--default'); + await radioDefaultPage.load({ ...props }); // Act - const radio = page.locator(inputSelector); + const radioInput = page.getByTestId(radio.selectors.input.dataTestId); // Assert - await expect(radio).toBeDisabled(); + await expect(radioInput).toBeDisabled(); }); }); test.describe('required', () => { - test('should not add a required attribute if the prop is not provided', async ({ mount }) => { + test('should not add a required attribute if the prop is not provided', async ({ page }) => { // Arrange - const component = await mount(PieRadio, { - props: { - value: 'testValue', - } as RadioProps, - }); + const radioDefaultPage = new BasePage(page, 'radio--default'); + await radioDefaultPage.load({ value: 'testValue' }); // Act - const radio = component.locator(inputSelector); + const radioInput = page.getByTestId(radio.selectors.input.dataTestId); // Assert - expect((await radio.getAttribute('required'))).toBe(null); + await expect(radioInput).not.toHaveAttribute('required'); }); - test('should apply the required attribute to the input element', async ({ mount }) => { + test('should apply the required attribute to the input element', async ({ page }) => { // Arrange - const component = await mount(PieRadio, { - props: { - required: true, - value: 'testValue', - } as RadioProps, - }); + const props : RadioProps = { + required: true, + value: 'testValue', + }; + + const radioDefaultPage = new BasePage(page, 'radio--default'); + await radioDefaultPage.load({ ...props }); // Act - const radio = component.locator(inputSelector); + const radioInput = page.getByTestId(radio.selectors.input.dataTestId); // Assert - expect((await radio.getAttribute('required'))).toBe(''); + await expect(radioInput).toHaveAttribute('required'); }); - test('should be in a valid state if the radio is required and checked', async ({ mount, page }) => { + test('should be in a valid state if the radio is required and checked', async ({ page }) => { // Arrange - await mount(PieRadio, { - props: { - checked: true, - required: true, - name: 'radio', - value: 'testValue', - } as RadioProps, - }); + const props : RadioProps = { + checked: true, + required: true, + name: 'radio', + value: 'testValue', + }; + const radioDefaultPage = new BasePage(page, 'radio--default'); + await radioDefaultPage.load({ ...props }); // Act - const isValid = await page.evaluate(() => document.querySelector('pie-radio')?.validity.valid); + const radioComponent = page.getByTestId(radio.selectors.container.dataTestId); + const isValid = await radioComponent.evaluate((el) => (el as HTMLInputElement).validity.valid); // Assert expect(isValid).toBe(true); }); - test('should be in a valid state if the radio is required and checked manually', async ({ mount, page }) => { + test('should be in a valid state if the radio is required and checked manually', async ({ page }) => { // Arrange - const component = await mount(PieRadio, { - props: { - required: true, - checked: false, - name: 'radio', - value: 'testValue', - } as RadioProps, - }); + const props : RadioProps = { + required: true, + checked: false, + name: 'radio', + value: 'testValue', + }; + + const radioDefaultPage = new BasePage(page, 'radio--default'); + await radioDefaultPage.load({ ...props }); // Act - await component.locator(inputSelector).click(); - const isValid = await page.evaluate(() => document.querySelector('pie-radio')?.validity.valid); - const isValueMissing = await page.evaluate(() => document.querySelector('pie-radio')?.validity.valueMissing); + const radioInput = page.getByTestId(radio.selectors.input.dataTestId); + await radioInput.click(); + + const radioComponent = page.getByTestId(radio.selectors.container.dataTestId); + const isValid = await radioComponent.evaluate((el) => (el as HTMLInputElement).validity.valid); + const isValueMissing = await radioComponent.evaluate((el) => (el as HTMLInputElement).validity.valueMissing); // Assert expect(isValid).toBe(true); expect(isValueMissing).toBe(false); }); - test('should be in an invalid state if the radio is required but unchecked', async ({ mount, page }) => { + test('should be in an invalid state if the radio is required but unchecked', async ({ page }) => { // Arrange - await mount(PieRadio, { - props: { - checked: false, - required: true, - name: 'radio', - value: 'testValue', - } as RadioProps, - }); + const props : RadioProps = { + checked: false, + required: true, + name: 'radio', + value: 'testValue', + }; + + const radioDefaultPage = new BasePage(page, 'radio--default'); + await radioDefaultPage.load({ ...props }); // Act - const isValid = await page.evaluate(() => document.querySelector('pie-radio')?.validity.valid); - const isValueMissing = await page.evaluate(() => document.querySelector('pie-radio')?.validity.valueMissing); + const radioComponent = page.getByTestId(radio.selectors.container.dataTestId); + const isValid = await radioComponent.evaluate((el) => (el as HTMLInputElement).validity.valid); + const isValueMissing = await radioComponent.evaluate((el) => (el as HTMLInputElement).validity.valueMissing); // Assert expect(isValid).toBe(false); expect(isValueMissing).toBe(true); }); - test('should be in a valid state if the radio is checked but not required', async ({ mount, page }) => { + test('should be in a valid state if the radio is checked but not required', async ({ page }) => { // Arrange - await mount(PieRadio, { - props: { - checked: true, - required: false, - name: 'radio', - value: 'testValue', - } as RadioProps, - }); + const props : RadioProps = { + checked: true, + required: false, + name: 'radio', + value: 'testValue', + }; + + const radioDefaultPage = new BasePage(page, 'radio--default'); + await radioDefaultPage.load({ ...props }); // Act - const isValid = await page.evaluate(() => document.querySelector('pie-radio')?.validity.valid); - const isValueMissing = await page.evaluate(() => document.querySelector('pie-radio')?.validity.valueMissing); + const radioComponent = page.getByTestId(radio.selectors.container.dataTestId); + const isValid = await radioComponent.evaluate((el) => (el as HTMLInputElement).validity.valid); + const isValueMissing = await radioComponent.evaluate((el) => (el as HTMLInputElement).validity.valueMissing); // Assert expect(isValid).toBe(true); expect(isValueMissing).toBe(false); }); - test('should be in a valid state if the radio is unchecked and not required', async ({ mount, page }) => { + test('should be in a valid state if the radio is unchecked and not required', async ({ page }) => { // Arrange - await mount(PieRadio, { - props: { - required: false, - checked: false, - name: 'radio', - value: 'testValue', - } as RadioProps, - }); + const props : RadioProps = { + required: false, + checked: false, + name: 'radio', + value: 'testValue', + }; + + const radioDefaultPage = new BasePage(page, 'radio--default'); + await radioDefaultPage.load({ ...props }); // Act - const isValid = await page.evaluate(() => document.querySelector('pie-radio')?.validity.valid); - const isValueMissing = await page.evaluate(() => document.querySelector('pie-radio')?.validity.valueMissing); + const radioComponent = page.getByTestId(radio.selectors.container.dataTestId); + const isValid = await radioComponent.evaluate((el) => (el as HTMLInputElement).validity.valid); + const isValueMissing = await radioComponent.evaluate((el) => (el as HTMLInputElement).validity.valueMissing); // Assert expect(isValid).toBe(true); @@ -328,97 +306,81 @@ test.describe('PieRadio - Component tests', () => { test.describe('Events', () => { test.describe('change', () => { test.describe('when radio is clicked', () => { - test('should dispatch a change event that contains the original native event', async ({ mount }) => { + test('should dispatch a change event that contains the original native event', async ({ page }) => { // Arrange - const messages: CustomEvent[] = []; - const expectedMessages = [{ sourceEvent: { isTrusted: true } }]; + const expectedMessages = [{ isTrusted: false }]; - const component = await mount(PieRadio, { - props: { - value: 'testValue', - } as RadioProps, - on: { - change: (data: CustomEvent) => { - messages.push(data); - }, - }, + const radioDefaultPage = new BasePage(page, 'radio--default'); + await radioDefaultPage.load(); + + // Set up listener for console messages + const consoleMessages: string[] = []; + page.on('console', (message) => { + if (message.type() === 'info') { + consoleMessages.push(message.text()); + } }); // Act - await component.locator(inputSelector).click(); + await page.getByTestId(radio.selectors.input.dataTestId).click(); // Assert - expect(messages.length).toEqual(1); - expect(messages).toStrictEqual(expectedMessages); + expect(consoleMessages).toHaveLength(1); + const parsedMessages = consoleMessages.map((msg) => JSON.parse(msg)); + expect(parsedMessages).toStrictEqual(expectedMessages); }); }); test.describe('when inside a form which is reset', () => { test('should dispatch a change event', async ({ page }) => { // Arrange - await page.setContent(` -
- - -
-
- `); - - await page.evaluate(() => { - const radio = document.querySelector('pie-radio') as PieRadio; - const eventsContainer = document.querySelector('#eventsContainer') as HTMLDivElement; - - radio.addEventListener('change', () => { - const el = document.createElement('div'); - el.innerText = 'change event fired'; - eventsContainer.appendChild(el); - }); - }); + const props : RadioProps = { + checked: true, + value: 'testValue', + }; - // Act - await page.click('button[type="reset"]'); + const radioFormPage = new BasePage(page, 'radio--example-form'); + await radioFormPage.load({ ...props }); - const eventElements = await page.evaluate(() => { - const events = document.querySelectorAll('#eventsContainer > div'); - return Array.from(events).map((el) => el.innerHTML); + // Set up listener for console messages + const consoleMessages: string[] = []; + page.on('console', (message) => { + if (message.type() === 'info') { + consoleMessages.push(message.text()); + } }); + // Act + await page.locator('pie-button', { hasText: 'Reset' }).click(); + // Assert - expect(eventElements).toHaveLength(1); - expect(eventElements[0]).toBe('change event fired'); + expect(consoleMessages).toHaveLength(1); + expect(consoleMessages[0]).toBe('change event fired'); }); test('should not dispatch a change event if the value has not changed', async ({ page }) => { // Arrange - await page.setContent(` -
- - -
-
- `); - - await page.evaluate(() => { - const radio = document.querySelector('pie-radio') as PieRadio; - const eventsContainer = document.querySelector('#eventsContainer') as HTMLDivElement; - - radio.addEventListener('change', () => { - const el = document.createElement('div'); - el.innerText = 'change event fired'; - eventsContainer.appendChild(el); - }); - }); + const props : RadioProps = { + checked: false, + value: 'testValue', + }; - // Act - await page.click('button[type="reset"]'); + const radioFormPage = new BasePage(page, 'radio--example-form'); + await radioFormPage.load({ ...props }); - const eventElements = await page.evaluate(() => { - const events = document.querySelectorAll('#eventsContainer > div'); - return Array.from(events).map((el) => el.innerHTML); + // Set up listener for console messages + const consoleMessages: string[] = []; + page.on('console', (message) => { + if (message.type() === 'info') { + consoleMessages.push(message.text()); + } }); + // Act + await page.locator('pie-button', { hasText: 'Reset' }).click(); + // Assert - expect(eventElements).toHaveLength(0); + expect(consoleMessages).toHaveLength(0); }); }); }); @@ -427,106 +389,70 @@ test.describe('PieRadio - Component tests', () => { test.describe('Form integration', () => { test('should correctly set the name and value of the radio in the FormData object when submitted', async ({ page }) => { // Arrange - await page.setContent(` -
- - -
-
- `); + const props : RadioProps = { + value: 'testValue', + name: 'testName', + }; - await setupFormDataExtraction(page, '#testForm', '#formDataJson'); + const radioFormPage = new BasePage(page, 'radio--example-form'); + await radioFormPage.load({ ...props }); // Act - await page.locator('pie-radio').click(); - await page.click('button[type="submit"]'); - const formDataObj = await getFormDataObject(page, '#formDataJson'); + await page.getByTestId(radio.selectors.input.dataTestId).click(); + await page.locator('pie-button', { hasText: 'Submit' }).click(); // Assert - expect(formDataObj).toStrictEqual({ testName: 'testValue' }); + const formDataOutput = await page.locator('#formDataOutput').textContent(); + expect(formDataOutput).toBe('{"testName":"testValue"}'); }); test('should submit the updated checked state if the checked attribute is changed programmatically', async ({ page }) => { // Arrange - await page.setContent(` -
- - -
-
- `); + const props : RadioProps = { + checked: false, + value: 'testValue', + name: 'testName', + }; - await setupFormDataExtraction(page, '#testForm', '#formDataJson'); + const radioFormPage = new BasePage(page, 'radio--example-form'); + await radioFormPage.load({ ...props }); // Act - await page.locator('pie-radio').click(); + await page.getByTestId(radio.selectors.input.dataTestId).click(); - await page.evaluate(() => { - const radio = document.querySelector('pie-radio') as PieRadio; + await page.getByTestId(radio.selectors.container.dataTestId).evaluate((el) => { + const radio = el as PieRadio; radio.checked = false; return radio; }); - await page.click('button[type="submit"]'); + await page.locator('pie-button', { hasText: 'Submit' }).click(); - const formDataObj = await getFormDataObject(page, '#formDataJson'); + const formDataOutput = await page.locator('#formDataOutput').textContent(); // Assert - expect(formDataObj).toStrictEqual({}); + expect(formDataOutput).toStrictEqual('{}'); }); [true, false].forEach((checked) => { test(`should not submit the value if the input is disabled when checked is ${checked}`, async ({ page }) => { // Arrange - await page.setContent(` -
- - - -
-
- `); - await setupFormDataExtraction(page, '#testForm', '#formDataJson'); - - // Act - await page.click('button[type="submit"]'); - const formDataObj = await getFormDataObject(page, '#formDataJson'); + const props : RadioProps = { + checked, + disabled: true, + value: 'testValue', + name: 'testName', + }; - // Assert - expect(formDataObj).toStrictEqual({}); - }); - }); - - [true, false].forEach((checked) => { - test(`should not submit the value inside a disabled fieldset when checked is ${checked}`, async ({ page }) => { - // Arrange - await page.setContent(` -
-
- - -
- -
-
- `); - - await setupFormDataExtraction(page, '#testForm', '#formDataJson'); + const radioFormPage = new BasePage(page, 'radio--example-form'); + await radioFormPage.load({ ...props }); // Act - await page.click('button[type="submit"]'); - - const formDataObj = await getFormDataObject(page, '#formDataJson'); + await page.locator('pie-button', { hasText: 'Submit' }).click(); + const formDataOutput = await page.locator('#formDataOutput').textContent(); // Assert - expect(formDataObj).toStrictEqual({}); + expect(formDataOutput).toStrictEqual('{}'); }); }); @@ -537,25 +463,24 @@ test.describe('PieRadio - Component tests', () => { test(`should reset the radio to its default state when defaultChecked is ${defaultChecked} and checked is ${checked}`, async ({ page }) => { // Arrange - await page.setContent(` -
- - - -
`); - - let isChecked = await page.evaluate(() => document.querySelector('pie-radio')?.checked); + const props : RadioProps = { + defaultChecked, + checked, + name: 'testName', + value: 'testValue', + }; + + const radioFormPage = new BasePage(page, 'radio--example-form'); + await radioFormPage.load({ ...props }); + + let isChecked = await page.getByTestId(radio.selectors.container.dataTestId).evaluate((el) => (el as HTMLInputElement).checked); expect.soft(isChecked).toBe(checked); // Act - await page.click('button[type="reset"]'); + await page.locator('pie-button', { hasText: 'Reset' }).click(); // Assert - isChecked = await page.evaluate(() => document.querySelector('pie-radio')?.checked); + isChecked = await page.getByTestId(radio.selectors.container.dataTestId).evaluate((el) => (el as HTMLInputElement).checked); expect(isChecked).toBe(defaultChecked); }); }); diff --git a/packages/components/pie-radio/test/helpers/page-object/selectors.ts b/packages/components/pie-radio/test/helpers/page-object/selectors.ts new file mode 100644 index 0000000000..78cd908bea --- /dev/null +++ b/packages/components/pie-radio/test/helpers/page-object/selectors.ts @@ -0,0 +1,13 @@ +const radio = { + selectors: { + container: { + dataTestId: 'pie-radio', + }, + input: { + dataTestId: 'pie-radio-input', + }, + }, +}; +export { + radio, +}; diff --git a/packages/components/pie-radio/test/visual/pie-radio.spec.ts b/packages/components/pie-radio/test/visual/pie-radio.spec.ts index aed4961bc9..4122b520e8 100644 --- a/packages/components/pie-radio/test/visual/pie-radio.spec.ts +++ b/packages/components/pie-radio/test/visual/pie-radio.spec.ts @@ -1,112 +1,18 @@ -import { test } from '@sand4rt/experimental-ct-web'; +import { test } from '@playwright/test'; import percySnapshot from '@percy/playwright'; -import { - type PropObject, type WebComponentPropValues, type 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 { BasePage } from '@justeattakeaway/pie-webc-testing/src/helpers/page-object/base-page.ts'; +import { radio } from '../helpers/page-object/selectors.ts'; -import { percyWidths } from '@justeattakeaway/pie-webc-testing/src/percy/breakpoints.ts'; -import { setRTL } from '@justeattakeaway/pie-webc-testing/src/helpers/set-rtl-direction.ts'; +const readingDirections = ['ltr', 'rtl']; -import { PieRadio, type RadioProps } from '../../src/index.ts'; +readingDirections.forEach((dir) => { + test(`should render all prop variations for the direction: ${dir}`, async ({ page }) => { + // Arrange + const radioVariations = new BasePage(page, 'radio--variations'); + await radioVariations.load({}, { writingDirection: dir }); + await page.waitForSelector(radio.selectors.container.dataTestId); -const readingDirections = ['LTR', 'RTL']; - -const props: PropObject = { - checked: [true, false], - disabled: [true, false], - value: 'foo', -}; - -const renderTestPieRadio = (propVals: WebComponentPropValues) => { - let attributes = ''; - - if (propVals.disabled) attributes += ` disabled="${propVals.disabled}"`; - if (propVals.checked) attributes += ` checked="${propVals.checked}"`; - - return `Label`; -}; - -const componentPropsMatrix: WebComponentPropValues[] = getAllPropCombinations(props); -const componentPropsMatrixByCheckedState: Record = splitCombinationsByPropertyValue(componentPropsMatrix, 'checked'); -const componentVariants: string[] = Object.keys(componentPropsMatrixByCheckedState); - -test.beforeEach(async ({ mount }, testInfo) => { - testInfo.setTimeout(testInfo.timeout + 40000); - - // This ensures the radio component is registered in the DOM for each test. - // It appears to add them to a Playwright cache which we understand is required for the tests to work correctly. - const radioComponent = await mount(PieRadio); - await radioComponent.unmount(); -}); - -componentVariants.forEach((variant) => test(`should render all prop variations for the checked state: ${variant}`, async ({ page, mount }) => { - for (const combo of componentPropsMatrixByCheckedState[variant]) { - const testComponent: WebComponentTestInput = createTestWebComponent(combo, renderTestPieRadio); - const propKeyValues = ` - checked: ${testComponent.propValues.checked}, - disabled: ${testComponent.propValues.disabled}`; - - await mount( - WebComponentTestWrapper, - { - props: { propKeyValues }, - slots: { - component: testComponent.renderedString.trim(), - }, - }, - ); - } - - await percySnapshot(page, `PIE Radio - Checked State: ${variant}`, percyWidths); -})); - -for (const dir of readingDirections) { - test(`Labelled - ${dir}`, async ({ mount, page }) => { - if (dir === 'RTL') { - setRTL(page); - } - - await mount( - PieRadio, - { - props: { - checked: true, - } as RadioProps, - slots: { - default: 'Label', - }, - }, - ); - - await percySnapshot(page, `PIE Radio - Labelled - ${dir}`, percyWidths); + // Assert + await percySnapshot(page, `PIE Radio Variations: ${dir}`, { widths: [1280] }); }); - - test(`Long label text - ${dir}`, async ({ mount, page }) => { - if (dir === 'RTL') { - setRTL(page); - } - - await mount( - PieRadio, - { - props: { - checked: true, - } as RadioProps, - slots: { - default: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi pretium quam eget dolor imperdiet placerat. Aliquam sollicitudin erat sed est lobortis sollicitudin. Nam vulputate, mi vel finibus convallis, mi dolor molestie arcu, vel pulvinar urna neque et sapien. Aenean euismod faucibus turpis et efficitur. Sed porttitor dui at justo cursus pulvinar. Sed scelerisque aliquet diam sed feugiat. Fusce id lorem finibus, tempor nulla tempor, tincidunt odio. Mauris consequat lectus ex, eget lacinia dui finibus sit amet. Phasellus maximus posuere sapien eget condimentum. Nunc viverra pharetra blandit. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Cras vel auctor erat, id ultrices ipsum. Suspendisse erat elit, facilisis et mi eget, interdum imperdiet ligula. Donec faucibus, lectus id sollicitudin accumsan, libero erat iaculis metus, vel finibus quam leo at mauris. Etiam pretium vitae est tempor commodo.', - }, - }, - ); - - await percySnapshot(page, `PIE Radio - Long label text - ${dir}`, percyWidths); - }); -} +}); diff --git a/packages/components/pie-radio/turbo.json b/packages/components/pie-radio/turbo.json new file mode 100644 index 0000000000..04a81f9c00 --- /dev/null +++ b/packages/components/pie-radio/turbo.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://turborepo.org/schema.json", + "extends": [ + "//" + ], + "pipeline": { + "test:browsers": { + "cache": true, + "dependsOn": [], + "inputs": [ + "$TURBO_DEFAULT$", + "../../../apps/pie-storybook/stories/testing/pie-radio.test.stories.ts" + ] + }, + "test:browsers:ci": { + "cache": true, + "dependsOn": [], + "inputs": [ + "$TURBO_DEFAULT$", + "../../../apps/pie-storybook/stories/testing/pie-radio.test.stories.ts" + ] + }, + "test:visual": { + "cache": false, + "dependsOn": [], + "inputs": [ + "$TURBO_DEFAULT$", + "../../../apps/pie-storybook/stories/testing/pie-radio.test.stories.ts" + ] + }, + "test:visual:ci": { + "cache": false, + "dependsOn": [], + "inputs": [ + "$TURBO_DEFAULT$", + "../../../apps/pie-storybook/stories/testing/pie-radio.test.stories.ts" + ] + } + } +} \ No newline at end of file