Skip to content

Commit

Permalink
VizReg e2e tests: programmatically test all combinations of a given l…
Browse files Browse the repository at this point in the history
…ist of props/values (#48260)

* Add custom controls decorator

* Add playwright util for parsing e2e controls and testing all combinations via snapshots

* Add HStack e2e story + playwright test

* Add VStack e2e story + playwright test

* Cleanup unused values

* Typo

* Simplify decorator, use globalTypes to enable it

* Update Playwright utils

1. expect prop config as input when computing permutations
2. use new form inputs to submit prop config instead of individual prop controls

* Move list of props to test from Storybook examples to test specs

* Update JSDoc

* Run tests in parallel

* Remove unnecessary imports

* Make the whole VizReg Playwright project to be fully parallel
  • Loading branch information
ciampo committed Feb 24, 2023
1 parent e630cf5 commit a4c2395
Show file tree
Hide file tree
Showing 8 changed files with 332 additions and 1 deletion.
36 changes: 36 additions & 0 deletions packages/components/src/h-stack/stories/e2e/index.tsx
Original file line number Diff line number Diff line change
@@ -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,
};
36 changes: 36 additions & 0 deletions packages/components/src/v-stack/stories/e2e/index.tsx
Original file line number Diff line number Diff line change
@@ -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,
};
1 change: 1 addition & 0 deletions test/storybook-playwright/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const config: PlaywrightTestConfig = {
reporter: [
[ 'html', { open: 'on-failure', outputFolder: 'test-results/report' } ],
],
fullyParallel: true,
};

export default config;
55 changes: 55 additions & 0 deletions test/storybook-playwright/specs/hstack.spec.ts
Original file line number Diff line number Diff line change
@@ -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 );
} );
} );
} );
55 changes: 55 additions & 0 deletions test/storybook-playwright/specs/vstack.spec.ts
Original file line number Diff line number Diff line change
@@ -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 );
} );
} );
} );
Original file line number Diff line number Diff line change
@@ -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>
</>
);
};
29 changes: 28 additions & 1 deletion test/storybook-playwright/storybook/preview.js
Original file line number Diff line number Diff line change
@@ -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 };
63 changes: 63 additions & 0 deletions test/storybook-playwright/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
* External dependencies
*/
import type { Page } from '@playwright/test';
import { expect } from '@playwright/test';

const STORYBOOK_PORT = '50241';

type Decorators = {
css?: 'none' | 'basic' | 'wordpress';
direction?: 'ltr' | 'rtl';
marginChecker?: 'show' | 'hide';
customE2EControls?: 'show' | 'hide';
};
type Options = { decorators?: Decorators };

Expand Down Expand Up @@ -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();
};

1 comment on commit a4c2395

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flaky tests detected in a4c2395.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/4261644126
📝 Reported issues:

Please sign in to comment.