diff --git a/packages/components/src/h-stack/stories/e2e/index.tsx b/packages/components/src/h-stack/stories/e2e/index.tsx new file mode 100644 index 00000000000000..38bbf7174e4714 --- /dev/null +++ b/packages/components/src/h-stack/stories/e2e/index.tsx @@ -0,0 +1,36 @@ +/** + * External dependencies + */ +import type { ComponentStory, ComponentMeta } from '@storybook/react'; + +/** + * Internal dependencies + */ +import { View } from '../../../view'; +import { HStack } from '../..'; + +const meta: ComponentMeta< typeof HStack > = { + component: HStack, + title: 'Components (Experimental)/HStack', +}; +export default meta; + +const Template: ComponentStory< typeof HStack > = ( props ) => { + return ( + <HStack + style={ { background: '#eee', minHeight: '3rem' } } + { ...props } + > + { [ 'One', 'Two', 'Three', 'Four', 'Five' ].map( ( text ) => ( + <View key={ text } style={ { background: '#b9f9ff' } }> + { text } + </View> + ) ) } + </HStack> + ); +}; + +export const Default: ComponentStory< typeof HStack > = Template.bind( {} ); +Default.args = { + spacing: 3, +}; diff --git a/packages/components/src/v-stack/stories/e2e/index.tsx b/packages/components/src/v-stack/stories/e2e/index.tsx new file mode 100644 index 00000000000000..54456551757925 --- /dev/null +++ b/packages/components/src/v-stack/stories/e2e/index.tsx @@ -0,0 +1,36 @@ +/** + * External dependencies + */ +import type { ComponentStory, ComponentMeta } from '@storybook/react'; + +/** + * Internal dependencies + */ +import { View } from '../../../view'; +import { VStack } from '../..'; + +const meta: ComponentMeta< typeof VStack > = { + component: VStack, + title: 'Components (Experimental)/VStack', +}; +export default meta; + +const Template: ComponentStory< typeof VStack > = ( props ) => { + return ( + <VStack + { ...props } + style={ { background: '#eee', minHeight: '3rem' } } + > + { [ 'One', 'Two', 'Three', 'Four', 'Five' ].map( ( text ) => ( + <View key={ text } style={ { background: '#b9f9ff' } }> + { text } + </View> + ) ) } + </VStack> + ); +}; + +export const Default: ComponentStory< typeof VStack > = Template.bind( {} ); +Default.args = { + spacing: 3, +}; diff --git a/test/storybook-playwright/playwright.config.ts b/test/storybook-playwright/playwright.config.ts index c89f3780c7229e..eabe9b17121f2d 100644 --- a/test/storybook-playwright/playwright.config.ts +++ b/test/storybook-playwright/playwright.config.ts @@ -8,6 +8,7 @@ const config: PlaywrightTestConfig = { reporter: [ [ 'html', { open: 'on-failure', outputFolder: 'test-results/report' } ], ], + fullyParallel: true, }; export default config; diff --git a/test/storybook-playwright/specs/hstack.spec.ts b/test/storybook-playwright/specs/hstack.spec.ts new file mode 100644 index 00000000000000..f9be20b6c12e0a --- /dev/null +++ b/test/storybook-playwright/specs/hstack.spec.ts @@ -0,0 +1,55 @@ +/** + * External dependencies + */ +import { test } from '@playwright/test'; + +/** + * Internal dependencies + */ +import { + gotoStoryId, + getAllPropsPermutations, + testSnapshotForPropsConfig, +} from '../utils'; + +const PROP_VALUES_TO_TEST = [ + { + propName: 'alignment', + valuesToTest: [ + undefined, + 'top', + 'topLeft', + 'topRight', + 'left', + 'center', + 'right', + 'bottom', + 'bottomLeft', + 'bottomRight', + 'edge', + 'stretch', + ], + }, + { + propName: 'direction', + valuesToTest: [ undefined, 'row', 'column' ], + }, +]; + +test.describe( 'HStack', () => { + test.beforeEach( async ( { page } ) => { + await gotoStoryId( page, 'components-experimental-hstack--default', { + decorators: { marginChecker: 'show', customE2EControls: 'show' }, + } ); + } ); + + getAllPropsPermutations( PROP_VALUES_TO_TEST ).forEach( ( propsConfig ) => { + test( `should render with ${ JSON.stringify( propsConfig ) }`, async ( { + page, + } ) => { + await page.waitForSelector( '.components-h-stack' ); + + await testSnapshotForPropsConfig( page, propsConfig ); + } ); + } ); +} ); diff --git a/test/storybook-playwright/specs/vstack.spec.ts b/test/storybook-playwright/specs/vstack.spec.ts new file mode 100644 index 00000000000000..7a6bad02f79d60 --- /dev/null +++ b/test/storybook-playwright/specs/vstack.spec.ts @@ -0,0 +1,55 @@ +/** + * External dependencies + */ +import { test } from '@playwright/test'; + +/** + * Internal dependencies + */ +import { + gotoStoryId, + getAllPropsPermutations, + testSnapshotForPropsConfig, +} from '../utils'; + +const PROP_VALUES_TO_TEST = [ + { + propName: 'alignment', + valuesToTest: [ + undefined, + 'top', + 'topLeft', + 'topRight', + 'left', + 'center', + 'right', + 'bottom', + 'bottomLeft', + 'bottomRight', + 'edge', + 'stretch', + ], + }, + { + propName: 'direction', + valuesToTest: [ undefined, 'row', 'column' ], + }, +]; + +test.describe( 'VStack', () => { + test.beforeEach( async ( { page } ) => { + await gotoStoryId( page, 'components-experimental-vstack--default', { + decorators: { marginChecker: 'show', customE2EControls: 'show' }, + } ); + } ); + + getAllPropsPermutations( PROP_VALUES_TO_TEST ).forEach( ( propsConfig ) => { + test( `should render with ${ JSON.stringify( propsConfig ) }`, async ( { + page, + } ) => { + await page.waitForSelector( '.components-v-stack' ); + + await testSnapshotForPropsConfig( page, propsConfig ); + } ); + } ); +} ); diff --git a/test/storybook-playwright/storybook/decorators/with-custom-controls.js b/test/storybook-playwright/storybook/decorators/with-custom-controls.js new file mode 100644 index 00000000000000..0f8c8e059f0a01 --- /dev/null +++ b/test/storybook-playwright/storybook/decorators/with-custom-controls.js @@ -0,0 +1,58 @@ +/** + * WordPress dependencies + */ +import { useId, useState } from '@wordpress/element'; + +export const WithCustomControls = ( Story, context ) => { + const textareaId = useId(); + const [ partialProps, setPartialProps ] = useState( {} ); + + if ( context.globals.customE2EControls === 'hide' ) { + return <Story { ...context } />; + } + + const contextWithControlledProps = { + ...context, + // override args with the ones set by custom controls + args: { ...context.args, ...partialProps }, + }; + + return ( + <> + <Story { ...contextWithControlledProps } /> + + <p>Props:</p> + <pre> + { JSON.stringify( + contextWithControlledProps.args, + undefined, + 4 + ) } + </pre> + + <hr /> + + <form + name="e2e-controls-form" + onSubmit={ ( event ) => { + event.preventDefault(); + + const propsRawText = event.target.elements.props.value; + + const propsParsed = JSON.parse( propsRawText ); + + setPartialProps( ( oldProps ) => ( { + ...oldProps, + ...propsParsed, + } ) ); + } } + > + <p> + <label htmlFor={ textareaId }>Raw props</label> + <textarea name="props" id={ textareaId } /> + </p> + <button type="submit">Set props</button> + </form> + </> + ); +}; diff --git a/test/storybook-playwright/storybook/preview.js b/test/storybook-playwright/storybook/preview.js index 911578742e33e1..5a176d59a82e2c 100644 --- a/test/storybook-playwright/storybook/preview.js +++ b/test/storybook-playwright/storybook/preview.js @@ -1 +1,28 @@ -export * from '../../../storybook/preview'; +/** + * Internal dependencies + */ + +import * as basePreviewConfig from '../../../storybook/preview'; +import { WithCustomControls } from './decorators/with-custom-controls'; + +export const globalTypes = { + ...basePreviewConfig.globalTypes, + customE2EControls: { + name: 'Custom E2E Controls', + description: + 'Shows custom UI used by e2e tests for setting props programmatically', + defaultValue: 'hide', + toolbar: { + icon: 'edit', + items: [ + { value: 'hide', title: 'Hide' }, + { value: 'show', title: 'Show' }, + ], + }, + }, +}; +export const decorators = [ + ...basePreviewConfig.decorators, + WithCustomControls, +]; +export const parameters = { ...basePreviewConfig.parameters }; diff --git a/test/storybook-playwright/utils.ts b/test/storybook-playwright/utils.ts index 80be7297c88437..d3f37aa7df26bc 100644 --- a/test/storybook-playwright/utils.ts +++ b/test/storybook-playwright/utils.ts @@ -2,6 +2,7 @@ * External dependencies */ import type { Page } from '@playwright/test'; +import { expect } from '@playwright/test'; const STORYBOOK_PORT = '50241'; @@ -9,6 +10,7 @@ type Decorators = { css?: 'none' | 'basic' | 'wordpress'; direction?: 'ltr' | 'rtl'; marginChecker?: 'show' | 'hide'; + customE2EControls?: 'show' | 'hide'; }; type Options = { decorators?: Decorators }; @@ -38,3 +40,64 @@ export const gotoStoryId = ( { waitUntil: 'load' } ); }; + +/** + * Generate all possible permutations of those controls. + * + * @param propsConfig + */ +export const getAllPropsPermutations = ( + propsConfig: { + propName: string; + valuesToTest: any[]; + }[] +) => { + const allPropsPermutations: Record< string, any >[] = []; + + const iterateOverNextPropValues = async ( + remainingProps: typeof propsConfig, + accProps: Record< string, any > + ) => { + const [ propObject, ...restProps ] = remainingProps; + + // Test all values for the given prop. + for ( const value of propObject.valuesToTest ) { + const newAccProps = { + ...accProps, + [ propObject.propName ]: value, + }; + + if ( restProps.length === 0 ) { + // If we exhausted all of the props to set for this specific combination, + // let's add this combination to the `allPropsPermutations` array. + allPropsPermutations.push( newAccProps ); + } else { + // If there are more props to iterate through, let's do that through + // recursively calling this function. + iterateOverNextPropValues( restProps, newAccProps ); + } + } + }; + + // Start! + iterateOverNextPropValues( propsConfig, {} ); + + return allPropsPermutations; +}; + +export const testSnapshotForPropsConfig = async ( + page: Page, + propsConfig: Record< string, any > +) => { + const textarea = await page.getByLabel( 'Raw props', { exact: true } ); + const submitButton = await page.getByRole( 'button', { + name: 'Set props', + exact: true, + } ); + + await textarea.type( JSON.stringify( propsConfig ) ); + + await submitButton.click(); + + expect( await page.screenshot() ).toMatchSnapshot(); +};