Skip to content
This repository was archived by the owner on Feb 23, 2024. It is now read-only.

Commit cef076a

Browse files
Product Query E2E tests: Sale and Stock status filters tests (#7684)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent bad1af8 commit cef076a

File tree

6 files changed

+318
-12
lines changed

6 files changed

+318
-12
lines changed
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
/**
2+
* External dependencies
3+
*/
4+
import { canvas, setPostContent, insertBlock } from '@wordpress/e2e-test-utils';
5+
import {
6+
visitBlockPage,
7+
saveOrPublish,
8+
selectBlockByName,
9+
findToolsPanelWithTitle,
10+
getFixtureProductsData,
11+
getFormElementIdByLabel,
12+
shopper,
13+
getToggleIdByLabel,
14+
} from '@woocommerce/blocks-test-utils';
15+
import { ElementHandle } from 'puppeteer';
16+
import { setCheckbox, unsetCheckbox } from '@woocommerce/e2e-utils';
17+
18+
/**
19+
* Internal dependencies
20+
*/
21+
import {
22+
GUTENBERG_EDITOR_CONTEXT,
23+
describeOrSkip,
24+
waitForCanvas,
25+
openBlockEditorSettings,
26+
} from '../../../utils';
27+
28+
const block = {
29+
name: 'Product Query',
30+
slug: 'core/query',
31+
class: '.wp-block-query',
32+
};
33+
34+
/**
35+
* Selectors used for interacting with the block in the editor. These selectors
36+
* can be changed upstream in Gutenberg, so we scope them here for
37+
* maintainability.
38+
*
39+
* There are also some labels that are used repeatedly, but we don't scope them
40+
* in favor of readability. Unlike selectors, those label are visible to end
41+
* users, so it's easier to understand what's going on if we don't scope them.
42+
* Those labels can get upated in the future, but the tests will fail and we'll
43+
* know to update them.
44+
*/
45+
const SELECTORS = {
46+
productFiltersDropdownButton: (
47+
{ expanded }: { expanded: boolean } = { expanded: false }
48+
) =>
49+
`.components-tools-panel-header .components-dropdown-menu button[aria-expanded="${ expanded }"]`,
50+
productFiltersDropdown:
51+
'.components-dropdown-menu__menu[aria-label="Product filters options"]',
52+
productFiltersDropdownItem: '.components-menu-item__button',
53+
editorPreview: {
54+
productsGrid: 'ul.wp-block-post-template',
55+
productsGridItem:
56+
'ul.wp-block-post-template > li.block-editor-block-preview__live-content',
57+
},
58+
productsGrid: `${ block.class } ul.wp-block-post-template`,
59+
productsGridItem: `${ block.class } ul.wp-block-post-template > li.product`,
60+
formTokenFieldLabel: '.components-form-token-field__label',
61+
tokenRemoveButton: '.components-form-token-field__remove-token',
62+
};
63+
64+
const toggleProductFilter = async ( filterName: string ) => {
65+
const $productFiltersPanel = await findToolsPanelWithTitle(
66+
'Product filters'
67+
);
68+
await expect( $productFiltersPanel ).toClick(
69+
SELECTORS.productFiltersDropdownButton()
70+
);
71+
await canvas().waitForSelector( SELECTORS.productFiltersDropdown );
72+
await expect( canvas() ).toClick( SELECTORS.productFiltersDropdownItem, {
73+
text: filterName,
74+
} );
75+
await expect( $productFiltersPanel ).toClick(
76+
SELECTORS.productFiltersDropdownButton( { expanded: true } )
77+
);
78+
};
79+
80+
const resetProductQueryBlockPage = async () => {
81+
await visitBlockPage( `${ block.name } Block` );
82+
await waitForCanvas();
83+
await setPostContent( '' );
84+
await insertBlock( block.name );
85+
await saveOrPublish();
86+
};
87+
88+
const getPreviewProducts = async (): Promise< ElementHandle[] > => {
89+
await canvas().waitForSelector( SELECTORS.editorPreview.productsGrid );
90+
return await canvas().$$( SELECTORS.editorPreview.productsGridItem );
91+
};
92+
93+
const getFrontEndProducts = async (): Promise< ElementHandle[] > => {
94+
await canvas().waitForSelector( SELECTORS.productsGrid );
95+
return await canvas().$$( SELECTORS.productsGridItem );
96+
};
97+
98+
describeOrSkip( GUTENBERG_EDITOR_CONTEXT === 'gutenberg' )(
99+
'Product Query > Products Filters',
100+
() => {
101+
let $productFiltersPanel: ElementHandle< Node >;
102+
beforeEach( async () => {
103+
/**
104+
* Reset the block page before each test to ensure the block is
105+
* inserted in a known state. This is also needed to ensure each
106+
* test can be run individually.
107+
*/
108+
await resetProductQueryBlockPage();
109+
await openBlockEditorSettings();
110+
await selectBlockByName( block.slug );
111+
$productFiltersPanel = await findToolsPanelWithTitle(
112+
'Product filters'
113+
);
114+
} );
115+
116+
/**
117+
* Reset the content of Product Query Block page after this test suite
118+
* to avoid breaking other tests.
119+
*/
120+
afterAll( async () => {
121+
await resetProductQueryBlockPage();
122+
} );
123+
124+
describe( 'Sale Status', () => {
125+
it( 'Sale status is disabled by default', async () => {
126+
await expect( $productFiltersPanel ).not.toMatch(
127+
'Show only products on sale'
128+
);
129+
} );
130+
131+
it( 'Can add and remove Sale Status filter', async () => {
132+
await toggleProductFilter( 'Sale status' );
133+
await expect( $productFiltersPanel ).toMatch(
134+
'Show only products on sale'
135+
);
136+
await toggleProductFilter( 'Sale status' );
137+
await expect( $productFiltersPanel ).not.toMatch(
138+
'Show only products on sale'
139+
);
140+
} );
141+
142+
it( 'Editor preview shows correct products corresponding to the value `Show only products on sale`', async () => {
143+
const defaultCount = getFixtureProductsData().length;
144+
const saleCount = getFixtureProductsData( 'sale_price' ).length;
145+
expect( await getPreviewProducts() ).toHaveLength(
146+
defaultCount
147+
);
148+
await toggleProductFilter( 'Sale status' );
149+
await setCheckbox(
150+
await getToggleIdByLabel( 'Show only products on sale' )
151+
);
152+
expect( await getPreviewProducts() ).toHaveLength( saleCount );
153+
await unsetCheckbox(
154+
await getToggleIdByLabel( 'Show only products on sale' )
155+
);
156+
expect( await getPreviewProducts() ).toHaveLength(
157+
defaultCount
158+
);
159+
} );
160+
161+
it( 'Works on the front end', async () => {
162+
await toggleProductFilter( 'Sale status' );
163+
await setCheckbox(
164+
await getToggleIdByLabel( 'Show only products on sale' )
165+
);
166+
await canvas().waitForSelector(
167+
SELECTORS.editorPreview.productsGrid
168+
);
169+
await saveOrPublish();
170+
await shopper.block.goToBlockPage( block.name );
171+
const saleCount = getFixtureProductsData( 'sale_price' ).length;
172+
expect( await getFrontEndProducts() ).toHaveLength( saleCount );
173+
} );
174+
} );
175+
176+
describe( 'Stock Status', () => {
177+
it( 'Stock status is enabled by default', async () => {
178+
await expect( $productFiltersPanel ).toMatchElement(
179+
SELECTORS.formTokenFieldLabel,
180+
{ text: 'Stock status' }
181+
);
182+
} );
183+
184+
it( 'Can add and remove Stock Status filter', async () => {
185+
await toggleProductFilter( 'Stock status' );
186+
await expect( $productFiltersPanel ).not.toMatchElement(
187+
SELECTORS.formTokenFieldLabel,
188+
{ text: 'Stock status' }
189+
);
190+
await toggleProductFilter( 'Stock status' );
191+
await expect( $productFiltersPanel ).toMatchElement(
192+
SELECTORS.formTokenFieldLabel,
193+
{ text: 'Stock status' }
194+
);
195+
} );
196+
197+
it( 'All statuses are enabled by default', async () => {
198+
await expect( $productFiltersPanel ).toMatch( 'In stock' );
199+
await expect( $productFiltersPanel ).toMatch( 'Out of stock' );
200+
await expect( $productFiltersPanel ).toMatch( 'On backorder' );
201+
} );
202+
203+
it( 'Editor preview shows all products by default', async () => {
204+
const defaultCount = getFixtureProductsData().length;
205+
206+
expect( await getPreviewProducts() ).toHaveLength(
207+
defaultCount
208+
);
209+
} );
210+
211+
/**
212+
* Skipping this test for now as Product Query doesn't show correct set of products based on stock status.
213+
*
214+
* @see https://github.com/woocommerce/woocommerce-blocks/pull/7682
215+
*/
216+
it.skip( 'Editor preview shows correct products that has enabled stock statuses', async () => {
217+
const $$tokenRemoveButtons = await $productFiltersPanel.$$(
218+
SELECTORS.tokenRemoveButton
219+
);
220+
for ( const $el of $$tokenRemoveButtons ) {
221+
await $el.click();
222+
}
223+
224+
const $stockStatusInput = await canvas().$(
225+
await getFormElementIdByLabel(
226+
'Stock status',
227+
SELECTORS.formTokenFieldLabel.replace( '.', '' )
228+
)
229+
);
230+
await $stockStatusInput.click();
231+
await canvas().keyboard.type( 'Out of Stock' );
232+
await canvas().keyboard.press( 'Enter' );
233+
const outOfStockCount = getFixtureProductsData(
234+
'stock_status'
235+
).filter( ( status ) => status === 'outofstock' ).length;
236+
expect( await getPreviewProducts() ).toHaveLength(
237+
outOfStockCount
238+
);
239+
} );
240+
241+
it( 'Works on the front end', async () => {
242+
const tokenRemoveButtons = await $productFiltersPanel.$$(
243+
SELECTORS.tokenRemoveButton
244+
);
245+
for ( const el of tokenRemoveButtons ) {
246+
await el.click();
247+
}
248+
const $stockStatusInput = await canvas().$(
249+
await getFormElementIdByLabel(
250+
'Stock status',
251+
SELECTORS.formTokenFieldLabel.replace( '.', '' )
252+
)
253+
);
254+
await $stockStatusInput.click();
255+
await canvas().keyboard.type( 'Out of stock' );
256+
await canvas().keyboard.press( 'Enter' );
257+
await canvas().waitForSelector(
258+
SELECTORS.editorPreview.productsGrid
259+
);
260+
await saveOrPublish();
261+
await shopper.block.goToBlockPage( block.name );
262+
const outOfStockCount = getFixtureProductsData(
263+
'stock_status'
264+
).filter( ( status ) => status === 'outofstock' ).length;
265+
expect( await getFrontEndProducts() ).toHaveLength(
266+
outOfStockCount
267+
);
268+
} );
269+
} );
270+
}
271+
);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* External dependencies
3+
*/
4+
import { canvas } from '@wordpress/e2e-test-utils';
5+
6+
export const findToolsPanelWithTitle = async ( panelTitle: string ) => {
7+
const panelToggleSelector = `//div[contains(@class, "components-tools-panel-header")]//h2[contains(@class, "components-heading") and contains(text(),"${ panelTitle }")]`;
8+
const panelSelector = `${ panelToggleSelector }/ancestor::*[contains(concat(" ", @class, " "), " components-tools-panel ")]`;
9+
return await canvas().waitForXPath( panelSelector );
10+
};

tests/utils/get-fixture-products-data.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import { Products } from '../e2e/fixtures/fixture-data';
66
/**
77
* Get products data by key from fixtures.
88
*/
9-
export const getFixtureProductsData = ( key: string ) => {
9+
export const getFixtureProductsData = ( key = '' ) => {
10+
if ( ! key ) {
11+
return Products();
12+
}
1013
return Products()
1114
.map( ( product ) => product[ key ] )
1215
.filter( Boolean );
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* External dependencies
3+
*/
4+
import { canvas } from '@wordpress/e2e-test-utils';
5+
6+
export const getFormElementIdByLabel = async (
7+
text: string,
8+
className: string
9+
) => {
10+
const labelElement = await canvas().waitForXPath(
11+
`//label[contains(text(), "${ text }") and contains(@class, "${ className }")]`,
12+
{ visible: true }
13+
);
14+
return await canvas().evaluate(
15+
( label ) => `#${ label.getAttribute( 'for' ) }`,
16+
labelElement
17+
);
18+
};

tests/utils/get-toggle-id-by-label.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1+
/**
2+
* External dependencies
3+
*/
4+
import { canvas } from '@wordpress/e2e-test-utils';
5+
16
/**
27
* Internal dependencies
38
*/
49
import { DEFAULT_TIMEOUT } from './constants';
10+
import { getFormElementIdByLabel } from './get-form-element-id-by-label';
511

612
/**
713
* Get the ID of the setting toogle so test can manipulate the toggle using
@@ -12,24 +18,20 @@ import { DEFAULT_TIMEOUT } from './constants';
1218
* check if the node still attached to the document before returning its
1319
* ID. If the node is detached, it means that the toggle is rendered, then
1420
* we retry by calling this function again with increased retry argument. We
15-
* will retry until the timeout is reached.
21+
* will retry until the default timeout is reached, which is 30s.
1622
*/
1723
export const getToggleIdByLabel = async (
1824
label: string,
1925
retry = 0
2026
): Promise< string > => {
2127
const delay = 1000;
22-
const labelElement = await page.waitForXPath(
23-
`//label[contains(text(), "${ label }") and contains(@class, "components-toggle-control__label")]`,
24-
{ visible: true }
25-
);
26-
const checkboxId = await page.evaluate(
27-
( toggleLabel ) => `#${ toggleLabel.getAttribute( 'for' ) }`,
28-
labelElement
29-
);
3028
// Wait a bit for toggle to finish rerendering.
31-
await page.waitForTimeout( delay );
32-
const checkbox = await page.$( checkboxId );
29+
await canvas().waitForTimeout( delay );
30+
const checkboxId = await getFormElementIdByLabel(
31+
label,
32+
'components-toggle-control__label'
33+
);
34+
const checkbox = await canvas().$( checkboxId );
3335
if ( ! checkbox ) {
3436
if ( retry * delay < DEFAULT_TIMEOUT ) {
3537
return await getToggleIdByLabel( label, retry + 1 );

tests/utils/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export * from './taxes';
1818
export * from './constants';
1919
export { insertInnerBlock } from './insert-inner-block';
2020
export { getFixtureProductsData } from './get-fixture-products-data';
21+
export { findToolsPanelWithTitle } from './find-tools-panel-with-title';
22+
export { getFormElementIdByLabel } from './get-form-element-id-by-label';
2123
export { getToggleIdByLabel } from './get-toggle-id-by-label';
2224
export { insertBlockUsingQuickInserter } from './insert-block-using-quick-inserter';
2325
export { insertBlockUsingSlash } from './insert-block-using-slash';

0 commit comments

Comments
 (0)