Skip to content

e2e: get Core e2e tests passing under Gutenberg nightly - #65324

Closed
kraftbj wants to merge 5 commits into
trunkfrom
gutenberg-nightly-test-failures
Closed

kraftbj wants to merge 5 commits into
trunkfrom
gutenberg-nightly-test-failures

Conversation

@kraftbj

@kraftbj kraftbj commented May 26, 2026 •

Copy link
Copy Markdown
Contributor

Submission Review Guidelines:

Changes proposed in this Pull Request:

Brings the Core e2e tests - Gutenberg nightly job back to green. Four tests were failing on the 10.8.0 release run (job); they decompose into three independent issues.

1. Cart block crashes under Gutenberg trunk (3 failures)

Affects tests/e2e-pw/tests/product/product-create-simple.spec.ts (virtual / non-virtual / downloadable).

Gutenberg's new esbuild-based bundling pipeline appends wp.X = Object.assign({}, wp.X) to every window.wp.* global to materialize getter-based exports. Object.assign only copies enumerable own properties, so the non-enumerable __esModule: true flag is dropped. That breaks webpack's default-import interop (__webpack_require__.n falls back to returning the whole namespace object instead of .default), and any external consumer doing import isShallowEqual from '@wordpress/is-shallow-equal' ends up calling the namespace object as a function. The first observable failure was the Cart block's error boundary: in client/blocks/assets/js/data/cart/push-changes.ts:197, isShallowEqual(localState.customerData, …) throws and crashes the whole block.

This is an upstream Gutenberg regression; filed as WordPress/gutenberg issue 78697 so the build pipeline can preserve __esModule. Until that lands, this PR routes isShallowEqual through a small namespace-aware shim that picks .default if present, falls back to the new named isShallowEqual export, and finally to the module object itself. Works on both WordPress core's bundled Gutenberg and Gutenberg trunk.

12 default imports in client/blocks/assets/js/ get redirected through assets/js/utils/is-shallow-equal.ts; the one default import in packages/checkout/filter-registry/index.ts gets the workaround inlined because it crosses a package boundary.

2. Post title locator stale on Gutenberg trunk (1 failure)

Affects tests/e2e-pw/tests/wp-core/create-post.spec.ts.

canvas.getByLabel( /Add title|Block: Title/ ) no longer matched the title field, but the underlying issue was actually two:

  • The title is still exposed as a textbox whose accessible name is Add title, but in recent Gutenberg builds it's no longer surfaced through aria-label. Switched to getByRole( 'textbox', { name: /^(Add title|Block: Title)$/ } ), which works on both Gutenberg trunk and older releases that exposed the same accessible name via aria-label / Block: Title.
  • getCanvas in the e2e-utils-playwright package checks for iframe[name="editor-canvas"] synchronously. On recent Gutenberg builds the iframe mounts slightly after initial paint, so getCanvas was returning the outer page and the locator then waited forever against the wrong frame. Added a short, soft-failing wait on the iframe so older Gutenberg releases (no iframe) keep working.

Screenshots or screen recordings:

Not applicable — e2e + a small runtime shim.

How to test the changes in this Pull Request:

  1. Set up wp-env on this branch, then install Gutenberg nightly:
    pnpm install
    pnpm --filter='@woocommerce/plugin-woocommerce' build
    cd plugins/woocommerce
    pnpm wp-env start
    bash tests/e2e-pw/envs/gutenberg-nightly/env-setup.sh
  2. Run the previously failing tests against Gutenberg nightly:
    pnpm exec playwright test \
      --config tests/e2e-pw/envs/gutenberg-nightly/playwright.config.ts \
      tests/e2e-pw/tests/product/product-create-simple.spec.ts \
      tests/e2e-pw/tests/wp-core/create-post.spec.ts
  3. Confirm all four originally failing tests pass:
    • can create a simple virtual product
    • can create a simple non virtual product
    • can create a simple downloadable product
    • can create new post
  4. Also run against gutenberg-stable (older Gutenberg) to confirm no regression on the previously-working path.

Testing that has already taken place:

  • Reproduced the cart-block crash locally against Gutenberg nightly 23.3.20260523 (loaded from bph/gutenberg) before the fix: console showed TypeError: vr(...) is not a function originating in wc-blocks-data.js at the isShallowEqual(localState.customerData, …) call site, and the cart hit its error boundary.
  • After applying the shim and rebuilding: the cart renders the product line, totals, and Proceed-to-Checkout button with zero console errors.
  • Ran the four originally failing tests under the local Gutenberg nightly env: all pass (6 passed (38.6s), including the [site setup] setup project).
  • pnpm --filter=block-library ts:check reports the same number of errors before and after this PR (2096 pre-existing errors, none introduced by the change).

Milestone

Changelog entry

  • Automatically create a changelog entry from the details below.
  • This Pull Request does not require a changelog entry. (Comment required below)

Changelog entries already added:

  • plugins/woocommerce/changelog/e2e-fix-gutenberg-nightly-title-locator
  • plugins/woocommerce/changelog/fix-gutenberg-nightly-is-shallow-equal-default-import

The Gutenberg-nightly e2e job started failing on `can create new post`
because `getByLabel(/Add title|Block: Title/)` no longer matches the
title field in recent Gutenberg builds. The element is still exposed
as a textbox with the accessible name "Add title", so target it via
`getByRole('textbox', ...)` to keep working on older Gutenberg
versions (which expose the same accessible name through aria-label)
as well as on trunk.
@github-actions github-actions Bot added focus: e2e tests Issues related to e2e tests plugin: woocommerce Issues related to the WooCommerce Core plugin. labels May 26, 2026
kraftbj added 2 commits May 26, 2026 14:47
Gutenberg's new esbuild-based bundling pipeline (WordPress/gutenberg
PR #72140) appends `wp.X = Object.assign({}, wp.X)` to every
`window.wp.*` global to materialize getter-based exports. Object.assign
only copies enumerable own properties, so the non-enumerable
`__esModule: true` flag is dropped — which breaks webpack's
default-import interop (`__webpack_require__.n` falls back to returning
the whole namespace object instead of `.default`). The first symptom
is the Cart block crashing with "isShallowEqual is not a function" on
sites running Gutenberg trunk.

Resolve isShallowEqual through the namespace so the function is picked
up whether the runtime exposes it as `default` (WordPress core's
existing build) or as a named export (Gutenberg trunk's new build).
`getCanvas` in the e2e-utils-playwright package checks for the
`iframe[name="editor-canvas"]` element synchronously, so on recent
Gutenberg builds — where the iframe is mounted slightly after initial
paint — the canvas falls back to the outer page. The title locator
then waits forever against the wrong frame and times out at 20s. Add
a short, soft-failing wait for the iframe so older Gutenberg releases
that render the title directly in the page DOM keep working.
@kraftbj kraftbj changed the title e2e: make post title locator resilient to Gutenberg trunk e2e: get Core e2e tests passing under Gutenberg nightly May 26, 2026
@kraftbj
kraftbj marked this pull request as ready for review May 26, 2026 19:50
@kraftbj
kraftbj requested a review from a team as a code owner May 26, 2026 19:50
@kraftbj
kraftbj requested review from senadir and removed request for a team May 26, 2026 19:50
@kraftbj kraftbj added this to the 10.8.0 milestone May 26, 2026
@woocommercebot
woocommercebot requested review from a team and ObliviousHarmony and removed request for a team May 26, 2026 19:50
@github-actions

github-actions Bot commented May 26, 2026 •

Copy link
Copy Markdown
Contributor

Testing Guidelines

Hi @senadir @ObliviousHarmony ,

Apart from reviewing the code changes, please make sure to review the testing instructions (Guide) and verify that relevant tests (E2E, Unit, Integration, etc.) have been added or updated as needed.

Reminder: PR reviewers are required to document testing performed. This includes:

  • 🖼️ Screenshots or screen recordings.
  • 📝 List of functionality tested / steps followed.
  • 🌐 Site details (environment attributes such as hosting type, plugins, theme, store size, store age, and relevant settings).
  • 🔍 Any analysis performed, such as assessing potential impacts on environment attributes and other plugins, conducting performance profiling, or using LLM/AI-based analysis.

⚠️ Within the testing details you provide, please ensure that no sensitive information (such as API keys, passwords, user data, etc.) is included in this public issue.

@kraftbj

kraftbj commented May 26, 2026

Copy link
Copy Markdown
Contributor Author

This is only impacted by nightly Gutenberg, best I know, but assigning it to 10.8.0 so we can get it out to sites before Gutenberg ships it.

The previous commit replaced the `is-shallow-equal` default imports
with relative-path imports of the local shim, but left them in the
"External dependencies" comment group. Re-group them under "Internal
dependencies" to satisfy the woocommerce/dependency-group ESLint rule
(a couple of files needed a new Internal block as they didn't have
one yet).
@coderabbitai

coderabbitai Bot commented May 26, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: e9618dfd-d6e4-446c-97e6-b66b7582d6ed

📥 Commits

Reviewing files that changed from the base of the PR and between d9bc3be and 3f47774.

📒 Files selected for processing (1)
  • plugins/woocommerce/client/blocks/assets/js/utils/is-shallow-equal.ts

📝 Walkthrough

Walkthrough

Creates a local isShallowEqual adapter, updates many imports to use it, adjusts a filter-registry import pattern, hardens an editor e2e title locator, and adds two changelog entries.

Changes

Gutenberg isShallowEqual namespace resolution and e2e test fixes

Layer / File(s) Summary
isShallowEqual utility adapter for export shape variations
plugins/woocommerce/client/blocks/assets/js/utils/is-shallow-equal.ts
New utility wraps @wordpress/is-shallow-equal, selecting a callable export from default, isShallowEqual, or the namespace, and exports it as the module default.
Component, hook, HOC, block, and data import migration
plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/form/form.tsx, plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/form/validate-state.ts, plugins/woocommerce/client/blocks/assets/js/base/context/hooks/shipping/use-shipping-data.ts, plugins/woocommerce/client/blocks/assets/js/base/context/hooks/use-query-state.js, plugins/woocommerce/client/blocks/assets/js/base/hocs/with-reviews.tsx, plugins/woocommerce/client/blocks/assets/js/base/hooks/use-shallow-equal.ts, plugins/woocommerce/client/blocks/assets/js/blocks/attribute-filter/block.tsx, plugins/woocommerce/client/blocks/assets/js/blocks/rating-filter/block.tsx, plugins/woocommerce/client/blocks/assets/js/blocks/stock-filter/block.tsx, plugins/woocommerce/client/blocks/assets/js/data/cart/push-changes.ts, plugins/woocommerce/client/blocks/assets/js/data/validation/reducers.ts, plugins/woocommerce/client/blocks/assets/js/hocs/with-product-variations.tsx, ...
Thirteen+ files switch isShallowEqual imports from @wordpress/is-shallow-equal to the local adapter to normalize shallow-equality behavior.
Filter registry namespace import pattern for isShallowEqual
plugins/woocommerce/client/blocks/packages/checkout/filter-registry/index.ts
Replaces default import with a namespace import and resolves isShallowEqual from module.default, module.isShallowEqual, or the module namespace fallback.
E2E test improvements for Gutenberg iframe and title locator
plugins/woocommerce/tests/e2e-pw/utils/editor.ts
fillPageTitle adds a soft wait for the editor-canvas iframe before getCanvas and changes the title locator to a textbox role lookup with anchored accessible-name regex for cross-version compatibility.
Changelog entries documenting fixes
plugins/woocommerce/changelog/e2e-fix-gutenberg-nightly-title-locator, plugins/woocommerce/changelog/fix-gutenberg-nightly-is-shallow-equal-default-import
Two patch-level changelog entries record the e2e title-locator update and the isShallowEqual import/namespace handling fix.

🎯 2 (Simple) | ⏱️ ~12 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the primary change: fixing Core e2e tests under Gutenberg nightly by addressing import and locator issues.
Description check ✅ Passed The description thoroughly documents the changes, root causes, solutions, testing performed, and includes detailed instructions for reproducing and validating the fixes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch gutenberg-nightly-test-failures

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@plugins/woocommerce/client/blocks/assets/js/utils/is-shallow-equal.ts`:
- Around line 23-26: The export resolution for isShallowEqual may pick
non-function values and later crash; update the assignment to pick the first
candidate that is a callable function by checking typeof === 'function' on
moduleExports.default, moduleExports.isShallowEqual, and IsShallowEqualModule
(and fallback) before assigning to isShallowEqual; reference the symbols
isShallowEqual, moduleExports, IsShallowEqualModule, and IsShallowEqualFn and
implement the guard so only a function-valued candidate is used (otherwise throw
a clear error or provide a safe noop).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 9e4bf724-1526-4c65-be1d-8b3987ad5ed9

📥 Commits

Reviewing files that changed from the base of the PR and between 8591124 and 16a5694.

📒 Files selected for processing (17)
  • plugins/woocommerce/changelog/e2e-fix-gutenberg-nightly-title-locator
  • plugins/woocommerce/changelog/fix-gutenberg-nightly-is-shallow-equal-default-import
  • plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/form/form.tsx
  • plugins/woocommerce/client/blocks/assets/js/base/components/cart-checkout/form/validate-state.ts
  • plugins/woocommerce/client/blocks/assets/js/base/context/hooks/shipping/use-shipping-data.ts
  • plugins/woocommerce/client/blocks/assets/js/base/context/hooks/use-query-state.js
  • plugins/woocommerce/client/blocks/assets/js/base/hocs/with-reviews.tsx
  • plugins/woocommerce/client/blocks/assets/js/base/hooks/use-shallow-equal.ts
  • plugins/woocommerce/client/blocks/assets/js/blocks/attribute-filter/block.tsx
  • plugins/woocommerce/client/blocks/assets/js/blocks/rating-filter/block.tsx
  • plugins/woocommerce/client/blocks/assets/js/blocks/stock-filter/block.tsx
  • plugins/woocommerce/client/blocks/assets/js/data/cart/push-changes.ts
  • plugins/woocommerce/client/blocks/assets/js/data/validation/reducers.ts
  • plugins/woocommerce/client/blocks/assets/js/hocs/with-product-variations.tsx
  • plugins/woocommerce/client/blocks/assets/js/utils/is-shallow-equal.ts
  • plugins/woocommerce/client/blocks/packages/checkout/filter-registry/index.ts
  • plugins/woocommerce/tests/e2e-pw/utils/editor.ts

Comment thread plugins/woocommerce/client/blocks/assets/js/utils/is-shallow-equal.ts Outdated
@github-actions

github-actions Bot commented May 26, 2026 •

Copy link
Copy Markdown
Contributor

Test using WordPress Playground

The changes in this pull request can be previewed and tested using a WordPress Playground instance.
WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Test this pull request with WordPress Playground.

Note that this URL is valid for 30 days from when this comment was last updated. You can update it by closing/reopening the PR or pushing a commit that changes plugin code.

@github-actions

github-actions Bot commented May 26, 2026 •

Copy link
Copy Markdown
Contributor

Size Change: +4.78 kB (+0.08%)

Total Size: 5.76 MB

Filename Size Change
./plugins/woocommerce/client/blocks/build/active-filters-frontend.js 6.32 kB +124 B (+2%)
./plugins/woocommerce/client/blocks/build/active-filters-wrapper-frontend.js 5.15 kB +152 B (+3.04%)
./plugins/woocommerce/client/blocks/build/active-filters.js 6.41 kB +130 B (+2.07%)
./plugins/woocommerce/client/blocks/build/all-products-frontend.js 9.1 kB +131 B (+1.46%)
./plugins/woocommerce/client/blocks/build/all-products.js 32.6 kB +127 B (+0.39%)
./plugins/woocommerce/client/blocks/build/all-reviews.js 8.43 kB +142 B (+1.71%)
./plugins/woocommerce/client/blocks/build/attribute-filter-frontend.js 7.83 kB +132 B (+1.72%)
./plugins/woocommerce/client/blocks/build/attribute-filter-wrapper-frontend.js 6.71 kB +139 B (+2.11%)
./plugins/woocommerce/client/blocks/build/attribute-filter.js 11.5 kB +132 B (+1.16%)
./plugins/woocommerce/client/blocks/build/cart.js 42.6 kB +147 B (+0.35%)
./plugins/woocommerce/client/blocks/build/checkout.js 55.4 kB +137 B (+0.25%)
./plugins/woocommerce/client/blocks/build/featured-category.js 15.7 kB +130 B (+0.83%)
./plugins/woocommerce/client/blocks/build/featured-product.js 15.8 kB +129 B (+0.82%)
./plugins/woocommerce/client/blocks/build/mini-cart-contents.js 18.3 kB +132 B (+0.72%)
./plugins/woocommerce/client/blocks/build/price-filter-frontend.js 7.44 kB +119 B (+1.63%)
./plugins/woocommerce/client/blocks/build/price-filter-wrapper-frontend.js 6.15 kB +155 B (+2.59%)
./plugins/woocommerce/client/blocks/build/price-filter.js 8.23 kB +124 B (+1.53%)
./plugins/woocommerce/client/blocks/build/product-collection.js 29.1 kB +137 B (+0.47%)
./plugins/woocommerce/client/blocks/build/product-filter-attribute.js 6.89 kB +122 B (+1.8%)
./plugins/woocommerce/client/blocks/build/product-filter-price.js 4.16 kB +128 B (+3.18%)
./plugins/woocommerce/client/blocks/build/product-filter-rating.js 5.38 kB +128 B (+2.44%)
./plugins/woocommerce/client/blocks/build/product-filter-status.js 4.78 kB +129 B (+2.77%)
./plugins/woocommerce/client/blocks/build/product-filter-taxonomy.js 6.53 kB +127 B (+1.98%)
./plugins/woocommerce/client/blocks/build/rating-filter-frontend.js 6.79 kB +123 B (+1.84%)
./plugins/woocommerce/client/blocks/build/rating-filter-wrapper-frontend.js 5.41 kB +139 B (+2.64%)
./plugins/woocommerce/client/blocks/build/rating-filter.js 6.75 kB +123 B (+1.86%)
./plugins/woocommerce/client/blocks/build/reviews-by-category.js 12.2 kB +139 B (+1.16%)
./plugins/woocommerce/client/blocks/build/reviews-by-product.js 13.5 kB +146 B (+1.09%)
./plugins/woocommerce/client/blocks/build/reviews-frontend.js 7.16 kB +144 B (+2.05%)
./plugins/woocommerce/client/blocks/build/single-product.js 11.8 kB +133 B (+1.14%)
./plugins/woocommerce/client/blocks/build/stock-filter-frontend.js 6.97 kB +128 B (+1.87%)
./plugins/woocommerce/client/blocks/build/stock-filter-wrapper-frontend.js 5.67 kB +136 B (+2.46%)
./plugins/woocommerce/client/blocks/build/stock-filter.js 7.08 kB +137 B (+1.97%)
./plugins/woocommerce/client/blocks/build/wc-blocks-data.js 24.1 kB +112 B (+0.47%)
./plugins/woocommerce/client/blocks/build/wc-cart-checkout-base-frontend.js 62.8 kB +184 B (+0.29%)

compressed-size-action

Walk the candidate exports and pick the first one that's actually a
function, rather than relying on `??` and a final type cast. If no
candidate is callable (a state we shouldn't be able to reach with
either the current npm `is-shallow-equal` package or the trunk
Gutenberg build), throw a clear error at import time so the failure
is loud and traceable instead of a delayed "X is not a function".

Also drop the placeholder issue number in the explanatory comment in
favor of the actual upstream report.
@ObliviousHarmony

Copy link
Copy Markdown
Contributor

Technically @kraftbj!

To be honest, having to deeply import a shim like this for any affected @wordpress/* package feels really fragile. Since the problem is caused by the dependency extraction step of our webpack builds, how would you feel about expanding our dependency extraction plugin to address it there? Both our Blocks and Admin builds utilize the plugin and this would apply to all affected @wordpress/* packages silently.

Essentially, we would add a runtime helper that restores the __esModule flag:

// Consumer-side guard that restores the non-enumerable `__esModule` flag when it's
// missing on an externalized `window.wp.*` module. Gutenberg's esbuild build pipeline
// (WordPress/gutenberg#78697) appends `wp.X = Object.assign({}, wp.X)` to every
// `window.wp.*` global to flatten getter-based exports; `Object.assign` only copies
// enumerable own properties, so the non-enumerable `__esModule` marker is dropped.
// Webpack's default-import interop helper (`__webpack_require__.n`) keys off that
// marker — without it, default imports of any `@wordpress/*` package with a default
// export resolve to the namespace object instead of `.default` and crash at the call
// site. We restore the flag in place on the same object Gutenberg shipped, which is
// equivalent to undoing the strip that the upstream pipeline introduced. Runtime is
// a no-op when the flag is already present, when the module has no `default`
// property, or when the module isn't an object.
const RESTORE_ESM_HELPER = `(function (m) {
	if (m && typeof m === 'object' && 'default' in m && !m.__esModule) {
		Object.defineProperty(m, '__esModule', { value: true });
	}
	return m;
})`;

With a helper like this:

/**
 * Build the JS expression that materializes a `window`-style external for the
 * non-module ('var' externalsType) path. For `@wordpress/*` requests the
 * expression is wrapped with RESTORE_ESM_HELPER to restore the `__esModule`
 * flag stripped by Gutenberg's esbuild pipeline; for everything else the
 * expression is the byte-equivalent property-walk that externalsType 'window'
 * would have produced (`window["seg1"]["seg2"]...`).
 *
 * Returns `undefined` when the request shouldn't be externalized, mirroring
 * the upstream callback contract.
 *
 * @param {string}                        request The original import string (e.g. `'@wordpress/is-shallow-equal'`).
 * @param {string | string[] | undefined} result  The value resolved by `externalizeWpDeps`.
 * @return {string | undefined} The JS expression to emit, or `undefined` if not externalized.
 */
const buildExternalExpression = ( request, result ) => {
	if ( result === undefined ) {
		return undefined;
	}
	const segments = Array.isArray( result ) ? result : [ result ];
	const expression =
		'window' +
		segments
			.map( ( segment ) => `[${ JSON.stringify( segment ) }]` )
			.join( '' );
	if ( request.startsWith( '@wordpress/' ) ) {
		return `${ RESTORE_ESM_HELPER }(${ expression })`;
	}
	return expression;
};

We could expand apply() to use our runtime callback to restore the missing __esModule attribute:

if ( this.useModules ) {
	// Script-modules output: externals resolve to ESM specifiers and consumers use direct
	// binding (not `__webpack_require__.n`), so the Gutenberg `__esModule`-strip regression
	// doesn't apply. Pass through to the parent's behavior unmodified.
	this.externalsPlugin = new compiler.webpack.ExternalsPlugin(
		'import',
		this.externalizeWpDeps.bind( this )
	);
} else {
	// Script (non-module) output: externals materialize as `window.wp.*` reads and consumers
	// route default imports through `__webpack_require__.n`, which keys off `__esModule`. We
	// switch externalsType from 'window' to 'var' so we can wrap each `@wordpress/*` external
	// with RESTORE_ESM_HELPER. See the helper definition above for the upstream Gutenberg
	// regression this works around. Non-`@wordpress/*` externals emit the byte-identical
	// `window[seg]...` access they would have under externalsType 'window'.
	this.externalsPlugin = new compiler.webpack.ExternalsPlugin(
		'var',
		( data, externalsCallback ) => {
			this.externalizeWpDeps( data, ( err, result ) => {
				if ( err ) {
					return externalsCallback( err );
				}
				externalsCallback(
					null,
					buildExternalExpression( data.request, result )
				);
			} );
		}
	);
}

It adds a few KB to the output size but if we're concerned about that we could emit it into the webpack runtime via a RuntimeModule.

I tested this approach by adding the following test to the plugin:

const { __test__ } = require( '../index' );

const { buildExternalExpression, RESTORE_ESM_HELPER } = __test__;

describe( 'buildExternalExpression', () => {
	describe( '@wordpress/* externals (wrapped with RESTORE_ESM_HELPER)', () => {
		it( 'wraps a two-segment @wordpress/* path', () => {
			expect(
				buildExternalExpression( '@wordpress/is-shallow-equal', [
					'wp',
					'isShallowEqual',
				] )
			).toBe(
				`${ RESTORE_ESM_HELPER }(window["wp"]["isShallowEqual"])`
			);
		} );

		it( 'wraps a single-segment @wordpress/* result', () => {
			expect(
				buildExternalExpression( '@wordpress/foo', 'wpFoo' )
			).toBe( `${ RESTORE_ESM_HELPER }(window["wpFoo"])` );
		} );

		it( 'each wrap begins with the same RESTORE_ESM_HELPER prefix', () => {
			const wrapped = buildExternalExpression(
				'@wordpress/is-shallow-equal',
				[ 'wp', 'isShallowEqual' ]
			);
			expect( wrapped.startsWith( RESTORE_ESM_HELPER + '(' ) ).toBe(
				true
			);
		} );
	} );

	describe( 'non-@wordpress externals (byte-equivalent to externalsType "window")', () => {
		it( 'emits a plain window[seg1][seg2] walk for @woocommerce/* paths', () => {
			expect(
				buildExternalExpression( '@woocommerce/blocks-checkout', [
					'wc',
					'blocksCheckout',
				] )
			).toBe( 'window["wc"]["blocksCheckout"]' );
		} );

		it( 'emits a single property access for single-string results', () => {
			expect( buildExternalExpression( 'jquery', 'jQuery' ) ).toBe(
				'window["jQuery"]'
			);
		} );

		it( 'JSON-escapes segments containing special characters', () => {
			expect(
				buildExternalExpression( 'weird', [ 'a', 'b"c', 'd\\e' ] )
			).toBe( 'window["a"]["b\\"c"]["d\\\\e"]' );
		} );

		it( 'does not wrap requests in other @-namespaces', () => {
			expect(
				buildExternalExpression( '@scoped/some-pkg', [ 'X', 'Y' ] )
			).toBe( 'window["X"]["Y"]' );
		} );
	} );

	describe( 'non-externalized requests', () => {
		it( 'returns undefined when result is undefined', () => {
			expect(
				buildExternalExpression( '@wordpress/is-shallow-equal', undefined )
			).toBeUndefined();
			expect(
				buildExternalExpression( 'unrelated-package', undefined )
			).toBeUndefined();
		} );
	} );
} );

describe( 'RESTORE_ESM_HELPER (runtime semantics)', () => {
	// Construct the actual function from the emitted string so we can exercise
	// it directly. This is what the bundle's external module would do at runtime.
	const helper = eval( RESTORE_ESM_HELPER ); // eslint-disable-line no-eval

	it( 'is a no-op when __esModule is already true', () => {
		const moduleObj = Object.defineProperty(
			{ default: () => 'ok' },
			'__esModule',
			{ value: true }
		);
		const result = helper( moduleObj );
		expect( result ).toBe( moduleObj );
		expect( result.__esModule ).toBe( true );
	} );

	it( 'restores __esModule in place when missing on a module with default export', () => {
		const moduleObj = { default: () => 'ok' };
		const result = helper( moduleObj );
		expect( result ).toBe( moduleObj ); // same reference, mutated in place
		expect( result.__esModule ).toBe( true );
	} );

	it( 'does not touch modules without a default export', () => {
		const moduleObj = { isShallowEqual: () => 'ok' };
		const result = helper( moduleObj );
		expect( result ).toBe( moduleObj );
		expect( '__esModule' in result ).toBe( false );
	} );

	it( 'returns non-object values unchanged', () => {
		expect( helper( null ) ).toBe( null );
		expect( helper( undefined ) ).toBe( undefined );
		const fn = function () {};
		expect( helper( fn ) ).toBe( fn );
	} );

	it( 'sets __esModule non-enumerable so it survives a subsequent spread', () => {
		const moduleObj = { default: () => 'ok' };
		helper( moduleObj );
		expect( Object.keys( moduleObj ) ).not.toContain( '__esModule' );
		expect(
			Object.getOwnPropertyDescriptor( moduleObj, '__esModule' )
				.enumerable
		).toBe( false );
	} );
} );

@PanosSynetos PanosSynetos modified the milestones: 10.8.0, 10.9.0 May 27, 2026
@kraftbj

kraftbj commented Jun 4, 2026

Copy link
Copy Markdown
Contributor Author

Gutenberg will be releasing a patch to resolve this. WordPress/gutenberg#78917

@kraftbj kraftbj closed this Jun 4, 2026
@kraftbj
kraftbj deleted the gutenberg-nightly-test-failures branch June 4, 2026 14:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

focus: e2e tests Issues related to e2e tests plugin: woocommerce Issues related to the WooCommerce Core plugin.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants