diff --git a/plugins/woocommerce-blocks/.github/workflows/playwright.yml b/plugins/woocommerce-blocks/.github/workflows/playwright.yml index 286988c42f1..b6c53956118 100644 --- a/plugins/woocommerce-blocks/.github/workflows/playwright.yml +++ b/plugins/woocommerce-blocks/.github/workflows/playwright.yml @@ -11,6 +11,7 @@ jobs: timeout-minutes: 60 runs-on: ubuntu-latest steps: + - uses: actions/checkout@v3 - name: Setup node version and npm cache @@ -58,7 +59,11 @@ jobs: - name: Run Playwright tests run: npm run test:e2e-pw - - uses: actions/upload-artifact@v3.1.2 + - uses: actions/upload-artifact@v3 + if: ${{ failure() }} + with: name: playwright-report - path: playwright-report + path: artifacts/test-results + if-no-files-found: error # 'warn' or 'ignore' are also available, defaults to `warn` + diff --git a/plugins/woocommerce-blocks/package.json b/plugins/woocommerce-blocks/package.json index 04817bc9919..14e09d6b11a 100644 --- a/plugins/woocommerce-blocks/package.json +++ b/plugins/woocommerce-blocks/package.json @@ -27,7 +27,10 @@ "./assets/js/blocks/filter-wrapper/register-components.ts", "./assets/js/blocks/product-query/variations/**.tsx", "./assets/js/blocks/product-query/index.tsx", - "./assets/js/blocks/product-query/inspector-controls.tsx" + "./assets/js/blocks/product-query/inspector-controls.tsx", + "./assets/js/blocks/product-gallery/**.tsx", + "./assets/js/blocks/product-gallery/inner-blocks/**/index.tsx", + "./assets/js/templates/revert-button/index.tsx" ], "repository": { "type": "git", diff --git a/plugins/woocommerce-blocks/tests/e2e-pw/tests/product-gallery/inner-blocks/product-gallery-thumbnails/product-gallery-thumbnails.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e-pw/tests/product-gallery/inner-blocks/product-gallery-thumbnails/product-gallery-thumbnails.block_theme.spec.ts new file mode 100644 index 00000000000..c245379f68e --- /dev/null +++ b/plugins/woocommerce-blocks/tests/e2e-pw/tests/product-gallery/inner-blocks/product-gallery-thumbnails/product-gallery-thumbnails.block_theme.spec.ts @@ -0,0 +1,375 @@ +/** + * External dependencies + */ +import { test, expect } from '@woocommerce/e2e-playwright-utils'; + +/** + * Internal dependencies + */ +import { addBlock } from './utils'; + +const blockData = { + name: 'woocommerce/product-gallery-thumbnails', + mainClass: '.wp-block-woocommerce-product-gallery-thumbnails', + selectors: { + frontend: {}, + editor: { + thumbnails: '.wp-block-woocommerce-product-gallery-thumbnails', + noThumbnailsOption: 'button[data-value=off]', + leftPositionThumbnailsOption: 'button[data-value=left]', + bottomPositionThumbnailsOption: 'button[data-value=bottom]', + rightPositionThumbnailsOption: 'button[data-value=right]', + }, + }, + slug: 'single-product', + productPage: '/product/v-neck-t-shirt/', +}; + +test.describe( `${ blockData.name }`, () => { + test.beforeEach( async ( { requestUtils, admin, editorUtils } ) => { + await requestUtils.deleteAllTemplates( 'wp_template' ); + await requestUtils.deleteAllTemplates( 'wp_template_part' ); + await admin.visitSiteEditor( { + postId: `woocommerce/woocommerce//${ blockData.slug }`, + postType: 'wp_template', + } ); + await editorUtils.enterEditMode(); + } ); + + test( 'Renders Product Gallery Thumbnails block on the editor and frontend side', async ( { + page, + editor, + editorUtils, + frontendUtils, + } ) => { + await editor.insertBlock( { + name: 'woocommerce/product-gallery', + } ); + + const block = await editorUtils.getBlockByName( blockData.name ); + + await expect( block ).toBeVisible(); + + await Promise.all( [ + editor.saveSiteEditorEntities(), + page.waitForResponse( ( response ) => + response.url().includes( 'wp-json/wp/v2/templates/' ) + ), + ] ); + + await page.goto( blockData.productPage, { + waitUntil: 'networkidle', + } ); + + const blockFrontend = await frontendUtils.getBlockByName( + 'woocommerce/product-gallery' + ); + + await expect( blockFrontend ).toBeVisible(); + } ); + + test.describe( `${ blockData.name } Settings`, () => { + test( 'Hide correctly the thumbnails', async ( { + page, + editor, + editorUtils, + admin, + } ) => { + await addBlock( admin, editor, editorUtils ); + await ( + await editorUtils.getBlockByName( blockData.name ) + ).click(); + await editor.openDocumentSettingsSidebar(); + await page + .locator( blockData.selectors.editor.noThumbnailsOption ) + .click(); + + const isVisible = await page + .locator( blockData.selectors.editor.thumbnails ) + .isVisible(); + + expect( isVisible ).toBe( false ); + + await editor.saveSiteEditorEntities(); + + await page.goto( blockData.productPage, { + waitUntil: 'networkidle', + } ); + } ); + + // We can test the left position of thumbnails by cross-checking: + // - The Gallery block has the classes "is-layout-flex" and "is-nowrap". + // - The Thumbnails block has a lower index than the Large Image block. + test( 'Position thumbnails on the left of the large image', async ( { + page, + editor, + editorUtils, + frontendUtils, + } ) => { + // Currently we are adding the block under the legacy Product Image Gallery block, but in the future we have to add replace the product gallery block with this block. + const parentBlock = await editorUtils.getBlockByName( + 'woocommerce/product-image-gallery' + ); + const clientId = + ( await parentBlock.getAttribute( 'data-block' ) ) ?? ''; + const parentClientId = + ( await editorUtils.getBlockRootClientId( clientId ) ) ?? ''; + + await editor.selectBlocks( parentBlock ); + await editorUtils.insertBlock( + { name: 'woocommerce/product-gallery' }, + undefined, + parentClientId + ); + await ( + await editorUtils.getBlockByName( blockData.name ) + ).click(); + + await editor.openDocumentSettingsSidebar(); + await page + .locator( + blockData.selectors.editor.leftPositionThumbnailsOption + ) + .click(); + + await page.waitForTimeout( 500 ); + + const groupBlock = await editorUtils.getBlockByTypeWithParent( + 'core/group', + 'woocommerce/product-gallery' + ); + + const groupBlockClassAttribute = await groupBlock.getAttribute( + 'class' + ); + expect( groupBlockClassAttribute ).toContain( 'is-layout-flex' ); + expect( groupBlockClassAttribute ).toContain( 'is-nowrap' ); + + const isThumbnailsBlockEarlier = + await editorUtils.isBlockEarlierThan( + groupBlock, + 'woocommerce/product-gallery-thumbnails', + 'woocommerce/product-gallery-large-image' + ); + + expect( isThumbnailsBlockEarlier ).toBe( true ); + + await Promise.all( [ + editor.saveSiteEditorEntities(), + page.waitForResponse( ( response ) => + response.url().includes( 'wp-json/wp/v2/templates/' ) + ), + ] ); + + await page.goto( blockData.productPage, { + waitUntil: 'networkidle', + } ); + + const groupBlockFrontend = + await frontendUtils.getBlockByClassWithParent( + 'wp-block-group', + 'woocommerce/product-gallery' + ); + + const groupBlockFrontendClassAttribute = + await groupBlockFrontend.getAttribute( 'class' ); + expect( groupBlockFrontendClassAttribute ).toContain( + 'is-layout-flex' + ); + expect( groupBlockFrontendClassAttribute ).toContain( 'is-nowrap' ); + + const isThumbnailsFrontendBlockEarlier = + await frontendUtils.isBlockEarlierThan( + groupBlockFrontend, + 'woocommerce/product-gallery-thumbnails', + 'woocommerce/product-gallery-large-image' + ); + + expect( isThumbnailsFrontendBlockEarlier ).toBe( true ); + } ); + + // We can test the bottom position of thumbnails by cross-checking: + // - The Gallery block has the classes "is-layout-flex" and "is-vertical". + // - The Thumbnails block has a higher index than the Large Image block. + test( 'Position thumbnails on the bottom of the large image', async ( { + page, + editor, + editorUtils, + frontendUtils, + } ) => { + // Currently we are adding the block under the legacy Product Image Gallery block, but in the future we have to add replace the product gallery block with this block. + const parentBlock = await editorUtils.getBlockByName( + 'woocommerce/product-image-gallery' + ); + const clientId = + ( await parentBlock.getAttribute( 'data-block' ) ) ?? ''; + const parentClientId = + ( await editorUtils.getBlockRootClientId( clientId ) ) ?? ''; + + await editor.selectBlocks( parentBlock ); + await editorUtils.insertBlock( + { name: 'woocommerce/product-gallery' }, + undefined, + parentClientId + ); + await ( + await editorUtils.getBlockByName( blockData.name ) + ).click(); + + await editor.openDocumentSettingsSidebar(); + await page + .locator( + blockData.selectors.editor.bottomPositionThumbnailsOption + ) + .click(); + + await page.waitForTimeout( 500 ); + + const groupBlock = await editorUtils.getBlockByTypeWithParent( + 'core/group', + 'woocommerce/product-gallery' + ); + + const groupBlockClassAttribute = await groupBlock.getAttribute( + 'class' + ); + expect( groupBlockClassAttribute ).toContain( 'is-layout-flex' ); + expect( groupBlockClassAttribute ).toContain( 'is-vertical' ); + + const isThumbnailsBlockEarlier = + await editorUtils.isBlockEarlierThan( + groupBlock, + 'woocommerce/product-gallery-thumbnails', + 'woocommerce/product-gallery-large-image' + ); + + expect( isThumbnailsBlockEarlier ).toBe( false ); + + await Promise.all( [ + editor.saveSiteEditorEntities(), + page.waitForResponse( ( response ) => + response.url().includes( 'wp-json/wp/v2/templates/' ) + ), + ] ); + + await page.goto( blockData.productPage, { + waitUntil: 'networkidle', + } ); + + const groupBlockFrontend = + await frontendUtils.getBlockByClassWithParent( + 'wp-block-group', + 'woocommerce/product-gallery' + ); + + const groupBlockFrontendClassAttribute = + await groupBlockFrontend.getAttribute( 'class' ); + expect( groupBlockFrontendClassAttribute ).toContain( + 'is-layout-flex' + ); + expect( groupBlockFrontendClassAttribute ).toContain( + 'is-vertical' + ); + + const isThumbnailsFrontendBlockEarlier = + await frontendUtils.isBlockEarlierThan( + groupBlockFrontend, + 'woocommerce/product-gallery-thumbnails', + 'woocommerce/product-gallery-large-image' + ); + + expect( isThumbnailsFrontendBlockEarlier ).toBe( false ); + } ); + + // We can test the right position of thumbnails by cross-checking: + // - The Gallery block has the classes "is-layout-flex" and "is-nowrap". + // - The Thumbnails block has a higher index than the Large Image block. + test( 'Position thumbnails on the right of the large image', async ( { + page, + editor, + editorUtils, + frontendUtils, + } ) => { + // Currently we are adding the block under the legacy Product Image Gallery block, but in the future we have to add replace the product gallery block with this block. + const parentBlock = await editorUtils.getBlockByName( + 'woocommerce/product-image-gallery' + ); + const clientId = + ( await parentBlock.getAttribute( 'data-block' ) ) ?? ''; + const parentClientId = + ( await editorUtils.getBlockRootClientId( clientId ) ) ?? ''; + + await editor.selectBlocks( parentBlock ); + await editorUtils.insertBlock( + { name: 'woocommerce/product-gallery' }, + undefined, + parentClientId + ); + await ( + await editorUtils.getBlockByName( blockData.name ) + ).click(); + + await editor.openDocumentSettingsSidebar(); + await page + .locator( + blockData.selectors.editor.rightPositionThumbnailsOption + ) + .click(); + + await page.waitForTimeout( 500 ); + + const groupBlock = await editorUtils.getBlockByTypeWithParent( + 'core/group', + 'woocommerce/product-gallery' + ); + + const groupBlockClassAttribute = await groupBlock.getAttribute( + 'class' + ); + expect( groupBlockClassAttribute ).toContain( 'is-layout-flex' ); + expect( groupBlockClassAttribute ).toContain( 'is-nowrap' ); + + const isThumbnailsBlockEarlier = + await editorUtils.isBlockEarlierThan( + groupBlock, + 'woocommerce/product-gallery-thumbnails', + 'woocommerce/product-gallery-large-image' + ); + + expect( isThumbnailsBlockEarlier ).toBe( false ); + + await Promise.all( [ + editor.saveSiteEditorEntities(), + page.waitForResponse( ( response ) => + response.url().includes( 'wp-json/wp/v2/templates/' ) + ), + ] ); + + await page.goto( blockData.productPage, { + waitUntil: 'networkidle', + } ); + + const groupBlockFrontend = + await frontendUtils.getBlockByClassWithParent( + 'wp-block-group', + 'woocommerce/product-gallery' + ); + + const groupBlockFrontendClassAttribute = + await groupBlockFrontend.getAttribute( 'class' ); + expect( groupBlockFrontendClassAttribute ).toContain( + 'is-layout-flex' + ); + expect( groupBlockFrontendClassAttribute ).toContain( 'is-nowrap' ); + + const isThumbnailsFrontendBlockEarlier = + await frontendUtils.isBlockEarlierThan( + groupBlockFrontend, + 'woocommerce/product-gallery-thumbnails', + 'woocommerce/product-gallery-large-image' + ); + + expect( isThumbnailsFrontendBlockEarlier ).toBe( false ); + } ); + } ); +} ); diff --git a/plugins/woocommerce-blocks/tests/e2e-pw/tests/product-gallery/inner-blocks/product-gallery-thumbnails/utils.ts b/plugins/woocommerce-blocks/tests/e2e-pw/tests/product-gallery/inner-blocks/product-gallery-thumbnails/utils.ts new file mode 100644 index 00000000000..b36ae591a46 --- /dev/null +++ b/plugins/woocommerce-blocks/tests/e2e-pw/tests/product-gallery/inner-blocks/product-gallery-thumbnails/utils.ts @@ -0,0 +1,26 @@ +/** + * External dependencies + */ +import { EditorUtils } from '@woocommerce/e2e-utils'; +import { Admin, Editor } from '@wordpress/e2e-test-utils-playwright'; + +// Define a utility function to add the "woocommerce/product-gallery" block to the editor +export const addBlock = async ( + admin: Admin, + editor: Editor, + editorUtils: EditorUtils +) => { + // Visit the site editor for the specific product page + await admin.visitSiteEditor( { + postId: `woocommerce/woocommerce//single-product`, + postType: 'wp_template', + } ); + + // Enter the edit mode + await editorUtils.enterEditMode(); + + // Insert the "woocommerce/product-gallery" block + await editor.insertBlock( { + name: 'woocommerce/product-gallery', + } ); +}; diff --git a/plugins/woocommerce-blocks/tests/e2e-pw/utils/editor/EditorUtils.ts b/plugins/woocommerce-blocks/tests/e2e-pw/utils/editor/EditorUtils.ts index f0913488942..ebb96b6e20a 100644 --- a/plugins/woocommerce-blocks/tests/e2e-pw/utils/editor/EditorUtils.ts +++ b/plugins/woocommerce-blocks/tests/e2e-pw/utils/editor/EditorUtils.ts @@ -17,6 +17,15 @@ export class EditorUtils { return this.editor.canvas.locator( `[data-type="${ name }"]` ); } + async getBlockByTypeWithParent( name: string, parentName: string ) { + const parentBlock = await this.getBlockByName( parentName ); + if ( ! parentBlock ) { + throw new Error( `Parent block "${ parentName }" not found.` ); + } + const block = await parentBlock.locator( `[data-type="${ name }"]` ); + return block; + } + // todo: Make a PR to @wordpress/e2e-test-utils-playwright to add this method. /** * Inserts a block after a given client ID. @@ -71,4 +80,48 @@ export class EditorUtils { ); await this.editor.canvas.click( 'body' ); } + + async isBlockEarlierThan< T >( + containerBlock: T, + firstBlock: string, + secondBlock: string + ) { + const container = + containerBlock instanceof Function + ? await containerBlock() + : containerBlock; + + if ( ! container ) { + throw new Error( 'Container block not found.' ); + } + + const childBlocks = container.locator( ':scope > .wp-block' ); + + let firstBlockIndex = -1; + let secondBlockIndex = -1; + + for ( let i = 0; i < ( await childBlocks.count() ); i++ ) { + const blockName = await childBlocks + .nth( i ) + .getAttribute( 'data-type' ); + + if ( blockName === firstBlock ) { + firstBlockIndex = i; + } + + if ( blockName === secondBlock ) { + secondBlockIndex = i; + } + + if ( firstBlockIndex !== -1 && secondBlockIndex !== -1 ) { + break; + } + } + + if ( firstBlockIndex === -1 || secondBlockIndex === -1 ) { + throw new Error( 'Both blocks must exist within the editor' ); + } + + return firstBlockIndex < secondBlockIndex; + } } diff --git a/plugins/woocommerce-blocks/tests/e2e-pw/utils/frontend/FrontendUtils.ts b/plugins/woocommerce-blocks/tests/e2e-pw/utils/frontend/FrontendUtils.ts index 2ff70be2b5c..9292678e695 100644 --- a/plugins/woocommerce-blocks/tests/e2e-pw/utils/frontend/FrontendUtils.ts +++ b/plugins/woocommerce-blocks/tests/e2e-pw/utils/frontend/FrontendUtils.ts @@ -18,6 +18,15 @@ export class FrontendUtils { return this.page.locator( `[data-block-name="${ name }"]` ); } + async getBlockByClassWithParent( blockClass: string, parentName: string ) { + const parentBlock = await this.getBlockByName( parentName ); + if ( ! parentBlock ) { + throw new Error( `Parent block "${ parentName }" not found.` ); + } + const block = await parentBlock.locator( `.${ blockClass }` ); + return block; + } + async addToCart() { await this.page.click( 'text=Add to cart' ); await this.page.waitForLoadState( 'networkidle' ); @@ -28,4 +37,48 @@ export class FrontendUtils { waitUntil: 'networkidle', } ); } + + async isBlockEarlierThan< T >( + containerBlock: T, + firstBlock: string, + secondBlock: string + ) { + const container = + containerBlock instanceof Function + ? await containerBlock() + : containerBlock; + + if ( ! container ) { + throw new Error( 'Container block not found.' ); + } + + const childBlocks = container.locator( '[data-block-name]' ); + + let firstBlockIndex = -1; + let secondBlockIndex = -1; + + for ( let i = 0; i < ( await childBlocks.count() ); i++ ) { + const blockName = await childBlocks + .nth( i ) + .getAttribute( 'data-block-name' ); + + if ( blockName === firstBlock ) { + firstBlockIndex = i; + } + + if ( blockName === secondBlock ) { + secondBlockIndex = i; + } + + if ( firstBlockIndex !== -1 && secondBlockIndex !== -1 ) { + break; + } + } + + if ( firstBlockIndex === -1 || secondBlockIndex === -1 ) { + throw new Error( 'Both blocks must exist within the editor' ); + } + + return firstBlockIndex < secondBlockIndex; + } }