Product Query E2E tests: Sale and Stock status filters tests (https://github.com/woocommerce/woocommerce-blocks/pull/7684)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Tung Du 2022-11-21 08:08:12 +07:00 committed by GitHub
parent 21beb29442
commit 65afb95e35
6 changed files with 318 additions and 12 deletions

View File

@ -0,0 +1,271 @@
/**
* External dependencies
*/
import { canvas, setPostContent, insertBlock } from '@wordpress/e2e-test-utils';
import {
visitBlockPage,
saveOrPublish,
selectBlockByName,
findToolsPanelWithTitle,
getFixtureProductsData,
getFormElementIdByLabel,
shopper,
getToggleIdByLabel,
} from '@woocommerce/blocks-test-utils';
import { ElementHandle } from 'puppeteer';
import { setCheckbox, unsetCheckbox } from '@woocommerce/e2e-utils';
/**
* Internal dependencies
*/
import {
GUTENBERG_EDITOR_CONTEXT,
describeOrSkip,
waitForCanvas,
openBlockEditorSettings,
} from '../../../utils';
const block = {
name: 'Product Query',
slug: 'core/query',
class: '.wp-block-query',
};
/**
* Selectors used for interacting with the block in the editor. These selectors
* can be changed upstream in Gutenberg, so we scope them here for
* maintainability.
*
* There are also some labels that are used repeatedly, but we don't scope them
* in favor of readability. Unlike selectors, those label are visible to end
* users, so it's easier to understand what's going on if we don't scope them.
* Those labels can get upated in the future, but the tests will fail and we'll
* know to update them.
*/
const SELECTORS = {
productFiltersDropdownButton: (
{ expanded }: { expanded: boolean } = { expanded: false }
) =>
`.components-tools-panel-header .components-dropdown-menu button[aria-expanded="${ expanded }"]`,
productFiltersDropdown:
'.components-dropdown-menu__menu[aria-label="Product filters options"]',
productFiltersDropdownItem: '.components-menu-item__button',
editorPreview: {
productsGrid: 'ul.wp-block-post-template',
productsGridItem:
'ul.wp-block-post-template > li.block-editor-block-preview__live-content',
},
productsGrid: `${ block.class } ul.wp-block-post-template`,
productsGridItem: `${ block.class } ul.wp-block-post-template > li.product`,
formTokenFieldLabel: '.components-form-token-field__label',
tokenRemoveButton: '.components-form-token-field__remove-token',
};
const toggleProductFilter = async ( filterName: string ) => {
const $productFiltersPanel = await findToolsPanelWithTitle(
'Product filters'
);
await expect( $productFiltersPanel ).toClick(
SELECTORS.productFiltersDropdownButton()
);
await canvas().waitForSelector( SELECTORS.productFiltersDropdown );
await expect( canvas() ).toClick( SELECTORS.productFiltersDropdownItem, {
text: filterName,
} );
await expect( $productFiltersPanel ).toClick(
SELECTORS.productFiltersDropdownButton( { expanded: true } )
);
};
const resetProductQueryBlockPage = async () => {
await visitBlockPage( `${ block.name } Block` );
await waitForCanvas();
await setPostContent( '' );
await insertBlock( block.name );
await saveOrPublish();
};
const getPreviewProducts = async (): Promise< ElementHandle[] > => {
await canvas().waitForSelector( SELECTORS.editorPreview.productsGrid );
return await canvas().$$( SELECTORS.editorPreview.productsGridItem );
};
const getFrontEndProducts = async (): Promise< ElementHandle[] > => {
await canvas().waitForSelector( SELECTORS.productsGrid );
return await canvas().$$( SELECTORS.productsGridItem );
};
describeOrSkip( GUTENBERG_EDITOR_CONTEXT === 'gutenberg' )(
'Product Query > Products Filters',
() => {
let $productFiltersPanel: ElementHandle< Node >;
beforeEach( async () => {
/**
* Reset the block page before each test to ensure the block is
* inserted in a known state. This is also needed to ensure each
* test can be run individually.
*/
await resetProductQueryBlockPage();
await openBlockEditorSettings();
await selectBlockByName( block.slug );
$productFiltersPanel = await findToolsPanelWithTitle(
'Product filters'
);
} );
/**
* Reset the content of Product Query Block page after this test suite
* to avoid breaking other tests.
*/
afterAll( async () => {
await resetProductQueryBlockPage();
} );
describe( 'Sale Status', () => {
it( 'Sale status is disabled by default', async () => {
await expect( $productFiltersPanel ).not.toMatch(
'Show only products on sale'
);
} );
it( 'Can add and remove Sale Status filter', async () => {
await toggleProductFilter( 'Sale status' );
await expect( $productFiltersPanel ).toMatch(
'Show only products on sale'
);
await toggleProductFilter( 'Sale status' );
await expect( $productFiltersPanel ).not.toMatch(
'Show only products on sale'
);
} );
it( 'Editor preview shows correct products corresponding to the value `Show only products on sale`', async () => {
const defaultCount = getFixtureProductsData().length;
const saleCount = getFixtureProductsData( 'sale_price' ).length;
expect( await getPreviewProducts() ).toHaveLength(
defaultCount
);
await toggleProductFilter( 'Sale status' );
await setCheckbox(
await getToggleIdByLabel( 'Show only products on sale' )
);
expect( await getPreviewProducts() ).toHaveLength( saleCount );
await unsetCheckbox(
await getToggleIdByLabel( 'Show only products on sale' )
);
expect( await getPreviewProducts() ).toHaveLength(
defaultCount
);
} );
it( 'Works on the front end', async () => {
await toggleProductFilter( 'Sale status' );
await setCheckbox(
await getToggleIdByLabel( 'Show only products on sale' )
);
await canvas().waitForSelector(
SELECTORS.editorPreview.productsGrid
);
await saveOrPublish();
await shopper.block.goToBlockPage( block.name );
const saleCount = getFixtureProductsData( 'sale_price' ).length;
expect( await getFrontEndProducts() ).toHaveLength( saleCount );
} );
} );
describe( 'Stock Status', () => {
it( 'Stock status is enabled by default', async () => {
await expect( $productFiltersPanel ).toMatchElement(
SELECTORS.formTokenFieldLabel,
{ text: 'Stock status' }
);
} );
it( 'Can add and remove Stock Status filter', async () => {
await toggleProductFilter( 'Stock status' );
await expect( $productFiltersPanel ).not.toMatchElement(
SELECTORS.formTokenFieldLabel,
{ text: 'Stock status' }
);
await toggleProductFilter( 'Stock status' );
await expect( $productFiltersPanel ).toMatchElement(
SELECTORS.formTokenFieldLabel,
{ text: 'Stock status' }
);
} );
it( 'All statuses are enabled by default', async () => {
await expect( $productFiltersPanel ).toMatch( 'In stock' );
await expect( $productFiltersPanel ).toMatch( 'Out of stock' );
await expect( $productFiltersPanel ).toMatch( 'On backorder' );
} );
it( 'Editor preview shows all products by default', async () => {
const defaultCount = getFixtureProductsData().length;
expect( await getPreviewProducts() ).toHaveLength(
defaultCount
);
} );
/**
* Skipping this test for now as Product Query doesn't show correct set of products based on stock status.
*
* @see https://github.com/woocommerce/woocommerce-blocks/pull/7682
*/
it.skip( 'Editor preview shows correct products that has enabled stock statuses', async () => {
const $$tokenRemoveButtons = await $productFiltersPanel.$$(
SELECTORS.tokenRemoveButton
);
for ( const $el of $$tokenRemoveButtons ) {
await $el.click();
}
const $stockStatusInput = await canvas().$(
await getFormElementIdByLabel(
'Stock status',
SELECTORS.formTokenFieldLabel.replace( '.', '' )
)
);
await $stockStatusInput.click();
await canvas().keyboard.type( 'Out of Stock' );
await canvas().keyboard.press( 'Enter' );
const outOfStockCount = getFixtureProductsData(
'stock_status'
).filter( ( status ) => status === 'outofstock' ).length;
expect( await getPreviewProducts() ).toHaveLength(
outOfStockCount
);
} );
it( 'Works on the front end', async () => {
const tokenRemoveButtons = await $productFiltersPanel.$$(
SELECTORS.tokenRemoveButton
);
for ( const el of tokenRemoveButtons ) {
await el.click();
}
const $stockStatusInput = await canvas().$(
await getFormElementIdByLabel(
'Stock status',
SELECTORS.formTokenFieldLabel.replace( '.', '' )
)
);
await $stockStatusInput.click();
await canvas().keyboard.type( 'Out of stock' );
await canvas().keyboard.press( 'Enter' );
await canvas().waitForSelector(
SELECTORS.editorPreview.productsGrid
);
await saveOrPublish();
await shopper.block.goToBlockPage( block.name );
const outOfStockCount = getFixtureProductsData(
'stock_status'
).filter( ( status ) => status === 'outofstock' ).length;
expect( await getFrontEndProducts() ).toHaveLength(
outOfStockCount
);
} );
} );
}
);

View File

@ -0,0 +1,10 @@
/**
* External dependencies
*/
import { canvas } from '@wordpress/e2e-test-utils';
export const findToolsPanelWithTitle = async ( panelTitle: string ) => {
const panelToggleSelector = `//div[contains(@class, "components-tools-panel-header")]//h2[contains(@class, "components-heading") and contains(text(),"${ panelTitle }")]`;
const panelSelector = `${ panelToggleSelector }/ancestor::*[contains(concat(" ", @class, " "), " components-tools-panel ")]`;
return await canvas().waitForXPath( panelSelector );
};

View File

@ -6,7 +6,10 @@ import { Products } from '../e2e/fixtures/fixture-data';
/**
* Get products data by key from fixtures.
*/
export const getFixtureProductsData = ( key: string ) => {
export const getFixtureProductsData = ( key = '' ) => {
if ( ! key ) {
return Products();
}
return Products()
.map( ( product ) => product[ key ] )
.filter( Boolean );

View File

@ -0,0 +1,18 @@
/**
* External dependencies
*/
import { canvas } from '@wordpress/e2e-test-utils';
export const getFormElementIdByLabel = async (
text: string,
className: string
) => {
const labelElement = await canvas().waitForXPath(
`//label[contains(text(), "${ text }") and contains(@class, "${ className }")]`,
{ visible: true }
);
return await canvas().evaluate(
( label ) => `#${ label.getAttribute( 'for' ) }`,
labelElement
);
};

View File

@ -1,7 +1,13 @@
/**
* External dependencies
*/
import { canvas } from '@wordpress/e2e-test-utils';
/**
* Internal dependencies
*/
import { DEFAULT_TIMEOUT } from './constants';
import { getFormElementIdByLabel } from './get-form-element-id-by-label';
/**
* Get the ID of the setting toogle so test can manipulate the toggle using
@ -12,24 +18,20 @@ import { DEFAULT_TIMEOUT } from './constants';
* check if the node still attached to the document before returning its
* ID. If the node is detached, it means that the toggle is rendered, then
* we retry by calling this function again with increased retry argument. We
* will retry until the timeout is reached.
* will retry until the default timeout is reached, which is 30s.
*/
export const getToggleIdByLabel = async (
label: string,
retry = 0
): Promise< string > => {
const delay = 1000;
const labelElement = await page.waitForXPath(
`//label[contains(text(), "${ label }") and contains(@class, "components-toggle-control__label")]`,
{ visible: true }
);
const checkboxId = await page.evaluate(
( toggleLabel ) => `#${ toggleLabel.getAttribute( 'for' ) }`,
labelElement
);
// Wait a bit for toggle to finish rerendering.
await page.waitForTimeout( delay );
const checkbox = await page.$( checkboxId );
await canvas().waitForTimeout( delay );
const checkboxId = await getFormElementIdByLabel(
label,
'components-toggle-control__label'
);
const checkbox = await canvas().$( checkboxId );
if ( ! checkbox ) {
if ( retry * delay < DEFAULT_TIMEOUT ) {
return await getToggleIdByLabel( label, retry + 1 );

View File

@ -18,6 +18,8 @@ export * from './taxes';
export * from './constants';
export { insertInnerBlock } from './insert-inner-block';
export { getFixtureProductsData } from './get-fixture-products-data';
export { findToolsPanelWithTitle } from './find-tools-panel-with-title';
export { getFormElementIdByLabel } from './get-form-element-id-by-label';
export { getToggleIdByLabel } from './get-toggle-id-by-label';
export { insertBlockUsingQuickInserter } from './insert-block-using-quick-inserter';
export { insertBlockUsingSlash } from './insert-block-using-slash';