Skip to content
3 changes: 2 additions & 1 deletion .wp-env.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
},
"mappings": {
"wp-content/mu-plugins": "./packages/e2e-tests/mu-plugins",
"wp-content/themes/raft": "https://downloads.wordpress.org/theme/raft.zip"
"wp-content/themes/raft": "https://downloads.wordpress.org/theme/raft.zip",
"wp-content/plugins/woocommerce": "./vendor/wp-content/plugins/woocommerce"
Comment thread
Soare-Robert-Daniel marked this conversation as resolved.
},
"lifecycleScripts": {
"afterStart": "bash bin/e2e-tests.sh"
Expand Down
78 changes: 78 additions & 0 deletions packages/e2e-tests/mu-plugins/otter-e2e-bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -1226,6 +1226,84 @@ function () {
)
);

register_rest_route(
REST_NAMESPACE,
'/woo/product',
array(
'methods' => \WP_REST_Server::CREATABLE,
'permission_callback' => __NAMESPACE__ . '\\require_admin',
'callback' => function ( \WP_REST_Request $request ) {
if ( ! class_exists( 'WC_Product_Simple' ) ) {
return new \WP_Error(
'otter_e2e_no_woocommerce',
'WooCommerce is not active in the environment.',
array( 'status' => 500 )
);
}

// The spec activates WooCommerce right before this call; drop
// the redirect it schedules so it cannot hijack admin visits.
delete_transient( '_wc_activation_redirect' );

$product = new \WC_Product_Simple();
$product->set_name( $request->get_param( 'title' ) ? sanitize_text_field( $request->get_param( 'title' ) ) : 'E2E Product' );
$product->set_regular_price( '49.99' );
$product->set_status( 'publish' );
$id = $product->save();

if ( rest_sanitize_boolean( $request->get_param( 'builder' ) ) ) {
update_post_meta( $id, '_themeisle_gutenberg_woo_builder', true );
}

return rest_ensure_response( array( 'id' => $id ) );
},
)
);

register_rest_route(
REST_NAMESPACE,
'/user/meta-box-order',
array(
'methods' => \WP_REST_Server::CREATABLE,
'permission_callback' => __NAMESPACE__ . '\\require_admin',
'callback' => function ( \WP_REST_Request $request ) {
$order = $request->get_param( 'order' );

if ( ! is_array( $order ) || empty( $order ) ) {
delete_user_meta( get_current_user_id(), 'meta-box-order_product' );
} else {
update_user_meta( get_current_user_id(), 'meta-box-order_product', array_map( 'sanitize_text_field', $order ) );
}

return rest_ensure_response( array( 'ok' => true ) );
},
)
);

register_rest_route(
REST_NAMESPACE,
'/user/meta-boxes-pane/reset',
array(
'methods' => \WP_REST_Server::CREATABLE,
'permission_callback' => __NAMESPACE__ . '\\require_admin',
'callback' => function () {
$user_id = get_current_user_id();
$meta_key = $GLOBALS['wpdb']->get_blog_prefix() . 'persisted_preferences';
$preferences = get_user_meta( $user_id, $meta_key, true );

if ( is_array( $preferences ) && isset( $preferences['core/edit-post'] ) ) {
unset(
$preferences['core/edit-post']['metaBoxesMainIsOpen'],
$preferences['core/edit-post']['metaBoxesMainOpenHeight']
);
update_user_meta( $user_id, $meta_key, $preferences );
}

return rest_ensure_response( array( 'ok' => true ) );
},
)
);

register_rest_route(
REST_NAMESPACE,
'/reset',
Expand Down
59 changes: 59 additions & 0 deletions plugins/otter-pro/inc/plugins/class-woocommerce-builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,65 @@ public function init() {
add_filter( 'wc_get_template_part', array( $this, 'wc_get_template_part' ), 1000, 3 );
add_action( 'otter_blocks_woocommerce_content', 'the_content' );
add_filter( 'body_class', array( $this, 'add_body_class' ), 1000, 1 );
add_action( 'enqueue_block_editor_assets', array( $this, 'show_meta_boxes_pane' ) );
add_filter( 'get_user_option_meta-box-order_product', array( $this, 'restore_product_data_location' ) );
}

/**
* Keep the Product data metabox out of the narrow side column.
*
* The metabox "Move up/down" arrows persist the order instantly and, at the
* edge of an area, relocate a box into the adjacent area. From the block
* editor (used by WooCommerce Builder products) one accidental click can
* move WooCommerce's Product data box into "side", where it renders inside
* the ~280px sidebar and its layout breaks. Correct it at read time; the
* stored user option is left untouched.
*
* @param mixed $order Saved metabox order for the product screen.
*
* @access public
* @return mixed
*/
public function restore_product_data_location( $order ) {
if ( ! boolval( get_post_meta( get_the_ID(), '_themeisle_gutenberg_woo_builder', true ) ) ) {
return $order;
}

if ( ! is_array( $order ) || ! isset( $order['side'] ) || false === strpos( $order['side'], 'woocommerce-product-data' ) ) {
Comment thread
Soare-Robert-Daniel marked this conversation as resolved.
return $order;
}

$side = array_diff( explode( ',', $order['side'] ), array( 'woocommerce-product-data' ) );
$normal = empty( $order['normal'] ) ? array() : explode( ',', $order['normal'] );
array_unshift( $normal, 'woocommerce-product-data' );

$order['side'] = implode( ',', $side );
$order['normal'] = implode( ',', array_unique( $normal ) );

return $order;
}

/**
* Keep the Meta Boxes pane open by default in the block editor.
*
* Since WP 6.7 the iframed post editor renders meta boxes inside a bottom
* drawer that is collapsed unless the user opened it before. On builder
* products that hides the WooCommerce Product data panel (price, inventory
* etc.), so default the drawer to open. An explicit user preference is not
* overridden, as setDefaults only applies to unset preferences.
*
* @access public
* @return void
*/
public function show_meta_boxes_pane() {
if ( 'product' !== get_post_type() || ! boolval( get_post_meta( get_the_ID(), '_themeisle_gutenberg_woo_builder', true ) ) ) {
Comment on lines +83 to +84

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 3bc2a95tests/test-woocommerce-builder.php creates the post contexts and inspects the wp-edit-post inline data: the default is present for a builder-enabled product and absent for both a product without the builder and a regular post.

return;
}

wp_add_inline_script(
'wp-edit-post',
'window.wp && wp.data && wp.data.dispatch( "core/preferences" ).setDefaults( "core/edit-post", { metaBoxesMainIsOpen: true } );'
);
}

/**
Expand Down
126 changes: 126 additions & 0 deletions src/blocks/test/e2e/blocks/woocommerce-builder.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/**
* Internal dependencies
*/
import { test, expect } from '../fixtures';

/**
* WooCommerce Builder forces the block editor for enabled products. Since
* WP 6.7 the iframed post editor renders meta boxes in a bottom drawer, so
* these tests pin the two guarantees that keep WooCommerce's Product data
* panel (price, inventory, …) reachable there:
*
* 1. the drawer defaults to open (an explicit user preference still wins);
* 2. a `meta-box-order_product` that strands the Product data box in the
* "side" area (one accidental click on the metabox move arrows persists
* that) is corrected back into the main area at render time.
*/

const PRODUCT_DATA = '#woocommerce-product-data';
const META_BOXES_DRAWER = '.edit-post-meta-boxes-main';

/**
* The Otter welcome tour pops on fresh profiles; close it if it appears.
*
* @param {import('@playwright/test').Page} page Playwright page.
*/
const dismissOtterTour = async( page ) => {
const guide = page.locator( '.components-guide' );
if ( await guide.isVisible({ timeout: 2000 }).catch( () => false ) ) {
await page.keyboard.press( 'Escape' );
await expect( guide ).toBeHidden();
}
};

test.describe( 'WooCommerce Builder product editor', () => {
// WooCommerce is mounted by wp-env but only activated around this spec —
// its editor integrations would change load behavior (and performance
// numbers) for every other suite. Serial project only.
test.beforeAll( async({ requestUtils }) => {
await requestUtils.activatePlugin( 'woocommerce' );
});

test.afterAll( async({ requestUtils }) => {
await requestUtils.deactivatePlugin( 'woocommerce' );
});

// A drawer preference or metabox order left by an earlier run would skew
// the first test as much as a later one, so reset on both sides.
const resetSharedUserState = async( otterUtils ) => {
await otterUtils.setProductMetaBoxOrder( null );
await otterUtils.resetMetaBoxesPane();
};

test.beforeEach( async({ otterUtils }) => {
await resetSharedUserState( otterUtils );
});

test.afterEach( async({ otterUtils }) => {
await resetSharedUserState( otterUtils );
});

test( 'builder products open in the block editor with the Product data panel visible', async({ admin, page, otterUtils }) => {
const { id } = await otterUtils.createWooProduct({ builder: true });

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6fd2307. Creations now go through a helper that records each id, and afterEach hard-deletes exactly those. It runs before afterAll deactivates WooCommerce, so the deletes also clear its product lookup tables, and the endpoint skips any id whose post type is not product so a stale id cannot remove unrelated content.


await admin.editPost( id );
await dismissOtterTour( page );

// Block editor is active for the builder product…
await expect( page.locator( 'body.block-editor-page' ) ).toHaveCount( 1 );

// …and the Product data metabox is visible without any interaction:
// the meta boxes drawer defaults to open on builder products.
await expect( page.locator( PRODUCT_DATA ) ).toBeVisible();
await expect( page.locator( `${ PRODUCT_DATA } input[name="_regular_price"]` ) ).toHaveValue( '49.99' );
});

test( 'Product data stranded in the side area is rescued into the drawer', async({ admin, page, otterUtils }) => {
const { id } = await otterUtils.createWooProduct({ builder: true });

// The corrupted layout one metabox arrow click can persist: Product
// data serialized into "side", which renders inside the ~280px
// sidebar where its layout breaks.
await otterUtils.setProductMetaBoxOrder({
normal: '',
advanced: 'commentsdiv,postexcerpt',
side: 'woocommerce-product-data,otter_woo_builder,woocommerce-product-images'
});

await admin.editPost( id );
await dismissOtterTour( page );

await expect( page.locator( `${ META_BOXES_DRAWER } ${ PRODUCT_DATA }` ) ).toBeVisible();
await expect( page.locator( `.interface-complementary-area ${ PRODUCT_DATA }` ) ).toHaveCount( 0 );
});

test( 'products without the builder keep the classic editor', async({ admin, page, otterUtils }) => {
const { id } = await otterUtils.createWooProduct({ builder: false });

await admin.visitAdminPage( 'post.php', `post=${ id }&action=edit` );

await expect( page.locator( 'body.block-editor-page' ) ).toHaveCount( 0 );
await expect( page.locator( PRODUCT_DATA ) ).toBeVisible();
});

test( 'an explicitly collapsed drawer stays collapsed (user preference wins)', async({ admin, page, otterUtils }) => {
const { id } = await otterUtils.createWooProduct({ builder: true });

await admin.editPost( id );
await dismissOtterTour( page );
await expect( page.locator( PRODUCT_DATA ) ).toBeVisible();

// Collapse the drawer — this persists the user preference, which
// setDefaults() must not override on the next load. Keyboard: the
// toggle's click point is covered by its drag-resize separator. The
// preference write is debounced, so hold for the REST flush before
// reloading.
await Promise.all([
page.waitForResponse( ( response ) => response.url().includes( '/wp/v2/users/me' ) ),
page.getByRole( 'button', { name: 'Meta Boxes' }).press( 'Enter' )
]);
await expect( page.locator( PRODUCT_DATA ) ).toBeHidden();

await admin.editPost( id );
await expect( page.locator( META_BOXES_DRAWER ) ).toBeVisible();
await expect( page.locator( PRODUCT_DATA ) ).toBeHidden();
});
});
14 changes: 13 additions & 1 deletion src/blocks/test/e2e/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ export type OtterUtils = {

/** Hard-delete all Submission Records. */
cleanupFormRecords: () => Promise<unknown>;

/** Create a published simple WooCommerce product (price 49.99); `builder` also enables WooCommerce Builder on it. */
createWooProduct: ( args?: { title?: string; builder?: boolean } ) => Promise<{ id: number }>;

/** Set the current user's product metabox order (`meta-box-order_product`); null/empty resets it. */
setProductMetaBoxOrder: ( order: Record<string, string> | null ) => Promise<unknown>;

/** Remove the current user's persisted meta-boxes-pane preferences (open state and height). */
resetMetaBoxesPane: () => Promise<unknown>;
};

export const test = base.extend<{ otterUtils: OtterUtils }>({
Expand Down Expand Up @@ -91,7 +100,10 @@ export const test = base.extend<{ otterUtils: OtterUtils }>({
return response.nonce;
},
getFormRecords: () => call( 'form/records' ) as Promise<FormRecord[]>,
cleanupFormRecords: () => call( 'form/records/cleanup' )
cleanupFormRecords: () => call( 'form/records/cleanup' ),
createWooProduct: ( args ) => call( 'woo/product', args ?? {}) as Promise<{ id: number }>,
setProductMetaBoxOrder: ( order ) => call( 'user/meta-box-order', { order }),
resetMetaBoxesPane: () => call( 'user/meta-boxes-pane/reset' )
});
}
});
Expand Down
5 changes: 4 additions & 1 deletion src/blocks/test/e2e/playwright.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ const SERIAL_SPECS = [
'**/blocks/design-library.spec.js',

// Flips the site-wide atomic-wind blocks option.
'**/blocks/atomic-wind-list-view.spec.js'
'**/blocks/atomic-wind-list-view.spec.js',

// Mutates the shared admin user's metabox order and editor preferences.
'**/blocks/woocommerce-builder.spec.js'
];

const config = defineConfig({
Expand Down
Loading
Loading