Product Gallery Block: Add Product Gallery template to allow users to edit full mode view (https://github.com/woocommerce/woocommerce-blocks/pull/10823)

* Product Gallery: add support for On Sale Badge Block

* add align support

* Add E2E tests

* set margin via Block Styles

* disable experimental flag

* add next previous block

* restore support file

* fix TS error

* fix layout

* change product

* change product

* Product Gallert Block: Add zoom on hover

* set to true by default

* remove block is already registered error

* remove unecessary await

* Improve zoom logic

Co-authored-by: Alexandre Lara <allexandrelara@gmail.com>

* Product Gallery Full view mode: Add the logic to render the dedicated template

* use template-part instead template

* add E2E tests

* update selectors

* add feature flag product gallery template part

* fix E2E tests

* remove not necessary file

---------

Co-authored-by: Alexandre Lara <allexandrelara@gmail.com>
This commit is contained in:
Luigi Teschio 2023-09-15 10:54:49 +02:00 committed by GitHub
parent 0abe53d079
commit 11062e8600
20 changed files with 592 additions and 202 deletions

View File

@ -49,11 +49,15 @@
},
"fullScreenOnClick": {
"type": "boolean",
"default": false
"default": true
},
"nextPreviousButtonsPosition":{
"type": "string",
"default": "insideTheImage"
},
"mode": {
"type": "string",
"default": "standard"
}
},
"viewScript": "wc-product-gallery-frontend"

View File

@ -9,6 +9,7 @@ import {
} from '@wordpress/block-editor';
import { BlockEditProps, InnerBlockTemplate } from '@wordpress/blocks';
import { useEffect } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
/**
* Internal dependencies
@ -86,6 +87,21 @@ const TEMPLATE: InnerBlockTemplate[] = [
],
];
const setMode = (
currentTemplateId: string,
templateType: string,
setAttributes: ( attrs: Partial< ProductGalleryAttributes > ) => void
) => {
if (
templateType === 'wp_template_part' &&
currentTemplateId.includes( 'product-gallery' )
) {
setAttributes( {
mode: 'full',
} );
}
};
export const Edit = ( {
clientId,
attributes,
@ -93,6 +109,18 @@ export const Edit = ( {
}: BlockEditProps< ProductGalleryAttributes > ) => {
const blockProps = useBlockProps();
const { currentTemplateId, templateType } = useSelect(
( select ) => ( {
currentTemplateId: select( 'core/edit-site' ).getEditedPostId(),
templateType: select( 'core/edit-site' ).getEditedPostType(),
} ),
[]
);
useEffect( () => {
setMode( currentTemplateId, templateType, setAttributes );
}, [ currentTemplateId, setAttributes, templateType ] );
useEffect( () => {
setAttributes( {
...attributes,

View File

@ -11,18 +11,22 @@ interface Context {
woocommerce: {
selectedImage: string;
imageId: string;
isDialogOpen: boolean;
};
}
interface Selectors {
woocommerce: {
isSelected: ( store: unknown ) => boolean;
isDialogOpen: ( store: unknown ) => boolean;
};
}
interface Actions {
woocommerce: {
handleClick: ( context: Context ) => void;
thumbnails: {
handleClick: ( context: Context ) => void;
};
};
}
@ -44,12 +48,18 @@ interactivityApiStore( {
context?.woocommerce.imageId
);
},
isDialogOpen: ( { context }: Store ) => {
return context?.woocommerce.isDialogOpen;
},
},
},
actions: {
woocommerce: {
handleClick: ( { context }: Store ) => {
context.woocommerce.selectedImage = context.woocommerce.imageId;
thumbnails: {
handleClick: ( { context }: Store ) => {
context.woocommerce.selectedImage =
context.woocommerce.imageId;
},
},
},
},

View File

@ -11,6 +11,7 @@ import { Edit } from './edit';
import { Save } from './save';
import metadata from './block.json';
import icon from './icon';
import './style.scss';
import './inner-blocks/product-gallery-large-image-next-previous';
import './inner-blocks/product-gallery-pager';
import './inner-blocks/product-gallery-thumbnails';

View File

@ -11,6 +11,7 @@ type Context = {
transform: string;
transition: string;
};
isDialogOpen: boolean;
};
};
@ -64,6 +65,9 @@ interactivityStore(
context.woocommerce.styles.transform = `scale(1.0)`;
context.woocommerce.styles[ 'transform-origin' ] = '';
},
handleClick: ( { context }: { context: Context } ) => {
context.woocommerce.isDialogOpen = true;
},
},
},
}

View File

@ -0,0 +1,10 @@
.wp-block-woocommerce-product-gallery {
dialog {
position: fixed;
width: 90vw;
height: 90vh;
top: 0;
margin: $gap-largest;
z-index: 9999;
}
}

View File

@ -9,6 +9,7 @@ export interface ProductGalleryBlockAttributes {
cropImages?: boolean;
hoverZoom?: boolean;
fullScreenOnClick?: boolean;
mode: 'standard' | 'full';
}
export interface ProductGalleryThumbnailsBlockAttributes {

View File

@ -34,7 +34,7 @@ The majority of our feature flagging is blocks, this is a list of them:
### Experimental flag
- Product Gallery ([PHP flag](https://github.com/woocommerce/woocommerce-blocks/blob/e3fe996251b270d45ecc73207ea4ad587c2dbc78/src/BlockTypesController.php#L232) | [webpack flag](https://github.com/woocommerce/woocommerce-blocks/blob/e3fe996251b270d45ecc73207ea4ad587c2dbc78/bin/webpack-entries.js#L50-L52C3)).
- Product Gallery ([PHP flag](https://github.com/woocommerce/woocommerce-blocks/blob/e3fe996251b270d45ecc73207ea4ad587c2dbc78/src/BlockTypesController.php#L232) | [webpack flag](https://github.com/woocommerce/woocommerce-blocks/blob/e3fe996251b270d45ecc73207ea4ad587c2dbc78/bin/webpack-entries.js#L50-L52C3) | [BlockTemplatesController](https://github.com/woocommerce/woocommerce-blocks/blob/211960f753d093f2f819273e130b34f893a784cd/src/BlockTemplatesController.php/#L467-L469)).
- Product Gallery Thumbnails ([PHP flag](https://github.com/woocommerce/woocommerce-blocks/blob/04af396b9aec5a915ad98188eded53e723a051d3/src/BlockTypesController.php#L234) | [webpack flag](https://github.com/woocommerce/woocommerce-blocks/blob/04af396b9aec5a915ad98188eded53e723a051d3/bin/webpack-entries.js#L57-L60)).
- Product Average Rating ([PHP flag](https://github.com/woocommerce/woocommerce-blocks/blob/1111e2fb9d6f5074df96a444b99e2fc00e4eb8d1/src/BlockTypesController.php#L229) | [webpack flag](https://github.com/woocommerce/woocommerce-blocks/blob/1111e2fb9d6f5074df96a444b99e2fc00e4eb8d1/bin/webpack-entries.js#L68-L70))
- Product Rating Stars ([PHP flag](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/src/BlockTypesController.php#L230) | [webpack flag](https://github.com/woocommerce/woocommerce-blocks/blob/trunk/bin/webpack-entries.js#L68-L70))

View File

@ -21,27 +21,6 @@ use \WP_Post;
*/
class BlockTemplatesController {
/**
* Holds the Package instance
*
* @var Package
*/
private $package;
/**
* Holds the path for the directory where the block templates will be kept.
*
* @var string
*/
private $templates_directory;
/**
* Holds the path for the directory where the block template parts will be kept.
*
* @var string
*/
private $template_parts_directory;
/**
* Directory which contains all templates
*
@ -49,6 +28,13 @@ class BlockTemplatesController {
*/
const TEMPLATES_ROOT_DIR = 'templates';
/**
* Package instance.
*
* @var Package
*/
private $package;
/**
* Constructor.
*
@ -59,9 +45,6 @@ class BlockTemplatesController {
// This feature is gated for WooCommerce versions 6.0.0 and above.
if ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '6.0.0', '>=' ) ) {
$root_path = plugin_dir_path( __DIR__ ) . self::TEMPLATES_ROOT_DIR . DIRECTORY_SEPARATOR;
$this->templates_directory = $root_path . BlockTemplateUtils::DIRECTORY_NAMES['TEMPLATES'];
$this->template_parts_directory = $root_path . BlockTemplateUtils::DIRECTORY_NAMES['TEMPLATE_PARTS'];
$this->init();
}
}
@ -321,7 +304,7 @@ class BlockTemplatesController {
return $template;
}
$directory = $this->get_templates_directory( $template_type );
$directory = BlockTemplateUtils::get_templates_directory( $template_type );
$template_file_path = $directory . '/' . $template_slug . '.html';
$template_object = BlockTemplateUtils::create_new_block_template_object( $template_file_path, $template_type, $template_slug );
$template_built = BlockTemplateUtils::build_template_result_from_file( $template_object, $template_type );
@ -476,11 +459,14 @@ class BlockTemplatesController {
* @return array Templates from the WooCommerce blocks plugin directory.
*/
public function get_block_templates_from_woocommerce( $slugs, $already_found_templates, $template_type = 'wp_template' ) {
$directory = $this->get_templates_directory( $template_type );
$directory = BlockTemplateUtils::get_templates_directory( $template_type );
$template_files = BlockTemplateUtils::get_template_paths( $directory );
$templates = array();
foreach ( $template_files as $template_file ) {
if ( ! $this->package->is_experimental_build() && str_contains( $template_file, 'templates/parts/product-gallery.html' ) ) {
break;
}
// Skip the template if it's blockified, and we should only use classic ones.
if ( ! BlockTemplateUtils::should_use_blockified_product_grid_templates() && strpos( $template_file, 'blockified' ) !== false ) {
continue;
@ -559,25 +545,6 @@ class BlockTemplatesController {
return BlockTemplateUtils::filter_block_templates_by_feature_flag( $templates );
}
/**
* Gets the directory where templates of a specific template type can be found.
*
* @param string $template_type wp_template or wp_template_part.
*
* @return string
*/
protected function get_templates_directory( $template_type = 'wp_template' ) {
if ( 'wp_template_part' === $template_type ) {
return $this->template_parts_directory;
}
if ( BlockTemplateUtils::should_use_blockified_product_grid_templates() ) {
return $this->templates_directory . '/blockified';
}
return $this->templates_directory;
}
/**
* Returns the path of a template on the Blocks template folder.
*
@ -587,7 +554,7 @@ class BlockTemplatesController {
* @return string
*/
public function get_template_path_from_woocommerce( $template_slug, $template_type = 'wp_template' ) {
return $this->get_templates_directory( $template_type ) . '/' . $template_slug . '.html';
return BlockTemplateUtils::get_templates_directory( $template_type ) . '/' . $template_slug . '.html';
}
/**
@ -602,7 +569,7 @@ class BlockTemplatesController {
if ( ! $template_name ) {
return false;
}
$directory = $this->get_templates_directory( $template_type ) . '/' . $template_name . '.html';
$directory = BlockTemplateUtils::get_templates_directory( $template_type ) . '/' . $template_name . '.html';
return is_readable(
$directory

View File

@ -1,7 +1,7 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\ProductGalleryUtils;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
/**
* ProductGallery class.
@ -14,6 +14,45 @@ class ProductGallery extends AbstractBlock {
*/
protected $block_name = 'product-gallery';
/**
* Return the dialog content.
*
* @return string
*/
protected function render_dialog() {
$template_part = BlockTemplateUtils::get_template_part( 'product-gallery' );
$parsed_template = parse_blocks(
$template_part
);
$html = array_reduce(
$parsed_template,
function( $carry, $item ) {
return $carry . render_block( $item );
},
''
);
$gallery_dialog = '<dialog data-wc-bind--open="selectors.woocommerce.isDialogOpen">' . $html . '</dialog>';
return $gallery_dialog;
}
/**
* This function remove the div wrapper.
* The content has a <div> with the class wp-block-woocommerce-product-gallery>.
* We don't need since that we add it in the render method.
*
* @param string $content Block content.
* @return string Rendered block type output.
*/
private function remove_div_wrapper( $content ) {
$parsed_string = preg_replace( '/<div class="wp-block-woocommerce-product-gallery">/', '', $content );
$parsed_string = preg_replace( '/<\/div>$/', '', $parsed_string );
return $parsed_string;
}
/**
* Include and render the block.
*
@ -27,15 +66,18 @@ class ProductGallery extends AbstractBlock {
global $product;
$classname = $attributes['className'] ?? '';
$wrapper_attributes = get_block_wrapper_attributes( array( 'class' => trim( sprintf( 'woocommerce %1$s', $classname ) ) ) );
$gallery = ( true === $attributes['fullScreenOnClick'] && isset( $attributes['mode'] ) && 'full' !== $attributes['mode'] ) ? $this->render_dialog() : '';
$html = sprintf(
'<div data-wc-interactive %1$s>
'<div %1$s>
%2$s
%3$s
</div>',
$wrapper_attributes,
$content
$this->remove_div_wrapper( $content ),
$gallery
);
$p = new \WP_HTML_Tag_Processor( $content );
$p = new \WP_HTML_Tag_Processor( $html );
if ( $p->next_tag() ) {
$p->set_attribute( 'data-wc-interactive', true );
@ -45,6 +87,7 @@ class ProductGallery extends AbstractBlock {
array(
'woocommerce' => array(
'selectedImage' => $product->get_image_id(),
'isDialogOpen' => false,
),
)
)

View File

@ -159,6 +159,7 @@ class ProductGalleryLargeImage extends AbstractBlock {
return array(
'data-wc-on--mousemove' => 'actions.woocommerce.handleMouseMove',
'data-wc-on--mouseleave' => 'actions.woocommerce.handleMouseLeave',
'data-wc-on--click' => 'actions.woocommerce.handleClick',
'data-wc-context' => wp_json_encode( $context, JSON_NUMERIC_CHECK ),
);
}

View File

@ -73,7 +73,7 @@ class ProductGalleryThumbnails extends AbstractBlock {
if ( $processor->next_tag() ) {
$processor->set_attribute(
'data-wc-on--click',
'actions.woocommerce.handleClick'
'actions.woocommerce.thumbnails.handleClick'
);
$html .= $processor->get_updated_html();

View File

@ -38,6 +38,8 @@ class BlockTemplateUtils {
'TEMPLATE_PARTS' => 'parts',
);
const TEMPLATES_ROOT_DIR = 'templates';
/**
* WooCommerce plugin slug
*
@ -274,6 +276,29 @@ class BlockTemplateUtils {
return $path_list;
}
/**
* Gets the directory where templates of a specific template type can be found.
*
* @param string $template_type wp_template or wp_template_part.
*
* @return string
*/
public static function get_templates_directory( $template_type = 'wp_template' ) {
$root_path = dirname( __DIR__, 2 ) . '/' . self::TEMPLATES_ROOT_DIR . DIRECTORY_SEPARATOR;
$templates_directory = $root_path . self::DIRECTORY_NAMES['TEMPLATES'];
$template_parts_directory = $root_path . self::DIRECTORY_NAMES['TEMPLATE_PARTS'];
if ( 'wp_template_part' === $template_type ) {
return $template_parts_directory;
}
if ( self::should_use_blockified_product_grid_templates() ) {
return $templates_directory . '/blockified';
}
return $templates_directory;
}
/**
* Returns template titles.
*
@ -735,4 +760,28 @@ class BlockTemplateUtils {
$saved_woo_templates
);
}
/**
* Gets the template part by slug
*
* @param string $slug The template part slug.
*
* @return string The template part content.
*/
public static function get_template_part( $slug ) {
$templates_from_db = self::get_block_templates_from_db( array( $slug ), 'wp_template_part' );
if ( count( $templates_from_db ) > 0 ) {
$template_slug_to_load = $templates_from_db[0]->theme;
} else {
$theme_has_template = self::theme_has_template_part( $slug );
$template_slug_to_load = $theme_has_template ? get_stylesheet() : self::PLUGIN_SLUG;
}
$template_part = self::get_block_template( $template_slug_to_load . '//' . $slug, 'wp_template_part' );
if ( $template_part && ! empty( $template_part->content ) ) {
return $template_part->content;
}
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
return file_get_contents( self::get_templates_directory( 'wp_template_part' ) . DIRECTORY_SEPARATOR . $slug . '.html' );
}
}

View File

@ -0,0 +1,20 @@
<!-- wp:woocommerce/product-gallery {"mode":"full"} -->
<div class="wp-block-woocommerce-product-gallery"><!-- wp:group {"layout":{"type":"flex","flexWrap":"nowrap"}} -->
<div class="wp-block-group"><!-- wp:woocommerce/product-gallery-thumbnails {"lock":{"move":true,"remove":true}} /-->
<!-- wp:woocommerce/product-gallery-large-image {"lock":{"move":true,"remove":true}} -->
<div
class="wp-block-woocommerce-product-gallery-large-image wc-block-product-gallery-large-image__inner-blocks">
<!-- wp:woocommerce/product-sale-badge {"isDescendentOfSingleProductTemplate":true,"align":"right","style":{"spacing":{"margin":{"top":"4px","right":"4px","bottom":"4px","left":"4px"}}}} /-->
<!-- wp:woocommerce/product-gallery-large-image-next-previous {"layout":{"type":"flex","verticalAlignment":"bottom"}} -->
<div class="wp-block-woocommerce-product-gallery-large-image-next-previous"></div>
<!-- /wp:woocommerce/product-gallery-large-image-next-previous -->
</div>
<!-- /wp:woocommerce/product-gallery-large-image -->
</div>
<!-- /wp:group -->
<!-- wp:woocommerce/product-gallery-pager {"lock":{"move":true,"remove":true}} /-->
</div>
<!-- /wp:woocommerce/product-gallery -->

View File

@ -1,12 +1,13 @@
/**
* External dependencies
*/
import { test, expect } from '@woocommerce/e2e-playwright-utils';
import { test as base, expect } from '@woocommerce/e2e-playwright-utils';
import { EditorUtils, FrontendUtils } from '@woocommerce/e2e-utils';
/**
* Internal dependencies
*/
import { ProductGalleryPage } from '../product-gallery/product-gallery.page';
const blockData = {
name: 'woocommerce/product-sale-badge',
@ -30,6 +31,18 @@ const blockData = {
productPageNotOnSale: '/product/album/',
};
const test = base.extend< { pageObject: ProductGalleryPage } >( {
pageObject: async ( { page, editor, frontendUtils, editorUtils }, use ) => {
const pageObject = new ProductGalleryPage( {
page,
editor,
frontendUtils,
editorUtils,
} );
await use( pageObject );
},
} );
const getBoundingClientRect = async ( {
frontendUtils,
editorUtils,
@ -92,10 +105,15 @@ test.describe( `${ blockData.name }`, () => {
frontendUtils,
editor,
page,
pageObject,
} ) => {
await editor.openDocumentSettingsSidebar();
await editor.insertBlock( {
name: 'woocommerce/product-gallery',
} );
await pageObject.toggleFullScreenOnClickSetting( false );
await Promise.all( [
editor.saveSiteEditorEntities(),
page.waitForResponse( ( response ) =>
@ -116,10 +134,15 @@ test.describe( `${ blockData.name }`, () => {
frontendUtils,
editor,
page,
pageObject,
} ) => {
await editor.openDocumentSettingsSidebar();
await editor.insertBlock( {
name: 'woocommerce/product-gallery',
} );
await pageObject.toggleFullScreenOnClickSetting( false );
await Promise.all( [
editor.saveSiteEditorEntities(),
page.waitForResponse( ( response ) =>
@ -141,11 +164,15 @@ test.describe( `${ blockData.name }`, () => {
editorUtils,
editor,
page,
pageObject,
} ) => {
await editor.openDocumentSettingsSidebar();
await editor.insertBlock( {
name: 'woocommerce/product-gallery',
} );
await pageObject.toggleFullScreenOnClickSetting( false );
const block = await editorUtils.getBlockByName( blockData.name );
await block.click();
@ -189,11 +216,15 @@ test.describe( `${ blockData.name }`, () => {
editorUtils,
editor,
page,
pageObject,
} ) => {
await editor.openDocumentSettingsSidebar();
await editor.insertBlock( {
name: 'woocommerce/product-gallery',
} );
await pageObject.toggleFullScreenOnClickSetting( false );
const block = await editorUtils.getBlockByName( blockData.name );
await block.click();
@ -241,10 +272,13 @@ test.describe( `${ blockData.name }`, () => {
editorUtils,
editor,
page,
pageObject,
} ) => {
await editor.openDocumentSettingsSidebar();
await editor.insertBlock( {
name: 'woocommerce/product-gallery',
} );
await pageObject.toggleFullScreenOnClickSetting( false );
const editorBoundingClientRect = await getBoundingClientRect( {
frontendUtils,

View File

@ -1,13 +1,13 @@
/**
* External dependencies
*/
import { test, expect } from '@woocommerce/e2e-playwright-utils';
import { EditorUtils, FrontendUtils } from '@woocommerce/e2e-utils';
import { test as base, expect } from '@woocommerce/e2e-playwright-utils';
/**
* Internal dependencies
*/
import { addBlock } from './utils';
import { ProductGalleryPage } from '../../product-gallery.page';
const blockData = {
name: 'woocommerce/product-gallery-large-image-next-previous',
@ -40,34 +40,52 @@ const blockData = {
};
const getBoundingClientRect = async ( {
frontendUtils,
editorUtils,
leftArrowSelector,
rightArrowSelector,
isFrontend,
pageObject,
}: {
frontendUtils: FrontendUtils;
editorUtils: EditorUtils;
pageObject: ProductGalleryPage;
leftArrowSelector: string;
rightArrowSelector: string;
isFrontend: boolean;
} ) => {
const page = isFrontend ? frontendUtils.page : editorUtils.editor.canvas;
const page = isFrontend ? 'frontend' : 'editor';
return {
leftArrow: await page
leftArrow: await (
await pageObject.getNextPreviousButtonsBlock( {
page,
} )
)
.locator( leftArrowSelector )
.evaluate( ( el ) => el.getBoundingClientRect() ),
rightArrow: await page
rightArrow: await (
await pageObject.getNextPreviousButtonsBlock( {
page,
} )
)
.locator( rightArrowSelector )
.evaluate( ( el ) => el.getBoundingClientRect() ),
gallery: await (
await ( isFrontend ? frontendUtils : editorUtils ).getBlockByName(
'woocommerce/product-gallery-large-image'
)
await pageObject.getMainImageBlock( {
page,
} )
).evaluate( ( el ) => el.getBoundingClientRect() ),
};
};
const test = base.extend< { pageObject: ProductGalleryPage } >( {
pageObject: async ( { page, editor, frontendUtils, editorUtils }, use ) => {
const pageObject = new ProductGalleryPage( {
page,
editor,
frontendUtils,
editorUtils,
} );
await use( pageObject );
},
} );
test.describe( `${ blockData.name }`, () => {
test.beforeEach( async ( { requestUtils, admin, editorUtils } ) => {
await requestUtils.deleteAllTemplates( 'wp_template' );
@ -85,14 +103,16 @@ test.describe( `${ blockData.name }`, () => {
} );
test( 'Renders Next/Previous Button block on the editor side', async ( {
editorUtils,
editor,
pageObject,
} ) => {
await editor.insertBlock( {
name: 'woocommerce/product-gallery',
} );
const block = await editorUtils.getBlockByName( blockData.name );
const block = await pageObject.getNextPreviousButtonsBlock( {
page: 'editor',
} );
await expect( block ).toBeVisible();
} );
@ -100,9 +120,9 @@ test.describe( `${ blockData.name }`, () => {
test( 'Renders Next/Previous Button block on the frontend side', async ( {
admin,
editorUtils,
frontendUtils,
editor,
page,
pageObject,
} ) => {
await addBlock( admin, editor, editorUtils );
@ -117,7 +137,9 @@ test.describe( `${ blockData.name }`, () => {
waitUntil: 'commit',
} );
const block = await frontendUtils.getBlockByName( blockData.name );
const block = await pageObject.getNextPreviousButtonsBlock( {
page: 'frontend',
} );
await expect( block ).toBeVisible();
} );
@ -127,13 +149,17 @@ test.describe( `${ blockData.name }`, () => {
page,
editor,
editorUtils,
pageObject,
admin,
} ) => {
await addBlock( admin, editor, editorUtils );
await (
await editorUtils.getBlockByName( blockData.name )
await pageObject.getNextPreviousButtonsBlock( {
page: 'editor',
} )
).click();
await editor.openDocumentSettingsSidebar();
await page
.locator( blockData.selectors.editor.noArrowsOption )
.click();
@ -168,7 +194,7 @@ test.describe( `${ blockData.name }`, () => {
page,
editor,
editorUtils,
frontendUtils,
pageObject,
} ) => {
// Currently we are adding the block under the related products block, but in the future we have to add replace the product gallery block with this block.
const parentBlock = await editorUtils.getBlockByName(
@ -188,7 +214,9 @@ test.describe( `${ blockData.name }`, () => {
parentClientId
);
await (
await editorUtils.getBlockByName( blockData.name )
await pageObject.getNextPreviousButtonsBlock( {
page: 'editor',
} )
).click();
await editor.openDocumentSettingsSidebar();
@ -197,8 +225,7 @@ test.describe( `${ blockData.name }`, () => {
.click();
const editorBoundingClientRect = await getBoundingClientRect( {
frontendUtils,
editorUtils,
pageObject,
leftArrowSelector:
blockData.selectors.editor.leftArrow.outsideTheImage,
rightArrowSelector:
@ -226,8 +253,7 @@ test.describe( `${ blockData.name }`, () => {
} );
const frontendBoundingClientRect = await getBoundingClientRect( {
frontendUtils,
editorUtils,
pageObject,
leftArrowSelector:
blockData.selectors.editor.leftArrow.outsideTheImage,
rightArrowSelector:
@ -248,7 +274,7 @@ test.describe( `${ blockData.name }`, () => {
page,
editor,
editorUtils,
frontendUtils,
pageObject,
} ) => {
// Currently we are adding the block under the related products block, but in the future we have to add replace the product gallery block with this block.
const parentBlock = await editorUtils.getBlockByName(
@ -268,7 +294,9 @@ test.describe( `${ blockData.name }`, () => {
parentClientId
);
await (
await editorUtils.getBlockByName( blockData.name )
await pageObject.getNextPreviousButtonsBlock( {
page: 'editor',
} )
).click();
await editor.openDocumentSettingsSidebar();
@ -277,8 +305,7 @@ test.describe( `${ blockData.name }`, () => {
.click();
const editorBoundingClientRect = await getBoundingClientRect( {
frontendUtils,
editorUtils,
pageObject,
leftArrowSelector:
blockData.selectors.editor.leftArrow.insideTheImage,
rightArrowSelector:
@ -306,8 +333,7 @@ test.describe( `${ blockData.name }`, () => {
} );
const frontendBoundingClientRect = await getBoundingClientRect( {
frontendUtils,
editorUtils,
pageObject,
leftArrowSelector:
blockData.selectors.editor.leftArrow.insideTheImage,
rightArrowSelector:
@ -328,7 +354,7 @@ test.describe( `${ blockData.name }`, () => {
page,
editor,
editorUtils,
frontendUtils,
pageObject,
} ) => {
// Currently we are adding the block under the related products block, but in the future we have to add replace the product gallery block with this block.
const parentBlock = await editorUtils.getBlockByName(
@ -348,12 +374,16 @@ test.describe( `${ blockData.name }`, () => {
parentClientId
);
await (
await editorUtils.getBlockByName( blockData.name )
await pageObject.getNextPreviousButtonsBlock( {
page: 'editor',
} )
).click();
await editorUtils.setLayoutOption( 'Align Top' );
const block = await editorUtils.getBlockByName( blockData.name );
const block = await pageObject.getNextPreviousButtonsBlock( {
page: 'editor',
} );
await expect( block ).toHaveCSS( 'align-items', 'flex-start' );
@ -368,8 +398,10 @@ test.describe( `${ blockData.name }`, () => {
waitUntil: 'commit',
} );
const frontendBlock = await frontendUtils.getBlockByName(
blockData.name
const frontendBlock = await pageObject.getNextPreviousButtonsBlock(
{
page: 'frontend',
}
);
await expect( frontendBlock ).toHaveCSS(
@ -382,7 +414,7 @@ test.describe( `${ blockData.name }`, () => {
page,
editor,
editorUtils,
frontendUtils,
pageObject,
} ) => {
// Currently we are adding the block under the related products block, but in the future we have to add replace the product gallery block with this block.
const parentBlock = await editorUtils.getBlockByName(
@ -402,12 +434,16 @@ test.describe( `${ blockData.name }`, () => {
parentClientId
);
await (
await editorUtils.getBlockByName( blockData.name )
await await pageObject.getNextPreviousButtonsBlock( {
page: 'editor',
} )
).click();
await editorUtils.setLayoutOption( 'Align Middle' );
const block = await editorUtils.getBlockByName( blockData.name );
const block = await pageObject.getNextPreviousButtonsBlock( {
page: 'editor',
} );
await expect( block ).toHaveCSS( 'align-items', 'center' );
@ -422,8 +458,10 @@ test.describe( `${ blockData.name }`, () => {
waitUntil: 'commit',
} );
const frontendBlock = await frontendUtils.getBlockByName(
blockData.name
const frontendBlock = await pageObject.getNextPreviousButtonsBlock(
{
page: 'frontend',
}
);
await expect( frontendBlock ).toHaveCSS( 'align-items', 'center' );
@ -433,7 +471,7 @@ test.describe( `${ blockData.name }`, () => {
page,
editor,
editorUtils,
frontendUtils,
pageObject,
} ) => {
// Currently we are adding the block under the related products block, but in the future we have to add replace the product gallery block with this block.
const parentBlock = await editorUtils.getBlockByName(
@ -453,10 +491,14 @@ test.describe( `${ blockData.name }`, () => {
parentClientId
);
await (
await editorUtils.getBlockByName( blockData.name )
await pageObject.getNextPreviousButtonsBlock( {
page: 'editor',
} )
).click();
const block = await editorUtils.getBlockByName( blockData.name );
const block = await pageObject.getNextPreviousButtonsBlock( {
page: 'editor',
} );
await expect( block ).toHaveCSS( 'align-items', 'flex-end' );
@ -471,8 +513,10 @@ test.describe( `${ blockData.name }`, () => {
waitUntil: 'commit',
} );
const frontendBlock = await frontendUtils.getBlockByName(
blockData.name
const frontendBlock = await pageObject.getNextPreviousButtonsBlock(
{
page: 'frontend',
}
);
await expect( frontendBlock ).toHaveCSS(

View File

@ -19,10 +19,12 @@ const blockData = {
};
const test = base.extend< { pageObject: ProductGalleryPage } >( {
pageObject: async ( { page, editor }, use ) => {
pageObject: async ( { page, editor, frontendUtils, editorUtils }, use ) => {
const pageObject = new ProductGalleryPage( {
page,
editor,
frontendUtils,
editorUtils,
} );
await use( pageObject );
},
@ -48,12 +50,13 @@ test.describe( `${ blockData.name }`, () => {
test( 'Renders Product Gallery Large Image block on the editor and frontend side', async ( {
page,
editorUtils,
frontendUtils,
pageObject,
} ) => {
await pageObject.addProductGalleryBlock( { cleanContent: true } );
const block = await editorUtils.getBlockByName( blockData.name );
const block = await pageObject.getMainImageBlock( {
page: 'editor',
} );
await expect( block ).toBeVisible();
@ -63,9 +66,9 @@ test.describe( `${ blockData.name }`, () => {
waitUntil: 'commit',
} );
const blockFrontend = await frontendUtils.getBlockByName(
blockData.name
);
const blockFrontend = await pageObject.getMainImageBlock( {
page: 'frontend',
} );
await expect( blockFrontend ).toBeVisible();
} );
@ -81,7 +84,6 @@ test.describe( `${ blockData.name }`, () => {
test( 'should work on frontend when is enabled', async ( {
pageObject,
editorUtils,
frontendUtils,
page,
} ) => {
await pageObject.addProductGalleryBlock( { cleanContent: true } );
@ -92,9 +94,9 @@ test.describe( `${ blockData.name }`, () => {
waitUntil: 'commit',
} );
const blockFrontend = await frontendUtils.getBlockByName(
blockData.name
);
const blockFrontend = await pageObject.getMainImageBlock( {
page: 'frontend',
} );
// img[style] is the selector because the style attribute is Interactivity API.
const imgElement = blockFrontend.locator(
@ -116,7 +118,6 @@ test.describe( `${ blockData.name }`, () => {
test( 'should not work on frontend when is disabled', async ( {
pageObject,
editorUtils,
frontendUtils,
page,
} ) => {
await pageObject.addProductGalleryBlock( { cleanContent: true } );
@ -132,9 +133,9 @@ test.describe( `${ blockData.name }`, () => {
waitUntil: 'commit',
} );
const blockFrontend = await frontendUtils.getBlockByName(
blockData.name
);
const blockFrontend = await pageObject.getMainImageBlock( {
page: 'frontend',
} );
// img[style] is the selector because the style attribute is added by Interactivity API. In this case, the style attribute should not be added.
const imgElement = blockFrontend.locator(

View File

@ -1,12 +1,13 @@
/**
* External dependencies
*/
import { test, expect } from '@woocommerce/e2e-playwright-utils';
import { test as base, expect } from '@woocommerce/e2e-playwright-utils';
/**
* Internal dependencies
*/
import { addBlock } from './utils';
import { ProductGalleryPage } from '../../product-gallery.page';
const blockData = {
name: 'woocommerce/product-gallery-thumbnails',
@ -25,6 +26,17 @@ const blockData = {
productPage: '/product/v-neck-t-shirt/',
};
const test = base.extend< { pageObject: ProductGalleryPage } >( {
pageObject: async ( { page, editor, frontendUtils, editorUtils }, use ) => {
const pageObject = new ProductGalleryPage( {
page,
editor,
frontendUtils,
editorUtils,
} );
await use( pageObject );
},
} );
test.describe( `${ blockData.name }`, () => {
test.beforeEach( async ( { requestUtils, admin, editorUtils } ) => {
await requestUtils.deleteAllTemplates( 'wp_template' );
@ -39,19 +51,18 @@ test.describe( `${ blockData.name }`, () => {
test( 'Renders Product Gallery Thumbnails block on the editor and frontend side', async ( {
page,
editor,
editorUtils,
frontendUtils,
pageObject,
} ) => {
await editor.insertBlock( {
name: 'woocommerce/product-gallery',
} );
const thumbnailsBlock = await editorUtils.getBlockByName(
blockData.name
);
const largeImageBlock = await editorUtils.getBlockByName(
'woocommerce/product-gallery-large-image'
);
const thumbnailsBlock = await pageObject.getThumbnailsBlock( {
page: 'editor',
} );
const largeImageBlock = await pageObject.getMainImageBlock( {
page: 'editor',
} );
const thumbnailsBlockBoundingRect = await thumbnailsBlock.boundingBox();
const largeImageBlockBoundingRect = await largeImageBlock.boundingBox();
@ -76,13 +87,13 @@ test.describe( `${ blockData.name }`, () => {
waitUntil: 'commit',
} );
const thumbnailsBlockFrontend = await frontendUtils.getBlockByName(
blockData.name
);
const thumbnailsBlockFrontend = await pageObject.getThumbnailsBlock( {
page: 'frontend',
} );
const largeImageBlockFrontend = await frontendUtils.getBlockByName(
'woocommerce/product-gallery-large-image'
);
const largeImageBlockFrontend = await pageObject.getMainImageBlock( {
page: 'frontend',
} );
const thumbnailsBlockFrontendBoundingRect =
await thumbnailsBlockFrontend.boundingBox();

View File

@ -3,7 +3,6 @@
*/
import { test as base, expect } from '@woocommerce/e2e-playwright-utils';
import { Locator, Page } from '@playwright/test';
import { FrontendUtils } from '@woocommerce/e2e-utils';
/**
* Internal dependencies
@ -21,23 +20,23 @@ const blockData = {
};
const test = base.extend< { pageObject: ProductGalleryPage } >( {
pageObject: async ( { page, editor }, use ) => {
pageObject: async ( { page, editor, frontendUtils, editorUtils }, use ) => {
const pageObject = new ProductGalleryPage( {
page,
editor,
frontendUtils,
editorUtils,
} );
await use( pageObject );
},
} );
export const getVisibleLargeImageId = async (
frontendUtils: FrontendUtils
mainImageBlockLocator: Locator
) => {
const mainImageBlock = await frontendUtils.getBlockByName(
'woocommerce/product-gallery-large-image'
);
const mainImage = mainImageBlock.locator( 'img:not([hidden])' ) as Locator;
const mainImage = mainImageBlockLocator.locator(
'img:not([hidden])'
) as Locator;
const mainImageContext = ( await mainImage.getAttribute(
'data-wc-context'
@ -48,7 +47,7 @@ export const getVisibleLargeImageId = async (
return mainImageParsedContext.woocommerce.imageId;
};
export const waitForJavascriptFrontendFileIsLoaded = async ( page: Page ) => {
const waitForJavascriptFrontendFileIsLoaded = async ( page: Page ) => {
await page.waitForResponse(
( response ) =>
response.url().includes( 'product-gallery-frontend' ) &&
@ -56,15 +55,11 @@ export const waitForJavascriptFrontendFileIsLoaded = async ( page: Page ) => {
);
};
export const getThumbnailImageIdByNth = async (
const getThumbnailImageIdByNth = async (
nth: number,
frontendUtils: FrontendUtils
thumbnailsLocator: Locator
) => {
const thumbnailsBlock = await frontendUtils.getBlockByName(
'woocommerce/product-gallery-thumbnails'
);
const image = thumbnailsBlock.locator( 'img' ).nth( nth );
const image = thumbnailsLocator.locator( 'img' ).nth( nth );
const imageContext = ( await image.getAttribute(
'data-wc-context'
@ -91,75 +86,154 @@ test.describe( `${ blockData.name }`, () => {
await requestUtils.deleteAllTemplates( 'wp_template_part' );
} );
test( 'should have as first thumbnail, the same image that it is visible in the Large Image block', async ( {
page,
editorUtils,
frontendUtils,
pageObject,
} ) => {
await pageObject.addProductGalleryBlock( { cleanContent: true } );
test.describe( 'with thumbnails', () => {
test( 'should have as first thumbnail, the same image that it is visible in the Large Image block', async ( {
page,
editorUtils,
pageObject,
} ) => {
await pageObject.addProductGalleryBlock( { cleanContent: true } );
await editorUtils.saveTemplate();
await editorUtils.saveTemplate();
await page.goto( blockData.productPage, {
waitUntil: 'commit',
await page.goto( blockData.productPage, {
waitUntil: 'commit',
} );
const visibleLargeImageId = await getVisibleLargeImageId(
await pageObject.getMainImageBlock( {
page: 'frontend',
} )
);
const firstImageThumbnailId = await getThumbnailImageIdByNth(
0,
await pageObject.getThumbnailsBlock( {
page: 'frontend',
} )
);
expect( visibleLargeImageId ).toBe( firstImageThumbnailId );
} );
const visibleLargeImageId = await getVisibleLargeImageId(
frontendUtils
);
// @todo: Fix this test. It's failing because the thumbnail images aren't generated correctly when the products are imported via .xml: https://github.com/woocommerce/woocommerce/issues/31646
// eslint-disable-next-line playwright/no-skipped-test
test.skip( 'should change the image when the user click on a thumbnail image', async ( {
page,
editorUtils,
pageObject,
} ) => {
await pageObject.addProductGalleryBlock( { cleanContent: true } );
const firstImageThumbnailId = await getThumbnailImageIdByNth(
0,
frontendUtils
);
await editorUtils.saveTemplate();
expect( visibleLargeImageId ).toBe( firstImageThumbnailId );
await Promise.all( [
page.goto( blockData.productPage, {
waitUntil: 'load',
} ),
waitForJavascriptFrontendFileIsLoaded( page ),
] );
const visibleLargeImageId = await getVisibleLargeImageId(
await pageObject.getMainImageBlock( {
page: 'frontend',
} )
);
const secondImageThumbnailId = await getThumbnailImageIdByNth(
1,
await pageObject.getThumbnailsBlock( {
page: 'frontend',
} )
);
expect( visibleLargeImageId ).not.toBe( secondImageThumbnailId );
await (
await pageObject.getThumbnailsBlock( {
page: 'frontend',
} )
)
.locator( 'img' )
.nth( 1 )
.click();
const newVisibleLargeImageId = await getVisibleLargeImageId(
await pageObject.getMainImageBlock( {
page: 'frontend',
} )
);
expect( newVisibleLargeImageId ).toBe( secondImageThumbnailId );
} );
} );
// @todo: Fix this test. It's failing because the thumbnail images aren't generated correctly when the products are imported via .xml: https://github.com/woocommerce/woocommerce/issues/31646
// eslint-disable-next-line playwright/no-skipped-test
test.skip( 'should change the image when the user click on a thumbnail image', async ( {
page,
editorUtils,
frontendUtils,
pageObject,
} ) => {
await pageObject.addProductGalleryBlock( { cleanContent: true } );
test.describe( 'full-screen when clicked option', () => {
test( 'should be enabled by default', async ( {
pageObject,
editor,
} ) => {
await pageObject.addProductGalleryBlock( { cleanContent: true } );
await editor.openDocumentSettingsSidebar();
const fullScreenOption =
await pageObject.getFullScreenOnClickSetting();
await editorUtils.saveTemplate();
await expect( fullScreenOption ).toBeChecked();
} );
await Promise.all( [
page.goto( blockData.productPage, {
waitUntil: 'load',
} ),
waitForJavascriptFrontendFileIsLoaded( page ),
] );
test( 'should open dialog on the frontend', async ( {
pageObject,
page,
editorUtils,
} ) => {
await pageObject.addProductGalleryBlock( { cleanContent: true } );
await editorUtils.saveTemplate();
const visibleLargeImageId = await getVisibleLargeImageId(
frontendUtils
);
await Promise.all( [
page.goto( blockData.productPage, {
waitUntil: 'domcontentloaded',
} ),
waitForJavascriptFrontendFileIsLoaded( page ),
] );
const secondImageThumbnailId = await getThumbnailImageIdByNth(
1,
frontendUtils
);
const mainImageBlock = await pageObject.getMainImageBlock( {
page: 'frontend',
} );
expect( visibleLargeImageId ).not.toBe( secondImageThumbnailId );
await expect( page.locator( 'dialog' ) ).toBeHidden();
await (
await frontendUtils.getBlockByName(
'woocommerce/product-gallery-thumbnails'
)
)
.locator( 'img' )
.nth( 1 )
.click();
await mainImageBlock.click();
const newVisibleLargeImageId = await getVisibleLargeImageId(
frontendUtils
);
await expect( page.locator( 'dialog' ) ).toBeVisible();
} );
expect( newVisibleLargeImageId ).toBe( secondImageThumbnailId );
test( 'should not open dialog when the setting is disable on the frontend', async ( {
pageObject,
page,
editor,
editorUtils,
} ) => {
await pageObject.addProductGalleryBlock( { cleanContent: true } );
await editor.openDocumentSettingsSidebar();
await pageObject.toggleFullScreenOnClickSetting( false );
await editorUtils.saveTemplate();
await Promise.all( [
page.goto( blockData.productPage, {
waitUntil: 'domcontentloaded',
} ),
waitForJavascriptFrontendFileIsLoaded( page ),
] );
await expect( page.locator( 'dialog' ) ).toBeHidden();
const mainImageBlock = await pageObject.getMainImageBlock( {
page: 'frontend',
} );
await mainImageBlock.click();
await expect( page.locator( 'dialog' ) ).toBeHidden();
} );
} );
} );

View File

@ -2,21 +2,38 @@
* External dependencies
*/
import { Page } from '@playwright/test';
import { EditorUtils, FrontendUtils } from '@woocommerce/e2e-utils';
import { Editor } from '@wordpress/e2e-test-utils-playwright';
const selectors = {
editor: {
zoomWhileHoveringSetting:
"xpath=//label[contains(text(), 'Zoom while hovering')]/preceding-sibling::span/input",
fullScreenOnClickSetting:
"xpath=//label[contains(text(), 'Full-screen when clicked')]/preceding-sibling::span/input",
},
};
export class ProductGalleryPage {
editor: Editor;
page: Page;
constructor( { editor, page }: { editor: Editor; page: Page } ) {
frontendUtils: FrontendUtils;
editorUtils: EditorUtils;
constructor( {
editor,
page,
frontendUtils,
editorUtils,
}: {
editor: Editor;
page: Page;
frontendUtils: FrontendUtils;
editorUtils: EditorUtils;
} ) {
this.editor = editor;
this.page = page;
this.frontendUtils = frontendUtils;
this.editorUtils = editorUtils;
}
async addProductGalleryBlock( { cleanContent = true } ) {
@ -32,6 +49,24 @@ export class ProductGalleryPage {
return this.page.locator( selectors.editor.zoomWhileHoveringSetting );
}
getFullScreenOnClickSetting() {
return this.page.locator( selectors.editor.fullScreenOnClickSetting );
}
async toggleFullScreenOnClickSetting( enable: boolean ) {
const button = this.page.locator(
selectors.editor.fullScreenOnClickSetting
);
const isChecked = await button.isChecked();
// Toggle the checkbox if it's not in the desired state.
if ( enable && ! isChecked ) {
await button.click();
} else if ( ! enable && isChecked ) {
await button.click();
}
}
async toggleZoomWhileHoveringSetting( enable: boolean ) {
const button = this.page.locator(
selectors.editor.zoomWhileHoveringSetting
@ -45,4 +80,57 @@ export class ProductGalleryPage {
await button.click();
}
}
async getMainImageBlock( { page }: { page: 'frontend' | 'editor' } ) {
const blockName = 'woocommerce/product-gallery-large-image';
if ( page === 'frontend' ) {
return (
await this.frontendUtils.getBlockByName( blockName )
).filter( {
has: this.page.locator( ':visible' ),
} );
}
return this.editorUtils.getBlockByName( blockName );
}
async getThumbnailsBlock( { page }: { page: 'frontend' | 'editor' } ) {
const blockName = 'woocommerce/product-gallery-thumbnails';
if ( page === 'frontend' ) {
return (
await this.frontendUtils.getBlockByName( blockName )
).filter( {
has: this.page.locator( ':visible' ),
} );
}
return this.editorUtils.getBlockByName( blockName );
}
async getNextPreviousButtonsBlock( {
page,
}: {
page: 'frontend' | 'editor';
} ) {
const blockName =
'woocommerce/product-gallery-large-image-next-previous';
if ( page === 'frontend' ) {
return (
await this.frontendUtils.getBlockByName( blockName )
).filter( {
has: this.page.locator( ':visible' ),
} );
}
return this.editorUtils.getBlockByName( blockName );
}
async getBlock( { page }: { page: 'frontend' | 'editor' } ) {
const blockName = 'woocommerce/product-gallery';
if ( page === 'frontend' ) {
return (
await this.frontendUtils.getBlockByName( blockName )
).filter( {
has: this.page.locator( ':visible' ),
} );
}
return this.editorUtils.getBlockByName( blockName );
}
}