Skip to content
3 changes: 2 additions & 1 deletion .wp-env.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"testsEnvironment": false,
"plugins": [
".",
"https://downloads.wordpress.org/plugin/ai-provider-for-openai.zip"
"https://downloads.wordpress.org/plugin/ai-provider-for-openai.zip",
"https://downloads.wordpress.org/plugin/woocommerce.latest-stable.zip"
],
"themes": [ "./test/emptytheme" ],
"config": {
Expand Down
3 changes: 2 additions & 1 deletion bin/e2e-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@

# Set-up the `wp_env` environment.
npm run wp-env run cli wp option set themeisle_open_ai_api_key "sk_XXXXXXXXXXXXXXXXXXXXXXXx" # Used by AI tools.
npm run wp-env run cli wp rewrite structure '/%postname%/' # Pretty permalinks: the e2e global-setup reads /wp-json/.
npm run wp-env run cli wp rewrite structure '/%postname%/' # Pretty permalinks: the e2e global-setup reads /wp-json/
npm run wp-env run cli wp transient delete _wc_activation_redirect # WooCommerce would hijack the first wp-admin visit with its setup wizard..
74 changes: 74 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,80 @@ 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 )
);
}

$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
54 changes: 54 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,60 @@
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 ( ! 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
*/
public function show_meta_boxes_pane() {

Check failure on line 78 in plugins/otter-pro/inc/plugins/class-woocommerce-builder.php

View workflow job for this annotation

GitHub Actions / PHPStan

Method ThemeIsle\OtterPro\Plugins\WooCommerce_Builder::show_meta_boxes_pane() has no return type specified.
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
108 changes: 108 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,108 @@
/**
* 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', () => {
test.afterEach( async({ otterUtils }) => {
await otterUtils.setProductMetaBoxOrder( null );

// The collapse test persists a drawer preference for the shared admin
// user; clear it server-side so every test starts as a fresh user.
await otterUtils.resetMetaBoxesPane();
});
Comment thread
Soare-Robert-Daniel marked this conversation as resolved.
Outdated

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