From 65afb95e35d2389f7ce8f8d86e93ce8c31d6009a Mon Sep 17 00:00:00 2001 From: Tung Du Date: Mon, 21 Nov 2022 08:08:12 +0700 Subject: [PATCH] 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> --- .../product-query/product-filters.test.ts | 271 ++++++++++++++++++ .../utils/find-tools-panel-with-title.ts | 10 + .../tests/utils/get-fixture-products-data.ts | 5 +- .../utils/get-form-element-id-by-label.ts | 18 ++ .../tests/utils/get-toggle-id-by-label.ts | 24 +- .../woocommerce-blocks/tests/utils/index.js | 2 + 6 files changed, 318 insertions(+), 12 deletions(-) create mode 100644 plugins/woocommerce-blocks/tests/e2e/specs/backend/product-query/product-filters.test.ts create mode 100644 plugins/woocommerce-blocks/tests/utils/find-tools-panel-with-title.ts create mode 100644 plugins/woocommerce-blocks/tests/utils/get-form-element-id-by-label.ts diff --git a/plugins/woocommerce-blocks/tests/e2e/specs/backend/product-query/product-filters.test.ts b/plugins/woocommerce-blocks/tests/e2e/specs/backend/product-query/product-filters.test.ts new file mode 100644 index 00000000000..6fe09b21881 --- /dev/null +++ b/plugins/woocommerce-blocks/tests/e2e/specs/backend/product-query/product-filters.test.ts @@ -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 + ); + } ); + } ); + } +); diff --git a/plugins/woocommerce-blocks/tests/utils/find-tools-panel-with-title.ts b/plugins/woocommerce-blocks/tests/utils/find-tools-panel-with-title.ts new file mode 100644 index 00000000000..9620d217dd1 --- /dev/null +++ b/plugins/woocommerce-blocks/tests/utils/find-tools-panel-with-title.ts @@ -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 ); +}; diff --git a/plugins/woocommerce-blocks/tests/utils/get-fixture-products-data.ts b/plugins/woocommerce-blocks/tests/utils/get-fixture-products-data.ts index feee6eaf3cc..02a727e04c1 100644 --- a/plugins/woocommerce-blocks/tests/utils/get-fixture-products-data.ts +++ b/plugins/woocommerce-blocks/tests/utils/get-fixture-products-data.ts @@ -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 ); diff --git a/plugins/woocommerce-blocks/tests/utils/get-form-element-id-by-label.ts b/plugins/woocommerce-blocks/tests/utils/get-form-element-id-by-label.ts new file mode 100644 index 00000000000..a99721471dd --- /dev/null +++ b/plugins/woocommerce-blocks/tests/utils/get-form-element-id-by-label.ts @@ -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 + ); +}; diff --git a/plugins/woocommerce-blocks/tests/utils/get-toggle-id-by-label.ts b/plugins/woocommerce-blocks/tests/utils/get-toggle-id-by-label.ts index 51195446905..814204e6bf2 100644 --- a/plugins/woocommerce-blocks/tests/utils/get-toggle-id-by-label.ts +++ b/plugins/woocommerce-blocks/tests/utils/get-toggle-id-by-label.ts @@ -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 ); diff --git a/plugins/woocommerce-blocks/tests/utils/index.js b/plugins/woocommerce-blocks/tests/utils/index.js index b30ce1b84fd..0727d21e73e 100644 --- a/plugins/woocommerce-blocks/tests/utils/index.js +++ b/plugins/woocommerce-blocks/tests/utils/index.js @@ -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';