|
| 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 | +); |
0 commit comments