Commit d3620aa3235 for woocommerce

commit d3620aa3235fd5d9671addf002c573621f008d08
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date:   Mon Sep 21 12:53:16 2026 +0300

    Fix Analytics commands missing from the command palette when stock management is off (#68753)

    * test(admin): Move command palette registration below E2E

    The command palette spec ran six browser titles. Four of them
    opened the palette, typed a command name, clicked the option and
    checked the heading of the page it landed on. What each command
    registers — its name, its label, its icon, and the URL its callback
    navigates to — is decided in the admin bundle's own JavaScript.

    Add Jest tests for that: one file covers the four static WooCommerce
    commands and the product loader, the other the analytics commands
    registered per report. Together they assert each command's name,
    label, icon, tracking event and target URL, and the loader's search
    debounce, its query, and the records it maps.

    Keep two browser titles: the Add new product command, which proves a
    registered command still reaches its destination through the real
    palette, and the product search command, which proves the loader's
    asynchronous path end to end.

    Consolidates the mega-branch slice:
    - Slice 073: test(admin): Move command palette below E2E

    Refs TESTOPS-288
    Refs #68046

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

    * test(admin): Guard the Analytics report handoff to the command palette

    The Jest suite for the Analytics command palette assigns
    window.wcCommandPaletteAnalytics itself, so once the E2E title was
    removed nothing proved that enqueue_command_palette_assets() enqueues
    the analytics bundle and localizes it from get_report_pages(). The
    review asked for a PHPUnit case to close that gap.

    Feed get_report_pages() two reports through its own
    woocommerce_analytics_report_menu_items filter, run the enqueue, and
    decode the localized global. Assert that the bundle is enqueued, that
    reports reach JS as an array, that titles lose their HTML and that
    paths pass through. A fixture keeps the test stable when a report is
    added to Analytics.

    The CI PHPUnit jobs do not build the admin bundles, so enqueue_script()
    throws there for want of an asset registry. When a bundle is not built,
    the test writes a stub .asset.php and tearDown removes it. Six source
    mutations each fail their own assertion, and the class passes with
    assets/client/admin hidden.

    Refs TESTOPS-288

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

    * fix(admin): Keep Analytics command palette reports a JSON list

    Since #41773, enqueue_command_palette_assets() drops invalid report
    entries with array_filter(), which keeps the original keys. When
    stock management is off, Analytics::get_report_pages() returns null
    for the Stock report in the middle of the list, so the filtered
    array has a gap and wp_localize_script() encodes reports as a JSON
    object. The analytics bundle registers commands only when reports is
    an array, so no WooCommerce Analytics command reaches the palette.

    Reindex the filtered reports with array_values() so they always reach
    JavaScript as a list. That also covers an extension adding a malformed
    entry through the woocommerce_analytics_report_menu_items filter.

    Add a PHPUnit case for a store with stock management off. It fails
    at the array assertion without the reindex, and a dev store showed
    zero analytics commands in the palette before the fix and 11 after.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

    * chore(changelog): Add changelog entry for the Analytics command palette fix

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

    * fix(admin): Skip Analytics command registration for an empty global

    The Analytics command palette bundle checked window.hasOwnProperty()
    and then called hasOwnProperty() on the global itself. When a script
    sets window.wcCommandPaletteAnalytics to null or undefined, that second
    call throws a TypeError inside the domReady callback instead of
    skipping registration. WooCommerce always localizes an object, so this
    guards input no known code produces, as CodeRabbit asked on #68620.

    Read reports through optional chaining and register only when the
    result is an array. One check now covers a missing, null or undefined
    global, missing reports and non-array reports. A "null global" row in
    the Jest table failed with the TypeError before the change.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

    * refactor(admin): Build the command palette report list in one pass

    enqueue_command_palette_assets() mapped every Analytics report to
    either a formatted entry or null, dropped the nulls with
    array_filter(), then reindexed with array_values(). The reindex
    existed only to undo the gap array_filter() leaves behind, so the
    JSON-array requirement read as a patch bolted onto the pipeline
    rather than a property of it.

    Build the list with a foreach that skips invalid entries and appends
    the valid ones. Appending always produces sequential keys, so
    wp_localize_script() encodes a JSON array by construction and no
    reindex step is needed. Suggested in review on #68753.

    Behavior is unchanged. A report is still dropped unless it is an
    array carrying a non-empty string title and path, titles still lose
    their HTML, and paths still pass through untouched. Replacing the
    append with a key taken from the source index reintroduces the gap
    and fails the stock-off PHPUnit case with a JSON object, so the
    existing tests cover the new shape.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

    ---------

    Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

diff --git a/plugins/woocommerce/changelog/fix-command-palette-analytics-stock-off b/plugins/woocommerce/changelog/fix-command-palette-analytics-stock-off
new file mode 100644
index 00000000000..8fd67a76613
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-command-palette-analytics-stock-off
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Show the WooCommerce Analytics commands in the command palette when stock management is turned off.
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/command-palette-analytics/__tests__/index.test.js b/plugins/woocommerce/client/admin/client/wp-admin-scripts/command-palette-analytics/__tests__/index.test.js
index 27334f56ca8..c7f3ccb48af 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/command-palette-analytics/__tests__/index.test.js
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/command-palette-analytics/__tests__/index.test.js
@@ -49,6 +49,7 @@ describe( 'Analytics Command Palette', () => {

 	it.each( [
 		{ caseName: 'missing global', analytics: undefined },
+		{ caseName: 'null global', analytics: null },
 		{ caseName: 'missing reports', analytics: {} },
 		{ caseName: 'non-array reports', analytics: { reports: {} } },
 		{ caseName: 'empty reports', analytics: { reports: [] } },
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/command-palette-analytics/index.js b/plugins/woocommerce/client/admin/client/wp-admin-scripts/command-palette-analytics/index.js
index 98c1ffc0725..cdf5890b963 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/command-palette-analytics/index.js
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/command-palette-analytics/index.js
@@ -30,13 +30,9 @@ const registerWooCommerceAnalyticsCommand = ( { label, path } ) => {
 };

 domReady( () => {
-	if (
-		window.hasOwnProperty( 'wcCommandPaletteAnalytics' ) &&
-		window.wcCommandPaletteAnalytics.hasOwnProperty( 'reports' ) &&
-		Array.isArray( window.wcCommandPaletteAnalytics.reports )
-	) {
-		const analyticsReports = window.wcCommandPaletteAnalytics.reports;
+	const analyticsReports = window.wcCommandPaletteAnalytics?.reports;

+	if ( Array.isArray( analyticsReports ) ) {
 		analyticsReports.forEach( ( analyticsReport ) => {
 			registerWooCommerceAnalyticsCommand( {
 				label: analyticsReport.title,
diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-assets.php b/plugins/woocommerce/includes/admin/class-wc-admin-assets.php
index f1c74f6d8ff..aea5fa1b8a2 100644
--- a/plugins/woocommerce/includes/admin/class-wc-admin-assets.php
+++ b/plugins/woocommerce/includes/admin/class-wc-admin-assets.php
@@ -921,24 +921,24 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) :
 			if ( ! $admin_features_disabled ) {
 				$analytics_reports = Analytics::get_report_pages();
 				if ( is_array( $analytics_reports ) && count( $analytics_reports ) > 0 ) {
-					$formatted_analytics_reports = array_map( function( $report ) {
+					// Append so the reports stay a sequential list; wp_localize_script() encodes a gapped array as a JSON object the command palette ignores.
+					$formatted_analytics_reports = array();
+					foreach ( $analytics_reports as $report ) {
 						if ( ! is_array( $report ) ) {
-							return null;
+							continue;
 						}
-						$title = array_key_exists( 'title', $report ) ? $report['title'] : '';
-						$path = array_key_exists( 'path', $report ) ? $report['path'] : '';
-						if (
-							is_string( $title ) && $title !== "" &&
-							is_string( $path ) && $path !== ""
-						) {
-							return array(
-								'title' => wp_strip_all_tags( $title ),
-								'path' => $path,
-							);
+
+						$title = $report['title'] ?? '';
+						$path  = $report['path'] ?? '';
+						if ( ! is_string( $title ) || '' === $title || ! is_string( $path ) || '' === $path ) {
+							continue;
 						}
-						return null;
-					}, $analytics_reports );
-					$formatted_analytics_reports = array_filter( $formatted_analytics_reports, 'is_array' );
+
+						$formatted_analytics_reports[] = array(
+							'title' => wp_strip_all_tags( $title ),
+							'path'  => $path,
+						);
+					}

 					$this->enqueue_script( 'wp-admin-scripts', 'command-palette-analytics' );
 					wp_localize_script(
diff --git a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-assets-test.php b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-assets-test.php
index 912c6e13ee1..7ea70a2b62d 100644
--- a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-assets-test.php
+++ b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-assets-test.php
@@ -180,6 +180,21 @@ class WC_Admin_Assets_Test extends WC_Unit_Test_Case {
 		$this->assertSame( array( '/analytics/revenue', '/customers' ), array_column( $localized->reports, 'path' ), 'Each report path should be passed unchanged' );
 	}

+	/**
+	 * @testdox Should localize the Analytics reports as an array when stock management is off and the Stock report is left out.
+	 */
+	public function test_enqueue_command_palette_assets_localizes_reports_as_array_without_stock_report(): void {
+		$this->ensure_wp_admin_script_asset_registry( 'command-palette' );
+		$this->ensure_wp_admin_script_asset_registry( 'command-palette-analytics' );
+		update_option( 'woocommerce_manage_stock', 'no' );
+
+		$this->sut->enqueue_command_palette_assets();
+
+		$localized = $this->get_localized_object( 'wc-admin-command-palette-analytics', 'wcCommandPaletteAnalytics' );
+		$this->assertIsArray( $localized->reports ?? null, 'Reports should stay an array once the missing Stock report is dropped, or the command palette registers none of them' );
+		$this->assertNotContains( '/analytics/stock', array_column( $localized->reports, 'path' ), 'The Stock report should not be offered when stock management is off' );
+	}
+
 	/**
 	 * Writes a stub asset registry for a wp-admin-scripts bundle that is not built, as in the CI PHPUnit jobs.
 	 * tearDown() removes every directory and file this creates.