Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(pie-button): DSW-2369 to use @playwright/test & storybook for testing #2109

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .cursorrules
Original file line number Diff line number Diff line change
@@ -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.
384 changes: 384 additions & 0 deletions apps/pie-storybook/stories/testing/pie-button.test.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,384 @@
import { html, nothing } from 'lit';
import { ifDefined } from 'lit/directives/if-defined.js';
import { type Meta, StoryObj } 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, createVariantStory, type TemplateFunction, sanitizeAndRenderHTML } from '../../utilities';
import { type SlottedComponentProps } from '../../types';

type ButtonProps = SlottedComponentProps<ButtonPropsBase> & {
showSubmitButton?: boolean;
showNativeResetButton?: boolean;
renderIncorrectForm?: boolean;
};
type ButtonStoryMeta = Meta<ButtonProps>;

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.<br>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.<br><br>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.<br /><br />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.<br><br>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<ButtonProps> = ({
size,
variant,
type,
disabled,
isFullWidth,
isLoading,
isResponsive,
slot,
iconPlacement,
name,
value,
responsiveSize,
}) => html`
<pie-button
tag="button"
size="${ifDefined(size)}"
variant="${ifDefined(variant)}"
type="${ifDefined(type)}"
iconPlacement="${ifDefined(iconPlacement)}"
?disabled="${disabled}"
?isLoading="${isLoading}"
?isFullWidth="${isFullWidth}"
?isResponsive="${isResponsive}"
name=${ifDefined(name)}
value=${ifDefined(value)}
@click=${handleClick}
responsiveSize="${ifDefined(responsiveSize)}">
${iconPlacement ? html`<icon-plus-circle slot="icon"></icon-plus-circle>` : nothing}
${sanitizeAndRenderHTML(slot)}
</pie-button>`;

const AnchorTemplate: TemplateFunction<ButtonProps> = (props: ButtonProps) => html`
<pie-button
tag="a"
size="${ifDefined(props.size)}"
variant="${ifDefined(props.variant)}"
iconPlacement="${ifDefined(props.iconPlacement)}"
?isFullWidth="${props.isFullWidth}"
?isResponsive="${props.isResponsive}"
responsiveSize="${ifDefined(props.responsiveSize)}"
href="${ifDefined(props.href)}"
rel="${ifDefined(props.rel)}"
target="${ifDefined(props.target)}">
${props.iconPlacement ? html`<icon-plus-circle slot="icon"></icon-plus-circle>` : nothing}
${sanitizeAndRenderHTML(props.slot)}
</pie-button>`;

const FormTemplate: TemplateFunction<ButtonProps> = ({
showSubmitButton,
showNativeResetButton,
renderIncorrectForm,
...props
}) => html`
${renderIncorrectForm ? html`<form id="wrongForm"></form>` : nothing}
<p id="formLog" style="display: none; font-size: 2rem; color: var(--dt-color-support-positive);"></p>
<h2>Fake form</h2>
<form data-test-id="testForm" id="testForm">
<p>Required fields are followed by <strong><span aria-label="required">*</span></strong>.</p>
<section>
<h2>Contact information</h2>
<p>
<label for="name">
<span>Name: </span>
<strong><span aria-label="required">*</span></strong>
</label>
<input type="text" id="name" data-test-id="name" name="username" required />
</p>
<p>
<label for="mail">
<span>E-mail: </span>
<strong><span aria-label="required">*</span></strong>
</label>
<input type="email" data-test-id="usermail" id="mail" name="usermail" required />
</p>
<p>
<label for="pwd">
<span>Password: </span>
<strong><span aria-label="required">*</span></strong>
</label>
<input type="password" data-test-id="password" id="pwd" name="password" required />
</p>
</section>
<section>
<h2>Payment information</h2>
<p>
<label for="card">
<span>Card type:</span>
</label>
<select data-test-id="usercard" id="card" name="usercard">
<option value="visa">Visa</option>
<option value="mastercard">Mastercard</option>
<option value="amex">American Express</option>
</select>
</p>
<p>
<label for="number">
<span>Card number:</span>
<strong><span aria-label="required">*</span></strong>
</label>
<input type="tel" data-test-id="card-number" id="number" name="cardnumber" required />
</p>
<p>
<label for="expiration">
<span>Expiration date:</span>
<strong><span aria-label="required">*</span></strong>
</label>
<input type="text" data-test-id="card-expiration" id="expiration" required placeholder="MM/YY"
pattern="^(0[1-9]|1[0-2])\/([0-9]{2})$" />
</p>
</section>
<section style="display: flex; gap: var(--dt-spacing-a); justify-content: flex-end; flex-wrap: wrap; margin-top: var(--dt-spacing-b);">
${showNativeResetButton ? html`<button data-test-id="button--reset" id="resetNativeButton" type="reset">Reset</button>` : Template({
...props,
variant: 'secondary',
slot: 'Reset',
type: 'reset',
})}
${showSubmitButton ? Template({
...props,
variant: 'primary',
slot: 'Submit payment',
type: 'submit',
}) : nothing}
</section>
</form>
<script>
// var is used to prevent storybook from erroring when the script is re-run
var form = document.querySelector('#testForm');
var formLog = document.querySelector('#formLog');

form.addEventListener('submit', (e) => {
e.preventDefault();

// Display a success message to the user when they submit the form
formLog.innerHTML = 'Form submitted!';
formLog.style.display = 'block';

const span = document.createElement('span');
span.id = 'formSubmittedFlag';
span.setAttribute('data-test-id', 'formSubmittedFlag');
span.style.display = 'none';
document.body.appendChild(span);

// Reset the success message after roughly 8 seconds
setTimeout(() => {
formLog.innerHTML = '';
formLog.style.display = 'none';
}, 8000);
});

</script>
`;

const createButtonStory = createStory<ButtonProps>(Template, defaultArgs);

const createButtonStoryWithForm = createStory<ButtonProps>(FormTemplate, defaultArgs);

export const Primary = createButtonStory();

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.<br>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<ButtonProps> = (props: ButtonProps) => html`
<form id="formSubmissionTestForm" action="/submit-endpoint" method="POST">
<input type="text" name="username" placeholder="Enter your username" required>
<pie-button
id="TestButton"
type="submit"
name="submitButton"
value="submitValue"
>
Submit
</pie-button>
</form>
`;

export const FormSubmission = createStory<ButtonProps>(FormSubmissionTemplate, {
...defaultArgs,
type: 'submit',
})();

// ... existing code ...

const FormWithAllAttributesTemplate: TemplateFunction<ButtonProps> = () => html`
<form id="testForm" action="/default-endpoint" method="GET">
<input type="text" name="username" required>
<pie-button id="testButton"
type="submit"
name="submitButton"
value="submitValue"
formaction="/custom-endpoint"
formenctype="multipart/form-data"
formmethod="POST"
formnovalidate
formtarget="_self">
Submit
</pie-button>
</form>
`;

export const FormWithAllAttributes = createStory<ButtonProps>(FormWithAllAttributesTemplate, {
...defaultArgs,
type: 'submit',
})();
Loading
Loading