Support testing many variants of a block at runtime via dynamically generated templates (#44223)

This commit is contained in:
Sam Seay 2024-02-16 20:00:36 +08:00 committed by GitHub
parent cb967f6dfd
commit 5ecea1b8c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 312 additions and 6 deletions

View File

@ -232,6 +232,7 @@
"github-label-sync": "2.2.0",
"glob": "7.2.3",
"glob-promise": "4.2.2",
"handlebars": "^4.7.8",
"husky": "8.0.3",
"ignore-loader": "0.1.2",
"jest": "^29.7.0",

View File

@ -104,7 +104,7 @@ pnpm run test:e2e:classic-theme
pnpm run test:e2e:block-theme-with-templates
```
_Note: All command parameters of `test:e2e` can be used for these commands too.
\_Note: All command parameters of `test:e2e` can be used for these commands too.
### Other ways of running tests
@ -144,6 +144,75 @@ To see all options, run the following command:
npx playwright test --help
```
### Generating dynamic posts to test block variations
Testing a single block can be daunting considering all the different attribute combinations that could be
considered valid for a single block. The basic templating system available in this test suite allows for
the generation of dynamic posts that can be used to test block variations.
Templates use the Handlebars templating system and you can put them anywhere. It's simplest to co-locate them
with the test. You can easily pass custom attributes to a block in your template using the wp-block helper
we've defined.
It looks like this in the template:
```handlebars
{{#> wp-block name="woocommerce/featured-category" attributes=attributes /}}
You can nest content here if you want to test the block with some content.
{{/wp-block}}
```
In your tests you can use `createPostFromTemplate` to create a post containing your template. If you use it
more than once in your test you can extend the test suite and provide the posts as fixtures, like in the example
below
```js
import { test as base } from '@playwright/test';
const test = base.extend< {
dropdownBlockPostPage: TestingPost;
defaultBlockPostPage: TestingPost;
} >( {
defaultBlockPostPage: async ( { requestUtils }, use ) => {
const testingPost = await requestUtils.createPostFromTemplate(
requestUtils,
{ title: 'Product Filter Stock Status Block' },
TEMPLATE_PATH,
{}
);
await use( testingPost );
await requestUtils.deletePost( post.id );
},
dropdownBlockPostPage: async ( { requestUtils }, use ) => {
const testingPost = await requestUtils.createPostFromTemplate(
requestUtils,
{ title: 'Product Filter Stock Status Block' },
TEMPLATE_PATH,
{
attributes: {
displayStyle: 'dropdown',
},
}
);
await use( testingPost );
await requestUtils.deletePost( post.id );
},
} );
```
In your test you can navigate to the page. You won't need to clean it up, because
the fixture will take care of that for you.
```js
test( 'Test the block', async ( { page, defaultBlockPostPage } ) => {
await page.goto( defaultBlockPostPage.link );
// do your tests here
} );
```
### Troubleshooting
If you run into problems the first time you try to run the tests, please run the following command before starting the test suite:

View File

@ -10,7 +10,6 @@ import {
PageUtils,
RequestUtils,
} from '@wordpress/e2e-test-utils-playwright';
import {
TemplateApiUtils,
STORAGE_STATE_PATH,
@ -23,6 +22,16 @@ import {
MiniCartUtils,
WPCLIUtils,
} from '@woocommerce/e2e-utils';
import { Post } from '@wordpress/e2e-test-utils-playwright/build-types/request-utils/posts';
/**
* Internal dependencies
*/
import {
PostPayload,
createPostFromTemplate,
deletePost,
} from '../utils/create-dynamic-content';
/**
* Set of console logging types observed to protect against unexpected yet
@ -124,7 +133,14 @@ const test = base.extend<
wpCliUtils: WPCLIUtils;
},
{
requestUtils: RequestUtils;
requestUtils: RequestUtils & {
createPostFromTemplate: (
post: PostPayload,
templatePath: string,
data: unknown
) => Promise< Post >;
deletePost: ( id: number ) => Promise< void >;
};
}
>( {
admin: async ( { page, pageUtils }, use ) => {
@ -181,7 +197,26 @@ const test = base.extend<
storageStatePath: STORAGE_STATE_PATH,
} );
await use( requestUtils );
const utilCreatePostFromTemplate = (
post: Partial< PostPayload >,
templatePath: string,
data: unknown
) =>
createPostFromTemplate(
requestUtils,
post,
templatePath,
data
);
const utilDeletePost = ( id: number ) =>
deletePost( requestUtils, id );
await use( {
...requestUtils,
createPostFromTemplate: utilCreatePostFromTemplate,
deletePost: utilDeletePost,
} );
},
{ scope: 'worker', auto: true },
],

View File

@ -0,0 +1,94 @@
/**
* External dependencies
*/
import { test as base, expect } from '@woocommerce/e2e-playwright-utils';
import path from 'path';
import { Post } from '@wordpress/e2e-test-utils-playwright/build-types/request-utils/posts';
const TEMPLATE_PATH = path.join( __dirname, './stock-status.handlebars' );
const test = base.extend< {
dropdownBlockPostPage: Post;
defaultBlockPostPage: Post;
} >( {
defaultBlockPostPage: async ( { requestUtils }, use ) => {
const testingPost = await requestUtils.createPostFromTemplate(
{ title: 'Product Filter Stock Status Block' },
TEMPLATE_PATH,
{}
);
await use( testingPost );
await requestUtils.deletePost( testingPost.id );
},
dropdownBlockPostPage: async ( { requestUtils }, use ) => {
const testingPost = await requestUtils.createPostFromTemplate(
{ title: 'Product Filter Stock Status Block' },
TEMPLATE_PATH,
{
attributes: {
displayStyle: 'dropdown',
},
}
);
await use( testingPost );
await requestUtils.deletePost( testingPost.id );
},
} );
test.describe( 'Product Filter: Stock Status Block', async () => {
test.describe( 'With default display style', () => {
test( 'renders a checkbox list with the available stock statuses', async ( {
page,
defaultBlockPostPage,
} ) => {
await page.goto( defaultBlockPostPage.link );
const stockStatuses = page.locator(
'.wc-block-components-checkbox__label'
);
await expect( stockStatuses ).toHaveCount( 2 );
await expect( stockStatuses.nth( 0 ) ).toHaveText( 'In stock' );
await expect( stockStatuses.nth( 1 ) ).toHaveText( 'Out of stock' );
} );
test( 'filters the list of products by selecting a stock status', async ( {
page,
defaultBlockPostPage,
} ) => {
await page.goto( defaultBlockPostPage.link );
const outOfStockCheckbox = page.getByText( 'Out of stock' );
await outOfStockCheckbox.click();
// wait for navigation
await page.waitForURL( /.*filter_stock_status=outofstock.*/ );
const products = page.locator( '.wc-block-product' );
await expect( products ).toHaveCount( 1 );
} );
} );
test.describe( 'With dropdown display style', () => {
test( 'a dropdown is displayed with the available stock statuses', async ( {
page,
dropdownBlockPostPage,
} ) => {
await page.goto( dropdownBlockPostPage.link );
const dropdownLocator = page.locator(
'.wc-interactivity-dropdown'
);
await expect( dropdownLocator ).toBeVisible();
await dropdownLocator.click();
await expect( page.getByText( 'In stock' ) ).toBeVisible();
await expect( page.getByText( 'Out of stock' ) ).toBeVisible();
} );
} );
} );

View File

@ -0,0 +1,22 @@
<!-- wp:woocommerce/product-collection {"id":"bee7a337-f64e-4efd-be51-e68670b10000","queryId":0,"query":{"perPage":9,"pages":0,"offset":0,"postType":"product","order":"asc","orderBy":"title","search":"","exclude":[],"inherit":false,"taxQuery":{},"isProductCollectionBlock":true,"featured":false,"woocommerceOnSale":false,"woocommerceStockStatus":["instock","outofstock","onbackorder"],"woocommerceAttributes":[],"woocommerceHandPickedProducts":[]},"tagName":"div","displayLayout":{"type":"flex","columns":3,"shrinkColumns":true}} -->
<div class='wp-block-woocommerce-product-collection'>
<!-- wp:woocommerce/product-template -->
<!-- wp:woocommerce/product-image {"imageSizing":"thumbnail","isDescendentOfQueryLoop":true} /-->
<!-- wp:post-title {"textAlign":"center","level":3,"isLink":true,"style":{"spacing":{"margin":{"bottom":"0.75rem","top":"0"}}},"fontSize":"medium","__woocommerceNamespace":"woocommerce/product-collection/product-title"} /-->
<!-- wp:woocommerce/product-price {"isDescendentOfQueryLoop":true,"textAlign":"center","fontSize":"small"} /-->
<!-- wp:woocommerce/product-button {"textAlign":"center","isDescendentOfQueryLoop":true,"fontSize":"small"} /-->
<!-- /wp:woocommerce/product-template -->
<!-- wp:woocommerce/product-filter {"filterType":"stock-filter","heading":"Filter by Stock Status"} -->
<!-- wp:heading {"level":3} -->
<h3 class='wp-block-heading'>Filter by Stock Status</h3>
<!-- /wp:heading -->
{{#> wp-block blockName='woocommerce/product-filter-stock-status' attributes=attributes }}
{{/ wp-block }}
</div>
<!-- /wp:woocommerce/product-collection -->

View File

@ -1,9 +1,17 @@
/**
* External dependencies
*/
import { exec } from 'child_process';
import { ExecException, exec } from 'child_process';
export function cli( cmd, args = [] ) {
export function cli(
cmd: string,
args = []
): Promise< {
code: number;
error: ExecException | null;
stdout: string;
stderr: string;
} > {
return new Promise( ( resolve ) => {
exec( `${ cmd } ${ args.join( ' ' ) }`, ( error, stdout, stderr ) => {
resolve( {

View File

@ -0,0 +1,70 @@
/**
* External dependencies
*/
import { readFile } from 'fs/promises';
import Handlebars from 'handlebars';
import {
CreatePostPayload,
Post,
} from '@wordpress/e2e-test-utils-playwright/build-types/request-utils/posts';
import { RequestUtils } from '@wordpress/e2e-test-utils-playwright';
export type TestingPost = {
post: Post;
deletePost: () => Promise< void >;
};
Handlebars.registerPartial(
'wp-block',
`
<!-- wp:{{blockName}} {{{stringify attributes}}} -->
{{> @partial-block }}
<!-- /wp:{{blockName}} -->
`
);
Handlebars.registerHelper( 'stringify', function ( context ) {
return JSON.stringify( context );
} );
export const deletePost = async ( requestUtils: RequestUtils, id: number ) => {
return requestUtils.rest( {
method: 'DELETE',
path: `/wp/v2/posts/${ id }`,
params: {
force: true,
},
} );
};
const posts: number[] = [];
const createPost = async (
requestUtils: RequestUtils,
payload: CreatePostPayload
) => {
const post = await requestUtils.createPost( payload );
posts.push( post.id );
return post;
};
export type PostPayload = Partial< CreatePostPayload >;
export const createPostFromTemplate = async (
requestUtils: RequestUtils,
post: PostPayload,
templatePath: string,
data: unknown
) => {
const templateContent = await readFile( templatePath, 'utf8' );
const content = Handlebars.compile( templateContent )( data );
const payload: CreatePostPayload = {
status: 'publish',
date_gmt: new Date().toISOString(),
content,
...post,
};
return createPost( requestUtils, payload );
};

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Comment: Add a system to support dynamic template testing in blocks e2e tests.

View File

@ -4326,6 +4326,9 @@ importers:
glob-promise:
specifier: 4.2.2
version: 4.2.2(glob@7.2.3)
handlebars:
specifier: ^4.7.8
version: 4.7.8
husky:
specifier: 8.0.3
version: 8.0.3