From d361e18b0c89e61e25b9cb4a9f4f8d6af7222c99 Mon Sep 17 00:00:00 2001 From: Ben Siggery Date: Wed, 4 Dec 2024 12:48:37 +0000 Subject: [PATCH 1/3] refactor(pie-button): DSW-2369 to use @playwright/test \& storybook for testing --- .../testing/pie-button.test.stories.ts | 319 ++++++ .../test/utilities/index.test.ts | 6 +- configs/pie-components-config/index.js | 6 + configs/pie-components-config/package.json | 4 +- .../playwright-native-lit-config.js | 49 + .../playwright-native-visual-config.js | 52 + package.json | 8 +- .../playwright-lit-visual.config.ts | 6 +- .../pie-button/playwright-lit.config.ts | 4 +- packages/components/pie-button/src/index.ts | 2 + .../test/component/pie-button.spec.ts | 906 ++++++------------ .../pie-button-form-integration-selectors.ts | 51 + .../pie-button-form-integration.page.ts | 188 ++++ .../page-object/pie-button-selectors.ts | 11 + .../helpers/page-object/pie-button.page.ts | 38 + packages/components/pie-button/turbo.json | 25 + .../pie-chip/playwright-lit-visual.config.ts | 6 +- .../src/helpers/page-object/base-page.ts | 9 +- playwright-browsers.config.ts | 2 +- playwright-visual.config.ts | 2 +- yarn.lock | 353 +++++-- 21 files changed, 1346 insertions(+), 701 deletions(-) create mode 100644 apps/pie-storybook/stories/testing/pie-button.test.stories.ts create mode 100644 configs/pie-components-config/playwright-native-lit-config.js create mode 100644 configs/pie-components-config/playwright-native-visual-config.js create mode 100644 packages/components/pie-button/test/helpers/page-object/pie-button-form-integration-selectors.ts create mode 100644 packages/components/pie-button/test/helpers/page-object/pie-button-form-integration.page.ts create mode 100644 packages/components/pie-button/test/helpers/page-object/pie-button-selectors.ts create mode 100644 packages/components/pie-button/test/helpers/page-object/pie-button.page.ts create mode 100644 packages/components/pie-button/turbo.json diff --git a/apps/pie-storybook/stories/testing/pie-button.test.stories.ts b/apps/pie-storybook/stories/testing/pie-button.test.stories.ts new file mode 100644 index 0000000000..5edaf17dd6 --- /dev/null +++ b/apps/pie-storybook/stories/testing/pie-button.test.stories.ts @@ -0,0 +1,319 @@ +import { html, nothing } from 'lit'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { type Meta } from '@storybook/web-components'; + +import '@justeattakeaway/pie-button'; +import { + type ButtonProps as ButtonPropsBase, defaultProps, iconPlacements, responsiveSizes, sizes, types, variants, +} from '@justeattakeaway/pie-button'; +import '@justeattakeaway/pie-icons-webc/dist/IconPlusCircle.js'; + +import { createStory, type TemplateFunction, sanitizeAndRenderHTML } from '../../utilities'; +import { type SlottedComponentProps } from '../../types'; + +type ButtonProps = SlottedComponentProps & { + showSubmitButton?: boolean; + showNativeResetButton?: boolean; + renderIncorrectForm?: boolean; +}; +type ButtonStoryMeta = Meta; + +function handleClick () { + // eslint-disable-next-line no-console + console.log('Button clicked!'); +} + +const defaultArgs: ButtonProps = { + ...defaultProps, + iconPlacement: undefined, + slot: 'Label', + showNativeResetButton: false, + showSubmitButton: true, + renderIncorrectForm: false, +}; + +const buttonStoryMeta: ButtonStoryMeta = { + title: 'Button', + component: 'pie-button', + argTypes: { + tag: { + description: 'Choose the HTML element that will be used to render the button.
For this story, the prop has the value of `button`. See the Anchor story to interact with the component when this prop has a value of `a`.', + control: { + disable: true, + }, + defaultValue: { + summary: 'button', + }, + }, + size: { + description: 'Set the size of the button.', + control: 'select', + options: sizes, + defaultValue: { + summary: defaultProps.size, + }, + }, + type: { + description: 'Set the type of the button.

Set this to `submit` to reveal more controls relating to form submission.', + control: 'select', + options: types, + defaultValue: { + summary: defaultProps.type, + }, + }, + variant: { + description: 'Set the variant of the button.', + control: 'select', + options: variants, + defaultValue: { + summary: defaultProps.variant, + }, + }, + iconPlacement: { + description: 'Show a leading/trailing icon.

To use this with pie-button, you can pass an icon into the `icon` slot', + control: 'select', + options: [undefined, ...iconPlacements], + }, + disabled: { + description: 'If `true`, disables the button.', + control: 'boolean', + defaultValue: { + summary: defaultProps.disabled, + }, + }, + isFullWidth: { + description: 'If `true`, sets the button width to 100% of it’s container.', + control: 'boolean', + defaultValue: { + summary: defaultProps.isFullWidth, + }, + }, + isLoading: { + description: 'If `true`, displays a loading indicator inside the button.', + control: 'boolean', + defaultValue: { + summary: defaultProps.isLoading, + }, + }, + isResponsive: { + description: 'If `true`, uses the next larger size on wide viewports.

Set this to `true` to show the `responsiveSize` control.', + control: 'boolean', + defaultValue: { + summary: defaultProps.isResponsive, + }, + }, + slot: { + description: 'The default slot is used to pass the button text into the component.', + control: 'text', + defaultValue: { + summary: '', + }, + }, + name: { + description: 'The name of the button, submitted as a pair with the button\'s value as part of the form data, when that button is used to submit the form.', + control: 'text', + defaultValue: { + summary: '', + }, + if: { arg: 'type', eq: 'submit' }, + }, + value: { + description: 'Defines the value associated with the button\'s name when it\'s submitted with the form data. This value is passed to the server in params when the form is submitted using this button.', + control: 'text', + defaultValue: { + summary: '', + }, + if: { arg: 'type', eq: 'submit' }, + }, + responsiveSize: { + description: 'Set the size of the button when set as responsive for wider viewports.', + control: 'select', + options: ['', ...responsiveSizes], + defaultValue: { + summary: 'productive', + }, + if: { arg: 'isResponsive', eq: true }, + }, + href: { + description: 'Set the href attribute for the underlying anchor tag.', + control: 'text', + }, + target: { + description: 'Set the target attribute for the underlying anchor tag.', + control: 'text', + }, + rel: { + description: 'Set the rel attribute for the underlying anchor tag', + control: 'text', + }, + showSubmitButton: { + description: 'If `true`, the submit button will be rendered.', + control: 'boolean', + defaultValue: { + summary: true, + }, + }, + showNativeResetButton: { + description: 'If `true`, a native reset button will be rendered instead of the pie-button reset button.', + control: 'boolean', + defaultValue: { + summary: false, + }, + }, + }, + args: { + ...defaultArgs, + }, +}; + +export default buttonStoryMeta; + +const Template: TemplateFunction = ({ + size, + variant, + type, + disabled, + isFullWidth, + isLoading, + isResponsive, + slot, + iconPlacement, + name, + value, + responsiveSize, +}) => html` + + ${iconPlacement ? html`` : nothing} + ${sanitizeAndRenderHTML(slot)} +`; + +const FormTemplate: TemplateFunction = ({ + showSubmitButton, + showNativeResetButton, + renderIncorrectForm, + ...props +}) => html` + ${renderIncorrectForm ? html`
` : nothing} + +

Fake form

+
+

Required fields are followed by *.

+
+

Contact information

+

+ + +

+

+ + +

+

+ + +

+
+
+

Payment information

+

+ + +

+

+ + +

+

+ + +

+
+
+ ${showNativeResetButton ? html`` : Template({ + ...props, + variant: 'secondary', + slot: 'Reset', + type: 'reset', +})} + ${showSubmitButton ? Template({ + ...props, + variant: 'primary', + slot: 'Submit payment', + type: 'submit', +}) : nothing} +
+
+ +`; + +const createButtonStory = createStory(Template, defaultArgs); + +const createButtonStoryWithForm = createStory(FormTemplate, defaultArgs); + +const anchorOnlyProps : Array = ['href', 'target', 'rel']; + +export const Primary = createButtonStory({}, { + controls: { exclude: ['variant', ...anchorOnlyProps] }, +}); + +export const FormIntegration = createButtonStoryWithForm({ type: 'submit' }); diff --git a/apps/pie-storybook/test/utilities/index.test.ts b/apps/pie-storybook/test/utilities/index.test.ts index 6c0f33c4ab..a07bac7553 100644 --- a/apps/pie-storybook/test/utilities/index.test.ts +++ b/apps/pie-storybook/test/utilities/index.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { html, render, type TemplateResult} from 'lit'; +import { html, render, type TemplateResult } from 'lit'; import { createStory, createVariantStory } from '../../utilities/index'; import { type StoryOptions } from '../../types/StoryOptions'; @@ -23,8 +23,8 @@ describe('createStory', () => { const actualTemplateResult = result.render(defaultArgs); const expectedTemplateResult: Partial = { - strings: Object.assign(['\n \n']), - values: ['medium', 'primary'] + strings: Object.assign(['\n \n']), + values: ['medium', 'primary'], }; // Compare the key properties of the TemplateResult diff --git a/configs/pie-components-config/index.js b/configs/pie-components-config/index.js index 7d1ac93a76..ed621f48b1 100644 --- a/configs/pie-components-config/index.js +++ b/configs/pie-components-config/index.js @@ -1,4 +1,10 @@ +// @playwright/test +export * from './playwright-native-lit-config.js'; +export * from './playwright-native-visual-config.js'; + +// @sand4rt/experimental-ct-web export * from './playwright-lit-config.js'; export * from './playwright-lit-visual-config.js'; + export * from './vite.config.js'; export * from './custom-elements-manifest.config.js'; diff --git a/configs/pie-components-config/package.json b/configs/pie-components-config/package.json index 087e542d42..7cbc99f5ac 100644 --- a/configs/pie-components-config/package.json +++ b/configs/pie-components-config/package.json @@ -14,7 +14,9 @@ "tsconfig.json", "playwright/**", "playwright-lit-config.js", - "playwright-lit-visual-config.js" + "playwright-native-lit-config.js", + "playwright-lit-visual-config.js", + "playwright-native-visual-config.js" ], "dependencies": { "deepmerge-ts": "5.1.0" diff --git a/configs/pie-components-config/playwright-native-lit-config.js b/configs/pie-components-config/playwright-native-lit-config.js new file mode 100644 index 0000000000..cef63fd501 --- /dev/null +++ b/configs/pie-components-config/playwright-native-lit-config.js @@ -0,0 +1,49 @@ +import { devices } from '@playwright/test'; +import path from 'path'; + +/** + * See https://playwright.dev/docs/test-configuration + */ +export function getPlaywrightNativeConfig () { + return { + /* The base directory, relative to the config file, for snapshot files created with toMatchSnapshot and toHaveScreenshot. */ + snapshotDir: './__snapshots__', + /* Maximum time one test can run for. */ + timeout: 10 * 1000, + testIgnore: '*-react.spec.js', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: '50%', + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: [['html', { outputFolder: '../../../lit-browsers-report' }]], + + /* Configure projects for major browsers */ + projects: [ + { + name: 'component:chrome', + use: { + ...devices['Desktop Chrome'], + }, + testMatch: ['**/test/component/*.spec.{js,ts}'], + }, + { + name: 'a11y:chrome', + use: { + ...devices['Desktop Chrome'], + }, + testMatch: ['**/test/accessibility/*.spec.{js,ts}'], + }, + ], + webServer: !process.env.CI ? { + command: 'npx turbo dev:testing --filter=pie-storybook', + url: 'http://localhost:6006', + timeout: 120 * 10000, + reuseExistingServer: !process.env.CI, + } : undefined, + }; +} diff --git a/configs/pie-components-config/playwright-native-visual-config.js b/configs/pie-components-config/playwright-native-visual-config.js new file mode 100644 index 0000000000..b2a1e33b30 --- /dev/null +++ b/configs/pie-components-config/playwright-native-visual-config.js @@ -0,0 +1,52 @@ +import { devices } from '@playwright/test'; + +/** + * See https://playwright.dev/docs/test-configuration + */ +export function getPlaywrightNativeVisualConfig () { + return { + /* Maximum time one test can run for. */ + timeout: 10 * 1000, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: '50%', + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: [['html', { outputFolder: '../../../lit-visual-report' }]], + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on', + testIdAttribute: 'data-test-id', + discovery: { + disallowedHostnames: [ + 'unpkg.com' + ], + }, + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'visual:desktop', + grepInvert: /@mobile/, + use: { + ...devices['Desktop Chrome'], + }, + testMatch: ['**/test/visual/*.spec.ts'], + }, + { + name: 'visual:mobile', + grep: /@mobile/, + use: { + ...devices['Galaxy S8'], + }, + testMatch: ['**/test/visual/*.spec.ts'], + }, + ], + }; +} diff --git a/package.json b/package.json index b69bba1e83..2c3883a6e1 100644 --- a/package.json +++ b/package.json @@ -80,10 +80,10 @@ "@justeattakeaway/stylelint-config-pie": "0.8.0", "@percy/cli": "1.29.3", "@percy/playwright": "1.0.6", - "@playwright/experimental-ct-react": "1.41.0", - "@playwright/test": "1.41.0", + "@playwright/experimental-ct-react": "1.49.0", + "@playwright/test": "1.49.0", "@rollup/plugin-node-resolve": "15.1.0", - "@sand4rt/experimental-ct-web": "1.41.0", + "@sand4rt/experimental-ct-web": "1.49.0", "@types/node": "20.4.8", "@types/react": "18.3.11", "@typescript-eslint/eslint-plugin": "5.62.0", @@ -139,6 +139,6 @@ "resolutions": { "lit": "3.2.0", "tar": "6.2.1", - "@playwright/experimental-ct-core": "1.41.0" + "@playwright/experimental-ct-core": "1.49.0" } } diff --git a/packages/components/pie-button/playwright-lit-visual.config.ts b/packages/components/pie-button/playwright-lit-visual.config.ts index fb0f14c480..2fd82d7d5f 100644 --- a/packages/components/pie-button/playwright-lit-visual.config.ts +++ b/packages/components/pie-button/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-button/playwright-lit.config.ts b/packages/components/pie-button/playwright-lit.config.ts index e50b9373b3..9828bca0dd 100644 --- a/packages/components/pie-button/playwright-lit.config.ts +++ b/packages/components/pie-button/playwright-lit.config.ts @@ -1,4 +1,4 @@ import { defineConfig } from '@sand4rt/experimental-ct-web'; -import { getPlaywrightConfig } from '@justeattakeaway/pie-components-config'; +import { getPlaywrightNativeConfig } from '@justeattakeaway/pie-components-config'; -export default defineConfig(getPlaywrightConfig()); +export default defineConfig(getPlaywrightNativeConfig()); diff --git a/packages/components/pie-button/src/index.ts b/packages/components/pie-button/src/index.ts index 7c7dbe7876..5f98bb5c94 100644 --- a/packages/components/pie-button/src/index.ts +++ b/packages/components/pie-button/src/index.ts @@ -30,6 +30,8 @@ export class PieButton extends FormControlMixin(LitElement) implements ButtonPro connectedCallback () { super.connectedCallback(); + this.setAttribute('data-test-id', `pie-button--${this.type}`); + if (this.type === 'submit') { this.form?.addEventListener('keydown', this._handleFormKeyDown); } 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 c845789874..7ac2cb3c77 100644 --- a/packages/components/pie-button/test/component/pie-button.spec.ts +++ b/packages/components/pie-button/test/component/pie-button.spec.ts @@ -1,6 +1,16 @@ -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 { PieButton, type ButtonProps } from '../../src/index.ts'; +import { test, expect } from '@playwright/test'; +import { ButtonComponent } from '../helpers/page-object/pie-button.page.ts'; +import { FormIntegrationPage, type FormInput } from '../helpers/page-object/pie-button-form-integration.page.ts'; +import type { ButtonProps } from '../../src/index.ts'; + +const formInput: FormInput = { + userName: 'John Doe', + userEmail: 'john.doe@example.com', + userPassword: 'password', + userPaymentCardType: 'mastercard', + userPaymentCardNumber: '4921111111111111', + userPaymentCardExpiration: '12/24', +}; type SizeResponsiveSize = { sizeName: ButtonProps['size']; @@ -15,501 +25,269 @@ const sizes: Array = [ { sizeName: 'large', responsiveSize: '--btn-height--large' }, ]; -test('should correctly work with native click events', async ({ mount }) => { - const messages: string[] = []; - const expectedEventMessage = 'Native event dispatched'; - const component = await mount( - PieButton, - { - slots: { - default: 'Click me!', - }, - on: { - click: () => { - messages.push(expectedEventMessage); - }, - }, - }, - ); - - await component.click(); - - expect(messages).toEqual([expectedEventMessage]); -}); +test('should correctly work with native click events', async ({ page }) => { + const buttonComponent = new ButtonComponent(page, 'button--primary'); + await buttonComponent.load(); -test.describe('Form Actions', () => { - test.beforeEach(async ({ mount }) => { - const component = await mount(PieButton); - await component.unmount(); + // Set up a listener for console messages + const consoleMessages: string[] = []; + page.on('console', (message) => { + if (message.type() === 'log') { + consoleMessages.push(message.text()); + } }); - test.describe('Submit', () => { - test('should correctly submit an HTML form when type is `submit`', async ({ page }) => { - // Arrange - // Inject the test form into the page - await page.evaluate(() => { - const formHTML = ` -
- - - Submit -
- `; - document.body.innerHTML = formHTML; - }); - - // Set up the form submission listener - await page.evaluate(() => { - const form = document.querySelector('#testForm') as HTMLFormElement; - - form.addEventListener('submit', (e) => { - e.preventDefault(); // Prevent the actual submission - we only care about detecting the submit + await buttonComponent.click(); - // Append a hidden span element to the body - const span = document.createElement('span'); - span.id = 'formSubmittedFlag'; - span.style.display = 'none'; - document.body.appendChild(span); - }); - }); + // Assert that the expected message was logged + expect(consoleMessages).toContain('Button clicked!'); +}); - // Fill out the form - await page.fill('#username', 'testUser'); - await page.fill('#password', 'testPassword'); +test.describe('Form Actions', () => { + test.describe('Submit', () => { + test('should correctly submit an HTML form when type is `submit`', async ({ page }) => { + // Arrange + const formIntegrationPage = new FormIntegrationPage(page, 'button--form-integration'); + await formIntegrationPage.load(); // Act - await page.click('#submitButton'); + await formIntegrationPage.fillForm(formInput); + + await formIntegrationPage.clickFormButton('pie-button-submit'); // Assert // Check if the hidden span was appended, indicating the form was submitted - const wasFormSubmitted = Boolean(await page.$('#formSubmittedFlag')); - - expect(wasFormSubmitted).toBe(true); + expect(await formIntegrationPage.formSubmittedFlag).toHaveCount(1); + expect(await formIntegrationPage.formSubmittedFlag).toBeHidden(); }); test('should trigger native HTML form validation for required fields and submit after correcting when type is `submit`', async ({ page }) => { - // Arrange - // Inject the test form into the page - await page.evaluate(() => { - const formHTML = ` -
- - - Submit -
- `; - document.body.innerHTML = formHTML; - }); - - // Set up the form submission listener - await page.evaluate(() => { - const form = document.querySelector('#testForm') as HTMLFormElement; - - form.addEventListener('submit', (e) => { - e.preventDefault(); // Prevent the actual submission - we only care about detecting the submit - - // Add a span to indicate the form was submitted - const span = document.createElement('span'); - span.id = 'formSubmittedFlag'; - document.body.appendChild(span); - }); - }); + // Arrange + const formIntegrationPage = new FormIntegrationPage(page, 'button--form-integration'); + await formIntegrationPage.load(); // Act & Assert // Attempt to click the submit button without filling out required fields - await page.click('#submitButton'); + await formIntegrationPage.clickFormButton('pie-button-submit'); // Check if the form was not submitted (indicated by the absence of the span) - let formSubmittedFlagExists = Boolean(await page.$('#formSubmittedFlag')); - expect(formSubmittedFlagExists).toBe(false); + expect.soft(await formIntegrationPage.formSubmittedFlag).toHaveCount(0); // Fill out the form - await page.fill('#username', 'testUser'); - await page.fill('#password', 'testPassword'); + await formIntegrationPage.fillForm(formInput); // Attempt to submit the form again - await page.click('pie-button[type="submit"]'); + await formIntegrationPage.clickFormButton('pie-button-submit'); // Check if the form was submitted this time (indicated by the presence of the span) - formSubmittedFlagExists = Boolean(await page.$('#formSubmittedFlag')); - expect(formSubmittedFlagExists).toBe(true); + expect(await formIntegrationPage.formSubmittedFlag).toHaveCount(1); }); test('should not submit the form when button is disabled and type is `submit`', async ({ page }) => { // Arrange - await page.evaluate(() => { - const formHTML = ` -
- - - Submit -
- `; - document.body.innerHTML = formHTML; - }); - - // Set up the form submission listener - await page.evaluate(() => { - const form = document.querySelector('#testForm') as HTMLFormElement; - - form.addEventListener('submit', (e) => { - e.preventDefault(); - - const span = document.createElement('span'); - span.id = 'formSubmittedFlag'; - document.body.appendChild(span); - }); - }); + const formIntegrationPage = new FormIntegrationPage(page, 'button--form-integration'); + await formIntegrationPage.load({ disabled: true }); // Act - await page.click('#submitButton'); + await formIntegrationPage.clickFormButton('pie-button-submit'); // Assert - const formSubmittedFlagExists = Boolean(await page.$('#formSubmittedFlag')); - expect(formSubmittedFlagExists).toBe(false); // Form should not submit + expect(await formIntegrationPage.formSubmittedFlag).toHaveCount(0); }); test('should not submit the form when button has isLoading set and type is `submit`', async ({ page }) => { // Arrange - await page.evaluate(() => { - const formHTML = ` -
- - - Submit -
- `; - document.body.innerHTML = formHTML; - }); - - // Set up the form submission listener - await page.evaluate(() => { - const form = document.querySelector('#testForm') as HTMLFormElement; - - form.addEventListener('submit', (e) => { - e.preventDefault(); - - const span = document.createElement('span'); - span.id = 'formSubmittedFlag'; - document.body.appendChild(span); - }); - }); - - // Act - await page.click('#submitButton'); - - // Assert - const formSubmittedFlagExists = Boolean(await page.$('#formSubmittedFlag')); - expect(formSubmittedFlagExists).toBe(false); // Form should not submit - }); - - test('should include pie-button\'s name and value in the form submission data when it triggers submission', async ({ page }) => { - // Arrange - // Inject the test form into the page with pie-button having name and value attributes - await page.evaluate(() => { - const formHTML = ` -
- - Submit -
- `; - document.body.innerHTML = formHTML; - }); - - // Intercept form submissions - const [request] = await Promise.all([ - page.waitForRequest('/submit-endpoint'), - page.fill('input[name="username"]', 'testUser'), - page.click('#testButton'), - ]); - - const formData = request.postData(); - - // Assert - expect(formData).toContain('submitButton=submitValue'); - }); - - test('should respect all form-related attributes on the pie-button', async ({ page }) => { - // Arrange - // Inject the test form into the page with pie-button having multiple form attributes - await page.evaluate(() => { - const formHTML = ` -
- - Submit -
- `; - document.body.innerHTML = formHTML; - }); - - // Act - // Intercept form submissions - const [request] = await Promise.all([ - page.waitForRequest('/custom-endpoint'), - page.fill('input[name="username"]', 'testUser'), - page.click('#testButton'), - ]); - - const postData = request.postData(); - const method = request.method(); - const headers = request.headers(); - - // Assert - expect(postData).toBeTruthy(); - const submitButtonDisposition = 'Content-Disposition: form-data; name="submitButton"'; - const submitButtonValuePosition = (postData as string).indexOf(submitButtonDisposition) + submitButtonDisposition.length; - expect((postData as string).includes(submitButtonDisposition)).toBeTruthy(); - expect((postData as string).substring(submitButtonValuePosition)).toContain('submitValue'); - expect(headers['content-type']).toMatch(/^multipart\/form-data;/); - expect(method).toBe('POST'); - }); - - test('should submit the form when pressing Enter with pie-button type `submit`', async ({ page }) => { - // Arrange - await page.evaluate(() => { - const formHTML = ` -
- - - Submit -
- `; - document.body.innerHTML = formHTML; - }); - - // Set up the form submission listener - await page.evaluate(() => { - const form = document.querySelector('#testForm') as HTMLFormElement; - - form.addEventListener('submit', (e) => { - e.preventDefault(); - - const span = document.createElement('span'); - span.id = 'formSubmittedFlag'; - document.body.appendChild(span); - }); - }); - - // Fill out the form - await page.fill('#username', 'testUser'); - await page.fill('#password', 'testPassword'); + const formIntegrationPage = new FormIntegrationPage(page, 'button--form-integration'); + await formIntegrationPage.load({ isLoading: true }); // Act - // Press Enter in the password field - await page.focus('#password'); - await page.press('#password', 'Enter'); + await formIntegrationPage.clickFormButton('pie-button-submit'); // Assert - const formSubmittedFlagExists = Boolean(await page.$('#formSubmittedFlag')); - expect(formSubmittedFlagExists).toBe(true); + expect(await formIntegrationPage.formSubmittedFlag).toHaveCount(0); }); - test('should NOT submit the form when pressing Enter with pie-button type other than `submit`', async ({ page }) => { + /* eslint-disable vitest/no-commented-out-tests */ + // test('NEED TO REFACTOR should include pie-button\'s name and value in the form submission data when it triggers submission', async ({ page }) => { + // // Arrange + // // Inject the test form into the page with pie-button having name and value attributes + // await page.evaluate(() => { + // const formHTML = ` + //
+ // + // Submit + //
+ // `; + // document.body.innerHTML = formHTML; + // }); + + // // Intercept form submissions + // const [request] = await Promise.all([ + // page.waitForRequest('/submit-endpoint'), + // page.fill('input[name="username"]', 'testUser'), + // page.click('#testButton'), + // ]); + + // const formData = request.postData(); + + // // Assert + // expect(formData).toContain('submitButton=submitValue'); + // }); + + // test('NEED TO REFACTOR - should respect all form-related attributes on the pie-button', async ({ page }) => { + // // Arrange + // // Inject the test form into the page with pie-button having multiple form attributes + // await page.evaluate(() => { + // const formHTML = ` + //
+ // + // Submit + //
+ // `; + // document.body.innerHTML = formHTML; + // }); + + // // Act + // // Intercept form submissions + // const [request] = await Promise.all([ + // page.waitForRequest('/custom-endpoint'), + // page.fill('input[name="username"]', 'testUser'), + // page.click('#testButton'), + // ]); + + // const postData = request.postData(); + // const method = request.method(); + // const headers = request.headers(); + + // // Assert + // expect(postData).toBeTruthy(); + // const submitButtonDisposition = 'Content-Disposition: form-data; name="submitButton"'; + // const submitButtonValuePosition = (postData as string).indexOf(submitButtonDisposition) + submitButtonDisposition.length; + // expect((postData as string).includes(submitButtonDisposition)).toBeTruthy(); + // expect((postData as string).substring(submitButtonValuePosition)).toContain('submitValue'); + // expect(headers['content-type']).toMatch(/^multipart\/form-data;/); + // expect(method).toBe('POST'); + // }); + + /* eslint-enable vitest/no-commented-out-tests */ + + const submitTestCases = [ + { titlePrefix: 'should submit', showSubmitButton: true, expectedFormSubmittedFlagCount: 1 }, + { titlePrefix: 'should not submit', showSubmitButton: false, expectedFormSubmittedFlagCount: 0 }, + ]; + + submitTestCases.forEach((testCase) => { + test(` ${testCase.titlePrefix} the form when pressing Enter with pie-button type other than 'submit'`, async ({ page }) => { // Arrange - await page.evaluate(() => { - const formHTML = ` -
- - - Reset -
- `; - document.body.innerHTML = formHTML; - }); + const formIntegrationPage = new FormIntegrationPage(page, 'button--form-integration'); + await formIntegrationPage.load({ showSubmitButton: testCase.showSubmitButton }); - // Set up the form submission listener - await page.evaluate(() => { - const form = document.querySelector('#testForm') as HTMLFormElement; + // Fill out the form + await formIntegrationPage.fillForm(formInput); - form.addEventListener('submit', (e) => { - e.preventDefault(); + // Act + // Press Enter in the password field + await formIntegrationPage.focusField('userPassword').then((locator) => locator.press('Enter')); - const span = document.createElement('span'); - span.id = 'formSubmittedFlag'; - document.body.appendChild(span); - }); + // Assert + expect(await formIntegrationPage.formSubmittedFlag).toHaveCount(testCase.expectedFormSubmittedFlagCount); }); - - // Fill out the form - await page.fill('#username', 'testUser'); - await page.fill('#password', 'testPassword'); - - // Act - // Press Enter in the password field - await page.focus('#password'); - await page.press('#password', 'Enter'); - - // Assert - const formSubmittedFlagExists = Boolean(await page.$('#formSubmittedFlag')); - expect(formSubmittedFlagExists).toBe(false); }); test('should NOT submit the form when pressing Enter on a pie-button that is not type of submit', async ({ page }) => { // Arrange - await page.evaluate(() => { - const formHTML = ` -
- - - Submit - Reset -
- `; - document.body.innerHTML = formHTML; - - const form = document.querySelector('#testForm') as HTMLFormElement; - - form.addEventListener('submit', (e) => { - e.preventDefault(); - - const span = document.createElement('span'); - span.id = 'formSubmittedFlag'; - document.body.appendChild(span); - }); - }); + const formIntegrationPage = new FormIntegrationPage(page, 'button--form-integration'); + await formIntegrationPage.load(); // Fill out the form - await page.fill('#username', 'testUser'); - await page.fill('#password', 'testPassword'); + await formIntegrationPage.fillForm(formInput); // Act - // Press Tab until we focus the reset button await page.keyboard.press('Tab'); - await page.keyboard.press('Tab'); - - // Press Enter on the pie-button with type reset - await page.press('#resetPieButton', 'Enter'); + await page.keyboard.press('Enter'); // Assert - const formSubmittedFlagExists = Boolean(await page.$('#formSubmittedFlag')); - expect(formSubmittedFlagExists).toBe(false); + expect(await formIntegrationPage.formSubmittedFlag).toHaveCount(0); }); test('should NOT submit the form when pressing Enter on a native non-submit button', async ({ page }) => { // Arrange - await page.evaluate(() => { - const formHTML = ` -
- - - Submit - -
- `; - document.body.innerHTML = formHTML; - - const form = document.querySelector('#testForm') as HTMLFormElement; - - form.addEventListener('submit', (e) => { - e.preventDefault(); - - const span = document.createElement('span'); - span.id = 'formSubmittedFlag'; - document.body.appendChild(span); - }); - }); + const formIntegrationPage = new FormIntegrationPage(page, 'button--form-integration'); + await formIntegrationPage.load({ showNativeResetButton: true }); - // Fill out the form - await page.fill('#username', 'testUser'); - await page.fill('#password', 'testPassword'); + await formIntegrationPage.fillForm(formInput); // Act // Press Enter on the native button with type reset - await page.focus('#resetNativeButton'); - await page.press('#resetNativeButton', 'Enter'); + await formIntegrationPage.focusButton('native-reset').then((locator) => locator.press('Enter')); // Assert - const formSubmittedFlagExists = Boolean(await page.$('#formSubmittedFlag')); - expect(formSubmittedFlagExists).toBe(false); + expect(await formIntegrationPage.formSubmittedFlag).toHaveCount(0); }); }); test.describe('Reset', () => { test('should reset the form by clicking the reset button when type is `reset`', async ({ page }) => { - // Arrange - // Inject the test form into the page with a reset button - await page.evaluate(() => { - const formHTML = ` -
- - - Reset - Submit -
- `; - document.body.innerHTML = formHTML; - }); + // Arrange + const formIntegrationPage = new FormIntegrationPage(page, 'button--form-integration'); + await formIntegrationPage.load(); - // Fill out the form - const testUsername = 'testUser'; - const testPassword = 'testPassword'; - await page.fill('#username', testUsername); - await page.fill('#password', testPassword); + await formIntegrationPage.fillForm(formInput); // Act - await page.click('#resetButton'); + await formIntegrationPage.clickFormButton('pie-button-reset'); - // Assert - // Check if the form fields have been reset - const usernameValue = await page.getByTestId('username').inputValue(); - const passwordValue = await page.getByTestId('password').inputValue(); + // Assert - Check if the form fields have been reset + const formFieldValues = await formIntegrationPage.getFormFieldValues(); - expect(usernameValue).toBe(''); - expect(passwordValue).toBe(''); + const expectedFormFieldValues: FormInput = { + userName: '', + userEmail: '', + userPassword: '', + userPaymentCardType: 'visa', + userPaymentCardNumber: '', + userPaymentCardExpiration: '', + }; + + expect(formFieldValues).toEqual(expectedFormFieldValues); }); test('should not reset the form when button is disabled and type is `reset`', async ({ page }) => { // Arrange - await page.evaluate(() => { - const formHTML = ` -
- - Reset -
- `; - document.body.innerHTML = formHTML; - }); + const formIntegrationPage = new FormIntegrationPage(page, 'button--form-integration'); + await formIntegrationPage.load({ disabled: true }); - // Change the form input value - await page.fill('#username', 'changedValue'); + await formIntegrationPage.fillForm(formInput); // Act - await page.click('#resetButton'); + await formIntegrationPage.clickFormButton('pie-button-reset'); // Assert - const inputValue = await page.inputValue('#username'); - expect(inputValue).toBe('changedValue'); // Input value should remain as 'changedValue' and not be reset to 'initialValue' + const formFieldValues = await formIntegrationPage.getFormFieldValues(); + expect(formFieldValues).toEqual(formInput); }); test('should not reset the form when button has `isLoading` set and type is `reset`', async ({ page }) => { - // Arrange - await page.evaluate(() => { - const formHTML = ` -
- - Reset -
- `; - document.body.innerHTML = formHTML; - }); + // Arrange + const formIntegrationPage = new FormIntegrationPage(page, 'button--form-integration'); + await formIntegrationPage.load({ isLoading: true }); - // Change the form input value - await page.fill('#username', 'changedValue'); + await formIntegrationPage.fillForm(formInput); // Act - await page.click('#resetButton'); + await formIntegrationPage.clickFormButton('pie-button-reset'); // Assert - const inputValue = await page.inputValue('#username'); - expect(inputValue).toBe('changedValue'); // Input value should remain as 'changedValue' and not be reset to 'initialValue' + const formFieldValues = await formIntegrationPage.getFormFieldValues(); + expect(formFieldValues).toEqual(formInput); }); }); @@ -517,24 +295,14 @@ test.describe('Form Actions', () => { test('should correctly associate with its containing form and not with other forms', async ({ page }) => { // Arrange // Inject two forms into the page - await page.evaluate(() => { - const formsHTML = ` -
- Submit -
-
- `; - document.body.innerHTML = formsHTML; - }); + const formIntegrationPage = new FormIntegrationPage(page, 'button--form-integration'); + await formIntegrationPage.load({ renderIncorrectForm: true }); // Act - const associatedFormId = await page.evaluate(() => { - const button = document.querySelector('#testButton') as HTMLButtonElement; - return button.form ? button.form.id : null; - }); + const associatedFormId = await formIntegrationPage.getAssociatedFormIdForButton('pie-button-submit'); // Assert - expect(associatedFormId).toBe('correctForm'); + expect(associatedFormId).toBe('testForm'); }); }); }); @@ -542,56 +310,29 @@ test.describe('Form Actions', () => { test.describe('props', () => { test.describe('isResponsive', () => { test.describe('when not set', () => { - test('the button should not have the attribute', async ({ mount }) => { - const component = await mount( - PieButton, - { - slots: { - default: 'Click me!', - }, - }, - ); - - await expect(component.locator('button')) + test('the button should not have the attribute', async ({ page }) => { + const buttonComponent = new ButtonComponent(page, 'button--primary'); + await buttonComponent.load({ isResponsive: false }); + + await expect(buttonComponent.componentLocator.locator('button')) .not.toHaveClass(/o-btn--responsive/); }); }); + test.describe('when set to true', () => { - test('the button should have the attribute', async ({ mount }) => { - const props: ButtonProps = { - size: 'xsmall', - isResponsive: true, - }; - - const component = await mount( - PieButton, - { - props, - slots: { - default: 'Click me!', - }, - }, - ); - - await expect(component.locator('button')) - .toHaveClass(/o-btn--responsive/); + test('the button should have the attribute', async ({ page }) => { + const buttonComponent = new ButtonComponent(page, 'button--primary'); + await buttonComponent.load({ isResponsive: true }); + + expect(await buttonComponent.isResponsive()).toBe(true); }); sizes.forEach(({ sizeName, responsiveSize }) => { - test(`a "${sizeName}" size button height should be equivalent to "${responsiveSize}"`, async ({ mount }) => { - const props: ButtonProps = { - size: sizeName, - isResponsive: true, - }; - - const component = await mount(PieButton, { - props, - slots: { - default: 'Click me!', - }, - }); + test(`a "${sizeName}" size button height should be equivalent to "${responsiveSize}"`, async ({ page }) => { + const buttonComponent = new ButtonComponent(page, 'button--primary'); + await buttonComponent.load({ isResponsive: true, size: sizeName }); - const [currentHeight, expectedHeight] = await getShadowElementStylePropValues(component, 'button', ['--btn-height', responsiveSize]); + const [currentHeight, expectedHeight] = await buttonComponent.getShadowElementStylePropValues(['--btn-height', responsiveSize]); await expect(currentHeight).toBe(expectedHeight); }); @@ -601,174 +342,117 @@ test.describe('props', () => { test.describe('responsiveSize', () => { test.describe('when not set', () => { - test('the button should not have the attribute', async ({ mount }) => { - const component = await mount( - PieButton, - { - slots: { - default: 'Click me!', - }, - }, - ); - - await expect(component.locator('button')) - .not.toHaveClass([/o-btn--productive/, /o-btn--expressive/]); - }); - }); + test('the button should not have the attribute', async ({ page }) => { + const buttonComponent = new ButtonComponent(page, 'button--primary'); + await buttonComponent.load(); - test.describe('when "isResponsive" is true', () => { - test.describe('when "responsiveSize" is "expressive"', () => { - test('the button should have the expected attribute', async ({ mount }) => { - const props: ButtonProps = { - size: 'xsmall', - isResponsive: true, - responsiveSize: 'expressive', - }; - - const component = await mount( - PieButton, - { - props, - slots: { - default: 'Click me!', - }, - }, - ); - - await expect(component.locator('button')) - .toHaveClass([/o-btn--expressive/]); - }); + await expect(buttonComponent.componentLocator.locator('button')).not.toHaveClass([/o-btn--productive/, /o-btn--responsive/]); }); }); - test.describe('when "responsiveSize" is "productive"', () => { - test('the button should have the expected attribute', async ({ mount }) => { - const props: ButtonProps = { - size: 'xsmall', - isResponsive: true, - responsiveSize: 'productive', - }; - - const component = await mount( - PieButton, - { - props, - slots: { - default: 'Click me!', - }, - }, - ); - - await expect(component.locator('button')) - .toHaveClass(/o-btn--productive/); + const responsiveSizeTestCases = [ + { size: 'xsmall', responsiveSize: 'expressive', expectedClass: [/o-btn--expressive/] }, + { size: 'xsmall', responsiveSize: 'productive', expectedClass: [/o-btn--productive/] }, + ]; + + responsiveSizeTestCases.forEach(({ size, responsiveSize, expectedClass }) => { + test.describe('when "isResponsive" is true', () => { + test.describe(`when "responsiveSize" is "${responsiveSize}"`, () => { + test('the button should have the expected attribute', async ({ page }) => { + const buttonComponent = new ButtonComponent(page, 'button--primary'); + await buttonComponent.load({ size, isResponsive: true, responsiveSize }); + + await expect(buttonComponent.componentLocator.locator('button')) + .toHaveClass(expectedClass); + }); + }); }); }); }); test.describe('tag', () => { test.describe('when set to "button"', () => { - test('should render a button element', async ({ mount }) => { + test('should render a button element', async ({ page }) => { // Arrange - const props: ButtonProps = { - tag: 'button', - }; - - // Act - const component = await mount(PieButton, { - props, - slots: { - default: 'Click me!', - }, - }); - - const button = component.locator('button'); + const buttonComponent = new ButtonComponent(page, 'button--primary'); + await buttonComponent.load({ tag: 'button' }); // Assert - expect(button).toBeVisible(); + expect(buttonComponent.componentLocator.locator('button')).toBeVisible(); }); - test('should not render anchor-specific attributes', async ({ mount }) => { - // Arrange - const props: ButtonProps = { - tag: 'button', - // Anchor-specific props - href: '/test', - rel: 'noopener noreferrer', - target: '_blank', - }; - - // Act - const component = await mount(PieButton, { - props, - slots: { - default: 'Click me!', - }, - }); - - const button = component.locator('button'); - - const href = await button.getAttribute('href'); - const rel = await button.getAttribute('rel'); - const target = await button.getAttribute('target'); - - // Assert - expect.soft(rel).toBeNull(); - expect.soft(target).toBeNull(); - expect(href).toBeNull(); - }); + /* eslint-disable vitest/no-commented-out-tests */ + + // test('should not render anchor-specific attributes', async ({ page }) => { + // // Arrange + // const props: ButtonProps = { + // tag: 'button', + // // Anchor-specific props + // href: '/test', + // rel: 'noopener noreferrer', + // target: '_blank', + // }; + // const buttonComponent = new ButtonComponent(page, 'button--primary'); + // await buttonComponent.load({...props }); + + // const buttonShadowElement = buttonComponent.componentLocator.locator('button'); + + // // Assert + // expect(buttonShadowElement).toHaveAttribute('rel', props.rel as string); + // expect(buttonShadowElement).toHaveAttribute('target', props.target as string); + // expect(buttonShadowElement).toHaveAttribute('href', props.href as string); + // }); }); - test.describe('when set to "a"', () => { - test('should render an anchor element', async ({ mount }) => { - // Arrange - const props: ButtonProps = { - tag: 'a', - }; - - // Act - const component = await mount(PieButton, { - props, - slots: { - default: 'Click me!', - }, - }); - - const anchor = component.locator('a'); - - // Assert - expect(anchor).toBeVisible(); - }); - - test('should not render button-specific attributes', async ({ mount }) => { - // Arrange - const props: ButtonProps = { - tag: 'a', - // Button-specific props - disabled: true, - isLoading: true, - type: 'submit', - }; - - // Act - const component = await mount(PieButton, { - props, - slots: { - default: 'Click me!', - }, - }); - - const anchor = component.locator('a'); - - // Assert - const disabled = await anchor.getAttribute('disabled'); - const type = await anchor.getAttribute('type'); - const spinner = component.locator('pie-spinner'); - - expect.soft(anchor).not.toHaveClass(/is-loading/); - expect.soft(disabled).toBeNull(); - expect.soft(type).toBeNull(); - expect(spinner).not.toBeVisible(); - }); - }); + // test.describe('when set to "a"', () => { + // test('should render an anchor element', async ({ page }) => { + // // Arrange + // const props: ButtonProps = { + // tag: 'a', + // }; + + // // Act + // const buttonComponent = new ButtonComponent(page, 'button--primary'); + // await buttonComponent.load({}); + + // const anchor = buttonComponent.componentLocator.locator('a'); + + // // Assert + // expect(anchor).toBeVisible(); + // }); + + // test('should not render button-specific attributes', async ({ mount }) => { + // // Arrange + // const props: ButtonProps = { + // tag: 'a', + // // Button-specific props + // disabled: true, + // isLoading: true, + // type: 'submit', + // }; + + // // Act + // const component = await mount(PieButton, { + // props, + // slots: { + // default: 'Click me!', + // }, + // }); + + // const anchor = component.locator('a'); + + // // Assert + // const disabled = await anchor.getAttribute('disabled'); + // const type = await anchor.getAttribute('type'); + // const spinner = component.locator('pie-spinner'); + + // expect.soft(anchor).not.toHaveClass(/is-loading/); + // expect.soft(disabled).toBeNull(); + // expect.soft(type).toBeNull(); + // expect(spinner).not.toBeVisible(); + // }); + // }); + + /* eslint-enable vitest/no-commented-out-tests */ }); }); diff --git a/packages/components/pie-button/test/helpers/page-object/pie-button-form-integration-selectors.ts b/packages/components/pie-button/test/helpers/page-object/pie-button-form-integration-selectors.ts new file mode 100644 index 0000000000..b6e5fa2a28 --- /dev/null +++ b/packages/components/pie-button/test/helpers/page-object/pie-button-form-integration-selectors.ts @@ -0,0 +1,51 @@ +const form = { + selectors: { + container: { + description: 'The selector for the form element', + dataTestId: 'testForm', + }, + name: { + description: 'The selector for the name input element', + dataTestId: 'name', + }, + email: { + description: 'The selector for the email input element', + dataTestId: 'usermail', + }, + password: { + description: 'The selector for the password input element', + dataTestId: 'password', + }, + userPaymentCardType: { + description: 'The selector for the user payment card type select element', + dataTestId: 'usercard', + }, + userPaymentCardNumber: { + description: 'The selector for the user payment card number input element', + dataTestId: 'card-number', + }, + userPaymentCardExpiration: { + description: 'The selector for the user payment card expiration input element', + dataTestId: 'card-expiration', + }, + formSubmittedFlag: { + description: 'The selector for the form submitted flag element', + dataTestId: 'formSubmittedFlag', + }, + pieButtonSubmit: { + description: 'The selector for the pie-button submit element', + dataTestId: 'pie-button--submit', + }, + pieButtonReset: { + description: 'The selector for the pie-button reset element', + dataTestId: 'pie-button--reset', + }, + nativeButtonReset: { + description: 'The selector for the native reset button element', + dataTestId: 'button--reset', + }, + }, +}; +export { + form, +}; diff --git a/packages/components/pie-button/test/helpers/page-object/pie-button-form-integration.page.ts b/packages/components/pie-button/test/helpers/page-object/pie-button-form-integration.page.ts new file mode 100644 index 0000000000..75f881bcfd --- /dev/null +++ b/packages/components/pie-button/test/helpers/page-object/pie-button-form-integration.page.ts @@ -0,0 +1,188 @@ +import { type Locator, type Page } from '@playwright/test'; +import { BasePage } from '@justeattakeaway/pie-webc-testing/src/helpers/page-object/base-page.ts'; +import { form } from './pie-button-form-integration-selectors.ts'; + +// Define the FormInput type with specific options for userPaymentCardType +export type FormInput = { + userName: string; + userEmail: string; + userPassword: string; + userPaymentCardType: 'visa' | 'mastercard' | 'amex'; + userPaymentCardNumber: string; + userPaymentCardExpiration: string; +}; + +export type FormButtonType = 'pie-button-submit' | 'pie-button-reset' | 'native-reset'; + +export class FormIntegrationPage extends BasePage { + private readonly formLocator: Locator; + private readonly userNameInput: Locator; + private readonly userEmailInput: Locator; + private readonly userPasswordInput: Locator; + private readonly userPaymentCardTypeSelect: Locator; + private readonly userPaymentCardNumberInput: Locator; + private readonly userPaymentCardExpirationInput: Locator; + private readonly resetPieButton: Locator; + private readonly resetNativeButton: Locator; + private readonly submitPieButton: Locator; + readonly formSubmittedFlag: Locator; + + constructor (page: Page, componentName: string) { + super(page, componentName); + + this.formLocator = page.getByTestId(form.selectors.container.dataTestId); + this.userNameInput = this.formLocator.getByTestId(form.selectors.name.dataTestId); + this.userEmailInput = this.formLocator.getByTestId(form.selectors.email.dataTestId); + this.userPasswordInput = this.formLocator.getByTestId(form.selectors.password.dataTestId); + this.userPaymentCardTypeSelect = this.formLocator.getByTestId(form.selectors.userPaymentCardType.dataTestId); + this.userPaymentCardNumberInput = this.formLocator.getByTestId(form.selectors.userPaymentCardNumber.dataTestId); + this.userPaymentCardExpirationInput = this.formLocator.getByTestId(form.selectors.userPaymentCardExpiration.dataTestId); + + this.resetPieButton = this.formLocator.locator(`[data-test-id="${form.selectors.pieButtonReset.dataTestId}"]`); + this.resetNativeButton = this.formLocator.locator(`[data-test-id="${form.selectors.nativeButtonReset.dataTestId}"]`); + this.submitPieButton = this.formLocator.locator(`[data-test-id="${form.selectors.pieButtonSubmit.dataTestId}"]`); + + this.formSubmittedFlag = this.page.getByTestId(form.selectors.formSubmittedFlag.dataTestId); + } + + /** + * Fills out the form with the provided input data. + * + * @param {FormInput} inputData - The data to fill the form with. + * @returns {Promise} A Promise that resolves with the current instance once the form has been filled. + */ + async fillForm (inputData: FormInput): Promise { + await this.userNameInput.fill(inputData.userName); + await this.userEmailInput.fill(inputData.userEmail); + await this.userPasswordInput.fill(inputData.userPassword); + await this.userPaymentCardTypeSelect.selectOption(inputData.userPaymentCardType); + await this.userPaymentCardNumberInput.fill(inputData.userPaymentCardNumber); + await this.userPaymentCardExpirationInput.fill(inputData.userPaymentCardExpiration); + + return this; + } + + /** + * Clicks the specified form button. + * + * @param {FormButtonType} buttonType - The type of the button to click. + * @returns {Promise} A Promise that resolves once the button has been clicked. + */ + async clickFormButton (buttonType: FormButtonType): Promise { + switch (buttonType) { + case 'pie-button-submit': + await this.submitPieButton.click(); + break; + case 'pie-button-reset': + await this.resetPieButton.click(); + break; + case 'native-reset': + await this.resetNativeButton.click(); + break; + default: + throw new Error(`Invalid button type: ${buttonType}`); + } + } + + /** + * Focuses on the specified field. + * + * @param {keyof FormInput} fieldName - The name of the field to focus on. + * @returns {Promise} A Promise that resolves once the field has been focused. + */ + async focusField (fieldName: keyof FormInput): Promise { + switch (fieldName) { + case 'userName': + await this.userNameInput.focus(); + return this.userNameInput; + case 'userEmail': + await this.userEmailInput.focus(); + return this.userEmailInput; + case 'userPassword': + await this.userPasswordInput.focus(); + return this.userPasswordInput; + case 'userPaymentCardType': + await this.userPaymentCardTypeSelect.focus(); + return this.userPaymentCardTypeSelect; + case 'userPaymentCardNumber': + await this.userPaymentCardNumberInput.focus(); + return this.userPaymentCardNumberInput; + case 'userPaymentCardExpiration': + await this.userPaymentCardExpirationInput.focus(); + return this.userPaymentCardExpirationInput; + default: + throw new Error(`Invalid field name: ${fieldName}`); + } + } + + /** + * Focuses on the specified button. + * + * @param {FormButtonType} formButtonType - The type of the button to focus on. + * @returns {Promise} A Promise that resolves once the button has been focused. + */ + async focusButton (buttonType: FormButtonType): Promise { + switch (buttonType) { + case 'pie-button-submit': + await this.submitPieButton.focus(); + return this.submitPieButton; + case 'pie-button-reset': + await this.resetPieButton.focus(); + return this.resetPieButton; + case 'native-reset': + await this.resetNativeButton.focus(); + return this.resetNativeButton; + default: + throw new Error(`Invalid button type: ${buttonType}`); + } + } + + /** + * Gets the values of the form fields. + * + * @returns {Promise} A Promise that resolves with the form field values. + */ + async getFormFieldValues (): Promise { + const formFieldValues: FormInput = { + userName: await this.userNameInput.inputValue(), + userEmail: await this.userEmailInput.inputValue(), + userPassword: await this.userPasswordInput.inputValue(), + userPaymentCardType: await this.userPaymentCardTypeSelect.inputValue() as 'visa' | 'mastercard' | 'amex', + userPaymentCardNumber: await this.userPaymentCardNumberInput.inputValue(), + userPaymentCardExpiration: await this.userPaymentCardExpirationInput.inputValue(), + }; + + return formFieldValues; + } + + /** + * Gets the associated form ID for the specified button. + * + * @param {FormButtonType} buttonType - The type of the button. + * @returns {Promise} A Promise that resolves with the associated form ID or null if no form is found. + */ + async getAssociatedFormIdForButton (buttonType: FormButtonType): Promise { + let selector: string; + + switch (buttonType) { + case 'pie-button-submit': + selector = form.selectors.pieButtonSubmit.dataTestId; + break; + case 'pie-button-reset': + selector = form.selectors.pieButtonReset.dataTestId; + break; + case 'native-reset': + selector = form.selectors.nativeButtonReset.dataTestId; + break; + default: + throw new Error(`Invalid button type: ${buttonType}`); + } + + const associatedFormId = await this.page.evaluate((selector) => { + const button = document.querySelector(`[data-test-id="${selector}"]`) as HTMLButtonElement; + return button.form ? button.form.id : null; + }, selector); + + return associatedFormId; + } +} diff --git a/packages/components/pie-button/test/helpers/page-object/pie-button-selectors.ts b/packages/components/pie-button/test/helpers/page-object/pie-button-selectors.ts new file mode 100644 index 0000000000..bb41365f2e --- /dev/null +++ b/packages/components/pie-button/test/helpers/page-object/pie-button-selectors.ts @@ -0,0 +1,11 @@ +const button = { + selectors: { + container: { + description: 'The selector for the pie button element', + dataTestId: 'pie-button', + }, + }, +}; +export { + button, +}; diff --git a/packages/components/pie-button/test/helpers/page-object/pie-button.page.ts b/packages/components/pie-button/test/helpers/page-object/pie-button.page.ts new file mode 100644 index 0000000000..dbe2540117 --- /dev/null +++ b/packages/components/pie-button/test/helpers/page-object/pie-button.page.ts @@ -0,0 +1,38 @@ +import type { Locator, Page } from '@playwright/test'; +import { BasePage } from '@justeattakeaway/pie-webc-testing/src/helpers/page-object/base-page.ts'; +import { getShadowElementStylePropValues } from '@justeattakeaway/pie-webc-testing/src/helpers/get-shadow-element-style-prop-values.ts'; + +export class ButtonComponent extends BasePage { + readonly componentLocator: Locator; + + constructor (page: Page, componentName: string) { + super(page, componentName); + this.componentLocator = page.locator('pie-button'); + } + + /** + * Clicks the "Necessary Only" button. + * + * @returns {Promise} A Promise that resolves once the PIE button + * has been successfully clicked. + */ + async click () : Promise { + await this.componentLocator.click(); + } + + /** + * Checks if the button is responsive. + * + * @returns {Promise} A Promise that resolves once the button is responsive. + */ + async isResponsive () : Promise { + const hasIsResponsiveAttribute = await this.componentLocator.getAttribute('isresponsive') !== null; + const buttonClasses = await this.componentLocator.locator('button').getAttribute('class') || ''; + + return hasIsResponsiveAttribute || buttonClasses.includes('o-btn--responsive'); + } + + async getShadowElementStylePropValues (props: Array): Promise> { + return getShadowElementStylePropValues(this.componentLocator, 'button', props); + } +} diff --git a/packages/components/pie-button/turbo.json b/packages/components/pie-button/turbo.json new file mode 100644 index 0000000000..3d34e03dda --- /dev/null +++ b/packages/components/pie-button/turbo.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://turborepo.org/schema.json", + "extends": [ + "//" + ], + "pipeline": { + "test:browsers": { + "cache": true, + "dependsOn": [], + "outputs": [] + }, + "test:browsers:ci": { + "cache": true, + "dependsOn": [] + }, + "test:visual": { + "cache": false, + "dependsOn": [] + }, + "test:visual:ci": { + "cache": false, + "dependsOn": [] + } + } +} \ No newline at end of file diff --git a/packages/components/pie-chip/playwright-lit-visual.config.ts b/packages/components/pie-chip/playwright-lit-visual.config.ts index fb0f14c480..2fd82d7d5f 100644 --- a/packages/components/pie-chip/playwright-lit-visual.config.ts +++ b/packages/components/pie-chip/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-webc-testing/src/helpers/page-object/base-page.ts b/packages/components/pie-webc-testing/src/helpers/page-object/base-page.ts index a28a74bf68..b84383b1e7 100644 --- a/packages/components/pie-webc-testing/src/helpers/page-object/base-page.ts +++ b/packages/components/pie-webc-testing/src/helpers/page-object/base-page.ts @@ -1,4 +1,4 @@ -import { type Page } from '@playwright/test'; +import { type Page, selectors } from '@playwright/test'; import { buildUrl } from './storybook-extensions'; declare global { @@ -13,6 +13,7 @@ export class BasePage { args: string; constructor (page: Page, componentName: string, componentTag = 'data-test-id') { + selectors.setTestIdAttribute('data-test-id'); this.page = page; this.componentName = componentName; this.componentTag = componentTag; @@ -22,11 +23,9 @@ export class BasePage { async load (queries: Record = {}) { const pageUrl = buildUrl(this.componentName, this.composePath(queries), this.args); - await this.open(pageUrl); - } + await this.page.goto(pageUrl); - async open (url: string) { - await this.page.goto(url, { waitUntil: 'networkidle' }); + await this.page.setDefaultTimeout(5000); return this; } diff --git a/playwright-browsers.config.ts b/playwright-browsers.config.ts index 59e5acbfc5..0832991eac 100644 --- a/playwright-browsers.config.ts +++ b/playwright-browsers.config.ts @@ -42,7 +42,7 @@ const config: PlaywrightTestConfig = { baseURL: process.env.BASE_URL, /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', + trace: 'on', /* Sets the default getByTestId function attribute to the data-test-id format */ testIdAttribute: 'data-test-id', diff --git a/playwright-visual.config.ts b/playwright-visual.config.ts index 6f559e9426..e40a98d42d 100644 --- a/playwright-visual.config.ts +++ b/playwright-visual.config.ts @@ -42,7 +42,7 @@ const config: PlaywrightTestConfig = { baseURL: process.env.BASE_URL, /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', + trace: 'on', /* Sets the default getByTestId function attribute to the data-test-id format */ testIdAttribute: 'data-test-id', diff --git a/yarn.lock b/yarn.lock index ef099cd74d..b90ddce665 100644 --- a/yarn.lock +++ b/yarn.lock @@ -763,7 +763,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.12.3, @babel/core@npm:^7.19.6, @babel/core@npm:^7.22.5, @babel/core@npm:^7.24.5, @babel/core@npm:^7.25.2": +"@babel/core@npm:^7.12.3, @babel/core@npm:^7.19.6, @babel/core@npm:^7.22.5, @babel/core@npm:^7.24.5, @babel/core@npm:^7.26.0": version: 7.26.0 resolution: "@babel/core@npm:7.26.0" dependencies: @@ -2087,7 +2087,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-react-jsx-self@npm:^7.24.5, @babel/plugin-transform-react-jsx-self@npm:^7.24.7": +"@babel/plugin-transform-react-jsx-self@npm:^7.24.5, @babel/plugin-transform-react-jsx-self@npm:^7.25.9": version: 7.25.9 resolution: "@babel/plugin-transform-react-jsx-self@npm:7.25.9" dependencies: @@ -2098,7 +2098,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-react-jsx-source@npm:^7.24.1, @babel/plugin-transform-react-jsx-source@npm:^7.24.7": +"@babel/plugin-transform-react-jsx-source@npm:^7.24.1, @babel/plugin-transform-react-jsx-source@npm:^7.25.9": version: 7.25.9 resolution: "@babel/plugin-transform-react-jsx-source@npm:7.25.9" dependencies: @@ -4876,18 +4876,6 @@ __metadata: languageName: unknown linkType: soft -"@justeattakeaway/pie-foo@0.0.0, @justeattakeaway/pie-foo@workspace:packages/components/pie-foo": - version: 0.0.0-use.local - resolution: "@justeattakeaway/pie-foo@workspace:packages/components/pie-foo" - dependencies: - "@custom-elements-manifest/analyzer": 0.9.0 - "@justeattakeaway/pie-components-config": 0.18.0 - "@justeattakeaway/pie-css": 0.13.1 - "@justeattakeaway/pie-webc-core": 0.24.2 - cem-plugin-module-file-extensions: 0.0.5 - languageName: unknown - linkType: soft - "@justeattakeaway/pie-form-label@0.14.4, @justeattakeaway/pie-form-label@workspace:packages/components/pie-form-label": version: 0.0.0-use.local resolution: "@justeattakeaway/pie-form-label@workspace:packages/components/pie-form-label" @@ -5234,7 +5222,6 @@ __metadata: "@justeattakeaway/pie-components-config": 0.18.0 "@justeattakeaway/pie-cookie-banner": 1.2.0 "@justeattakeaway/pie-divider": 1.0.0 - "@justeattakeaway/pie-foo": 0.0.0 "@justeattakeaway/pie-form-label": 0.14.4 "@justeattakeaway/pie-icon-button": 1.0.0 "@justeattakeaway/pie-link": 1.0.0 @@ -6443,39 +6430,37 @@ __metadata: languageName: node linkType: hard -"@playwright/experimental-ct-core@npm:1.41.0": - version: 1.41.0 - resolution: "@playwright/experimental-ct-core@npm:1.41.0" +"@playwright/experimental-ct-core@npm:1.49.0": + version: 1.49.0 + resolution: "@playwright/experimental-ct-core@npm:1.49.0" dependencies: - playwright: 1.41.0 - playwright-core: 1.41.0 - vite: ^4.4.12 - bin: - playwright: cli.js - checksum: fe602b6e590a18f56b16e3b82b4958e2ff65469f8b1fb461315b19f3cd65d54740506ac7fcea24481c10560f2d1162c762fa8fcef4d6e24eabba8a3247b94cc2 + playwright: 1.49.0 + playwright-core: 1.49.0 + vite: ^5.2.8 + checksum: 8aedab566a580f03254b846e988a0634c4412f11f92d2c52a11f0f7624e7fa86b81e45f3e2794a6b179b80099a26140aef31db8f682ee33bab57b4fec916463d languageName: node linkType: hard -"@playwright/experimental-ct-react@npm:1.41.0": - version: 1.41.0 - resolution: "@playwright/experimental-ct-react@npm:1.41.0" +"@playwright/experimental-ct-react@npm:1.49.0": + version: 1.49.0 + resolution: "@playwright/experimental-ct-react@npm:1.49.0" dependencies: - "@playwright/experimental-ct-core": 1.41.0 - "@vitejs/plugin-react": ^4.0.0 + "@playwright/experimental-ct-core": 1.49.0 + "@vitejs/plugin-react": ^4.2.1 bin: playwright: cli.js - checksum: 81177e0fc6810f75f1eaa6209817ae14d6cdc5d651127f2f2db10473f9e5ea508bccc9c115862d07225609c5561540c0bb6a13b42635ec24ff26aeb4506775e8 + checksum: a4094b7fd23c22316e5da2408d5761ce3b1661c108b794b036a279ef481f6bcaaf2f6558c323f2a92f87607fe1d411990d460c2c023527f46c0b3dc7705722d5 languageName: node linkType: hard -"@playwright/test@npm:1.41.0": - version: 1.41.0 - resolution: "@playwright/test@npm:1.41.0" +"@playwright/test@npm:1.49.0": + version: 1.49.0 + resolution: "@playwright/test@npm:1.49.0" dependencies: - playwright: 1.41.0 + playwright: 1.49.0 bin: playwright: cli.js - checksum: 3a7039f8cd14dd242154255417c100a99c3254a3c1fd26df2a11be24c10b06ef77d2736336d7743dedc5e1a6a52748e58ced730b6048f8bd75d8867ce81661e0 + checksum: f8477aa61d59fd22c6161c48221ab246340dc37bbe2804e1a7d1be8cbd0fd861747fcb7ca559f4bc7328226ff2c90ccb7efa588a7d7d7829f3e57902b28fe39a languageName: node linkType: hard @@ -6617,6 +6602,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-android-arm-eabi@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.27.4" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@rollup/rollup-android-arm64@npm:4.26.0": version: 4.26.0 resolution: "@rollup/rollup-android-arm64@npm:4.26.0" @@ -6624,6 +6616,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-android-arm64@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-android-arm64@npm:4.27.4" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-darwin-arm64@npm:4.26.0": version: 4.26.0 resolution: "@rollup/rollup-darwin-arm64@npm:4.26.0" @@ -6631,6 +6630,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-darwin-arm64@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-darwin-arm64@npm:4.27.4" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-darwin-x64@npm:4.26.0": version: 4.26.0 resolution: "@rollup/rollup-darwin-x64@npm:4.26.0" @@ -6638,6 +6644,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-darwin-x64@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-darwin-x64@npm:4.27.4" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-freebsd-arm64@npm:4.26.0": version: 4.26.0 resolution: "@rollup/rollup-freebsd-arm64@npm:4.26.0" @@ -6645,6 +6658,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-freebsd-arm64@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.27.4" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-freebsd-x64@npm:4.26.0": version: 4.26.0 resolution: "@rollup/rollup-freebsd-x64@npm:4.26.0" @@ -6652,6 +6672,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-freebsd-x64@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-freebsd-x64@npm:4.27.4" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-linux-arm-gnueabihf@npm:4.26.0": version: 4.26.0 resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.26.0" @@ -6659,6 +6686,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm-gnueabihf@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.27.4" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-arm-musleabihf@npm:4.26.0": version: 4.26.0 resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.26.0" @@ -6666,6 +6700,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm-musleabihf@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.27.4" + conditions: os=linux & cpu=arm & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-arm64-gnu@npm:4.26.0": version: 4.26.0 resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.26.0" @@ -6673,6 +6714,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm64-gnu@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.27.4" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-arm64-musl@npm:4.26.0": version: 4.26.0 resolution: "@rollup/rollup-linux-arm64-musl@npm:4.26.0" @@ -6680,6 +6728,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm64-musl@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.27.4" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-powerpc64le-gnu@npm:4.26.0": version: 4.26.0 resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.26.0" @@ -6687,6 +6742,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-powerpc64le-gnu@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.27.4" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-riscv64-gnu@npm:4.26.0": version: 4.26.0 resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.26.0" @@ -6694,6 +6756,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-riscv64-gnu@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.27.4" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-s390x-gnu@npm:4.26.0": version: 4.26.0 resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.26.0" @@ -6701,6 +6770,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-s390x-gnu@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.27.4" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-x64-gnu@npm:4.26.0": version: 4.26.0 resolution: "@rollup/rollup-linux-x64-gnu@npm:4.26.0" @@ -6708,6 +6784,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-x64-gnu@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.27.4" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-x64-musl@npm:4.26.0": version: 4.26.0 resolution: "@rollup/rollup-linux-x64-musl@npm:4.26.0" @@ -6715,6 +6798,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-x64-musl@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.27.4" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-win32-arm64-msvc@npm:4.26.0": version: 4.26.0 resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.26.0" @@ -6722,6 +6812,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-arm64-msvc@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.27.4" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-win32-ia32-msvc@npm:4.26.0": version: 4.26.0 resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.26.0" @@ -6729,6 +6826,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-ia32-msvc@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.27.4" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@rollup/rollup-win32-x64-msvc@npm:4.26.0": version: 4.26.0 resolution: "@rollup/rollup-win32-x64-msvc@npm:4.26.0" @@ -6736,6 +6840,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-x64-msvc@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.27.4" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@rushstack/node-core-library@npm:5.9.0": version: 5.9.0 resolution: "@rushstack/node-core-library@npm:5.9.0" @@ -6814,17 +6925,14 @@ __metadata: languageName: node linkType: hard -"@sand4rt/experimental-ct-web@npm:1.41.0": - version: 1.41.0 - resolution: "@sand4rt/experimental-ct-web@npm:1.41.0" +"@sand4rt/experimental-ct-web@npm:1.49.0": + version: 1.49.0 + resolution: "@sand4rt/experimental-ct-web@npm:1.49.0" dependencies: - "@playwright/experimental-ct-core": ^1.41.0 - vite: ^4.4.7 - peerDependencies: - "@playwright/test": ">=1.41.0" + "@playwright/experimental-ct-core": 1.49.0 bin: playwright: cli.js - checksum: f486c0bdd23f31ef1129b239e221d0bd0806be919d1c1a8d45963b51c157be68c8b80f323bc429e2aaea47c03fd3af34bd8367e2c0f787730f3e9eb7210ca8df + checksum: fbb8e5c8ad0f34d7539959b73b35853412373290b35db22f4c3dcc7bc554c96d4465f7e4fef686b856d5470464d87fc70581145ea3b35c3c9376fccf3cb58a79 languageName: node linkType: hard @@ -8701,18 +8809,18 @@ __metadata: languageName: node linkType: hard -"@vitejs/plugin-react@npm:^4.0.0": - version: 4.3.3 - resolution: "@vitejs/plugin-react@npm:4.3.3" +"@vitejs/plugin-react@npm:^4.2.1": + version: 4.3.4 + resolution: "@vitejs/plugin-react@npm:4.3.4" dependencies: - "@babel/core": ^7.25.2 - "@babel/plugin-transform-react-jsx-self": ^7.24.7 - "@babel/plugin-transform-react-jsx-source": ^7.24.7 + "@babel/core": ^7.26.0 + "@babel/plugin-transform-react-jsx-self": ^7.25.9 + "@babel/plugin-transform-react-jsx-source": ^7.25.9 "@types/babel__core": ^7.20.5 react-refresh: ^0.14.2 peerDependencies: - vite: ^4.2.0 || ^5.0.0 - checksum: 1ad449cb7934e14ad265a0044aa2461cdb47587c436c2a0324e2b6a73de1b63a34a84396de41b77988fac67ff43302bf0186674344e11a881ba50936cc4297d8 + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 + checksum: d417f40d9259a1d5193152f7d9fee081d5bf41cbeef9662ae1123ccc1e26aa4b6b04bc82ebb8c4fbfde9516a746fb3af7da19fdd449819c30f0631daaa10a44b languageName: node linkType: hard @@ -22780,10 +22888,10 @@ __metadata: "@justeattakeaway/stylelint-config-pie": 0.8.0 "@percy/cli": 1.29.3 "@percy/playwright": 1.0.6 - "@playwright/experimental-ct-react": 1.41.0 - "@playwright/test": 1.41.0 + "@playwright/experimental-ct-react": 1.49.0 + "@playwright/test": 1.49.0 "@rollup/plugin-node-resolve": 15.1.0 - "@sand4rt/experimental-ct-web": 1.41.0 + "@sand4rt/experimental-ct-web": 1.49.0 "@types/node": 20.4.8 "@types/react": 18.3.11 "@typescript-eslint/eslint-plugin": 5.62.0 @@ -22840,7 +22948,6 @@ __metadata: "@justeattakeaway/pie-cookie-banner": 1.2.0 "@justeattakeaway/pie-css": 0.13.1 "@justeattakeaway/pie-divider": 1.0.0 - "@justeattakeaway/pie-foo": 0.0.0 "@justeattakeaway/pie-form-label": 0.14.4 "@justeattakeaway/pie-icon-button": 1.0.0 "@justeattakeaway/pie-icons-configs": 4.5.1 @@ -22976,27 +23083,27 @@ __metadata: languageName: node linkType: hard -"playwright-core@npm:1.41.0": - version: 1.41.0 - resolution: "playwright-core@npm:1.41.0" +"playwright-core@npm:1.49.0": + version: 1.49.0 + resolution: "playwright-core@npm:1.49.0" bin: playwright-core: cli.js - checksum: 14671265916a1fd0c71d94640de19c48bcce3f7dec35530f10e349e97030ea44ffa8ee518cbf811501e3ab2b74874aecf917e46bf40fea0570db1d4bea1fe7ac + checksum: d8423ad0cab2e672856529bf6b98b406e7e605da098b847b9b54ee8ebd8d716ed8880a9afff4b38f0a2e3f59b95661c74589116ce3ff2b5e0ae3561507086c94 languageName: node linkType: hard -"playwright@npm:1.41.0": - version: 1.41.0 - resolution: "playwright@npm:1.41.0" +"playwright@npm:1.49.0": + version: 1.49.0 + resolution: "playwright@npm:1.49.0" dependencies: fsevents: 2.3.2 - playwright-core: 1.41.0 + playwright-core: 1.49.0 dependenciesMeta: fsevents: optional: true bin: playwright: cli.js - checksum: e7c32136911c58e06b964fe7d33f8b3d8f6a067ae5218662a0811dd6c90e007db1774eb7e161f4aa748d760f404f4c066b7b7303c2b617f7448b6ee4b86c9999 + checksum: f1bfb2fff65cad2ce996edab74ec231dfd21aeb5961554b765ce1eaec27efb87eaba37b00e91ecd27727b82861e5d8c230abe4960e93f6ada8be5ad1020df306 languageName: node linkType: hard @@ -24282,7 +24389,7 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.0.0, postcss@npm:^8.1.10, postcss@npm:^8.2.1, postcss@npm:^8.2.14, postcss@npm:^8.2.15, postcss@npm:^8.3.7, postcss@npm:^8.4.14, postcss@npm:^8.4.19, postcss@npm:^8.4.24, postcss@npm:^8.4.27, postcss@npm:^8.4.32, postcss@npm:^8.4.33, postcss@npm:^8.4.39": +"postcss@npm:^8.0.0, postcss@npm:^8.1.10, postcss@npm:^8.2.1, postcss@npm:^8.2.14, postcss@npm:^8.2.15, postcss@npm:^8.3.7, postcss@npm:^8.4.14, postcss@npm:^8.4.19, postcss@npm:^8.4.24, postcss@npm:^8.4.27, postcss@npm:^8.4.32, postcss@npm:^8.4.33, postcss@npm:^8.4.39, postcss@npm:^8.4.43": version: 8.4.49 resolution: "postcss@npm:8.4.49" dependencies: @@ -25869,6 +25976,75 @@ __metadata: languageName: node linkType: hard +"rollup@npm:^4.20.0": + version: 4.27.4 + resolution: "rollup@npm:4.27.4" + dependencies: + "@rollup/rollup-android-arm-eabi": 4.27.4 + "@rollup/rollup-android-arm64": 4.27.4 + "@rollup/rollup-darwin-arm64": 4.27.4 + "@rollup/rollup-darwin-x64": 4.27.4 + "@rollup/rollup-freebsd-arm64": 4.27.4 + "@rollup/rollup-freebsd-x64": 4.27.4 + "@rollup/rollup-linux-arm-gnueabihf": 4.27.4 + "@rollup/rollup-linux-arm-musleabihf": 4.27.4 + "@rollup/rollup-linux-arm64-gnu": 4.27.4 + "@rollup/rollup-linux-arm64-musl": 4.27.4 + "@rollup/rollup-linux-powerpc64le-gnu": 4.27.4 + "@rollup/rollup-linux-riscv64-gnu": 4.27.4 + "@rollup/rollup-linux-s390x-gnu": 4.27.4 + "@rollup/rollup-linux-x64-gnu": 4.27.4 + "@rollup/rollup-linux-x64-musl": 4.27.4 + "@rollup/rollup-win32-arm64-msvc": 4.27.4 + "@rollup/rollup-win32-ia32-msvc": 4.27.4 + "@rollup/rollup-win32-x64-msvc": 4.27.4 + "@types/estree": 1.0.6 + fsevents: ~2.3.2 + dependenciesMeta: + "@rollup/rollup-android-arm-eabi": + optional: true + "@rollup/rollup-android-arm64": + optional: true + "@rollup/rollup-darwin-arm64": + optional: true + "@rollup/rollup-darwin-x64": + optional: true + "@rollup/rollup-freebsd-arm64": + optional: true + "@rollup/rollup-freebsd-x64": + optional: true + "@rollup/rollup-linux-arm-gnueabihf": + optional: true + "@rollup/rollup-linux-arm-musleabihf": + optional: true + "@rollup/rollup-linux-arm64-gnu": + optional: true + "@rollup/rollup-linux-arm64-musl": + optional: true + "@rollup/rollup-linux-powerpc64le-gnu": + optional: true + "@rollup/rollup-linux-riscv64-gnu": + optional: true + "@rollup/rollup-linux-s390x-gnu": + optional: true + "@rollup/rollup-linux-x64-gnu": + optional: true + "@rollup/rollup-linux-x64-musl": + optional: true + "@rollup/rollup-win32-arm64-msvc": + optional: true + "@rollup/rollup-win32-ia32-msvc": + optional: true + "@rollup/rollup-win32-x64-msvc": + optional: true + fsevents: + optional: true + bin: + rollup: dist/bin/rollup + checksum: 7268678ce9a645fda79efa2dc3c9b458357683b0bbd8cc44f8e52d406df4d40468ea3efdf24ad01e25210594cd40902b2b3d20730e2d58e9b226cb3c48dcbd8b + languageName: node + linkType: hard + "rrweb-cssom@npm:^0.6.0": version: 0.6.0 resolution: "rrweb-cssom@npm:0.6.0" @@ -29207,7 +29383,7 @@ __metadata: languageName: node linkType: hard -"vite@npm:^3.0.0 || ^4.0.0, vite@npm:^4.4.12, vite@npm:^4.4.7": +"vite@npm:^3.0.0 || ^4.0.0": version: 4.5.5 resolution: "vite@npm:4.5.5" dependencies: @@ -29247,6 +29423,49 @@ __metadata: languageName: node linkType: hard +"vite@npm:^5.2.8": + version: 5.4.11 + resolution: "vite@npm:5.4.11" + dependencies: + esbuild: ^0.21.3 + fsevents: ~2.3.3 + postcss: ^8.4.43 + rollup: ^4.20.0 + peerDependencies: + "@types/node": ^18.0.0 || >=20.0.0 + less: "*" + lightningcss: ^1.21.0 + sass: "*" + sass-embedded: "*" + stylus: "*" + sugarss: "*" + terser: ^5.4.0 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + bin: + vite: bin/vite.js + checksum: 8c5b31d17487b69c40a30419dc0ade9f33360eb6893dbfa33a90980271bd74d35ae550b5cbb2a9e640f0df41ea36fd1bb4f222c98f6d02e607080f20832e69e8 + languageName: node + linkType: hard + "vitest@npm:0.29.8": version: 0.29.8 resolution: "vitest@npm:0.29.8" From d5bc13c3e058726dc63029ff8cafdc39a7974c74 Mon Sep 17 00:00:00 2001 From: Ben Siggery Date: Wed, 4 Dec 2024 12:54:49 +0000 Subject: [PATCH 2/3] update config with correct import --- packages/components/pie-button/playwright-lit.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/pie-button/playwright-lit.config.ts b/packages/components/pie-button/playwright-lit.config.ts index 9828bca0dd..6dcc0f833d 100644 --- a/packages/components/pie-button/playwright-lit.config.ts +++ b/packages/components/pie-button/playwright-lit.config.ts @@ -1,4 +1,4 @@ -import { defineConfig } from '@sand4rt/experimental-ct-web'; +import { defineConfig } from '@playwright/test'; import { getPlaywrightNativeConfig } from '@justeattakeaway/pie-components-config'; export default defineConfig(getPlaywrightNativeConfig()); From 8758b5d25d616f140993e8b7190c8791d7862d33 Mon Sep 17 00:00:00 2001 From: Ben Siggery Date: Mon, 9 Dec 2024 11:53:42 +0000 Subject: [PATCH 3/3] refactor(pie-button): DSW-2369 remaining component / a11y tests --- .cursorrules | 1 + .../testing/pie-button.test.stories.ts | 75 ++++- .../test/accessibility/pie-button.spec.ts | 44 +-- .../test/component/pie-button.spec.ts | 298 ++++++++---------- .../test/visual/pie-button-anchor.spec.ts | 51 +-- .../test/visual/pie-button-size.spec.ts | 70 +--- .../pie-button/test/visual/pie-button.spec.ts | 74 +---- 7 files changed, 240 insertions(+), 373 deletions(-) create mode 100644 .cursorrules diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000000..b4e612d5ea --- /dev/null +++ b/.cursorrules @@ -0,0 +1 @@ +You are an expert in TypeScript, Node.js, Next.js App Router, React, Expo, tRPC, Shadcn UI, Radix UI, and Tailwind.Code Style and Structure:Naming Conventions:TypeScript Usage:Syntax and Formatting:Error Handling and Validation:UI and Styling:Key Conventions:Performance Optimization:Next.js Specific:Expo Specific:Follow Next.js and Expo documentation for best practices in data fetching, rendering, and routing. \ No newline at end of file diff --git a/apps/pie-storybook/stories/testing/pie-button.test.stories.ts b/apps/pie-storybook/stories/testing/pie-button.test.stories.ts index 5edaf17dd6..98184db0cb 100644 --- a/apps/pie-storybook/stories/testing/pie-button.test.stories.ts +++ b/apps/pie-storybook/stories/testing/pie-button.test.stories.ts @@ -1,6 +1,6 @@ import { html, nothing } from 'lit'; import { ifDefined } from 'lit/directives/if-defined.js'; -import { type Meta } from '@storybook/web-components'; +import { type Meta, StoryObj } from '@storybook/web-components'; import '@justeattakeaway/pie-button'; import { @@ -8,7 +8,7 @@ import { } from '@justeattakeaway/pie-button'; import '@justeattakeaway/pie-icons-webc/dist/IconPlusCircle.js'; -import { createStory, type TemplateFunction, sanitizeAndRenderHTML } from '../../utilities'; +import { createStory, createVariantStory, type TemplateFunction, sanitizeAndRenderHTML } from '../../utilities'; import { type SlottedComponentProps } from '../../types'; type ButtonProps = SlottedComponentProps & { @@ -200,6 +200,22 @@ const Template: TemplateFunction = ({ ${sanitizeAndRenderHTML(slot)} `; +const AnchorTemplate: TemplateFunction = (props: ButtonProps) => html` + + ${props.iconPlacement ? html`` : nothing} + ${sanitizeAndRenderHTML(props.slot)} + `; + const FormTemplate: TemplateFunction = ({ showSubmitButton, showNativeResetButton, @@ -310,10 +326,59 @@ const createButtonStory = createStory(Template, defaultArgs); const createButtonStoryWithForm = createStory(FormTemplate, defaultArgs); -const anchorOnlyProps : Array = ['href', 'target', 'rel']; +export const Primary = createButtonStory(); -export const Primary = createButtonStory({}, { - controls: { exclude: ['variant', ...anchorOnlyProps] }, +export const Anchor = createStory(AnchorTemplate, defaultArgs)({ + href: '/?path=/story/button--anchor', +}, { + argTypes: { + tag: { + description: 'Choose the HTML element that will be used to render the button.
For this story, the prop has the value of `a`. See the other stories to interact with the component when this prop has a value of `button`.', + }, + } }); export const FormIntegration = createButtonStoryWithForm({ type: 'submit' }); + +const FormSubmissionTemplate: TemplateFunction = (props: ButtonProps) => html` +
+ + + Submit + +
+`; + +export const FormSubmission = createStory(FormSubmissionTemplate, { + ...defaultArgs, + type: 'submit', +})(); + +// ... existing code ... + +const FormWithAllAttributesTemplate: TemplateFunction = () => html` +
+ + + Submit + +
+`; + +export const FormWithAllAttributes = createStory(FormWithAllAttributesTemplate, { + ...defaultArgs, + type: 'submit', +})(); \ No newline at end of file diff --git a/packages/components/pie-button/test/accessibility/pie-button.spec.ts b/packages/components/pie-button/test/accessibility/pie-button.spec.ts index 94ee8a3163..03b31ebd0d 100644 --- a/packages/components/pie-button/test/accessibility/pie-button.spec.ts +++ b/packages/components/pie-button/test/accessibility/pie-button.spec.ts @@ -1,35 +1,15 @@ -import { test, expect } from '@justeattakeaway/pie-webc-testing/src/playwright/webc-fixtures.ts'; -import { getAllPropCombinations, splitCombinationsByPropertyValue } from '@justeattakeaway/pie-webc-testing/src/helpers/get-all-prop-combos.ts'; -import { type PropObject, type WebComponentPropValues } from '@justeattakeaway/pie-webc-testing/src/helpers/defs.ts'; -import { PieButton } from '../../src/index.ts'; -import { type ButtonProps, sizes, variants } from '../../src/defs.ts'; +import { test, expect } from '@justeattakeaway/pie-webc-testing/src/playwright/playwright-fixtures.ts'; +import { ButtonComponent } from '../helpers/page-object/pie-button.page.ts'; +import { variants } from '../../src/defs.ts'; -const props: PropObject = { - variant: variants, - size: sizes, - type: 'button', // Changing the type does not affect the appearance of the button - isFullWidth: [true, false], - disabled: [true, false], -}; +variants.forEach((variant) => { + test(`should test a11y for Variant: ${variant}`, async ({ makeAxeBuilder, page }) => { + const button = new ButtonComponent(page, `button--${variant}-prop-variations`); + await button.load(); -const componentPropsMatrix: WebComponentPropValues[] = getAllPropCombinations(props); -const componentPropsMatrixByVariant: Record = splitCombinationsByPropertyValue(componentPropsMatrix, 'variant'); -const componentVariants: string[] = Object.keys(componentPropsMatrixByVariant); + const results = await makeAxeBuilder().analyze(); -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( - PieButton, - { - props: { ...combo }, - slots: { - default: 'Hello world', - }, - }, - ); - })); - - const results = await makeAxeBuilder().analyze(); - - expect(results.violations).toEqual([]); -})); + console.log(results.violations); + expect(results.violations).toEqual([]); + }); +}); 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 7ac2cb3c77..f80492e9bd 100644 --- a/packages/components/pie-button/test/component/pie-button.spec.ts +++ b/packages/components/pie-button/test/component/pie-button.spec.ts @@ -13,16 +13,16 @@ const formInput: FormInput = { }; type SizeResponsiveSize = { - sizeName: ButtonProps['size']; - responsiveSize: string; + size: ButtonProps['size']; + responsiveSize: string; }; const sizes: Array = [ - { sizeName: 'xsmall', responsiveSize: '--btn-height--small' }, - { sizeName: 'small-expressive', responsiveSize: '--btn-height--medium' }, - { sizeName: 'small-productive', responsiveSize: '--btn-height--medium' }, - { sizeName: 'medium', responsiveSize: '--btn-height--large' }, - { sizeName: 'large', responsiveSize: '--btn-height--large' }, + { size: 'xsmall', responsiveSize: '--btn-height--small' }, + { size: 'small-expressive', responsiveSize: '--btn-height--medium' }, + { size: 'small-productive', responsiveSize: '--btn-height--medium' }, + { size: 'medium', responsiveSize: '--btn-height--large' }, + { size: 'large', responsiveSize: '--btn-height--large' }, ]; test('should correctly work with native click events', async ({ page }) => { @@ -62,7 +62,7 @@ test.describe('Form Actions', () => { }); test('should trigger native HTML form validation for required fields and submit after correcting when type is `submit`', async ({ page }) => { - // Arrange + // Arrange const formIntegrationPage = new FormIntegrationPage(page, 'button--form-integration'); await formIntegrationPage.load(); @@ -107,77 +107,52 @@ test.describe('Form Actions', () => { expect(await formIntegrationPage.formSubmittedFlag).toHaveCount(0); }); - /* eslint-disable vitest/no-commented-out-tests */ - // test('NEED TO REFACTOR should include pie-button\'s name and value in the form submission data when it triggers submission', async ({ page }) => { - // // Arrange - // // Inject the test form into the page with pie-button having name and value attributes - // await page.evaluate(() => { - // const formHTML = ` - //
- // - // Submit - //
- // `; - // document.body.innerHTML = formHTML; - // }); - - // // Intercept form submissions - // const [request] = await Promise.all([ - // page.waitForRequest('/submit-endpoint'), - // page.fill('input[name="username"]', 'testUser'), - // page.click('#testButton'), - // ]); - - // const formData = request.postData(); - - // // Assert - // expect(formData).toContain('submitButton=submitValue'); - // }); - - // test('NEED TO REFACTOR - should respect all form-related attributes on the pie-button', async ({ page }) => { - // // Arrange - // // Inject the test form into the page with pie-button having multiple form attributes - // await page.evaluate(() => { - // const formHTML = ` - //
- // - // Submit - //
- // `; - // document.body.innerHTML = formHTML; - // }); - - // // Act - // // Intercept form submissions - // const [request] = await Promise.all([ - // page.waitForRequest('/custom-endpoint'), - // page.fill('input[name="username"]', 'testUser'), - // page.click('#testButton'), - // ]); - - // const postData = request.postData(); - // const method = request.method(); - // const headers = request.headers(); - - // // Assert - // expect(postData).toBeTruthy(); - // const submitButtonDisposition = 'Content-Disposition: form-data; name="submitButton"'; - // const submitButtonValuePosition = (postData as string).indexOf(submitButtonDisposition) + submitButtonDisposition.length; - // expect((postData as string).includes(submitButtonDisposition)).toBeTruthy(); - // expect((postData as string).substring(submitButtonValuePosition)).toContain('submitValue'); - // expect(headers['content-type']).toMatch(/^multipart\/form-data;/); - // expect(method).toBe('POST'); - // }); - - /* eslint-enable vitest/no-commented-out-tests */ + test('should include pie-button\'s name and value in the form submission data when it triggers submission', async ({ page }) => { + // Arrange + const formSubmissionPage = new FormIntegrationPage(page, 'button--form-submission'); + await formSubmissionPage.load(); + + // Intercept form submissions + const requestPromise = page.waitForRequest(/submit-endpoint/); + await page.fill('input[name="username"]', 'testUser'); + + // Act + await page.locator('pie-button').click(); + + const request = await requestPromise; + const formData = request.postData(); + + // Assert + expect(formData).toContain('submitButton=submitValue'); + }); + + test('should respect all form-related attributes on the pie-button', async ({ page }) => { + // Arrange + const formAttributesPage = new FormIntegrationPage(page, 'button--form-with-all-attributes'); + await formAttributesPage.load(); + + // Act + // Intercept form submissions + + const requestPromise = page.waitForRequest(/custom-endpoint/); + await page.fill('input[name="username"]', 'testUser'); + await page.click('#testButton'); + + const request = await requestPromise; + + const postData = request.postData(); + const method = request.method(); + const headers = request.headers(); + + // Assert + expect(postData).not.toBeNull(); + const submitButtonDisposition = 'Content-Disposition: form-data; name="submitButton"'; + const submitButtonValuePosition = (postData as string).indexOf(submitButtonDisposition) + submitButtonDisposition.length; + expect((postData as string).includes(submitButtonDisposition)).toBeTruthy(); + expect((postData as string).substring(submitButtonValuePosition)).toContain('submitValue'); + expect(headers['content-type']).toMatch(/^multipart\/form-data;/); + expect(method).toBe('POST'); + }); const submitTestCases = [ { titlePrefix: 'should submit', showSubmitButton: true, expectedFormSubmittedFlagCount: 1 }, @@ -236,7 +211,7 @@ test.describe('Form Actions', () => { test.describe('Reset', () => { test('should reset the form by clicking the reset button when type is `reset`', async ({ page }) => { - // Arrange + // Arrange const formIntegrationPage = new FormIntegrationPage(page, 'button--form-integration'); await formIntegrationPage.load(); @@ -327,12 +302,12 @@ test.describe('props', () => { expect(await buttonComponent.isResponsive()).toBe(true); }); - sizes.forEach(({ sizeName, responsiveSize }) => { - test(`a "${sizeName}" size button height should be equivalent to "${responsiveSize}"`, async ({ page }) => { + sizes.forEach((testCase) => { + test(`a "${testCase.size}" size button height should be equivalent to "${testCase.responsiveSize}"`, async ({ page }) => { const buttonComponent = new ButtonComponent(page, 'button--primary'); - await buttonComponent.load({ isResponsive: true, size: sizeName }); + await buttonComponent.load({ ...testCase }); - const [currentHeight, expectedHeight] = await buttonComponent.getShadowElementStylePropValues(['--btn-height', responsiveSize]); + const [currentHeight, expectedHeight] = await buttonComponent.getShadowElementStylePropValues(['--btn-height', testCase.responsiveSize]); await expect(currentHeight).toBe(expectedHeight); }); @@ -351,19 +326,23 @@ test.describe('props', () => { }); const responsiveSizeTestCases = [ - { size: 'xsmall', responsiveSize: 'expressive', expectedClass: [/o-btn--expressive/] }, - { size: 'xsmall', responsiveSize: 'productive', expectedClass: [/o-btn--productive/] }, + { + size: 'xsmall', responsiveSize: 'expressive', expectedClass: [/o-btn--expressive/], isResponsive: true, + }, + { + size: 'xsmall', responsiveSize: 'productive', expectedClass: [/o-btn--productive/], isResponsive: true, + }, ]; - responsiveSizeTestCases.forEach(({ size, responsiveSize, expectedClass }) => { + responsiveSizeTestCases.forEach((testCase) => { test.describe('when "isResponsive" is true', () => { - test.describe(`when "responsiveSize" is "${responsiveSize}"`, () => { + test.describe(`when "responsiveSize" is "${testCase.responsiveSize}"`, () => { test('the button should have the expected attribute', async ({ page }) => { const buttonComponent = new ButtonComponent(page, 'button--primary'); - await buttonComponent.load({ size, isResponsive: true, responsiveSize }); + await buttonComponent.load({ ...testCase }); await expect(buttonComponent.componentLocator.locator('button')) - .toHaveClass(expectedClass); + .toHaveClass(testCase.expectedClass); }); }); }); @@ -374,85 +353,80 @@ test.describe('props', () => { test.describe('when set to "button"', () => { test('should render a button element', async ({ page }) => { // Arrange + const props: ButtonProps = { + tag: 'button', + }; + const buttonComponent = new ButtonComponent(page, 'button--primary'); - await buttonComponent.load({ tag: 'button' }); + await buttonComponent.load({ ...props }); // Assert expect(buttonComponent.componentLocator.locator('button')).toBeVisible(); }); - /* eslint-disable vitest/no-commented-out-tests */ - - // test('should not render anchor-specific attributes', async ({ page }) => { - // // Arrange - // const props: ButtonProps = { - // tag: 'button', - // // Anchor-specific props - // href: '/test', - // rel: 'noopener noreferrer', - // target: '_blank', - // }; - // const buttonComponent = new ButtonComponent(page, 'button--primary'); - // await buttonComponent.load({...props }); - - // const buttonShadowElement = buttonComponent.componentLocator.locator('button'); - - // // Assert - // expect(buttonShadowElement).toHaveAttribute('rel', props.rel as string); - // expect(buttonShadowElement).toHaveAttribute('target', props.target as string); - // expect(buttonShadowElement).toHaveAttribute('href', props.href as string); - // }); + test('should not render anchor-specific attributes', async ({ page }) => { + // Arrange + const props: ButtonProps = { + tag: 'button', + // Anchor-specific props + href: '/test', + rel: 'noopener noreferrer', + target: '_blank', + }; + const buttonComponent = new ButtonComponent(page, 'button--primary'); + await buttonComponent.load({ ...props }); + + const buttonShadowElement = buttonComponent.componentLocator.locator('button'); + + // Assert + expect(buttonShadowElement).not.toHaveAttribute('rel', props.rel as string); + expect(buttonShadowElement).not.toHaveAttribute('target', props.target as string); + expect(buttonShadowElement).not.toHaveAttribute('href', props.href as string); + }); }); - // test.describe('when set to "a"', () => { - // test('should render an anchor element', async ({ page }) => { - // // Arrange - // const props: ButtonProps = { - // tag: 'a', - // }; - - // // Act - // const buttonComponent = new ButtonComponent(page, 'button--primary'); - // await buttonComponent.load({}); - - // const anchor = buttonComponent.componentLocator.locator('a'); - - // // Assert - // expect(anchor).toBeVisible(); - // }); - - // test('should not render button-specific attributes', async ({ mount }) => { - // // Arrange - // const props: ButtonProps = { - // tag: 'a', - // // Button-specific props - // disabled: true, - // isLoading: true, - // type: 'submit', - // }; - - // // Act - // const component = await mount(PieButton, { - // props, - // slots: { - // default: 'Click me!', - // }, - // }); - - // const anchor = component.locator('a'); - - // // Assert - // const disabled = await anchor.getAttribute('disabled'); - // const type = await anchor.getAttribute('type'); - // const spinner = component.locator('pie-spinner'); - - // expect.soft(anchor).not.toHaveClass(/is-loading/); - // expect.soft(disabled).toBeNull(); - // expect.soft(type).toBeNull(); - // expect(spinner).not.toBeVisible(); - // }); - // }); - - /* eslint-enable vitest/no-commented-out-tests */ + test.describe('when set to "a"', () => { + test('should render an anchor element', async ({ page }) => { + // Arrange + const props: ButtonProps = { + tag: 'a', + }; + + // Act + const buttonComponent = new ButtonComponent(page, 'button--anchor'); + await buttonComponent.load({ ...props }); + + const anchor = buttonComponent.componentLocator.locator('a'); + + // Assert + expect(anchor).toBeVisible(); + }); + + test('should not render button-specific attributes', async ({ page }) => { + // Arrange + const props: ButtonProps = { + tag: 'a', + // Button-specific props + disabled: true, + isLoading: true, + type: 'submit', + }; + + // Act + const buttonComponent = new ButtonComponent(page, 'button--anchor'); + await buttonComponent.load({ ...props }); + + const anchor = buttonComponent.componentLocator.locator('a'); + + // Assert + + const spinner = anchor.locator('pie-spinner'); + + expect.soft(anchor).not.toHaveClass(/is-loading/); + expect.soft(anchor).not.toHaveAttribute('disabled'); + expect.soft(anchor).not.toHaveAttribute('type'); + expect(spinner).not.toBeVisible(); + }); + }); }); }); diff --git a/packages/components/pie-button/test/visual/pie-button-anchor.spec.ts b/packages/components/pie-button/test/visual/pie-button-anchor.spec.ts index a18832a64a..31dc4ae5a4 100644 --- a/packages/components/pie-button/test/visual/pie-button-anchor.spec.ts +++ b/packages/components/pie-button/test/visual/pie-button-anchor.spec.ts @@ -1,48 +1,13 @@ -import { test } from '@sand4rt/experimental-ct-web'; +import { test } from '@playwright/test'; import percySnapshot from '@percy/playwright'; - -import { type PropObject, type WebComponentPropValues } from '@justeattakeaway/pie-webc-testing/src/helpers/defs.ts'; -import { getAllPropCombinations } 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 { ButtonComponent } from 'test/helpers/page-object/pie-button.page'; -import { PieButton } from '../../src/index.ts'; -import { type ButtonProps, sizes, variants } from '../../src/defs.ts'; - -const props: PropObject = { - variant: variants, - size: sizes, - tag: 'a', -}; - -// Renders a HTML string with the given prop values -const renderTestPieButton = (propVals: WebComponentPropValues) => `Hello world`; - -const componentPropsMatrix = getAllPropCombinations(props); - -test.beforeEach(async ({ mount }, testInfo) => { - testInfo.setTimeout(testInfo.timeout + 40000); - const component = await mount(PieButton); - await component.unmount(); -}); - -test('should render all size and variant variations for anchor tag', async ({ page, mount }) => { - for (const combo of componentPropsMatrix) { - const { renderedString, propValues } = createTestWebComponent(combo, renderTestPieButton); - const propKeyValues = `tag: ${propValues.tag}, size: ${propValues.size}, variant: ${propValues.variant}`; - const darkMode = ['inverse', 'ghost-inverse', 'outline-inverse'].includes(propValues.variant); - - await mount( - WebComponentTestWrapper, - { - props: { propKeyValues, darkMode }, - slots: { - component: renderedString.trim(), - }, - }, - ); - } +test('should render all size and variant variations for anchor tag', async ({ page }) => { + // Arrange + const button = new ButtonComponent(page, 'button--anchor-prop-variations'); + await button.load(); - await percySnapshot(page, 'PIE Button Anchor - sizes/variants', percyWidths); + // Assert + await percySnapshot(page, 'PIE Button Anchor - sizes/variants', percyWidths); }); diff --git a/packages/components/pie-button/test/visual/pie-button-size.spec.ts b/packages/components/pie-button/test/visual/pie-button-size.spec.ts index a0c2076a01..223afe30ac 100644 --- a/packages/components/pie-button/test/visual/pie-button-size.spec.ts +++ b/packages/components/pie-button/test/visual/pie-button-size.spec.ts @@ -1,55 +1,12 @@ -import { test } from '@sand4rt/experimental-ct-web'; +import { test } from '@playwright/test'; import percySnapshot from '@percy/playwright'; -import { - type PropObject, type WebComponentPropValues, -} from '@justeattakeaway/pie-webc-testing/src/helpers/defs.ts'; -import { - getAllPropCombinations, -} 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 { PieButton } from '../../src/index.ts'; -import { type ButtonProps, sizes } from '../../src/defs.ts'; +import { ButtonComponent } from '../helpers/page-object/pie-button.page.ts'; -const props: PropObject = { - variant: ['primary'], - size: sizes, - isResponsive: [true, false], - responsiveSize: ['productive', 'expressive'], -}; +test('should render all size variations', async ({ page }) => { -// Renders a HTML string with the given prop values -const renderTestPieButton = (propVals: WebComponentPropValues) => `Hello world`; -const renderTestPieButtonLargeText = (propVals: WebComponentPropValues) => `This is a really long button string to test the overflow`; - -const componentPropsMatrix = getAllPropCombinations(props); - -test.beforeEach(async ({ mount }, testInfo) => { - testInfo.setTimeout(testInfo.timeout + 40000); - const component = await mount(PieButton); - await component.unmount(); -}); - -test('should render all size variations', async ({ page, mount }) => { - for (const combo of componentPropsMatrix) { - const testComponent = createTestWebComponent(combo, renderTestPieButton); - const propKeyValues = `size: ${testComponent.propValues.size}, isResponsive: ${testComponent.propValues.isResponsive}, responsiveSize: ${testComponent.propValues.responsiveSize}`; - - await mount( - WebComponentTestWrapper, - { - props: { propKeyValues }, - slots: { - component: testComponent.renderedString.trim(), - }, - }, - ); - } + const button = new ButtonComponent(page, 'button--button-size-prop-variations'); + await button.load(); // Follow up to remove in Jan await page.waitForTimeout(2500); @@ -57,21 +14,10 @@ test('should render all size variations', async ({ page, mount }) => { await percySnapshot(page, 'PIE Button - sizes/isResponsive/responsiveSize', percyWidths); }); -test('should render all size variations, with larger button text string', async ({ page, mount }) => { - for (const combo of componentPropsMatrix) { - const testComponent = createTestWebComponent(combo, renderTestPieButtonLargeText); - const propKeyValues = `size: ${testComponent.propValues.size}, isResponsive: ${testComponent.propValues.isResponsive}, responsiveSize: ${testComponent.propValues.responsiveSize}`; +test('should render all size variations, with larger button text string', async ({ page }) => { - await mount( - WebComponentTestWrapper, - { - props: { propKeyValues }, - slots: { - component: testComponent.renderedString.trim(), - }, - }, - ); - } + const button = new ButtonComponent(page, 'button--double-line-button-prop-variations'); + await button.load(); // Follow up to remove in Jan await page.waitForTimeout(2500); diff --git a/packages/components/pie-button/test/visual/pie-button.spec.ts b/packages/components/pie-button/test/visual/pie-button.spec.ts index 6b837bb453..a0bc2fee46 100644 --- a/packages/components/pie-button/test/visual/pie-button.spec.ts +++ b/packages/components/pie-button/test/visual/pie-button.spec.ts @@ -1,80 +1,16 @@ import { test } from '@sand4rt/experimental-ct-web'; 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 { percyWidths } from '@justeattakeaway/pie-webc-testing/src/percy/breakpoints.ts'; -import { IconHeartFilled } from '@justeattakeaway/pie-icons-webc/dist/IconHeartFilled'; -import { PieButton } from '../../src/index.ts'; -import { - type ButtonProps, sizes, variants, iconPlacements, -} from '../../src/defs.ts'; +import { sizes, variants } from '../../src/defs.ts'; +import { ButtonComponent } from 'test/helpers/page-object/pie-button.page.ts'; -const props: PropObject = { - variant: variants, - size: sizes, - type: 'button', // Changing the type does not affect the appearance of the button - isFullWidth: [true, false], - disabled: [true, false], - isLoading: [true, false], - iconPlacement: iconPlacements, -}; - -// Renders a HTML string with the given prop values -const renderTestPieButton = (propVals: WebComponentPropValues) => `${propVals.iconPlacement ? '' : ''} Hello world`; - -const componentPropsMatrix : WebComponentPropValues[] = getAllPropCombinations(props); -const componentPropsMatrixByVariant: Record = splitCombinationsByPropertyValue(componentPropsMatrix, 'variant'); -const componentVariants: string[] = Object.keys(componentPropsMatrixByVariant); - -test.beforeEach(async ({ mount }, testInfo) => { - testInfo.setTimeout(testInfo.timeout + 40000); - - // This ensures the button and icon components are 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 buttonComponent = await mount(PieButton); - await buttonComponent.unmount(); - const iconComponent = await mount(IconHeartFilled); - await iconComponent.unmount(); -}); - -componentVariants.forEach((variant) => { - const componentPropsMatrixBySize = splitCombinationsByPropertyValue(componentPropsMatrixByVariant[variant], 'size'); - const componentSizes: string[] = Object.keys(componentPropsMatrixBySize); - - componentSizes.forEach((size) => { - test(`should render all prop variations for Variant: ${variant} and Size: ${size}`, async ({ page, mount }) => { - const combos = componentPropsMatrixBySize[size]; - - for (const combo of combos) { - const testComponent: WebComponentTestInput = createTestWebComponent(combo, renderTestPieButton); - const propKeyValues = `size: ${testComponent.propValues.size}, iconPlacement: ${testComponent.propValues.iconPlacement}, isFullWidth: ${testComponent.propValues.isFullWidth}, disabled: ${testComponent.propValues.disabled}, isLoading: ${testComponent.propValues.isLoading}`; - const darkMode = ['inverse', 'ghost-inverse', 'outline-inverse'].includes(variant); - - await mount( - WebComponentTestWrapper, - { - props: { propKeyValues, darkMode }, - slots: { - component: testComponent.renderedString.trim(), - }, - }, - ); - } + variants.forEach((variant) => { + test(`should render all prop variations for Variant: ${variant}`, async ({ page }) => { + const button = new ButtonComponent(page, `button--${variant}-prop-variations`); await page.waitForLoadState('domcontentloaded'); const snapshotName = `PIE Button - Variant: ${variant} - Size: ${size}`; await percySnapshot(page, snapshotName, percyWidths); }); }); -});