1400 add the generate alt text buttons to the product editor in the images sections - #23568
Conversation
…ctions Registers a product-image-alt script and integration that injects an ImageAltNotice into both the featured image metabox (via the admin_post_thumbnail_html filter) and the WooCommerce product gallery metabox (via JS DOM injection into #woocommerce-product-images .inside), showing the count of images missing alt text when non-zero. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lassic product editor Rewrites useProductImages to read featured and gallery images directly from the WooCommerce classic editor DOM and keep them in sync via MutationObservers and jQuery/AJAX event listeners. Adds useVariationImages as a dedicated hook that handles variation image discovery and alt text fetching. Extracts countImagesMissingAlt into a shared helper and adds JSDoc typedefs for ProductImage, VariationImage, ProductImagesState and ProductImages. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…se event Replaces the stale-element MutationObserver on #set-post-thumbnail with a body-level observer that detects when the WooCommerce thickbox overlay is removed — WordPress updates the thumbnail DOM before closing the modal, so the image is already readable when this fires. Moves the container mount point to #postimagediv (outside .inside) so WordPress AJAX replacements don't unmount React. Removes the PHP admin_post_thumbnail_html filter in favour of JS injection. Renames shouldRenderNotice to shouldHideNotice and updates hook return keys for isLoadingAlts to be more descriptive. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
function_exists is a PHP internal that Patchwork cannot stub; Brain\Monkey
automatically defines wc_get_product when an expect() is set up, so
function_exists('wc_get_product') returns true without any mock.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Coverage Report for CI Build 8Warning Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes. Coverage increased (+0.09%) to 56.732%Details
Uncovered Changes
Coverage Regressions6 previously-covered lines in 1 file lost coverage.
Coverage Stats💛 - Coveralls |
There was a problem hiding this comment.
Pull request overview
Adds a new WooCommerce admin integration and accompanying JS module to display “image alt text missing” notices (with a “Generate image alt text” CTA) in the classic WooCommerce product editor’s featured image, gallery, and variations sections.
Changes:
- Introduces a new PHP integration that conditionally enqueues a
product-image-altadmin asset and passes initial variation-image data to JS. - Adds a new JS entrypoint that mounts a React notice app into the WooCommerce product editor DOM and keeps image/alt state in sync via DOM observers + REST fetches.
- Adds unit test coverage for the PHP integration plus Jest tests for the new hooks/helpers.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Unit/Integrations/Third_Party/WooCommerce_Product_Image_Alt_Integration_Test.php | Unit tests for conditional hook registration and asset enqueue/localization behavior. |
| src/integrations/third-party/woocommerce-product-image-alt-integration.php | New WooCommerce integration that enqueues assets and provides initial variation image data to JS. |
| packages/js/tests/product-image-alt/hooks/use-variations-images.test.js | Tests for variation image DOM parsing, refresh triggers, REST alt fetch, and cleanup. |
| packages/js/tests/product-image-alt/hooks/use-product-image.test.js | Tests for featured image DOM parsing, thickbox detection, REST alt fetch, and cleanup. |
| packages/js/tests/product-image-alt/hooks/use-product-gallery.test.js | Tests for gallery DOM parsing, refresh triggers, REST alt fetch, and cleanup. |
| packages/js/tests/product-image-alt/helpers/should-hide-notice.test.js | Tests for notice visibility logic across featured/gallery/variations locations. |
| packages/js/tests/product-image-alt/helpers/fetch-attachment-alts.test.js | Tests for REST fetching alt text and failure handling. |
| packages/js/tests/product-image-alt/helpers/count-images-missing-alt.test.js | Tests for counting missing-alt images across featured/gallery/variations. |
| packages/js/src/product-image-alt/initialize.js | Mounts the notices into the WooCommerce product editor DOM on domReady. |
| packages/js/src/product-image-alt/hooks/use-variations-images.js | Hook to track variation images/alts from DOM + WooCommerce events + REST. |
| packages/js/src/product-image-alt/hooks/use-product-image.js | Hook to track featured image/alts via DOM observers + AJAX/media events + REST. |
| packages/js/src/product-image-alt/hooks/use-product-gallery.js | Hook to track gallery images/alts via DOM observers + gallery events + REST. |
| packages/js/src/product-image-alt/hooks/index.js | Barrel exports for the new hooks. |
| packages/js/src/product-image-alt/helpers/should-hide-notice.js | Helper to decide when the notice should be hidden per location. |
| packages/js/src/product-image-alt/helpers/index.js | Barrel exports for the new helpers. |
| packages/js/src/product-image-alt/helpers/fetch-attachment-alts.js | Helper to fetch attachment alt_text via REST and return a Map keyed by ID. |
| packages/js/src/product-image-alt/helpers/count-images-missing-alt.js | Helper to count images missing alt text across the three image sources. |
| packages/js/src/product-image-alt/components/image-alt-notice.js | Presentational notice component including the “Generate image alt text” button. |
| packages/js/src/product-image-alt/components/app.js | App wiring: gathers image state, decides visibility, and renders ImageAltNotice. |
| config/webpack/paths.js | Registers product-image-alt as a webpack entry. |
| admin/class-admin-asset-manager.php | Registers the product-image-alt stylesheet in the admin asset manager. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
| }, [] ); | ||
| const initialVariationImages = get( window, "wpseoProductImageAlt.variationImages", [] ); | ||
|
|
||
| const { featuredImage, isLoadingFeaturedImageAlt } = useProductImage(); |
There was a problem hiding this comment.
All three hooks (image, gallery, variations) run here even though this mount only needs one location's data — that triples REST calls and DOM observers across the three notice mounts.
| const { galleryImages, isLoadingProductGalleryAlts } = useProductGallery(); | ||
| const { variationImages, isLoadingVariationImagesAlts } = useVariationImages( initialVariationImages ); | ||
|
|
||
| const isLoading = isLoadingFeaturedImageAlt || isLoadingProductGalleryAlts || isLoadingVariationImagesAlts; |
There was a problem hiding this comment.
Because of the above, this waits on all three sections' fetches before showing anything — so a section whose own data is ready still waits on the other two.
There was a problem hiding this comment.
Thats ok, because the count in the notice is for all the sections.
| const isLoading = isLoadingFeaturedImageAlt || isLoadingProductGalleryAlts || isLoadingVariationImagesAlts; | ||
|
|
||
| const imagesWithoutAlt = useMemo( () => | ||
| countImagesMissingAlt( { featuredImage, galleryImages, variationImages } ) |
There was a problem hiding this comment.
This counts missing alt text across all sections, but it's shown identically in every notice — each section's notice ends up reporting the combined total, not just its own count.
| const ids = images.map( ( img ) => img.id ).filter( ( id ) => id > 0 ); | ||
| const alts = await fetchAttachmentAlts( ids ); | ||
|
|
||
| setGalleryImages( images.map( ( img ) => ( { ...img, alt: alts.get( img.id ) ?? img.alt } ) ) ); |
There was a problem hiding this comment.
If two refresh() calls overlap and resolve out of order, this can overwrite newer state with a stale image list — no check for which call is the latest.
| // Re-run when images are added or removed from the gallery <ul>. | ||
| const galleryContainer = document.querySelector( ".product_images" ); | ||
| if ( galleryContainer ) { | ||
| const galleryObserver = new MutationObserver( () => refresh() ); |
There was a problem hiding this comment.
This MutationObserver setup/teardown is hand-rolled here and in the other two hooks — there's already a useMutationObserver hook in packages/js/src/hooks for this. Also worth debouncing the callback (like editor-watcher.js does), since refresh() re-scans the DOM and refetches on every single mutation.
| ids.map( ( id ) => | ||
| apiFetch( { path: `/wp/v2/media/${ id }?_fields=id,alt_text` } ) | ||
| .then( ( media ) => [ media.id, media.alt_text ?? "" ] ) | ||
| .catch( () => [ id, "" ] ) |
There was a problem hiding this comment.
Failed fetches map to "", same as a real empty alt — so the ?? img.alt fallback in the callers never actually triggers on failure, and a network hiccup wrongly flags an image as missing alt text.
| /** | ||
| * Counts the number of product images missing alt text across featured, gallery, and variation images. | ||
| * | ||
| * @param {object|null} featuredImage The product's featured image, or null. |
There was a problem hiding this comment.
These are documented as three separate params, but the function actually takes one destructured object — doc doesn't match the function.
| return true; | ||
| } | ||
|
|
||
| const countGalleryImagesWithoutAlt = galleryImages.filter( ( img ) => ! img.alt ).length; |
There was a problem hiding this comment.
These two counts are computed even when location is 'product-image' (already returned above).
| // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only, used only to pass data to JS. | ||
| $post_id = isset( $_GET['post'] ) ? (int) $_GET['post'] : 0; | ||
|
|
||
| // Use add_inline_script with wp_json_encode so that number values are |
There was a problem hiding this comment.
This comment says add_inline_script + wp_json_encode is used to keep numbers as numbers, but the code below actually calls localize_script (which wraps wp_localize_script) — comment doesn't match the implementation.
| $this->asset_manager->localize_script( | ||
| 'product-image-alt', | ||
| 'wpseoProductImageAlt', | ||
| [ 'variationImages' => $this->get_variation_images( $post_id ) ], |
There was a problem hiding this comment.
No current_user_can check before this data goes to JS. Not exploitable today since WP core already gates this admin page, but worth a defensive check if this method ever gets reused elsewhere.
|
Moved that logic to the |
Context
Summary
This PR can be summarized in the following changelog entry:
Relevant technical choices:
WooCommerce's internal JS store because it is the classic editor, keeping the integration compatible with the classic editor without relying on WooCommerce internals.
document.body rather than watching #set-post-thumbnail directly. WordPress updates the thumbnail DOM before closing the thickbox, so the overlay removal is the most reliable signal that the image has been set or changed.
outer metabox wrapper) rather than .inside. WordPress's AJAX thumbnail replacement replaces the inner HTML of
.inside, which would unmount the React tree if it were mounted there.
wpseoProductImageAlt.variationImageson page load so the variations notice renders immediately, before WooCommerce loads variation rows into the DOM via its woocommerce_load_variations AJAX call.(/wp/v2/media) to get the saved alt attribute from the database. Newly set images may have an empty img.alt in the DOM until the modal closes, so the REST fetch is the source of truth.
Test instructions
Test instructions for the acceptance test before the PR gets merged
This PR can be acceptance tested by following these steps:
Prerequisites
Test instructions for the acceptance test before the PR gets merged
Simple product — featured image:
should appear below the thumbnail reading "1 Product image is missing alt text."
Simple product — gallery:
of the gallery box showing the count of images missing alt text.
Variable product:
missing alt text. The notice should render immediately on page load (seeded from PHP — no waiting for AJAX).
Relevant test scenarios
Relevant test scenarios
Test instructions for QA when the code is in the RC
QA can test this PR by following these steps:
Impact check
This PR affects the following parts of the plugin, which may require extra testing:
Other environments
[shopify-seo], added test instructions for Shopify and attached theShopifylabel to this PR.[yoast-doc-extension], added test instructions for Yoast SEO for Google Docs and attached theGoogle Docs Add-onlabel to this PR.Documentation
Quality assurance
grunt build:imagesand committed the results, if my PR introduces or edits images or SVGs.Innovation
innovationlabel.Fixes Add the "Generate alt text" buttons to the product editor in the images sections.