Commit bc1c529f4b for woocommerce

commit bc1c529f4b8126c323a0a3bda6cf431c008863b8
Author: Amit Raj <77401999+amitraj2203@users.noreply.github.com>
Date:   Mon Jan 19 21:17:51 2026 +0530

    Migrate WooCommerce Accordion block usages to WordPress core Accordion (#62787)

    * Add deprecation notice and conversion logic for WooCommerce Accordion block

    * Add changelog file

    * Refactor accordion block conversion logic to remove incompatible attributes

    * Fix deprecated block edit logic to use blockEditorStore and handle missing attributes

    * Move convertInnerBlocks function inside DeprecatedBlockEdit

    * Refactor accordion block tests to verify deprecation notice and conversion to core accordion

    * Add test for deprecated blocks

    * Add test for WP version below 6.9

diff --git a/plugins/woocommerce/changelog/feat-62121-migrate-wc-accordion b/plugins/woocommerce/changelog/feat-62121-migrate-wc-accordion
new file mode 100644
index 0000000000..0b62a27efb
--- /dev/null
+++ b/plugins/woocommerce/changelog/feat-62121-migrate-wc-accordion
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Add migration UI and conversion logic to upgrade WooCommerce Accordion blocks to the WordPress core Accordion block in WP 6.9+.
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/accordion/accordion-group/edit.js b/plugins/woocommerce/client/blocks/assets/js/blocks/accordion/accordion-group/edit.js
index 56e6b4f8b3..558621dc71 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/accordion/accordion-group/edit.js
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/accordion/accordion-group/edit.js
@@ -5,16 +5,164 @@ import {
 	useBlockProps,
 	useInnerBlocksProps,
 	InspectorControls,
+	Warning,
+	store as blockEditorStore,
 } from '@wordpress/block-editor';
 import { __ } from '@wordpress/i18n';
-import { PanelBody, ToggleControl } from '@wordpress/components';
+import { PanelBody, ToggleControl, Button } from '@wordpress/components';
+import { useDispatch, useSelect } from '@wordpress/data';
+import { createBlock } from '@wordpress/blocks';
+import { isWpVersion } from '@woocommerce/settings';

 const ACCORDION_BLOCK_NAME = 'woocommerce/accordion-item';
 const ACCORDION_BLOCK = {
 	name: ACCORDION_BLOCK_NAME,
 };

-export default function Edit( { attributes: { autoclose }, setAttributes } ) {
+/**
+ * Deprecation notice component for the WooCommerce Accordion block.
+ *
+ * @param {Object} props          - Component props.
+ * @param {string} props.clientId - The block client ID.
+ *
+ * @return {JSX.Element} The deprecation notice component.
+ */
+function DeprecatedBlockEdit( { clientId } ) {
+	const { replaceBlocks } = useDispatch( blockEditorStore );
+
+	const { currentBlockAttributes, innerBlocks } = useSelect(
+		( select ) => {
+			const blockEditor = select( blockEditorStore );
+			return {
+				currentBlockAttributes:
+					blockEditor.getBlockAttributes( clientId ),
+				innerBlocks: blockEditor.getBlocks( clientId ),
+			};
+		},
+		[ clientId ]
+	);
+
+	/**
+	 * Recursively convert WooCommerce accordion blocks to WordPress core accordion blocks.
+	 *
+	 * @param {Array<*>} blocks - The inner blocks to convert.
+	 *
+	 * @return {Array<*>} The converted blocks.
+	 */
+	const convertInnerBlocks = ( blocks ) => {
+		// Define attributes to REMOVE for each block type.
+		const attributesToRemove = {
+			'woocommerce/accordion-header': [
+				'icon',
+				'textAlignment',
+				'levelOptions',
+			],
+			'woocommerce/accordion-panel': [
+				'allowedBlocks',
+				'isSelected',
+				'openByDefault',
+			],
+		};
+
+		return blocks.map( ( block ) => {
+			let newBlockName = block.name;
+			const newAttributes = { ...block.attributes };
+
+			// Map WooCommerce block names to WordPress core block names.
+			if ( block.name === 'woocommerce/accordion-item' ) {
+				newBlockName = 'core/accordion-item';
+				// No attribute changes needed.
+			} else if ( block.name === 'woocommerce/accordion-header' ) {
+				newBlockName = 'core/accordion-heading';
+
+				// Convert icon to showIcon.
+				if ( block.attributes.icon !== undefined ) {
+					newAttributes.showIcon = block.attributes.icon !== false;
+				}
+
+				// Remove incompatible attributes.
+				const headerAttrs =
+					attributesToRemove[ 'woocommerce/accordion-header' ];
+				headerAttrs.forEach( ( attr ) => {
+					delete newAttributes[ attr ];
+				} );
+			} else if ( block.name === 'woocommerce/accordion-panel' ) {
+				newBlockName = 'core/accordion-panel';
+
+				// Remove incompatible attributes.
+				const panelAttrs =
+					attributesToRemove[ 'woocommerce/accordion-panel' ];
+				panelAttrs.forEach( ( attr ) => {
+					delete newAttributes[ attr ];
+				} );
+			}
+
+			// Recursively convert inner blocks.
+			const convertedInnerBlocks = block.innerBlocks?.length
+				? convertInnerBlocks( block.innerBlocks )
+				: [];
+
+			return createBlock(
+				newBlockName,
+				newAttributes,
+				convertedInnerBlocks
+			);
+		} );
+	};
+
+	const updateBlock = () => {
+		if ( ! currentBlockAttributes ) {
+			return;
+		}
+
+		const convertedInnerBlocks = convertInnerBlocks( innerBlocks );
+
+		// Filter accordion-group attributes - remove 'allowedBlocks'.
+		const { allowedBlocks, ...filteredGroupAttributes } =
+			currentBlockAttributes;
+
+		replaceBlocks(
+			clientId,
+			createBlock(
+				'core/accordion',
+				filteredGroupAttributes,
+				convertedInnerBlocks
+			)
+		);
+	};
+
+	const actions = [
+		<Button key="update" onClick={ updateBlock } variant="primary">
+			{ __( 'Upgrade Block', 'woocommerce' ) }
+		</Button>,
+	];
+
+	return (
+		<Warning actions={ actions } className="wc-block-components-actions">
+			{ __(
+				'This version of the Accordion block is outdated. Upgrade to continue using.',
+				'woocommerce'
+			) }
+		</Warning>
+	);
+}
+
+/**
+ * Edit component for the WooCommerce Accordion Group block.
+ *
+ * @param {Object}   props                      - Component props.
+ * @param {Object}   props.attributes           - Block attributes.
+ * @param {boolean}  props.attributes.autoclose - Whether to auto-close other accordions.
+ * @param {Function} props.setAttributes        - Function to set block attributes.
+ * @param {string}   props.clientId             - The block client ID.
+ *
+ * @return {JSX.Element} The edit component.
+ */
+export default function Edit( {
+	attributes: { autoclose },
+	setAttributes,
+	clientId,
+} ) {
 	const blockProps = useBlockProps();

 	const innerBlocksProps = useInnerBlocksProps( blockProps, {
@@ -23,6 +171,12 @@ export default function Edit( { attributes: { autoclose }, setAttributes } ) {
 		directInsert: true,
 	} );

+	// Show deprecation notice for WordPress 6.9+.
+	if ( isWpVersion( '6.9', '>=' ) ) {
+		return <DeprecatedBlockEdit clientId={ clientId } />;
+	}
+
+	// Original edit UI for WordPress 6.8 and below.
 	return (
 		<>
 			<InspectorControls key="setting">
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/tests/accordion/accordion.block_theme.spec.ts b/plugins/woocommerce/client/blocks/tests/e2e/tests/accordion/accordion.block_theme.spec.ts
index c1bb2906a8..d9866e3906 100644
--- a/plugins/woocommerce/client/blocks/tests/e2e/tests/accordion/accordion.block_theme.spec.ts
+++ b/plugins/woocommerce/client/blocks/tests/e2e/tests/accordion/accordion.block_theme.spec.ts
@@ -1,214 +1,206 @@
 /**
  * External dependencies
  */
-import { expect, test as base } from '@woocommerce/e2e-utils';
-
-/**
- * Internal dependencies
- */
-import { AccordionPage } from './accordion.page';
+import { expect, test } from '@woocommerce/e2e-utils';

 const blockData = {
 	slug: 'woocommerce/accordion-group',
 };

-const test = base.extend< { pageObject: AccordionPage } >( {
-	pageObject: async (
-		{ page, editor, frontendUtils, requestUtils },
-		use
-	) => {
-		const pageObject = new AccordionPage( {
-			page,
-			editor,
-			frontendUtils,
-			requestUtils,
-		} );
-		await use( pageObject );
+const accordionInnerBlocks = [
+	{
+		name: 'woocommerce/accordion-item',
+		innerBlocks: [
+			{
+				name: 'woocommerce/accordion-header',
+				attributes: {
+					title: 'First Accordion Header',
+				},
+			},
+			{
+				name: 'woocommerce/accordion-panel',
+				innerBlocks: [
+					{
+						name: 'core/paragraph',
+						attributes: {
+							content: 'First accordion content',
+						},
+					},
+				],
+			},
+		],
 	},
-} );
+	{
+		name: 'woocommerce/accordion-item',
+		innerBlocks: [
+			{
+				name: 'woocommerce/accordion-header',
+				attributes: {
+					title: 'Second Accordion Header',
+				},
+			},
+			{
+				name: 'woocommerce/accordion-panel',
+				innerBlocks: [
+					{
+						name: 'core/paragraph',
+						attributes: {
+							content: 'Second accordion content',
+						},
+					},
+				],
+			},
+		],
+	},
+];

-test.describe( `${ blockData.slug } Block`, () => {
+test.describe( `${ blockData.slug } Block - Deprecation`, () => {
 	test.beforeEach( async ( { admin } ) => {
 		await admin.createNewPost();
 	} );

-	test( 'can be inserted in Post Editor and it is visible on the frontend when feature flag is enabled', async ( {
+	test( 'shows deprecation notice and converts all inner blocks to core accordion on upgrade (WP 6.9+)', async ( {
 		editor,
 		frontendUtils,
+		page,
+		wpCoreVersion,
 	} ) => {
-		await editor.insertBlock( { name: blockData.slug } );
-		const blockLocator = await editor.getBlockByName( blockData.slug );
-		await expect(
-			blockLocator.getByLabel( 'Accordion title' )
-		).toHaveCount( 2 );
-		await editor.publishAndVisitPost();
-		const blockLocatorFrontend = await frontendUtils.getBlockByName(
-			blockData.slug
-		);
-		await expect( blockLocatorFrontend.getByRole( 'button' ) ).toHaveCount(
-			2
+		// eslint-disable-next-line playwright/no-skipped-test
+		test.skip(
+			wpCoreVersion < 6.9,
+			'This test requires WordPress 6.9 or later'
 		);
-	} );

-	test( 'can add title and panel content', async ( {
-		editor,
-		frontendUtils,
-		pageObject,
-	} ) => {
-		await pageObject.insertAccordionGroup( [
-			{
-				title: 'Accordion title 1',
-				content: 'Test paragraph content for first panel',
-			},
-			{
-				title: 'Accordion title 2',
-				content: 'Test paragraph content for second panel',
-			},
-		] );
-		await editor.publishAndVisitPost();
-		const blockLocatorFrontend = await frontendUtils.getBlockByName(
-			blockData.slug
-		);
+		// Insert WooCommerce accordion block with inner blocks and content.
+		await editor.insertBlock( {
+			name: blockData.slug,
+			innerBlocks: accordionInnerBlocks,
+		} );
+
+		// Wait for the deprecation notice to appear.
 		await expect(
-			blockLocatorFrontend.getByText( 'Accordion title 1' )
+			editor.canvas.getByText(
+				'This version of the Accordion block is outdated. Upgrade to continue using.'
+			)
 		).toBeVisible();
+
+		// Verify legacy block still renders as expected before upgrade. Save as draft and preview it.
+		await editor.saveDraft();
+		const postId = await page.evaluate( () => {
+			return window.wp.data.select( 'core/editor' ).getCurrentPostId();
+		} );
+		await page.goto( `/?p=${ postId }&preview=true` );
+		const legacyAccordionFrontend = frontendUtils.page.locator(
+			'.wp-block-woocommerce-accordion-group'
+		);
+		await expect( legacyAccordionFrontend ).toBeVisible();
+
+		// Verify legacy accordion has accordion items with buttons.
+		const legacyAccordionButtons =
+			legacyAccordionFrontend.getByRole( 'button' );
+		const legacyItemCount = await legacyAccordionButtons.count();
+		expect( legacyItemCount ).toBe( 2 );
+
+		// Verify the content is visible.
 		await expect(
-			blockLocatorFrontend.getByText( 'Accordion title 2' )
+			legacyAccordionFrontend.getByText( 'First Accordion Header' )
 		).toBeVisible();
 		await expect(
-			blockLocatorFrontend.getByText(
-				'Test paragraph content for first panel'
-			)
-		).toBeAttached();
-		await expect(
-			blockLocatorFrontend.getByText(
-				'Test paragraph content for second panel'
-			)
-		).toBeAttached();
-	} );
+			legacyAccordionFrontend.getByText( 'Second Accordion Header' )
+		).toBeVisible();

-	test( 'can toggle panel visibility', async ( {
-		editor,
-		frontendUtils,
-		pageObject,
-	} ) => {
-		await pageObject.insertAccordionGroup( [
-			{
-				title: 'Accordion title',
-				content: 'Test paragraph content for first panel',
-			},
-			{
-				title: 'Accordion title 2',
-				content: 'Test paragraph content for second panel',
-			},
-		] );
-		await editor.publishAndVisitPost();
-		const blockLocatorFrontend = await frontendUtils.getBlockByName(
-			blockData.slug
+		// Go back to editor.
+		await page.goBack();
+
+		// Verify upgrade button is displayed.
+		const upgradeButton = editor.canvas.getByRole( 'button', {
+			name: 'Upgrade Block',
+		} );
+		await expect( upgradeButton ).toBeVisible();
+
+		// Click the upgrade button.
+		await upgradeButton.click();
+
+		// Verify the block was converted to core/accordion.
+		const coreAccordion = await editor.getBlockByName( 'core/accordion' );
+		await expect( coreAccordion ).toBeVisible();
+
+		// Verify the WooCommerce accordion block is no longer present.
+		const wooAccordion = editor.canvas.locator(
+			'[data-type="woocommerce/accordion-group"]'
 		);
-		await expect(
-			blockLocatorFrontend.getByText(
-				'Test paragraph content for first panel'
-			)
-		).not.toBeInViewport();
-		await blockLocatorFrontend.getByRole( 'button' ).first().click();
-		await expect(
-			blockLocatorFrontend.getByText(
-				'Test paragraph content for first panel'
-			)
-		).toBeInViewport();
-	} );
+		await expect( wooAccordion ).toHaveCount( 0 );

-	test( 'can set panel to open by default and should close when clicked', async ( {
-		editor,
-		frontendUtils,
-		pageObject,
-	} ) => {
-		await pageObject.insertAccordionGroup( [
-			{
-				title: 'Accordion title',
-				content: 'Test paragraph content 1',
-			},
-			{
-				title: 'Accordion title 2',
-				content: 'Test paragraph content 2',
-			},
-		] );
-		const accordionPanel = await editor.getBlockByName(
-			'woocommerce/accordion-item'
+		// Verify all inner blocks are converted correctly.
+		// Check that accordion items exist (woocommerce/accordion-item → core/accordion-item).
+		const coreAccordionItems = editor.canvas.locator(
+			'[data-type="core/accordion-item"]'
+		);
+		const itemCount = await coreAccordionItems.count();
+		expect( itemCount ).toBeGreaterThan( 0 );
+
+		// Check accordion headings (woocommerce/accordion-header → core/accordion-heading).
+		const coreAccordionHeadings = editor.canvas.locator(
+			'[data-type="core/accordion-heading"]'
+		);
+		await expect( coreAccordionHeadings ).toHaveCount( itemCount );
+
+		// Check accordion panels (woocommerce/accordion-panel → core/accordion-panel).
+		const coreAccordionPanels = editor.canvas.locator(
+			'[data-type="core/accordion-panel"]'
 		);
-		await editor.selectBlocks( accordionPanel.first() );
+		await expect( coreAccordionPanels ).toHaveCount( itemCount );

-		// Open block settings sidebar and check "Open by default" option
-		await editor.openDocumentSettingsSidebar();
-		await editor.page
-			.getByLabel( 'Settings' )
-			.getByRole( 'checkbox', { name: 'Open by default' } )
-			.check();
+		// Verify no WooCommerce accordion inner blocks remain.
+		const wooAccordionItems = editor.canvas.locator(
+			'[data-type="woocommerce/accordion-item"]'
+		);
+		await expect( wooAccordionItems ).toHaveCount( 0 );
+
+		const wooAccordionHeaders = editor.canvas.locator(
+			'[data-type="woocommerce/accordion-header"]'
+		);
+		await expect( wooAccordionHeaders ).toHaveCount( 0 );

-		// Publish and visit post and check that the panel is hidden.
+		const wooAccordionPanels = editor.canvas.locator(
+			'[data-type="woocommerce/accordion-panel"]'
+		);
+		await expect( wooAccordionPanels ).toHaveCount( 0 );
+
+		// Publish the post.
 		await editor.publishAndVisitPost();
-		const blockLocatorFrontend = await frontendUtils.getBlockByName(
-			blockData.slug
+
+		// Verify the core accordion block is visible on the frontend.
+		const accordionFrontend = frontendUtils.page.locator(
+			'.wp-block-accordion'
 		);
-		await expect(
-			blockLocatorFrontend.getByText( 'Test paragraph content 1' )
-		).toBeInViewport();
-		await blockLocatorFrontend.getByRole( 'button' ).first().click();
-		await expect(
-			blockLocatorFrontend.getByText( 'Test paragraph content 1' )
-		).not.toBeInViewport();
+		await expect( accordionFrontend ).toBeVisible();
+
+		// Verify accordion buttons are present.
+		const accordionButtons = accordionFrontend.getByRole( 'button' );
+		await expect( accordionButtons ).toHaveCount( itemCount );
 	} );

-	test( 'can set to auto close when another panel is clicked', async ( {
+	test( 'does not show deprecation notice in WordPress 6.8 or earlier', async ( {
 		editor,
-		frontendUtils,
-		pageObject,
+		wpCoreVersion,
 	} ) => {
-		await pageObject.insertAccordionGroup( [
-			{
-				title: 'Accordion title 1',
-				content: 'Test paragraph content 1',
-			},
-			{
-				title: 'Accordion title 2',
-				content: 'Test paragraph content 2',
-			},
-			{
-				title: 'Accordion title 3',
-				content: 'Test paragraph content 3',
-			},
-		] );
-		const accordionPanel = await editor.getBlockByName(
-			'woocommerce/accordion-group'
+		// eslint-disable-next-line playwright/no-skipped-test
+		test.skip(
+			wpCoreVersion >= 6.9,
+			'This test is only for WordPress 6.8 or earlier'
 		);
-		await editor.selectBlocks( accordionPanel.first() );
-
-		// Open block settings sidebar and check "Open by default" option
-		await editor.openDocumentSettingsSidebar();
-		await editor.page
-			.getByLabel( 'Settings' )
-			.getByRole( 'checkbox', {
-				name: 'Auto-close',
-			} )
-			.check();
-
-		// Publish and visit post and check that the panel is hidden.
-		await editor.publishAndVisitPost();
-		const blockLocatorFrontend = await frontendUtils.getBlockByName(
-			blockData.slug
+
+		// Insert WooCommerce accordion block with inner blocks and content.
+		await editor.insertBlock( {
+			name: blockData.slug,
+			innerBlocks: accordionInnerBlocks,
+		} );
+
+		// Verify deprecation notice is NOT shown.
+		const deprecationNotice = editor.canvas.getByText(
+			'This version of the Accordion block is outdated. Upgrade to continue using.'
 		);
-		await blockLocatorFrontend.getByRole( 'button' ).first().click();
-		await expect(
-			blockLocatorFrontend.getByText( 'Test paragraph content 1' )
-		).toBeInViewport();
-		await blockLocatorFrontend.getByRole( 'button' ).nth( 1 ).click();
-		await expect(
-			blockLocatorFrontend.getByText( 'Test paragraph content 1' )
-		).not.toBeInViewport();
-		await blockLocatorFrontend.getByRole( 'button' ).nth( 2 ).click();
-		await expect(
-			blockLocatorFrontend.getByText( 'Test paragraph content 2' )
-		).not.toBeInViewport();
+		await expect( deprecationNotice ).toBeHidden();
 	} );
 } );