From 6cb816d2766ce093bea5fa6680688ca863cbdf32 Mon Sep 17 00:00:00 2001 From: Ankit Gade Date: Tue, 13 Jan 2026 13:05:28 +0530 Subject: [PATCH 001/123] Update GenAICard to latest designs. --- js/src/components/paid-ads/gen-ai-card.js | 82 ++++++------------- js/src/components/paid-ads/gen-ai-card.scss | 35 ++++++-- .../gen-ai-check-notice.svg | 3 + .../pmax-assets-improvements/gen-ai.svg | 41 +++++++++- 4 files changed, 99 insertions(+), 62 deletions(-) create mode 100644 js/src/images/pmax-assets-improvements/gen-ai-check-notice.svg diff --git a/js/src/components/paid-ads/gen-ai-card.js b/js/src/components/paid-ads/gen-ai-card.js index 7851c943dc..2cfd5a22d8 100644 --- a/js/src/components/paid-ads/gen-ai-card.js +++ b/js/src/components/paid-ads/gen-ai-card.js @@ -2,25 +2,17 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { addQueryArgs } from '@wordpress/url'; -import { external as externalIcon } from '@wordpress/icons'; -import { createInterpolateElement } from '@wordpress/element'; -import { Icon, Flex, FlexBlock, CardBody } from '@wordpress/components'; +import { Flex, FlexBlock, CardBody, Notice } from '@wordpress/components'; /** * Internal dependencies */ -import Badge from '~/components/badge'; import Section from '~/components/section'; -import AppButton from '~/components/app-button'; import useGoogleAdsAccount from '~/hooks/useGoogleAdsAccount'; -import AppDocumentationLink from '~/components/app-documentation-link'; import genAIImageURL from '~/images/pmax-assets-improvements/gen-ai.svg'; -import { GOOGLE_ADS_ACCOUNT_STATUS } from '~/constants'; +import genAICheckMark from '~/images/pmax-assets-improvements/gen-ai-check-notice.svg'; import './gen-ai-card.scss'; -const { CONNECTED, INCOMPLETE } = GOOGLE_ADS_ACCOUNT_STATUS; - /** * GenAICard component displays a promotional card for Google AI-powered asset generation * within Performance Max campaigns. It provides information about the feature, a link to @@ -31,9 +23,6 @@ const { CONNECTED, INCOMPLETE } = GOOGLE_ADS_ACCOUNT_STATUS; */ const GenAICard = () => { const { googleAdsAccount } = useGoogleAdsAccount(); - const hasAdsAccount = [ CONNECTED, INCOMPLETE ].includes( - googleAdsAccount?.status - ); const queryArgs = {}; if ( googleAdsAccount?.ocid ) { @@ -42,11 +31,6 @@ const GenAICard = () => { queryArgs.ecid = googleAdsAccount.id; } - const recommendationsURL = addQueryArgs( - 'https://ads.google.com/aw/recommendations', - queryArgs - ); - return ( @@ -58,63 +42,49 @@ const GenAICard = () => { > - - { __( - 'Now available', - 'google-listings-and-ads' - ) } - -
{ __( - 'You can use Google AI to help build Performance Max assets with a few clicks.', + 'Review Your AI Suggestions', 'google-listings-and-ads' ) }
- { createInterpolateElement( - __( - 'Starting with your website, Google AI will understand what you’re advertising and can generate or suggest text, image, logo, and video assets for you. Learn more', - 'google-listings-and-ads' - ), - { - link: ( - - ), - } + { __( + 'Google AI analyzed your campaign’s URL to automatically generate your ad assets. Please review the suggested text and images below to ensure they align with your brand.', + 'google-listings-and-ads' ) }
- } - iconPosition="right" - href={ recommendationsURL } - disabled={ ! hasAdsAccount } - target="_blank" - isSecondary - > - { __( - 'Generate assets with GenAI', - 'google-listings-and-ads' - ) } - + + { +
+ { __( + 'Text assets were auto-populate with Google AI', + 'google-listings-and-ads' + ) } +
+
- + { diff --git a/js/src/components/paid-ads/gen-ai-card.scss b/js/src/components/paid-ads/gen-ai-card.scss index fed8366217..0a5280f96f 100644 --- a/js/src/components/paid-ads/gen-ai-card.scss +++ b/js/src/components/paid-ads/gen-ai-card.scss @@ -1,4 +1,6 @@ .gla-gen-ai-card { + font-family: "SF Pro Text", $default-font; + :where(.gla-gen-ai-card__wrapper) { flex-direction: column-reverse; @@ -10,12 +12,35 @@ :where(.gla-section-card-title) { font-size: 16px; margin-bottom: 8px; + + div { + font-family: "SF Pro Display", $default-font; + } + } + + :where(.is-success) { + width: 100%; + + :where(.components-notice__content) { + display: flex; + align-items: center; + gap: 12px; + } } - img { - display: block; - max-height: 100%; - margin: 0 auto; - max-width: 100%; + :where(.gla-gen-ai-card__image-block) { + flex: 0 0 92px; + + @media (min-width: $break-small) { + margin-top: 0; + margin-left: 24px; + } + + img { + display: block; + max-height: 100%; + margin: 0 auto; + max-width: 100%; + } } } diff --git a/js/src/images/pmax-assets-improvements/gen-ai-check-notice.svg b/js/src/images/pmax-assets-improvements/gen-ai-check-notice.svg new file mode 100644 index 0000000000..8f283f9378 --- /dev/null +++ b/js/src/images/pmax-assets-improvements/gen-ai-check-notice.svg @@ -0,0 +1,3 @@ + + + diff --git a/js/src/images/pmax-assets-improvements/gen-ai.svg b/js/src/images/pmax-assets-improvements/gen-ai.svg index 098022ed98..73ea45c64b 100644 --- a/js/src/images/pmax-assets-improvements/gen-ai.svg +++ b/js/src/images/pmax-assets-improvements/gen-ai.svg @@ -1 +1,40 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 59a2edd8427d48c968a9c39644be67a2ec5fc8e9 Mon Sep 17 00:00:00 2001 From: asvinb Date: Tue, 13 Jan 2026 12:39:45 +0400 Subject: [PATCH 002/123] Add Gen AI media and text asset generation functionality --- js/src/data/action-types.js | 2 + js/src/data/actions.js | 104 ++++++++++++++++++++++++++++ js/src/data/reducer.js | 13 ++++ js/src/data/selectors.js | 44 ++++++++++++ js/src/data/test/reducer.test.js | 1 + js/src/hooks/useGenAIMediaAssets.js | 29 ++++++++ js/src/hooks/useGenAITextAssets.js | 29 ++++++++ 7 files changed, 222 insertions(+) create mode 100644 js/src/hooks/useGenAIMediaAssets.js create mode 100644 js/src/hooks/useGenAITextAssets.js diff --git a/js/src/data/action-types.js b/js/src/data/action-types.js index 851829cc29..910a18a154 100644 --- a/js/src/data/action-types.js +++ b/js/src/data/action-types.js @@ -57,6 +57,8 @@ const TYPES = { RECEIVE_PRICE_BENCHMARK_SUGGESTIONS_PRODUCT_PRICE: 'RECEIVE_PRICE_BENCHMARK_SUGGESTIONS_PRODUCT_PRICE', RECEIVE_ADS_RECOMMENDATIONS: 'RECEIVE_ADS_RECOMMENDATIONS', + RECEIVE_GEN_AI_MEDIA_ASSETS: 'RECEIVE_GEN_AI_MEDIA_ASSETS', + RECEIVE_GEN_AI_TEXT_ASSETS: 'RECEIVE_GEN_AI_TEXT_ASSETS', }; export default TYPES; diff --git a/js/src/data/actions.js b/js/src/data/actions.js index 8364e6aa15..57a90d9af2 100644 --- a/js/src/data/actions.js +++ b/js/src/data/actions.js @@ -1283,3 +1283,107 @@ export function* receiveAdsRecommendations( recommendationTypes, }; } + +/** + * Fetches Gen AI media assets. If no asset type is provided, it will fetch all asset types. + * + * @param {string} url The final URL for which to generate media assets. + * @param {'marketing_image'|'square_marketing_image'|'portrait_marketing_image'|undefined} [assetType] - The type of media asset to retrieve. + * @return {Object} Action object to save generated media assets. + * @throws Will throw an error if the request failed. + */ +export function* fetchGenAIMediaAssets( url, assetType ) { + try { + const response = yield apiFetch( { + path: `${ API_NAMESPACE }/ads/assets/generate-images`, + method: REQUEST_ACTIONS.POST, + data: { + final_url: url, + type: assetType, + }, + } ); + + const items = response.items || []; + const formattedData = items.reduce( + ( accumulator, { temporary_image_url, type } ) => { + if ( assetType && type !== assetType ) { + return accumulator; + } + + if ( ! temporary_image_url ) { + return accumulator; + } + + accumulator[ type ] = accumulator[ type ] || []; + accumulator[ type ].push( temporary_image_url ); + return accumulator; + }, + {} + ); + + return { + type: TYPES.RECEIVE_GEN_AI_MEDIA_ASSETS, + url, + data: formattedData, + }; + } catch ( error ) { + handleApiError( + error, + __( + 'There was an error generating media assets.', + 'google-listings-and-ads' + ) + ); + throw error; + } +} + +/** + * Fetches Gen AI text assets. If no asset type is provided, it will fetch all asset types. + * + * @param {string} url The final URL for which to generate text assets. + * @param {'headline'|'long_headline'|'description'|undefined} [assetType] - The type of text asset to retrieve. + * @return {Object} Action object to save generated text assets. + */ +export function* fetchGenAITextAssets( url, assetType ) { + try { + const response = yield apiFetch( { + path: `${ API_NAMESPACE }/ads/assets/generate-text`, + method: REQUEST_ACTIONS.POST, + data: { + final_url: url, + type: assetType, + }, + } ); + + const items = response.items || []; + const formattedData = items.reduce( ( accumulator, { text, type } ) => { + if ( assetType && type !== assetType ) { + return accumulator; + } + + if ( ! text ) { + return accumulator; + } + + accumulator[ type ] = accumulator[ type ] || []; + accumulator[ type ].push( text ); + return accumulator; + }, {} ); + + return { + type: TYPES.RECEIVE_GEN_AI_TEXT_ASSETS, + url, + data: formattedData, + }; + } catch ( error ) { + handleApiError( + error, + __( + 'There was an error generating text assets.', + 'google-listings-and-ads' + ) + ); + throw error; + } +} diff --git a/js/src/data/reducer.js b/js/src/data/reducer.js index 2402e397b6..5c3bbbdfec 100644 --- a/js/src/data/reducer.js +++ b/js/src/data/reducer.js @@ -83,6 +83,7 @@ const DEFAULT_STATE = { }, summary: {}, }, + gen_ai_assets: {}, }; /** @@ -631,6 +632,18 @@ const reducer = ( state = DEFAULT_STATE, action ) => { ); } + case TYPES.RECEIVE_GEN_AI_MEDIA_ASSETS: { + const { url, data } = action; + + return setIn( state, [ 'gen_ai_assets', url, 'media' ], data ); + } + + case TYPES.RECEIVE_GEN_AI_TEXT_ASSETS: { + const { url, data } = action; + + return setIn( state, [ 'gen_ai_assets', url, 'text' ], data ); + } + // Page will be reloaded after all accounts have been disconnected, so no need to mutate state. case TYPES.DISCONNECT_ACCOUNTS_ALL: default: diff --git a/js/src/data/selectors.js b/js/src/data/selectors.js index a16b2f7740..64b6e7740b 100644 --- a/js/src/data/selectors.js +++ b/js/src/data/selectors.js @@ -496,3 +496,47 @@ export const getAdsRecommendations = ( state, types, campaign_id = null ) => { const key = arrayToUnderscoreKey( keyToHash ); return state.ads.recommendations[ key ] || null; }; + +/** + * Retrieves the GenAI media assets from the state for a given URL and type. + * + * @param {Object} state - The Redux state object containing GenAI assets data. + * @param {string} url - The URL associated with the GenAI assets. + * @param {'marketing_image'|'square_marketing_image'|'portrait_marketing_image'|undefined} [assetType] - The type of media asset to retrieve. + * @return {Object|null} The media assets for the specified URL and type, or null if not found. + */ +export const getGenAIMediaAssets = ( state, url, assetType ) => { + const mediaAssets = state.gen_ai_assets?.[ url ]?.media; + + if ( ! url || ! mediaAssets ) { + return null; + } + + if ( assetType ) { + return mediaAssets[ assetType ] ?? null; + } + + return mediaAssets; +}; + +/** + * Retrieves the GenAI text assets from the state for a given URL and type. + * + * @param {Object} state - The Redux state object containing GenAI assets data. + * @param {string} url - The URL associated with the GenAI assets. + * @param {'headline'|'long_headline'|'description'|undefined} [assetType] - The type of text asset to retrieve. + * @return {Object|null} The text assets for the specified URL and type, or null if not found. + */ +export const getGenAITextAssets = ( state, url, assetType ) => { + const textAssets = state.gen_ai_assets?.[ url ]?.text; + + if ( ! url || ! textAssets ) { + return null; + } + + if ( assetType ) { + return textAssets[ assetType ] ?? null; + } + + return textAssets; +}; diff --git a/js/src/data/test/reducer.test.js b/js/src/data/test/reducer.test.js index 76ed2e24a6..81edea494b 100644 --- a/js/src/data/test/reducer.test.js +++ b/js/src/data/test/reducer.test.js @@ -85,6 +85,7 @@ describe( 'reducer', () => { }, summary: {}, }, + gen_ai_assets: {}, } ); prepareState = prepareImmutableState.bind( null, defaultState ); diff --git a/js/src/hooks/useGenAIMediaAssets.js b/js/src/hooks/useGenAIMediaAssets.js new file mode 100644 index 0000000000..9a7294d7c6 --- /dev/null +++ b/js/src/hooks/useGenAIMediaAssets.js @@ -0,0 +1,29 @@ +/** + * External dependencies + */ +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { STORE_KEY } from '~/data/constants'; + +const selectorName = 'getGenAIMediaAssets'; + +const useGenAIMediaAssets = ( url, assetType ) => { + return useSelect( + ( select ) => { + const assets = select( STORE_KEY )[ selectorName ]( + url, + assetType + ); + + return { + assets, + }; + }, + [ url, assetType ] + ); +}; + +export default useGenAIMediaAssets; diff --git a/js/src/hooks/useGenAITextAssets.js b/js/src/hooks/useGenAITextAssets.js new file mode 100644 index 0000000000..dd5fcafc43 --- /dev/null +++ b/js/src/hooks/useGenAITextAssets.js @@ -0,0 +1,29 @@ +/** + * External dependencies + */ +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { STORE_KEY } from '~/data/constants'; + +const selectorName = 'getGenAITextAssets'; + +const useGenAITextAssets = ( url, assetType ) => { + return useSelect( + ( select ) => { + const assets = select( STORE_KEY )[ selectorName ]( + url, + assetType + ); + + return { + assets, + }; + }, + [ url, assetType ] + ); +}; + +export default useGenAITextAssets; From a1bdd84d9f90327657fa58e2913fb50a2d1dba7b Mon Sep 17 00:00:00 2001 From: asvinb Date: Tue, 13 Jan 2026 12:54:52 +0400 Subject: [PATCH 003/123] chore: Update package.json maxSize value --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2d40b2594f..9e8489d7f1 100644 --- a/package.json +++ b/package.json @@ -148,7 +148,7 @@ }, { "path": "./js/build/index.js", - "maxSize": "18.7 kB" + "maxSize": "19 kB" }, { "path": "./js/build/commons.js", From 0ef29b58b508ac337fde6b044a9f42aeace76db2 Mon Sep 17 00:00:00 2001 From: Ankit Gade Date: Tue, 13 Jan 2026 15:27:52 +0530 Subject: [PATCH 004/123] Fix: JS tests. --- .../__snapshots__/gen-ai-card.test.js.snap | 102 ++++++++++++++++++ .../components/paid-ads/gen-ai-card.test.js | 72 +++---------- 2 files changed, 117 insertions(+), 57 deletions(-) create mode 100644 js/src/components/paid-ads/__snapshots__/gen-ai-card.test.js.snap diff --git a/js/src/components/paid-ads/__snapshots__/gen-ai-card.test.js.snap b/js/src/components/paid-ads/__snapshots__/gen-ai-card.test.js.snap new file mode 100644 index 0000000000..2b4c6b2d0d --- /dev/null +++ b/js/src/components/paid-ads/__snapshots__/gen-ai-card.test.js.snap @@ -0,0 +1,102 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GenAICard Generate assets with GenAI button Match the snapshot 1`] = ` + +
+
+
+
+
+
+
+
+ Review Your AI Suggestions +
+
+ Google AI analyzed your campaign’s URL to automatically generate your ad assets. Please review the suggested text and images below to ensure they align with your brand. +
+
+
+
+ Notice +
+
+ Checkmark indicating success +
+ Text assets were auto-populate with Google AI +
+
+
+
+
+
+
+ Drawing of a person who successfully launched a campaign +
+
+
+
+ } > +
{ ASSET_IMAGE_SPECS.map( ( spec ) => { const initialImageUrls = initialValues[ spec.key ]; diff --git a/js/src/components/paid-ads/gen-ai-images-notice.js b/js/src/components/paid-ads/gen-ai-images-notice.js new file mode 100644 index 0000000000..fddb8b2bbe --- /dev/null +++ b/js/src/components/paid-ads/gen-ai-images-notice.js @@ -0,0 +1,34 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Notice } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import GenAIImagesNoticeGraphic from '~/images/pmax-assets-improvements/gen-ai-images-notice.svg'; +import './gen-ai-images-notice.scss'; + +const GenAIImagesNotice = () => { + return ( + + + { __( + "We've used your final URL to auto-populate images…", + 'google-listings-and-ads' + ) } + + ); +}; + +export default GenAIImagesNotice; diff --git a/js/src/components/paid-ads/gen-ai-images-notice.scss b/js/src/components/paid-ads/gen-ai-images-notice.scss new file mode 100644 index 0000000000..da12e86de3 --- /dev/null +++ b/js/src/components/paid-ads/gen-ai-images-notice.scss @@ -0,0 +1,12 @@ +.gla-gen-ai-images-notice { + background-color: #f0f6fc; + border-left: none; + border: 1px solid #c5d9ed; + font-family: "SF Pro Text", $default-font; + + :where(.components-notice__content) { + display: flex; + align-items: center; + gap: 12px; + } +} diff --git a/js/src/images/pmax-assets-improvements/gen-ai-images-notice.svg b/js/src/images/pmax-assets-improvements/gen-ai-images-notice.svg new file mode 100644 index 0000000000..4470ea76b2 --- /dev/null +++ b/js/src/images/pmax-assets-improvements/gen-ai-images-notice.svg @@ -0,0 +1,5 @@ + + + + + From de71d050d12c1f8f363d54c156f26bddcab20fc0 Mon Sep 17 00:00:00 2001 From: asvinb Date: Tue, 13 Jan 2026 17:27:30 +0400 Subject: [PATCH 007/123] Rename AddAssetItemButton to AssetItemActionButton --- .../add-asset-item-button.js | 22 ------------ .../asset-group-text-section.js | 6 ++++ .../asset-item-action-button.js | 36 +++++++++++++++++++ ...ton.scss => asset-item-action-button.scss} | 2 +- .../asset-group-editor/images-selector.js | 4 +-- .../asset-group-editor/texts-editor.js | 25 +++++++++++-- .../youtube-video-selector/index.js | 4 +-- js/src/components/paid-ads/assetSpecs.js | 24 +++++++++++++ js/src/images/ai-icon.svg | 1 + 9 files changed, 95 insertions(+), 29 deletions(-) delete mode 100644 js/src/components/paid-ads/asset-group/asset-group-editor/add-asset-item-button.js create mode 100644 js/src/components/paid-ads/asset-group/asset-group-editor/asset-item-action-button.js rename js/src/components/paid-ads/asset-group/asset-group-editor/{add-asset-item-button.scss => asset-item-action-button.scss} (58%) create mode 100644 js/src/images/ai-icon.svg diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/add-asset-item-button.js b/js/src/components/paid-ads/asset-group/asset-group-editor/add-asset-item-button.js deleted file mode 100644 index eae84ccdd4..0000000000 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/add-asset-item-button.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * External dependencies - */ -import GridiconPlusSmall from 'gridicons/dist/plus-small'; - -/** - * Internal dependencies - */ -import AppButton from '~/components/app-button'; -import './add-asset-item-button.scss'; - -export default function AddAssetItemButton( props ) { - return ( - } - iconSize={ 16 } - { ...props } - /> - ); -} diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/asset-group-text-section.js b/js/src/components/paid-ads/asset-group/asset-group-editor/asset-group-text-section.js index d72a138f21..5e3a18203a 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/asset-group-text-section.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/asset-group-text-section.js @@ -129,6 +129,12 @@ const AssetGroupTextSection = ( { maxCharacterCounts={ spec.maxCharacterCounts } placeholder={ spec.capitalizedName } addButtonText={ spec.addButtonText } + generateButtonPluralText={ + spec.generateButtonPluralText + } + generateButtonSingularText={ + spec.generateButtonSingularText + } onChange={ ( texts ) => { if ( spec.requiredSingleValue ) { textProps.onChange( texts[ 0 ] ); diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/asset-item-action-button.js b/js/src/components/paid-ads/asset-group/asset-group-editor/asset-item-action-button.js new file mode 100644 index 0000000000..f1bf38cf79 --- /dev/null +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/asset-item-action-button.js @@ -0,0 +1,36 @@ +/** + * External dependencies + */ +import GridiconPlusSmall from 'gridicons/dist/plus-small'; + +/** + * Internal dependencies + */ +import AppButton from '~/components/app-button'; +import AIIcon from '~/images/ai-icon.svg?inline'; +import './asset-item-action-button.scss'; + +export const ACTION_TYPES = { + ADD: 'add', + GENERATE: 'generate', +}; + +const ACTION_ICONS = { + [ ACTION_TYPES.ADD ]: , + [ ACTION_TYPES.GENERATE ]: , +}; + +export default function AssetItemActionButton( { + action = ACTION_TYPES.ADD, + ...props +} ) { + return ( + + ); +} diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/add-asset-item-button.scss b/js/src/components/paid-ads/asset-group/asset-group-editor/asset-item-action-button.scss similarity index 58% rename from js/src/components/paid-ads/asset-group/asset-group-editor/add-asset-item-button.scss rename to js/src/components/paid-ads/asset-group/asset-group-editor/asset-item-action-button.scss index 5c1e09cd17..bb73355127 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/add-asset-item-button.scss +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/asset-item-action-button.scss @@ -1,4 +1,4 @@ -.gla-add-asset-item-button { +.gla-asset-item-action-button { &.has-icon { padding: $grid-unit-05; } diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/images-selector.js b/js/src/components/paid-ads/asset-group/asset-group-editor/images-selector.js index 92a0b2d69f..949e2314ae 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/images-selector.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/images-selector.js @@ -10,7 +10,7 @@ import { useState, useEffect, useRef } from '@wordpress/element'; */ import useCroppedImageSelector from '~/hooks/useCroppedImageSelector'; import AppTooltip from '~/components/app-tooltip'; -import AddAssetItemButton from './add-asset-item-button'; +import AssetItemActionButton from './asset-item-action-button'; import MediaSelector from './media-selector'; /** @@ -104,7 +104,7 @@ export default function ImagesSelector( { const disabled = maxNumberOfImages !== -1 && images.length >= maxNumberOfImages; const button = ( - ) => void} [props.onChange] Callback function to be called when the texts are changed. @@ -43,6 +47,8 @@ export default function TextsEditor( { maxNumberOfTexts = 0, maxCharacterCounts, addButtonText, + generateButtonPluralText, + generateButtonSingularText, placeholder, children, onChange = noop, @@ -90,6 +96,14 @@ export default function TextsEditor( { }; const normalizedMaxCharacterCounts = [ maxCharacterCounts ].flat(); + const emptyFieldsCount = texts.filter( ( value ) => value === '' ).length; + let generateButtonText; + + if ( emptyFieldsCount === 1 && generateButtonSingularText ) { + generateButtonText = generateButtonSingularText; + } else if ( emptyFieldsCount > 1 && generateButtonPluralText ) { + generateButtonText = generateButtonPluralText; + } return (
@@ -133,7 +147,7 @@ export default function TextsEditor( { } ) }
{ children } -
); } diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/youtube-video-selector/index.js b/js/src/components/paid-ads/asset-group/asset-group-editor/youtube-video-selector/index.js index 2a54fe7281..4bd8b0f996 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/youtube-video-selector/index.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/youtube-video-selector/index.js @@ -8,7 +8,7 @@ import { useState } from '@wordpress/element'; * Internal dependencies */ import AppTooltip from '~/components/app-tooltip'; -import AddAssetItemButton from '../add-asset-item-button'; +import AssetItemActionButton from '../asset-item-action-button'; import MediaSelector from '../media-selector'; import YouTubeVideoInputControl from './youtube-video-input-control'; @@ -37,7 +37,7 @@ export default function YoutubeVideoSelector( { const renderAddButton = () => { const disabled = videoIds.length >= maxNumberOfVideos; const button = ( - ), + generateButtonPluralText: __( + 'Generate long headlines', + 'google-listings-and-ads' + ), + generateButtonSingularText: __( + 'Generate long headline', + 'google-listings-and-ads' + ), }, { @@ -333,6 +349,14 @@ const ASSET_TEXT_SPECS = [
), + generateButtonPluralText: __( + 'Generate descriptions', + 'google-listings-and-ads' + ), + generateButtonSingularText: __( + 'Generate description', + 'google-listings-and-ads' + ), }, ]; diff --git a/js/src/images/ai-icon.svg b/js/src/images/ai-icon.svg new file mode 100644 index 0000000000..23b02113d1 --- /dev/null +++ b/js/src/images/ai-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file From fd244c8384a2e3b769195642b867bd5838e0a6f3 Mon Sep 17 00:00:00 2001 From: Ankit Gade Date: Tue, 13 Jan 2026 21:08:06 +0530 Subject: [PATCH 008/123] Upgrade ads library to V21 --- bin/GoogleAdsCleanupServices.php | 2 +- composer.json | 2 +- src/API/Google/Ads.php | 12 +- src/API/Google/AdsAsset.php | 22 ++-- src/API/Google/AdsAssetGroup.php | 24 ++-- src/API/Google/AdsAssetGroupAsset.php | 10 +- src/API/Google/AdsCampaign.php | 22 ++-- src/API/Google/AdsCampaignBudget.php | 10 +- src/API/Google/AdsCampaignCriterion.php | 12 +- src/API/Google/AdsCampaignLabel.php | 14 +-- src/API/Google/AdsConversionAction.php | 26 ++-- src/API/Google/AdsReport.php | 4 +- src/API/Google/AssetFieldType.php | 2 +- src/API/Google/BillingSetupStatus.php | 2 +- src/API/Google/BudgetMetrics.php | 16 +-- src/API/Google/BudgetRecommendations.php | 14 +-- src/API/Google/CallToActionType.php | 2 +- src/API/Google/CampaignStatus.php | 2 +- src/API/Google/CampaignType.php | 2 +- src/API/Google/MerchantMetrics.php | 2 +- src/API/Google/Query/AdsQuery.php | 6 +- src/API/Google/Query/AdsReportQuery.php | 2 +- src/Ads/AdsRecommendationsService.php | 4 +- src/Google/Ads/ServiceClientFactoryTrait.php | 38 +++--- .../GoogleServiceProvider.php | 2 +- .../HelperTrait/GoogleAdsClientTrait.php | 112 +++++++++--------- .../API/Google/AdsAssetGroupAssetTest.php | 4 +- tests/Unit/API/Google/AdsAssetGroupTest.php | 6 +- tests/Unit/API/Google/AdsAssetTest.php | 2 +- .../API/Google/AdsCampaignCriterionTest.php | 2 +- .../API/Google/AdsConversionActionTest.php | 2 +- tests/Unit/API/Google/AdsTest.php | 8 +- tests/Unit/API/Google/MerchantMetricsTest.php | 6 +- 33 files changed, 198 insertions(+), 198 deletions(-) diff --git a/bin/GoogleAdsCleanupServices.php b/bin/GoogleAdsCleanupServices.php index 83d17a3497..864764bb87 100644 --- a/bin/GoogleAdsCleanupServices.php +++ b/bin/GoogleAdsCleanupServices.php @@ -26,7 +26,7 @@ class GoogleAdsCleanupServices { * * @var string */ - protected $version = 'V20'; + protected $version = 'V21'; /** * @var Event Composer event. diff --git a/composer.json b/composer.json index 1e1c3daf22..4c22d2f0a9 100644 --- a/composer.json +++ b/composer.json @@ -60,7 +60,7 @@ "Google\\Task\\Composer::cleanup", "Automattic\\WooCommerce\\GoogleListingsAndAds\\Util\\SymfonyPolyfillCleanup::remove", "Automattic\\WooCommerce\\GoogleListingsAndAds\\Util\\GoogleAdsCleanupServices::remove", - "composer run-script remove-google-ads-api-version-support -- 18 19 21", + "composer run-script remove-google-ads-api-version-support -- 18 19 20", "php ./bin/prefix-vendor-namespace.php", "bash ./bin/cleanup-vendor-files.sh", "composer dump-autoload" diff --git a/src/API/Google/Ads.php b/src/API/Google/Ads.php index 97bda86023..51b7fa049d 100644 --- a/src/API/Google/Ads.php +++ b/src/API/Google/Ads.php @@ -13,12 +13,12 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait; use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsInterface; use Exception; -use Google\Ads\GoogleAds\Util\V20\ResourceNames; -use Google\Ads\GoogleAds\V20\Enums\AccessRoleEnum\AccessRole; -use Google\Ads\GoogleAds\V20\Enums\ProductLinkInvitationStatusEnum\ProductLinkInvitationStatus; -use Google\Ads\GoogleAds\V20\Resources\ProductLinkInvitation; -use Google\Ads\GoogleAds\V20\Services\ListAccessibleCustomersRequest; -use Google\Ads\GoogleAds\V20\Services\UpdateProductLinkInvitationRequest; +use Google\Ads\GoogleAds\Util\V21\ResourceNames; +use Google\Ads\GoogleAds\V21\Enums\AccessRoleEnum\AccessRole; +use Google\Ads\GoogleAds\V21\Enums\ProductLinkInvitationStatusEnum\ProductLinkInvitationStatus; +use Google\Ads\GoogleAds\V21\Resources\ProductLinkInvitation; +use Google\Ads\GoogleAds\V21\Services\ListAccessibleCustomersRequest; +use Google\Ads\GoogleAds\V21\Services\UpdateProductLinkInvitationRequest; use Google\ApiCore\ApiException; use Google\ApiCore\ValidationException; diff --git a/src/API/Google/AdsAsset.php b/src/API/Google/AdsAsset.php index 3308454626..0be762dd02 100644 --- a/src/API/Google/AdsAsset.php +++ b/src/API/Google/AdsAsset.php @@ -6,17 +6,17 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Google\Ads\GoogleAdsClient; use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait; use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface; -use Google\Ads\GoogleAds\V20\Services\GoogleAdsRow; -use Google\Ads\GoogleAds\V20\Enums\AssetTypeEnum\AssetType; -use Google\Ads\GoogleAds\V20\Resources\Asset; -use Google\Ads\GoogleAds\V20\Services\AssetOperation; -use Google\Ads\GoogleAds\V20\Services\MutateGoogleAdsRequest; -use Google\Ads\GoogleAds\V20\Services\MutateOperation; -use Google\Ads\GoogleAds\Util\V20\ResourceNames; -use Google\Ads\GoogleAds\V20\Common\TextAsset; -use Google\Ads\GoogleAds\V20\Common\ImageAsset; -use Google\Ads\GoogleAds\V20\Common\CallToActionAsset; -use Google\Ads\GoogleAds\V20\Common\YoutubeVideoAsset; +use Google\Ads\GoogleAds\V21\Services\GoogleAdsRow; +use Google\Ads\GoogleAds\V21\Enums\AssetTypeEnum\AssetType; +use Google\Ads\GoogleAds\V21\Resources\Asset; +use Google\Ads\GoogleAds\V21\Services\AssetOperation; +use Google\Ads\GoogleAds\V21\Services\MutateGoogleAdsRequest; +use Google\Ads\GoogleAds\V21\Services\MutateOperation; +use Google\Ads\GoogleAds\Util\V21\ResourceNames; +use Google\Ads\GoogleAds\V21\Common\TextAsset; +use Google\Ads\GoogleAds\V21\Common\ImageAsset; +use Google\Ads\GoogleAds\V21\Common\CallToActionAsset; +use Google\Ads\GoogleAds\V21\Common\YoutubeVideoAsset; use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WP; use Google\ApiCore\ApiException; use Exception; diff --git a/src/API/Google/AdsAssetGroup.php b/src/API/Google/AdsAssetGroup.php index 1c43b255cc..8f813d3a58 100644 --- a/src/API/Google/AdsAssetGroup.php +++ b/src/API/Google/AdsAssetGroup.php @@ -7,18 +7,18 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Google\Ads\GoogleAdsClient; use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface; use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait; -use Google\Ads\GoogleAds\Util\V20\ResourceNames; -use Google\Ads\GoogleAds\V20\Enums\ListingGroupFilterListingSourceEnum\ListingGroupFilterListingSource; -use Google\Ads\GoogleAds\V20\Enums\AssetGroupStatusEnum\AssetGroupStatus; -use Google\Ads\GoogleAds\V20\Enums\ListingGroupFilterTypeEnum\ListingGroupFilterType; -use Google\Ads\GoogleAds\V20\Resources\AssetGroup; -use Google\Ads\GoogleAds\V20\Resources\AssetGroupListingGroupFilter; -use Google\Ads\GoogleAds\V20\Services\AssetGroupListingGroupFilterOperation; -use Google\Ads\GoogleAds\V20\Services\AssetGroupOperation; -use Google\Ads\GoogleAds\V20\Services\GoogleAdsRow; -use Google\Ads\GoogleAds\V20\Services\MutateGoogleAdsRequest; -use Google\Ads\GoogleAds\V20\Services\MutateOperation; -use Google\Ads\GoogleAds\V20\Services\Client\AssetGroupServiceClient; +use Google\Ads\GoogleAds\Util\V21\ResourceNames; +use Google\Ads\GoogleAds\V21\Enums\ListingGroupFilterListingSourceEnum\ListingGroupFilterListingSource; +use Google\Ads\GoogleAds\V21\Enums\AssetGroupStatusEnum\AssetGroupStatus; +use Google\Ads\GoogleAds\V21\Enums\ListingGroupFilterTypeEnum\ListingGroupFilterType; +use Google\Ads\GoogleAds\V21\Resources\AssetGroup; +use Google\Ads\GoogleAds\V21\Resources\AssetGroupListingGroupFilter; +use Google\Ads\GoogleAds\V21\Services\AssetGroupListingGroupFilterOperation; +use Google\Ads\GoogleAds\V21\Services\AssetGroupOperation; +use Google\Ads\GoogleAds\V21\Services\GoogleAdsRow; +use Google\Ads\GoogleAds\V21\Services\MutateGoogleAdsRequest; +use Google\Ads\GoogleAds\V21\Services\MutateOperation; +use Google\Ads\GoogleAds\V21\Services\Client\AssetGroupServiceClient; use Google\ApiCore\ApiException; use Google\ApiCore\ValidationException; use Google\Protobuf\FieldMask; diff --git a/src/API/Google/AdsAssetGroupAsset.php b/src/API/Google/AdsAssetGroupAsset.php index 99addcd37d..751634bbf7 100644 --- a/src/API/Google/AdsAssetGroupAsset.php +++ b/src/API/Google/AdsAssetGroupAsset.php @@ -7,13 +7,13 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Google\Ads\GoogleAdsClient; use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface; use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait; -use Google\Ads\GoogleAds\V20\Services\GoogleAdsRow; -use Google\Ads\GoogleAds\V20\Resources\AssetGroupAsset; +use Google\Ads\GoogleAds\V21\Services\GoogleAdsRow; +use Google\Ads\GoogleAds\V21\Resources\AssetGroupAsset; use Google\ApiCore\ApiException; use Automattic\WooCommerce\GoogleListingsAndAds\Exception\ExceptionWithResponseData; -use Google\Ads\GoogleAds\V20\Services\MutateOperation; -use Google\Ads\GoogleAds\V20\Services\AssetGroupAssetOperation; -use Google\Ads\GoogleAds\Util\V20\ResourceNames; +use Google\Ads\GoogleAds\V21\Services\MutateOperation; +use Google\Ads\GoogleAds\V21\Services\AssetGroupAssetOperation; +use Google\Ads\GoogleAds\Util\V21\ResourceNames; diff --git a/src/API/Google/AdsCampaign.php b/src/API/Google/AdsCampaign.php index faa38acc29..30fc93dcf5 100644 --- a/src/API/Google/AdsCampaign.php +++ b/src/API/Google/AdsCampaign.php @@ -17,17 +17,17 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Options\TransientsInterface; use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WC; use Google\Ads\GoogleAds\Util\FieldMasks; -use Google\Ads\GoogleAds\Util\V20\ResourceNames; -use Google\Ads\GoogleAds\V20\Common\MaximizeConversionValue; -use Google\Ads\GoogleAds\V20\Enums\AdvertisingChannelTypeEnum\AdvertisingChannelType; -use Google\Ads\GoogleAds\V20\Resources\Campaign; -use Google\Ads\GoogleAds\V20\Enums\EuPoliticalAdvertisingStatusEnum\EuPoliticalAdvertisingStatus; -use Google\Ads\GoogleAds\V20\Resources\Campaign\ShoppingSetting; -use Google\Ads\GoogleAds\V20\Services\Client\CampaignServiceClient; -use Google\Ads\GoogleAds\V20\Services\CampaignOperation; -use Google\Ads\GoogleAds\V20\Services\GoogleAdsRow; -use Google\Ads\GoogleAds\V20\Services\MutateGoogleAdsRequest; -use Google\Ads\GoogleAds\V20\Services\MutateOperation; +use Google\Ads\GoogleAds\Util\V21\ResourceNames; +use Google\Ads\GoogleAds\V21\Common\MaximizeConversionValue; +use Google\Ads\GoogleAds\V21\Enums\AdvertisingChannelTypeEnum\AdvertisingChannelType; +use Google\Ads\GoogleAds\V21\Resources\Campaign; +use Google\Ads\GoogleAds\V21\Enums\EuPoliticalAdvertisingStatusEnum\EuPoliticalAdvertisingStatus; +use Google\Ads\GoogleAds\V21\Resources\Campaign\ShoppingSetting; +use Google\Ads\GoogleAds\V21\Services\Client\CampaignServiceClient; +use Google\Ads\GoogleAds\V21\Services\CampaignOperation; +use Google\Ads\GoogleAds\V21\Services\GoogleAdsRow; +use Google\Ads\GoogleAds\V21\Services\MutateGoogleAdsRequest; +use Google\Ads\GoogleAds\V21\Services\MutateOperation; use Google\ApiCore\ApiException; use Google\ApiCore\ValidationException; use Exception; diff --git a/src/API/Google/AdsCampaignBudget.php b/src/API/Google/AdsCampaignBudget.php index 602a564b7b..5f1afe0e23 100644 --- a/src/API/Google/AdsCampaignBudget.php +++ b/src/API/Google/AdsCampaignBudget.php @@ -9,11 +9,11 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface; use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait; use Google\Ads\GoogleAds\Util\FieldMasks; -use Google\Ads\GoogleAds\Util\V20\ResourceNames; -use Google\Ads\GoogleAds\V20\Resources\CampaignBudget; -use Google\Ads\GoogleAds\V20\Services\CampaignBudgetOperation; -use Google\Ads\GoogleAds\V20\Services\Client\CampaignBudgetServiceClient; -use Google\Ads\GoogleAds\V20\Services\MutateOperation; +use Google\Ads\GoogleAds\Util\V21\ResourceNames; +use Google\Ads\GoogleAds\V21\Resources\CampaignBudget; +use Google\Ads\GoogleAds\V21\Services\CampaignBudgetOperation; +use Google\Ads\GoogleAds\V21\Services\Client\CampaignBudgetServiceClient; +use Google\Ads\GoogleAds\V21\Services\MutateOperation; use Google\ApiCore\ValidationException; use Exception; diff --git a/src/API/Google/AdsCampaignCriterion.php b/src/API/Google/AdsCampaignCriterion.php index d3e77e3eac..9ab50584b8 100644 --- a/src/API/Google/AdsCampaignCriterion.php +++ b/src/API/Google/AdsCampaignCriterion.php @@ -3,12 +3,12 @@ namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Google; -use Google\Ads\GoogleAds\Util\V20\ResourceNames; -use Google\Ads\GoogleAds\V20\Common\LocationInfo; -use Google\Ads\GoogleAds\V20\Enums\CampaignCriterionStatusEnum\CampaignCriterionStatus; -use Google\Ads\GoogleAds\V20\Resources\CampaignCriterion; -use Google\Ads\GoogleAds\V20\Services\CampaignCriterionOperation; -use Google\Ads\GoogleAds\V20\Services\MutateOperation; +use Google\Ads\GoogleAds\Util\V21\ResourceNames; +use Google\Ads\GoogleAds\V21\Common\LocationInfo; +use Google\Ads\GoogleAds\V21\Enums\CampaignCriterionStatusEnum\CampaignCriterionStatus; +use Google\Ads\GoogleAds\V21\Resources\CampaignCriterion; +use Google\Ads\GoogleAds\V21\Services\CampaignCriterionOperation; +use Google\Ads\GoogleAds\V21\Services\MutateOperation; /** * Class AdsCampaignCriterion diff --git a/src/API/Google/AdsCampaignLabel.php b/src/API/Google/AdsCampaignLabel.php index cc50a0dc6b..528fc0d0c3 100644 --- a/src/API/Google/AdsCampaignLabel.php +++ b/src/API/Google/AdsCampaignLabel.php @@ -7,13 +7,13 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Google\Ads\GoogleAdsClient; use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface; use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait; -use Google\Ads\GoogleAds\Util\V20\ResourceNames; -use Google\Ads\GoogleAds\V20\Resources\Label; -use Google\Ads\GoogleAds\V20\Resources\CampaignLabel; -use Google\Ads\GoogleAds\V20\Services\LabelOperation; -use Google\Ads\GoogleAds\V20\Services\CampaignLabelOperation; -use Google\Ads\GoogleAds\V20\Services\MutateOperation; -use Google\Ads\GoogleAds\V20\Services\MutateGoogleAdsRequest; +use Google\Ads\GoogleAds\Util\V21\ResourceNames; +use Google\Ads\GoogleAds\V21\Resources\Label; +use Google\Ads\GoogleAds\V21\Resources\CampaignLabel; +use Google\Ads\GoogleAds\V21\Services\LabelOperation; +use Google\Ads\GoogleAds\V21\Services\CampaignLabelOperation; +use Google\Ads\GoogleAds\V21\Services\MutateOperation; +use Google\Ads\GoogleAds\V21\Services\MutateGoogleAdsRequest; /** * Class AdsCampaignLabel diff --git a/src/API/Google/AdsConversionAction.php b/src/API/Google/AdsConversionAction.php index 42dbe04e5b..21e182d60e 100644 --- a/src/API/Google/AdsConversionAction.php +++ b/src/API/Google/AdsConversionAction.php @@ -8,19 +8,19 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface; use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait; use Exception; -use Google\Ads\GoogleAds\V20\Common\TagSnippet; -use Google\Ads\GoogleAds\V20\Enums\ConversionActionCategoryEnum\ConversionActionCategory; -use Google\Ads\GoogleAds\V20\Enums\ConversionActionStatusEnum\ConversionActionStatus; -use Google\Ads\GoogleAds\V20\Enums\ConversionActionTypeEnum\ConversionActionType; -use Google\Ads\GoogleAds\V20\Enums\TrackingCodePageFormatEnum\TrackingCodePageFormat; -use Google\Ads\GoogleAds\V20\Enums\TrackingCodeTypeEnum\TrackingCodeType; -use Google\Ads\GoogleAds\V20\Resources\ConversionAction; -use Google\Ads\GoogleAds\V20\Resources\ConversionAction\ValueSettings; -use Google\Ads\GoogleAds\V20\Services\ConversionActionOperation; -use Google\Ads\GoogleAds\V20\Services\Client\ConversionActionServiceClient; -use Google\Ads\GoogleAds\V20\Services\GoogleAdsRow; -use Google\Ads\GoogleAds\V20\Services\MutateConversionActionResult; -use Google\Ads\GoogleAds\V20\Services\MutateConversionActionsRequest; +use Google\Ads\GoogleAds\V21\Common\TagSnippet; +use Google\Ads\GoogleAds\V21\Enums\ConversionActionCategoryEnum\ConversionActionCategory; +use Google\Ads\GoogleAds\V21\Enums\ConversionActionStatusEnum\ConversionActionStatus; +use Google\Ads\GoogleAds\V21\Enums\ConversionActionTypeEnum\ConversionActionType; +use Google\Ads\GoogleAds\V21\Enums\TrackingCodePageFormatEnum\TrackingCodePageFormat; +use Google\Ads\GoogleAds\V21\Enums\TrackingCodeTypeEnum\TrackingCodeType; +use Google\Ads\GoogleAds\V21\Resources\ConversionAction; +use Google\Ads\GoogleAds\V21\Resources\ConversionAction\ValueSettings; +use Google\Ads\GoogleAds\V21\Services\ConversionActionOperation; +use Google\Ads\GoogleAds\V21\Services\Client\ConversionActionServiceClient; +use Google\Ads\GoogleAds\V21\Services\GoogleAdsRow; +use Google\Ads\GoogleAds\V21\Services\MutateConversionActionResult; +use Google\Ads\GoogleAds\V21\Services\MutateConversionActionsRequest; use Google\ApiCore\ApiException; /** diff --git a/src/API/Google/AdsReport.php b/src/API/Google/AdsReport.php index 5c257c97ae..1f05af7161 100644 --- a/src/API/Google/AdsReport.php +++ b/src/API/Google/AdsReport.php @@ -15,8 +15,8 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface; use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait; use DateTime; -use Google\Ads\GoogleAds\V20\Common\Segments; -use Google\Ads\GoogleAds\V20\Services\GoogleAdsRow; +use Google\Ads\GoogleAds\V21\Common\Segments; +use Google\Ads\GoogleAds\V21\Services\GoogleAdsRow; use Google\ApiCore\ApiException; /** diff --git a/src/API/Google/AssetFieldType.php b/src/API/Google/AssetFieldType.php index ed152c9aee..dbb782f5d4 100644 --- a/src/API/Google/AssetFieldType.php +++ b/src/API/Google/AssetFieldType.php @@ -3,7 +3,7 @@ namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Google; -use Google\Ads\GoogleAds\V20\Enums\AssetFieldTypeEnum\AssetFieldType as AdsAssetFieldType; +use Google\Ads\GoogleAds\V21\Enums\AssetFieldTypeEnum\AssetFieldType as AdsAssetFieldType; use Automattic\WooCommerce\GoogleListingsAndAds\Internal\StatusMapping; use UnexpectedValueException; diff --git a/src/API/Google/BillingSetupStatus.php b/src/API/Google/BillingSetupStatus.php index 9bcfdbd32d..bb092e9a3a 100644 --- a/src/API/Google/BillingSetupStatus.php +++ b/src/API/Google/BillingSetupStatus.php @@ -3,7 +3,7 @@ namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Google; -use Google\Ads\GoogleAds\V20\Enums\BillingSetupStatusEnum\BillingSetupStatus as AdsBillingSetupStatus; +use Google\Ads\GoogleAds\V21\Enums\BillingSetupStatusEnum\BillingSetupStatus as AdsBillingSetupStatus; use Automattic\WooCommerce\GoogleListingsAndAds\Internal\StatusMapping; /** diff --git a/src/API/Google/BudgetMetrics.php b/src/API/Google/BudgetMetrics.php index cde49b9b91..084a71efbb 100644 --- a/src/API/Google/BudgetMetrics.php +++ b/src/API/Google/BudgetMetrics.php @@ -12,14 +12,14 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Options\TransientsAwareTrait; use Automattic\WooCommerce\GoogleListingsAndAds\Options\TransientsInterface; use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper; -use Google\Ads\GoogleAds\V20\Enums\AdvertisingChannelTypeEnum\AdvertisingChannelType; -use Google\Ads\GoogleAds\V20\Enums\BiddingStrategyTypeEnum\BiddingStrategyType; -use Google\Ads\GoogleAds\V20\Enums\RecommendationTypeEnum\RecommendationType; -use Google\Ads\GoogleAds\V20\Resources\Recommendation\CampaignBudgetRecommendation; -use Google\Ads\GoogleAds\V20\Services\GenerateRecommendationsRequest; -use Google\Ads\GoogleAds\V20\Services\GenerateRecommendationsRequest\AssetGroupInfo; -use Google\Ads\GoogleAds\V20\Services\GenerateRecommendationsRequest\BiddingInfo; -use Google\Ads\GoogleAds\V20\Services\GenerateRecommendationsRequest\BudgetInfo; +use Google\Ads\GoogleAds\V21\Enums\AdvertisingChannelTypeEnum\AdvertisingChannelType; +use Google\Ads\GoogleAds\V21\Enums\BiddingStrategyTypeEnum\BiddingStrategyType; +use Google\Ads\GoogleAds\V21\Enums\RecommendationTypeEnum\RecommendationType; +use Google\Ads\GoogleAds\V21\Resources\Recommendation\CampaignBudgetRecommendation; +use Google\Ads\GoogleAds\V21\Services\GenerateRecommendationsRequest; +use Google\Ads\GoogleAds\V21\Services\GenerateRecommendationsRequest\AssetGroupInfo; +use Google\Ads\GoogleAds\V21\Services\GenerateRecommendationsRequest\BiddingInfo; +use Google\Ads\GoogleAds\V21\Services\GenerateRecommendationsRequest\BudgetInfo; use Google\ApiCore\ApiException; /** diff --git a/src/API/Google/BudgetRecommendations.php b/src/API/Google/BudgetRecommendations.php index c2181c5c21..7611d955fd 100644 --- a/src/API/Google/BudgetRecommendations.php +++ b/src/API/Google/BudgetRecommendations.php @@ -12,13 +12,13 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Options\TransientsAwareTrait; use Automattic\WooCommerce\GoogleListingsAndAds\Options\TransientsInterface; use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper; -use Google\Ads\GoogleAds\V20\Enums\AdvertisingChannelTypeEnum\AdvertisingChannelType; -use Google\Ads\GoogleAds\V20\Enums\BiddingStrategyTypeEnum\BiddingStrategyType; -use Google\Ads\GoogleAds\V20\Enums\RecommendationTypeEnum\RecommendationType; -use Google\Ads\GoogleAds\V20\Resources\Recommendation\CampaignBudgetRecommendation; -use Google\Ads\GoogleAds\V20\Services\GenerateRecommendationsRequest; -use Google\Ads\GoogleAds\V20\Services\GenerateRecommendationsRequest\AssetGroupInfo; -use Google\Ads\GoogleAds\V20\Services\GenerateRecommendationsRequest\BiddingInfo; +use Google\Ads\GoogleAds\V21\Enums\AdvertisingChannelTypeEnum\AdvertisingChannelType; +use Google\Ads\GoogleAds\V21\Enums\BiddingStrategyTypeEnum\BiddingStrategyType; +use Google\Ads\GoogleAds\V21\Enums\RecommendationTypeEnum\RecommendationType; +use Google\Ads\GoogleAds\V21\Resources\Recommendation\CampaignBudgetRecommendation; +use Google\Ads\GoogleAds\V21\Services\GenerateRecommendationsRequest; +use Google\Ads\GoogleAds\V21\Services\GenerateRecommendationsRequest\AssetGroupInfo; +use Google\Ads\GoogleAds\V21\Services\GenerateRecommendationsRequest\BiddingInfo; use Google\ApiCore\ApiException; /** diff --git a/src/API/Google/CallToActionType.php b/src/API/Google/CallToActionType.php index faab4e5403..966b9f36f8 100644 --- a/src/API/Google/CallToActionType.php +++ b/src/API/Google/CallToActionType.php @@ -3,7 +3,7 @@ namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Google; -use Google\Ads\GoogleAds\V20\Enums\CallToActionTypeEnum\CallToActionType as AdsCallToActionType; +use Google\Ads\GoogleAds\V21\Enums\CallToActionTypeEnum\CallToActionType as AdsCallToActionType; use Automattic\WooCommerce\GoogleListingsAndAds\Internal\StatusMapping; diff --git a/src/API/Google/CampaignStatus.php b/src/API/Google/CampaignStatus.php index 3a59b8595f..2928951e0a 100644 --- a/src/API/Google/CampaignStatus.php +++ b/src/API/Google/CampaignStatus.php @@ -3,7 +3,7 @@ namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Google; -use Google\Ads\GoogleAds\V20\Enums\CampaignStatusEnum\CampaignStatus as AdsCampaignStatus; +use Google\Ads\GoogleAds\V21\Enums\CampaignStatusEnum\CampaignStatus as AdsCampaignStatus; use Automattic\WooCommerce\GoogleListingsAndAds\Internal\StatusMapping; /** diff --git a/src/API/Google/CampaignType.php b/src/API/Google/CampaignType.php index 447178591a..03f28f179d 100644 --- a/src/API/Google/CampaignType.php +++ b/src/API/Google/CampaignType.php @@ -3,7 +3,7 @@ namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Google; -use Google\Ads\GoogleAds\V20\Enums\AdvertisingChannelTypeEnum\AdvertisingChannelType as AdsCampaignType; +use Google\Ads\GoogleAds\V21\Enums\AdvertisingChannelTypeEnum\AdvertisingChannelType as AdsCampaignType; use Automattic\WooCommerce\GoogleListingsAndAds\Internal\StatusMapping; /** diff --git a/src/API/Google/MerchantMetrics.php b/src/API/Google/MerchantMetrics.php index 9a73125659..d879561bd3 100644 --- a/src/API/Google/MerchantMetrics.php +++ b/src/API/Google/MerchantMetrics.php @@ -15,7 +15,7 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\SearchResponse; use DateTime; use Exception; -use Google\Ads\GoogleAds\V20\Services\GoogleAdsRow; +use Google\Ads\GoogleAds\V21\Services\GoogleAdsRow; use Google\ApiCore\PagedListResponse; /** diff --git a/src/API/Google/Query/AdsQuery.php b/src/API/Google/Query/AdsQuery.php index 20850e2f2e..9c9e4c2e2f 100644 --- a/src/API/Google/Query/AdsQuery.php +++ b/src/API/Google/Query/AdsQuery.php @@ -5,9 +5,9 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidProperty; use Automattic\WooCommerce\GoogleListingsAndAds\Google\Ads\GoogleAdsClient; -use Google\Ads\GoogleAds\V20\Services\GoogleAdsRow; -use Google\Ads\GoogleAds\V20\Services\SearchGoogleAdsRequest; -use Google\Ads\GoogleAds\V20\Services\SearchSettings; +use Google\Ads\GoogleAds\V21\Services\GoogleAdsRow; +use Google\Ads\GoogleAds\V21\Services\SearchGoogleAdsRequest; +use Google\Ads\GoogleAds\V21\Services\SearchSettings; use Google\ApiCore\ApiException; defined( 'ABSPATH' ) || exit; diff --git a/src/API/Google/Query/AdsReportQuery.php b/src/API/Google/Query/AdsReportQuery.php index 121fde8b9b..dab79aba86 100644 --- a/src/API/Google/Query/AdsReportQuery.php +++ b/src/API/Google/Query/AdsReportQuery.php @@ -3,7 +3,7 @@ namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Query; -use Google\Ads\GoogleAds\V20\Resources\ShoppingPerformanceView; +use Google\Ads\GoogleAds\V21\Resources\ShoppingPerformanceView; defined( 'ABSPATH' ) || exit; diff --git a/src/Ads/AdsRecommendationsService.php b/src/Ads/AdsRecommendationsService.php index f182924653..0338244b52 100644 --- a/src/Ads/AdsRecommendationsService.php +++ b/src/Ads/AdsRecommendationsService.php @@ -15,8 +15,8 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Options\TransientsAwareTrait; use Automattic\WooCommerce\GoogleListingsAndAds\Options\TransientsInterface; use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Exception as GoogleException; -use Google\Ads\GoogleAds\V20\Resources\Recommendation; -use Google\Ads\GoogleAds\V20\Enums\RecommendationTypeEnum\RecommendationType; +use Google\Ads\GoogleAds\V21\Resources\Recommendation; +use Google\Ads\GoogleAds\V21\Enums\RecommendationTypeEnum\RecommendationType; use Exception; defined( 'ABSPATH' ) || exit; diff --git a/src/Google/Ads/ServiceClientFactoryTrait.php b/src/Google/Ads/ServiceClientFactoryTrait.php index 2b9e867bfa..d697b41ada 100644 --- a/src/Google/Ads/ServiceClientFactoryTrait.php +++ b/src/Google/Ads/ServiceClientFactoryTrait.php @@ -13,25 +13,25 @@ use Google\Ads\GoogleAds\Constants; use Google\Ads\GoogleAds\Lib\ConfigurationTrait; -use Google\Ads\GoogleAds\V20\Services\Client\AccountLinkServiceClient; -use Google\Ads\GoogleAds\V20\Services\Client\AdGroupAdLabelServiceClient; -use Google\Ads\GoogleAds\V20\Services\Client\AdGroupAdServiceClient; -use Google\Ads\GoogleAds\V20\Services\Client\AdGroupCriterionServiceClient; -use Google\Ads\GoogleAds\V20\Services\Client\AdGroupServiceClient; -use Google\Ads\GoogleAds\V20\Services\Client\AdServiceClient; -use Google\Ads\GoogleAds\V20\Services\Client\AssetGroupListingGroupFilterServiceClient; -use Google\Ads\GoogleAds\V20\Services\Client\AssetGroupServiceClient; -use Google\Ads\GoogleAds\V20\Services\Client\BillingSetupServiceClient; -use Google\Ads\GoogleAds\V20\Services\Client\CampaignBudgetServiceClient; -use Google\Ads\GoogleAds\V20\Services\Client\CampaignCriterionServiceClient; -use Google\Ads\GoogleAds\V20\Services\Client\CampaignServiceClient; -use Google\Ads\GoogleAds\V20\Services\Client\ConversionActionServiceClient; -use Google\Ads\GoogleAds\V20\Services\Client\CustomerServiceClient; -use Google\Ads\GoogleAds\V20\Services\Client\CustomerUserAccessServiceClient; -use Google\Ads\GoogleAds\V20\Services\Client\GeoTargetConstantServiceClient; -use Google\Ads\GoogleAds\V20\Services\Client\GoogleAdsServiceClient; -use Google\Ads\GoogleAds\V20\Services\Client\ProductLinkInvitationServiceClient; -use Google\Ads\GoogleAds\V20\Services\Client\RecommendationServiceClient; +use Google\Ads\GoogleAds\V21\Services\Client\AccountLinkServiceClient; +use Google\Ads\GoogleAds\V21\Services\Client\AdGroupAdLabelServiceClient; +use Google\Ads\GoogleAds\V21\Services\Client\AdGroupAdServiceClient; +use Google\Ads\GoogleAds\V21\Services\Client\AdGroupCriterionServiceClient; +use Google\Ads\GoogleAds\V21\Services\Client\AdGroupServiceClient; +use Google\Ads\GoogleAds\V21\Services\Client\AdServiceClient; +use Google\Ads\GoogleAds\V21\Services\Client\AssetGroupListingGroupFilterServiceClient; +use Google\Ads\GoogleAds\V21\Services\Client\AssetGroupServiceClient; +use Google\Ads\GoogleAds\V21\Services\Client\BillingSetupServiceClient; +use Google\Ads\GoogleAds\V21\Services\Client\CampaignBudgetServiceClient; +use Google\Ads\GoogleAds\V21\Services\Client\CampaignCriterionServiceClient; +use Google\Ads\GoogleAds\V21\Services\Client\CampaignServiceClient; +use Google\Ads\GoogleAds\V21\Services\Client\ConversionActionServiceClient; +use Google\Ads\GoogleAds\V21\Services\Client\CustomerServiceClient; +use Google\Ads\GoogleAds\V21\Services\Client\CustomerUserAccessServiceClient; +use Google\Ads\GoogleAds\V21\Services\Client\GeoTargetConstantServiceClient; +use Google\Ads\GoogleAds\V21\Services\Client\GoogleAdsServiceClient; +use Google\Ads\GoogleAds\V21\Services\Client\ProductLinkInvitationServiceClient; +use Google\Ads\GoogleAds\V21\Services\Client\RecommendationServiceClient; /** * Contains service client factory methods. diff --git a/src/Internal/DependencyManagement/GoogleServiceProvider.php b/src/Internal/DependencyManagement/GoogleServiceProvider.php index 0ddf8f7321..f2715c8d9d 100644 --- a/src/Internal/DependencyManagement/GoogleServiceProvider.php +++ b/src/Internal/DependencyManagement/GoogleServiceProvider.php @@ -49,7 +49,7 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\League\Container\Definition\Definition; use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Psr\Http\Message\RequestInterface; use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Psr\Http\Message\ResponseInterface; -use Google\Ads\GoogleAds\Util\V20\GoogleAdsFailures; +use Google\Ads\GoogleAds\Util\V21\GoogleAdsFailures; use Jetpack_Options; defined( 'ABSPATH' ) || exit; diff --git a/tests/Tools/HelperTrait/GoogleAdsClientTrait.php b/tests/Tools/HelperTrait/GoogleAdsClientTrait.php index 2b9dc2a68c..02534244cc 100644 --- a/tests/Tools/HelperTrait/GoogleAdsClientTrait.php +++ b/tests/Tools/HelperTrait/GoogleAdsClientTrait.php @@ -10,62 +10,62 @@ use Automattic\WooCommerce\GoogleListingsAndAds\API\MicroTrait; use Automattic\WooCommerce\GoogleListingsAndAds\Google\Ads\GoogleAdsClient; use Exception; -use Google\Ads\GoogleAds\Util\V20\ResourceNames; -use Google\Ads\GoogleAds\V20\Common\LocationInfo; -use Google\Ads\GoogleAds\V20\Common\Metrics; -use Google\Ads\GoogleAds\V20\Common\Segments; -use Google\Ads\GoogleAds\V20\Common\TagSnippet; -use Google\Ads\GoogleAds\V20\Common\ImageAsset; -use Google\Ads\GoogleAds\V20\Common\TextAsset; -use Google\Ads\GoogleAds\V20\Common\CallToActionAsset; -use Google\Ads\GoogleAds\V20\Common\ImageDimension; -use Google\Ads\GoogleAds\V20\Common\YoutubeVideoAsset; -use Google\Ads\GoogleAds\V20\Enums\AccessRoleEnum\AccessRole; -use Google\Ads\GoogleAds\V20\Enums\CampaignStatusEnum\CampaignStatus as AdsCampaignStatus; -use Google\Ads\GoogleAds\V20\Enums\AdvertisingChannelTypeEnum\AdvertisingChannelType as AdsCampaignType; -use Google\Ads\GoogleAds\V20\Enums\AssetTypeEnum\AssetType; -use Google\Ads\GoogleAds\V20\Enums\TrackingCodePageFormatEnum\TrackingCodePageFormat; -use Google\Ads\GoogleAds\V20\Enums\TrackingCodeTypeEnum\TrackingCodeType; -use Google\Ads\GoogleAds\V20\Resources\BillingSetup; -use Google\Ads\GoogleAds\V20\Resources\Campaign; -use Google\Ads\GoogleAds\V20\Resources\Label; -use Google\Ads\GoogleAds\V20\Resources\Asset; -use Google\Ads\GoogleAds\V20\Resources\AssetGroup; -use Google\Ads\GoogleAds\V20\Resources\AssetGroupAsset; -use Google\Ads\GoogleAds\V20\Services\AssetGroupAssetOperation; -use Google\Ads\GoogleAds\V20\Resources\CampaignBudget; -use Google\Ads\GoogleAds\V20\Resources\CampaignCriterion; -use Google\Ads\GoogleAds\V20\Resources\Campaign\ShoppingSetting; -use Google\Ads\GoogleAds\V20\Resources\ConversionAction; -use Google\Ads\GoogleAds\V20\Resources\Customer; -use Google\Ads\GoogleAds\V20\Resources\CustomerUserAccess; -use Google\Ads\GoogleAds\V20\Resources\GeoTargetConstant; -use Google\Ads\GoogleAds\V20\Resources\Recommendation; -use Google\Ads\GoogleAds\V20\Resources\Recommendation\CampaignBudgetRecommendation; -use Google\Ads\GoogleAds\V20\Resources\Recommendation\CampaignBudgetRecommendation\CampaignBudgetRecommendationOption; -use Google\Ads\GoogleAds\V20\Resources\Recommendation\RecommendationImpact; -use Google\Ads\GoogleAds\V20\Resources\Recommendation\RecommendationMetrics; -use Google\Ads\GoogleAds\V20\Resources\ShoppingPerformanceView; -use Google\Ads\GoogleAds\V20\Services\Client\ConversionActionServiceClient; -use Google\Ads\GoogleAds\V20\Services\Client\CustomerServiceClient; -use Google\Ads\GoogleAds\V20\Services\Client\GoogleAdsServiceClient; -use Google\Ads\GoogleAds\V20\Services\Client\ProductLinkInvitationServiceClient; -use Google\Ads\GoogleAds\V20\Services\Client\RecommendationServiceClient; -use Google\Ads\GoogleAds\V20\Services\GenerateRecommendationsResponse; -use Google\Ads\GoogleAds\V20\Services\GoogleAdsRow; -use Google\Ads\GoogleAds\V20\Services\ListAccessibleCustomersResponse; -use Google\Ads\GoogleAds\V20\Services\MutateCampaignResult; -use Google\Ads\GoogleAds\V20\Services\MutateLabelResult; -use Google\Ads\GoogleAds\V20\Services\MutateConversionActionResult; -use Google\Ads\GoogleAds\V20\Services\MutateConversionActionsRequest; -use Google\Ads\GoogleAds\V20\Services\MutateConversionActionsResponse; -use Google\Ads\GoogleAds\V20\Services\MutateGoogleAdsRequest; -use Google\Ads\GoogleAds\V20\Services\MutateGoogleAdsResponse; -use Google\Ads\GoogleAds\V20\Services\MutateOperationResponse; -use Google\Ads\GoogleAds\V20\Services\MutateOperation; -use Google\Ads\GoogleAds\V20\Services\MutateAssetGroupResult; -use Google\Ads\GoogleAds\V20\Services\MutateAssetResult; -use Google\Ads\GoogleAds\V20\Services\SearchGoogleAdsResponse; +use Google\Ads\GoogleAds\Util\V21\ResourceNames; +use Google\Ads\GoogleAds\V21\Common\LocationInfo; +use Google\Ads\GoogleAds\V21\Common\Metrics; +use Google\Ads\GoogleAds\V21\Common\Segments; +use Google\Ads\GoogleAds\V21\Common\TagSnippet; +use Google\Ads\GoogleAds\V21\Common\ImageAsset; +use Google\Ads\GoogleAds\V21\Common\TextAsset; +use Google\Ads\GoogleAds\V21\Common\CallToActionAsset; +use Google\Ads\GoogleAds\V21\Common\ImageDimension; +use Google\Ads\GoogleAds\V21\Common\YoutubeVideoAsset; +use Google\Ads\GoogleAds\V21\Enums\AccessRoleEnum\AccessRole; +use Google\Ads\GoogleAds\V21\Enums\CampaignStatusEnum\CampaignStatus as AdsCampaignStatus; +use Google\Ads\GoogleAds\V21\Enums\AdvertisingChannelTypeEnum\AdvertisingChannelType as AdsCampaignType; +use Google\Ads\GoogleAds\V21\Enums\AssetTypeEnum\AssetType; +use Google\Ads\GoogleAds\V21\Enums\TrackingCodePageFormatEnum\TrackingCodePageFormat; +use Google\Ads\GoogleAds\V21\Enums\TrackingCodeTypeEnum\TrackingCodeType; +use Google\Ads\GoogleAds\V21\Resources\BillingSetup; +use Google\Ads\GoogleAds\V21\Resources\Campaign; +use Google\Ads\GoogleAds\V21\Resources\Label; +use Google\Ads\GoogleAds\V21\Resources\Asset; +use Google\Ads\GoogleAds\V21\Resources\AssetGroup; +use Google\Ads\GoogleAds\V21\Resources\AssetGroupAsset; +use Google\Ads\GoogleAds\V21\Services\AssetGroupAssetOperation; +use Google\Ads\GoogleAds\V21\Resources\CampaignBudget; +use Google\Ads\GoogleAds\V21\Resources\CampaignCriterion; +use Google\Ads\GoogleAds\V21\Resources\Campaign\ShoppingSetting; +use Google\Ads\GoogleAds\V21\Resources\ConversionAction; +use Google\Ads\GoogleAds\V21\Resources\Customer; +use Google\Ads\GoogleAds\V21\Resources\CustomerUserAccess; +use Google\Ads\GoogleAds\V21\Resources\GeoTargetConstant; +use Google\Ads\GoogleAds\V21\Resources\Recommendation; +use Google\Ads\GoogleAds\V21\Resources\Recommendation\CampaignBudgetRecommendation; +use Google\Ads\GoogleAds\V21\Resources\Recommendation\CampaignBudgetRecommendation\CampaignBudgetRecommendationOption; +use Google\Ads\GoogleAds\V21\Resources\Recommendation\RecommendationImpact; +use Google\Ads\GoogleAds\V21\Resources\Recommendation\RecommendationMetrics; +use Google\Ads\GoogleAds\V21\Resources\ShoppingPerformanceView; +use Google\Ads\GoogleAds\V21\Services\Client\ConversionActionServiceClient; +use Google\Ads\GoogleAds\V21\Services\Client\CustomerServiceClient; +use Google\Ads\GoogleAds\V21\Services\Client\GoogleAdsServiceClient; +use Google\Ads\GoogleAds\V21\Services\Client\ProductLinkInvitationServiceClient; +use Google\Ads\GoogleAds\V21\Services\Client\RecommendationServiceClient; +use Google\Ads\GoogleAds\V21\Services\GenerateRecommendationsResponse; +use Google\Ads\GoogleAds\V21\Services\GoogleAdsRow; +use Google\Ads\GoogleAds\V21\Services\ListAccessibleCustomersResponse; +use Google\Ads\GoogleAds\V21\Services\MutateCampaignResult; +use Google\Ads\GoogleAds\V21\Services\MutateLabelResult; +use Google\Ads\GoogleAds\V21\Services\MutateConversionActionResult; +use Google\Ads\GoogleAds\V21\Services\MutateConversionActionsRequest; +use Google\Ads\GoogleAds\V21\Services\MutateConversionActionsResponse; +use Google\Ads\GoogleAds\V21\Services\MutateGoogleAdsRequest; +use Google\Ads\GoogleAds\V21\Services\MutateGoogleAdsResponse; +use Google\Ads\GoogleAds\V21\Services\MutateOperationResponse; +use Google\Ads\GoogleAds\V21\Services\MutateOperation; +use Google\Ads\GoogleAds\V21\Services\MutateAssetGroupResult; +use Google\Ads\GoogleAds\V21\Services\MutateAssetResult; +use Google\Ads\GoogleAds\V21\Services\SearchGoogleAdsResponse; use Google\ApiCore\ApiException; use Google\ApiCore\Page; use Google\ApiCore\PagedListResponse; diff --git a/tests/Unit/API/Google/AdsAssetGroupAssetTest.php b/tests/Unit/API/Google/AdsAssetGroupAssetTest.php index 380264ac28..69247a8622 100644 --- a/tests/Unit/API/Google/AdsAssetGroupAssetTest.php +++ b/tests/Unit/API/Google/AdsAssetGroupAssetTest.php @@ -10,8 +10,8 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Tests\Tools\HelperTrait\GoogleAdsClientTrait; use PHPUnit\Framework\MockObject\MockObject; use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\AssetFieldType; -use Google\Ads\GoogleAds\V20\Enums\AssetTypeEnum\AssetType; -use Google\Ads\GoogleAds\Util\V20\ResourceNames; +use Google\Ads\GoogleAds\V21\Enums\AssetTypeEnum\AssetType; +use Google\Ads\GoogleAds\Util\V21\ResourceNames; defined( 'ABSPATH' ) || exit; diff --git a/tests/Unit/API/Google/AdsAssetGroupTest.php b/tests/Unit/API/Google/AdsAssetGroupTest.php index 2ff00136fc..bb4e192ea5 100644 --- a/tests/Unit/API/Google/AdsAssetGroupTest.php +++ b/tests/Unit/API/Google/AdsAssetGroupTest.php @@ -8,9 +8,9 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsInterface; use Automattic\WooCommerce\GoogleListingsAndAds\Tests\Framework\UnitTest; use Automattic\WooCommerce\GoogleListingsAndAds\Tests\Tools\HelperTrait\GoogleAdsClientTrait; -use Google\Ads\GoogleAds\V20\Enums\AssetGroupStatusEnum\AssetGroupStatus; -use Google\Ads\GoogleAds\V20\Enums\ListingGroupFilterListingSourceEnum\ListingGroupFilterListingSource; -use Google\Ads\GoogleAds\V20\Enums\ListingGroupFilterTypeEnum\ListingGroupFilterType; +use Google\Ads\GoogleAds\V21\Enums\AssetGroupStatusEnum\AssetGroupStatus; +use Google\Ads\GoogleAds\V21\Enums\ListingGroupFilterListingSourceEnum\ListingGroupFilterListingSource; +use Google\Ads\GoogleAds\V21\Enums\ListingGroupFilterTypeEnum\ListingGroupFilterType; use PHPUnit\Framework\MockObject\MockObject; use Google\ApiCore\ApiException; use Automattic\WooCommerce\GoogleListingsAndAds\Exception\ExceptionWithResponseData; diff --git a/tests/Unit/API/Google/AdsAssetTest.php b/tests/Unit/API/Google/AdsAssetTest.php index b897156944..ad9a9fa0e7 100644 --- a/tests/Unit/API/Google/AdsAssetTest.php +++ b/tests/Unit/API/Google/AdsAssetTest.php @@ -10,7 +10,7 @@ use PHPUnit\Framework\MockObject\MockObject; use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\AssetFieldType; use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\CallToActionType; -use Google\Ads\GoogleAds\Util\V20\ResourceNames; +use Google\Ads\GoogleAds\Util\V21\ResourceNames; use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WP; use Exception; use WP_Error; diff --git a/tests/Unit/API/Google/AdsCampaignCriterionTest.php b/tests/Unit/API/Google/AdsCampaignCriterionTest.php index cecdde30e1..8eba9904cd 100644 --- a/tests/Unit/API/Google/AdsCampaignCriterionTest.php +++ b/tests/Unit/API/Google/AdsCampaignCriterionTest.php @@ -6,7 +6,7 @@ use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\AdsCampaignCriterion; use Automattic\WooCommerce\GoogleListingsAndAds\Tests\Framework\UnitTest; use Automattic\WooCommerce\GoogleListingsAndAds\Tests\Tools\HelperTrait\GoogleAdsClientTrait; -use Google\Ads\GoogleAds\V20\Enums\CampaignCriterionStatusEnum\CampaignCriterionStatus; +use Google\Ads\GoogleAds\V21\Enums\CampaignCriterionStatusEnum\CampaignCriterionStatus; defined( 'ABSPATH' ) || exit; diff --git a/tests/Unit/API/Google/AdsConversionActionTest.php b/tests/Unit/API/Google/AdsConversionActionTest.php index d692ee2823..ba4c6ec022 100644 --- a/tests/Unit/API/Google/AdsConversionActionTest.php +++ b/tests/Unit/API/Google/AdsConversionActionTest.php @@ -8,7 +8,7 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Tests\Framework\UnitTest; use Automattic\WooCommerce\GoogleListingsAndAds\Tests\Tools\HelperTrait\GoogleAdsClientTrait; use Exception; -use Google\Ads\GoogleAds\V20\Enums\ConversionActionStatusEnum\ConversionActionStatus; +use Google\Ads\GoogleAds\V21\Enums\ConversionActionStatusEnum\ConversionActionStatus; use Google\ApiCore\ApiException; use PHPUnit\Framework\MockObject\MockObject; diff --git a/tests/Unit/API/Google/AdsTest.php b/tests/Unit/API/Google/AdsTest.php index 24b3bc64f3..ce9ad5be5c 100644 --- a/tests/Unit/API/Google/AdsTest.php +++ b/tests/Unit/API/Google/AdsTest.php @@ -10,10 +10,10 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Tests\Framework\UnitTest; use Automattic\WooCommerce\GoogleListingsAndAds\Tests\Tools\HelperTrait\GoogleAdsClientTrait; use Exception; -use Google\Ads\GoogleAds\V20\Enums\BillingSetupStatusEnum\BillingSetupStatus as AdsBillingSetupStatus; -use Google\Ads\GoogleAds\V20\Enums\ProductLinkInvitationStatusEnum\ProductLinkInvitationStatus; -use Google\Ads\GoogleAds\V20\Resources\MerchantCenterLinkInvitationIdentifier; -use Google\Ads\GoogleAds\V20\Resources\ProductLinkInvitation; +use Google\Ads\GoogleAds\V21\Enums\BillingSetupStatusEnum\BillingSetupStatus as AdsBillingSetupStatus; +use Google\Ads\GoogleAds\V21\Enums\ProductLinkInvitationStatusEnum\ProductLinkInvitationStatus; +use Google\Ads\GoogleAds\V21\Resources\MerchantCenterLinkInvitationIdentifier; +use Google\Ads\GoogleAds\V21\Resources\ProductLinkInvitation; use Google\ApiCore\ApiException; use PHPUnit\Framework\MockObject\MockObject; diff --git a/tests/Unit/API/Google/MerchantMetricsTest.php b/tests/Unit/API/Google/MerchantMetricsTest.php index 841134a23f..8713929ac5 100644 --- a/tests/Unit/API/Google/MerchantMetricsTest.php +++ b/tests/Unit/API/Google/MerchantMetricsTest.php @@ -16,9 +16,9 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\SearchResponse; use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\Resource\Reports; use DateTime; -use Google\Ads\GoogleAds\V20\Common\Metrics as AdMetrics; -use Google\Ads\GoogleAds\V20\Services\GoogleAdsRow; -use Google\Ads\GoogleAds\V20\Services\Client\GoogleAdsServiceClient; +use Google\Ads\GoogleAds\V21\Common\Metrics as AdMetrics; +use Google\Ads\GoogleAds\V21\Services\GoogleAdsRow; +use Google\Ads\GoogleAds\V21\Services\Client\GoogleAdsServiceClient; use Google\ApiCore\Page; use Google\ApiCore\PagedListResponse; use PHPUnit\Framework\MockObject\MockObject; From 159641e766c114ddcaa2d5f74261cee3417c485f Mon Sep 17 00:00:00 2001 From: Ankit Gade Date: Tue, 13 Jan 2026 21:14:06 +0530 Subject: [PATCH 009/123] Update bundle size. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2d40b2594f..53b307b1fa 100644 --- a/package.json +++ b/package.json @@ -168,7 +168,7 @@ }, { "path": "./google-listings-and-ads.zip", - "maxSize": "8.22 mB", + "maxSize": "8.26 mB", "compression": "none" } ], From 281ca3dddb0f352e80c950886ed507552ed2bf2c Mon Sep 17 00:00:00 2001 From: asvinb Date: Tue, 13 Jan 2026 20:08:49 +0400 Subject: [PATCH 010/123] feat(paid-ads): Add AI-generated text assets and improve UI --- .../asset-group-text-section.js | 2 + .../asset-item-action-button.js | 4 +- .../asset-item-action-button.scss | 5 + .../asset-group-editor/texts-editor.js | 103 ++++++++++++++++++ 4 files changed, 113 insertions(+), 1 deletion(-) diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/asset-group-text-section.js b/js/src/components/paid-ads/asset-group/asset-group-editor/asset-group-text-section.js index 5e3a18203a..7e8bf864e7 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/asset-group-text-section.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/asset-group-text-section.js @@ -129,6 +129,8 @@ const AssetGroupTextSection = ( { maxCharacterCounts={ spec.maxCharacterCounts } placeholder={ spec.capitalizedName } addButtonText={ spec.addButtonText } + finalUrl={ finalUrl } + assetKey={ spec.key } generateButtonPluralText={ spec.generateButtonPluralText } diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/asset-item-action-button.js b/js/src/components/paid-ads/asset-group/asset-group-editor/asset-item-action-button.js index f1bf38cf79..50ccff0594 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/asset-item-action-button.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/asset-item-action-button.js @@ -22,14 +22,16 @@ const ACTION_ICONS = { export default function AssetItemActionButton( { action = ACTION_TYPES.ADD, + loading, ...props } ) { return ( ); diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/asset-item-action-button.scss b/js/src/components/paid-ads/asset-group/asset-group-editor/asset-item-action-button.scss index bb73355127..e0ec55f797 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/asset-item-action-button.scss +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/asset-item-action-button.scss @@ -2,4 +2,9 @@ &.has-icon { padding: $grid-unit-05; } + + .woocommerce-spinner { + width: $spinner-size; + height: $spinner-size; + } } diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js b/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js index 1eac6ec600..2d1f7deabc 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js @@ -9,6 +9,8 @@ import GridiconCrossSmall from 'gridicons/dist/cross-small'; /** * Internal dependencies */ +import { useAppDispatch } from '~/data'; +import useDispatchCoreNotices from '~/hooks/useDispatchCoreNotices'; import AppButton from '~/components/app-button'; import AppInputControl from '~/components/app-input-control'; import AssetItemActionButton, { @@ -26,10 +28,67 @@ function normalizeNumberOfTexts( texts, minNumberOfTexts, maxNumberOfTexts ) { return texts.concat( supplement ).slice( ...sliceArgs ); } +/** + * Result returned by fillEmptyAssetSlotsWithUniqueValues. + * + * @typedef {Object} FillEmptyAssetSlotsResult + * @property {string[]} assets Updated asset list. + * @property {number} updatedCount Number of empty ("") slots that were filled. + */ + +/** + * Fill empty asset slots (represented by empty strings "") with unique + * generated values. + * + * Existing non-empty values are preserved. + * Empty slots that cannot be filled remain as "". + * + * @param {string[]} currentAssets Current asset values, where "" represents an empty slot. + * @param {string[]} generatedAssets Newly generated candidate asset values. + * + * @return {FillEmptyAssetSlotsResult} Result containing updated assets and count of filled slots. + */ +export function fillEmptyAssetSlotsWithUniqueValues( + currentAssets, + generatedAssets +) { + const existingAssetValues = new Set( currentAssets.filter( Boolean ) ); + + let generatedIndex = 0; + let updatedCount = 0; + + const assets = currentAssets.map( ( assetValue ) => { + if ( assetValue !== '' ) { + return assetValue; + } + + while ( + generatedIndex < generatedAssets.length && + existingAssetValues.has( generatedAssets[ generatedIndex ] ) + ) { + generatedIndex++; + } + + if ( generatedIndex < generatedAssets.length ) { + const nextGeneratedValue = generatedAssets[ generatedIndex ]; + existingAssetValues.add( nextGeneratedValue ); + generatedIndex++; + updatedCount++; + return nextGeneratedValue; + } + + return ''; + } ); + + return { assets, updatedCount }; +} + /** * Renders a list of text inputs for managing the single type of asset texts. * * @param {Object} props React props. + * @param {string} props.assetKey Key of the text asset. + * @param {string} props.finalUrl The final URL for the ad. * @param {string[]} [props.initialTexts=[]] Initial texts. * @param {number} [props.minNumberOfTexts=0] Minimum number of texts. * @param {number} [props.maxNumberOfTexts=0] Maximum number of texts. @@ -42,6 +101,8 @@ function normalizeNumberOfTexts( texts, minNumberOfTexts, maxNumberOfTexts ) { * @param {(texts: Array) => void} [props.onChange] Callback function to be called when the texts are changed. */ export default function TextsEditor( { + assetKey, + finalUrl, initialTexts = [], minNumberOfTexts = 0, maxNumberOfTexts = 0, @@ -54,7 +115,10 @@ export default function TextsEditor( { onChange = noop, } ) { const updateTextsRef = useRef(); + const { createNotice } = useDispatchCoreNotices(); + const { fetchGenAITextAssets } = useAppDispatch(); const [ texts, setTexts ] = useState( initialTexts ); + const [ isGeneratingAssets, setIsGeneratingAssets ] = useState( false ); const updateTexts = ( nextTexts ) => { setTexts( nextTexts ); @@ -95,6 +159,43 @@ export default function TextsEditor( { updateTexts( texts.concat( '' ) ); }; + const handleGenerateClick = async () => { + setIsGeneratingAssets( true ); + + try { + const response = await fetchGenAITextAssets( finalUrl, assetKey ); + const generatedTextAssets = response?.data?.[ assetKey ] ?? []; + + const { assets: updatedTexts, updatedCount } = + fillEmptyAssetSlotsWithUniqueValues( + texts, + generatedTextAssets + ); + + if ( updatedCount > 0 ) { + updateTexts( updatedTexts ); + } else { + createNotice( + 'info', + __( + 'No texts were generated. Please try again.', + 'google-listings-and-ads' + ) + ); + } + } catch ( error ) { + createNotice( + 'error', + __( + 'Something went wrong while generating texts. Please try again.', + 'google-listings-and-ads' + ) + ); + } finally { + setIsGeneratingAssets( false ); + } + }; + const normalizedMaxCharacterCounts = [ maxCharacterCounts ].flat(); const emptyFieldsCount = texts.filter( ( value ) => value === '' ).length; let generateButtonText; @@ -164,6 +265,8 @@ export default function TextsEditor( { ) }
From b415f3f26196769b9da97d71aace345a7366ad97 Mon Sep 17 00:00:00 2001 From: asvinb Date: Wed, 14 Jan 2026 16:37:52 +0400 Subject: [PATCH 011/123] Use existing Tip component and remove GenAIImagesNotice component. --- .../asset-group-images-section.js | 81 +++++++++++-------- .../paid-ads/gen-ai-images-notice.js | 34 -------- .../paid-ads/gen-ai-images-notice.scss | 12 --- .../gen-ai-images-notice.svg | 5 -- 4 files changed, 49 insertions(+), 83 deletions(-) delete mode 100644 js/src/components/paid-ads/gen-ai-images-notice.js delete mode 100644 js/src/components/paid-ads/gen-ai-images-notice.scss delete mode 100644 js/src/images/pmax-assets-improvements/gen-ai-images-notice.svg diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/asset-group-images-section.js b/js/src/components/paid-ads/asset-group/asset-group-editor/asset-group-images-section.js index 76a6afcf3b..6bd7c5701d 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/asset-group-images-section.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/asset-group-images-section.js @@ -2,6 +2,7 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; +import { Flex, FlexItem, Tip } from '@wordpress/components'; /** * Internal dependencies @@ -11,7 +12,6 @@ import ImagesSelector from './images-selector'; import AssetField from './asset-field'; import Section from '~/components/section'; import AppDocumentationLink from '~/components/app-documentation-link'; -import GenAIImagesNotice from '../../gen-ai-images-notice'; import { ASSET_IMAGE_SPECS } from '../../assetSpecs'; /** @@ -34,7 +34,8 @@ const AssetGroupImagesSection = ( { getNumOfIssues, renderErrors, } ) => { - const { values, getInputProps } = useAdaptiveFormContext(); + const { values, getInputProps, adapter } = useAdaptiveFormContext(); + const showTip = adapter.hasImportedAssets; return (
} > -
- { ASSET_IMAGE_SPECS.map( ( spec ) => { - const initialImageUrls = initialValues[ spec.key ]; - const imageProps = getInputProps( spec.key ); - - return ( - - + { showTip && ( + + + { __( + "We've used your final URL to auto-populate images…", + 'google-listings-and-ads' ) } - imageConfig={ spec.imageConfig } - onChange={ imageProps.onChange } - > - { renderErrors( spec.key ) } - - - ); - } ) } + + + ) } + + + { ASSET_IMAGE_SPECS.map( ( spec ) => { + const initialImageUrls = initialValues[ spec.key ]; + const imageProps = getInputProps( spec.key ); + + return ( + + + { renderErrors( spec.key ) } + + + ); + } ) } + +
); diff --git a/js/src/components/paid-ads/gen-ai-images-notice.js b/js/src/components/paid-ads/gen-ai-images-notice.js deleted file mode 100644 index fddb8b2bbe..0000000000 --- a/js/src/components/paid-ads/gen-ai-images-notice.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; -import { Notice } from '@wordpress/components'; - -/** - * Internal dependencies - */ -import GenAIImagesNoticeGraphic from '~/images/pmax-assets-improvements/gen-ai-images-notice.svg'; -import './gen-ai-images-notice.scss'; - -const GenAIImagesNotice = () => { - return ( - - - { __( - "We've used your final URL to auto-populate images…", - 'google-listings-and-ads' - ) } - - ); -}; - -export default GenAIImagesNotice; diff --git a/js/src/components/paid-ads/gen-ai-images-notice.scss b/js/src/components/paid-ads/gen-ai-images-notice.scss deleted file mode 100644 index da12e86de3..0000000000 --- a/js/src/components/paid-ads/gen-ai-images-notice.scss +++ /dev/null @@ -1,12 +0,0 @@ -.gla-gen-ai-images-notice { - background-color: #f0f6fc; - border-left: none; - border: 1px solid #c5d9ed; - font-family: "SF Pro Text", $default-font; - - :where(.components-notice__content) { - display: flex; - align-items: center; - gap: 12px; - } -} diff --git a/js/src/images/pmax-assets-improvements/gen-ai-images-notice.svg b/js/src/images/pmax-assets-improvements/gen-ai-images-notice.svg deleted file mode 100644 index 4470ea76b2..0000000000 --- a/js/src/images/pmax-assets-improvements/gen-ai-images-notice.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - From 8a0cb5618f6101f50baad5a0b19d028d05a1361c Mon Sep 17 00:00:00 2001 From: asvinb Date: Wed, 14 Jan 2026 18:11:05 +0400 Subject: [PATCH 012/123] feat(data): Add adaptGenAIAssets function and refactor GenAI media/text asset fetching logic --- js/src/data/actions.js | 41 ++++++++++------------------------------- js/src/data/adapters.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 31 deletions(-) diff --git a/js/src/data/actions.js b/js/src/data/actions.js index 57a90d9af2..a38ae2e7bb 100644 --- a/js/src/data/actions.js +++ b/js/src/data/actions.js @@ -15,7 +15,7 @@ import { EMPTY_ASSET_ENTITY_GROUP, } from './constants'; import { handleApiError } from '~/utils/handleError'; -import { adaptAdsCampaign } from './adapters'; +import { adaptAdsCampaign, adaptGenAIAssets } from './adapters'; import { isWCIos, isWCAndroid } from '~/utils/isMobileApp'; import { convertKeysFromSnakeCaseToCamelCase } from './utils'; @@ -1303,22 +1303,10 @@ export function* fetchGenAIMediaAssets( url, assetType ) { }, } ); - const items = response.items || []; - const formattedData = items.reduce( - ( accumulator, { temporary_image_url, type } ) => { - if ( assetType && type !== assetType ) { - return accumulator; - } - - if ( ! temporary_image_url ) { - return accumulator; - } - - accumulator[ type ] = accumulator[ type ] || []; - accumulator[ type ].push( temporary_image_url ); - return accumulator; - }, - {} + const formattedData = adaptGenAIAssets( + response.items, + 'temporary_image_url', + assetType ); return { @@ -1356,20 +1344,11 @@ export function* fetchGenAITextAssets( url, assetType ) { }, } ); - const items = response.items || []; - const formattedData = items.reduce( ( accumulator, { text, type } ) => { - if ( assetType && type !== assetType ) { - return accumulator; - } - - if ( ! text ) { - return accumulator; - } - - accumulator[ type ] = accumulator[ type ] || []; - accumulator[ type ].push( text ); - return accumulator; - }, {} ); + const formattedData = adaptGenAIAssets( + response.items, + 'text', + assetType + ); return { type: TYPES.RECEIVE_GEN_AI_TEXT_ASSETS, diff --git a/js/src/data/adapters.js b/js/src/data/adapters.js index e4d8d8097b..35a51d73a6 100644 --- a/js/src/data/adapters.js +++ b/js/src/data/adapters.js @@ -262,3 +262,33 @@ export function adaptRaiseAdsBudgetRecommendations( rawData ) { return finalData; } + +/** + * Formats raw API items into a grouped object by type. + * @param {Array} items The raw items array from API. + * @param {string} valueKey The key to extract (e.g., 'text' or 'temporary_image_url'). + * @param {string} [filterType] Optional type to filter by. + * @return {Object} Groups of assets keyed by their type. + */ +export function adaptGenAIAssets( items = [], valueKey, filterType ) { + const data = {}; + + for ( const item of items ) { + const { type, [ valueKey ]: value } = item; + + // Skip if: + // 1. We have a filter and it doesn't match + // 2. The value for the specified key is empty/null + if ( ( filterType && type !== filterType ) || ! value ) { + continue; + } + + if ( ! data[ type ] ) { + data[ type ] = []; + } + + data[ type ].push( value ); + } + + return data; +} From 40d6b1a2679804aa2b4a9ce9ee5ac427bd442909 Mon Sep 17 00:00:00 2001 From: Ankit Gade Date: Thu, 15 Jan 2026 15:13:39 +0530 Subject: [PATCH 013/123] Update google ads library. --- composer.json | 2 +- composer.lock | 127 +++++++++++++++++++++++++++++--------------------- 2 files changed, 75 insertions(+), 54 deletions(-) diff --git a/composer.json b/composer.json index 4c22d2f0a9..a8a6b3e883 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,7 @@ "ext-json": "*", "google/apiclient": "^2.16", "google/apiclient-services": "^0.350.0", - "googleads/google-ads-php": "dev-legacy-v31.0.1", + "googleads/google-ads-php": "dev-legacy-v31.1.0", "league/container": "^4.2", "league/iso3166": "^4.1", "phpseclib/bcmath_compat": "^2.0", diff --git a/composer.lock b/composer.lock index 6f979bc7be..bee4987b18 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "34bf83c9b42bffe4e88d69b8031e6cfc", + "content-hash": "29f7915131697943e319a56e9a05e9a0", "packages": [ { "name": "firebase/php-jwt", @@ -349,20 +349,20 @@ }, { "name": "google/longrunning", - "version": "0.4.7", + "version": "0.6.0", "source": { "type": "git", "url": "https://github.com/googleapis/php-longrunning.git", - "reference": "624cabb874c10e5ddc9034c999f724894b70a3d3" + "reference": "226d3b5166eaa13754cc5e452b37872478e23375" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/googleapis/php-longrunning/zipball/624cabb874c10e5ddc9034c999f724894b70a3d3", - "reference": "624cabb874c10e5ddc9034c999f724894b70a3d3", + "url": "https://api.github.com/repos/googleapis/php-longrunning/zipball/226d3b5166eaa13754cc5e452b37872478e23375", + "reference": "226d3b5166eaa13754cc5e452b37872478e23375", "shasum": "" }, "require-dev": { - "google/gax": "^1.36.0", + "google/gax": "^1.38.0", "phpunit/phpunit": "^9.0" }, "type": "library", @@ -387,9 +387,9 @@ ], "description": "Google LongRunning Client for PHP", "support": { - "source": "https://github.com/googleapis/php-longrunning/tree/v0.4.7" + "source": "https://github.com/googleapis/php-longrunning/tree/v0.6.0" }, - "time": "2025-01-24T21:24:06+00:00" + "time": "2025-10-07T18:41:09+00:00" }, { "name": "google/protobuf", @@ -437,19 +437,20 @@ }, { "name": "googleads/google-ads-php", - "version": "dev-legacy-v31.0.1", + "version": "dev-legacy-v31.1.0", "source": { "type": "git", "url": "https://github.com/googleads/google-ads-php.git", - "reference": "e217e6176fb5d72ff51f9c47e91b8e71bbf21dfb" + "reference": "73a755b69f8088a22da27fd0124e2a2d43ec7b82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/googleads/google-ads-php/zipball/e217e6176fb5d72ff51f9c47e91b8e71bbf21dfb", - "reference": "e217e6176fb5d72ff51f9c47e91b8e71bbf21dfb", + "url": "https://api.github.com/repos/googleads/google-ads-php/zipball/73a755b69f8088a22da27fd0124e2a2d43ec7b82", + "reference": "73a755b69f8088a22da27fd0124e2a2d43ec7b82", "shasum": "" }, "require": { + "google/auth": "^1.30 || ^2.0", "google/gax": "^1.19.1", "google/protobuf": "^3.21.5 || >=4.26 <=4.30.0", "grpc/grpc": ">=1.36.0 <=1.57.0", @@ -494,28 +495,28 @@ "homepage": "https://github.com/googleads/google-ads-php", "support": { "issues": "https://github.com/googleads/google-ads-php/issues", - "source": "https://github.com/googleads/google-ads-php/tree/legacy-v31.0.1" + "source": "https://github.com/googleads/google-ads-php/tree/legacy-v31.1.0" }, - "time": "2025-08-28T15:13:29+00:00" + "time": "2026-01-09T20:17:46+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "7.9.3", + "version": "7.10.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77" + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", - "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.5.3 || ^2.0.3", - "guzzlehttp/psr7": "^2.7.0", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.2 || ^3.0" @@ -606,7 +607,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.9.3" + "source": "https://github.com/guzzle/guzzle/tree/7.10.0" }, "funding": [ { @@ -622,20 +623,20 @@ "type": "tidelift" } ], - "time": "2025-03-27T13:37:11+00:00" + "time": "2025-08-23T22:36:01+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.2.0", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c" + "reference": "481557b130ef3790cf82b713667b43030dc9c957" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c", - "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", "shasum": "" }, "require": { @@ -643,7 +644,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" + "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "type": "library", "extra": { @@ -689,7 +690,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.2.0" + "source": "https://github.com/guzzle/promises/tree/2.3.0" }, "funding": [ { @@ -705,20 +706,20 @@ "type": "tidelift" } ], - "time": "2025-03-27T13:27:01+00:00" + "time": "2025-08-22T14:34:08+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.7.1", + "version": "2.8.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16" + "reference": "21dc724a0583619cd1652f673303492272778051" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16", - "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", + "reference": "21dc724a0583619cd1652f673303492272778051", "shasum": "" }, "require": { @@ -734,7 +735,7 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "http-interop/http-factory-tests": "0.9.0", - "phpunit/phpunit": "^8.5.39 || ^9.6.20" + "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -805,7 +806,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.7.1" + "source": "https://github.com/guzzle/psr7/tree/2.8.0" }, "funding": [ { @@ -821,7 +822,7 @@ "type": "tidelift" } ], - "time": "2025-03-27T12:30:47+00:00" + "time": "2025-08-23T21:21:41+00:00" }, { "name": "league/container", @@ -974,16 +975,16 @@ }, { "name": "monolog/monolog", - "version": "2.10.0", + "version": "2.11.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "5cf826f2991858b54d5c3809bee745560a1042a7" + "reference": "37308608e599f34a1a4845b16440047ec98a172a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/5cf826f2991858b54d5c3809bee745560a1042a7", - "reference": "5cf826f2991858b54d5c3809bee745560a1042a7", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/37308608e599f34a1a4845b16440047ec98a172a", + "reference": "37308608e599f34a1a4845b16440047ec98a172a", "shasum": "" }, "require": { @@ -1001,7 +1002,7 @@ "graylog2/gelf-php": "^1.4.2 || ^2@dev", "guzzlehttp/guzzle": "^7.4", "guzzlehttp/psr7": "^2.2", - "mongodb/mongodb": "^1.8", + "mongodb/mongodb": "^1.8 || ^2.0", "php-amqplib/php-amqplib": "~2.4 || ^3", "phpspec/prophecy": "^1.15", "phpstan/phpstan": "^1.10", @@ -1060,7 +1061,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/2.10.0" + "source": "https://github.com/Seldaek/monolog/tree/2.11.0" }, "funding": [ { @@ -1072,7 +1073,7 @@ "type": "tidelift" } ], - "time": "2024-11-12T12:43:37+00:00" + "time": "2026-01-01T13:05:00+00:00" }, { "name": "paragonie/constant_time_encoding", @@ -1783,7 +1784,7 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -1842,7 +1843,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" }, "funding": [ { @@ -1853,6 +1854,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -1862,7 +1867,7 @@ }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -1923,7 +1928,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" }, "funding": [ { @@ -1934,6 +1939,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -1943,7 +1952,7 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", @@ -2004,7 +2013,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" }, "funding": [ { @@ -2015,6 +2024,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -2100,7 +2113,7 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", @@ -2160,7 +2173,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" }, "funding": [ { @@ -2171,6 +2184,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -2180,7 +2197,7 @@ }, { "name": "symfony/polyfill-php81", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", @@ -2236,7 +2253,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0" }, "funding": [ { @@ -2247,6 +2264,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" From 458917f051ca4d8033715efa8d1dc425fd318f4c Mon Sep 17 00:00:00 2001 From: asvinb Date: Thu, 15 Jan 2026 15:52:41 +0400 Subject: [PATCH 014/123] Add E2E tests --- .../asset-group-editor/texts-editor.js | 1 - js/src/data/actions.js | 4 +- .../add-paid-campaigns.test.js | 955 ++++++++++++------ tests/e2e/utils/mock-requests.js | 86 ++ tests/e2e/utils/pages/create-campaign.js | 520 ++++++++++ 5 files changed, 1270 insertions(+), 296 deletions(-) create mode 100644 tests/e2e/utils/pages/create-campaign.js diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js b/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js index 2d1f7deabc..1c365dce5e 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js @@ -253,7 +253,6 @@ export default function TextsEditor( { minNumberOfTexts > 0 && minNumberOfTexts === maxNumberOfTexts } - aria-label={ __( 'Add text', 'google-listings-and-ads' ) } disabled={ maxNumberOfTexts > 0 && texts.length >= maxNumberOfTexts } diff --git a/js/src/data/actions.js b/js/src/data/actions.js index 57a90d9af2..4ca5f2e6a9 100644 --- a/js/src/data/actions.js +++ b/js/src/data/actions.js @@ -1299,7 +1299,7 @@ export function* fetchGenAIMediaAssets( url, assetType ) { method: REQUEST_ACTIONS.POST, data: { final_url: url, - type: assetType, + types: assetType ? [ assetType ] : undefined, }, } ); @@ -1352,7 +1352,7 @@ export function* fetchGenAITextAssets( url, assetType ) { method: REQUEST_ACTIONS.POST, data: { final_url: url, - type: assetType, + types: assetType ? [ assetType ] : undefined, }, } ); diff --git a/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js b/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js index 9ca407bf4b..ad7c578133 100644 --- a/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js +++ b/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js @@ -6,15 +6,22 @@ import { expect, test } from '@playwright/test'; /** * Internal dependencies */ -import { clearOnboardedMerchant, setOnboardedMerchant } from '../../utils/api'; +import { + clearOnboardedMerchant, + setOnboardedMerchant, + setCompletedAdsSetup, + clearCompletedAdsSetup, +} from '../../utils/api'; import DashboardPage from '../../utils/pages/dashboard'; import SetupAdsAccountsPage from '../../utils/pages/ads-onboarding/setup-ads-accounts'; import SetupBudgetPage from '../../utils/pages/ads-onboarding/setup-budget'; +import CreateCampaignPage from '../../utils/pages/create-campaign'; import { LOAD_STATE } from '../../utils/constants'; import { getFAQPanelTitle, getFAQPanelRow, checkFAQExpandable, + checkSnackBarMessage, } from '../../utils/page'; const ADS_ACCOUNTS = [ @@ -37,6 +44,11 @@ test.describe.configure( { mode: 'serial' } ); */ let dashboardPage = null; +/** + * @type {import('../../utils/pages/create-campaign').default} createCampaignPage + */ +let createCampaignPage = null; + /** * @type {import('../../utils/pages/ads-onboarding/setup-ads-accounts').default} setupAdsAccounts */ @@ -52,12 +64,13 @@ let setupBudgetPage = null; */ let page = null; -test.describe( 'Set up Ads account', () => { +test.describe( 'Add paid campaign', () => { test.beforeAll( async ( { browser } ) => { page = await browser.newPage(); dashboardPage = new DashboardPage( page ); setupAdsAccounts = new SetupAdsAccountsPage( page ); setupBudgetPage = new SetupBudgetPage( page ); + createCampaignPage = new CreateCampaignPage( page ); await setOnboardedMerchant(); await setupAdsAccounts.mockAdsAccountsResponse( [] ); await setupBudgetPage.fulfillBillingStatusRequest( { @@ -115,389 +128,745 @@ test.describe( 'Set up Ads account', () => { await expect( dashboardPage.addPaidCampaignButton ).toBeEnabled(); } ); - test.describe( 'Set up your accounts page', async () => { - test.beforeAll( async () => { - await setupAdsAccounts.mockAdsAccountsResponse( [] ); - await dashboardPage.addPaidCampaignButton.click(); - await page.waitForLoadState( LOAD_STATE.DOM_CONTENT_LOADED ); - } ); - - test( 'Page header should be "Set up your accounts"', async () => { - await expect( - page.getByRole( 'heading', { name: 'Set up your accounts' } ) - ).toBeVisible(); - await expect( - page.getByText( - 'Connect your Google account and your Google Ads account to set up a Performance Max campaign.' - ) - ).toBeVisible(); - } ); + test.describe( 'With Ads account not connected', async () => { + test.describe( 'Set up your accounts page', async () => { + test.beforeAll( async () => { + await setupAdsAccounts.mockAdsAccountsResponse( [] ); + await dashboardPage.addPaidCampaignButton.click(); + await page.waitForLoadState( LOAD_STATE.DOM_CONTENT_LOADED ); + } ); - test( 'Google Account should show as connected', async () => { - await expect( - page.getByText( - 'This Google account is connected to your store’s product feed.' - ) - ).toBeVisible(); - } ); + test( 'Page header should be "Set up your accounts"', async () => { + await expect( + page.getByRole( 'heading', { + name: 'Set up your accounts', + } ) + ).toBeVisible(); + await expect( + page.getByText( + 'Connect your Google account and your Google Ads account to set up a Performance Max campaign.' + ) + ).toBeVisible(); + } ); - test( 'Continue Button should be disabled', async () => { - await expect( setupAdsAccounts.getContinueButton() ).toBeDisabled(); - } ); - } ); + test( 'Google Account should show as connected', async () => { + await expect( + page.getByText( + 'This Google account is connected to your store’s product feed.' + ) + ).toBeVisible(); + } ); - test.describe( 'Add campaigns with no Ads account', async () => { - test( 'Create an account should be visible', async () => { - const createAccountButton = page.getByRole( 'button', { - name: 'Create account', + test( 'Continue Button should be disabled', async () => { + await expect( + setupAdsAccounts.getContinueButton() + ).toBeDisabled(); } ); + } ); - await expect( createAccountButton ).toBeVisible(); + test.describe( 'Add campaigns with no Ads account', async () => { + test( 'Create an account should be visible', async () => { + const createAccountButton = page.getByRole( 'button', { + name: 'Create account', + } ); - await expect( setupAdsAccounts.getContinueButton() ).toBeDisabled(); + await expect( createAccountButton ).toBeVisible(); - await expect( - page.getByText( - 'Required to set up conversion measurement and create campaigns.' - ) - ).toBeVisible(); + await expect( + setupAdsAccounts.getContinueButton() + ).toBeDisabled(); - await createAccountButton.click(); - } ); + await expect( + page.getByText( + 'Required to set up conversion measurement and create campaigns.' + ) + ).toBeVisible(); - test( 'Create account button should be disable if the ToS have not been accepted.', async () => { - await expect( - page.getByRole( 'heading', { - name: 'Create Google Ads Account', - } ) - ).toBeVisible(); - - await expect( - page.getByText( - 'By creating a Google Ads account, you agree to the following terms and conditions:' - ) - ).toBeVisible(); - - await expect( - setupAdsAccounts.getCreateAdsAccountButtonModal() - ).toBeDisabled(); - } ); + await createAccountButton.click(); + } ); - test( 'Accept terms and conditions to enable the create account button', async () => { - await setupAdsAccounts.getAcceptTermCreateAccount().check(); + test( 'Create account button should be disable if the ToS have not been accepted.', async () => { + await expect( + page.getByRole( 'heading', { + name: 'Create Google Ads Account', + } ) + ).toBeVisible(); - await expect( - setupAdsAccounts.getCreateAdsAccountButtonModal() - ).toBeEnabled(); - } ); + await expect( + page.getByText( + 'By creating a Google Ads account, you agree to the following terms and conditions:' + ) + ).toBeVisible(); - test( 'Create an Ads account', async () => { - // Intercept Ads connection request. - const connectAdsAccountRequest = - setupAdsAccounts.registerConnectAdsAccountRequests(); + await expect( + setupAdsAccounts.getCreateAdsAccountButtonModal() + ).toBeDisabled(); + } ); - await setupAdsAccounts.mockAdsAccountsResponse( ADS_ACCOUNTS ); + test( 'Accept terms and conditions to enable the create account button', async () => { + await setupAdsAccounts.getAcceptTermCreateAccount().check(); - // Mock request to fulfill Ads connection. - await setupAdsAccounts.fulfillAdsConnection( { - id: ADS_ACCOUNTS[ 0 ].id, - currency: 'USD', - symbol: '$', - status: 'incomplete', - step: 'account_access', + await expect( + setupAdsAccounts.getCreateAdsAccountButtonModal() + ).toBeEnabled(); } ); - await setupAdsAccounts.mockAdsStatusNotClaimed(); + test( 'Create an Ads account', async () => { + // Intercept Ads connection request. + const connectAdsAccountRequest = + setupAdsAccounts.registerConnectAdsAccountRequests(); - await setupAdsAccounts.getCreateAdsAccountButtonModal().click(); + await setupAdsAccounts.mockAdsAccountsResponse( ADS_ACCOUNTS ); - await connectAdsAccountRequest; + // Mock request to fulfill Ads connection. + await setupAdsAccounts.fulfillAdsConnection( { + id: ADS_ACCOUNTS[ 0 ].id, + currency: 'USD', + symbol: '$', + status: 'incomplete', + step: 'account_access', + } ); - const modal = setupAdsAccounts.getAcceptAccountModal(); - await expect( modal ).toBeVisible(); - } ); + await setupAdsAccounts.mockAdsStatusNotClaimed(); - test( 'Show Unclaimed Ads account', async () => { - await setupAdsAccounts.clickCloseAcceptAccountButtonFromModal(); + await setupAdsAccounts.getCreateAdsAccountButtonModal().click(); - const claimButton = setupAdsAccounts.getAdsClaimAccountButton(); - const claimText = setupAdsAccounts.getAdsClaimAccountText(); + await connectAdsAccountRequest; - await expect( claimButton ).toBeVisible(); - await expect( claimText ).toBeVisible(); + const modal = setupAdsAccounts.getAcceptAccountModal(); + await expect( modal ).toBeVisible(); + } ); - await expect( setupAdsAccounts.getContinueButton() ).toBeDisabled(); - } ); + test( 'Show Unclaimed Ads account', async () => { + await setupAdsAccounts.clickCloseAcceptAccountButtonFromModal(); + + const claimButton = setupAdsAccounts.getAdsClaimAccountButton(); + const claimText = setupAdsAccounts.getAdsClaimAccountText(); + + await expect( claimButton ).toBeVisible(); + await expect( claimText ).toBeVisible(); - test( 'Show Claimed Ads account', async () => { - // Intercept Ads connection request. - await setupAdsAccounts.fulfillAdsConnection( { - id: ADS_ACCOUNTS[ 0 ].id, - currency: 'USD', - symbol: '$', - status: 'connected', - step: '', + await expect( + setupAdsAccounts.getContinueButton() + ).toBeDisabled(); } ); - await setupAdsAccounts.mockAdsStatusClaimed(); + test( 'Show Claimed Ads account', async () => { + // Intercept Ads connection request. + await setupAdsAccounts.fulfillAdsConnection( { + id: ADS_ACCOUNTS[ 0 ].id, + currency: 'USD', + symbol: '$', + status: 'connected', + step: '', + } ); - await page.dispatchEvent( 'body', 'blur' ); - await page.dispatchEvent( 'body', 'focus' ); + await setupAdsAccounts.mockAdsStatusClaimed(); - await expect( setupAdsAccounts.getContinueButton() ).toBeEnabled(); + await page.dispatchEvent( 'body', 'blur' ); + await page.dispatchEvent( 'body', 'focus' ); - await expect( - page.getByRole( 'link', { - name: `Account ${ ADS_ACCOUNTS[ 0 ].id }`, - } ) - ).toBeVisible(); + await expect( + setupAdsAccounts.getContinueButton() + ).toBeEnabled(); - await expect( setupAdsAccounts.getContinueButton() ).toBeEnabled(); - } ); - } ); + await expect( + page.getByRole( 'link', { + name: `Account ${ ADS_ACCOUNTS[ 0 ].id }`, + } ) + ).toBeVisible(); - test.describe( 'Add campaigns with existing Ads accounts', () => { - test.beforeAll( async () => { - await setupAdsAccounts.mockAdsAccountsResponse( ADS_ACCOUNTS ); - //Disconnect the account from the previous test - setupAdsAccounts.fulfillAdsConnection( { - id: ADS_ACCOUNTS[ 1 ].id, - currency: 'EUR', - symbol: '\u20ac', - status: 'disconnected', + await expect( + setupAdsAccounts.getContinueButton() + ).toBeEnabled(); } ); - - await page.reload(); } ); - test( 'Select one existing account', async () => { - const adsAccountSelected = `${ ADS_ACCOUNTS[ 1 ].id }`; + test.describe( 'Add campaigns with existing Ads accounts', () => { + test.beforeAll( async () => { + await setupAdsAccounts.mockAdsAccountsResponse( ADS_ACCOUNTS ); + //Disconnect the account from the previous test + setupAdsAccounts.fulfillAdsConnection( { + id: ADS_ACCOUNTS[ 1 ].id, + currency: 'EUR', + symbol: '\u20ac', + status: 'disconnected', + } ); + + await page.reload(); + } ); - await setupAdsAccounts.selectAnExistingAdsAccount( - adsAccountSelected - ); + test( 'Select one existing account', async () => { + const adsAccountSelected = `${ ADS_ACCOUNTS[ 1 ].id }`; - //Intercept Ads connection request - const connectAdsAccountRequest = - setupAdsAccounts.registerConnectAdsAccountRequests( + await setupAdsAccounts.selectAnExistingAdsAccount( adsAccountSelected ); - //Mock request to fulfill Ads connection - setupAdsAccounts.fulfillAdsConnection( { - id: ADS_ACCOUNTS[ 1 ].id, - currency: 'EUR', - symbol: '\u20ac', - status: 'connected', - } ); + //Intercept Ads connection request + const connectAdsAccountRequest = + setupAdsAccounts.registerConnectAdsAccountRequests( + adsAccountSelected + ); - await setupAdsAccounts.clickConnectAds(); - await connectAdsAccountRequest; + //Mock request to fulfill Ads connection + setupAdsAccounts.fulfillAdsConnection( { + id: ADS_ACCOUNTS[ 1 ].id, + currency: 'EUR', + symbol: '\u20ac', + status: 'connected', + } ); - await expect( setupAdsAccounts.getContinueButton() ).toBeEnabled(); - } ); - } ); + await setupAdsAccounts.clickConnectAds(); + await connectAdsAccountRequest; - test.describe( 'Create your campaign', () => { - test( 'Continue to create your campaign', async () => { - await setupAdsAccounts.clickContinue(); - await page.waitForLoadState( LOAD_STATE.DOM_CONTENT_LOADED ); - await expect( - page.getByRole( 'heading', { - name: 'Create your campaign', - } ) - ).toBeVisible(); - - await expect( - page.getByRole( 'heading', { name: 'Set your budget' } ) - ).toBeVisible(); - - await expect( - page.getByRole( 'link', { - name: 'See what your ads will look like.', - } ) - ).toBeVisible(); + await expect( + setupAdsAccounts.getContinueButton() + ).toBeEnabled(); + } ); } ); - test.describe( 'Preview product ad', () => { - test( 'Preview product ad should be visible', async () => { + test.describe( 'Create your campaign', () => { + test( 'Continue to create your campaign', async () => { + await setupAdsAccounts.clickContinue(); + await page.waitForLoadState( LOAD_STATE.DOM_CONTENT_LOADED ); await expect( - page.getByText( 'Preview product ad' ) + page.getByRole( 'heading', { + name: 'Create your campaign', + } ) ).toBeVisible(); + await expect( - page.getByText( - "Each of your product variants will have its own ad. Previews shown here are examples and don't include all possible formats." - ) + page.getByRole( 'heading', { name: 'Set your budget' } ) ).toBeVisible(); - } ); - - test( 'Change image buttons should be enabled', async () => { - const buttonsToChangeImage = page.locator( - '.gla-campaign-preview-card__moving-button' - ); - expect( buttonsToChangeImage ).toHaveCount( 2 ); - - for ( const button of await buttonsToChangeImage.all() ) { - await expect( button ).toBeEnabled(); - } + await expect( + page.getByRole( 'link', { + name: 'See what your ads will look like.', + } ) + ).toBeVisible(); } ); - } ); - test.describe( 'FAQ panels', () => { - test( 'should see five questions in FAQ', async () => { - const faqTitles = getFAQPanelTitle( page ); - await expect( faqTitles ).toHaveCount( 5 ); + test.describe( 'Preview product ad', () => { + test( 'Preview product ad should be visible', async () => { + await expect( + page.getByText( 'Preview product ad' ) + ).toBeVisible(); + await expect( + page.getByText( + "Each of your product variants will have its own ad. Previews shown here are examples and don't include all possible formats." + ) + ).toBeVisible(); + } ); + + test( 'Change image buttons should be enabled', async () => { + const buttonsToChangeImage = page.locator( + '.gla-campaign-preview-card__moving-button' + ); + + expect( buttonsToChangeImage ).toHaveCount( 2 ); + + for ( const button of await buttonsToChangeImage.all() ) { + await expect( button ).toBeEnabled(); + } + } ); } ); - test( 'should not see FAQ rows when FAQ titles are not clicked', async () => { - const faqRows = getFAQPanelRow( page ); - await expect( faqRows ).toHaveCount( 0 ); + test.describe( 'FAQ panels', () => { + test( 'should see five questions in FAQ', async () => { + const faqTitles = getFAQPanelTitle( page ); + await expect( faqTitles ).toHaveCount( 5 ); + } ); + + test( 'should not see FAQ rows when FAQ titles are not clicked', async () => { + const faqRows = getFAQPanelRow( page ); + await expect( faqRows ).toHaveCount( 0 ); + } ); + + // eslint-disable-next-line jest/expect-expect + test( 'should see FAQ rows when all FAQ titles are clicked', async () => { + await checkFAQExpandable( page ); + } ); } ); + } ); - // eslint-disable-next-line jest/expect-expect - test( 'should see FAQ rows when all FAQ titles are clicked', async () => { - await checkFAQExpandable( page ); + test.describe( 'Create Ads with billing data already setup', () => { + test.describe( 'Set the budget', async () => { + test( 'Continue button should be disabled if budget is 0', async () => { + await setupBudgetPage.fillBudget( '0' ); + + await expect( + setupBudgetPage.getCreateCampaignButton() + ).toBeDisabled(); + } ); + + test( 'Continue button should be enabled when selecting an option from the recommendations, even if the entered value is invalid', async () => { + await setupBudgetPage.fillBudget( '0' ); + await expect( + setupBudgetPage.getCreateCampaignButton() + ).toBeDisabled(); + + await page.getByLabel( 'low' ).click(); + await expect( + setupBudgetPage.getCreateCampaignButton() + ).toBeEnabled(); + + await page.getByLabel( 'custom' ).click(); + await expect( + setupBudgetPage.getCreateCampaignButton() + ).toBeDisabled(); + + await page.getByLabel( 'high' ).click(); + await expect( + setupBudgetPage.getCreateCampaignButton() + ).toBeEnabled(); + + await page.getByLabel( 'custom' ).click(); + await expect( + setupBudgetPage.getCreateCampaignButton() + ).toBeDisabled(); + + await page.getByLabel( 'recommended' ).click(); + await expect( + setupBudgetPage.getCreateCampaignButton() + ).toBeEnabled(); + } ); + + test( 'Continue button should be disabled if budget is less than 30% of the daily budget baseline', async () => { + await setupBudgetPage.fillBudget( '2' ); + + await expect( + setupBudgetPage.getCreateCampaignButton() + ).toBeDisabled(); + } ); + + test( 'User is notified of the minimum value', async () => { + await setupBudgetPage.fillBudget( '3' ); + await setupBudgetPage.getBudgetInput().blur(); + + await expect( + page.getByText( + 'Please make sure daily average cost is at least €4.00' + ) + ).toBeVisible(); + } ); + + test( 'Continue button should be enabled if budget is above the recommended value', async () => { + await setupBudgetPage.fillBudget( '5' ); + + await expect( + setupBudgetPage.getCreateCampaignButton() + ).toBeEnabled(); + } ); + + test( 'Display the recommended budget if the budget is valid but lower than the lowest recommended value', async () => { + await setupBudgetPage.fillBudget( '6' ); + + await expect( + page.getByText( + `Your budget is lower than other advertisers' budgets, which may affect performance. For best results, we recommend at least €15.00 per day.` + ) + ).toBeVisible(); + } ); } ); - } ); - } ); - test.describe( 'Create Ads with billing data already setup', () => { - test.describe( 'Set the budget', async () => { - test( 'Continue button should be disabled if budget is 0', async () => { - await setupBudgetPage.fillBudget( '0' ); + test( 'It should show the campaign creation success message', async () => { + await setupBudgetPage.fillBudget( '6' ); + await setupBudgetPage.getCreateCampaignButton().click(); + const cancelButton = page.getByRole( 'button', { + name: 'Cancel', + } ); await expect( - setupBudgetPage.getCreateCampaignButton() - ).toBeDisabled(); - } ); + page.getByText( 'This offer won’t last long!' ) + ).toBeVisible(); + await expect( cancelButton ).toBeEnabled(); - test( 'Continue button should be enabled when selecting an option from the recommendations, even if the entered value is invalid', async () => { - await setupBudgetPage.fillBudget( '0' ); - await expect( - setupBudgetPage.getCreateCampaignButton() - ).toBeDisabled(); + await cancelButton.click(); - await page.getByLabel( 'low' ).click(); - await expect( - setupBudgetPage.getCreateCampaignButton() - ).toBeEnabled(); + await expect( cancelButton ).not.toBeVisible(); - await page.getByLabel( 'custom' ).click(); - await expect( - setupBudgetPage.getCreateCampaignButton() - ).toBeDisabled(); + // Mock the campaign creation request. + const campaignCreation = + setupBudgetPage.mockCampaignCreationAndAdsSetupCompletion( + '6', + [ 'US' ] + ); - await page.getByLabel( 'high' ).click(); - await expect( - setupBudgetPage.getCreateCampaignButton() - ).toBeEnabled(); + await setupBudgetPage.getCreateCampaignButton().click(); + + await campaignCreation; + + //It should redirect to the dashboard page + await page.waitForURL( + '/wp-admin/admin.php?page=wc-admin&path=%2Fgoogle%2Fdashboard&guide=campaign-creation-success', + { + waitUntil: LOAD_STATE.DOM_CONTENT_LOADED, + } + ); - await page.getByLabel( 'custom' ).click(); await expect( - setupBudgetPage.getCreateCampaignButton() - ).toBeDisabled(); + page.getByRole( 'heading', { + name: "You've set up a Performance Max Campaign!", + } ) + ).toBeVisible(); - await page.getByLabel( 'recommended' ).click(); await expect( - setupBudgetPage.getCreateCampaignButton() + page.getByRole( 'button', { + name: 'Create another campaign', + } ) ).toBeEnabled(); - } ); - - test( 'Continue button should be disabled if budget is less than 30% of the daily budget baseline', async () => { - await setupBudgetPage.fillBudget( '2' ); await expect( - setupBudgetPage.getCreateCampaignButton() - ).toBeDisabled(); + page.getByRole( 'button', { + name: 'Got It', + } ) + ).toBeEnabled(); + + await page + .getByRole( 'button', { + name: 'Got It', + } ) + .click(); } ); + } ); + } ); + + test.describe( 'With connected Ads account', async () => { + test.beforeAll( async () => { + await setCompletedAdsSetup(); + await createCampaignPage.mockRequests(); + await createCampaignPage.mockOptimizeCampaignRequests(); + createCampaignPage.goto(); + } ); - test( 'User is notified of the minimum value', async () => { - await setupBudgetPage.fillBudget( '3' ); - await setupBudgetPage.getBudgetInput().blur(); + test.afterAll( async () => { + await clearCompletedAdsSetup(); + await page.close(); + } ); + test.describe( 'Create Campaign page', async () => { + test( 'Page header should be "Create your campaign"', async () => { + await expect( + page.getByRole( 'heading', { + name: 'Create your campaign', + } ) + ).toBeVisible(); await expect( page.getByText( - 'Please make sure daily average cost is at least €4.00' + 'Performance Max campaigns are automatically optimized for you by Google.' ) ).toBeVisible(); } ); - test( 'Continue button should be enabled if budget is above the recommended value', async () => { - await setupBudgetPage.fillBudget( '5' ); - - await expect( - setupBudgetPage.getCreateCampaignButton() - ).toBeEnabled(); - } ); - - test( 'Display the recommended budget if the budget is valid but lower than the lowest recommended value', async () => { - await setupBudgetPage.fillBudget( '6' ); + test( 'Clicking the "Continue" button takes you to the "Optimize your campaign" step', async () => { + const continueButton = createCampaignPage.getContinueButton(); + continueButton.click(); await expect( - page.getByText( - `Your budget is lower than other advertisers' budgets, which may affect performance. For best results, we recommend at least €15.00 per day.` - ) + page.getByRole( 'heading', { + name: 'Optimize your campaign', + } ) ).toBeVisible(); } ); } ); - test( 'It should show the campaign creation success message', async () => { - await setupBudgetPage.fillBudget( '6' ); - await setupBudgetPage.getCreateCampaignButton().click(); + test.describe( 'Optimize your campaign step', async () => { + test( 'Create Campaign button should be disabled if no URL selected', async () => { + const createCampaignButton = + createCampaignPage.getCreateCampaignButton(); + await expect( createCampaignButton ).toBeDisabled(); + } ); - const cancelButton = page.getByRole( 'button', { name: 'Cancel' } ); - await expect( - page.getByText( 'This offer won’t last long!' ) - ).toBeVisible(); - await expect( cancelButton ).toBeEnabled(); + test( 'Selecting final URL enables Create Campaign button', async () => { + await createCampaignPage.selectUrlOption(); - await cancelButton.click(); + const createCampaignButton = + createCampaignPage.getCreateCampaignButton(); + await expect( createCampaignButton ).toBeEnabled(); + } ); - await expect( cancelButton ).not.toBeVisible(); + test( 'Selecting the "Or, select a different Final URL" button disables the Create Campaign button', async () => { + const selectDifferentFinalUrlButton = + createCampaignPage.getSelectDifferentFinalUrlButton(); + await selectDifferentFinalUrlButton.click(); - // Mock the campaign creation request. - const campaignCreation = - setupBudgetPage.mockCampaignCreationAndAdsSetupCompletion( - '6', - [ 'US' ] - ); + const createCampaignButton = + createCampaignPage.getCreateCampaignButton(); + await expect( createCampaignButton ).toBeDisabled(); + } ); - await setupBudgetPage.getCreateCampaignButton().click(); + test( 'Selecting the Final URL again enables the Create Campaign button', async () => { + await createCampaignPage.selectUrlOption(); - await campaignCreation; + const createCampaignButton = + createCampaignPage.getCreateCampaignButton(); + await expect( createCampaignButton ).toBeEnabled(); + } ); - //It should redirect to the dashboard page - await page.waitForURL( - '/wp-admin/admin.php?page=wc-admin&path=%2Fgoogle%2Fdashboard&guide=campaign-creation-success', - { - waitUntil: LOAD_STATE.DOM_CONTENT_LOADED, - } - ); - - await expect( - page.getByRole( 'heading', { - name: "You've set up a Performance Max Campaign!", - } ) - ).toBeVisible(); - - await expect( - page.getByRole( 'button', { - name: 'Create another campaign', - } ) - ).toBeEnabled(); - - await expect( - page.getByRole( 'button', { - name: 'Got It', - } ) - ).toBeEnabled(); - - await page - .getByRole( 'button', { - name: 'Got It', - } ) - .click(); + test.describe( 'Gen AI', () => { + test.describe( 'Text Assets', () => { + test.describe( 'Headlines', () => { + test.describe( 'Visibility', () => { + test( 'Generate headline button is hidden when all inputs are filled', async () => { + const generateHeadlineButton = + createCampaignPage.getGenerateHeadlineButton(); + await expect( + generateHeadlineButton + ).not.toBeVisible(); + } ); + + test( 'Generate headline button is visible when at least one input is empty', async () => { + const addHeadlineButton = + createCampaignPage.getAddHeadlineButton(); + await addHeadlineButton.click(); + + const generateHeadlineButton = + createCampaignPage.getGenerateHeadlineButton(); + await expect( + generateHeadlineButton + ).toBeVisible(); + + const generateHeadlinesButton = + createCampaignPage.getGenerateHeadlinesButton(); + await expect( + generateHeadlinesButton + ).not.toBeVisible(); + + const headlineInputsValues = + await createCampaignPage.getHeadlineInputsValues(); + + await expect( + headlineInputsValues + ).toHaveLength( 4 ); + + const lastValue = + headlineInputsValues[ + headlineInputsValues.length - 1 + ]; + expect( lastValue ).toBe( '' ); + } ); + } ); + + test.describe( 'Generate action', () => { + test.beforeEach( async () => { + createCampaignPage.mockGenerateTextAssetsSuccess(); + } ); + + test( 'Clicking generate headline sends the correct POST request', async () => { + const generateRequest = + createCampaignPage.awaitForGenerateTextRequest( + 'https://woo.com/shop/', + [ 'headline' ] + ); + + const generateHeadlineButton = + createCampaignPage.getGenerateHeadlineButton(); + await generateHeadlineButton.click(); + await generateRequest; + } ); + } ); + + test.describe( 'Success', () => { + test.beforeEach( async () => { + createCampaignPage.mockGenerateTextAssetsSuccess(); + } ); + + test( 'Clicking generate headline fills empty headline inputs', async () => { + const headlineInputsValues = + await createCampaignPage.getHeadlineInputsValues(); + const lastValue = + headlineInputsValues[ + headlineInputsValues.length - 1 + ]; + expect( lastValue ).toBe( + 'Shop the Latest Deals' + ); + } ); + } ); + } ); + + test.describe( 'Long Headlines', () => { + test.describe( 'Visibility', () => { + test( 'Generate long headline button is hidden when all inputs are filled', async () => { + const generateLongHeadlineButton = + createCampaignPage.getGenerateLongHeadlineButton(); + await expect( + generateLongHeadlineButton + ).not.toBeVisible(); + } ); + + test( 'Generate long headline button is visible when at least one input is empty', async () => { + const addLongHeadlineButton = + createCampaignPage.getAddLongHeadlineButton(); + await addLongHeadlineButton.click(); + + const generateLongHeadlineButton = + createCampaignPage.getGenerateLongHeadlineButton(); + await expect( + generateLongHeadlineButton + ).toBeVisible(); + + const generateLongHeadlinesButton = + createCampaignPage.getGenerateLongHeadlinesButton(); + await expect( + generateLongHeadlinesButton + ).not.toBeVisible(); + + const longHeadlineInputsValues = + await createCampaignPage.getLongHeadlineInputsValues(); + + await expect( + longHeadlineInputsValues + ).toHaveLength( 2 ); + + const lastValue = + longHeadlineInputsValues[ + longHeadlineInputsValues.length - 1 + ]; + expect( lastValue ).toBe( '' ); + } ); + } ); + + test.describe( 'Generate action', () => { + test.beforeEach( async () => { + createCampaignPage.mockGenerateTextAssetsSuccess(); + } ); + + test( 'Clicking generate long headline sends the correct POST request', async () => { + const generateRequest = + createCampaignPage.awaitForGenerateTextRequest( + 'https://woo.com/shop/', + [ 'long_headline' ] + ); + + const generateLongHeadlineButton = + createCampaignPage.getGenerateLongHeadlineButton(); + await generateLongHeadlineButton.click(); + await generateRequest; + } ); + } ); + + test.describe( 'Success', () => { + test.beforeEach( async () => { + createCampaignPage.mockGenerateTextAssetsSuccess(); + } ); + + test( 'Clicking generate long headline fills empty long headline inputs', async () => { + const longHeadlineInputsValues = + await createCampaignPage.getLongHeadlineInputsValues(); + const lastValue = + longHeadlineInputsValues[ + longHeadlineInputsValues.length - 1 + ]; + expect( lastValue ).toBe( + 'Discover quality products at great prices' + ); + } ); + } ); + } ); + + test.describe( 'Descriptions', () => { + test.describe( 'Visibility', () => { + test( 'Generate description button is hidden when all inputs are filled', async () => { + const generateDescriptionButton = + createCampaignPage.getGenerateDescriptionButton(); + await expect( + generateDescriptionButton + ).not.toBeVisible(); + } ); + + test( 'Generate description button is visible when at least one input is empty', async () => { + const addDescriptionButton = + createCampaignPage.getAddDescriptionButton(); + await addDescriptionButton.click(); + + const generateDescriptionButton = + createCampaignPage.getGenerateDescriptionButton(); + await expect( + generateDescriptionButton + ).toBeVisible(); + + const generateDescriptionsButton = + createCampaignPage.getGenerateDescriptionsButton(); + await expect( + generateDescriptionsButton + ).not.toBeVisible(); + + const descriptionInputsValues = + await createCampaignPage.getDescriptionInputsValues(); + await expect( + descriptionInputsValues + ).toHaveLength( 3 ); + + const lastValue = + descriptionInputsValues[ + descriptionInputsValues.length - 1 + ]; + expect( lastValue ).toBe( '' ); + } ); + } ); + + test.describe( 'Generate action', () => { + test.beforeEach( async () => { + createCampaignPage.mockGenerateTextAssetsSuccess(); + } ); + + test( 'Clicking generate description sends the correct POST request', async () => { + const generateRequest = + createCampaignPage.awaitForGenerateTextRequest( + 'https://woo.com/shop/', + [ 'description' ] + ); + + const generateDescriptionButton = + createCampaignPage.getGenerateDescriptionButton(); + await generateDescriptionButton.click(); + await generateRequest; + } ); + } ); + + test.describe( 'Success', () => { + test.beforeEach( async () => { + createCampaignPage.mockGenerateTextAssetsSuccess(); + } ); + + test( 'Clicking generate description fills empty description inputs', async () => { + const descriptionInputsValues = + await createCampaignPage.getDescriptionInputsValues(); + const lastValue = + descriptionInputsValues[ + descriptionInputsValues.length - 1 + ]; + expect( lastValue ).toBe( + 'Browse top picks and enjoy exclusive savings.' + ); + } ); + } ); + } ); + + test.describe( 'Error', () => { + test.beforeEach( async () => { + createCampaignPage.mockEmptyGenerateTextAssets(); + } ); + + test( 'Displays error message when there are no more generated text', async () => { + const addDescriptionButton = + createCampaignPage.getAddDescriptionButton(); + await addDescriptionButton.click(); + + const generateDescriptionButton = + createCampaignPage.getGenerateDescriptionButton(); + await generateDescriptionButton.click(); + + await checkSnackBarMessage( + page, + 'No texts were generated. Please try again.' + ); + } ); + } ); + } ); + } ); } ); } ); } ); diff --git a/tests/e2e/utils/mock-requests.js b/tests/e2e/utils/mock-requests.js index ab83eaa7fa..732ff5ec54 100644 --- a/tests/e2e/utils/mock-requests.js +++ b/tests/e2e/utils/mock-requests.js @@ -1111,4 +1111,90 @@ export default class MockRequests { [ 'GET' ] ); } + + /** + * Fulfills a mock request for the final URL suggestions endpoint for campaign assets. + * + * @param {Object} payload - The mock response payload to be returned. + * @return {Promise} A promise that resolves when the request is fulfilled. + */ + async fulfillFinalUrlSuggestions( payload, status = 200 ) { + await this.fulfillRequest( + /\/wc\/gla\/assets\/final-url\/suggestions\b/, + payload, + status, + [ 'GET' ] + ); + } + + /** + * Mocks a request for final URL suggestions. + * + * @param {Object} payload - The mock response payload to be returned. + * @param {number} [status=200] - The HTTP status code to be returned. Defaults to 200. + * @return {Promise} A promise that resolves when the request is mocked. + */ + async mockFinalUrlSuggestions( payload, status = 200 ) { + await this.fulfillFinalUrlSuggestions( payload, status ); + } + + /** + * Fulfills a mock request for the asset suggestions endpoint. + * + * @param {Object} payload - The mock response payload to be returned. + * @param {number} [status=200] - The HTTP status code to be returned. + * @return {Promise} A promise that resolves when the request is fulfilled. + */ + async fulfillAssetSuggestions( payload, status = 200 ) { + await this.fulfillRequest( + /\/wc\/gla\/assets\/suggestions\b/, + payload, + status, + [ 'GET' ] + ); + } + + /** + * Mocks a request for asset suggestions. + * + * @param {Object} payload - The mock response payload to be returned. + */ + async mockAssetSuggestions( payload, status = 200 ) { + await this.fulfillAssetSuggestions( payload, status ); + } + + /** + * Fulfills a mock request for the asset groups of a specific campaign. + * + * @param {string|number} campaignId - The ID of the campaign to get asset groups for. + * @param {Object} payload - The mock response payload to be returned. + * @param {number} [status=200] - The HTTP status code to be returned. + * @return {Promise} A promise that resolves when the request is fulfilled. + */ + async fulfillAssetGroupsForCampaign( campaignId, payload, status = 200 ) { + await this.fulfillRequest( + new RegExp( + `\\/wc\\/gla\\/ads\\/campaigns\\/asset-groups\\?.*campaign_id=${ campaignId }\\b` + ), + payload, + status, + [ 'GET' ] + ); + } + + /** + * Fulfill generate text assets request. + * + * @param {Object} payload - The response payload to return. + * @param {number} status - The HTTP status in the response. + * @return {Promise} + */ + async fulfillGenerateTextAssetsRequest( payload, status = 200 ) { + await this.fulfillRequest( + /\/wc\/gla\/ads\/assets\/generate-text\b/, + payload, + status, + [ 'POST' ] + ); + } } diff --git a/tests/e2e/utils/pages/create-campaign.js b/tests/e2e/utils/pages/create-campaign.js new file mode 100644 index 0000000000..e8770f5ee3 --- /dev/null +++ b/tests/e2e/utils/pages/create-campaign.js @@ -0,0 +1,520 @@ +/** + * Internal dependencies + */ +import { LOAD_STATE } from '../constants'; +import MockRequests from '../mock-requests'; + +/** + * Dashboard page object class. + */ +export default class CreateCampaignPage extends MockRequests { + /** + * @param {import('@playwright/test').Page} page + */ + constructor( page ) { + super( page ); + this.page = page; + } + + /** + * Close the current page. + * + * @return {Promise} + */ + async closePage() { + await this.page.close(); + } + + /** + * Mock all requests related to external accounts such as Merchant Center, Google, etc. + * + * @return {Promise} + */ + async mockRequests() { + // Mock Reports Programs + await this.fulfillJetPackConnection( { + active: 'yes', + owner: 'yes', + displayName: 'John', + email: 'john@email.com', + } ); + + await this.mockGoogleConnected(); + await this.mockAdsAccountConnected(); + } + + /** + * Go to the Create Campaign page. + * + * @return {Promise} + */ + async goto() { + await this.page.goto( + '/wp-admin/admin.php?page=wc-admin&path=%2Fgoogle%2Fdashboard&subpath=%2Fcampaigns%2Fcreate', + { waitUntil: LOAD_STATE.DOM_CONTENT_LOADED } + ); + } + + /** + * Get the Continue button. + * + * @return {import('@playwright/test').Locator} Continue button. + */ + getContinueButton() { + return this.page.getByRole( 'button', { + name: 'Continue', + exact: true, + } ); + } + + /** + * Get create campaign button. + * + * @return {import('@playwright/test').Locator} Get create campaign button. + */ + getCreateCampaignButton() { + // Intentionally not using getByRole here, as another button with the same accessible name exists in the Stepper header. + return this.page.locator( + 'button[data-action="submit-campaign-and-assets"]' + ); + } + + /** + * Click create campaign button. + * + * @return {Promise} + */ + async clickCreateCampaignButton() { + const createCampaignButton = this.getCreateCampaignButton(); + await createCampaignButton.click(); + } + + /** + * Get final URL select dropdown. + * + * @return {import('@playwright/test').Locator} Get final URL select dropdown. + */ + getFinalUrlSelect() { + return this.page.getByRole( 'combobox' ); + } + + /** + * Get select button. + * + * @return {import('@playwright/test').Locator} Get select button. + */ + getSelectButton() { + return this.page.getByRole( 'button', { name: 'Select' } ); + } + + /** + * Get select different final URL button. + * + * @return {import('@playwright/test').Locator} Get select different final URL button. + */ + getSelectDifferentFinalUrlButton() { + return this.page.getByRole( 'button', { + name: 'Or, select a different Final URL', + } ); + } + + /** + * Get Add headline button. + * + * @return {import('@playwright/test').Locator} Get Add headline button. + */ + getAddHeadlineButton() { + return this.page.getByRole( 'button', { name: 'Add headline' } ); + } + + /** + * Get Add long headline button. + * + * @return {import('@playwright/test').Locator} Get Add long headline button. + */ + getAddLongHeadlineButton() { + return this.page.getByRole( 'button', { name: 'Add long headline' } ); + } + + /** + * Get Add description button. + * + * @return {import('@playwright/test').Locator} Get Add description button. + */ + getAddDescriptionButton() { + return this.page.getByRole( 'button', { name: 'Add description' } ); + } + + /** + * Get generate headline button. + * + * @return {import('@playwright/test').Locator} Get generate headline button. + */ + getGenerateHeadlineButton() { + return this.page.getByRole( 'button', { + name: 'Generate headline', + } ); + } + + /** + * Get generate headlines button. + * + * @return {import('@playwright/test').Locator} Get generate headlines button. + */ + getGenerateHeadlinesButton() { + return this.page.getByRole( 'button', { + name: 'Generate headlines', + } ); + } + + /** + * Get generate long headline button. + * + * @return {import('@playwright/test').Locator} Get generate long headline button. + */ + getGenerateLongHeadlineButton() { + return this.page.getByRole( 'button', { + name: 'Generate long headline', + } ); + } + + /** + * Get generate long headlines button. + * + * @return {import('@playwright/test').Locator} Get generate long headlines button. + */ + getGenerateLongHeadlinesButton() { + return this.page.getByRole( 'button', { + name: 'Generate long headlines', + } ); + } + + /** + * Get generate description button. + * + * @return {import('@playwright/test').Locator} Get generate description button. + */ + getGenerateDescriptionButton() { + return this.page.getByRole( 'button', { + name: 'Generate description', + } ); + } + + /** + * Get generate descriptions button. + * + * @return {import('@playwright/test').Locator} Get generate descriptions button. + */ + getGenerateDescriptionsButton() { + return this.page.getByRole( 'button', { + name: 'Generate descriptions', + } ); + } + + /** + * Get headlines section. + * + * @return {import('@playwright/test').Locator} Get headlines section. + */ + getHeadlinesSection() { + return this.page + .locator( + '.gla-asset-field:has(:where(.gla-asset-field__heading):has-text("Headlines"))' + ) + .first(); + } + + /** + * Get headline inputs. + * + * @return {import('@playwright/test').Locator} Get headline inputs. + */ + getHeadlineInputs() { + const headlinesSection = this.getHeadlinesSection(); + const headlineInputs = headlinesSection.locator( + 'input[placeholder="Headline"]' + ); + + return headlineInputs; + } + + /** + * Get headline inputs values. + * + * @return {Promise} Get headline inputs values. + */ + async getHeadlineInputsValues() { + const headlineInputs = this.getHeadlineInputs(); + const values = await headlineInputs.evaluateAll( ( inputs ) => + inputs.map( ( input ) => input.value ) + ); + + return values; + } + + /** + * Get long headlines section. + * + * @return {import('@playwright/test').Locator} Get long headlines section. + */ + getLongHeadlinesSection() { + return this.page + .locator( + '.gla-asset-field:has(:where(.gla-asset-field__heading):has-text("Long headlines"))' + ) + .first(); + } + + /** + * Get headline inputs. + * + * @return {import('@playwright/test').Locator} Get headline inputs. + */ + getLongHeadlineInputs() { + const longHeadlinesSection = this.getLongHeadlinesSection(); + const longHeadlineInputs = longHeadlinesSection.locator( + 'input[placeholder="Long headline"]' + ); + + return longHeadlineInputs; + } + + /** + * Get descriptions section. + * + * @return {import('@playwright/test').Locator} Get descriptions section. + */ + getDescriptionsSection() { + return this.page + .locator( + '.gla-asset-field:has(:where(.gla-asset-field__heading):has-text("Descriptions"))' + ) + .first(); + } + + /** + * Get description inputs. + * + * @return {import('@playwright/test').Locator} Get description inputs. + */ + getDescriptionInputs() { + const descriptionsSection = this.getDescriptionsSection(); + const descriptionInputs = descriptionsSection.locator( + 'input[placeholder="Description"]' + ); + + return descriptionInputs; + } + + /** + * Get description inputs values. + * + * @return {Promise} Get description inputs values. + */ + async getDescriptionInputsValues() { + const descriptionInputs = this.getDescriptionInputs(); + const values = await descriptionInputs.evaluateAll( ( inputs ) => + inputs.map( ( input ) => input.value ) + ); + + return values; + } + + /** + * Get long headline inputs values. + * + * @return {Promise} Get long headline inputs values. + */ + async getLongHeadlineInputsValues() { + const longHeadlineInputs = this.getLongHeadlineInputs(); + const values = await longHeadlineInputs.evaluateAll( ( inputs ) => + inputs.map( ( input ) => input.value ) + ); + + return values; + } + + /** + * Select URL option. + * + * @return {Promise} + */ + async selectUrlOption() { + const finalUrlSelect = this.getFinalUrlSelect(); + await finalUrlSelect.focus(); + const option = this.page.getByRole( 'option', { + name: 'Shop', + } ); + await option.click(); + + const selectButton = this.getSelectButton(); + await selectButton.click(); + } + + /** + * Mock optimize campaign requests. + * + * @return {Promise} + */ + async mockOptimizeCampaignRequests() { + await this.mockFinalUrlSuggestions( [ + { + id: 0, + type: 'homepage', + title: 'Homepage', + url: 'https://woo.com', + }, + { + id: 7, + type: 'post', + title: 'Shop', + url: 'https://woo.com/shop/', + }, + ] ); + + await this.mockAssetSuggestions( { + logo: [ + 'https://tpc.googlesyndication.com/simgad/2643735098967285793', + ], + business_name: 'My Woo Store', + square_marketing_image: [ + 'https://tpc.googlesyndication.com/simgad/2643735098967285793', + ], + marketing_image: [ + 'https://tpc.googlesyndication.com/simgad/6792129722137622820', + ], + portrait_marketing_image: [], + call_to_action_selection: null, + final_url: 'https://woo.com/shop/', + display_url_path: [ 'shop', '' ], + headline: [ 'My Woo Store', 'Shop', 'Buy Now' ], + description: [ 'Best products available here.', 'Shop today!' ], + long_headline: [ 'My Woo Store: Shop' ], + } ); + + await this.fulfillAssetGroupsForCampaign( 1, [ + { + id: 1, + final_url: '', + display_url_path: [ '', '' ], + assets: {}, + }, + ] ); + } + + /** + * Await for the generate text assets request. + * + * @param {string} finalUrl The final URL. + * @param {Array} types The requested asset types. + * @return {Promise} The request. + */ + async awaitForGenerateTextRequest( finalUrl, types ) { + return this.page.waitForRequest( ( request ) => { + if ( + ! request.url().includes( '/gla/ads/assets/generate-text' ) || + request.method() !== 'POST' + ) { + return false; + } + + const payload = request.postDataJSON(); + + return ( + payload.final_url === finalUrl && + Array.isArray( payload.types ) && + types.every( ( type ) => payload.types.includes( type ) ) + ); + } ); + } + + /** + * Mock generate text assets success response. + * + * @return {Promise} + */ + async mockGenerateTextAssetsSuccess() { + await this.fulfillGenerateTextAssetsRequest( { + final_url: 'https://woo.com/shop/', + items: [ + // Headlines + { + text: 'Shop the Latest Deals', + type: 'headline', + }, + { + text: 'Limited-Time Offers', + type: 'headline', + }, + { + text: 'New Arrivals In Store', + type: 'headline', + }, + { + text: 'Top Deals This Week', + type: 'headline', + }, + { + text: 'Fast Shipping Available', + type: 'headline', + }, + + // Long headlines + { + text: 'Discover quality products at great prices', + type: 'long_headline', + }, + { + text: 'Everything you need, delivered fast', + type: 'long_headline', + }, + { + text: 'Upgrade your everyday shopping experience', + type: 'long_headline', + }, + { + text: 'Find your next favorite product today', + type: 'long_headline', + }, + { + text: 'Smart shopping starts right here', + type: 'long_headline', + }, + + // Descriptions + { + text: 'Browse top picks and enjoy exclusive savings.', + type: 'description', + }, + { + text: 'Shop trusted products with fast delivery.', + type: 'description', + }, + { + text: 'Great value items curated just for you.', + type: 'description', + }, + { + text: 'Simple shopping with reliable service.', + type: 'description', + }, + { + text: 'Quality products backed by great support.', + type: 'description', + }, + ], + } ); + } + + /** + * Mock generate text assets empty response. + * + * @return {Promise} + */ + async mockEmptyGenerateTextAssets() { + await this.fulfillGenerateTextAssetsRequest( { + final_url: 'https://woo.com/shop/', + items: [], + } ); + } +} From cfae55d9cb166df90795f284521ba7ca6785de0b Mon Sep 17 00:00:00 2001 From: asvinb Date: Thu, 15 Jan 2026 16:02:35 +0400 Subject: [PATCH 015/123] Add @svgr/webpack for SVG processing in webpack config --- package-lock.json | 1 + package.json | 1 + webpack.config.js | 44 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 3c4b73b17e..64ae1e304e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "@hapi/h2o2": "^10.0.4", "@hapi/hapi": "^21.3.10", "@playwright/test": "^1.56.1", + "@svgr/webpack": "^8.1.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^16.0.0", diff --git a/package.json b/package.json index 2d40b2594f..b725449f17 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@hapi/h2o2": "^10.0.4", "@hapi/hapi": "^21.3.10", "@playwright/test": "^1.56.1", + "@svgr/webpack": "^8.1.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^16.0.0", diff --git a/webpack.config.js b/webpack.config.js index b500499b56..ab72b0a344 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -22,7 +22,49 @@ const webpackConfig = { // Remove `@wordpress/` rules for SVGs. ...defaultConfig.module.rules.filter( exceptSVGAndPNGRule ), { - test: /\.(svg|png|jpe?g|gif)$/i, + test: /\.svg$/i, + oneOf: [ + { + resourceQuery: /inline/, + issuer: /\.[jt]sx?$/, + use: [ + { + loader: require.resolve( '@svgr/webpack' ), + options: { + svgoConfig: { + plugins: [ + { + name: 'preset-default', + params: { + overrides: { + removeViewBox: false, + }, + }, + }, + { + name: 'prefixIds', + params: { + prefix: true, + }, + }, + ], + }, + exportType: 'default', + }, + }, + ], + }, + // Default: emit SVG as a file + { + type: 'asset/resource', + generator: { + filename: 'images/[path][contenthash].[name][ext]', + }, + }, + ], + }, + { + test: /\.(png|jpe?g|gif)$/i, type: 'asset/resource', generator: { filename: 'images/[path][contenthash].[name][ext]', From 6024bd33f1a9fb4e7326944d6de080cc428b2592 Mon Sep 17 00:00:00 2001 From: asvinb Date: Thu, 15 Jan 2026 16:06:33 +0400 Subject: [PATCH 016/123] chore(package.json): Update package.json max size value --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 488bb3e9b0..0513341331 100644 --- a/package.json +++ b/package.json @@ -149,7 +149,7 @@ }, { "path": "./js/build/index.js", - "maxSize": "19 kB" + "maxSize": "19.1 kB" }, { "path": "./js/build/commons.js", From 75ff3169d3a2c6be66b237a663cfdd3340f5fd03 Mon Sep 17 00:00:00 2001 From: Ankit Gade Date: Thu, 15 Jan 2026 17:38:36 +0530 Subject: [PATCH 017/123] Update imports for V22. --- bin/GoogleAdsCleanupServices.php | 2 +- composer.json | 2 +- src/API/Google/Ads.php | 12 +- src/API/Google/AdsAsset.php | 22 ++-- src/API/Google/AdsAssetGroup.php | 24 ++-- src/API/Google/AdsAssetGroupAsset.php | 10 +- src/API/Google/AdsCampaign.php | 22 ++-- src/API/Google/AdsCampaignBudget.php | 10 +- src/API/Google/AdsCampaignCriterion.php | 12 +- src/API/Google/AdsCampaignLabel.php | 14 +-- src/API/Google/AdsConversionAction.php | 26 ++-- src/API/Google/AdsReport.php | 4 +- src/API/Google/AssetFieldType.php | 2 +- src/API/Google/BillingSetupStatus.php | 2 +- src/API/Google/BudgetMetrics.php | 16 +-- src/API/Google/BudgetRecommendations.php | 14 +-- src/API/Google/CallToActionType.php | 2 +- src/API/Google/CampaignStatus.php | 2 +- src/API/Google/CampaignType.php | 2 +- src/API/Google/MerchantMetrics.php | 2 +- src/API/Google/Query/AdsQuery.php | 6 +- src/API/Google/Query/AdsReportQuery.php | 2 +- src/Ads/AdsRecommendationsService.php | 4 +- src/Google/Ads/ServiceClientFactoryTrait.php | 38 +++--- .../GoogleServiceProvider.php | 2 +- .../HelperTrait/GoogleAdsClientTrait.php | 112 +++++++++--------- .../API/Google/AdsAssetGroupAssetTest.php | 4 +- tests/Unit/API/Google/AdsAssetGroupTest.php | 6 +- tests/Unit/API/Google/AdsAssetTest.php | 2 +- .../API/Google/AdsCampaignCriterionTest.php | 2 +- .../API/Google/AdsConversionActionTest.php | 2 +- tests/Unit/API/Google/AdsTest.php | 8 +- tests/Unit/API/Google/MerchantMetricsTest.php | 6 +- 33 files changed, 198 insertions(+), 198 deletions(-) diff --git a/bin/GoogleAdsCleanupServices.php b/bin/GoogleAdsCleanupServices.php index 864764bb87..239ac5443a 100644 --- a/bin/GoogleAdsCleanupServices.php +++ b/bin/GoogleAdsCleanupServices.php @@ -26,7 +26,7 @@ class GoogleAdsCleanupServices { * * @var string */ - protected $version = 'V21'; + protected $version = 'V22'; /** * @var Event Composer event. diff --git a/composer.json b/composer.json index a8a6b3e883..d03f8f9d76 100644 --- a/composer.json +++ b/composer.json @@ -60,7 +60,7 @@ "Google\\Task\\Composer::cleanup", "Automattic\\WooCommerce\\GoogleListingsAndAds\\Util\\SymfonyPolyfillCleanup::remove", "Automattic\\WooCommerce\\GoogleListingsAndAds\\Util\\GoogleAdsCleanupServices::remove", - "composer run-script remove-google-ads-api-version-support -- 18 19 20", + "composer run-script remove-google-ads-api-version-support -- 18 19 20 21", "php ./bin/prefix-vendor-namespace.php", "bash ./bin/cleanup-vendor-files.sh", "composer dump-autoload" diff --git a/src/API/Google/Ads.php b/src/API/Google/Ads.php index 51b7fa049d..971ee743fb 100644 --- a/src/API/Google/Ads.php +++ b/src/API/Google/Ads.php @@ -13,12 +13,12 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait; use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsInterface; use Exception; -use Google\Ads\GoogleAds\Util\V21\ResourceNames; -use Google\Ads\GoogleAds\V21\Enums\AccessRoleEnum\AccessRole; -use Google\Ads\GoogleAds\V21\Enums\ProductLinkInvitationStatusEnum\ProductLinkInvitationStatus; -use Google\Ads\GoogleAds\V21\Resources\ProductLinkInvitation; -use Google\Ads\GoogleAds\V21\Services\ListAccessibleCustomersRequest; -use Google\Ads\GoogleAds\V21\Services\UpdateProductLinkInvitationRequest; +use Google\Ads\GoogleAds\Util\V22\ResourceNames; +use Google\Ads\GoogleAds\V22\Enums\AccessRoleEnum\AccessRole; +use Google\Ads\GoogleAds\V22\Enums\ProductLinkInvitationStatusEnum\ProductLinkInvitationStatus; +use Google\Ads\GoogleAds\V22\Resources\ProductLinkInvitation; +use Google\Ads\GoogleAds\V22\Services\ListAccessibleCustomersRequest; +use Google\Ads\GoogleAds\V22\Services\UpdateProductLinkInvitationRequest; use Google\ApiCore\ApiException; use Google\ApiCore\ValidationException; diff --git a/src/API/Google/AdsAsset.php b/src/API/Google/AdsAsset.php index 0be762dd02..fcf8dac4df 100644 --- a/src/API/Google/AdsAsset.php +++ b/src/API/Google/AdsAsset.php @@ -6,17 +6,17 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Google\Ads\GoogleAdsClient; use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait; use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface; -use Google\Ads\GoogleAds\V21\Services\GoogleAdsRow; -use Google\Ads\GoogleAds\V21\Enums\AssetTypeEnum\AssetType; -use Google\Ads\GoogleAds\V21\Resources\Asset; -use Google\Ads\GoogleAds\V21\Services\AssetOperation; -use Google\Ads\GoogleAds\V21\Services\MutateGoogleAdsRequest; -use Google\Ads\GoogleAds\V21\Services\MutateOperation; -use Google\Ads\GoogleAds\Util\V21\ResourceNames; -use Google\Ads\GoogleAds\V21\Common\TextAsset; -use Google\Ads\GoogleAds\V21\Common\ImageAsset; -use Google\Ads\GoogleAds\V21\Common\CallToActionAsset; -use Google\Ads\GoogleAds\V21\Common\YoutubeVideoAsset; +use Google\Ads\GoogleAds\V22\Services\GoogleAdsRow; +use Google\Ads\GoogleAds\V22\Enums\AssetTypeEnum\AssetType; +use Google\Ads\GoogleAds\V22\Resources\Asset; +use Google\Ads\GoogleAds\V22\Services\AssetOperation; +use Google\Ads\GoogleAds\V22\Services\MutateGoogleAdsRequest; +use Google\Ads\GoogleAds\V22\Services\MutateOperation; +use Google\Ads\GoogleAds\Util\V22\ResourceNames; +use Google\Ads\GoogleAds\V22\Common\TextAsset; +use Google\Ads\GoogleAds\V22\Common\ImageAsset; +use Google\Ads\GoogleAds\V22\Common\CallToActionAsset; +use Google\Ads\GoogleAds\V22\Common\YoutubeVideoAsset; use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WP; use Google\ApiCore\ApiException; use Exception; diff --git a/src/API/Google/AdsAssetGroup.php b/src/API/Google/AdsAssetGroup.php index 8f813d3a58..c17f750ad9 100644 --- a/src/API/Google/AdsAssetGroup.php +++ b/src/API/Google/AdsAssetGroup.php @@ -7,18 +7,18 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Google\Ads\GoogleAdsClient; use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface; use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait; -use Google\Ads\GoogleAds\Util\V21\ResourceNames; -use Google\Ads\GoogleAds\V21\Enums\ListingGroupFilterListingSourceEnum\ListingGroupFilterListingSource; -use Google\Ads\GoogleAds\V21\Enums\AssetGroupStatusEnum\AssetGroupStatus; -use Google\Ads\GoogleAds\V21\Enums\ListingGroupFilterTypeEnum\ListingGroupFilterType; -use Google\Ads\GoogleAds\V21\Resources\AssetGroup; -use Google\Ads\GoogleAds\V21\Resources\AssetGroupListingGroupFilter; -use Google\Ads\GoogleAds\V21\Services\AssetGroupListingGroupFilterOperation; -use Google\Ads\GoogleAds\V21\Services\AssetGroupOperation; -use Google\Ads\GoogleAds\V21\Services\GoogleAdsRow; -use Google\Ads\GoogleAds\V21\Services\MutateGoogleAdsRequest; -use Google\Ads\GoogleAds\V21\Services\MutateOperation; -use Google\Ads\GoogleAds\V21\Services\Client\AssetGroupServiceClient; +use Google\Ads\GoogleAds\Util\V22\ResourceNames; +use Google\Ads\GoogleAds\V22\Enums\ListingGroupFilterListingSourceEnum\ListingGroupFilterListingSource; +use Google\Ads\GoogleAds\V22\Enums\AssetGroupStatusEnum\AssetGroupStatus; +use Google\Ads\GoogleAds\V22\Enums\ListingGroupFilterTypeEnum\ListingGroupFilterType; +use Google\Ads\GoogleAds\V22\Resources\AssetGroup; +use Google\Ads\GoogleAds\V22\Resources\AssetGroupListingGroupFilter; +use Google\Ads\GoogleAds\V22\Services\AssetGroupListingGroupFilterOperation; +use Google\Ads\GoogleAds\V22\Services\AssetGroupOperation; +use Google\Ads\GoogleAds\V22\Services\GoogleAdsRow; +use Google\Ads\GoogleAds\V22\Services\MutateGoogleAdsRequest; +use Google\Ads\GoogleAds\V22\Services\MutateOperation; +use Google\Ads\GoogleAds\V22\Services\Client\AssetGroupServiceClient; use Google\ApiCore\ApiException; use Google\ApiCore\ValidationException; use Google\Protobuf\FieldMask; diff --git a/src/API/Google/AdsAssetGroupAsset.php b/src/API/Google/AdsAssetGroupAsset.php index 751634bbf7..dd6be91c12 100644 --- a/src/API/Google/AdsAssetGroupAsset.php +++ b/src/API/Google/AdsAssetGroupAsset.php @@ -7,13 +7,13 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Google\Ads\GoogleAdsClient; use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface; use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait; -use Google\Ads\GoogleAds\V21\Services\GoogleAdsRow; -use Google\Ads\GoogleAds\V21\Resources\AssetGroupAsset; +use Google\Ads\GoogleAds\V22\Services\GoogleAdsRow; +use Google\Ads\GoogleAds\V22\Resources\AssetGroupAsset; use Google\ApiCore\ApiException; use Automattic\WooCommerce\GoogleListingsAndAds\Exception\ExceptionWithResponseData; -use Google\Ads\GoogleAds\V21\Services\MutateOperation; -use Google\Ads\GoogleAds\V21\Services\AssetGroupAssetOperation; -use Google\Ads\GoogleAds\Util\V21\ResourceNames; +use Google\Ads\GoogleAds\V22\Services\MutateOperation; +use Google\Ads\GoogleAds\V22\Services\AssetGroupAssetOperation; +use Google\Ads\GoogleAds\Util\V22\ResourceNames; diff --git a/src/API/Google/AdsCampaign.php b/src/API/Google/AdsCampaign.php index 30fc93dcf5..dcca194b79 100644 --- a/src/API/Google/AdsCampaign.php +++ b/src/API/Google/AdsCampaign.php @@ -17,17 +17,17 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Options\TransientsInterface; use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WC; use Google\Ads\GoogleAds\Util\FieldMasks; -use Google\Ads\GoogleAds\Util\V21\ResourceNames; -use Google\Ads\GoogleAds\V21\Common\MaximizeConversionValue; -use Google\Ads\GoogleAds\V21\Enums\AdvertisingChannelTypeEnum\AdvertisingChannelType; -use Google\Ads\GoogleAds\V21\Resources\Campaign; -use Google\Ads\GoogleAds\V21\Enums\EuPoliticalAdvertisingStatusEnum\EuPoliticalAdvertisingStatus; -use Google\Ads\GoogleAds\V21\Resources\Campaign\ShoppingSetting; -use Google\Ads\GoogleAds\V21\Services\Client\CampaignServiceClient; -use Google\Ads\GoogleAds\V21\Services\CampaignOperation; -use Google\Ads\GoogleAds\V21\Services\GoogleAdsRow; -use Google\Ads\GoogleAds\V21\Services\MutateGoogleAdsRequest; -use Google\Ads\GoogleAds\V21\Services\MutateOperation; +use Google\Ads\GoogleAds\Util\V22\ResourceNames; +use Google\Ads\GoogleAds\V22\Common\MaximizeConversionValue; +use Google\Ads\GoogleAds\V22\Enums\AdvertisingChannelTypeEnum\AdvertisingChannelType; +use Google\Ads\GoogleAds\V22\Resources\Campaign; +use Google\Ads\GoogleAds\V22\Enums\EuPoliticalAdvertisingStatusEnum\EuPoliticalAdvertisingStatus; +use Google\Ads\GoogleAds\V22\Resources\Campaign\ShoppingSetting; +use Google\Ads\GoogleAds\V22\Services\Client\CampaignServiceClient; +use Google\Ads\GoogleAds\V22\Services\CampaignOperation; +use Google\Ads\GoogleAds\V22\Services\GoogleAdsRow; +use Google\Ads\GoogleAds\V22\Services\MutateGoogleAdsRequest; +use Google\Ads\GoogleAds\V22\Services\MutateOperation; use Google\ApiCore\ApiException; use Google\ApiCore\ValidationException; use Exception; diff --git a/src/API/Google/AdsCampaignBudget.php b/src/API/Google/AdsCampaignBudget.php index 5f1afe0e23..80c2d50564 100644 --- a/src/API/Google/AdsCampaignBudget.php +++ b/src/API/Google/AdsCampaignBudget.php @@ -9,11 +9,11 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface; use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait; use Google\Ads\GoogleAds\Util\FieldMasks; -use Google\Ads\GoogleAds\Util\V21\ResourceNames; -use Google\Ads\GoogleAds\V21\Resources\CampaignBudget; -use Google\Ads\GoogleAds\V21\Services\CampaignBudgetOperation; -use Google\Ads\GoogleAds\V21\Services\Client\CampaignBudgetServiceClient; -use Google\Ads\GoogleAds\V21\Services\MutateOperation; +use Google\Ads\GoogleAds\Util\V22\ResourceNames; +use Google\Ads\GoogleAds\V22\Resources\CampaignBudget; +use Google\Ads\GoogleAds\V22\Services\CampaignBudgetOperation; +use Google\Ads\GoogleAds\V22\Services\Client\CampaignBudgetServiceClient; +use Google\Ads\GoogleAds\V22\Services\MutateOperation; use Google\ApiCore\ValidationException; use Exception; diff --git a/src/API/Google/AdsCampaignCriterion.php b/src/API/Google/AdsCampaignCriterion.php index 9ab50584b8..08dfe877a8 100644 --- a/src/API/Google/AdsCampaignCriterion.php +++ b/src/API/Google/AdsCampaignCriterion.php @@ -3,12 +3,12 @@ namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Google; -use Google\Ads\GoogleAds\Util\V21\ResourceNames; -use Google\Ads\GoogleAds\V21\Common\LocationInfo; -use Google\Ads\GoogleAds\V21\Enums\CampaignCriterionStatusEnum\CampaignCriterionStatus; -use Google\Ads\GoogleAds\V21\Resources\CampaignCriterion; -use Google\Ads\GoogleAds\V21\Services\CampaignCriterionOperation; -use Google\Ads\GoogleAds\V21\Services\MutateOperation; +use Google\Ads\GoogleAds\Util\V22\ResourceNames; +use Google\Ads\GoogleAds\V22\Common\LocationInfo; +use Google\Ads\GoogleAds\V22\Enums\CampaignCriterionStatusEnum\CampaignCriterionStatus; +use Google\Ads\GoogleAds\V22\Resources\CampaignCriterion; +use Google\Ads\GoogleAds\V22\Services\CampaignCriterionOperation; +use Google\Ads\GoogleAds\V22\Services\MutateOperation; /** * Class AdsCampaignCriterion diff --git a/src/API/Google/AdsCampaignLabel.php b/src/API/Google/AdsCampaignLabel.php index 528fc0d0c3..22259c8e71 100644 --- a/src/API/Google/AdsCampaignLabel.php +++ b/src/API/Google/AdsCampaignLabel.php @@ -7,13 +7,13 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Google\Ads\GoogleAdsClient; use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface; use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait; -use Google\Ads\GoogleAds\Util\V21\ResourceNames; -use Google\Ads\GoogleAds\V21\Resources\Label; -use Google\Ads\GoogleAds\V21\Resources\CampaignLabel; -use Google\Ads\GoogleAds\V21\Services\LabelOperation; -use Google\Ads\GoogleAds\V21\Services\CampaignLabelOperation; -use Google\Ads\GoogleAds\V21\Services\MutateOperation; -use Google\Ads\GoogleAds\V21\Services\MutateGoogleAdsRequest; +use Google\Ads\GoogleAds\Util\V22\ResourceNames; +use Google\Ads\GoogleAds\V22\Resources\Label; +use Google\Ads\GoogleAds\V22\Resources\CampaignLabel; +use Google\Ads\GoogleAds\V22\Services\LabelOperation; +use Google\Ads\GoogleAds\V22\Services\CampaignLabelOperation; +use Google\Ads\GoogleAds\V22\Services\MutateOperation; +use Google\Ads\GoogleAds\V22\Services\MutateGoogleAdsRequest; /** * Class AdsCampaignLabel diff --git a/src/API/Google/AdsConversionAction.php b/src/API/Google/AdsConversionAction.php index 21e182d60e..4e9895773b 100644 --- a/src/API/Google/AdsConversionAction.php +++ b/src/API/Google/AdsConversionAction.php @@ -8,19 +8,19 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface; use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait; use Exception; -use Google\Ads\GoogleAds\V21\Common\TagSnippet; -use Google\Ads\GoogleAds\V21\Enums\ConversionActionCategoryEnum\ConversionActionCategory; -use Google\Ads\GoogleAds\V21\Enums\ConversionActionStatusEnum\ConversionActionStatus; -use Google\Ads\GoogleAds\V21\Enums\ConversionActionTypeEnum\ConversionActionType; -use Google\Ads\GoogleAds\V21\Enums\TrackingCodePageFormatEnum\TrackingCodePageFormat; -use Google\Ads\GoogleAds\V21\Enums\TrackingCodeTypeEnum\TrackingCodeType; -use Google\Ads\GoogleAds\V21\Resources\ConversionAction; -use Google\Ads\GoogleAds\V21\Resources\ConversionAction\ValueSettings; -use Google\Ads\GoogleAds\V21\Services\ConversionActionOperation; -use Google\Ads\GoogleAds\V21\Services\Client\ConversionActionServiceClient; -use Google\Ads\GoogleAds\V21\Services\GoogleAdsRow; -use Google\Ads\GoogleAds\V21\Services\MutateConversionActionResult; -use Google\Ads\GoogleAds\V21\Services\MutateConversionActionsRequest; +use Google\Ads\GoogleAds\V22\Common\TagSnippet; +use Google\Ads\GoogleAds\V22\Enums\ConversionActionCategoryEnum\ConversionActionCategory; +use Google\Ads\GoogleAds\V22\Enums\ConversionActionStatusEnum\ConversionActionStatus; +use Google\Ads\GoogleAds\V22\Enums\ConversionActionTypeEnum\ConversionActionType; +use Google\Ads\GoogleAds\V22\Enums\TrackingCodePageFormatEnum\TrackingCodePageFormat; +use Google\Ads\GoogleAds\V22\Enums\TrackingCodeTypeEnum\TrackingCodeType; +use Google\Ads\GoogleAds\V22\Resources\ConversionAction; +use Google\Ads\GoogleAds\V22\Resources\ConversionAction\ValueSettings; +use Google\Ads\GoogleAds\V22\Services\ConversionActionOperation; +use Google\Ads\GoogleAds\V22\Services\Client\ConversionActionServiceClient; +use Google\Ads\GoogleAds\V22\Services\GoogleAdsRow; +use Google\Ads\GoogleAds\V22\Services\MutateConversionActionResult; +use Google\Ads\GoogleAds\V22\Services\MutateConversionActionsRequest; use Google\ApiCore\ApiException; /** diff --git a/src/API/Google/AdsReport.php b/src/API/Google/AdsReport.php index 1f05af7161..ced62e7c07 100644 --- a/src/API/Google/AdsReport.php +++ b/src/API/Google/AdsReport.php @@ -15,8 +15,8 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface; use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait; use DateTime; -use Google\Ads\GoogleAds\V21\Common\Segments; -use Google\Ads\GoogleAds\V21\Services\GoogleAdsRow; +use Google\Ads\GoogleAds\V22\Common\Segments; +use Google\Ads\GoogleAds\V22\Services\GoogleAdsRow; use Google\ApiCore\ApiException; /** diff --git a/src/API/Google/AssetFieldType.php b/src/API/Google/AssetFieldType.php index dbb782f5d4..160298f6ce 100644 --- a/src/API/Google/AssetFieldType.php +++ b/src/API/Google/AssetFieldType.php @@ -3,7 +3,7 @@ namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Google; -use Google\Ads\GoogleAds\V21\Enums\AssetFieldTypeEnum\AssetFieldType as AdsAssetFieldType; +use Google\Ads\GoogleAds\V22\Enums\AssetFieldTypeEnum\AssetFieldType as AdsAssetFieldType; use Automattic\WooCommerce\GoogleListingsAndAds\Internal\StatusMapping; use UnexpectedValueException; diff --git a/src/API/Google/BillingSetupStatus.php b/src/API/Google/BillingSetupStatus.php index bb092e9a3a..547e117aea 100644 --- a/src/API/Google/BillingSetupStatus.php +++ b/src/API/Google/BillingSetupStatus.php @@ -3,7 +3,7 @@ namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Google; -use Google\Ads\GoogleAds\V21\Enums\BillingSetupStatusEnum\BillingSetupStatus as AdsBillingSetupStatus; +use Google\Ads\GoogleAds\V22\Enums\BillingSetupStatusEnum\BillingSetupStatus as AdsBillingSetupStatus; use Automattic\WooCommerce\GoogleListingsAndAds\Internal\StatusMapping; /** diff --git a/src/API/Google/BudgetMetrics.php b/src/API/Google/BudgetMetrics.php index 084a71efbb..640d4637e8 100644 --- a/src/API/Google/BudgetMetrics.php +++ b/src/API/Google/BudgetMetrics.php @@ -12,14 +12,14 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Options\TransientsAwareTrait; use Automattic\WooCommerce\GoogleListingsAndAds\Options\TransientsInterface; use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper; -use Google\Ads\GoogleAds\V21\Enums\AdvertisingChannelTypeEnum\AdvertisingChannelType; -use Google\Ads\GoogleAds\V21\Enums\BiddingStrategyTypeEnum\BiddingStrategyType; -use Google\Ads\GoogleAds\V21\Enums\RecommendationTypeEnum\RecommendationType; -use Google\Ads\GoogleAds\V21\Resources\Recommendation\CampaignBudgetRecommendation; -use Google\Ads\GoogleAds\V21\Services\GenerateRecommendationsRequest; -use Google\Ads\GoogleAds\V21\Services\GenerateRecommendationsRequest\AssetGroupInfo; -use Google\Ads\GoogleAds\V21\Services\GenerateRecommendationsRequest\BiddingInfo; -use Google\Ads\GoogleAds\V21\Services\GenerateRecommendationsRequest\BudgetInfo; +use Google\Ads\GoogleAds\V22\Enums\AdvertisingChannelTypeEnum\AdvertisingChannelType; +use Google\Ads\GoogleAds\V22\Enums\BiddingStrategyTypeEnum\BiddingStrategyType; +use Google\Ads\GoogleAds\V22\Enums\RecommendationTypeEnum\RecommendationType; +use Google\Ads\GoogleAds\V22\Resources\Recommendation\CampaignBudgetRecommendation; +use Google\Ads\GoogleAds\V22\Services\GenerateRecommendationsRequest; +use Google\Ads\GoogleAds\V22\Services\GenerateRecommendationsRequest\AssetGroupInfo; +use Google\Ads\GoogleAds\V22\Services\GenerateRecommendationsRequest\BiddingInfo; +use Google\Ads\GoogleAds\V22\Services\GenerateRecommendationsRequest\BudgetInfo; use Google\ApiCore\ApiException; /** diff --git a/src/API/Google/BudgetRecommendations.php b/src/API/Google/BudgetRecommendations.php index 7611d955fd..c489f7dd89 100644 --- a/src/API/Google/BudgetRecommendations.php +++ b/src/API/Google/BudgetRecommendations.php @@ -12,13 +12,13 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Options\TransientsAwareTrait; use Automattic\WooCommerce\GoogleListingsAndAds\Options\TransientsInterface; use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper; -use Google\Ads\GoogleAds\V21\Enums\AdvertisingChannelTypeEnum\AdvertisingChannelType; -use Google\Ads\GoogleAds\V21\Enums\BiddingStrategyTypeEnum\BiddingStrategyType; -use Google\Ads\GoogleAds\V21\Enums\RecommendationTypeEnum\RecommendationType; -use Google\Ads\GoogleAds\V21\Resources\Recommendation\CampaignBudgetRecommendation; -use Google\Ads\GoogleAds\V21\Services\GenerateRecommendationsRequest; -use Google\Ads\GoogleAds\V21\Services\GenerateRecommendationsRequest\AssetGroupInfo; -use Google\Ads\GoogleAds\V21\Services\GenerateRecommendationsRequest\BiddingInfo; +use Google\Ads\GoogleAds\V22\Enums\AdvertisingChannelTypeEnum\AdvertisingChannelType; +use Google\Ads\GoogleAds\V22\Enums\BiddingStrategyTypeEnum\BiddingStrategyType; +use Google\Ads\GoogleAds\V22\Enums\RecommendationTypeEnum\RecommendationType; +use Google\Ads\GoogleAds\V22\Resources\Recommendation\CampaignBudgetRecommendation; +use Google\Ads\GoogleAds\V22\Services\GenerateRecommendationsRequest; +use Google\Ads\GoogleAds\V22\Services\GenerateRecommendationsRequest\AssetGroupInfo; +use Google\Ads\GoogleAds\V22\Services\GenerateRecommendationsRequest\BiddingInfo; use Google\ApiCore\ApiException; /** diff --git a/src/API/Google/CallToActionType.php b/src/API/Google/CallToActionType.php index 966b9f36f8..bf85248317 100644 --- a/src/API/Google/CallToActionType.php +++ b/src/API/Google/CallToActionType.php @@ -3,7 +3,7 @@ namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Google; -use Google\Ads\GoogleAds\V21\Enums\CallToActionTypeEnum\CallToActionType as AdsCallToActionType; +use Google\Ads\GoogleAds\V22\Enums\CallToActionTypeEnum\CallToActionType as AdsCallToActionType; use Automattic\WooCommerce\GoogleListingsAndAds\Internal\StatusMapping; diff --git a/src/API/Google/CampaignStatus.php b/src/API/Google/CampaignStatus.php index 2928951e0a..b0d6fb9550 100644 --- a/src/API/Google/CampaignStatus.php +++ b/src/API/Google/CampaignStatus.php @@ -3,7 +3,7 @@ namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Google; -use Google\Ads\GoogleAds\V21\Enums\CampaignStatusEnum\CampaignStatus as AdsCampaignStatus; +use Google\Ads\GoogleAds\V22\Enums\CampaignStatusEnum\CampaignStatus as AdsCampaignStatus; use Automattic\WooCommerce\GoogleListingsAndAds\Internal\StatusMapping; /** diff --git a/src/API/Google/CampaignType.php b/src/API/Google/CampaignType.php index 03f28f179d..ed2d7d1f9b 100644 --- a/src/API/Google/CampaignType.php +++ b/src/API/Google/CampaignType.php @@ -3,7 +3,7 @@ namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Google; -use Google\Ads\GoogleAds\V21\Enums\AdvertisingChannelTypeEnum\AdvertisingChannelType as AdsCampaignType; +use Google\Ads\GoogleAds\V22\Enums\AdvertisingChannelTypeEnum\AdvertisingChannelType as AdsCampaignType; use Automattic\WooCommerce\GoogleListingsAndAds\Internal\StatusMapping; /** diff --git a/src/API/Google/MerchantMetrics.php b/src/API/Google/MerchantMetrics.php index d879561bd3..cad3fe3177 100644 --- a/src/API/Google/MerchantMetrics.php +++ b/src/API/Google/MerchantMetrics.php @@ -15,7 +15,7 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\SearchResponse; use DateTime; use Exception; -use Google\Ads\GoogleAds\V21\Services\GoogleAdsRow; +use Google\Ads\GoogleAds\V22\Services\GoogleAdsRow; use Google\ApiCore\PagedListResponse; /** diff --git a/src/API/Google/Query/AdsQuery.php b/src/API/Google/Query/AdsQuery.php index 9c9e4c2e2f..8b3023369c 100644 --- a/src/API/Google/Query/AdsQuery.php +++ b/src/API/Google/Query/AdsQuery.php @@ -5,9 +5,9 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Exception\InvalidProperty; use Automattic\WooCommerce\GoogleListingsAndAds\Google\Ads\GoogleAdsClient; -use Google\Ads\GoogleAds\V21\Services\GoogleAdsRow; -use Google\Ads\GoogleAds\V21\Services\SearchGoogleAdsRequest; -use Google\Ads\GoogleAds\V21\Services\SearchSettings; +use Google\Ads\GoogleAds\V22\Services\GoogleAdsRow; +use Google\Ads\GoogleAds\V22\Services\SearchGoogleAdsRequest; +use Google\Ads\GoogleAds\V22\Services\SearchSettings; use Google\ApiCore\ApiException; defined( 'ABSPATH' ) || exit; diff --git a/src/API/Google/Query/AdsReportQuery.php b/src/API/Google/Query/AdsReportQuery.php index dab79aba86..30e8a1354c 100644 --- a/src/API/Google/Query/AdsReportQuery.php +++ b/src/API/Google/Query/AdsReportQuery.php @@ -3,7 +3,7 @@ namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Query; -use Google\Ads\GoogleAds\V21\Resources\ShoppingPerformanceView; +use Google\Ads\GoogleAds\V22\Resources\ShoppingPerformanceView; defined( 'ABSPATH' ) || exit; diff --git a/src/Ads/AdsRecommendationsService.php b/src/Ads/AdsRecommendationsService.php index 0338244b52..8d9716730a 100644 --- a/src/Ads/AdsRecommendationsService.php +++ b/src/Ads/AdsRecommendationsService.php @@ -15,8 +15,8 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Options\TransientsAwareTrait; use Automattic\WooCommerce\GoogleListingsAndAds\Options\TransientsInterface; use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Exception as GoogleException; -use Google\Ads\GoogleAds\V21\Resources\Recommendation; -use Google\Ads\GoogleAds\V21\Enums\RecommendationTypeEnum\RecommendationType; +use Google\Ads\GoogleAds\V22\Resources\Recommendation; +use Google\Ads\GoogleAds\V22\Enums\RecommendationTypeEnum\RecommendationType; use Exception; defined( 'ABSPATH' ) || exit; diff --git a/src/Google/Ads/ServiceClientFactoryTrait.php b/src/Google/Ads/ServiceClientFactoryTrait.php index d697b41ada..e1a575b2fb 100644 --- a/src/Google/Ads/ServiceClientFactoryTrait.php +++ b/src/Google/Ads/ServiceClientFactoryTrait.php @@ -13,25 +13,25 @@ use Google\Ads\GoogleAds\Constants; use Google\Ads\GoogleAds\Lib\ConfigurationTrait; -use Google\Ads\GoogleAds\V21\Services\Client\AccountLinkServiceClient; -use Google\Ads\GoogleAds\V21\Services\Client\AdGroupAdLabelServiceClient; -use Google\Ads\GoogleAds\V21\Services\Client\AdGroupAdServiceClient; -use Google\Ads\GoogleAds\V21\Services\Client\AdGroupCriterionServiceClient; -use Google\Ads\GoogleAds\V21\Services\Client\AdGroupServiceClient; -use Google\Ads\GoogleAds\V21\Services\Client\AdServiceClient; -use Google\Ads\GoogleAds\V21\Services\Client\AssetGroupListingGroupFilterServiceClient; -use Google\Ads\GoogleAds\V21\Services\Client\AssetGroupServiceClient; -use Google\Ads\GoogleAds\V21\Services\Client\BillingSetupServiceClient; -use Google\Ads\GoogleAds\V21\Services\Client\CampaignBudgetServiceClient; -use Google\Ads\GoogleAds\V21\Services\Client\CampaignCriterionServiceClient; -use Google\Ads\GoogleAds\V21\Services\Client\CampaignServiceClient; -use Google\Ads\GoogleAds\V21\Services\Client\ConversionActionServiceClient; -use Google\Ads\GoogleAds\V21\Services\Client\CustomerServiceClient; -use Google\Ads\GoogleAds\V21\Services\Client\CustomerUserAccessServiceClient; -use Google\Ads\GoogleAds\V21\Services\Client\GeoTargetConstantServiceClient; -use Google\Ads\GoogleAds\V21\Services\Client\GoogleAdsServiceClient; -use Google\Ads\GoogleAds\V21\Services\Client\ProductLinkInvitationServiceClient; -use Google\Ads\GoogleAds\V21\Services\Client\RecommendationServiceClient; +use Google\Ads\GoogleAds\V22\Services\Client\AccountLinkServiceClient; +use Google\Ads\GoogleAds\V22\Services\Client\AdGroupAdLabelServiceClient; +use Google\Ads\GoogleAds\V22\Services\Client\AdGroupAdServiceClient; +use Google\Ads\GoogleAds\V22\Services\Client\AdGroupCriterionServiceClient; +use Google\Ads\GoogleAds\V22\Services\Client\AdGroupServiceClient; +use Google\Ads\GoogleAds\V22\Services\Client\AdServiceClient; +use Google\Ads\GoogleAds\V22\Services\Client\AssetGroupListingGroupFilterServiceClient; +use Google\Ads\GoogleAds\V22\Services\Client\AssetGroupServiceClient; +use Google\Ads\GoogleAds\V22\Services\Client\BillingSetupServiceClient; +use Google\Ads\GoogleAds\V22\Services\Client\CampaignBudgetServiceClient; +use Google\Ads\GoogleAds\V22\Services\Client\CampaignCriterionServiceClient; +use Google\Ads\GoogleAds\V22\Services\Client\CampaignServiceClient; +use Google\Ads\GoogleAds\V22\Services\Client\ConversionActionServiceClient; +use Google\Ads\GoogleAds\V22\Services\Client\CustomerServiceClient; +use Google\Ads\GoogleAds\V22\Services\Client\CustomerUserAccessServiceClient; +use Google\Ads\GoogleAds\V22\Services\Client\GeoTargetConstantServiceClient; +use Google\Ads\GoogleAds\V22\Services\Client\GoogleAdsServiceClient; +use Google\Ads\GoogleAds\V22\Services\Client\ProductLinkInvitationServiceClient; +use Google\Ads\GoogleAds\V22\Services\Client\RecommendationServiceClient; /** * Contains service client factory methods. diff --git a/src/Internal/DependencyManagement/GoogleServiceProvider.php b/src/Internal/DependencyManagement/GoogleServiceProvider.php index f2715c8d9d..678abd2d72 100644 --- a/src/Internal/DependencyManagement/GoogleServiceProvider.php +++ b/src/Internal/DependencyManagement/GoogleServiceProvider.php @@ -49,7 +49,7 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\League\Container\Definition\Definition; use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Psr\Http\Message\RequestInterface; use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Psr\Http\Message\ResponseInterface; -use Google\Ads\GoogleAds\Util\V21\GoogleAdsFailures; +use Google\Ads\GoogleAds\Util\V22\GoogleAdsFailures; use Jetpack_Options; defined( 'ABSPATH' ) || exit; diff --git a/tests/Tools/HelperTrait/GoogleAdsClientTrait.php b/tests/Tools/HelperTrait/GoogleAdsClientTrait.php index 02534244cc..32112ec846 100644 --- a/tests/Tools/HelperTrait/GoogleAdsClientTrait.php +++ b/tests/Tools/HelperTrait/GoogleAdsClientTrait.php @@ -10,62 +10,62 @@ use Automattic\WooCommerce\GoogleListingsAndAds\API\MicroTrait; use Automattic\WooCommerce\GoogleListingsAndAds\Google\Ads\GoogleAdsClient; use Exception; -use Google\Ads\GoogleAds\Util\V21\ResourceNames; -use Google\Ads\GoogleAds\V21\Common\LocationInfo; -use Google\Ads\GoogleAds\V21\Common\Metrics; -use Google\Ads\GoogleAds\V21\Common\Segments; -use Google\Ads\GoogleAds\V21\Common\TagSnippet; -use Google\Ads\GoogleAds\V21\Common\ImageAsset; -use Google\Ads\GoogleAds\V21\Common\TextAsset; -use Google\Ads\GoogleAds\V21\Common\CallToActionAsset; -use Google\Ads\GoogleAds\V21\Common\ImageDimension; -use Google\Ads\GoogleAds\V21\Common\YoutubeVideoAsset; -use Google\Ads\GoogleAds\V21\Enums\AccessRoleEnum\AccessRole; -use Google\Ads\GoogleAds\V21\Enums\CampaignStatusEnum\CampaignStatus as AdsCampaignStatus; -use Google\Ads\GoogleAds\V21\Enums\AdvertisingChannelTypeEnum\AdvertisingChannelType as AdsCampaignType; -use Google\Ads\GoogleAds\V21\Enums\AssetTypeEnum\AssetType; -use Google\Ads\GoogleAds\V21\Enums\TrackingCodePageFormatEnum\TrackingCodePageFormat; -use Google\Ads\GoogleAds\V21\Enums\TrackingCodeTypeEnum\TrackingCodeType; -use Google\Ads\GoogleAds\V21\Resources\BillingSetup; -use Google\Ads\GoogleAds\V21\Resources\Campaign; -use Google\Ads\GoogleAds\V21\Resources\Label; -use Google\Ads\GoogleAds\V21\Resources\Asset; -use Google\Ads\GoogleAds\V21\Resources\AssetGroup; -use Google\Ads\GoogleAds\V21\Resources\AssetGroupAsset; -use Google\Ads\GoogleAds\V21\Services\AssetGroupAssetOperation; -use Google\Ads\GoogleAds\V21\Resources\CampaignBudget; -use Google\Ads\GoogleAds\V21\Resources\CampaignCriterion; -use Google\Ads\GoogleAds\V21\Resources\Campaign\ShoppingSetting; -use Google\Ads\GoogleAds\V21\Resources\ConversionAction; -use Google\Ads\GoogleAds\V21\Resources\Customer; -use Google\Ads\GoogleAds\V21\Resources\CustomerUserAccess; -use Google\Ads\GoogleAds\V21\Resources\GeoTargetConstant; -use Google\Ads\GoogleAds\V21\Resources\Recommendation; -use Google\Ads\GoogleAds\V21\Resources\Recommendation\CampaignBudgetRecommendation; -use Google\Ads\GoogleAds\V21\Resources\Recommendation\CampaignBudgetRecommendation\CampaignBudgetRecommendationOption; -use Google\Ads\GoogleAds\V21\Resources\Recommendation\RecommendationImpact; -use Google\Ads\GoogleAds\V21\Resources\Recommendation\RecommendationMetrics; -use Google\Ads\GoogleAds\V21\Resources\ShoppingPerformanceView; -use Google\Ads\GoogleAds\V21\Services\Client\ConversionActionServiceClient; -use Google\Ads\GoogleAds\V21\Services\Client\CustomerServiceClient; -use Google\Ads\GoogleAds\V21\Services\Client\GoogleAdsServiceClient; -use Google\Ads\GoogleAds\V21\Services\Client\ProductLinkInvitationServiceClient; -use Google\Ads\GoogleAds\V21\Services\Client\RecommendationServiceClient; -use Google\Ads\GoogleAds\V21\Services\GenerateRecommendationsResponse; -use Google\Ads\GoogleAds\V21\Services\GoogleAdsRow; -use Google\Ads\GoogleAds\V21\Services\ListAccessibleCustomersResponse; -use Google\Ads\GoogleAds\V21\Services\MutateCampaignResult; -use Google\Ads\GoogleAds\V21\Services\MutateLabelResult; -use Google\Ads\GoogleAds\V21\Services\MutateConversionActionResult; -use Google\Ads\GoogleAds\V21\Services\MutateConversionActionsRequest; -use Google\Ads\GoogleAds\V21\Services\MutateConversionActionsResponse; -use Google\Ads\GoogleAds\V21\Services\MutateGoogleAdsRequest; -use Google\Ads\GoogleAds\V21\Services\MutateGoogleAdsResponse; -use Google\Ads\GoogleAds\V21\Services\MutateOperationResponse; -use Google\Ads\GoogleAds\V21\Services\MutateOperation; -use Google\Ads\GoogleAds\V21\Services\MutateAssetGroupResult; -use Google\Ads\GoogleAds\V21\Services\MutateAssetResult; -use Google\Ads\GoogleAds\V21\Services\SearchGoogleAdsResponse; +use Google\Ads\GoogleAds\Util\V22\ResourceNames; +use Google\Ads\GoogleAds\V22\Common\LocationInfo; +use Google\Ads\GoogleAds\V22\Common\Metrics; +use Google\Ads\GoogleAds\V22\Common\Segments; +use Google\Ads\GoogleAds\V22\Common\TagSnippet; +use Google\Ads\GoogleAds\V22\Common\ImageAsset; +use Google\Ads\GoogleAds\V22\Common\TextAsset; +use Google\Ads\GoogleAds\V22\Common\CallToActionAsset; +use Google\Ads\GoogleAds\V22\Common\ImageDimension; +use Google\Ads\GoogleAds\V22\Common\YoutubeVideoAsset; +use Google\Ads\GoogleAds\V22\Enums\AccessRoleEnum\AccessRole; +use Google\Ads\GoogleAds\V22\Enums\CampaignStatusEnum\CampaignStatus as AdsCampaignStatus; +use Google\Ads\GoogleAds\V22\Enums\AdvertisingChannelTypeEnum\AdvertisingChannelType as AdsCampaignType; +use Google\Ads\GoogleAds\V22\Enums\AssetTypeEnum\AssetType; +use Google\Ads\GoogleAds\V22\Enums\TrackingCodePageFormatEnum\TrackingCodePageFormat; +use Google\Ads\GoogleAds\V22\Enums\TrackingCodeTypeEnum\TrackingCodeType; +use Google\Ads\GoogleAds\V22\Resources\BillingSetup; +use Google\Ads\GoogleAds\V22\Resources\Campaign; +use Google\Ads\GoogleAds\V22\Resources\Label; +use Google\Ads\GoogleAds\V22\Resources\Asset; +use Google\Ads\GoogleAds\V22\Resources\AssetGroup; +use Google\Ads\GoogleAds\V22\Resources\AssetGroupAsset; +use Google\Ads\GoogleAds\V22\Services\AssetGroupAssetOperation; +use Google\Ads\GoogleAds\V22\Resources\CampaignBudget; +use Google\Ads\GoogleAds\V22\Resources\CampaignCriterion; +use Google\Ads\GoogleAds\V22\Resources\Campaign\ShoppingSetting; +use Google\Ads\GoogleAds\V22\Resources\ConversionAction; +use Google\Ads\GoogleAds\V22\Resources\Customer; +use Google\Ads\GoogleAds\V22\Resources\CustomerUserAccess; +use Google\Ads\GoogleAds\V22\Resources\GeoTargetConstant; +use Google\Ads\GoogleAds\V22\Resources\Recommendation; +use Google\Ads\GoogleAds\V22\Resources\Recommendation\CampaignBudgetRecommendation; +use Google\Ads\GoogleAds\V22\Resources\Recommendation\CampaignBudgetRecommendation\CampaignBudgetRecommendationOption; +use Google\Ads\GoogleAds\V22\Resources\Recommendation\RecommendationImpact; +use Google\Ads\GoogleAds\V22\Resources\Recommendation\RecommendationMetrics; +use Google\Ads\GoogleAds\V22\Resources\ShoppingPerformanceView; +use Google\Ads\GoogleAds\V22\Services\Client\ConversionActionServiceClient; +use Google\Ads\GoogleAds\V22\Services\Client\CustomerServiceClient; +use Google\Ads\GoogleAds\V22\Services\Client\GoogleAdsServiceClient; +use Google\Ads\GoogleAds\V22\Services\Client\ProductLinkInvitationServiceClient; +use Google\Ads\GoogleAds\V22\Services\Client\RecommendationServiceClient; +use Google\Ads\GoogleAds\V22\Services\GenerateRecommendationsResponse; +use Google\Ads\GoogleAds\V22\Services\GoogleAdsRow; +use Google\Ads\GoogleAds\V22\Services\ListAccessibleCustomersResponse; +use Google\Ads\GoogleAds\V22\Services\MutateCampaignResult; +use Google\Ads\GoogleAds\V22\Services\MutateLabelResult; +use Google\Ads\GoogleAds\V22\Services\MutateConversionActionResult; +use Google\Ads\GoogleAds\V22\Services\MutateConversionActionsRequest; +use Google\Ads\GoogleAds\V22\Services\MutateConversionActionsResponse; +use Google\Ads\GoogleAds\V22\Services\MutateGoogleAdsRequest; +use Google\Ads\GoogleAds\V22\Services\MutateGoogleAdsResponse; +use Google\Ads\GoogleAds\V22\Services\MutateOperationResponse; +use Google\Ads\GoogleAds\V22\Services\MutateOperation; +use Google\Ads\GoogleAds\V22\Services\MutateAssetGroupResult; +use Google\Ads\GoogleAds\V22\Services\MutateAssetResult; +use Google\Ads\GoogleAds\V22\Services\SearchGoogleAdsResponse; use Google\ApiCore\ApiException; use Google\ApiCore\Page; use Google\ApiCore\PagedListResponse; diff --git a/tests/Unit/API/Google/AdsAssetGroupAssetTest.php b/tests/Unit/API/Google/AdsAssetGroupAssetTest.php index 69247a8622..51c39f7b37 100644 --- a/tests/Unit/API/Google/AdsAssetGroupAssetTest.php +++ b/tests/Unit/API/Google/AdsAssetGroupAssetTest.php @@ -10,8 +10,8 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Tests\Tools\HelperTrait\GoogleAdsClientTrait; use PHPUnit\Framework\MockObject\MockObject; use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\AssetFieldType; -use Google\Ads\GoogleAds\V21\Enums\AssetTypeEnum\AssetType; -use Google\Ads\GoogleAds\Util\V21\ResourceNames; +use Google\Ads\GoogleAds\V22\Enums\AssetTypeEnum\AssetType; +use Google\Ads\GoogleAds\Util\V22\ResourceNames; defined( 'ABSPATH' ) || exit; diff --git a/tests/Unit/API/Google/AdsAssetGroupTest.php b/tests/Unit/API/Google/AdsAssetGroupTest.php index bb4e192ea5..7099054553 100644 --- a/tests/Unit/API/Google/AdsAssetGroupTest.php +++ b/tests/Unit/API/Google/AdsAssetGroupTest.php @@ -8,9 +8,9 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsInterface; use Automattic\WooCommerce\GoogleListingsAndAds\Tests\Framework\UnitTest; use Automattic\WooCommerce\GoogleListingsAndAds\Tests\Tools\HelperTrait\GoogleAdsClientTrait; -use Google\Ads\GoogleAds\V21\Enums\AssetGroupStatusEnum\AssetGroupStatus; -use Google\Ads\GoogleAds\V21\Enums\ListingGroupFilterListingSourceEnum\ListingGroupFilterListingSource; -use Google\Ads\GoogleAds\V21\Enums\ListingGroupFilterTypeEnum\ListingGroupFilterType; +use Google\Ads\GoogleAds\V22\Enums\AssetGroupStatusEnum\AssetGroupStatus; +use Google\Ads\GoogleAds\V22\Enums\ListingGroupFilterListingSourceEnum\ListingGroupFilterListingSource; +use Google\Ads\GoogleAds\V22\Enums\ListingGroupFilterTypeEnum\ListingGroupFilterType; use PHPUnit\Framework\MockObject\MockObject; use Google\ApiCore\ApiException; use Automattic\WooCommerce\GoogleListingsAndAds\Exception\ExceptionWithResponseData; diff --git a/tests/Unit/API/Google/AdsAssetTest.php b/tests/Unit/API/Google/AdsAssetTest.php index ad9a9fa0e7..66726b610f 100644 --- a/tests/Unit/API/Google/AdsAssetTest.php +++ b/tests/Unit/API/Google/AdsAssetTest.php @@ -10,7 +10,7 @@ use PHPUnit\Framework\MockObject\MockObject; use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\AssetFieldType; use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\CallToActionType; -use Google\Ads\GoogleAds\Util\V21\ResourceNames; +use Google\Ads\GoogleAds\Util\V22\ResourceNames; use Automattic\WooCommerce\GoogleListingsAndAds\Proxies\WP; use Exception; use WP_Error; diff --git a/tests/Unit/API/Google/AdsCampaignCriterionTest.php b/tests/Unit/API/Google/AdsCampaignCriterionTest.php index 8eba9904cd..17be13ccf4 100644 --- a/tests/Unit/API/Google/AdsCampaignCriterionTest.php +++ b/tests/Unit/API/Google/AdsCampaignCriterionTest.php @@ -6,7 +6,7 @@ use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\AdsCampaignCriterion; use Automattic\WooCommerce\GoogleListingsAndAds\Tests\Framework\UnitTest; use Automattic\WooCommerce\GoogleListingsAndAds\Tests\Tools\HelperTrait\GoogleAdsClientTrait; -use Google\Ads\GoogleAds\V21\Enums\CampaignCriterionStatusEnum\CampaignCriterionStatus; +use Google\Ads\GoogleAds\V22\Enums\CampaignCriterionStatusEnum\CampaignCriterionStatus; defined( 'ABSPATH' ) || exit; diff --git a/tests/Unit/API/Google/AdsConversionActionTest.php b/tests/Unit/API/Google/AdsConversionActionTest.php index ba4c6ec022..5f6056b96b 100644 --- a/tests/Unit/API/Google/AdsConversionActionTest.php +++ b/tests/Unit/API/Google/AdsConversionActionTest.php @@ -8,7 +8,7 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Tests\Framework\UnitTest; use Automattic\WooCommerce\GoogleListingsAndAds\Tests\Tools\HelperTrait\GoogleAdsClientTrait; use Exception; -use Google\Ads\GoogleAds\V21\Enums\ConversionActionStatusEnum\ConversionActionStatus; +use Google\Ads\GoogleAds\V22\Enums\ConversionActionStatusEnum\ConversionActionStatus; use Google\ApiCore\ApiException; use PHPUnit\Framework\MockObject\MockObject; diff --git a/tests/Unit/API/Google/AdsTest.php b/tests/Unit/API/Google/AdsTest.php index ce9ad5be5c..7dea294866 100644 --- a/tests/Unit/API/Google/AdsTest.php +++ b/tests/Unit/API/Google/AdsTest.php @@ -10,10 +10,10 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Tests\Framework\UnitTest; use Automattic\WooCommerce\GoogleListingsAndAds\Tests\Tools\HelperTrait\GoogleAdsClientTrait; use Exception; -use Google\Ads\GoogleAds\V21\Enums\BillingSetupStatusEnum\BillingSetupStatus as AdsBillingSetupStatus; -use Google\Ads\GoogleAds\V21\Enums\ProductLinkInvitationStatusEnum\ProductLinkInvitationStatus; -use Google\Ads\GoogleAds\V21\Resources\MerchantCenterLinkInvitationIdentifier; -use Google\Ads\GoogleAds\V21\Resources\ProductLinkInvitation; +use Google\Ads\GoogleAds\V22\Enums\BillingSetupStatusEnum\BillingSetupStatus as AdsBillingSetupStatus; +use Google\Ads\GoogleAds\V22\Enums\ProductLinkInvitationStatusEnum\ProductLinkInvitationStatus; +use Google\Ads\GoogleAds\V22\Resources\MerchantCenterLinkInvitationIdentifier; +use Google\Ads\GoogleAds\V22\Resources\ProductLinkInvitation; use Google\ApiCore\ApiException; use PHPUnit\Framework\MockObject\MockObject; diff --git a/tests/Unit/API/Google/MerchantMetricsTest.php b/tests/Unit/API/Google/MerchantMetricsTest.php index 8713929ac5..980d6720bf 100644 --- a/tests/Unit/API/Google/MerchantMetricsTest.php +++ b/tests/Unit/API/Google/MerchantMetricsTest.php @@ -16,9 +16,9 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\SearchResponse; use Automattic\WooCommerce\GoogleListingsAndAds\Vendor\Google\Service\ShoppingContent\Resource\Reports; use DateTime; -use Google\Ads\GoogleAds\V21\Common\Metrics as AdMetrics; -use Google\Ads\GoogleAds\V21\Services\GoogleAdsRow; -use Google\Ads\GoogleAds\V21\Services\Client\GoogleAdsServiceClient; +use Google\Ads\GoogleAds\V22\Common\Metrics as AdMetrics; +use Google\Ads\GoogleAds\V22\Services\GoogleAdsRow; +use Google\Ads\GoogleAds\V22\Services\Client\GoogleAdsServiceClient; use Google\ApiCore\Page; use Google\ApiCore\PagedListResponse; use PHPUnit\Framework\MockObject\MockObject; From 5a9ac176e6c678a74d3f8a105ee33054e7ba8cf5 Mon Sep 17 00:00:00 2001 From: asvinb Date: Thu, 15 Jan 2026 16:13:58 +0400 Subject: [PATCH 018/123] feat(jest): Update moduleNameMapper regex for SVG handling --- jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jest.config.js b/jest.config.js index e642a11e17..f03a9dd236 100644 --- a/jest.config.js +++ b/jest.config.js @@ -12,7 +12,7 @@ module.exports = { ], moduleNameMapper: { '\\.(png|jpg)$': '/tests/mocks/assets/imageMock.js', - '\\.svg$': '/tests/mocks/assets/svgrMock.js', + '\\.svg(\\?inline)?$': '/tests/mocks/assets/svgrMock.js', '\\.scss$': '/tests/mocks/assets/styleMock.js', // Transform our `~/` alias. '^~/(.*)$': '/js/src/$1', From 41301247146a5094a19dfa093d08ccf380048a7a Mon Sep 17 00:00:00 2001 From: asvinb Date: Thu, 15 Jan 2026 16:53:06 +0400 Subject: [PATCH 019/123] Update Jest config and mock SVG handling --- jest.config.js | 3 ++- .../asset-group/asset-group-editor/texts-editor.js | 1 + tests/e2e/utils/pages/create-campaign.js | 12 +++++++++--- tests/mocks/assets/svgrMock.js | 14 +++++++++----- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/jest.config.js b/jest.config.js index f03a9dd236..7a9808961d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -12,7 +12,8 @@ module.exports = { ], moduleNameMapper: { '\\.(png|jpg)$': '/tests/mocks/assets/imageMock.js', - '\\.svg(\\?inline)?$': '/tests/mocks/assets/svgrMock.js', + '\\.svg\\?inline$': '/tests/mocks/assets/svgrMock.js', + '\\.svg$': '/tests/mocks/assets/svgrMock.js', '\\.scss$': '/tests/mocks/assets/styleMock.js', // Transform our `~/` alias. '^~/(.*)$': '/js/src/$1', diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js b/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js index 1c365dce5e..2d1f7deabc 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js @@ -253,6 +253,7 @@ export default function TextsEditor( { minNumberOfTexts > 0 && minNumberOfTexts === maxNumberOfTexts } + aria-label={ __( 'Add text', 'google-listings-and-ads' ) } disabled={ maxNumberOfTexts > 0 && texts.length >= maxNumberOfTexts } diff --git a/tests/e2e/utils/pages/create-campaign.js b/tests/e2e/utils/pages/create-campaign.js index e8770f5ee3..3abd5fc1ac 100644 --- a/tests/e2e/utils/pages/create-campaign.js +++ b/tests/e2e/utils/pages/create-campaign.js @@ -124,7 +124,9 @@ export default class CreateCampaignPage extends MockRequests { * @return {import('@playwright/test').Locator} Get Add headline button. */ getAddHeadlineButton() { - return this.page.getByRole( 'button', { name: 'Add headline' } ); + return this.page + .getByRole( 'button', { name: 'Add text' } ) + .filter( { hasText: 'Add headline' } ); } /** @@ -133,7 +135,9 @@ export default class CreateCampaignPage extends MockRequests { * @return {import('@playwright/test').Locator} Get Add long headline button. */ getAddLongHeadlineButton() { - return this.page.getByRole( 'button', { name: 'Add long headline' } ); + return this.page + .getByRole( 'button', { name: 'Add text' } ) + .filter( { hasText: 'Add long headline' } ); } /** @@ -142,7 +146,9 @@ export default class CreateCampaignPage extends MockRequests { * @return {import('@playwright/test').Locator} Get Add description button. */ getAddDescriptionButton() { - return this.page.getByRole( 'button', { name: 'Add description' } ); + return this.page + .getByRole( 'button', { name: 'Add text' } ) + .filter( { hasText: 'Add description' } ); } /** diff --git a/tests/mocks/assets/svgrMock.js b/tests/mocks/assets/svgrMock.js index 99b5ffa206..146f6be6dd 100644 --- a/tests/mocks/assets/svgrMock.js +++ b/tests/mocks/assets/svgrMock.js @@ -1,10 +1,14 @@ /** * External dependencies */ -import { forwardRef } from '@wordpress/element'; +import { createElement, forwardRef } from '@wordpress/element'; -export const ReactComponent = forwardRef( ( props, ref ) => ( - -) ); +const SvgMock = forwardRef( ( props, ref ) => + createElement( 'svg', { ref, ...props } ) +); -export default 'SvgrURL'; +// Common SVGR compatibility: named export ReactComponent +export const ReactComponent = SvgMock; + +// Default export should be the component for `?inline` usage +export default SvgMock; From 70d9a5c9cd9f958de4f9171698f4647ae1178ddf Mon Sep 17 00:00:00 2001 From: asvinb Date: Thu, 15 Jan 2026 17:10:40 +0400 Subject: [PATCH 020/123] refactor(jest): Simplify svg handling in moduleNameMapper --- jest.config.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/jest.config.js b/jest.config.js index 7a9808961d..f03a9dd236 100644 --- a/jest.config.js +++ b/jest.config.js @@ -12,8 +12,7 @@ module.exports = { ], moduleNameMapper: { '\\.(png|jpg)$': '/tests/mocks/assets/imageMock.js', - '\\.svg\\?inline$': '/tests/mocks/assets/svgrMock.js', - '\\.svg$': '/tests/mocks/assets/svgrMock.js', + '\\.svg(\\?inline)?$': '/tests/mocks/assets/svgrMock.js', '\\.scss$': '/tests/mocks/assets/styleMock.js', // Transform our `~/` alias. '^~/(.*)$': '/js/src/$1', From 7cc51119d8359564977d2c72f23a39a4eb272ea8 Mon Sep 17 00:00:00 2001 From: James Morrison Date: Thu, 15 Jan 2026 13:12:19 +0000 Subject: [PATCH 021/123] Add AdsAssetGenerationService for AI asset generation. --- src/Ads/AdsAssetGenerationService.php | 237 ++++++++++++++++++ src/Google/Ads/ServiceClientFactoryTrait.php | 8 + .../GoogleServiceProvider.php | 3 + .../HelperTrait/GoogleAdsClientTrait.php | 82 ++++++ .../Ads/AdsAssetGenerationServiceTest.php | 198 +++++++++++++++ 5 files changed, 528 insertions(+) create mode 100644 src/Ads/AdsAssetGenerationService.php create mode 100644 tests/Unit/Ads/AdsAssetGenerationServiceTest.php diff --git a/src/Ads/AdsAssetGenerationService.php b/src/Ads/AdsAssetGenerationService.php new file mode 100644 index 0000000000..352448795e --- /dev/null +++ b/src/Ads/AdsAssetGenerationService.php @@ -0,0 +1,237 @@ + AssetFieldType::HEADLINE, + 'LONG_HEADLINE' => AssetFieldType::LONG_HEADLINE, + 'DESCRIPTION' => AssetFieldType::DESCRIPTION, + 'MARKETING_IMAGE' => AssetFieldType::MARKETING_IMAGE, + 'SQUARE_MARKETING_IMAGE' => AssetFieldType::SQUARE_MARKETING_IMAGE, + 'PORTRAIT_MARKETING_IMAGE' => AssetFieldType::PORTRAIT_MARKETING_IMAGE, + ]; + + /** + * AdsAssetGenerationService constructor. + * + * @param GoogleAdsClient $client The Google Ads client. + */ + public function __construct( GoogleAdsClient $client ) { + $this->client = $client; + } + + /** + * Generate text assets using Google's AI. + * + * @param array $args { + * Optional. Arguments for generating text assets. + * + * @type string $final_url The final URL - defaults to the Site URL. + * @type array $asset_field_types Can be one or more of: HEADLINE, LONG_HEADLINE, DESCRIPTION. + * } + * @return array Array of generated text objects with 'text' and 'type' keys. + * @throws Exception If the text assets can't be generated. + */ + public function generate_text( array $args = [] ): array { + $customer_id = $this->options->get_ads_id(); + if ( empty( $customer_id ) ) { + throw new Exception( __( 'Ads account ID is required.', 'google-listings-and-ads' ) ); + } + + $final_url = $args['final_url'] ?? $this->get_site_url(); + + // Convert asset field types from uppercase strings to enum numbers. + $asset_field_types = $this->convert_text_types_to_enums( $args['asset_field_types'] ?? [] ); + + $request = new GenerateTextRequest( + [ + 'customer_id' => $customer_id, + 'final_url' => $final_url, + 'advertising_channel_type' => AdvertisingChannelType::PERFORMANCE_MAX, + 'asset_field_types' => $asset_field_types, + ] + ); + + try { + $service_client = $this->client->getAssetGenerationServiceClient(); + $response = $service_client->generateTextAssets( $request ); + + $results = []; + foreach ( $response->getTextAssets() as $text_asset ) { + $asset_field_type_number = $text_asset->getAssetFieldType(); + $asset_field_type_label = AssetFieldType::label( $asset_field_type_number ); + $results[] = [ + 'text' => $text_asset->getText(), + 'type' => AssetFieldType::name( $asset_field_type_label ), + ]; + } + + return $results; + } catch ( ApiException $e ) { + do_action( 'woocommerce_gla_ads_client_exception', $e, __METHOD__ ); + throw new Exception( __( 'Unable to generate text assets.', 'google-listings-and-ads' ) . ' ' . $e->getMessage(), $e->getCode() ); + } + } + + /** + * Generate image assets using Google's AI. + * + * @param array $args { + * Optional. Arguments for generating image assets. + * + * @type string $final_url The final URL - defaults to the Site URL. + * @type array $asset_field_types Can be one or more of: MARKETING_IMAGE, SQUARE_MARKETING_IMAGE, PORTRAIT_MARKETING_IMAGE. + * } + * @return array Array of generated image objects with 'temporary_image_url' and 'type' keys. + * @throws Exception If the image assets can't be generated. + */ + public function generate_images( array $args = [] ): array { + $customer_id = $this->options->get_ads_id(); + if ( empty( $customer_id ) ) { + throw new Exception( __( 'Ads account ID is required.', 'google-listings-and-ads' ) ); + } + + $final_url = $args['final_url'] ?? $this->get_site_url(); + + // Convert asset field types from uppercase strings to enum numbers (if provided). + $asset_field_types = []; + if ( ! empty( $args['asset_field_types'] ) ) { + $asset_field_types = $this->convert_image_types_to_enums( $args['asset_field_types'] ); + } + + $request_data = [ + 'customer_id' => $customer_id, + 'generation_type' => 'final_url_generation', + 'advertising_channel_type' => AdvertisingChannelType::PERFORMANCE_MAX, + ]; + + // Add final_url_generation_input. + $request_data['final_url_generation_input'] = new FinalUrlImageGenerationInput( + [ + 'final_url' => $final_url, + ] + ); + + // Add asset_field_types only if provided. + if ( ! empty( $asset_field_types ) ) { + $request_data['asset_field_types'] = $asset_field_types; + } + + $request = new GenerateImagesRequest( $request_data ); + + try { + $service_client = $this->client->getAssetGenerationServiceClient(); + $response = $service_client->generateImages( $request ); + + $results = []; + foreach ( $response->getImageAssets() as $image_asset ) { + $asset_field_type_number = $image_asset->getAssetFieldType(); + $asset_field_type_label = AssetFieldType::label( $asset_field_type_number ); + $results[] = [ + 'temporary_image_url' => $image_asset->getTemporaryImageUrl(), + 'type' => AssetFieldType::name( $asset_field_type_label ), + ]; + } + + return $results; + } catch ( ApiException $e ) { + do_action( 'woocommerce_gla_ads_client_exception', $e, __METHOD__ ); + throw new Exception( __( 'Unable to generate image assets.', 'google-listings-and-ads' ) . ' ' . $e->getMessage(), $e->getCode() ); + } + } + + /** + * Convert text asset field types from uppercase strings to enum numbers. + * + * @param array $types Array of uppercase type strings (HEADLINE, LONG_HEADLINE, DESCRIPTION). + * @return array Array of enum numbers. Defaults to all text types if empty. + */ + protected function convert_text_types_to_enums( array $types ): array { + if ( empty( $types ) ) { + // Default to all text types. + return [ + AssetFieldType::number( AssetFieldType::HEADLINE ), + AssetFieldType::number( AssetFieldType::LONG_HEADLINE ), + AssetFieldType::number( AssetFieldType::DESCRIPTION ), + ]; + } + + $enums = []; + foreach ( $types as $type ) { + if ( ! isset( self::TYPE_MAPPING[ $type ] ) ) { + continue; + } + + $internal_type = self::TYPE_MAPPING[ $type ]; + // Only include text types. + if ( in_array( $internal_type, [ AssetFieldType::HEADLINE, AssetFieldType::LONG_HEADLINE, AssetFieldType::DESCRIPTION ], true ) ) { + $enums[] = AssetFieldType::number( $internal_type ); + } + } + + return $enums; + } + + /** + * Convert image asset field types from uppercase strings to enum numbers. + * + * @param array $types Array of uppercase type strings (MARKETING_IMAGE, SQUARE_MARKETING_IMAGE, PORTRAIT_MARKETING_IMAGE). + * @return array Array of enum numbers. + */ + protected function convert_image_types_to_enums( array $types ): array { + $enums = []; + foreach ( $types as $type ) { + if ( ! isset( self::TYPE_MAPPING[ $type ] ) ) { + continue; + } + + $internal_type = self::TYPE_MAPPING[ $type ]; + // Only include image types. + if ( in_array( $internal_type, [ AssetFieldType::MARKETING_IMAGE, AssetFieldType::SQUARE_MARKETING_IMAGE, AssetFieldType::PORTRAIT_MARKETING_IMAGE ], true ) ) { + $enums[] = AssetFieldType::number( $internal_type ); + } + } + + return $enums; + } +} diff --git a/src/Google/Ads/ServiceClientFactoryTrait.php b/src/Google/Ads/ServiceClientFactoryTrait.php index 2b9e867bfa..87aac4049d 100644 --- a/src/Google/Ads/ServiceClientFactoryTrait.php +++ b/src/Google/Ads/ServiceClientFactoryTrait.php @@ -32,6 +32,7 @@ use Google\Ads\GoogleAds\V20\Services\Client\GoogleAdsServiceClient; use Google\Ads\GoogleAds\V20\Services\Client\ProductLinkInvitationServiceClient; use Google\Ads\GoogleAds\V20\Services\Client\RecommendationServiceClient; +use Google\Ads\GoogleAds\V22\Services\Client\AssetGenerationServiceClient; /** * Contains service client factory methods. @@ -209,4 +210,11 @@ public function getProductLinkInvitationServiceClient(): ProductLinkInvitationSe public function getRecommendationServiceClient(): RecommendationServiceClient { return new RecommendationServiceClient( $this->getGoogleAdsClientOptions() ); } + + /** + * @return AssetGenerationServiceClient + */ + public function getAssetGenerationServiceClient(): AssetGenerationServiceClient { + return new AssetGenerationServiceClient( $this->getGoogleAdsClientOptions() ); + } } diff --git a/src/Internal/DependencyManagement/GoogleServiceProvider.php b/src/Internal/DependencyManagement/GoogleServiceProvider.php index 0ddf8f7321..ce90f3affe 100644 --- a/src/Internal/DependencyManagement/GoogleServiceProvider.php +++ b/src/Internal/DependencyManagement/GoogleServiceProvider.php @@ -4,6 +4,7 @@ namespace Automattic\WooCommerce\GoogleListingsAndAds\Internal\DependencyManagement; use Automattic\Jetpack\Connection\Manager; +use Automattic\WooCommerce\GoogleListingsAndAds\Ads\AdsAssetGenerationService; use Automattic\WooCommerce\GoogleListingsAndAds\Ads\AdsRecommendationsService; use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Ads; use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\AdsAssetGroup; @@ -87,6 +88,7 @@ class GoogleServiceProvider extends AbstractServiceProvider { AdsConversionAction::class => true, AdsReport::class => true, AdsRecommendationsService::class => true, + AdsAssetGenerationService::class => true, AdsAssetGroupAsset::class => true, AdsAsset::class => true, BudgetMetrics::class => true, @@ -125,6 +127,7 @@ public function register(): void { $this->share( AdsConversionAction::class, GoogleAdsClient::class ); $this->share( AdsReport::class, GoogleAdsClient::class ); $this->share( AdsRecommendationsService::class, GoogleAdsClient::class ); + $this->share( AdsAssetGenerationService::class, GoogleAdsClient::class ); $this->share( BudgetMetrics::class, GoogleAdsClient::class ); $this->share( BudgetRecommendations::class, GoogleAdsClient::class ); diff --git a/tests/Tools/HelperTrait/GoogleAdsClientTrait.php b/tests/Tools/HelperTrait/GoogleAdsClientTrait.php index 2b9dc2a68c..a462d03fd7 100644 --- a/tests/Tools/HelperTrait/GoogleAdsClientTrait.php +++ b/tests/Tools/HelperTrait/GoogleAdsClientTrait.php @@ -94,6 +94,9 @@ trait GoogleAdsClientTrait { /** @var MockObject|GoogleAdsServiceClient $service_client */ protected $service_client; + /** @var MockObject|\Google\Ads\GoogleAds\V22\Services\Client\AssetGenerationServiceClient $asset_generation_service */ + protected $asset_generation_service; + /** @var int $ads_id */ protected $ads_id; @@ -115,6 +118,9 @@ protected function ads_client_setup() { $this->recommendation_service = $this->createMock( RecommendationServiceClient::class ); $this->client->method( 'getRecommendationServiceClient' )->willReturn( $this->recommendation_service ); + + $this->asset_generation_service = $this->createMock( \Google\Ads\GoogleAds\V22\Services\Client\AssetGenerationServiceClient::class ); + $this->client->method( 'getAssetGenerationServiceClient' )->willReturn( $this->asset_generation_service ); } /** @@ -1157,4 +1163,80 @@ protected function generate_location_ids_mock( array $locations ) { $this->generate_ads_query_mock( array_values( $locations ) ); } + + /** + * Generates a mocked response for text asset generation. + * + * @param array $text_assets Array of text assets with 'text' and 'type' keys (type in uppercase like 'HEADLINE'). + */ + protected function generate_text_assets_mock( array $text_assets ) { + $type_mapping = [ + 'HEADLINE' => AssetFieldType::HEADLINE, + 'LONG_HEADLINE' => AssetFieldType::LONG_HEADLINE, + 'DESCRIPTION' => AssetFieldType::DESCRIPTION, + ]; + + $text_asset_objects = []; + foreach ( $text_assets as $asset ) { + $text_asset = $this->createMock( \Google\Ads\GoogleAds\V22\Services\TextAsset::class ); + $text_asset->method( 'getText' )->willReturn( $asset['text'] ); + // Convert uppercase type string to enum number. + $type_label = $type_mapping[ $asset['type'] ] ?? AssetFieldType::HEADLINE; + $type_number = AssetFieldType::number( $type_label ); + $text_asset->method( 'getAssetFieldType' )->willReturn( $type_number ); + $text_asset_objects[] = $text_asset; + } + + $response = $this->createMock( \Google\Ads\GoogleAds\V22\Services\GenerateTextResponse::class ); + $response->method( 'getTextAssets' )->willReturn( $text_asset_objects ); + + $this->asset_generation_service->method( 'generateTextAssets' )->willReturn( $response ); + } + + /** + * Generates a mocked exception when text assets are requested. + * + * @param ApiException $exception + */ + protected function generate_text_assets_mock_exception( ApiException $exception ) { + $this->asset_generation_service->method( 'generateTextAssets' )->willThrowException( $exception ); + } + + /** + * Generates a mocked response for image asset generation. + * + * @param array $image_assets Array of image assets with 'temporary_image_url' and 'type' keys (type in uppercase like 'MARKETING_IMAGE'). + */ + protected function generate_image_assets_mock( array $image_assets ) { + $type_mapping = [ + 'MARKETING_IMAGE' => AssetFieldType::MARKETING_IMAGE, + 'SQUARE_MARKETING_IMAGE' => AssetFieldType::SQUARE_MARKETING_IMAGE, + 'PORTRAIT_MARKETING_IMAGE' => AssetFieldType::PORTRAIT_MARKETING_IMAGE, + ]; + + $image_asset_objects = []; + foreach ( $image_assets as $asset ) { + $image_asset = $this->createMock( \Google\Ads\GoogleAds\V22\Services\ImageAsset::class ); + $image_asset->method( 'getTemporaryImageUrl' )->willReturn( $asset['temporary_image_url'] ); + // Convert uppercase type string to enum number. + $type_label = $type_mapping[ $asset['type'] ] ?? AssetFieldType::MARKETING_IMAGE; + $type_number = AssetFieldType::number( $type_label ); + $image_asset->method( 'getAssetFieldType' )->willReturn( $type_number ); + $image_asset_objects[] = $image_asset; + } + + $response = $this->createMock( \Google\Ads\GoogleAds\V22\Services\GenerateImagesResponse::class ); + $response->method( 'getImageAssets' )->willReturn( $image_asset_objects ); + + $this->asset_generation_service->method( 'generateImages' )->willReturn( $response ); + } + + /** + * Generates a mocked exception when image assets are requested. + * + * @param ApiException $exception + */ + protected function generate_image_assets_mock_exception( ApiException $exception ) { + $this->asset_generation_service->method( 'generateImages' )->willThrowException( $exception ); + } } diff --git a/tests/Unit/Ads/AdsAssetGenerationServiceTest.php b/tests/Unit/Ads/AdsAssetGenerationServiceTest.php new file mode 100644 index 0000000000..f79759844a --- /dev/null +++ b/tests/Unit/Ads/AdsAssetGenerationServiceTest.php @@ -0,0 +1,198 @@ +ads_client_setup(); + + $this->options = $this->createMock( OptionsInterface::class ); + $this->service = new AdsAssetGenerationService( $this->client ); + $this->service->set_options_object( $this->options ); + + $this->options->method( 'get_ads_id' )->willReturn( self::TEST_ADS_ID ); + } + + public function test_generate_text_with_defaults() { + $expected_text_assets = [ + [ + 'text' => 'Generated headline text example.', + 'type' => 'HEADLINE', + ], + [ + 'text' => 'Generated long headline text example.', + 'type' => 'LONG_HEADLINE', + ], + [ + 'text' => 'Generated description text example.', + 'type' => 'DESCRIPTION', + ], + ]; + + $this->generate_text_assets_mock( $expected_text_assets ); + + $result = $this->service->generate_text( [] ); + + $this->assertEquals( $expected_text_assets, $result ); + } + + public function test_generate_text_with_custom_final_url() { + $final_url = 'https://custom-url.com'; + $expected_text_assets = [ + [ + 'text' => 'Custom headline', + 'type' => 'HEADLINE', + ], + ]; + + $this->generate_text_assets_mock( $expected_text_assets ); + + $result = $this->service->generate_text( [ 'final_url' => $final_url ] ); + + $this->assertEquals( $expected_text_assets, $result ); + } + + public function test_generate_text_with_specific_types() { + $expected_text_assets = [ + [ + 'text' => 'Headline only', + 'type' => 'HEADLINE', + ], + ]; + + $this->generate_text_assets_mock( $expected_text_assets ); + + $result = $this->service->generate_text( [ 'asset_field_types' => [ 'HEADLINE' ] ] ); + + $this->assertEquals( $expected_text_assets, $result ); + } + + public function test_generate_text_exception() { + $this->generate_text_assets_mock_exception( + new ApiException( 'API error', 7 ) + ); + + $this->expectException( Exception::class ); + $this->expectExceptionMessage( 'Unable to generate text assets' ); + + $this->service->generate_text( [] ); + $this->assertEquals( 1, did_action( 'woocommerce_gla_ads_client_exception' ) ); + } + + public function test_generate_text_no_ads_id() { + $this->options->method( 'get_ads_id' )->willReturn( null ); + + $this->expectException( Exception::class ); + $this->expectExceptionMessage( 'Ads account ID is required' ); + + $this->service->generate_text( [] ); + } + + public function test_generate_images_with_defaults() { + $expected_image_assets = [ + [ + 'temporary_image_url' => 'https://example.com/temporary_image_url-marketing.jpg', + 'type' => 'MARKETING_IMAGE', + ], + [ + 'temporary_image_url' => 'https://example.com/temporary_image_url-square.jpg', + 'type' => 'SQUARE_MARKETING_IMAGE', + ], + [ + 'temporary_image_url' => 'https://example.com/temporary_image_url-portrait.jpg', + 'type' => 'PORTRAIT_MARKETING_IMAGE', + ], + ]; + + $this->generate_image_assets_mock( $expected_image_assets ); + + $result = $this->service->generate_images( [] ); + + $this->assertEquals( $expected_image_assets, $result ); + } + + public function test_generate_images_with_custom_final_url() { + $final_url = 'https://custom-url.com'; + $expected_image_assets = [ + [ + 'temporary_image_url' => 'https://example.com/custom-image.jpg', + 'type' => 'MARKETING_IMAGE', + ], + ]; + + $this->generate_image_assets_mock( $expected_image_assets ); + + $result = $this->service->generate_images( [ 'final_url' => $final_url ] ); + + $this->assertEquals( $expected_image_assets, $result ); + } + + public function test_generate_images_with_specific_types() { + $expected_image_assets = [ + [ + 'temporary_image_url' => 'https://example.com/marketing-image.jpg', + 'type' => 'MARKETING_IMAGE', + ], + ]; + + $this->generate_image_assets_mock( $expected_image_assets ); + + $result = $this->service->generate_images( [ 'asset_field_types' => [ 'MARKETING_IMAGE' ] ] ); + + $this->assertEquals( $expected_image_assets, $result ); + } + + public function test_generate_images_exception() { + $this->generate_image_assets_mock_exception( + new ApiException( 'API error', 7 ) + ); + + $this->expectException( Exception::class ); + $this->expectExceptionMessage( 'Unable to generate image assets' ); + + $this->service->generate_images( [] ); + $this->assertEquals( 1, did_action( 'woocommerce_gla_ads_client_exception' ) ); + } + + public function test_generate_images_no_ads_id() { + $this->options->method( 'get_ads_id' )->willReturn( null ); + + $this->expectException( Exception::class ); + $this->expectExceptionMessage( 'Ads account ID is required' ); + + $this->service->generate_images( [] ); + } +} From 0cbc15e4bb75e275ffe3b0016cc591f8473004f6 Mon Sep 17 00:00:00 2001 From: asvinb Date: Thu, 15 Jan 2026 17:33:05 +0400 Subject: [PATCH 022/123] Update svgr mock component in tests --- tests/mocks/assets/svgrMock.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/mocks/assets/svgrMock.js b/tests/mocks/assets/svgrMock.js index 99b5ffa206..146f6be6dd 100644 --- a/tests/mocks/assets/svgrMock.js +++ b/tests/mocks/assets/svgrMock.js @@ -1,10 +1,14 @@ /** * External dependencies */ -import { forwardRef } from '@wordpress/element'; +import { createElement, forwardRef } from '@wordpress/element'; -export const ReactComponent = forwardRef( ( props, ref ) => ( - -) ); +const SvgMock = forwardRef( ( props, ref ) => + createElement( 'svg', { ref, ...props } ) +); -export default 'SvgrURL'; +// Common SVGR compatibility: named export ReactComponent +export const ReactComponent = SvgMock; + +// Default export should be the component for `?inline` usage +export default SvgMock; From 7392d7e56194ecd16e2ef5f8d14c66a4e18abdce Mon Sep 17 00:00:00 2001 From: James Morrison Date: Thu, 15 Jan 2026 14:16:25 +0000 Subject: [PATCH 023/123] Add AI asset generation service and REST endpoints --- .../Ads/AssetGenerationController.php | 249 +++++++++++++ .../RESTServiceProvider.php | 3 + .../Ads/AssetGenerationControllerTest.php | 349 ++++++++++++++++++ 3 files changed, 601 insertions(+) create mode 100644 src/API/Site/Controllers/Ads/AssetGenerationController.php create mode 100644 tests/Unit/API/Site/Controllers/Ads/AssetGenerationControllerTest.php diff --git a/src/API/Site/Controllers/Ads/AssetGenerationController.php b/src/API/Site/Controllers/Ads/AssetGenerationController.php new file mode 100644 index 0000000000..4c79d89eb6 --- /dev/null +++ b/src/API/Site/Controllers/Ads/AssetGenerationController.php @@ -0,0 +1,249 @@ +service = $service; + } + + /** + * Register rest routes with WordPress. + */ + public function register_routes(): void { + $this->register_route( + 'ads/assets/generate-text', + [ + [ + 'methods' => TransportMethods::CREATABLE, + 'callback' => $this->get_generate_text_callback(), + 'permission_callback' => $this->get_permission_callback(), + 'args' => $this->get_generate_text_params(), + ], + ] + ); + + $this->register_route( + 'ads/assets/generate-images', + [ + [ + 'methods' => TransportMethods::CREATABLE, + 'callback' => $this->get_generate_images_callback(), + 'permission_callback' => $this->get_permission_callback(), + 'args' => $this->get_generate_images_params(), + ], + ] + ); + } + + /** + * Get the parameters for the generate-text endpoint. + * + * @return array + */ + protected function get_generate_text_params(): array { + return [ + 'final_url' => [ + 'description' => __( 'The final URL for asset generation', 'google-listings-and-ads' ), + 'type' => 'string', + 'sanitize_callback' => 'esc_url_raw', + 'validate_callback' => 'rest_validate_request_arg', + ], + 'types' => [ + 'description' => __( 'Asset types to generate', 'google-listings-and-ads' ), + 'type' => 'array', + 'items' => [ + 'type' => 'string', + 'enum' => [ 'headline', 'long_headline', 'description' ], + ], + 'sanitize_callback' => function ( $types ) { + return array_map( 'sanitize_text_field', $types ); + }, + 'validate_callback' => 'rest_validate_request_arg', + ], + ]; + } + + /** + * Get the parameters for the generate-images endpoint. + * + * @return array + */ + protected function get_generate_images_params(): array { + return [ + 'final_url' => [ + 'description' => __( 'The final URL for asset generation', 'google-listings-and-ads' ), + 'type' => 'string', + 'sanitize_callback' => 'esc_url_raw', + 'validate_callback' => 'rest_validate_request_arg', + ], + 'types' => [ + 'description' => __( 'Asset types to generate', 'google-listings-and-ads' ), + 'type' => 'array', + 'items' => [ + 'type' => 'string', + 'enum' => [ 'marketing_image', 'square_marketing_image', 'portrait_marketing_image' ], + ], + 'sanitize_callback' => function ( $types ) { + return array_map( 'sanitize_text_field', $types ); + }, + 'validate_callback' => 'rest_validate_request_arg', + ], + ]; + } + + /** + * Get the callback function for the generate-text request. + * + * @return callable + */ + protected function get_generate_text_callback(): callable { + return function ( Request $request ) { + try { + $final_url = $request->get_param( 'final_url' ) ?: $this->get_site_url(); + $types = $request->get_param( 'types' ) ?: [ 'headline', 'long_headline', 'description' ]; + + // Convert lowercase types to uppercase for service. + $uppercase_types = $this->convert_types_to_uppercase( $types ); + + // Call service. + $items = $this->service->generate_text( + [ + 'final_url' => $final_url, + 'asset_field_types' => $uppercase_types, + ] + ); + + // Format response with lowercase types. + return $this->format_response( $final_url, $items ); + } catch ( Exception $e ) { + return $this->response_from_exception( $e ); + } + }; + } + + /** + * Get the callback function for the generate-images request. + * + * @return callable + */ + protected function get_generate_images_callback(): callable { + return function ( Request $request ) { + try { + $final_url = $request->get_param( 'final_url' ) ?: $this->get_site_url(); + $types = $request->get_param( 'types' ) ?: []; + + // Convert lowercase types to uppercase for service (if provided). + $uppercase_types = ! empty( $types ) ? $this->convert_types_to_uppercase( $types ) : []; + + // Call service. + $args = [ 'final_url' => $final_url ]; + if ( ! empty( $uppercase_types ) ) { + $args['asset_field_types'] = $uppercase_types; + } + $items = $this->service->generate_images( $args ); + + // Format response with lowercase types. + return $this->format_response( $final_url, $items ); + } catch ( Exception $e ) { + return $this->response_from_exception( $e ); + } + }; + } + + /** + * Convert types to uppercase. + * + * @param array $types Array of lowercase type strings. + * @return array Array of uppercase type strings. + */ + protected function convert_types_to_uppercase( array $types ): array { + return array_map( 'strtoupper', $types ); + } + + /** + * Format the response with final_url and items. + * + * @param string $final_url The final URL. + * @param array $service_items Items from the service (with uppercase types). + * @return array Formatted response with lowercase types. + */ + protected function format_response( string $final_url, array $service_items ): array { + $items = []; + foreach ( $service_items as $item ) { + $item['type'] = strtolower( $item['type'] ); + $items[] = $item; + } + + return [ + 'final_url' => $final_url, + 'items' => $items, + ]; + } + + /** + * Get the item schema properties for the controller. + * + * @return array + */ + protected function get_schema_properties(): array { + return [ + 'final_url' => [ + 'type' => 'string', + 'description' => __( 'The final URL used for generation', 'google-listings-and-ads' ), + 'context' => [ 'view' ], + 'readonly' => true, + ], + 'items' => [ + 'type' => 'array', + 'description' => __( 'Generated assets', 'google-listings-and-ads' ), + 'context' => [ 'view' ], + 'readonly' => true, + ], + ]; + } + + /** + * Get the item schema name for the controller. + * + * Used for building the API response schema. + * + * @return string + */ + protected function get_schema_title(): string { + return 'asset_generation'; + } +} diff --git a/src/Internal/DependencyManagement/RESTServiceProvider.php b/src/Internal/DependencyManagement/RESTServiceProvider.php index 70c044053d..cae384bdc2 100644 --- a/src/Internal/DependencyManagement/RESTServiceProvider.php +++ b/src/Internal/DependencyManagement/RESTServiceProvider.php @@ -5,6 +5,7 @@ use Automattic\Jetpack\Connection\Manager; use Automattic\WooCommerce\GoogleListingsAndAds\Ads\AccountService as AdsAccountService; +use Automattic\WooCommerce\GoogleListingsAndAds\Ads\AdsAssetGenerationService; use Automattic\WooCommerce\GoogleListingsAndAds\Ads\AssetSuggestionsService as AdsAssetSuggestionsService; use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\Ads; use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\AdsCampaign; @@ -23,6 +24,7 @@ use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads\ReportsController as AdsReportsController; use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads\SetupCompleteController; use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads\AssetGroupController as AdsAssetGroupController; +use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads\AssetGenerationController; use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads\AssetSuggestionsController; use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads\RecommendationsController; use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\GTINMigrationController; @@ -144,6 +146,7 @@ public function register(): void { $this->share( DisconnectController::class ); $this->share( SetupCompleteController::class, MerchantMetrics::class ); $this->share( AssetSuggestionsController::class, AdsAssetSuggestionsService::class ); + $this->share( AssetGenerationController::class, AdsAssetGenerationService::class ); $this->share( SyncableProductsCountController::class, JobRepository::class ); $this->share( PolicyComplianceCheckController::class, PolicyComplianceCheck::class ); $this->share( AttributeMappingDataController::class, AttributeMappingHelper::class ); diff --git a/tests/Unit/API/Site/Controllers/Ads/AssetGenerationControllerTest.php b/tests/Unit/API/Site/Controllers/Ads/AssetGenerationControllerTest.php new file mode 100644 index 0000000000..25d51c46f3 --- /dev/null +++ b/tests/Unit/API/Site/Controllers/Ads/AssetGenerationControllerTest.php @@ -0,0 +1,349 @@ +service = $this->createMock( AdsAssetGenerationService::class ); + $this->controller = new AssetGenerationController( $this->server, $this->service ); + $this->controller->register(); + } + + public function test_generate_text_with_defaults() { + // Service expects uppercase types. + $this->service->expects( $this->once() ) + ->method( 'generate_text' ) + ->with( + [ + 'final_url' => self::TEST_SITE_URL, + 'asset_field_types' => [ 'HEADLINE', 'LONG_HEADLINE', 'DESCRIPTION' ], + ] + ) + ->willReturn( + [ + [ + 'text' => 'Test headline', + 'type' => 'HEADLINE', + ], + [ + 'text' => 'Test long headline', + 'type' => 'LONG_HEADLINE', + ], + [ + 'text' => 'Test description', + 'type' => 'DESCRIPTION', + ], + ] + ); + + $response = $this->do_request( self::ROUTE_GENERATE_TEXT, 'POST' ); + + $data = $response->get_data(); + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( self::TEST_SITE_URL, $data['final_url'] ); + $this->assertCount( 3, $data['items'] ); + // Verify types are lowercase in response. + $this->assertEquals( 'headline', $data['items'][0]['type'] ); + $this->assertEquals( 'long_headline', $data['items'][1]['type'] ); + $this->assertEquals( 'description', $data['items'][2]['type'] ); + $this->assertEquals( 'Test headline', $data['items'][0]['text'] ); + } + + public function test_generate_text_with_custom_url() { + $params = [ + 'final_url' => 'https://custom-url.com', + ]; + + $this->service->expects( $this->once() ) + ->method( 'generate_text' ) + ->with( + [ + 'final_url' => 'https://custom-url.com', + 'asset_field_types' => [ 'HEADLINE', 'LONG_HEADLINE', 'DESCRIPTION' ], + ] + ) + ->willReturn( + [ + [ + 'text' => 'Custom headline', + 'type' => 'HEADLINE', + ], + ] + ); + + $response = $this->do_request( self::ROUTE_GENERATE_TEXT, 'POST', $params ); + + $data = $response->get_data(); + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 'https://custom-url.com', $data['final_url'] ); + $this->assertEquals( 'headline', $data['items'][0]['type'] ); + } + + public function test_generate_text_with_specific_types() { + $params = [ + 'types' => [ 'headline' ], + ]; + + $this->service->expects( $this->once() ) + ->method( 'generate_text' ) + ->with( + [ + 'final_url' => self::TEST_SITE_URL, + 'asset_field_types' => [ 'HEADLINE' ], + ] + ) + ->willReturn( + [ + [ + 'text' => 'Headline only', + 'type' => 'HEADLINE', + ], + ] + ); + + $response = $this->do_request( self::ROUTE_GENERATE_TEXT, 'POST', $params ); + + $data = $response->get_data(); + $this->assertEquals( 200, $response->get_status() ); + $this->assertCount( 1, $data['items'] ); + $this->assertEquals( 'headline', $data['items'][0]['type'] ); + } + + public function test_generate_text_type_conversion() { + $params = [ + 'types' => [ 'headline', 'description' ], + ]; + + // Verify lowercase input is converted to uppercase for service. + $this->service->expects( $this->once() ) + ->method( 'generate_text' ) + ->with( + $this->callback( + function ( $args ) { + return $args['asset_field_types'] === [ 'HEADLINE', 'DESCRIPTION' ]; + } + ) + ) + ->willReturn( + [ + [ + 'text' => 'Test', + 'type' => 'HEADLINE', + ], + [ + 'text' => 'Test', + 'type' => 'DESCRIPTION', + ], + ] + ); + + $response = $this->do_request( self::ROUTE_GENERATE_TEXT, 'POST', $params ); + + // Verify uppercase response is converted to lowercase. + $data = $response->get_data(); + $this->assertEquals( 'headline', $data['items'][0]['type'] ); + $this->assertEquals( 'description', $data['items'][1]['type'] ); + } + + public function test_generate_text_exception() { + $this->service + ->method( 'generate_text' ) + ->willThrowException( new Exception( 'Service error', 500 ) ); + + $response = $this->do_request( self::ROUTE_GENERATE_TEXT, 'POST' ); + + $this->assertEquals( 'Service error', $response->get_data()['message'] ); + $this->assertEquals( 500, $response->get_status() ); + } + + public function test_generate_images_with_defaults() { + // Service expects empty array for types (API generates all). + $this->service->expects( $this->once() ) + ->method( 'generate_images' ) + ->with( + [ + 'final_url' => self::TEST_SITE_URL, + ] + ) + ->willReturn( + [ + [ + 'temporary_image_url' => 'https://example.com/image-marketing.jpg', + 'type' => 'MARKETING_IMAGE', + ], + [ + 'temporary_image_url' => 'https://example.com/image-square.jpg', + 'type' => 'SQUARE_MARKETING_IMAGE', + ], + [ + 'temporary_image_url' => 'https://example.com/image-portrait.jpg', + 'type' => 'PORTRAIT_MARKETING_IMAGE', + ], + ] + ); + + $response = $this->do_request( self::ROUTE_GENERATE_IMAGES, 'POST' ); + + $data = $response->get_data(); + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( self::TEST_SITE_URL, $data['final_url'] ); + $this->assertCount( 3, $data['items'] ); + // Verify types are lowercase in response. + $this->assertEquals( 'marketing_image', $data['items'][0]['type'] ); + $this->assertEquals( 'square_marketing_image', $data['items'][1]['type'] ); + $this->assertEquals( 'portrait_marketing_image', $data['items'][2]['type'] ); + } + + public function test_generate_images_with_custom_url() { + $params = [ + 'final_url' => 'https://custom-url.com', + ]; + + $this->service->expects( $this->once() ) + ->method( 'generate_images' ) + ->with( + [ + 'final_url' => 'https://custom-url.com', + ] + ) + ->willReturn( + [ + [ + 'temporary_image_url' => 'https://example.com/custom-image.jpg', + 'type' => 'MARKETING_IMAGE', + ], + ] + ); + + $response = $this->do_request( self::ROUTE_GENERATE_IMAGES, 'POST', $params ); + + $data = $response->get_data(); + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 'https://custom-url.com', $data['final_url'] ); + $this->assertEquals( 'marketing_image', $data['items'][0]['type'] ); + } + + public function test_generate_images_with_specific_types() { + $params = [ + 'types' => [ 'marketing_image' ], + ]; + + // Service expects uppercase types. + $this->service->expects( $this->once() ) + ->method( 'generate_images' ) + ->with( + [ + 'final_url' => self::TEST_SITE_URL, + 'asset_field_types' => [ 'MARKETING_IMAGE' ], + ] + ) + ->willReturn( + [ + [ + 'temporary_image_url' => 'https://example.com/image.jpg', + 'type' => 'MARKETING_IMAGE', + ], + ] + ); + + $response = $this->do_request( self::ROUTE_GENERATE_IMAGES, 'POST', $params ); + + $data = $response->get_data(); + $this->assertEquals( 200, $response->get_status() ); + // Verify type is lowercase in response. + $this->assertEquals( 'marketing_image', $data['items'][0]['type'] ); + } + + public function test_generate_images_type_conversion() { + $params = [ + 'types' => [ 'marketing_image', 'square_marketing_image' ], + ]; + + // Verify lowercase input is converted to uppercase for service. + $this->service->expects( $this->once() ) + ->method( 'generate_images' ) + ->with( + $this->callback( + function ( $args ) { + return $args['asset_field_types'] === [ 'MARKETING_IMAGE', 'SQUARE_MARKETING_IMAGE' ]; + } + ) + ) + ->willReturn( + [ + [ + 'temporary_image_url' => 'https://example.com/image1.jpg', + 'type' => 'MARKETING_IMAGE', + ], + [ + 'temporary_image_url' => 'https://example.com/image2.jpg', + 'type' => 'SQUARE_MARKETING_IMAGE', + ], + ] + ); + + $response = $this->do_request( self::ROUTE_GENERATE_IMAGES, 'POST', $params ); + + // Verify uppercase response is converted to lowercase. + $data = $response->get_data(); + $this->assertEquals( 'marketing_image', $data['items'][0]['type'] ); + $this->assertEquals( 'square_marketing_image', $data['items'][1]['type'] ); + } + + public function test_generate_images_exception() { + $this->service + ->method( 'generate_images' ) + ->willThrowException( new Exception( 'Service error', 500 ) ); + + $response = $this->do_request( self::ROUTE_GENERATE_IMAGES, 'POST' ); + + $this->assertEquals( 'Service error', $response->get_data()['message'] ); + $this->assertEquals( 500, $response->get_status() ); + } + + public function test_generate_text_without_permission() { + // Remove admin capabilities. + wp_set_current_user( 0 ); + + $response = $this->do_request( self::ROUTE_GENERATE_TEXT, 'POST' ); + + $this->assertEquals( 401, $response->get_status() ); + } + + public function test_generate_images_without_permission() { + // Remove admin capabilities. + wp_set_current_user( 0 ); + + $response = $this->do_request( self::ROUTE_GENERATE_IMAGES, 'POST' ); + + $this->assertEquals( 401, $response->get_status() ); + } +} From dfe7fc0f63f5eef939a87fae85bba9a6f50d9a38 Mon Sep 17 00:00:00 2001 From: Ankit Gade Date: Thu, 15 Jan 2026 21:12:51 +0530 Subject: [PATCH 024/123] Use AssetAutomationSetting in place of url_expansion_opt_out. --- composer.lock | 627 ++++++++++++++++++++------------- src/API/Google/AdsCampaign.php | 12 +- 2 files changed, 388 insertions(+), 251 deletions(-) diff --git a/composer.lock b/composer.lock index bee4987b18..526d4be5f4 100644 --- a/composer.lock +++ b/composer.lock @@ -908,16 +908,16 @@ }, { "name": "league/iso3166", - "version": "4.3.2", + "version": "4.4.0", "source": { "type": "git", "url": "https://github.com/alcohol/iso3166.git", - "reference": "5133fed7d54728222f4058702487dccedda20472" + "reference": "928ac7ecc569db9123a83ef5b1c6efc279e7cb49" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/alcohol/iso3166/zipball/5133fed7d54728222f4058702487dccedda20472", - "reference": "5133fed7d54728222f4058702487dccedda20472", + "url": "https://api.github.com/repos/alcohol/iso3166/zipball/928ac7ecc569db9123a83ef5b1c6efc279e7cb49", + "reference": "928ac7ecc569db9123a83ef5b1c6efc279e7cb49", "shasum": "" }, "require": { @@ -971,7 +971,7 @@ "type": "github" } ], - "time": "2024-10-10T07:39:24+00:00" + "time": "2026-01-02T09:49:36+00:00" }, { "name": "monolog/monolog", @@ -1077,16 +1077,16 @@ }, { "name": "paragonie/constant_time_encoding", - "version": "v2.7.0", + "version": "v2.8.2", "source": { "type": "git", "url": "https://github.com/paragonie/constant_time_encoding.git", - "reference": "52a0d99e69f56b9ec27ace92ba56897fe6993105" + "reference": "e30811f7bc69e4b5b6d5783e712c06c8eabf0226" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/52a0d99e69f56b9ec27ace92ba56897fe6993105", - "reference": "52a0d99e69f56b9ec27ace92ba56897fe6993105", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/e30811f7bc69e4b5b6d5783e712c06c8eabf0226", + "reference": "e30811f7bc69e4b5b6d5783e712c06c8eabf0226", "shasum": "" }, "require": { @@ -1140,7 +1140,7 @@ "issues": "https://github.com/paragonie/constant_time_encoding/issues", "source": "https://github.com/paragonie/constant_time_encoding" }, - "time": "2024-05-08T12:18:48+00:00" + "time": "2025-09-24T15:12:37+00:00" }, { "name": "paragonie/random_compat", @@ -1256,16 +1256,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.43", + "version": "3.0.48", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "709ec107af3cb2f385b9617be72af8cf62441d02" + "reference": "64065a5679c50acb886e82c07aa139b0f757bb89" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/709ec107af3cb2f385b9617be72af8cf62441d02", - "reference": "709ec107af3cb2f385b9617be72af8cf62441d02", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/64065a5679c50acb886e82c07aa139b0f757bb89", + "reference": "64065a5679c50acb886e82c07aa139b0f757bb89", "shasum": "" }, "require": { @@ -1346,7 +1346,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.43" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.48" }, "funding": [ { @@ -1362,7 +1362,7 @@ "type": "tidelift" } ], - "time": "2024-12-14T21:12:59+00:00" + "time": "2025-12-15T11:51:42+00:00" }, { "name": "psr/cache", @@ -2037,7 +2037,7 @@ }, { "name": "symfony/polyfill-php73", - "version": "v1.31.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", @@ -2055,8 +2055,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -2093,7 +2093,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.33.0" }, "funding": [ { @@ -2104,6 +2104,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -2297,12 +2301,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "2.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -2545,16 +2549,16 @@ }, { "name": "dg/bypass-finals", - "version": "v1.6.0", + "version": "v1.9.0", "source": { "type": "git", "url": "https://github.com/dg/bypass-finals.git", - "reference": "efe2fe04bae9f0de271dd462afc049067889e6d1" + "reference": "920a7da2f3c1422fd83f9ec4df007af53dc4018b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dg/bypass-finals/zipball/efe2fe04bae9f0de271dd462afc049067889e6d1", - "reference": "efe2fe04bae9f0de271dd462afc049067889e6d1", + "url": "https://api.github.com/repos/dg/bypass-finals/zipball/920a7da2f3c1422fd83f9ec4df007af53dc4018b", + "reference": "920a7da2f3c1422fd83f9ec4df007af53dc4018b", "shasum": "" }, "require": { @@ -2573,8 +2577,8 @@ "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause", - "GPL-2.0", - "GPL-3.0" + "GPL-2.0-only", + "GPL-3.0-only" ], "authors": [ { @@ -2592,9 +2596,9 @@ ], "support": { "issues": "https://github.com/dg/bypass-finals/issues", - "source": "https://github.com/dg/bypass-finals/tree/v1.6.0" + "source": "https://github.com/dg/bypass-finals/tree/v1.9.0" }, - "time": "2023-11-19T22:19:30+00:00" + "time": "2025-01-16T00:46:05+00:00" }, { "name": "doctrine/instantiator", @@ -2726,16 +2730,16 @@ }, { "name": "gettext/gettext", - "version": "v4.8.11", + "version": "v4.8.12", "source": { "type": "git", "url": "https://github.com/php-gettext/Gettext.git", - "reference": "b632aaf5e4579d0b2ae8bc61785e238bff4c5156" + "reference": "11af89ee6c087db3cf09ce2111a150bca5c46e12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-gettext/Gettext/zipball/b632aaf5e4579d0b2ae8bc61785e238bff4c5156", - "reference": "b632aaf5e4579d0b2ae8bc61785e238bff4c5156", + "url": "https://api.github.com/repos/php-gettext/Gettext/zipball/11af89ee6c087db3cf09ce2111a150bca5c46e12", + "reference": "11af89ee6c087db3cf09ce2111a150bca5c46e12", "shasum": "" }, "require": { @@ -2787,7 +2791,7 @@ "support": { "email": "oom@oscarotero.com", "issues": "https://github.com/oscarotero/Gettext/issues", - "source": "https://github.com/php-gettext/Gettext/tree/v4.8.11" + "source": "https://github.com/php-gettext/Gettext/tree/v4.8.12" }, "funding": [ { @@ -2803,20 +2807,20 @@ "type": "patreon" } ], - "time": "2023-08-14T15:15:05+00:00" + "time": "2024-05-18T10:25:07+00:00" }, { "name": "gettext/languages", - "version": "2.10.0", + "version": "2.12.1", "source": { "type": "git", "url": "https://github.com/php-gettext/Languages.git", - "reference": "4d61d67fe83a2ad85959fe6133d6d9ba7dddd1ab" + "reference": "0b0b0851c55168e1dfb14305735c64019732b5f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-gettext/Languages/zipball/4d61d67fe83a2ad85959fe6133d6d9ba7dddd1ab", - "reference": "4d61d67fe83a2ad85959fe6133d6d9ba7dddd1ab", + "url": "https://api.github.com/repos/php-gettext/Languages/zipball/0b0b0851c55168e1dfb14305735c64019732b5f1", + "reference": "0b0b0851c55168e1dfb14305735c64019732b5f1", "shasum": "" }, "require": { @@ -2826,7 +2830,8 @@ "phpunit/phpunit": "^4.8 || ^5.7 || ^6.5 || ^7.5 || ^8.4" }, "bin": [ - "bin/export-plural-rules" + "bin/export-plural-rules", + "bin/import-cldr-data" ], "type": "library", "autoload": { @@ -2865,7 +2870,7 @@ ], "support": { "issues": "https://github.com/php-gettext/Languages/issues", - "source": "https://github.com/php-gettext/Languages/tree/2.10.0" + "source": "https://github.com/php-gettext/Languages/tree/2.12.1" }, "funding": [ { @@ -2877,20 +2882,20 @@ "type": "github" } ], - "time": "2022-10-18T15:00:10+00:00" + "time": "2025-03-19T11:14:02+00:00" }, { "name": "mck89/peast", - "version": "v1.15.4", + "version": "v1.17.4", "source": { "type": "git", "url": "https://github.com/mck89/peast.git", - "reference": "1df4dc28a6b5bb7ab117ab073c1712256e954e18" + "reference": "c6a63f32410d2e4ee2cd20fe94b35af147fb852d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mck89/peast/zipball/1df4dc28a6b5bb7ab117ab073c1712256e954e18", - "reference": "1df4dc28a6b5bb7ab117ab073c1712256e954e18", + "url": "https://api.github.com/repos/mck89/peast/zipball/c6a63f32410d2e4ee2cd20fe94b35af147fb852d", + "reference": "c6a63f32410d2e4ee2cd20fe94b35af147fb852d", "shasum": "" }, "require": { @@ -2903,7 +2908,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.15.4-dev" + "dev-master": "1.17.4-dev" } }, "autoload": { @@ -2924,72 +2929,22 @@ "description": "Peast is PHP library that generates AST for JavaScript code", "support": { "issues": "https://github.com/mck89/peast/issues", - "source": "https://github.com/mck89/peast/tree/v1.15.4" + "source": "https://github.com/mck89/peast/tree/v1.17.4" }, - "time": "2023-08-12T08:29:29+00:00" - }, - { - "name": "mustache/mustache", - "version": "v2.14.2", - "source": { - "type": "git", - "url": "https://github.com/bobthecow/mustache.php.git", - "reference": "e62b7c3849d22ec55f3ec425507bf7968193a6cb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/bobthecow/mustache.php/zipball/e62b7c3849d22ec55f3ec425507bf7968193a6cb", - "reference": "e62b7c3849d22ec55f3ec425507bf7968193a6cb", - "shasum": "" - }, - "require": { - "php": ">=5.2.4" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "~1.11", - "phpunit/phpunit": "~3.7|~4.0|~5.0" - }, - "type": "library", - "autoload": { - "psr-0": { - "Mustache": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Justin Hileman", - "email": "justin@justinhileman.info", - "homepage": "http://justinhileman.com" - } - ], - "description": "A Mustache implementation in PHP.", - "homepage": "https://github.com/bobthecow/mustache.php", - "keywords": [ - "mustache", - "templating" - ], - "support": { - "issues": "https://github.com/bobthecow/mustache.php/issues", - "source": "https://github.com/bobthecow/mustache.php/tree/v2.14.2" - }, - "time": "2022-08-23T13:07:01+00:00" + "time": "2025-10-10T12:53:17+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.12.1", + "version": "1.13.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { @@ -3028,7 +2983,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { @@ -3036,20 +2991,20 @@ "type": "tidelift" } ], - "time": "2024-11-08T17:47:46+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { "name": "nikic/php-parser", - "version": "v5.3.1", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -3068,7 +3023,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -3092,9 +3047,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2024-10-08T18:51:32+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "phar-io/manifest", @@ -3216,29 +3171,29 @@ }, { "name": "phpcsstandards/phpcsextra", - "version": "1.1.2", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", - "reference": "746c3190ba8eb2f212087c947ba75f4f5b9a58d5" + "reference": "b598aa890815b8df16363271b659d73280129101" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/746c3190ba8eb2f212087c947ba75f4f5b9a58d5", - "reference": "746c3190ba8eb2f212087c947ba75f4f5b9a58d5", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/b598aa890815b8df16363271b659d73280129101", + "reference": "b598aa890815b8df16363271b659d73280129101", "shasum": "" }, "require": { "php": ">=5.4", - "phpcsstandards/phpcsutils": "^1.0.8", - "squizlabs/php_codesniffer": "^3.7.1" + "phpcsstandards/phpcsutils": "^1.2.0", + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" }, "require-dev": { "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.3.2", - "phpcsstandards/phpcsdevcs": "^1.1.6", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", "phpcsstandards/phpcsdevtools": "^1.2.1", - "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0" + "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" }, "type": "phpcodesniffer-standard", "extra": { @@ -3273,35 +3228,54 @@ ], "support": { "issues": "https://github.com/PHPCSStandards/PHPCSExtra/issues", + "security": "https://github.com/PHPCSStandards/PHPCSExtra/security/policy", "source": "https://github.com/PHPCSStandards/PHPCSExtra" }, - "time": "2023-09-20T22:06:18+00:00" + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-11-12T23:06:57+00:00" }, { "name": "phpcsstandards/phpcsutils", - "version": "1.0.8", + "version": "1.2.2", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", - "reference": "69465cab9d12454e5e7767b9041af0cd8cd13be7" + "reference": "c216317e96c8b3f5932808f9b0f1f7a14e3bbf55" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/69465cab9d12454e5e7767b9041af0cd8cd13be7", - "reference": "69465cab9d12454e5e7767b9041af0cd8cd13be7", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/c216317e96c8b3f5932808f9b0f1f7a14e3bbf55", + "reference": "c216317e96c8b3f5932808f9b0f1f7a14e3bbf55", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", "php": ">=5.4", - "squizlabs/php_codesniffer": "^3.7.1 || 4.0.x-dev@dev" + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" }, "require-dev": { "ext-filter": "*", "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.3.2", - "phpcsstandards/phpcsdevcs": "^1.1.6", - "yoast/phpunit-polyfills": "^1.0.5 || ^2.0.0" + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", + "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0 || ^3.0.0" }, "type": "phpcodesniffer-standard", "extra": { @@ -3338,6 +3312,7 @@ "phpcodesniffer-standard", "phpcs", "phpcs3", + "phpcs4", "standards", "static analysis", "tokens", @@ -3346,9 +3321,28 @@ "support": { "docs": "https://phpcsutils.com/", "issues": "https://github.com/PHPCSStandards/PHPCSUtils/issues", + "security": "https://github.com/PHPCSStandards/PHPCSUtils/security/policy", "source": "https://github.com/PHPCSStandards/PHPCSUtils" }, - "time": "2023-07-16T21:39:41+00:00" + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-12-08T14:27:58+00:00" }, { "name": "phpunit/php-code-coverage", @@ -3671,16 +3665,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.21", + "version": "9.6.31", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa" + "reference": "945d0b7f346a084ce5549e95289962972c4272e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", - "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/945d0b7f346a084ce5549e95289962972c4272e5", + "reference": "945d0b7f346a084ce5549e95289962972c4272e5", "shasum": "" }, "require": { @@ -3691,7 +3685,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.0", + "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=7.3", @@ -3702,11 +3696,11 @@ "phpunit/php-timer": "^5.0.3", "sebastian/cli-parser": "^1.0.2", "sebastian/code-unit": "^1.0.8", - "sebastian/comparator": "^4.0.8", + "sebastian/comparator": "^4.0.9", "sebastian/diff": "^4.0.6", "sebastian/environment": "^5.1.5", - "sebastian/exporter": "^4.0.6", - "sebastian/global-state": "^5.0.7", + "sebastian/exporter": "^4.0.8", + "sebastian/global-state": "^5.0.8", "sebastian/object-enumerator": "^4.0.4", "sebastian/resource-operations": "^3.0.4", "sebastian/type": "^3.2.1", @@ -3754,7 +3748,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.21" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.31" }, "funding": [ { @@ -3765,12 +3759,20 @@ "url": "https://github.com/sebastianbergmann", "type": "github" }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, { "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", "type": "tidelift" } ], - "time": "2024-09-19T10:50:18+00:00" + "time": "2025-12-06T07:45:52+00:00" }, { "name": "sebastian/cli-parser", @@ -3941,16 +3943,16 @@ }, { "name": "sebastian/comparator", - "version": "4.0.8", + "version": "4.0.9", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/67a2df3a62639eab2cc5906065e9805d4fd5dfc5", + "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5", "shasum": "" }, "require": { @@ -4003,15 +4005,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.9" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" } ], - "time": "2022-09-14T12:41:17+00:00" + "time": "2025-08-10T06:51:50+00:00" }, { "name": "sebastian/complexity", @@ -4201,16 +4215,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.6", + "version": "4.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", "shasum": "" }, "require": { @@ -4266,28 +4280,40 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2024-03-02T06:33:00+00:00" + "time": "2025-09-24T06:03:27+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.7", + "version": "5.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", "shasum": "" }, "require": { @@ -4330,15 +4356,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" } ], - "time": "2024-03-02T06:35:11+00:00" + "time": "2025-08-10T07:10:35+00:00" }, { "name": "sebastian/lines-of-code", @@ -4511,16 +4549,16 @@ }, { "name": "sebastian/recursion-context", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", "shasum": "" }, "require": { @@ -4562,15 +4600,27 @@ "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" } ], - "time": "2023-02-03T06:07:39+00:00" + "time": "2025-08-10T06:57:39+00:00" }, { "name": "sebastian/resource-operations", @@ -4737,16 +4787,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.7.2", + "version": "3.13.5", "source": { "type": "git", - "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879" + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879", - "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0ca86845ce43291e8f5692c7356fccf3bcf02bf4", + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4", "shasum": "" }, "require": { @@ -4756,18 +4806,13 @@ "php": ">=5.4.0" }, "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" }, "bin": [ - "bin/phpcs", - "bin/phpcbf" + "bin/phpcbf", + "bin/phpcs" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" @@ -4775,35 +4820,62 @@ "authors": [ { "name": "Greg Sherwood", - "role": "lead" + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" } ], "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", - "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", "keywords": [ "phpcs", "standards", "static analysis" ], "support": { - "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", - "source": "https://github.com/squizlabs/PHP_CodeSniffer", - "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" }, - "time": "2023-02-22T23:07:41+00:00" + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-11-04T16:30:35+00:00" }, { "name": "symfony/finder", - "version": "v5.4.27", + "version": "v5.4.45", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "ff4bce3c33451e7ec778070e45bd23f74214cd5d" + "reference": "63741784cd7b9967975eec610b256eed3ede022b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/ff4bce3c33451e7ec778070e45bd23f74214cd5d", - "reference": "ff4bce3c33451e7ec778070e45bd23f74214cd5d", + "url": "https://api.github.com/repos/symfony/finder/zipball/63741784cd7b9967975eec610b256eed3ede022b", + "reference": "63741784cd7b9967975eec610b256eed3ede022b", "shasum": "" }, "require": { @@ -4837,7 +4909,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.4.27" + "source": "https://github.com/symfony/finder/tree/v5.4.45" }, "funding": [ { @@ -4853,20 +4925,20 @@ "type": "tidelift" } ], - "time": "2023-07-31T08:02:31+00:00" + "time": "2024-09-28T13:32:08+00:00" }, { "name": "theseer/tokenizer", - "version": "1.2.3", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", "shasum": "" }, "require": { @@ -4895,7 +4967,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" }, "funding": [ { @@ -4903,31 +4975,31 @@ "type": "github" } ], - "time": "2024-03-03T12:36:25+00:00" + "time": "2025-11-17T20:03:58+00:00" }, { "name": "wp-cli/i18n-command", - "version": "v2.4.4", + "version": "v2.6.6", "source": { "type": "git", "url": "https://github.com/wp-cli/i18n-command.git", - "reference": "7d82e675f271359b1af614e6325d8eeaeb7d7474" + "reference": "94f72ddc4be8919f2cea181ba39cd140dd480d64" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wp-cli/i18n-command/zipball/7d82e675f271359b1af614e6325d8eeaeb7d7474", - "reference": "7d82e675f271359b1af614e6325d8eeaeb7d7474", + "url": "https://api.github.com/repos/wp-cli/i18n-command/zipball/94f72ddc4be8919f2cea181ba39cd140dd480d64", + "reference": "94f72ddc4be8919f2cea181ba39cd140dd480d64", "shasum": "" }, "require": { "eftec/bladeone": "3.52", "gettext/gettext": "^4.8", "mck89/peast": "^1.13.11", - "wp-cli/wp-cli": "^2.5" + "wp-cli/wp-cli": "^2.12" }, "require-dev": { "wp-cli/scaffold-command": "^1.2 || ^2", - "wp-cli/wp-cli-tests": "^4" + "wp-cli/wp-cli-tests": "^5.0.0" }, "suggest": { "ext-json": "Used for reading and generating JSON translation files", @@ -4935,17 +5007,18 @@ }, "type": "wp-cli-package", "extra": { - "branch-alias": { - "dev-main": "2.x-dev" - }, "bundled": true, "commands": [ "i18n", "i18n make-pot", "i18n make-json", "i18n make-mo", + "i18n make-php", "i18n update-po" - ] + ], + "branch-alias": { + "dev-main": "2.x-dev" + } }, "autoload": { "files": [ @@ -4969,9 +5042,61 @@ "homepage": "https://github.com/wp-cli/i18n-command", "support": { "issues": "https://github.com/wp-cli/i18n-command/issues", - "source": "https://github.com/wp-cli/i18n-command/tree/v2.4.4" + "source": "https://github.com/wp-cli/i18n-command/tree/v2.6.6" + }, + "time": "2025-11-21T04:23:34+00:00" + }, + { + "name": "wp-cli/mustache", + "version": "v2.14.99", + "source": { + "type": "git", + "url": "https://github.com/wp-cli/mustache.php.git", + "reference": "ca23b97ac35fbe01c160549eb634396183d04a59" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wp-cli/mustache.php/zipball/ca23b97ac35fbe01c160549eb634396183d04a59", + "reference": "ca23b97ac35fbe01c160549eb634396183d04a59", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "replace": { + "mustache/mustache": "^2.14.2" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.19.3", + "yoast/phpunit-polyfills": "^2.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "Mustache": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info", + "homepage": "http://justinhileman.com" + } + ], + "description": "A Mustache implementation in PHP.", + "homepage": "https://github.com/bobthecow/mustache.php", + "keywords": [ + "mustache", + "templating" + ], + "support": { + "source": "https://github.com/wp-cli/mustache.php/tree/v2.14.99" }, - "time": "2023-08-30T18:00:10+00:00" + "time": "2025-05-06T16:15:37+00:00" }, { "name": "wp-cli/mustangostang-spyc", @@ -5026,29 +5151,29 @@ }, { "name": "wp-cli/php-cli-tools", - "version": "v0.11.21", + "version": "v0.12.6", "source": { "type": "git", "url": "https://github.com/wp-cli/php-cli-tools.git", - "reference": "b3457a8d60cd0b1c48cab76ad95df136d266f0b6" + "reference": "f12b650d3738e471baed6dd47982d53c5c0ab1c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wp-cli/php-cli-tools/zipball/b3457a8d60cd0b1c48cab76ad95df136d266f0b6", - "reference": "b3457a8d60cd0b1c48cab76ad95df136d266f0b6", + "url": "https://api.github.com/repos/wp-cli/php-cli-tools/zipball/f12b650d3738e471baed6dd47982d53c5c0ab1c3", + "reference": "f12b650d3738e471baed6dd47982d53c5c0ab1c3", "shasum": "" }, "require": { - "php": ">= 5.3.0" + "php": ">= 7.2.24" }, "require-dev": { "roave/security-advisories": "dev-latest", - "wp-cli/wp-cli-tests": "^4" + "wp-cli/wp-cli-tests": "^5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "0.11.x-dev" + "dev-master": "0.12.x-dev" } }, "autoload": { @@ -5083,39 +5208,38 @@ ], "support": { "issues": "https://github.com/wp-cli/php-cli-tools/issues", - "source": "https://github.com/wp-cli/php-cli-tools/tree/v0.11.21" + "source": "https://github.com/wp-cli/php-cli-tools/tree/v0.12.6" }, - "time": "2023-09-29T15:28:10+00:00" + "time": "2025-09-11T12:43:04+00:00" }, { "name": "wp-cli/wp-cli", - "version": "v2.9.0", + "version": "v2.12.0", "source": { "type": "git", "url": "https://github.com/wp-cli/wp-cli.git", - "reference": "8a3befba2d947fbf5cc6d1941edf2dd99da4d4b7" + "reference": "03d30d4138d12b4bffd8b507b82e56e129e0523f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wp-cli/wp-cli/zipball/8a3befba2d947fbf5cc6d1941edf2dd99da4d4b7", - "reference": "8a3befba2d947fbf5cc6d1941edf2dd99da4d4b7", + "url": "https://api.github.com/repos/wp-cli/wp-cli/zipball/03d30d4138d12b4bffd8b507b82e56e129e0523f", + "reference": "03d30d4138d12b4bffd8b507b82e56e129e0523f", "shasum": "" }, "require": { "ext-curl": "*", - "mustache/mustache": "^2.14.1", "php": "^5.6 || ^7.0 || ^8.0", "symfony/finder": ">2.7", + "wp-cli/mustache": "^2.14.99", "wp-cli/mustangostang-spyc": "^0.6.3", - "wp-cli/php-cli-tools": "~0.11.2" + "wp-cli/php-cli-tools": "~0.12.4" }, "require-dev": { - "roave/security-advisories": "dev-latest", "wp-cli/db-command": "^1.3 || ^2", "wp-cli/entity-command": "^1.2 || ^2", "wp-cli/extension-command": "^1.1 || ^2", "wp-cli/package-command": "^1 || ^2", - "wp-cli/wp-cli-tests": "^4.0.1" + "wp-cli/wp-cli-tests": "^4.3.10" }, "suggest": { "ext-readline": "Include for a better --prompt implementation", @@ -5128,7 +5252,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.9.x-dev" + "dev-main": "2.12.x-dev" } }, "autoload": { @@ -5155,20 +5279,20 @@ "issues": "https://github.com/wp-cli/wp-cli/issues", "source": "https://github.com/wp-cli/wp-cli" }, - "time": "2023-10-25T09:06:37+00:00" + "time": "2025-05-07T01:16:12+00:00" }, { "name": "wp-coding-standards/wpcs", - "version": "3.0.1", + "version": "3.3.0", "source": { "type": "git", "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", - "reference": "b4caf9689f1a0e4a4c632679a44e638c1c67aff1" + "reference": "7795ec6fa05663d716a549d0b44e47ffc8b0d4a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/b4caf9689f1a0e4a4c632679a44e638c1c67aff1", - "reference": "b4caf9689f1a0e4a4c632679a44e638c1c67aff1", + "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/7795ec6fa05663d716a549d0b44e47ffc8b0d4a6", + "reference": "7795ec6fa05663d716a549d0b44e47ffc8b0d4a6", "shasum": "" }, "require": { @@ -5176,17 +5300,17 @@ "ext-libxml": "*", "ext-tokenizer": "*", "ext-xmlreader": "*", - "php": ">=5.4", - "phpcsstandards/phpcsextra": "^1.1.0", - "phpcsstandards/phpcsutils": "^1.0.8", - "squizlabs/php_codesniffer": "^3.7.2" + "php": ">=7.2", + "phpcsstandards/phpcsextra": "^1.5.0", + "phpcsstandards/phpcsutils": "^1.1.0", + "squizlabs/php_codesniffer": "^3.13.4" }, "require-dev": { "php-parallel-lint/php-console-highlighter": "^1.0.0", - "php-parallel-lint/php-parallel-lint": "^1.3.2", - "phpcompatibility/php-compatibility": "^9.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^10.0.0@dev", "phpcsstandards/phpcsdevtools": "^1.2.0", - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + "phpunit/phpunit": "^8.0 || ^9.0" }, "suggest": { "ext-iconv": "For improved results", @@ -5217,24 +5341,24 @@ }, "funding": [ { - "url": "https://opencollective.com/thewpcc/contribute/wp-php-63406", + "url": "https://opencollective.com/php_codesniffer", "type": "custom" } ], - "time": "2023-09-14T07:06:09+00:00" + "time": "2025-11-25T12:08:04+00:00" }, { "name": "yoast/phpunit-polyfills", - "version": "1.1.0", + "version": "1.1.5", "source": { "type": "git", "url": "https://github.com/Yoast/PHPUnit-Polyfills.git", - "reference": "224e4a1329c03d8bad520e3fc4ec980034a4b212" + "reference": "41aaac462fbd80feb8dd129e489f4bbc53fe26b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/224e4a1329c03d8bad520e3fc4ec980034a4b212", - "reference": "224e4a1329c03d8bad520e3fc4ec980034a4b212", + "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/41aaac462fbd80feb8dd129e489f4bbc53fe26b0", + "reference": "41aaac462fbd80feb8dd129e489f4bbc53fe26b0", "shasum": "" }, "require": { @@ -5242,12 +5366,14 @@ "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.0" }, "require-dev": { - "yoast/yoastcs": "^2.3.0" + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "yoast/yoastcs": "^3.2.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "2.x-dev" + "dev-main": "4.x-dev" } }, "autoload": { @@ -5279,9 +5405,10 @@ ], "support": { "issues": "https://github.com/Yoast/PHPUnit-Polyfills/issues", + "security": "https://github.com/Yoast/PHPUnit-Polyfills/security/policy", "source": "https://github.com/Yoast/PHPUnit-Polyfills" }, - "time": "2023-08-19T14:25:08+00:00" + "time": "2025-08-10T04:54:36+00:00" } ], "aliases": [], diff --git a/src/API/Google/AdsCampaign.php b/src/API/Google/AdsCampaign.php index dcca194b79..3bb09cfee4 100644 --- a/src/API/Google/AdsCampaign.php +++ b/src/API/Google/AdsCampaign.php @@ -28,6 +28,9 @@ use Google\Ads\GoogleAds\V22\Services\GoogleAdsRow; use Google\Ads\GoogleAds\V22\Services\MutateGoogleAdsRequest; use Google\Ads\GoogleAds\V22\Services\MutateOperation; +use Google\Ads\GoogleAds\V22\Resources\Campaign\AssetAutomationSetting; +use Google\Ads\GoogleAds\V22\Enums\AssetAutomationTypeEnum\AssetAutomationType; +use Google\Ads\GoogleAds\V22\Enums\AssetAutomationStatusEnum\AssetAutomationStatus; use Google\ApiCore\ApiException; use Google\ApiCore\ValidationException; use Exception; @@ -495,7 +498,14 @@ protected function create_operation( string $campaign_name, string $country, boo 'status' => CampaignStatus::number( 'enabled' ), 'campaign_budget' => $this->budget->temporary_resource_name(), 'maximize_conversion_value' => new MaximizeConversionValue(), - 'url_expansion_opt_out' => false, + 'asset_automation_settings' => [ + new AssetAutomationSetting( + [ + 'asset_automation_type' => AssetAutomationType::FINAL_URL_EXPANSION_TEXT_ASSET_AUTOMATION, + 'asset_automation_status' => AssetAutomationStatus::OPTED_IN + ] + ) + ], 'shopping_setting' => new ShoppingSetting( [ 'merchant_id' => $this->options->get_merchant_id(), From 41c81517148eaa47bd8e64468c64c20ee8743628 Mon Sep 17 00:00:00 2001 From: Ankit Gade Date: Thu, 15 Jan 2026 21:15:32 +0530 Subject: [PATCH 025/123] Fix: php lint. --- src/API/Google/AdsCampaign.php | 10 +++++----- src/Coupon/WCCouponAdapter.php | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/API/Google/AdsCampaign.php b/src/API/Google/AdsCampaign.php index 3bb09cfee4..a612b11b8f 100644 --- a/src/API/Google/AdsCampaign.php +++ b/src/API/Google/AdsCampaign.php @@ -500,11 +500,11 @@ protected function create_operation( string $campaign_name, string $country, boo 'maximize_conversion_value' => new MaximizeConversionValue(), 'asset_automation_settings' => [ new AssetAutomationSetting( - [ - 'asset_automation_type' => AssetAutomationType::FINAL_URL_EXPANSION_TEXT_ASSET_AUTOMATION, - 'asset_automation_status' => AssetAutomationStatus::OPTED_IN - ] - ) + [ + 'asset_automation_type' => AssetAutomationType::FINAL_URL_EXPANSION_TEXT_ASSET_AUTOMATION, + 'asset_automation_status' => AssetAutomationStatus::OPTED_IN, + ] + ), ], 'shopping_setting' => new ShoppingSetting( [ diff --git a/src/Coupon/WCCouponAdapter.php b/src/Coupon/WCCouponAdapter.php index 4b1a037148..9851f26534 100644 --- a/src/Coupon/WCCouponAdapter.php +++ b/src/Coupon/WCCouponAdapter.php @@ -385,6 +385,7 @@ public function get_wc_coupon_id(): int { /** * * @param string $targetCountry + * * phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase */ public function setTargetCountry( $targetCountry ) { From 335504e8ffdd11102edfecded3e68aaa35411dd5 Mon Sep 17 00:00:00 2001 From: asvinb Date: Thu, 15 Jan 2026 19:49:01 +0400 Subject: [PATCH 026/123] feat(paid-ads): Add AI-generated image picker --- .../asset-group-editor/asset-group-editor.js | 1 + .../asset-group-images-section.js | 13 +++ .../gen-ai-image-picker/index.js | 103 ++++++++++++++++++ .../gen-ai-image-picker/index.scss | 47 ++++++++ js/src/images/ai-icon.svg | 1 + 5 files changed, 165 insertions(+) create mode 100644 js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.js create mode 100644 js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.scss create mode 100644 js/src/images/ai-icon.svg diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/asset-group-editor.js b/js/src/components/paid-ads/asset-group/asset-group-editor/asset-group-editor.js index df2b5f5236..e4e9a4b231 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/asset-group-editor.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/asset-group-editor.js @@ -104,6 +104,7 @@ export default function AssetGroupEditor() { /> + + { renderErrors( spec.key ) } diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.js b/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.js new file mode 100644 index 0000000000..788fdd5c83 --- /dev/null +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.js @@ -0,0 +1,103 @@ +/** + * External dependencies + */ +import { noop } from 'lodash'; +import { __ } from '@wordpress/i18n'; +import { useState } from '@wordpress/element'; +import { + Flex, + FlexItem, + CheckboxControl, + FlexBlock, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import AppButton from '~/components/app-button'; +import AIIcon from '~/images/ai-icon.svg?inline'; +import './index.scss'; + +export default function GenAIImagePicker( { + assetKey, + finalUrl, + images, + onAddSelectedImages = noop, +} ) { + const [ selectedImages, setSelectedImages ] = useState( [] ); + const handleOnAddSelectedImages = () => { + onAddSelectedImages( selectedImages ); + setSelectedImages( [] ); + }; + + const toggleImageSelection = ( src ) => { + setSelectedImages( ( previousImages ) => + previousImages.includes( src ) + ? previousImages.filter( ( image ) => image !== src ) + : [ ...previousImages, src ] + ); + }; + + return ( + + +

+ + + { __( 'AI-generated images', 'google-listings-and-ads' ) } +

+

+ { __( + 'Select to add these images to this set for your product.', + 'google-listings-and-ads' + ) } +

+
+ + + + { images.map( ( src ) => ( + + toggleImageSelection( src ) } + > + + + + toggleImageSelection( src ) } + value={ src } + /> + + ) ) } + + + + + + +
+ ); +} diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.scss b/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.scss new file mode 100644 index 0000000000..94e74c1603 --- /dev/null +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.scss @@ -0,0 +1,47 @@ +.gla-gen-ai-image-picker { + background-color: $gla-color-gray-0; + padding: $grid-unit-30; + margin: 0 0 $grid-unit-40 0; + + .gla-gen-ai-image-picker__image { + position: relative; + } + + .gla-gen-ai-image-picker__medium-button { + justify-content: center; + width: 100px; + height: 100px; + padding: 0; + border: 1px solid $gray-200; + border-radius: 4px; + overflow: hidden; + background-color: $white; + position: relative; + display: block; + } + + .gla-gen-ai-image-picker__checkbox { + position: absolute; + top: $grid-unit-10; + left: $grid-unit-10; + width: 16px; + height: 16px; + } + + .gla-gen-ai-image-picker__title { + font-size: $gla-font-base; + font-weight: 400; + margin: 0; + + svg { + color: var(--wp-components-color-accent, var(--wp-admin-theme-color, #3858e9)); + vertical-align: bottom; + } + } + + .gla-gen-ai-image-picker__description { + font-size: $gla-font-smaller; + color: $gray-700; + margin: $grid-unit-05 0 0; + } +} diff --git a/js/src/images/ai-icon.svg b/js/src/images/ai-icon.svg new file mode 100644 index 0000000000..23b02113d1 --- /dev/null +++ b/js/src/images/ai-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file From bbf5aa11dd22176367cd71adc59c8f2aafa5fe81 Mon Sep 17 00:00:00 2001 From: asvinb Date: Thu, 15 Jan 2026 21:10:54 +0400 Subject: [PATCH 027/123] feat(paid-ads): Implement GenAIImagePicker component --- .../asset-group-images-section.js | 12 +-- .../gen-ai-image-picker/index.js | 79 ++++++++++++------- .../gen-ai-image-picker/index.scss | 5 ++ .../asset-group-editor/images-selector.js | 22 ++++++ 4 files changed, 77 insertions(+), 41 deletions(-) diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/asset-group-images-section.js b/js/src/components/paid-ads/asset-group/asset-group-editor/asset-group-images-section.js index 4bcdf97358..d9ad3f5cbf 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/asset-group-images-section.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/asset-group-images-section.js @@ -11,7 +11,6 @@ import ImagesSelector from './images-selector'; import AssetField from './asset-field'; import Section from '~/components/section'; import AppDocumentationLink from '~/components/app-documentation-link'; -import GenAIImagePicker from './gen-ai-image-picker'; import { ASSET_IMAGE_SPECS } from '../../assetSpecs'; /** @@ -95,6 +94,7 @@ const AssetGroupImagesSection = ( { initialExpanded={ isSelectedFinalUrl } > - - { renderErrors( spec.key ) } diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.js b/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.js index 788fdd5c83..23d78c188e 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.js @@ -1,7 +1,6 @@ /** * External dependencies */ -import { noop } from 'lodash'; import { __ } from '@wordpress/i18n'; import { useState } from '@wordpress/element'; import { @@ -14,17 +13,22 @@ import { /** * Internal dependencies */ +import { useAdaptiveFormContext } from '~/components/adaptive-form'; import AppButton from '~/components/app-button'; import AIIcon from '~/images/ai-icon.svg?inline'; import './index.scss'; export default function GenAIImagePicker( { assetKey, - finalUrl, images, - onAddSelectedImages = noop, + onAddSelectedImages, } ) { + const { values } = useAdaptiveFormContext(); + const addedImageUrls = values[ assetKey ] || []; + const { final_url: finalUrl } = values; + const [ selectedImages, setSelectedImages ] = useState( [] ); + const handleOnAddSelectedImages = () => { onAddSelectedImages( selectedImages ); setSelectedImages( [] ); @@ -55,35 +59,50 @@ export default function GenAIImagePicker( {
- - { images.map( ( src ) => ( - - toggleImageSelection( src ) } + + { images.map( ( src ) => { + if ( addedImageUrls.includes( src ) ) { + return null; + } + + return ( + - - + + toggleImageSelection( src ) + } + > + + - toggleImageSelection( src ) } - value={ src } - /> - - ) ) } + + toggleImageSelection( src ) + } + value={ src } + /> + + ); + } ) } diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.scss b/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.scss index 94e74c1603..72a019efab 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.scss +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.scss @@ -3,6 +3,10 @@ padding: $grid-unit-30; margin: 0 0 $grid-unit-40 0; + &:has(.gla-gen-ai-image-picker__images:empty) { + display: none; + } + .gla-gen-ai-image-picker__image { position: relative; } @@ -18,6 +22,7 @@ background-color: $white; position: relative; display: block; + } .gla-gen-ai-image-picker__checkbox { diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/images-selector.js b/js/src/components/paid-ads/asset-group/asset-group-editor/images-selector.js index 92a0b2d69f..e0fec76847 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/images-selector.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/images-selector.js @@ -12,6 +12,7 @@ import useCroppedImageSelector from '~/hooks/useCroppedImageSelector'; import AppTooltip from '~/components/app-tooltip'; import AddAssetItemButton from './add-asset-item-button'; import MediaSelector from './media-selector'; +import GenAIImagePicker from './gen-ai-image-picker'; /** * @typedef {Object} AssetImageConfig @@ -25,6 +26,7 @@ import MediaSelector from './media-selector'; * Renders a selector for asset images. * * @param {Object} props React props. + * @param {string} props.assetKey The asset key. * @param {AssetImageConfig} props.imageConfig The config of the asset image. * @param {string[]} props.initialImageUrls The initial image URLs. * @param {number} [props.maxNumberOfImages=-1] The maximum number of images. -1 by default and it means unlimited number. @@ -33,6 +35,7 @@ import MediaSelector from './media-selector'; * @param {(urls: Array) => void} [props.onChange] Callback function to be called when the texts are changed. */ export default function ImagesSelector( { + assetKey, imageConfig, initialImageUrls = [], maxNumberOfImages = -1, @@ -100,6 +103,15 @@ export default function ImagesSelector( { handle.openSelector( image?.id ); }; + const handleOnAddSelectedImages = ( selectedImageUrls ) => { + const selectedImages = selectedImageUrls.map( ( url ) => ( { + url, + id: url, + alt: '', + } ) ); + updateImages( [ ...images, ...selectedImages ] ); + }; + const renderAddButton = () => { const disabled = maxNumberOfImages !== -1 && images.length >= maxNumberOfImages; @@ -130,6 +142,16 @@ export default function ImagesSelector( { onRemoveMedia={ handleRemoveImage } /> + + { children } { renderAddButton() } From 533e8c994183dba6726eb269a428059a7cef8b8c Mon Sep 17 00:00:00 2001 From: asvinb Date: Thu, 15 Jan 2026 21:42:44 +0400 Subject: [PATCH 028/123] feat(paid-ads): Implement GenAI image generation for marketing assets --- .../asset-group-editor/asset-group-editor.js | 1 - .../asset-group-images-section.js | 3 +- .../gen-ai-image-picker/index.js | 14 +++--- .../asset-group-editor/images-selector.js | 46 ++++++++++++++++--- js/src/components/paid-ads/assetSpecs.js | 12 +++++ js/src/data/selectors.js | 12 ++--- 6 files changed, 67 insertions(+), 21 deletions(-) diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/asset-group-editor.js b/js/src/components/paid-ads/asset-group/asset-group-editor/asset-group-editor.js index e4e9a4b231..df2b5f5236 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/asset-group-editor.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/asset-group-editor.js @@ -104,7 +104,6 @@ export default function AssetGroupEditor() { /> { renderErrors( spec.key ) } diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.js b/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.js index 23d78c188e..87befb95c6 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.js @@ -14,18 +14,16 @@ import { * Internal dependencies */ import { useAdaptiveFormContext } from '~/components/adaptive-form'; +import useGenAIMediaAssets from '~/hooks/useGenAIMediaAssets'; import AppButton from '~/components/app-button'; import AIIcon from '~/images/ai-icon.svg?inline'; import './index.scss'; -export default function GenAIImagePicker( { - assetKey, - images, - onAddSelectedImages, -} ) { +export default function GenAIImagePicker( { assetKey, onAddSelectedImages } ) { const { values } = useAdaptiveFormContext(); const addedImageUrls = values[ assetKey ] || []; const { final_url: finalUrl } = values; + const { assets } = useGenAIMediaAssets( finalUrl, assetKey ); const [ selectedImages, setSelectedImages ] = useState( [] ); @@ -42,6 +40,10 @@ export default function GenAIImagePicker( { ); }; + if ( ! assets || assets.length === 0 ) { + return null; + } + return ( @@ -65,7 +67,7 @@ export default function GenAIImagePicker( { className="gla-gen-ai-image-picker__images" wrap > - { images.map( ( src ) => { + { assets.map( ( src ) => { if ( addedImageUrls.includes( src ) ) { return null; } diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/images-selector.js b/js/src/components/paid-ads/asset-group/asset-group-editor/images-selector.js index 5ba1c0b576..5c8ad4c405 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/images-selector.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/images-selector.js @@ -8,9 +8,14 @@ import { useState, useEffect, useRef } from '@wordpress/element'; /** * Internal dependencies */ +import { useAppDispatch } from '~/data'; +import useDispatchCoreNotices from '~/hooks/useDispatchCoreNotices'; +import { useAdaptiveFormContext } from '~/components/adaptive-form'; import useCroppedImageSelector from '~/hooks/useCroppedImageSelector'; import AppTooltip from '~/components/app-tooltip'; -import AssetItemActionButton from './asset-item-action-button'; +import AssetItemActionButton, { + ACTION_TYPES, +} from './asset-item-action-button'; import MediaSelector from './media-selector'; import GenAIImagePicker from './gen-ai-image-picker'; @@ -29,6 +34,7 @@ import GenAIImagePicker from './gen-ai-image-picker'; * @param {string} props.assetKey The asset key. * @param {AssetImageConfig} props.imageConfig The config of the asset image. * @param {string[]} props.initialImageUrls The initial image URLs. + * @param {string} props.generateButtonText The text for the generate button. * @param {number} [props.maxNumberOfImages=-1] The maximum number of images. -1 by default and it means unlimited number. * @param {string} [props.reachedMaxNumberTip] The tooltip content floating on the add button when reaching the max number of images. * @param {JSX.Element} [props.children] Content to be rendered above the add button. @@ -38,13 +44,18 @@ export default function ImagesSelector( { assetKey, imageConfig, initialImageUrls = [], + generateButtonText, maxNumberOfImages = -1, reachedMaxNumberTip, children, onChange = noop, } ) { + const { values } = useAdaptiveFormContext(); const updateImagesRef = useRef(); + const [ isGeneratingAssets, setIsGeneratingAssets ] = useState( false ); const [ awaitingActionImage, setAwaitingActionImage ] = useState( null ); + const { fetchGenAIMediaAssets } = useAppDispatch(); + const { createNotice } = useDispatchCoreNotices(); const [ images, setImages ] = useState( () => // The asset images fetched from Google Ads are only URLs. initialImageUrls.map( ( url ) => ( { url, id: url, alt: '' } ) ) @@ -134,6 +145,25 @@ export default function ImagesSelector( { return button; }; + const handleGenerateClick = async () => { + setIsGeneratingAssets( true ); + + try { + const { final_url: finalUrl } = values; + await fetchGenAIMediaAssets( finalUrl, assetKey ); + } catch ( error ) { + createNotice( + 'error', + __( + 'Something went wrong while generating texts. Please try again.', + 'google-listings-and-ads' + ) + ); + } finally { + setIsGeneratingAssets( false ); + } + }; + return (
{ children } { renderAddButton() } + + { generateButtonText && ( + + ) }
); } diff --git a/js/src/components/paid-ads/assetSpecs.js b/js/src/components/paid-ads/assetSpecs.js index 383976655f..cd6c488e2e 100644 --- a/js/src/components/paid-ads/assetSpecs.js +++ b/js/src/components/paid-ads/assetSpecs.js @@ -64,6 +64,10 @@ const ASSET_MARKETING_IMAGE_SPECS = [ 'Lowercase asset field name', 'google-listings-and-ads' ), + generateButtonText: __( + 'Generate landscape images', + 'google-listings-and-ads' + ), }, { key: ASSET_FORM_KEY.SQUARE_MARKETING_IMAGE, @@ -93,6 +97,10 @@ const ASSET_MARKETING_IMAGE_SPECS = [ 'Lowercase asset field name', 'google-listings-and-ads' ), + generateButtonText: __( + 'Generate square images', + 'google-listings-and-ads' + ), }, { key: ASSET_FORM_KEY.PORTRAIT_MARKETING_IMAGE, @@ -122,6 +130,10 @@ const ASSET_MARKETING_IMAGE_SPECS = [ 'Lowercase asset field name', 'google-listings-and-ads' ), + generateButtonText: __( + 'Generate portrait images', + 'google-listings-and-ads' + ), }, ]; diff --git a/js/src/data/selectors.js b/js/src/data/selectors.js index 64b6e7740b..cfdebdc4ed 100644 --- a/js/src/data/selectors.js +++ b/js/src/data/selectors.js @@ -503,17 +503,17 @@ export const getAdsRecommendations = ( state, types, campaign_id = null ) => { * @param {Object} state - The Redux state object containing GenAI assets data. * @param {string} url - The URL associated with the GenAI assets. * @param {'marketing_image'|'square_marketing_image'|'portrait_marketing_image'|undefined} [assetType] - The type of media asset to retrieve. - * @return {Object|null} The media assets for the specified URL and type, or null if not found. + * @return {Array} The media assets for the specified URL and type, or an empty array if not found. */ export const getGenAIMediaAssets = ( state, url, assetType ) => { const mediaAssets = state.gen_ai_assets?.[ url ]?.media; if ( ! url || ! mediaAssets ) { - return null; + return []; } if ( assetType ) { - return mediaAssets[ assetType ] ?? null; + return mediaAssets[ assetType ] ?? []; } return mediaAssets; @@ -525,17 +525,17 @@ export const getGenAIMediaAssets = ( state, url, assetType ) => { * @param {Object} state - The Redux state object containing GenAI assets data. * @param {string} url - The URL associated with the GenAI assets. * @param {'headline'|'long_headline'|'description'|undefined} [assetType] - The type of text asset to retrieve. - * @return {Object|null} The text assets for the specified URL and type, or null if not found. + * @return {Array} The text assets for the specified URL and type, or an empty array if not found. */ export const getGenAITextAssets = ( state, url, assetType ) => { const textAssets = state.gen_ai_assets?.[ url ]?.text; if ( ! url || ! textAssets ) { - return null; + return []; } if ( assetType ) { - return textAssets[ assetType ] ?? null; + return textAssets[ assetType ] ?? []; } return textAssets; From 5478e049db2711fec98754dd61bc145fa0d6523d Mon Sep 17 00:00:00 2001 From: asvinb Date: Thu, 15 Jan 2026 21:45:53 +0400 Subject: [PATCH 029/123] fix: Remove unused state variable and update image selection logic --- .../asset-group/asset-group-editor/gen-ai-image-picker/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.js b/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.js index 87befb95c6..78d167612f 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.js @@ -24,7 +24,6 @@ export default function GenAIImagePicker( { assetKey, onAddSelectedImages } ) { const addedImageUrls = values[ assetKey ] || []; const { final_url: finalUrl } = values; const { assets } = useGenAIMediaAssets( finalUrl, assetKey ); - const [ selectedImages, setSelectedImages ] = useState( [] ); const handleOnAddSelectedImages = () => { From e2c7a45ce0189c08359931926b21e844012f26fc Mon Sep 17 00:00:00 2001 From: asvinb Date: Thu, 15 Jan 2026 21:55:53 +0400 Subject: [PATCH 030/123] Review comments --- .../gen-ai-image-picker/index.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.js b/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.js index 78d167612f..3c7fb5b6ea 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.js @@ -19,6 +19,14 @@ import AppButton from '~/components/app-button'; import AIIcon from '~/images/ai-icon.svg?inline'; import './index.scss'; +/** + * GenAIImagePicker component. + * Allows users to pick AI-generated images based on the final URL and the spec type. + * + * @param {Object} props Component props. + * @param {string} props.assetKey Asset key. + * @param {Function} props.onAddSelectedImages Callback to add selected images. + */ export default function GenAIImagePicker( { assetKey, onAddSelectedImages } ) { const { values } = useAdaptiveFormContext(); const addedImageUrls = values[ assetKey ] || []; @@ -28,7 +36,6 @@ export default function GenAIImagePicker( { assetKey, onAddSelectedImages } ) { const handleOnAddSelectedImages = () => { onAddSelectedImages( selectedImages ); - setSelectedImages( [] ); }; const toggleImageSelection = ( src ) => { @@ -39,7 +46,7 @@ export default function GenAIImagePicker( { assetKey, onAddSelectedImages } ) { ); }; - if ( ! assets || assets.length === 0 ) { + if ( ! assets || assets.length === 0 || ! finalUrl ) { return null; } @@ -67,6 +74,7 @@ export default function GenAIImagePicker( { assetKey, onAddSelectedImages } ) { wrap > { assets.map( ( src ) => { + // Hide the image if it's already been added to the asset group. if ( addedImageUrls.includes( src ) ) { return null; } @@ -79,7 +87,7 @@ export default function GenAIImagePicker( { assetKey, onAddSelectedImages } ) { @@ -99,7 +107,6 @@ export default function GenAIImagePicker( { assetKey, onAddSelectedImages } ) { onChange={ () => toggleImageSelection( src ) } - value={ src } /> ); From 26fc8afeb47831d37f0f5c16381c6a1a273e3bb1 Mon Sep 17 00:00:00 2001 From: asvinb Date: Fri, 16 Jan 2026 10:38:44 +0400 Subject: [PATCH 031/123] =?UTF-8?q?Add=20mock=20implementation=20for=20use?= =?UTF-8?q?AdaptiveFormContext=20in=20ImagesSelector=20test=E2=8F=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../asset-group-editor/images-selector.test.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/images-selector.test.js b/js/src/components/paid-ads/asset-group/asset-group-editor/images-selector.test.js index f6fcb58c02..b018984c32 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/images-selector.test.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/images-selector.test.js @@ -20,6 +20,19 @@ jest.mock( '~/components/app-tooltip', () => jest.fn( ( props ) =>
).mockName( 'AppTooltip' ) ); +jest.mock( '~/components/adaptive-form', () => ( { + useAdaptiveFormContext: jest + .fn() + .mockName( 'useAdaptiveFormContext' ) + .mockImplementation( () => { + return { + values: { + final_url: 'https://example.com', + }, + }; + } ), +} ) ); + describe( 'ImagesSelector', () => { const imageConfig = { minWidth: 150, From 09991f7d89d11a3fade0e90d409931f896ef2d9d Mon Sep 17 00:00:00 2001 From: Ankit Gade Date: Fri, 16 Jan 2026 17:31:53 +0530 Subject: [PATCH 032/123] Address CR feedback. --- js/src/components/paid-ads/gen-ai-card.js | 38 ++++++++++++--------- js/src/components/paid-ads/gen-ai-card.scss | 26 ++------------ 2 files changed, 23 insertions(+), 41 deletions(-) diff --git a/js/src/components/paid-ads/gen-ai-card.js b/js/src/components/paid-ads/gen-ai-card.js index 2cfd5a22d8..2a5e7ae1f0 100644 --- a/js/src/components/paid-ads/gen-ai-card.js +++ b/js/src/components/paid-ads/gen-ai-card.js @@ -2,7 +2,14 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { Flex, FlexBlock, CardBody, Notice } from '@wordpress/components'; +import { Icon, check } from '@wordpress/icons'; +import { + Flex, + FlexBlock, + FlexItem, + CardBody, + Notice, +} from '@wordpress/components'; /** * Internal dependencies @@ -10,7 +17,6 @@ import { Flex, FlexBlock, CardBody, Notice } from '@wordpress/components'; import Section from '~/components/section'; import useGoogleAdsAccount from '~/hooks/useGoogleAdsAccount'; import genAIImageURL from '~/images/pmax-assets-improvements/gen-ai.svg'; -import genAICheckMark from '~/images/pmax-assets-improvements/gen-ai-check-notice.svg'; import './gen-ai-card.scss'; /** @@ -43,7 +49,9 @@ const GenAICard = () => {
- + { __( 'Review Your AI Suggestions', 'google-listings-and-ads' @@ -58,35 +66,31 @@ const GenAICard = () => {
- { -
+

{ __( - 'Text assets were auto-populate with Google AI', + 'Text assets were auto-populated with Google AI', 'google-listings-and-ads' ) } -

+

- + { - + diff --git a/js/src/components/paid-ads/gen-ai-card.scss b/js/src/components/paid-ads/gen-ai-card.scss index 0a5280f96f..d653912588 100644 --- a/js/src/components/paid-ads/gen-ai-card.scss +++ b/js/src/components/paid-ads/gen-ai-card.scss @@ -1,9 +1,5 @@ .gla-gen-ai-card { - font-family: "SF Pro Text", $default-font; - :where(.gla-gen-ai-card__wrapper) { - flex-direction: column-reverse; - @media (min-width: $break-small) { flex-direction: row; } @@ -12,13 +8,11 @@ :where(.gla-section-card-title) { font-size: 16px; margin-bottom: 8px; - - div { - font-family: "SF Pro Display", $default-font; - } } :where(.is-success) { + padding-top: 0; + padding-bottom: 0; width: 100%; :where(.components-notice__content) { @@ -27,20 +21,4 @@ gap: 12px; } } - - :where(.gla-gen-ai-card__image-block) { - flex: 0 0 92px; - - @media (min-width: $break-small) { - margin-top: 0; - margin-left: 24px; - } - - img { - display: block; - max-height: 100%; - margin: 0 auto; - max-width: 100%; - } - } } From 2fb6516c3c187a48b9cd25d23fb2e6022028f72b Mon Sep 17 00:00:00 2001 From: Ankit Gade Date: Fri, 16 Jan 2026 17:39:03 +0530 Subject: [PATCH 033/123] Update snapshot. --- .../__snapshots__/gen-ai-card.test.js.snap | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/js/src/components/paid-ads/__snapshots__/gen-ai-card.test.js.snap b/js/src/components/paid-ads/__snapshots__/gen-ai-card.test.js.snap index 2b4c6b2d0d..2de7c7244b 100644 --- a/js/src/components/paid-ads/__snapshots__/gen-ai-card.test.js.snap +++ b/js/src/components/paid-ads/__snapshots__/gen-ai-card.test.js.snap @@ -33,6 +33,7 @@ exports[`GenAICard Generate assets with GenAI button Match the snapshot 1`] = `
Review Your AI Suggestions
@@ -54,15 +55,21 @@ exports[`GenAICard Generate assets with GenAI button Match the snapshot 1`] = `
- -
- Text assets were auto-populate with Google AI -
+ xmlns="http://www.w3.org/2000/svg" + > + + +

+ Text assets were auto-populated with Google AI +

@@ -71,12 +78,12 @@ exports[`GenAICard Generate assets with GenAI button Match the snapshot 1`] = `
Drawing of a person who successfully launched a campaign Date: Fri, 16 Jan 2026 17:02:00 +0400 Subject: [PATCH 034/123] Finalize E2E tests --- .../asset-group-editor/images-selector.js | 20 +- .../add-paid-campaigns.test.js | 1131 +++++++++++------ tests/e2e/utils/mock-requests.js | 16 + tests/e2e/utils/pages/create-campaign.js | 276 ++++ 4 files changed, 1021 insertions(+), 422 deletions(-) diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/images-selector.js b/js/src/components/paid-ads/asset-group/asset-group-editor/images-selector.js index 5c8ad4c405..9e5b0bbc64 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/images-selector.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/images-selector.js @@ -115,12 +115,18 @@ export default function ImagesSelector( { }; const handleOnAddSelectedImages = ( selectedImageUrls ) => { - const selectedImages = selectedImageUrls.map( ( url ) => ( { - url, - id: url, - alt: '', - } ) ); - updateImages( [ ...images, ...selectedImages ] ); + const nextImages = [ ...images ]; + const selectedImages = selectedImageUrls + .filter( + ( url ) => ! nextImages.some( ( img ) => img?.url === url ) + ) + .map( ( url ) => ( { + url, + id: url, + alt: '', + } ) ); + + updateImages( [ ...nextImages, ...selectedImages ] ); }; const renderAddButton = () => { @@ -155,7 +161,7 @@ export default function ImagesSelector( { createNotice( 'error', __( - 'Something went wrong while generating texts. Please try again.', + 'Something went wrong while generating images. Please try again.', 'google-listings-and-ads' ) ); diff --git a/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js b/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js index ad7c578133..6b081830ee 100644 --- a/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js +++ b/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js @@ -128,409 +128,409 @@ test.describe( 'Add paid campaign', () => { await expect( dashboardPage.addPaidCampaignButton ).toBeEnabled(); } ); - test.describe( 'With Ads account not connected', async () => { - test.describe( 'Set up your accounts page', async () => { - test.beforeAll( async () => { - await setupAdsAccounts.mockAdsAccountsResponse( [] ); - await dashboardPage.addPaidCampaignButton.click(); - await page.waitForLoadState( LOAD_STATE.DOM_CONTENT_LOADED ); - } ); - - test( 'Page header should be "Set up your accounts"', async () => { - await expect( - page.getByRole( 'heading', { - name: 'Set up your accounts', - } ) - ).toBeVisible(); - await expect( - page.getByText( - 'Connect your Google account and your Google Ads account to set up a Performance Max campaign.' - ) - ).toBeVisible(); - } ); - - test( 'Google Account should show as connected', async () => { - await expect( - page.getByText( - 'This Google account is connected to your store’s product feed.' - ) - ).toBeVisible(); - } ); - - test( 'Continue Button should be disabled', async () => { - await expect( - setupAdsAccounts.getContinueButton() - ).toBeDisabled(); - } ); - } ); - - test.describe( 'Add campaigns with no Ads account', async () => { - test( 'Create an account should be visible', async () => { - const createAccountButton = page.getByRole( 'button', { - name: 'Create account', - } ); - - await expect( createAccountButton ).toBeVisible(); - - await expect( - setupAdsAccounts.getContinueButton() - ).toBeDisabled(); - - await expect( - page.getByText( - 'Required to set up conversion measurement and create campaigns.' - ) - ).toBeVisible(); - - await createAccountButton.click(); - } ); - - test( 'Create account button should be disable if the ToS have not been accepted.', async () => { - await expect( - page.getByRole( 'heading', { - name: 'Create Google Ads Account', - } ) - ).toBeVisible(); - - await expect( - page.getByText( - 'By creating a Google Ads account, you agree to the following terms and conditions:' - ) - ).toBeVisible(); - - await expect( - setupAdsAccounts.getCreateAdsAccountButtonModal() - ).toBeDisabled(); - } ); - - test( 'Accept terms and conditions to enable the create account button', async () => { - await setupAdsAccounts.getAcceptTermCreateAccount().check(); - - await expect( - setupAdsAccounts.getCreateAdsAccountButtonModal() - ).toBeEnabled(); - } ); - - test( 'Create an Ads account', async () => { - // Intercept Ads connection request. - const connectAdsAccountRequest = - setupAdsAccounts.registerConnectAdsAccountRequests(); - - await setupAdsAccounts.mockAdsAccountsResponse( ADS_ACCOUNTS ); - - // Mock request to fulfill Ads connection. - await setupAdsAccounts.fulfillAdsConnection( { - id: ADS_ACCOUNTS[ 0 ].id, - currency: 'USD', - symbol: '$', - status: 'incomplete', - step: 'account_access', - } ); - - await setupAdsAccounts.mockAdsStatusNotClaimed(); - - await setupAdsAccounts.getCreateAdsAccountButtonModal().click(); - - await connectAdsAccountRequest; - - const modal = setupAdsAccounts.getAcceptAccountModal(); - await expect( modal ).toBeVisible(); - } ); - - test( 'Show Unclaimed Ads account', async () => { - await setupAdsAccounts.clickCloseAcceptAccountButtonFromModal(); - - const claimButton = setupAdsAccounts.getAdsClaimAccountButton(); - const claimText = setupAdsAccounts.getAdsClaimAccountText(); - - await expect( claimButton ).toBeVisible(); - await expect( claimText ).toBeVisible(); - - await expect( - setupAdsAccounts.getContinueButton() - ).toBeDisabled(); - } ); - - test( 'Show Claimed Ads account', async () => { - // Intercept Ads connection request. - await setupAdsAccounts.fulfillAdsConnection( { - id: ADS_ACCOUNTS[ 0 ].id, - currency: 'USD', - symbol: '$', - status: 'connected', - step: '', - } ); - - await setupAdsAccounts.mockAdsStatusClaimed(); - - await page.dispatchEvent( 'body', 'blur' ); - await page.dispatchEvent( 'body', 'focus' ); - - await expect( - setupAdsAccounts.getContinueButton() - ).toBeEnabled(); - - await expect( - page.getByRole( 'link', { - name: `Account ${ ADS_ACCOUNTS[ 0 ].id }`, - } ) - ).toBeVisible(); - - await expect( - setupAdsAccounts.getContinueButton() - ).toBeEnabled(); - } ); - } ); - - test.describe( 'Add campaigns with existing Ads accounts', () => { - test.beforeAll( async () => { - await setupAdsAccounts.mockAdsAccountsResponse( ADS_ACCOUNTS ); - //Disconnect the account from the previous test - setupAdsAccounts.fulfillAdsConnection( { - id: ADS_ACCOUNTS[ 1 ].id, - currency: 'EUR', - symbol: '\u20ac', - status: 'disconnected', - } ); - - await page.reload(); - } ); - - test( 'Select one existing account', async () => { - const adsAccountSelected = `${ ADS_ACCOUNTS[ 1 ].id }`; - - await setupAdsAccounts.selectAnExistingAdsAccount( - adsAccountSelected - ); - - //Intercept Ads connection request - const connectAdsAccountRequest = - setupAdsAccounts.registerConnectAdsAccountRequests( - adsAccountSelected - ); - - //Mock request to fulfill Ads connection - setupAdsAccounts.fulfillAdsConnection( { - id: ADS_ACCOUNTS[ 1 ].id, - currency: 'EUR', - symbol: '\u20ac', - status: 'connected', - } ); - - await setupAdsAccounts.clickConnectAds(); - await connectAdsAccountRequest; - - await expect( - setupAdsAccounts.getContinueButton() - ).toBeEnabled(); - } ); - } ); - - test.describe( 'Create your campaign', () => { - test( 'Continue to create your campaign', async () => { - await setupAdsAccounts.clickContinue(); - await page.waitForLoadState( LOAD_STATE.DOM_CONTENT_LOADED ); - await expect( - page.getByRole( 'heading', { - name: 'Create your campaign', - } ) - ).toBeVisible(); - - await expect( - page.getByRole( 'heading', { name: 'Set your budget' } ) - ).toBeVisible(); - - await expect( - page.getByRole( 'link', { - name: 'See what your ads will look like.', - } ) - ).toBeVisible(); - } ); - - test.describe( 'Preview product ad', () => { - test( 'Preview product ad should be visible', async () => { - await expect( - page.getByText( 'Preview product ad' ) - ).toBeVisible(); - await expect( - page.getByText( - "Each of your product variants will have its own ad. Previews shown here are examples and don't include all possible formats." - ) - ).toBeVisible(); - } ); - - test( 'Change image buttons should be enabled', async () => { - const buttonsToChangeImage = page.locator( - '.gla-campaign-preview-card__moving-button' - ); - - expect( buttonsToChangeImage ).toHaveCount( 2 ); - - for ( const button of await buttonsToChangeImage.all() ) { - await expect( button ).toBeEnabled(); - } - } ); - } ); - - test.describe( 'FAQ panels', () => { - test( 'should see five questions in FAQ', async () => { - const faqTitles = getFAQPanelTitle( page ); - await expect( faqTitles ).toHaveCount( 5 ); - } ); - - test( 'should not see FAQ rows when FAQ titles are not clicked', async () => { - const faqRows = getFAQPanelRow( page ); - await expect( faqRows ).toHaveCount( 0 ); - } ); - - // eslint-disable-next-line jest/expect-expect - test( 'should see FAQ rows when all FAQ titles are clicked', async () => { - await checkFAQExpandable( page ); - } ); - } ); - } ); - - test.describe( 'Create Ads with billing data already setup', () => { - test.describe( 'Set the budget', async () => { - test( 'Continue button should be disabled if budget is 0', async () => { - await setupBudgetPage.fillBudget( '0' ); - - await expect( - setupBudgetPage.getCreateCampaignButton() - ).toBeDisabled(); - } ); - - test( 'Continue button should be enabled when selecting an option from the recommendations, even if the entered value is invalid', async () => { - await setupBudgetPage.fillBudget( '0' ); - await expect( - setupBudgetPage.getCreateCampaignButton() - ).toBeDisabled(); - - await page.getByLabel( 'low' ).click(); - await expect( - setupBudgetPage.getCreateCampaignButton() - ).toBeEnabled(); - - await page.getByLabel( 'custom' ).click(); - await expect( - setupBudgetPage.getCreateCampaignButton() - ).toBeDisabled(); - - await page.getByLabel( 'high' ).click(); - await expect( - setupBudgetPage.getCreateCampaignButton() - ).toBeEnabled(); - - await page.getByLabel( 'custom' ).click(); - await expect( - setupBudgetPage.getCreateCampaignButton() - ).toBeDisabled(); - - await page.getByLabel( 'recommended' ).click(); - await expect( - setupBudgetPage.getCreateCampaignButton() - ).toBeEnabled(); - } ); - - test( 'Continue button should be disabled if budget is less than 30% of the daily budget baseline', async () => { - await setupBudgetPage.fillBudget( '2' ); - - await expect( - setupBudgetPage.getCreateCampaignButton() - ).toBeDisabled(); - } ); - - test( 'User is notified of the minimum value', async () => { - await setupBudgetPage.fillBudget( '3' ); - await setupBudgetPage.getBudgetInput().blur(); - - await expect( - page.getByText( - 'Please make sure daily average cost is at least €4.00' - ) - ).toBeVisible(); - } ); - - test( 'Continue button should be enabled if budget is above the recommended value', async () => { - await setupBudgetPage.fillBudget( '5' ); - - await expect( - setupBudgetPage.getCreateCampaignButton() - ).toBeEnabled(); - } ); - - test( 'Display the recommended budget if the budget is valid but lower than the lowest recommended value', async () => { - await setupBudgetPage.fillBudget( '6' ); - - await expect( - page.getByText( - `Your budget is lower than other advertisers' budgets, which may affect performance. For best results, we recommend at least €15.00 per day.` - ) - ).toBeVisible(); - } ); - } ); - - test( 'It should show the campaign creation success message', async () => { - await setupBudgetPage.fillBudget( '6' ); - await setupBudgetPage.getCreateCampaignButton().click(); - - const cancelButton = page.getByRole( 'button', { - name: 'Cancel', - } ); - await expect( - page.getByText( 'This offer won’t last long!' ) - ).toBeVisible(); - await expect( cancelButton ).toBeEnabled(); - - await cancelButton.click(); - - await expect( cancelButton ).not.toBeVisible(); - - // Mock the campaign creation request. - const campaignCreation = - setupBudgetPage.mockCampaignCreationAndAdsSetupCompletion( - '6', - [ 'US' ] - ); - - await setupBudgetPage.getCreateCampaignButton().click(); - - await campaignCreation; - - //It should redirect to the dashboard page - await page.waitForURL( - '/wp-admin/admin.php?page=wc-admin&path=%2Fgoogle%2Fdashboard&guide=campaign-creation-success', - { - waitUntil: LOAD_STATE.DOM_CONTENT_LOADED, - } - ); - - await expect( - page.getByRole( 'heading', { - name: "You've set up a Performance Max Campaign!", - } ) - ).toBeVisible(); - - await expect( - page.getByRole( 'button', { - name: 'Create another campaign', - } ) - ).toBeEnabled(); - - await expect( - page.getByRole( 'button', { - name: 'Got It', - } ) - ).toBeEnabled(); - - await page - .getByRole( 'button', { - name: 'Got It', - } ) - .click(); - } ); - } ); - } ); + // test.describe( 'With Ads account not connected', async () => { + // test.describe( 'Set up your accounts page', async () => { + // test.beforeAll( async () => { + // await setupAdsAccounts.mockAdsAccountsResponse( [] ); + // await dashboardPage.addPaidCampaignButton.click(); + // await page.waitForLoadState( LOAD_STATE.DOM_CONTENT_LOADED ); + // } ); + + // test( 'Page header should be "Set up your accounts"', async () => { + // await expect( + // page.getByRole( 'heading', { + // name: 'Set up your accounts', + // } ) + // ).toBeVisible(); + // await expect( + // page.getByText( + // 'Connect your Google account and your Google Ads account to set up a Performance Max campaign.' + // ) + // ).toBeVisible(); + // } ); + + // test( 'Google Account should show as connected', async () => { + // await expect( + // page.getByText( + // 'This Google account is connected to your store’s product feed.' + // ) + // ).toBeVisible(); + // } ); + + // test( 'Continue Button should be disabled', async () => { + // await expect( + // setupAdsAccounts.getContinueButton() + // ).toBeDisabled(); + // } ); + // } ); + + // test.describe( 'Add campaigns with no Ads account', async () => { + // test( 'Create an account should be visible', async () => { + // const createAccountButton = page.getByRole( 'button', { + // name: 'Create account', + // } ); + + // await expect( createAccountButton ).toBeVisible(); + + // await expect( + // setupAdsAccounts.getContinueButton() + // ).toBeDisabled(); + + // await expect( + // page.getByText( + // 'Required to set up conversion measurement and create campaigns.' + // ) + // ).toBeVisible(); + + // await createAccountButton.click(); + // } ); + + // test( 'Create account button should be disable if the ToS have not been accepted.', async () => { + // await expect( + // page.getByRole( 'heading', { + // name: 'Create Google Ads Account', + // } ) + // ).toBeVisible(); + + // await expect( + // page.getByText( + // 'By creating a Google Ads account, you agree to the following terms and conditions:' + // ) + // ).toBeVisible(); + + // await expect( + // setupAdsAccounts.getCreateAdsAccountButtonModal() + // ).toBeDisabled(); + // } ); + + // test( 'Accept terms and conditions to enable the create account button', async () => { + // await setupAdsAccounts.getAcceptTermCreateAccount().check(); + + // await expect( + // setupAdsAccounts.getCreateAdsAccountButtonModal() + // ).toBeEnabled(); + // } ); + + // test( 'Create an Ads account', async () => { + // // Intercept Ads connection request. + // const connectAdsAccountRequest = + // setupAdsAccounts.registerConnectAdsAccountRequests(); + + // await setupAdsAccounts.mockAdsAccountsResponse( ADS_ACCOUNTS ); + + // // Mock request to fulfill Ads connection. + // await setupAdsAccounts.fulfillAdsConnection( { + // id: ADS_ACCOUNTS[ 0 ].id, + // currency: 'USD', + // symbol: '$', + // status: 'incomplete', + // step: 'account_access', + // } ); + + // await setupAdsAccounts.mockAdsStatusNotClaimed(); + + // await setupAdsAccounts.getCreateAdsAccountButtonModal().click(); + + // await connectAdsAccountRequest; + + // const modal = setupAdsAccounts.getAcceptAccountModal(); + // await expect( modal ).toBeVisible(); + // } ); + + // test( 'Show Unclaimed Ads account', async () => { + // await setupAdsAccounts.clickCloseAcceptAccountButtonFromModal(); + + // const claimButton = setupAdsAccounts.getAdsClaimAccountButton(); + // const claimText = setupAdsAccounts.getAdsClaimAccountText(); + + // await expect( claimButton ).toBeVisible(); + // await expect( claimText ).toBeVisible(); + + // await expect( + // setupAdsAccounts.getContinueButton() + // ).toBeDisabled(); + // } ); + + // test( 'Show Claimed Ads account', async () => { + // // Intercept Ads connection request. + // await setupAdsAccounts.fulfillAdsConnection( { + // id: ADS_ACCOUNTS[ 0 ].id, + // currency: 'USD', + // symbol: '$', + // status: 'connected', + // step: '', + // } ); + + // await setupAdsAccounts.mockAdsStatusClaimed(); + + // await page.dispatchEvent( 'body', 'blur' ); + // await page.dispatchEvent( 'body', 'focus' ); + + // await expect( + // setupAdsAccounts.getContinueButton() + // ).toBeEnabled(); + + // await expect( + // page.getByRole( 'link', { + // name: `Account ${ ADS_ACCOUNTS[ 0 ].id }`, + // } ) + // ).toBeVisible(); + + // await expect( + // setupAdsAccounts.getContinueButton() + // ).toBeEnabled(); + // } ); + // } ); + + // test.describe( 'Add campaigns with existing Ads accounts', () => { + // test.beforeAll( async () => { + // await setupAdsAccounts.mockAdsAccountsResponse( ADS_ACCOUNTS ); + // //Disconnect the account from the previous test + // setupAdsAccounts.fulfillAdsConnection( { + // id: ADS_ACCOUNTS[ 1 ].id, + // currency: 'EUR', + // symbol: '\u20ac', + // status: 'disconnected', + // } ); + + // await page.reload(); + // } ); + + // test( 'Select one existing account', async () => { + // const adsAccountSelected = `${ ADS_ACCOUNTS[ 1 ].id }`; + + // await setupAdsAccounts.selectAnExistingAdsAccount( + // adsAccountSelected + // ); + + // //Intercept Ads connection request + // const connectAdsAccountRequest = + // setupAdsAccounts.registerConnectAdsAccountRequests( + // adsAccountSelected + // ); + + // //Mock request to fulfill Ads connection + // setupAdsAccounts.fulfillAdsConnection( { + // id: ADS_ACCOUNTS[ 1 ].id, + // currency: 'EUR', + // symbol: '\u20ac', + // status: 'connected', + // } ); + + // await setupAdsAccounts.clickConnectAds(); + // await connectAdsAccountRequest; + + // await expect( + // setupAdsAccounts.getContinueButton() + // ).toBeEnabled(); + // } ); + // } ); + + // test.describe( 'Create your campaign', () => { + // test( 'Continue to create your campaign', async () => { + // await setupAdsAccounts.clickContinue(); + // await page.waitForLoadState( LOAD_STATE.DOM_CONTENT_LOADED ); + // await expect( + // page.getByRole( 'heading', { + // name: 'Create your campaign', + // } ) + // ).toBeVisible(); + + // await expect( + // page.getByRole( 'heading', { name: 'Set your budget' } ) + // ).toBeVisible(); + + // await expect( + // page.getByRole( 'link', { + // name: 'See what your ads will look like.', + // } ) + // ).toBeVisible(); + // } ); + + // test.describe( 'Preview product ad', () => { + // test( 'Preview product ad should be visible', async () => { + // await expect( + // page.getByText( 'Preview product ad' ) + // ).toBeVisible(); + // await expect( + // page.getByText( + // "Each of your product variants will have its own ad. Previews shown here are examples and don't include all possible formats." + // ) + // ).toBeVisible(); + // } ); + + // test( 'Change image buttons should be enabled', async () => { + // const buttonsToChangeImage = page.locator( + // '.gla-campaign-preview-card__moving-button' + // ); + + // expect( buttonsToChangeImage ).toHaveCount( 2 ); + + // for ( const button of await buttonsToChangeImage.all() ) { + // await expect( button ).toBeEnabled(); + // } + // } ); + // } ); + + // test.describe( 'FAQ panels', () => { + // test( 'should see five questions in FAQ', async () => { + // const faqTitles = getFAQPanelTitle( page ); + // await expect( faqTitles ).toHaveCount( 5 ); + // } ); + + // test( 'should not see FAQ rows when FAQ titles are not clicked', async () => { + // const faqRows = getFAQPanelRow( page ); + // await expect( faqRows ).toHaveCount( 0 ); + // } ); + + // // eslint-disable-next-line jest/expect-expect + // test( 'should see FAQ rows when all FAQ titles are clicked', async () => { + // await checkFAQExpandable( page ); + // } ); + // } ); + // } ); + + // test.describe( 'Create Ads with billing data already setup', () => { + // test.describe( 'Set the budget', async () => { + // test( 'Continue button should be disabled if budget is 0', async () => { + // await setupBudgetPage.fillBudget( '0' ); + + // await expect( + // setupBudgetPage.getCreateCampaignButton() + // ).toBeDisabled(); + // } ); + + // test( 'Continue button should be enabled when selecting an option from the recommendations, even if the entered value is invalid', async () => { + // await setupBudgetPage.fillBudget( '0' ); + // await expect( + // setupBudgetPage.getCreateCampaignButton() + // ).toBeDisabled(); + + // await page.getByLabel( 'low' ).click(); + // await expect( + // setupBudgetPage.getCreateCampaignButton() + // ).toBeEnabled(); + + // await page.getByLabel( 'custom' ).click(); + // await expect( + // setupBudgetPage.getCreateCampaignButton() + // ).toBeDisabled(); + + // await page.getByLabel( 'high' ).click(); + // await expect( + // setupBudgetPage.getCreateCampaignButton() + // ).toBeEnabled(); + + // await page.getByLabel( 'custom' ).click(); + // await expect( + // setupBudgetPage.getCreateCampaignButton() + // ).toBeDisabled(); + + // await page.getByLabel( 'recommended' ).click(); + // await expect( + // setupBudgetPage.getCreateCampaignButton() + // ).toBeEnabled(); + // } ); + + // test( 'Continue button should be disabled if budget is less than 30% of the daily budget baseline', async () => { + // await setupBudgetPage.fillBudget( '2' ); + + // await expect( + // setupBudgetPage.getCreateCampaignButton() + // ).toBeDisabled(); + // } ); + + // test( 'User is notified of the minimum value', async () => { + // await setupBudgetPage.fillBudget( '3' ); + // await setupBudgetPage.getBudgetInput().blur(); + + // await expect( + // page.getByText( + // 'Please make sure daily average cost is at least €4.00' + // ) + // ).toBeVisible(); + // } ); + + // test( 'Continue button should be enabled if budget is above the recommended value', async () => { + // await setupBudgetPage.fillBudget( '5' ); + + // await expect( + // setupBudgetPage.getCreateCampaignButton() + // ).toBeEnabled(); + // } ); + + // test( 'Display the recommended budget if the budget is valid but lower than the lowest recommended value', async () => { + // await setupBudgetPage.fillBudget( '6' ); + + // await expect( + // page.getByText( + // `Your budget is lower than other advertisers' budgets, which may affect performance. For best results, we recommend at least €15.00 per day.` + // ) + // ).toBeVisible(); + // } ); + // } ); + + // test( 'It should show the campaign creation success message', async () => { + // await setupBudgetPage.fillBudget( '6' ); + // await setupBudgetPage.getCreateCampaignButton().click(); + + // const cancelButton = page.getByRole( 'button', { + // name: 'Cancel', + // } ); + // await expect( + // page.getByText( 'This offer won’t last long!' ) + // ).toBeVisible(); + // await expect( cancelButton ).toBeEnabled(); + + // await cancelButton.click(); + + // await expect( cancelButton ).not.toBeVisible(); + + // // Mock the campaign creation request. + // const campaignCreation = + // setupBudgetPage.mockCampaignCreationAndAdsSetupCompletion( + // '6', + // [ 'US' ] + // ); + + // await setupBudgetPage.getCreateCampaignButton().click(); + + // await campaignCreation; + + // //It should redirect to the dashboard page + // await page.waitForURL( + // '/wp-admin/admin.php?page=wc-admin&path=%2Fgoogle%2Fdashboard&guide=campaign-creation-success', + // { + // waitUntil: LOAD_STATE.DOM_CONTENT_LOADED, + // } + // ); + + // await expect( + // page.getByRole( 'heading', { + // name: "You've set up a Performance Max Campaign!", + // } ) + // ).toBeVisible(); + + // await expect( + // page.getByRole( 'button', { + // name: 'Create another campaign', + // } ) + // ).toBeEnabled(); + + // await expect( + // page.getByRole( 'button', { + // name: 'Got It', + // } ) + // ).toBeEnabled(); + + // await page + // .getByRole( 'button', { + // name: 'Got It', + // } ) + // .click(); + // } ); + // } ); + // } ); test.describe( 'With connected Ads account', async () => { test.beforeAll( async () => { @@ -668,10 +668,6 @@ test.describe( 'Add paid campaign', () => { } ); test.describe( 'Success', () => { - test.beforeEach( async () => { - createCampaignPage.mockGenerateTextAssetsSuccess(); - } ); - test( 'Clicking generate headline fills empty headline inputs', async () => { const headlineInputsValues = await createCampaignPage.getHeadlineInputsValues(); @@ -748,10 +744,6 @@ test.describe( 'Add paid campaign', () => { } ); test.describe( 'Success', () => { - test.beforeEach( async () => { - createCampaignPage.mockGenerateTextAssetsSuccess(); - } ); - test( 'Clicking generate long headline fills empty long headline inputs', async () => { const longHeadlineInputsValues = await createCampaignPage.getLongHeadlineInputsValues(); @@ -827,10 +819,6 @@ test.describe( 'Add paid campaign', () => { } ); test.describe( 'Success', () => { - test.beforeEach( async () => { - createCampaignPage.mockGenerateTextAssetsSuccess(); - } ); - test( 'Clicking generate description fills empty description inputs', async () => { const descriptionInputsValues = await createCampaignPage.getDescriptionInputsValues(); @@ -866,6 +854,319 @@ test.describe( 'Add paid campaign', () => { } ); } ); } ); + + test.describe( 'Image Assets', () => { + test.describe( 'Landscape images', () => { + test.describe( 'Visibility', () => { + test( 'Generate landscape images button is visible', async () => { + const generateLandscapeImagesButton = + createCampaignPage.getGenerateLandscapeImagesButton(); + await expect( + generateLandscapeImagesButton + ).toBeVisible(); + } ); + + test( 'There is only one image loaded for the campaign', async () => { + const campaignImages = + createCampaignPage.getCampaignLandscapeImageItems(); + await expect( campaignImages ).toHaveCount( 1 ); + } ); + } ); + + test.describe( 'Generate action', () => { + test.beforeEach( async () => { + createCampaignPage.mockGenerateImageAssetsSuccess(); + } ); + + test( 'Clicking generate landscape images sends the correct POST request', async () => { + const generateRequest = + createCampaignPage.awaitForGenerateImageRequest( + 'https://woo.com/shop/', + [ 'marketing_image' ] + ); + + const generateLandscapeImagesButton = + createCampaignPage.getGenerateLandscapeImagesButton(); + await generateLandscapeImagesButton.click(); + await generateRequest; + } ); + } ); + + test.describe( 'Success', () => { + test( 'Image picker is displayed', async () => { + const imagePicker = + createCampaignPage.getLandscapeImagesSectionImagePicker(); + await expect( imagePicker ).toBeVisible(); + } ); + + test( 'Image picker renders generated images', async () => { + const generatedImages = + createCampaignPage.getLandscapeGeneratedImages(); + await expect( generatedImages ).toHaveCount( + 4 + ); + } ); + } ); + + test.describe( 'No generated assets', () => { + test.beforeEach( async () => { + createCampaignPage.mockEmptyGenerateImageAssets(); + } ); + + test( 'Hides the image picker if there are no generated images', async () => { + const generateLandscapeImagesButton = + createCampaignPage.getGenerateLandscapeImagesButton(); + await generateLandscapeImagesButton.click(); + + const imagePicker = + createCampaignPage.getLandscapeImagesSectionImagePicker(); + await expect( imagePicker ).not.toBeVisible(); + } ); + } ); + } ); + + test.describe( 'Image Picker', () => { + test.beforeEach( async () => { + createCampaignPage.mockGenerateImageAssetsSuccess(); + } ); + + test( '"Add selected images" button is disabled if no image is selected', async () => { + const generateLandscapeImagesButton = + createCampaignPage.getGenerateLandscapeImagesButton(); + await generateLandscapeImagesButton.click(); + + const addSelectedImagesButton = + createCampaignPage.getLandscapeImagePickerAddSelectedImagesButton(); + await expect( + addSelectedImagesButton + ).toBeDisabled(); + } ); + + test( 'Clicking an image enables the "Add selected images" button', async () => { + const generatedImages = + createCampaignPage.getLandscapeGeneratedImages(); + generatedImages.first().click(); + + const addSelectedImagesButton = + createCampaignPage.getLandscapeImagePickerAddSelectedImagesButton(); + await expect( + addSelectedImagesButton + ).toBeEnabled(); + } ); + + test( 'Clicking the "Add selected images" button adds the selected images to the campaign and remove them from the image picker ', async () => { + let generatedImages = + createCampaignPage.getLandscapeGeneratedImages(); + const firstGeneratedImageUrl = await generatedImages + .first() + .locator( 'img' ) + .getAttribute( 'src' ); + const addSelectedImagesButton = + createCampaignPage.getLandscapeImagePickerAddSelectedImagesButton(); + await addSelectedImagesButton.click(); + + generatedImages = + createCampaignPage.getLandscapeGeneratedImages(); + await expect( generatedImages ).toHaveCount( 3 ); + + const campaignImages = + createCampaignPage.getCampaignLandscapeImageItems(); + const campaignLastImageUrl = await campaignImages + .last() + .locator( 'img' ) + .getAttribute( 'src' ); + expect( campaignLastImageUrl ).toEqual( + firstGeneratedImageUrl + ); + await expect( campaignImages ).toHaveCount( 2 ); + } ); + + test( 'Adding all generated images hides the image picker', async () => { + const generatedImages = + createCampaignPage.getLandscapeGeneratedImages(); + await generatedImages.nth( 0 ).click(); + await generatedImages.nth( 1 ).click(); + await generatedImages.nth( 2 ).click(); + + const addSelectedImagesButton = + createCampaignPage.getLandscapeImagePickerAddSelectedImagesButton(); + await addSelectedImagesButton.click(); + + const imagePicker = + createCampaignPage.getLandscapeImagesSectionImagePicker(); + await expect( imagePicker ).not.toBeVisible(); + + const campaignImages = + createCampaignPage.getCampaignLandscapeImageItems(); + await expect( campaignImages ).toHaveCount( 5 ); + } ); + + test( 'Removing an image from the campaign shows it back in the image picker', async () => { + const campaignImageItems = + createCampaignPage.getCampaignLandscapeImageItems(); + const lastCampaignImageItem = + campaignImageItems.last(); + await lastCampaignImageItem.hover(); + const lastCampaignImageUrl = + await lastCampaignImageItem + .locator( 'img' ) + .getAttribute( 'src' ); + + const removeButton = lastCampaignImageItem.locator( + '.gla-media-selector__remove-medium-button' + ); + await removeButton.click(); + + const imagePicker = + createCampaignPage.getLandscapeImagesSectionImagePicker(); + await expect( imagePicker ).toBeVisible(); + + const generatedImages = + createCampaignPage.getLandscapeGeneratedImages(); + const firstGeneratedImageUrl = await generatedImages + .first() + .locator( 'img' ) + .getAttribute( 'src' ); + expect( firstGeneratedImageUrl ).toEqual( + lastCampaignImageUrl + ); + } ); + } ); + } ); + + test.describe( 'Square images', () => { + test.describe( 'Visibility', () => { + test( 'Generate square images button is visible', async () => { + const generateSquareImagesButton = + createCampaignPage.getGenerateSquareImagesButton(); + await expect( + generateSquareImagesButton + ).toBeVisible(); + } ); + + test( 'There is only one image loaded for the campaign', async () => { + const campaignImages = + createCampaignPage.getCampaignSquareImageItems(); + await expect( campaignImages ).toHaveCount( 1 ); + } ); + } ); + + test.describe( 'Generate action', () => { + test.beforeEach( async () => { + createCampaignPage.mockGenerateImageAssetsSuccess(); + } ); + + test( 'Clicking generate square images sends the correct POST request', async () => { + const generateRequest = + createCampaignPage.awaitForGenerateImageRequest( + 'https://woo.com/shop/', + [ 'square_marketing_image' ] + ); + + const generateSquareImagesButton = + createCampaignPage.getGenerateSquareImagesButton(); + await generateSquareImagesButton.click(); + await generateRequest; + } ); + } ); + + test.describe( 'Success', () => { + test( 'Image picker is displayed', async () => { + const imagePicker = + createCampaignPage.getSquareImagesSectionImagePicker(); + await expect( imagePicker ).toBeVisible(); + } ); + + test( 'Image picker renders generated images', async () => { + const generatedImages = + createCampaignPage.getSquareGeneratedImages(); + await expect( generatedImages ).toHaveCount( 3 ); + } ); + } ); + + test.describe( 'No generated assets', () => { + test.beforeEach( async () => { + createCampaignPage.mockEmptyGenerateImageAssets(); + } ); + + test( 'Hides the image picker if there are no generated images', async () => { + const generateSquareImagesButton = + createCampaignPage.getGenerateSquareImagesButton(); + await generateSquareImagesButton.click(); + + const imagePicker = + createCampaignPage.getSquareImagesSectionImagePicker(); + await expect( imagePicker ).not.toBeVisible(); + } ); + } ); + } ); + + test.describe( 'Portrait images', () => { + test.describe( 'Visibility', () => { + test( 'Generate portrait images button is visible', async () => { + const generatePortraitImagesButton = + createCampaignPage.getGeneratePortraitImagesButton(); + await expect( + generatePortraitImagesButton + ).toBeVisible(); + } ); + + test( 'There are no images loaded for the campaign', async () => { + const campaignImages = + createCampaignPage.getCampaignPortraitImageItems(); + await expect( campaignImages ).toHaveCount( 0 ); + } ); + } ); + + test.describe( 'Generate action', () => { + test.beforeEach( async () => { + createCampaignPage.mockGenerateImageAssetsSuccess(); + } ); + + test( 'Clicking generate portrait images sends the correct POST request', async () => { + const generateRequest = + createCampaignPage.awaitForGenerateImageRequest( + 'https://woo.com/shop/', + [ 'portrait_marketing_image' ] + ); + + const generatePortraitImagesButton = + createCampaignPage.getGeneratePortraitImagesButton(); + await generatePortraitImagesButton.click(); + await generateRequest; + } ); + } ); + + test.describe( 'Success', () => { + test( 'Image picker is displayed', async () => { + const imagePicker = + createCampaignPage.getPortraitImagesSectionImagePicker(); + await expect( imagePicker ).toBeVisible(); + } ); + + test( 'Image picker renders generated images', async () => { + const generatedImages = + createCampaignPage.getPortraitGeneratedImages(); + await expect( generatedImages ).toHaveCount( 2 ); + } ); + } ); + + test.describe( 'No generated assets', () => { + test.beforeEach( async () => { + createCampaignPage.mockEmptyGenerateImageAssets(); + } ); + + test( 'Hides the image picker if there are no generated images', async () => { + const generatePortraitImagesButton = + createCampaignPage.getGeneratePortraitImagesButton(); + await generatePortraitImagesButton.click(); + + const imagePicker = + createCampaignPage.getPortraitImagesSectionImagePicker(); + await expect( imagePicker ).not.toBeVisible(); + } ); + } ); + } ); } ); } ); } ); diff --git a/tests/e2e/utils/mock-requests.js b/tests/e2e/utils/mock-requests.js index 732ff5ec54..ba6b549884 100644 --- a/tests/e2e/utils/mock-requests.js +++ b/tests/e2e/utils/mock-requests.js @@ -1197,4 +1197,20 @@ export default class MockRequests { [ 'POST' ] ); } + + /** + * Fulfill generate image assets request. + * + * @param {Object} payload - The response payload to return. + * @param {number} status - The HTTP status in the response. + * @return {Promise} + */ + async fulfillGenerateImageAssetsRequest( payload, status = 200 ) { + await this.fulfillRequest( + /\/wc\/gla\/ads\/assets\/generate-images\b/, + payload, + status, + [ 'POST' ] + ); + } } diff --git a/tests/e2e/utils/pages/create-campaign.js b/tests/e2e/utils/pages/create-campaign.js index 3abd5fc1ac..1ce6773beb 100644 --- a/tests/e2e/utils/pages/create-campaign.js +++ b/tests/e2e/utils/pages/create-campaign.js @@ -340,6 +340,186 @@ export default class CreateCampaignPage extends MockRequests { return values; } + /** + * Get landscape images section. + * + * @return {import('@playwright/test').Locator} Get landscape images section. + */ + getLandscapeImagesSection() { + return this.page + .locator( + '.gla-asset-field:has(:where(.gla-asset-field__heading):has-text("Landscape images"))' + ) + .first(); + } + + /** + * Get generate landscape images button. + * + * @return {import('@playwright/test').Locator} Get generate landscape images button. + */ + getGenerateLandscapeImagesButton() { + return this.page.getByRole( 'button', { + name: 'Generate landscape images', + } ); + } + + /** + * Get landscape images section image picker. + * + * @return {import('@playwright/test').Locator} Get landscape images section image picker. + */ + getLandscapeImagesSectionImagePicker() { + const landscapeImagesSection = this.getLandscapeImagesSection(); + return landscapeImagesSection.locator( '.gla-gen-ai-image-picker' ); + } + + /** + * Get landscape generated images. + * + * @return {import('@playwright/test').Locator} Get landscape generated images. + */ + getLandscapeGeneratedImages() { + const landscapeImagesSection = this.getLandscapeImagesSection(); + return landscapeImagesSection.locator( + '.gla-gen-ai-image-picker__medium-button' + ); + } + + /** + * Get landscape image picker add selected images button. + * + * @return {import('@playwright/test').Locator} Get landscape image picker add selected images button. + */ + getLandscapeImagePickerAddSelectedImagesButton() { + const landscapeImagesSection = this.getLandscapeImagesSection(); + return landscapeImagesSection.getByRole( 'button', { + name: 'Add selected images', + } ); + } + + /** + * Get landscape campaign images. + * + * @return {import('@playwright/test').Locator} Get landscape campaign images. + */ + getCampaignLandscapeImageItems() { + const landscapeImagesSection = this.getLandscapeImagesSection(); + return landscapeImagesSection.locator( '.gla-media-selector__item' ); + } + + /** + * Get square images section. + * + * @return {import('@playwright/test').Locator} Get square images section. + */ + getSquareImagesSection() { + return this.page + .locator( + '.gla-asset-field:has(:where(.gla-asset-field__heading):has-text("Square images"))' + ) + .first(); + } + + /** + * Get square campaign images. + * + * @return {import('@playwright/test').Locator} Get square campaign images. + */ + getCampaignSquareImageItems() { + const squareImagesSection = this.getSquareImagesSection(); + return squareImagesSection.locator( '.gla-media-selector__item' ); + } + + /** + * Get square images section image picker. + * + * @return {import('@playwright/test').Locator} Get square images section image picker. + */ + getSquareImagesSectionImagePicker() { + const squareImagesSection = this.getSquareImagesSection(); + return squareImagesSection.locator( '.gla-gen-ai-image-picker' ); + } + + /** + * Get generate square images button. + * + * @return {import('@playwright/test').Locator} Get generate square images button. + */ + getGenerateSquareImagesButton() { + return this.page.getByRole( 'button', { + name: 'Generate square images', + } ); + } + + /** + * Get square generated images. + * + * @return {import('@playwright/test').Locator} Get square generated images. + */ + getSquareGeneratedImages() { + const squareImagesSection = this.getSquareImagesSection(); + return squareImagesSection.locator( + '.gla-gen-ai-image-picker__medium-button' + ); + } + + /** + * Get portrait section. + * + * @return {import('@playwright/test').Locator} Get portrait images section. + */ + getPortraitImagesSection() { + return this.page + .locator( + '.gla-asset-field:has(:where(.gla-asset-field__heading):has-text("Portrait images"))' + ) + .first(); + } + + /** + * Get generate portrait images button. + * + * @return {import('@playwright/test').Locator} Get generate portrait images button. + */ + getGeneratePortraitImagesButton() { + return this.page.getByRole( 'button', { + name: 'Generate portrait images', + } ); + } + + /** + * Get portrait campaign images. + * + * @return {import('@playwright/test').Locator} Get portrait campaign images. + */ + getCampaignPortraitImageItems() { + const portraitImagesSection = this.getPortraitImagesSection(); + return portraitImagesSection.locator( '.gla-media-selector__item' ); + } + + /** + * Get portrait images section image picker. + * + * @return {import('@playwright/test').Locator} Get portrait images section image picker. + */ + getPortraitImagesSectionImagePicker() { + const portraitImagesSection = this.getPortraitImagesSection(); + return portraitImagesSection.locator( '.gla-gen-ai-image-picker' ); + } + + /** + * Get portrait generated images. + * + * @return {import('@playwright/test').Locator} Get portrait generated images. + */ + getPortraitGeneratedImages() { + const portraitImagesSection = this.getPortraitImagesSection(); + return portraitImagesSection.locator( + '.gla-gen-ai-image-picker__medium-button' + ); + } + /** * Select URL option. * @@ -523,4 +703,100 @@ export default class CreateCampaignPage extends MockRequests { items: [], } ); } + + /** + * Mock generate media assets success response. + * + * @return {Promise} + */ + async mockGenerateImageAssetsSuccess() { + await this.fulfillGenerateImageAssetsRequest( { + final_url: 'https://woo.com/shop/', + items: [ + { + temporary_image_url: + 'https://placehold.co/400x225?text=Marketing+Image+1', + type: 'marketing_image', + }, + { + temporary_image_url: + 'https://placehold.co/400x225?text=Marketing+Image+2', + type: 'marketing_image', + }, + { + temporary_image_url: + 'https://placehold.co/400x225?text=Marketing+Image+3', + type: 'marketing_image', + }, + { + temporary_image_url: + 'https://placehold.co/400x225?text=Marketing+Image+4', + type: 'marketing_image', + }, + { + temporary_image_url: + 'https://placehold.co/200x200?text=Square+Marketing+Image+1', + type: 'square_marketing_image', + }, + { + temporary_image_url: + 'https://placehold.co/200x200?text=Square+Marketing+Image+2', + type: 'square_marketing_image', + }, + { + temporary_image_url: + 'https://placehold.co/200x200?text=Square+Marketing+Image+3', + type: 'square_marketing_image', + }, + { + temporary_image_url: + 'https://placehold.co/200x300?text=Portrait+Marketing+Image+1', + type: 'portrait_marketing_image', + }, + { + temporary_image_url: + 'https://placehold.co/200x300?text=Portrait+Marketing+Image+2', + type: 'portrait_marketing_image', + }, + ], + } ); + } + + /** + * Mock generate image assets empty response. + * + * @return {Promise} + */ + async mockEmptyGenerateImageAssets() { + await this.fulfillGenerateImageAssetsRequest( { + final_url: 'https://woo.com/shop/', + items: [], + } ); + } + + /** + * Await for the generate image assets request. + * + * @param {string} finalUrl The final URL. + * @param {Array} types The requested asset types. + * @return {Promise} The request. + */ + async awaitForGenerateImageRequest( finalUrl, types ) { + return this.page.waitForRequest( ( request ) => { + if ( + ! request.url().includes( '/gla/ads/assets/generate-images' ) || + request.method() !== 'POST' + ) { + return false; + } + + const payload = request.postDataJSON(); + + return ( + payload.final_url === finalUrl && + Array.isArray( payload.types ) && + types.every( ( type ) => payload.types.includes( type ) ) + ); + } ); + } } From 5046353f2ac569856c5f7e194be79627b5fb8db2 Mon Sep 17 00:00:00 2001 From: asvinb Date: Fri, 16 Jan 2026 17:02:28 +0400 Subject: [PATCH 035/123] Revert comments --- .../add-paid-campaigns.test.js | 806 +++++++++--------- 1 file changed, 403 insertions(+), 403 deletions(-) diff --git a/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js b/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js index 6b081830ee..b376eba596 100644 --- a/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js +++ b/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js @@ -128,409 +128,409 @@ test.describe( 'Add paid campaign', () => { await expect( dashboardPage.addPaidCampaignButton ).toBeEnabled(); } ); - // test.describe( 'With Ads account not connected', async () => { - // test.describe( 'Set up your accounts page', async () => { - // test.beforeAll( async () => { - // await setupAdsAccounts.mockAdsAccountsResponse( [] ); - // await dashboardPage.addPaidCampaignButton.click(); - // await page.waitForLoadState( LOAD_STATE.DOM_CONTENT_LOADED ); - // } ); - - // test( 'Page header should be "Set up your accounts"', async () => { - // await expect( - // page.getByRole( 'heading', { - // name: 'Set up your accounts', - // } ) - // ).toBeVisible(); - // await expect( - // page.getByText( - // 'Connect your Google account and your Google Ads account to set up a Performance Max campaign.' - // ) - // ).toBeVisible(); - // } ); - - // test( 'Google Account should show as connected', async () => { - // await expect( - // page.getByText( - // 'This Google account is connected to your store’s product feed.' - // ) - // ).toBeVisible(); - // } ); - - // test( 'Continue Button should be disabled', async () => { - // await expect( - // setupAdsAccounts.getContinueButton() - // ).toBeDisabled(); - // } ); - // } ); - - // test.describe( 'Add campaigns with no Ads account', async () => { - // test( 'Create an account should be visible', async () => { - // const createAccountButton = page.getByRole( 'button', { - // name: 'Create account', - // } ); - - // await expect( createAccountButton ).toBeVisible(); - - // await expect( - // setupAdsAccounts.getContinueButton() - // ).toBeDisabled(); - - // await expect( - // page.getByText( - // 'Required to set up conversion measurement and create campaigns.' - // ) - // ).toBeVisible(); - - // await createAccountButton.click(); - // } ); - - // test( 'Create account button should be disable if the ToS have not been accepted.', async () => { - // await expect( - // page.getByRole( 'heading', { - // name: 'Create Google Ads Account', - // } ) - // ).toBeVisible(); - - // await expect( - // page.getByText( - // 'By creating a Google Ads account, you agree to the following terms and conditions:' - // ) - // ).toBeVisible(); - - // await expect( - // setupAdsAccounts.getCreateAdsAccountButtonModal() - // ).toBeDisabled(); - // } ); - - // test( 'Accept terms and conditions to enable the create account button', async () => { - // await setupAdsAccounts.getAcceptTermCreateAccount().check(); - - // await expect( - // setupAdsAccounts.getCreateAdsAccountButtonModal() - // ).toBeEnabled(); - // } ); - - // test( 'Create an Ads account', async () => { - // // Intercept Ads connection request. - // const connectAdsAccountRequest = - // setupAdsAccounts.registerConnectAdsAccountRequests(); - - // await setupAdsAccounts.mockAdsAccountsResponse( ADS_ACCOUNTS ); - - // // Mock request to fulfill Ads connection. - // await setupAdsAccounts.fulfillAdsConnection( { - // id: ADS_ACCOUNTS[ 0 ].id, - // currency: 'USD', - // symbol: '$', - // status: 'incomplete', - // step: 'account_access', - // } ); - - // await setupAdsAccounts.mockAdsStatusNotClaimed(); - - // await setupAdsAccounts.getCreateAdsAccountButtonModal().click(); - - // await connectAdsAccountRequest; - - // const modal = setupAdsAccounts.getAcceptAccountModal(); - // await expect( modal ).toBeVisible(); - // } ); - - // test( 'Show Unclaimed Ads account', async () => { - // await setupAdsAccounts.clickCloseAcceptAccountButtonFromModal(); - - // const claimButton = setupAdsAccounts.getAdsClaimAccountButton(); - // const claimText = setupAdsAccounts.getAdsClaimAccountText(); - - // await expect( claimButton ).toBeVisible(); - // await expect( claimText ).toBeVisible(); - - // await expect( - // setupAdsAccounts.getContinueButton() - // ).toBeDisabled(); - // } ); - - // test( 'Show Claimed Ads account', async () => { - // // Intercept Ads connection request. - // await setupAdsAccounts.fulfillAdsConnection( { - // id: ADS_ACCOUNTS[ 0 ].id, - // currency: 'USD', - // symbol: '$', - // status: 'connected', - // step: '', - // } ); - - // await setupAdsAccounts.mockAdsStatusClaimed(); - - // await page.dispatchEvent( 'body', 'blur' ); - // await page.dispatchEvent( 'body', 'focus' ); - - // await expect( - // setupAdsAccounts.getContinueButton() - // ).toBeEnabled(); - - // await expect( - // page.getByRole( 'link', { - // name: `Account ${ ADS_ACCOUNTS[ 0 ].id }`, - // } ) - // ).toBeVisible(); - - // await expect( - // setupAdsAccounts.getContinueButton() - // ).toBeEnabled(); - // } ); - // } ); - - // test.describe( 'Add campaigns with existing Ads accounts', () => { - // test.beforeAll( async () => { - // await setupAdsAccounts.mockAdsAccountsResponse( ADS_ACCOUNTS ); - // //Disconnect the account from the previous test - // setupAdsAccounts.fulfillAdsConnection( { - // id: ADS_ACCOUNTS[ 1 ].id, - // currency: 'EUR', - // symbol: '\u20ac', - // status: 'disconnected', - // } ); - - // await page.reload(); - // } ); - - // test( 'Select one existing account', async () => { - // const adsAccountSelected = `${ ADS_ACCOUNTS[ 1 ].id }`; - - // await setupAdsAccounts.selectAnExistingAdsAccount( - // adsAccountSelected - // ); - - // //Intercept Ads connection request - // const connectAdsAccountRequest = - // setupAdsAccounts.registerConnectAdsAccountRequests( - // adsAccountSelected - // ); - - // //Mock request to fulfill Ads connection - // setupAdsAccounts.fulfillAdsConnection( { - // id: ADS_ACCOUNTS[ 1 ].id, - // currency: 'EUR', - // symbol: '\u20ac', - // status: 'connected', - // } ); - - // await setupAdsAccounts.clickConnectAds(); - // await connectAdsAccountRequest; - - // await expect( - // setupAdsAccounts.getContinueButton() - // ).toBeEnabled(); - // } ); - // } ); - - // test.describe( 'Create your campaign', () => { - // test( 'Continue to create your campaign', async () => { - // await setupAdsAccounts.clickContinue(); - // await page.waitForLoadState( LOAD_STATE.DOM_CONTENT_LOADED ); - // await expect( - // page.getByRole( 'heading', { - // name: 'Create your campaign', - // } ) - // ).toBeVisible(); - - // await expect( - // page.getByRole( 'heading', { name: 'Set your budget' } ) - // ).toBeVisible(); - - // await expect( - // page.getByRole( 'link', { - // name: 'See what your ads will look like.', - // } ) - // ).toBeVisible(); - // } ); - - // test.describe( 'Preview product ad', () => { - // test( 'Preview product ad should be visible', async () => { - // await expect( - // page.getByText( 'Preview product ad' ) - // ).toBeVisible(); - // await expect( - // page.getByText( - // "Each of your product variants will have its own ad. Previews shown here are examples and don't include all possible formats." - // ) - // ).toBeVisible(); - // } ); - - // test( 'Change image buttons should be enabled', async () => { - // const buttonsToChangeImage = page.locator( - // '.gla-campaign-preview-card__moving-button' - // ); - - // expect( buttonsToChangeImage ).toHaveCount( 2 ); - - // for ( const button of await buttonsToChangeImage.all() ) { - // await expect( button ).toBeEnabled(); - // } - // } ); - // } ); - - // test.describe( 'FAQ panels', () => { - // test( 'should see five questions in FAQ', async () => { - // const faqTitles = getFAQPanelTitle( page ); - // await expect( faqTitles ).toHaveCount( 5 ); - // } ); - - // test( 'should not see FAQ rows when FAQ titles are not clicked', async () => { - // const faqRows = getFAQPanelRow( page ); - // await expect( faqRows ).toHaveCount( 0 ); - // } ); - - // // eslint-disable-next-line jest/expect-expect - // test( 'should see FAQ rows when all FAQ titles are clicked', async () => { - // await checkFAQExpandable( page ); - // } ); - // } ); - // } ); - - // test.describe( 'Create Ads with billing data already setup', () => { - // test.describe( 'Set the budget', async () => { - // test( 'Continue button should be disabled if budget is 0', async () => { - // await setupBudgetPage.fillBudget( '0' ); - - // await expect( - // setupBudgetPage.getCreateCampaignButton() - // ).toBeDisabled(); - // } ); - - // test( 'Continue button should be enabled when selecting an option from the recommendations, even if the entered value is invalid', async () => { - // await setupBudgetPage.fillBudget( '0' ); - // await expect( - // setupBudgetPage.getCreateCampaignButton() - // ).toBeDisabled(); - - // await page.getByLabel( 'low' ).click(); - // await expect( - // setupBudgetPage.getCreateCampaignButton() - // ).toBeEnabled(); - - // await page.getByLabel( 'custom' ).click(); - // await expect( - // setupBudgetPage.getCreateCampaignButton() - // ).toBeDisabled(); - - // await page.getByLabel( 'high' ).click(); - // await expect( - // setupBudgetPage.getCreateCampaignButton() - // ).toBeEnabled(); - - // await page.getByLabel( 'custom' ).click(); - // await expect( - // setupBudgetPage.getCreateCampaignButton() - // ).toBeDisabled(); - - // await page.getByLabel( 'recommended' ).click(); - // await expect( - // setupBudgetPage.getCreateCampaignButton() - // ).toBeEnabled(); - // } ); - - // test( 'Continue button should be disabled if budget is less than 30% of the daily budget baseline', async () => { - // await setupBudgetPage.fillBudget( '2' ); - - // await expect( - // setupBudgetPage.getCreateCampaignButton() - // ).toBeDisabled(); - // } ); - - // test( 'User is notified of the minimum value', async () => { - // await setupBudgetPage.fillBudget( '3' ); - // await setupBudgetPage.getBudgetInput().blur(); - - // await expect( - // page.getByText( - // 'Please make sure daily average cost is at least €4.00' - // ) - // ).toBeVisible(); - // } ); - - // test( 'Continue button should be enabled if budget is above the recommended value', async () => { - // await setupBudgetPage.fillBudget( '5' ); - - // await expect( - // setupBudgetPage.getCreateCampaignButton() - // ).toBeEnabled(); - // } ); - - // test( 'Display the recommended budget if the budget is valid but lower than the lowest recommended value', async () => { - // await setupBudgetPage.fillBudget( '6' ); - - // await expect( - // page.getByText( - // `Your budget is lower than other advertisers' budgets, which may affect performance. For best results, we recommend at least €15.00 per day.` - // ) - // ).toBeVisible(); - // } ); - // } ); - - // test( 'It should show the campaign creation success message', async () => { - // await setupBudgetPage.fillBudget( '6' ); - // await setupBudgetPage.getCreateCampaignButton().click(); - - // const cancelButton = page.getByRole( 'button', { - // name: 'Cancel', - // } ); - // await expect( - // page.getByText( 'This offer won’t last long!' ) - // ).toBeVisible(); - // await expect( cancelButton ).toBeEnabled(); - - // await cancelButton.click(); - - // await expect( cancelButton ).not.toBeVisible(); - - // // Mock the campaign creation request. - // const campaignCreation = - // setupBudgetPage.mockCampaignCreationAndAdsSetupCompletion( - // '6', - // [ 'US' ] - // ); - - // await setupBudgetPage.getCreateCampaignButton().click(); - - // await campaignCreation; - - // //It should redirect to the dashboard page - // await page.waitForURL( - // '/wp-admin/admin.php?page=wc-admin&path=%2Fgoogle%2Fdashboard&guide=campaign-creation-success', - // { - // waitUntil: LOAD_STATE.DOM_CONTENT_LOADED, - // } - // ); - - // await expect( - // page.getByRole( 'heading', { - // name: "You've set up a Performance Max Campaign!", - // } ) - // ).toBeVisible(); - - // await expect( - // page.getByRole( 'button', { - // name: 'Create another campaign', - // } ) - // ).toBeEnabled(); - - // await expect( - // page.getByRole( 'button', { - // name: 'Got It', - // } ) - // ).toBeEnabled(); - - // await page - // .getByRole( 'button', { - // name: 'Got It', - // } ) - // .click(); - // } ); - // } ); - // } ); + test.describe( 'With Ads account not connected', async () => { + test.describe( 'Set up your accounts page', async () => { + test.beforeAll( async () => { + await setupAdsAccounts.mockAdsAccountsResponse( [] ); + await dashboardPage.addPaidCampaignButton.click(); + await page.waitForLoadState( LOAD_STATE.DOM_CONTENT_LOADED ); + } ); + + test( 'Page header should be "Set up your accounts"', async () => { + await expect( + page.getByRole( 'heading', { + name: 'Set up your accounts', + } ) + ).toBeVisible(); + await expect( + page.getByText( + 'Connect your Google account and your Google Ads account to set up a Performance Max campaign.' + ) + ).toBeVisible(); + } ); + + test( 'Google Account should show as connected', async () => { + await expect( + page.getByText( + 'This Google account is connected to your store’s product feed.' + ) + ).toBeVisible(); + } ); + + test( 'Continue Button should be disabled', async () => { + await expect( + setupAdsAccounts.getContinueButton() + ).toBeDisabled(); + } ); + } ); + + test.describe( 'Add campaigns with no Ads account', async () => { + test( 'Create an account should be visible', async () => { + const createAccountButton = page.getByRole( 'button', { + name: 'Create account', + } ); + + await expect( createAccountButton ).toBeVisible(); + + await expect( + setupAdsAccounts.getContinueButton() + ).toBeDisabled(); + + await expect( + page.getByText( + 'Required to set up conversion measurement and create campaigns.' + ) + ).toBeVisible(); + + await createAccountButton.click(); + } ); + + test( 'Create account button should be disable if the ToS have not been accepted.', async () => { + await expect( + page.getByRole( 'heading', { + name: 'Create Google Ads Account', + } ) + ).toBeVisible(); + + await expect( + page.getByText( + 'By creating a Google Ads account, you agree to the following terms and conditions:' + ) + ).toBeVisible(); + + await expect( + setupAdsAccounts.getCreateAdsAccountButtonModal() + ).toBeDisabled(); + } ); + + test( 'Accept terms and conditions to enable the create account button', async () => { + await setupAdsAccounts.getAcceptTermCreateAccount().check(); + + await expect( + setupAdsAccounts.getCreateAdsAccountButtonModal() + ).toBeEnabled(); + } ); + + test( 'Create an Ads account', async () => { + // Intercept Ads connection request. + const connectAdsAccountRequest = + setupAdsAccounts.registerConnectAdsAccountRequests(); + + await setupAdsAccounts.mockAdsAccountsResponse( ADS_ACCOUNTS ); + + // Mock request to fulfill Ads connection. + await setupAdsAccounts.fulfillAdsConnection( { + id: ADS_ACCOUNTS[ 0 ].id, + currency: 'USD', + symbol: '$', + status: 'incomplete', + step: 'account_access', + } ); + + await setupAdsAccounts.mockAdsStatusNotClaimed(); + + await setupAdsAccounts.getCreateAdsAccountButtonModal().click(); + + await connectAdsAccountRequest; + + const modal = setupAdsAccounts.getAcceptAccountModal(); + await expect( modal ).toBeVisible(); + } ); + + test( 'Show Unclaimed Ads account', async () => { + await setupAdsAccounts.clickCloseAcceptAccountButtonFromModal(); + + const claimButton = setupAdsAccounts.getAdsClaimAccountButton(); + const claimText = setupAdsAccounts.getAdsClaimAccountText(); + + await expect( claimButton ).toBeVisible(); + await expect( claimText ).toBeVisible(); + + await expect( + setupAdsAccounts.getContinueButton() + ).toBeDisabled(); + } ); + + test( 'Show Claimed Ads account', async () => { + // Intercept Ads connection request. + await setupAdsAccounts.fulfillAdsConnection( { + id: ADS_ACCOUNTS[ 0 ].id, + currency: 'USD', + symbol: '$', + status: 'connected', + step: '', + } ); + + await setupAdsAccounts.mockAdsStatusClaimed(); + + await page.dispatchEvent( 'body', 'blur' ); + await page.dispatchEvent( 'body', 'focus' ); + + await expect( + setupAdsAccounts.getContinueButton() + ).toBeEnabled(); + + await expect( + page.getByRole( 'link', { + name: `Account ${ ADS_ACCOUNTS[ 0 ].id }`, + } ) + ).toBeVisible(); + + await expect( + setupAdsAccounts.getContinueButton() + ).toBeEnabled(); + } ); + } ); + + test.describe( 'Add campaigns with existing Ads accounts', () => { + test.beforeAll( async () => { + await setupAdsAccounts.mockAdsAccountsResponse( ADS_ACCOUNTS ); + //Disconnect the account from the previous test + setupAdsAccounts.fulfillAdsConnection( { + id: ADS_ACCOUNTS[ 1 ].id, + currency: 'EUR', + symbol: '\u20ac', + status: 'disconnected', + } ); + + await page.reload(); + } ); + + test( 'Select one existing account', async () => { + const adsAccountSelected = `${ ADS_ACCOUNTS[ 1 ].id }`; + + await setupAdsAccounts.selectAnExistingAdsAccount( + adsAccountSelected + ); + + //Intercept Ads connection request + const connectAdsAccountRequest = + setupAdsAccounts.registerConnectAdsAccountRequests( + adsAccountSelected + ); + + //Mock request to fulfill Ads connection + setupAdsAccounts.fulfillAdsConnection( { + id: ADS_ACCOUNTS[ 1 ].id, + currency: 'EUR', + symbol: '\u20ac', + status: 'connected', + } ); + + await setupAdsAccounts.clickConnectAds(); + await connectAdsAccountRequest; + + await expect( + setupAdsAccounts.getContinueButton() + ).toBeEnabled(); + } ); + } ); + + test.describe( 'Create your campaign', () => { + test( 'Continue to create your campaign', async () => { + await setupAdsAccounts.clickContinue(); + await page.waitForLoadState( LOAD_STATE.DOM_CONTENT_LOADED ); + await expect( + page.getByRole( 'heading', { + name: 'Create your campaign', + } ) + ).toBeVisible(); + + await expect( + page.getByRole( 'heading', { name: 'Set your budget' } ) + ).toBeVisible(); + + await expect( + page.getByRole( 'link', { + name: 'See what your ads will look like.', + } ) + ).toBeVisible(); + } ); + + test.describe( 'Preview product ad', () => { + test( 'Preview product ad should be visible', async () => { + await expect( + page.getByText( 'Preview product ad' ) + ).toBeVisible(); + await expect( + page.getByText( + "Each of your product variants will have its own ad. Previews shown here are examples and don't include all possible formats." + ) + ).toBeVisible(); + } ); + + test( 'Change image buttons should be enabled', async () => { + const buttonsToChangeImage = page.locator( + '.gla-campaign-preview-card__moving-button' + ); + + expect( buttonsToChangeImage ).toHaveCount( 2 ); + + for ( const button of await buttonsToChangeImage.all() ) { + await expect( button ).toBeEnabled(); + } + } ); + } ); + + test.describe( 'FAQ panels', () => { + test( 'should see five questions in FAQ', async () => { + const faqTitles = getFAQPanelTitle( page ); + await expect( faqTitles ).toHaveCount( 5 ); + } ); + + test( 'should not see FAQ rows when FAQ titles are not clicked', async () => { + const faqRows = getFAQPanelRow( page ); + await expect( faqRows ).toHaveCount( 0 ); + } ); + + // eslint-disable-next-line jest/expect-expect + test( 'should see FAQ rows when all FAQ titles are clicked', async () => { + await checkFAQExpandable( page ); + } ); + } ); + } ); + + test.describe( 'Create Ads with billing data already setup', () => { + test.describe( 'Set the budget', async () => { + test( 'Continue button should be disabled if budget is 0', async () => { + await setupBudgetPage.fillBudget( '0' ); + + await expect( + setupBudgetPage.getCreateCampaignButton() + ).toBeDisabled(); + } ); + + test( 'Continue button should be enabled when selecting an option from the recommendations, even if the entered value is invalid', async () => { + await setupBudgetPage.fillBudget( '0' ); + await expect( + setupBudgetPage.getCreateCampaignButton() + ).toBeDisabled(); + + await page.getByLabel( 'low' ).click(); + await expect( + setupBudgetPage.getCreateCampaignButton() + ).toBeEnabled(); + + await page.getByLabel( 'custom' ).click(); + await expect( + setupBudgetPage.getCreateCampaignButton() + ).toBeDisabled(); + + await page.getByLabel( 'high' ).click(); + await expect( + setupBudgetPage.getCreateCampaignButton() + ).toBeEnabled(); + + await page.getByLabel( 'custom' ).click(); + await expect( + setupBudgetPage.getCreateCampaignButton() + ).toBeDisabled(); + + await page.getByLabel( 'recommended' ).click(); + await expect( + setupBudgetPage.getCreateCampaignButton() + ).toBeEnabled(); + } ); + + test( 'Continue button should be disabled if budget is less than 30% of the daily budget baseline', async () => { + await setupBudgetPage.fillBudget( '2' ); + + await expect( + setupBudgetPage.getCreateCampaignButton() + ).toBeDisabled(); + } ); + + test( 'User is notified of the minimum value', async () => { + await setupBudgetPage.fillBudget( '3' ); + await setupBudgetPage.getBudgetInput().blur(); + + await expect( + page.getByText( + 'Please make sure daily average cost is at least €4.00' + ) + ).toBeVisible(); + } ); + + test( 'Continue button should be enabled if budget is above the recommended value', async () => { + await setupBudgetPage.fillBudget( '5' ); + + await expect( + setupBudgetPage.getCreateCampaignButton() + ).toBeEnabled(); + } ); + + test( 'Display the recommended budget if the budget is valid but lower than the lowest recommended value', async () => { + await setupBudgetPage.fillBudget( '6' ); + + await expect( + page.getByText( + `Your budget is lower than other advertisers' budgets, which may affect performance. For best results, we recommend at least €15.00 per day.` + ) + ).toBeVisible(); + } ); + } ); + + test( 'It should show the campaign creation success message', async () => { + await setupBudgetPage.fillBudget( '6' ); + await setupBudgetPage.getCreateCampaignButton().click(); + + const cancelButton = page.getByRole( 'button', { + name: 'Cancel', + } ); + await expect( + page.getByText( 'This offer won’t last long!' ) + ).toBeVisible(); + await expect( cancelButton ).toBeEnabled(); + + await cancelButton.click(); + + await expect( cancelButton ).not.toBeVisible(); + + // Mock the campaign creation request. + const campaignCreation = + setupBudgetPage.mockCampaignCreationAndAdsSetupCompletion( + '6', + [ 'US' ] + ); + + await setupBudgetPage.getCreateCampaignButton().click(); + + await campaignCreation; + + //It should redirect to the dashboard page + await page.waitForURL( + '/wp-admin/admin.php?page=wc-admin&path=%2Fgoogle%2Fdashboard&guide=campaign-creation-success', + { + waitUntil: LOAD_STATE.DOM_CONTENT_LOADED, + } + ); + + await expect( + page.getByRole( 'heading', { + name: "You've set up a Performance Max Campaign!", + } ) + ).toBeVisible(); + + await expect( + page.getByRole( 'button', { + name: 'Create another campaign', + } ) + ).toBeEnabled(); + + await expect( + page.getByRole( 'button', { + name: 'Got It', + } ) + ).toBeEnabled(); + + await page + .getByRole( 'button', { + name: 'Got It', + } ) + .click(); + } ); + } ); + } ); test.describe( 'With connected Ads account', async () => { test.beforeAll( async () => { From 66ec26acff7ac3f37796a2da01f6c4cde830f5ae Mon Sep 17 00:00:00 2001 From: asvinb Date: Fri, 16 Jan 2026 17:11:57 +0400 Subject: [PATCH 036/123] chore(package.json): Update max size limit for index.js --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9e8489d7f1..b50db87d53 100644 --- a/package.json +++ b/package.json @@ -148,7 +148,7 @@ }, { "path": "./js/build/index.js", - "maxSize": "19 kB" + "maxSize": "19.1 kB" }, { "path": "./js/build/commons.js", From 078fca753c42219b79bda914cf6f81862bd40269 Mon Sep 17 00:00:00 2001 From: asvinb Date: Fri, 16 Jan 2026 18:57:36 +0400 Subject: [PATCH 037/123] Refactor GenAICard component layout and styling --- js/src/components/paid-ads/gen-ai-card.js | 46 ++++++++++----------- js/src/components/paid-ads/gen-ai-card.scss | 26 +++++------- 2 files changed, 33 insertions(+), 39 deletions(-) diff --git a/js/src/components/paid-ads/gen-ai-card.js b/js/src/components/paid-ads/gen-ai-card.js index 2a5e7ae1f0..baec6d18fd 100644 --- a/js/src/components/paid-ads/gen-ai-card.js +++ b/js/src/components/paid-ads/gen-ai-card.js @@ -15,7 +15,6 @@ import { * Internal dependencies */ import Section from '~/components/section'; -import useGoogleAdsAccount from '~/hooks/useGoogleAdsAccount'; import genAIImageURL from '~/images/pmax-assets-improvements/gen-ai.svg'; import './gen-ai-card.scss'; @@ -28,23 +27,13 @@ import './gen-ai-card.scss'; * @return {JSX.Element} The rendered GenAICard component. */ const GenAICard = () => { - const { googleAdsAccount } = useGoogleAdsAccount(); - const queryArgs = {}; - - if ( googleAdsAccount?.ocid ) { - queryArgs.ocid = googleAdsAccount.ocid; - } else if ( googleAdsAccount?.id ) { - queryArgs.ecid = googleAdsAccount.id; - } - return ( @@ -66,27 +55,36 @@ const GenAICard = () => {
- -

- { __( - 'Text assets were auto-populated with Google AI', - 'google-listings-and-ads' - ) } -

+ + + + + + +

+ { __( + 'Text assets were auto-populated with Google AI', + 'google-listings-and-ads' + ) } +

+
+
- + { diff --git a/js/src/components/paid-ads/gen-ai-card.scss b/js/src/components/paid-ads/gen-ai-card.scss index d653912588..b9f63b840d 100644 --- a/js/src/components/paid-ads/gen-ai-card.scss +++ b/js/src/components/paid-ads/gen-ai-card.scss @@ -1,24 +1,20 @@ .gla-gen-ai-card { - :where(.gla-gen-ai-card__wrapper) { - @media (min-width: $break-small) { - flex-direction: row; - } - } - :where(.gla-section-card-title) { font-size: 16px; - margin-bottom: 8px; } - :where(.is-success) { - padding-top: 0; - padding-bottom: 0; - width: 100%; + .components-notice__content { + :where(svg) { + display: block; + fill: $gla-color-green-70; + } - :where(.components-notice__content) { - display: flex; - align-items: center; - gap: 12px; + p { + margin: 0; } } + + :where(.is-success) { + width: 100%; + } } From 17b651f0d2ea14daf90316c69402a5a737fe4307 Mon Sep 17 00:00:00 2001 From: asvinb Date: Fri, 16 Jan 2026 18:59:50 +0400 Subject: [PATCH 038/123] Update snapshot --- .../__snapshots__/gen-ai-card.test.js.snap | 52 +++++++++++++------ 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/js/src/components/paid-ads/__snapshots__/gen-ai-card.test.js.snap b/js/src/components/paid-ads/__snapshots__/gen-ai-card.test.js.snap index 2de7c7244b..a4de36ac77 100644 --- a/js/src/components/paid-ads/__snapshots__/gen-ai-card.test.js.snap +++ b/js/src/components/paid-ads/__snapshots__/gen-ai-card.test.js.snap @@ -16,7 +16,7 @@ exports[`GenAICard Generate assets with GenAI button Match the snapshot 1`] = ` data-wp-component="CardBody" >
@@ -55,21 +55,39 @@ exports[`GenAICard Generate assets with GenAI button Match the snapshot 1`] = `
- -

- Text assets were auto-populated with Google AI -

+
+ +
+
+

+ Text assets were auto-populated with Google AI +

+
+
@@ -78,13 +96,15 @@ exports[`GenAICard Generate assets with GenAI button Match the snapshot 1`] = `
Google's Gen AI illustration From c4f5fd744517fefdfd99ab4e9bdef4a77642dd2b Mon Sep 17 00:00:00 2001 From: Ankit Gade Date: Fri, 16 Jan 2026 21:34:38 +0530 Subject: [PATCH 039/123] Add GenAI progress bar component. --- .../paid-ads/asset-group/asset-group.js | 2 + js/src/components/paid-ads/gen-ai-progress.js | 40 ++++++++++ .../components/paid-ads/gen-ai-progress.scss | 24 ++++++ .../gen-ai-progress.svg | 74 +++++++++++++++++++ 4 files changed, 140 insertions(+) create mode 100644 js/src/components/paid-ads/gen-ai-progress.js create mode 100644 js/src/components/paid-ads/gen-ai-progress.scss create mode 100644 js/src/images/pmax-assets-improvements/gen-ai-progress.svg diff --git a/js/src/components/paid-ads/asset-group/asset-group.js b/js/src/components/paid-ads/asset-group/asset-group.js index 3590234848..8278a07e50 100644 --- a/js/src/components/paid-ads/asset-group/asset-group.js +++ b/js/src/components/paid-ads/asset-group/asset-group.js @@ -18,6 +18,7 @@ import { recordGlaEvent } from '~/utils/tracks'; import useTargetAudienceFinalCountryCodes from '~/hooks/useTargetAudienceFinalCountryCodes'; import AssetGroupHeader from './asset-group-header'; import AssetGroupEditor from './asset-group-editor'; +import GenAIProgress from '../gen-ai-progress'; import { upsertActionedCampaign } from '~/utils/actionedCampaignsCache'; import './asset-group.scss'; @@ -154,6 +155,7 @@ export default function AssetGroup( { campaign } ) { ) } /> + diff --git a/js/src/components/paid-ads/gen-ai-progress.js b/js/src/components/paid-ads/gen-ai-progress.js new file mode 100644 index 0000000000..b31ce0ea25 --- /dev/null +++ b/js/src/components/paid-ads/gen-ai-progress.js @@ -0,0 +1,40 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Flex, FlexBlock, ProgressBar } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import ProgressGraphics from '~/images/pmax-assets-improvements/gen-ai-progress.svg'; +import './gen-ai-progress.scss'; + +const GenAIProgress = () => { + return ( +
+ Gen AI Progress + + +

+ { __( 'Generating assets', 'google-listings-and-ads' ) } +

+ +

+ { __( + 'Google AI is analyzing your campaign’s URL to automatically generate your ad assets', + 'google-listings-and-ads' + ) } +

+
+
+
+ ); +}; + +export default GenAIProgress; diff --git a/js/src/components/paid-ads/gen-ai-progress.scss b/js/src/components/paid-ads/gen-ai-progress.scss new file mode 100644 index 0000000000..9baff3dea5 --- /dev/null +++ b/js/src/components/paid-ads/gen-ai-progress.scss @@ -0,0 +1,24 @@ +.gen-ai-progress { + align-items: center; + display: flex; + flex-direction: column; + padding: $grid-unit-80 0; + + :where(.gen-ai-progress__bar) { + max-width: 520px; + height: 4px; + width: 100%; + + > div:first-child { + background-color: #3858E9; + } + } + + :where(.gen-ai-progress__text-content) { + text-align: center; + + h2{ + font-size: $gla-font-small-medium; + } + } +} \ No newline at end of file diff --git a/js/src/images/pmax-assets-improvements/gen-ai-progress.svg b/js/src/images/pmax-assets-improvements/gen-ai-progress.svg new file mode 100644 index 0000000000..d0ba29c20b --- /dev/null +++ b/js/src/images/pmax-assets-improvements/gen-ai-progress.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 8ab751ace6e02b64628b87ff8027b3f2d7e3a926 Mon Sep 17 00:00:00 2001 From: asvinb Date: Fri, 16 Jan 2026 20:20:34 +0400 Subject: [PATCH 040/123] Add SVG mock support in Jest config --- jest.config.js | 3 ++- tests/mocks/assets/svgFileMock.js | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 tests/mocks/assets/svgFileMock.js diff --git a/jest.config.js b/jest.config.js index f03a9dd236..1695b281e3 100644 --- a/jest.config.js +++ b/jest.config.js @@ -12,7 +12,8 @@ module.exports = { ], moduleNameMapper: { '\\.(png|jpg)$': '/tests/mocks/assets/imageMock.js', - '\\.svg(\\?inline)?$': '/tests/mocks/assets/svgrMock.js', + '\\.svg\\?inline$': '/tests/mocks/assets/svgrMock.js', + '\\.svg$': '/tests/mocks/assets/svgFileMock.js', '\\.scss$': '/tests/mocks/assets/styleMock.js', // Transform our `~/` alias. '^~/(.*)$': '/js/src/$1', diff --git a/tests/mocks/assets/svgFileMock.js b/tests/mocks/assets/svgFileMock.js new file mode 100644 index 0000000000..95b0b6d997 --- /dev/null +++ b/tests/mocks/assets/svgFileMock.js @@ -0,0 +1 @@ +module.exports = 'SvgrURL'; From a8e4a1da23765cf0c98f010016ec064ebc955769 Mon Sep 17 00:00:00 2001 From: Ankit Gade Date: Fri, 16 Jan 2026 22:25:36 +0530 Subject: [PATCH 041/123] Suppress ESLint. ProgressBar is available, but false reporting by eslint. --- js/src/components/paid-ads/gen-ai-progress.js | 1 + 1 file changed, 1 insertion(+) diff --git a/js/src/components/paid-ads/gen-ai-progress.js b/js/src/components/paid-ads/gen-ai-progress.js index b31ce0ea25..73813fefac 100644 --- a/js/src/components/paid-ads/gen-ai-progress.js +++ b/js/src/components/paid-ads/gen-ai-progress.js @@ -2,6 +2,7 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; +// eslint-disable-next-line import/named, @woocommerce/dependency-group import { Flex, FlexBlock, ProgressBar } from '@wordpress/components'; /** From e66a39e504581955ad84b4da5499ffe6ef339a01 Mon Sep 17 00:00:00 2001 From: asvinb Date: Fri, 16 Jan 2026 21:10:35 +0400 Subject: [PATCH 042/123] Add AIIcon for AI generated texts. --- .../asset-group-editor/texts-editor.js | 15 +++++++++++++++ .../asset-group-editor/texts-editor.scss | 4 ++++ 2 files changed, 19 insertions(+) diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js b/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js index 2d1f7deabc..a154806faf 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js @@ -11,11 +11,13 @@ import GridiconCrossSmall from 'gridicons/dist/cross-small'; */ import { useAppDispatch } from '~/data'; import useDispatchCoreNotices from '~/hooks/useDispatchCoreNotices'; +import useGenAITextAssets from '~/hooks/useGenAITextAssets'; import AppButton from '~/components/app-button'; import AppInputControl from '~/components/app-input-control'; import AssetItemActionButton, { ACTION_TYPES, } from './asset-item-action-button'; +import AIIcon from '~/images/ai-icon.svg?inline'; import './texts-editor.scss'; function normalizeNumberOfTexts( texts, minNumberOfTexts, maxNumberOfTexts ) { @@ -119,6 +121,10 @@ export default function TextsEditor( { const { fetchGenAITextAssets } = useAppDispatch(); const [ texts, setTexts ] = useState( initialTexts ); const [ isGeneratingAssets, setIsGeneratingAssets ] = useState( false ); + const { assets: genAITextAssets } = useGenAITextAssets( + finalUrl, + assetKey + ); const updateTexts = ( nextTexts ) => { setTexts( nextTexts ); @@ -227,6 +233,15 @@ export default function TextsEditor( { placeholder={ placeholder } data-index={ index } onChange={ handleChange } + suffix={ + genAITextAssets?.includes( text ) && ( + + ) + } />
{ index + 1 > minNumberOfTexts && ( diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.scss b/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.scss index c062d29336..e5c12ccd5a 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.scss +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.scss @@ -37,4 +37,8 @@ top: 50%; transform: translate(-50%, -50%); } + + .gla-texts-editor__ai-icon { + color: var(--wp-components-color-accent, var(--wp-admin-theme-color, #3858e9)); + } } From 1de2c0284cee8c81d861bf78baf76c916d5baa2b Mon Sep 17 00:00:00 2001 From: asvinb Date: Mon, 19 Jan 2026 13:32:39 +0400 Subject: [PATCH 043/123] Add tests for AI icon. --- .../add-paid-campaigns.test.js | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js b/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js index ad7c578133..db9fba37c0 100644 --- a/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js +++ b/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js @@ -845,6 +845,29 @@ test.describe( 'Add paid campaign', () => { } ); } ); + test.describe( 'AI Icon', () => { + test( 'is visible next to generated text assets and not visible if changed', async () => { + const descriptionInputs = + createCampaignPage.getDescriptionInputs(); + const lastDescriptionInput = + descriptionInputs.last(); + + // Move one level up + const row = lastDescriptionInput.locator( '..' ); + const aiIcon = row.locator( + '.gla-texts-editor__ai-icon' + ); + + await expect( aiIcon ).toHaveCount( 1 ); + + await lastDescriptionInput.fill( + 'Custom description text' + ); + + await expect( aiIcon ).toHaveCount( 0 ); + } ); + } ); + test.describe( 'Error', () => { test.beforeEach( async () => { createCampaignPage.mockEmptyGenerateTextAssets(); From efd68f318cea4f37be8634f69dc2063d4aad4f91 Mon Sep 17 00:00:00 2001 From: Ankit Gade Date: Mon, 19 Jan 2026 15:41:07 +0530 Subject: [PATCH 044/123] Fix: ConversionValueRule class cleanup should not happen. --- bin/GoogleAdsCleanupServices.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/bin/GoogleAdsCleanupServices.php b/bin/GoogleAdsCleanupServices.php index 239ac5443a..99484946fe 100644 --- a/bin/GoogleAdsCleanupServices.php +++ b/bin/GoogleAdsCleanupServices.php @@ -52,12 +52,11 @@ class GoogleAdsCleanupServices { * @var string[] List of Service to NOT remove even when usage is not found. */ protected $avoid_cleanup = [ - // Some methods like `ResourceNames::forGeoTargetConstant` are changed to use - // `BatchJobServiceClient` class instead of `GoogleAdsServiceClient` when - // upgrading from v18 to v20, so we need to keep this service. See: - // - https://github.com/googleads/google-ads-php/blob/v28.0.0/src/Google/Ads/GoogleAds/Util/V18/ResourceNames.php#L1704-L1710 - // - https://github.com/googleads/google-ads-php/blob/v28.0.0/src/Google/Ads/GoogleAds/Util/V20/ResourceNames.php#L1433-L1439 - 'BatchJob', + // ConversionValueRuleService is now used in `ResourceNames::forGeoTargetConstant` in V22. + // instead of the previous BatchJobServiceClient. See: + // - https://github.com/googleads/google-ads-php/blob/v28.0.0/src/Google/Ads/GoogleAds/Util/V20/ResourceNames.php#L1433-L1439 + // - https://github.com/googleads/google-ads-php/blob/v31.1.0/src/Google/Ads/GoogleAds/Util/V22/ResourceNames.php#L1460 + 'ConversionValueRule', ]; /** From b1241f09cf89d8047bf85d026a6fd61c3d90ba8f Mon Sep 17 00:00:00 2001 From: Ankit Gade Date: Mon, 19 Jan 2026 16:14:14 +0530 Subject: [PATCH 045/123] Fix: php lint. --- bin/GoogleAdsCleanupServices.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/GoogleAdsCleanupServices.php b/bin/GoogleAdsCleanupServices.php index 99484946fe..6f28316c37 100644 --- a/bin/GoogleAdsCleanupServices.php +++ b/bin/GoogleAdsCleanupServices.php @@ -54,7 +54,7 @@ class GoogleAdsCleanupServices { protected $avoid_cleanup = [ // ConversionValueRuleService is now used in `ResourceNames::forGeoTargetConstant` in V22. // instead of the previous BatchJobServiceClient. See: - // - https://github.com/googleads/google-ads-php/blob/v28.0.0/src/Google/Ads/GoogleAds/Util/V20/ResourceNames.php#L1433-L1439 + // - https://github.com/googleads/google-ads-php/blob/v28.0.0/src/Google/Ads/GoogleAds/Util/V20/ResourceNames.php#L1433-L1439 // - https://github.com/googleads/google-ads-php/blob/v31.1.0/src/Google/Ads/GoogleAds/Util/V22/ResourceNames.php#L1460 'ConversionValueRule', ]; From e6c8f8ab9059b8e5733e4bebd9d681f495e6fd25 Mon Sep 17 00:00:00 2001 From: asvinb Date: Mon, 19 Jan 2026 16:20:19 +0400 Subject: [PATCH 046/123] Tweak to styling. --- js/src/components/paid-ads/gen-ai-progress.js | 31 +++++++-------- .../components/paid-ads/gen-ai-progress.scss | 38 +++++++++---------- 2 files changed, 35 insertions(+), 34 deletions(-) diff --git a/js/src/components/paid-ads/gen-ai-progress.js b/js/src/components/paid-ads/gen-ai-progress.js index 73813fefac..b971dccde9 100644 --- a/js/src/components/paid-ads/gen-ai-progress.js +++ b/js/src/components/paid-ads/gen-ai-progress.js @@ -3,7 +3,7 @@ */ import { __ } from '@wordpress/i18n'; // eslint-disable-next-line import/named, @woocommerce/dependency-group -import { Flex, FlexBlock, ProgressBar } from '@wordpress/components'; +import { ProgressBar } from '@wordpress/components'; /** * Internal dependencies @@ -20,20 +20,21 @@ const GenAIProgress = () => { width={ 212 } height={ 212 } /> - - -

- { __( 'Generating assets', 'google-listings-and-ads' ) } -

- -

- { __( - 'Google AI is analyzing your campaign’s URL to automatically generate your ad assets', - 'google-listings-and-ads' - ) } -

-
-
+ +
+

+ { __( 'Generating assets', 'google-listings-and-ads' ) } +

+ + + +

+ { __( + 'Google AI is analyzing your campaign’s URL to automatically generate your ad assets', + 'google-listings-and-ads' + ) } +

+
); }; diff --git a/js/src/components/paid-ads/gen-ai-progress.scss b/js/src/components/paid-ads/gen-ai-progress.scss index 9baff3dea5..50c4b0d225 100644 --- a/js/src/components/paid-ads/gen-ai-progress.scss +++ b/js/src/components/paid-ads/gen-ai-progress.scss @@ -1,24 +1,24 @@ .gen-ai-progress { - align-items: center; - display: flex; - flex-direction: column; - padding: $grid-unit-80 0; + --wp-components-color-foreground: #3858e9; + text-align: center; + padding: $grid-unit-80 0; - :where(.gen-ai-progress__bar) { - max-width: 520px; - height: 4px; - width: 100%; + .gen-ai-progress__bar { + height: $grid-unit-05; + width: 100%; + } - > div:first-child { - background-color: #3858E9; - } - } + .gen-ai-progress__text-content { + margin: $grid-unit-40 auto $grid-unit-20; + max-width: 520px; - :where(.gen-ai-progress__text-content) { - text-align: center; + h2 { + font-size: $gla-font-small-medium; + margin: 0 0 $grid-unit-20; + } - h2{ - font-size: $gla-font-small-medium; - } - } -} \ No newline at end of file + p { + margin: $grid-unit-20 0 0; + } + } +} From e4a9675a11596af07fc5153cea68c9e80b60255b Mon Sep 17 00:00:00 2001 From: asvinb Date: Mon, 19 Jan 2026 16:22:30 +0400 Subject: [PATCH 047/123] Added comment. --- js/src/components/paid-ads/gen-ai-progress.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/src/components/paid-ads/gen-ai-progress.js b/js/src/components/paid-ads/gen-ai-progress.js index b971dccde9..a1dadd304d 100644 --- a/js/src/components/paid-ads/gen-ai-progress.js +++ b/js/src/components/paid-ads/gen-ai-progress.js @@ -2,7 +2,7 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -// eslint-disable-next-line import/named, @woocommerce/dependency-group +// eslint-disable-next-line import/named, @woocommerce/dependency-group -- ProgressBar exists in @wordpress/components build output but isn't exported from index.ts (not part of the public API maybe). import { ProgressBar } from '@wordpress/components'; /** From cbb31726752923f3d234b72557d838fb6a22ce60 Mon Sep 17 00:00:00 2001 From: asvinb Date: Mon, 19 Jan 2026 17:12:33 +0400 Subject: [PATCH 048/123] Add images to existing images. --- js/src/data/reducer.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/js/src/data/reducer.js b/js/src/data/reducer.js index 5c3bbbdfec..85afadcc19 100644 --- a/js/src/data/reducer.js +++ b/js/src/data/reducer.js @@ -635,7 +635,12 @@ const reducer = ( state = DEFAULT_STATE, action ) => { case TYPES.RECEIVE_GEN_AI_MEDIA_ASSETS: { const { url, data } = action; - return setIn( state, [ 'gen_ai_assets', url, 'media' ], data ); + const existingMedia = state.gen_ai_assets?.[ url ]?.media ?? {}; + + return setIn( state, [ 'gen_ai_assets', url, 'media' ], { + ...existingMedia, + ...data, + } ); } case TYPES.RECEIVE_GEN_AI_TEXT_ASSETS: { From cba94cd42aba61f75a599be4eec768fdb9924d6b Mon Sep 17 00:00:00 2001 From: asvinb Date: Mon, 19 Jan 2026 17:21:49 +0400 Subject: [PATCH 049/123] Update bundle size. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0513341331..1a8ae93de2 100644 --- a/package.json +++ b/package.json @@ -149,7 +149,7 @@ }, { "path": "./js/build/index.js", - "maxSize": "19.1 kB" + "maxSize": "19.2 kB" }, { "path": "./js/build/commons.js", From bde0be5916fc28b3037624ddbb02647e5abe9a55 Mon Sep 17 00:00:00 2001 From: Ankit Gade Date: Mon, 19 Jan 2026 22:39:50 +0530 Subject: [PATCH 050/123] Select home url on mount. --- .../asset-group-header/assets-loader.js | 43 ++++++++++++------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/js/src/components/paid-ads/asset-group/asset-group-header/assets-loader.js b/js/src/components/paid-ads/asset-group/asset-group-header/assets-loader.js index 13f19c5ed2..62557a5257 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-header/assets-loader.js +++ b/js/src/components/paid-ads/asset-group/asset-group-header/assets-loader.js @@ -4,7 +4,7 @@ import { __ } from '@wordpress/i18n'; import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; -import { useState, useRef } from '@wordpress/element'; +import { useState, useRef, useEffect } from '@wordpress/element'; import { Spinner } from '@woocommerce/components'; /** @@ -105,6 +105,31 @@ export default function AssetsLoader( { onAssetsLoaded } ) { const [ fetching, setFetching ] = useState( false ); const { createNotice } = useDispatchCoreNotices(); + // Reusable loader for suggested assets by final URL descriptor. + const loadSuggestedAssets = ( { id, type } ) => { + setFetching( true ); + return fetchSuggestedAssets( id, type ) + .then( ( assets ) => { + onAssetsLoaded( assets ); + } ) + .catch( () => { + setFetching( false ); + createNotice( + 'error', + __( + 'Unable to load assets data from the selected page.', + 'google-listings-and-ads' + ) + ); + } ); + }; + + // On mount, prefetch suggested assets for homepage. + useEffect( () => { + loadSuggestedAssets( { id: -1, type: 'homepage' } ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [] ); + // To have the searching state and keep the entered search value, this handler needs to // be called immediately after keying values. Therefore, it also needs to implement the // debounce here. @@ -154,21 +179,7 @@ export default function AssetsLoader( { onAssetsLoaded } ) { const handleClick = async () => { const { finalUrl } = selectedOptions[ 0 ]; - - setFetching( true ); - - fetchSuggestedAssets( finalUrl.id, finalUrl.type ) - .then( onAssetsLoaded ) - .catch( () => { - setFetching( false ); - createNotice( - 'error', - __( - 'Unable to load assets data from the selected page.', - 'google-listings-and-ads' - ) - ); - } ); + loadSuggestedAssets( { id: finalUrl.id, type: finalUrl.type } ); }; const { finalUrl } = selectedOptions[ 0 ] || {}; From 41ca59d54cc65cd087c4a97a493395ef32460eed Mon Sep 17 00:00:00 2001 From: Ankit Gade Date: Mon, 19 Jan 2026 22:55:19 +0530 Subject: [PATCH 051/123] Update to async function. --- .../asset-group-header/assets-loader.js | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/js/src/components/paid-ads/asset-group/asset-group-header/assets-loader.js b/js/src/components/paid-ads/asset-group/asset-group-header/assets-loader.js index 62557a5257..d28d1e7562 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-header/assets-loader.js +++ b/js/src/components/paid-ads/asset-group/asset-group-header/assets-loader.js @@ -105,26 +105,20 @@ export default function AssetsLoader( { onAssetsLoaded } ) { const [ fetching, setFetching ] = useState( false ); const { createNotice } = useDispatchCoreNotices(); - // Reusable loader for suggested assets by final URL descriptor. - const loadSuggestedAssets = ( { id, type } ) => { + const loadSuggestedAssets = async ( { id, type } ) => { setFetching( true ); - return fetchSuggestedAssets( id, type ) - .then( ( assets ) => { - onAssetsLoaded( assets ); - } ) - .catch( () => { - setFetching( false ); - createNotice( - 'error', - __( - 'Unable to load assets data from the selected page.', - 'google-listings-and-ads' - ) - ); - } ); + try { + const assets = await fetchSuggestedAssets( id, type ); + onAssetsLoaded( assets ); + } catch ( error ) { + setFetching( false ); + createNotice( + 'error', + __( 'Unable to load assets data.', 'google-listings-and-ads' ) + ); + } }; - // On mount, prefetch suggested assets for homepage. useEffect( () => { loadSuggestedAssets( { id: -1, type: 'homepage' } ); // eslint-disable-next-line react-hooks/exhaustive-deps From 85fb6985889794a6847f678b32be84e0519fa0cf Mon Sep 17 00:00:00 2001 From: James Morrison Date: Tue, 20 Jan 2026 10:49:54 +0000 Subject: [PATCH 052/123] PR Feedback. --- src/Ads/AdsAssetGenerationService.php | 49 +++++++++---------- .../Ads/AdsAssetGenerationServiceTest.php | 4 +- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/src/Ads/AdsAssetGenerationService.php b/src/Ads/AdsAssetGenerationService.php index 352448795e..2a02d83de2 100644 --- a/src/Ads/AdsAssetGenerationService.php +++ b/src/Ads/AdsAssetGenerationService.php @@ -38,17 +38,17 @@ class AdsAssetGenerationService implements OptionsAwareInterface, Service { protected $client; /** - * Mapping from uppercase input strings to AssetFieldType constants. + * Mapping from lowercase input strings to AssetFieldType constants. * * @var array */ protected const TYPE_MAPPING = [ - 'HEADLINE' => AssetFieldType::HEADLINE, - 'LONG_HEADLINE' => AssetFieldType::LONG_HEADLINE, - 'DESCRIPTION' => AssetFieldType::DESCRIPTION, - 'MARKETING_IMAGE' => AssetFieldType::MARKETING_IMAGE, - 'SQUARE_MARKETING_IMAGE' => AssetFieldType::SQUARE_MARKETING_IMAGE, - 'PORTRAIT_MARKETING_IMAGE' => AssetFieldType::PORTRAIT_MARKETING_IMAGE, + 'headline' => AssetFieldType::HEADLINE, + 'long_headline' => AssetFieldType::LONG_HEADLINE, + 'description' => AssetFieldType::DESCRIPTION, + 'marketing_image' => AssetFieldType::MARKETING_IMAGE, + 'square_marketing_image' => AssetFieldType::SQUARE_MARKETING_IMAGE, + 'portrait_marketing_image' => AssetFieldType::PORTRAIT_MARKETING_IMAGE, ]; /** @@ -67,7 +67,7 @@ public function __construct( GoogleAdsClient $client ) { * Optional. Arguments for generating text assets. * * @type string $final_url The final URL - defaults to the Site URL. - * @type array $asset_field_types Can be one or more of: HEADLINE, LONG_HEADLINE, DESCRIPTION. + * @type array $asset_field_types Can be one or more of: headline, long_headline, description. * } * @return array Array of generated text objects with 'text' and 'type' keys. * @throws Exception If the text assets can't be generated. @@ -80,8 +80,14 @@ public function generate_text( array $args = [] ): array { $final_url = $args['final_url'] ?? $this->get_site_url(); - // Convert asset field types from uppercase strings to enum numbers. - $asset_field_types = $this->convert_text_types_to_enums( $args['asset_field_types'] ?? [] ); + // Set default types if not provided. + $types = $args['asset_field_types'] ?? []; + if ( empty( $types ) ) { + $types = [ 'headline', 'long_headline', 'description' ]; + } + + // Convert asset field types from lowercase strings to enum numbers. + $asset_field_types = $this->convert_text_types_to_enums( $types ); $request = new GenerateTextRequest( [ @@ -120,7 +126,7 @@ public function generate_text( array $args = [] ): array { * Optional. Arguments for generating image assets. * * @type string $final_url The final URL - defaults to the Site URL. - * @type array $asset_field_types Can be one or more of: MARKETING_IMAGE, SQUARE_MARKETING_IMAGE, PORTRAIT_MARKETING_IMAGE. + * @type array $asset_field_types Can be one or more of: marketing_image, square_marketing_image, portrait_marketing_image. * } * @return array Array of generated image objects with 'temporary_image_url' and 'type' keys. * @throws Exception If the image assets can't be generated. @@ -133,7 +139,7 @@ public function generate_images( array $args = [] ): array { $final_url = $args['final_url'] ?? $this->get_site_url(); - // Convert asset field types from uppercase strings to enum numbers (if provided). + // Convert asset field types from lowercase strings to enum numbers (if provided). $asset_field_types = []; if ( ! empty( $args['asset_field_types'] ) ) { $asset_field_types = $this->convert_image_types_to_enums( $args['asset_field_types'] ); @@ -181,21 +187,12 @@ public function generate_images( array $args = [] ): array { } /** - * Convert text asset field types from uppercase strings to enum numbers. + * Convert text asset field types from lowercase strings to enum numbers. * - * @param array $types Array of uppercase type strings (HEADLINE, LONG_HEADLINE, DESCRIPTION). - * @return array Array of enum numbers. Defaults to all text types if empty. + * @param array $types Array of lowercase type strings (headline, long_headline, description). + * @return array Array of enum numbers. */ protected function convert_text_types_to_enums( array $types ): array { - if ( empty( $types ) ) { - // Default to all text types. - return [ - AssetFieldType::number( AssetFieldType::HEADLINE ), - AssetFieldType::number( AssetFieldType::LONG_HEADLINE ), - AssetFieldType::number( AssetFieldType::DESCRIPTION ), - ]; - } - $enums = []; foreach ( $types as $type ) { if ( ! isset( self::TYPE_MAPPING[ $type ] ) ) { @@ -213,9 +210,9 @@ protected function convert_text_types_to_enums( array $types ): array { } /** - * Convert image asset field types from uppercase strings to enum numbers. + * Convert image asset field types from lowercase strings to enum numbers. * - * @param array $types Array of uppercase type strings (MARKETING_IMAGE, SQUARE_MARKETING_IMAGE, PORTRAIT_MARKETING_IMAGE). + * @param array $types Array of lowercase type strings (marketing_image, square_marketing_image, portrait_marketing_image). * @return array Array of enum numbers. */ protected function convert_image_types_to_enums( array $types ): array { diff --git a/tests/Unit/Ads/AdsAssetGenerationServiceTest.php b/tests/Unit/Ads/AdsAssetGenerationServiceTest.php index f79759844a..ed6e3438c8 100644 --- a/tests/Unit/Ads/AdsAssetGenerationServiceTest.php +++ b/tests/Unit/Ads/AdsAssetGenerationServiceTest.php @@ -95,7 +95,7 @@ public function test_generate_text_with_specific_types() { $this->generate_text_assets_mock( $expected_text_assets ); - $result = $this->service->generate_text( [ 'asset_field_types' => [ 'HEADLINE' ] ] ); + $result = $this->service->generate_text( [ 'asset_field_types' => [ 'headline' ] ] ); $this->assertEquals( $expected_text_assets, $result ); } @@ -170,7 +170,7 @@ public function test_generate_images_with_specific_types() { $this->generate_image_assets_mock( $expected_image_assets ); - $result = $this->service->generate_images( [ 'asset_field_types' => [ 'MARKETING_IMAGE' ] ] ); + $result = $this->service->generate_images( [ 'asset_field_types' => [ 'marketing_image' ] ] ); $this->assertEquals( $expected_image_assets, $result ); } From ed1573a80e2383b5f04afd10a6004afec9f8f991 Mon Sep 17 00:00:00 2001 From: James Morrison Date: Tue, 20 Jan 2026 10:58:05 +0000 Subject: [PATCH 053/123] Merge convert functions. --- src/Ads/AdsAssetGenerationService.php | 39 ++++++++------------------- 1 file changed, 11 insertions(+), 28 deletions(-) diff --git a/src/Ads/AdsAssetGenerationService.php b/src/Ads/AdsAssetGenerationService.php index 2a02d83de2..c16116369b 100644 --- a/src/Ads/AdsAssetGenerationService.php +++ b/src/Ads/AdsAssetGenerationService.php @@ -87,7 +87,8 @@ public function generate_text( array $args = [] ): array { } // Convert asset field types from lowercase strings to enum numbers. - $asset_field_types = $this->convert_text_types_to_enums( $types ); + $allowed_types = [ AssetFieldType::HEADLINE, AssetFieldType::LONG_HEADLINE, AssetFieldType::DESCRIPTION ]; + $asset_field_types = $this->convert_types_to_enums( $types, $allowed_types ); $request = new GenerateTextRequest( [ @@ -142,7 +143,8 @@ public function generate_images( array $args = [] ): array { // Convert asset field types from lowercase strings to enum numbers (if provided). $asset_field_types = []; if ( ! empty( $args['asset_field_types'] ) ) { - $asset_field_types = $this->convert_image_types_to_enums( $args['asset_field_types'] ); + $allowed_types = [ AssetFieldType::MARKETING_IMAGE, AssetFieldType::SQUARE_MARKETING_IMAGE, AssetFieldType::PORTRAIT_MARKETING_IMAGE ]; + $asset_field_types = $this->convert_types_to_enums( $args['asset_field_types'], $allowed_types ); } $request_data = [ @@ -187,12 +189,13 @@ public function generate_images( array $args = [] ): array { } /** - * Convert text asset field types from lowercase strings to enum numbers. + * Convert asset field types from lowercase strings to enum numbers. * - * @param array $types Array of lowercase type strings (headline, long_headline, description). + * @param array $types Array of lowercase type strings. + * @param array $allowed_types Optional. Array of AssetFieldType constants to filter by. * @return array Array of enum numbers. */ - protected function convert_text_types_to_enums( array $types ): array { + protected function convert_types_to_enums( array $types, array $allowed_types = [] ): array { $enums = []; foreach ( $types as $type ) { if ( ! isset( self::TYPE_MAPPING[ $type ] ) ) { @@ -200,33 +203,13 @@ protected function convert_text_types_to_enums( array $types ): array { } $internal_type = self::TYPE_MAPPING[ $type ]; - // Only include text types. - if ( in_array( $internal_type, [ AssetFieldType::HEADLINE, AssetFieldType::LONG_HEADLINE, AssetFieldType::DESCRIPTION ], true ) ) { - $enums[] = AssetFieldType::number( $internal_type ); - } - } - return $enums; - } - - /** - * Convert image asset field types from lowercase strings to enum numbers. - * - * @param array $types Array of lowercase type strings (marketing_image, square_marketing_image, portrait_marketing_image). - * @return array Array of enum numbers. - */ - protected function convert_image_types_to_enums( array $types ): array { - $enums = []; - foreach ( $types as $type ) { - if ( ! isset( self::TYPE_MAPPING[ $type ] ) ) { + // Filter by allowed types if specified. + if ( ! empty( $allowed_types ) && ! in_array( $internal_type, $allowed_types, true ) ) { continue; } - $internal_type = self::TYPE_MAPPING[ $type ]; - // Only include image types. - if ( in_array( $internal_type, [ AssetFieldType::MARKETING_IMAGE, AssetFieldType::SQUARE_MARKETING_IMAGE, AssetFieldType::PORTRAIT_MARKETING_IMAGE ], true ) ) { - $enums[] = AssetFieldType::number( $internal_type ); - } + $enums[] = AssetFieldType::number( $internal_type ); } return $enums; From 965a9352601b324507aad60f50f67751b6f76e3f Mon Sep 17 00:00:00 2001 From: James Morrison Date: Tue, 20 Jan 2026 11:27:07 +0000 Subject: [PATCH 054/123] PR feedback. --- .../Ads/AssetGenerationController.php | 39 ++++++++----------- .../Ads/AssetGenerationControllerTest.php | 14 +++---- 2 files changed, 23 insertions(+), 30 deletions(-) diff --git a/src/API/Site/Controllers/Ads/AssetGenerationController.php b/src/API/Site/Controllers/Ads/AssetGenerationController.php index 4c79d89eb6..d541a8b78c 100644 --- a/src/API/Site/Controllers/Ads/AssetGenerationController.php +++ b/src/API/Site/Controllers/Ads/AssetGenerationController.php @@ -4,6 +4,7 @@ namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads; use Automattic\WooCommerce\GoogleListingsAndAds\Ads\AdsAssetGenerationService; +use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\AssetFieldType; use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BaseController; use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\ResponseFromExceptionTrait; use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods; @@ -87,7 +88,11 @@ protected function get_generate_text_params(): array { 'type' => 'array', 'items' => [ 'type' => 'string', - 'enum' => [ 'headline', 'long_headline', 'description' ], + 'enum' => [ + AssetFieldType::HEADLINE, + AssetFieldType::LONG_HEADLINE, + AssetFieldType::DESCRIPTION, + ], ], 'sanitize_callback' => function ( $types ) { return array_map( 'sanitize_text_field', $types ); @@ -115,7 +120,11 @@ protected function get_generate_images_params(): array { 'type' => 'array', 'items' => [ 'type' => 'string', - 'enum' => [ 'marketing_image', 'square_marketing_image', 'portrait_marketing_image' ], + 'enum' => [ + AssetFieldType::MARKETING_IMAGE, + AssetFieldType::SQUARE_MARKETING_IMAGE, + AssetFieldType::PORTRAIT_MARKETING_IMAGE, + ], ], 'sanitize_callback' => function ( $types ) { return array_map( 'sanitize_text_field', $types ); @@ -136,14 +145,11 @@ protected function get_generate_text_callback(): callable { $final_url = $request->get_param( 'final_url' ) ?: $this->get_site_url(); $types = $request->get_param( 'types' ) ?: [ 'headline', 'long_headline', 'description' ]; - // Convert lowercase types to uppercase for service. - $uppercase_types = $this->convert_types_to_uppercase( $types ); - - // Call service. + // Call service with lowercase types. $items = $this->service->generate_text( [ 'final_url' => $final_url, - 'asset_field_types' => $uppercase_types, + 'asset_field_types' => $types, ] ); @@ -166,13 +172,10 @@ protected function get_generate_images_callback(): callable { $final_url = $request->get_param( 'final_url' ) ?: $this->get_site_url(); $types = $request->get_param( 'types' ) ?: []; - // Convert lowercase types to uppercase for service (if provided). - $uppercase_types = ! empty( $types ) ? $this->convert_types_to_uppercase( $types ) : []; - - // Call service. + // Call service with lowercase types. $args = [ 'final_url' => $final_url ]; - if ( ! empty( $uppercase_types ) ) { - $args['asset_field_types'] = $uppercase_types; + if ( ! empty( $types ) ) { + $args['asset_field_types'] = $types; } $items = $this->service->generate_images( $args ); @@ -184,16 +187,6 @@ protected function get_generate_images_callback(): callable { }; } - /** - * Convert types to uppercase. - * - * @param array $types Array of lowercase type strings. - * @return array Array of uppercase type strings. - */ - protected function convert_types_to_uppercase( array $types ): array { - return array_map( 'strtoupper', $types ); - } - /** * Format the response with final_url and items. * diff --git a/tests/Unit/API/Site/Controllers/Ads/AssetGenerationControllerTest.php b/tests/Unit/API/Site/Controllers/Ads/AssetGenerationControllerTest.php index 25d51c46f3..873437b395 100644 --- a/tests/Unit/API/Site/Controllers/Ads/AssetGenerationControllerTest.php +++ b/tests/Unit/API/Site/Controllers/Ads/AssetGenerationControllerTest.php @@ -38,13 +38,13 @@ public function setUp(): void { } public function test_generate_text_with_defaults() { - // Service expects uppercase types. + // Service expects lowercase types. $this->service->expects( $this->once() ) ->method( 'generate_text' ) ->with( [ 'final_url' => self::TEST_SITE_URL, - 'asset_field_types' => [ 'HEADLINE', 'LONG_HEADLINE', 'DESCRIPTION' ], + 'asset_field_types' => [ 'headline', 'long_headline', 'description' ], ] ) ->willReturn( @@ -87,7 +87,7 @@ public function test_generate_text_with_custom_url() { ->with( [ 'final_url' => 'https://custom-url.com', - 'asset_field_types' => [ 'HEADLINE', 'LONG_HEADLINE', 'DESCRIPTION' ], + 'asset_field_types' => [ 'headline', 'long_headline', 'description' ], ] ) ->willReturn( @@ -117,7 +117,7 @@ public function test_generate_text_with_specific_types() { ->with( [ 'final_url' => self::TEST_SITE_URL, - 'asset_field_types' => [ 'HEADLINE' ], + 'asset_field_types' => [ 'headline' ], ] ) ->willReturn( @@ -148,7 +148,7 @@ public function test_generate_text_type_conversion() { ->with( $this->callback( function ( $args ) { - return $args['asset_field_types'] === [ 'HEADLINE', 'DESCRIPTION' ]; + return $args['asset_field_types'] === [ 'headline', 'description' ]; } ) ) @@ -262,7 +262,7 @@ public function test_generate_images_with_specific_types() { ->with( [ 'final_url' => self::TEST_SITE_URL, - 'asset_field_types' => [ 'MARKETING_IMAGE' ], + 'asset_field_types' => [ 'marketing_image' ], ] ) ->willReturn( @@ -293,7 +293,7 @@ public function test_generate_images_type_conversion() { ->with( $this->callback( function ( $args ) { - return $args['asset_field_types'] === [ 'MARKETING_IMAGE', 'SQUARE_MARKETING_IMAGE' ]; + return $args['asset_field_types'] === [ 'marketing_image', 'square_marketing_image' ]; } ) ) From f0e61f11b9f19b15cb0d434ca486a9e0b788dc1b Mon Sep 17 00:00:00 2001 From: James Morrison Date: Tue, 20 Jan 2026 11:51:21 +0000 Subject: [PATCH 055/123] PR feedback: API controller handles defaults, generation service validates required types are set. --- src/Ads/AdsAssetGenerationService.php | 9 ++++----- .../Ads/AdsAssetGenerationServiceTest.php | 20 ++++++++++++++++--- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/Ads/AdsAssetGenerationService.php b/src/Ads/AdsAssetGenerationService.php index c16116369b..ca64b9e39e 100644 --- a/src/Ads/AdsAssetGenerationService.php +++ b/src/Ads/AdsAssetGenerationService.php @@ -80,15 +80,14 @@ public function generate_text( array $args = [] ): array { $final_url = $args['final_url'] ?? $this->get_site_url(); - // Set default types if not provided. - $types = $args['asset_field_types'] ?? []; - if ( empty( $types ) ) { - $types = [ 'headline', 'long_headline', 'description' ]; + // Validate that asset field types are provided. + if ( empty( $args['asset_field_types'] ) ) { + throw new Exception( __( 'Asset field types are required for text generation.', 'google-listings-and-ads' ) ); } // Convert asset field types from lowercase strings to enum numbers. $allowed_types = [ AssetFieldType::HEADLINE, AssetFieldType::LONG_HEADLINE, AssetFieldType::DESCRIPTION ]; - $asset_field_types = $this->convert_types_to_enums( $types, $allowed_types ); + $asset_field_types = $this->convert_types_to_enums( $args['asset_field_types'], $allowed_types ); $request = new GenerateTextRequest( [ diff --git a/tests/Unit/Ads/AdsAssetGenerationServiceTest.php b/tests/Unit/Ads/AdsAssetGenerationServiceTest.php index ed6e3438c8..d4a5c9495c 100644 --- a/tests/Unit/Ads/AdsAssetGenerationServiceTest.php +++ b/tests/Unit/Ads/AdsAssetGenerationServiceTest.php @@ -64,7 +64,9 @@ public function test_generate_text_with_defaults() { $this->generate_text_assets_mock( $expected_text_assets ); - $result = $this->service->generate_text( [] ); + $result = $this->service->generate_text( [ + 'asset_field_types' => [ 'headline', 'long_headline', 'description' ], + ] ); $this->assertEquals( $expected_text_assets, $result ); } @@ -80,7 +82,10 @@ public function test_generate_text_with_custom_final_url() { $this->generate_text_assets_mock( $expected_text_assets ); - $result = $this->service->generate_text( [ 'final_url' => $final_url ] ); + $result = $this->service->generate_text( [ + 'final_url' => $final_url, + 'asset_field_types' => [ 'headline', 'long_headline', 'description' ], + ] ); $this->assertEquals( $expected_text_assets, $result ); } @@ -108,7 +113,9 @@ public function test_generate_text_exception() { $this->expectException( Exception::class ); $this->expectExceptionMessage( 'Unable to generate text assets' ); - $this->service->generate_text( [] ); + $this->service->generate_text( [ + 'asset_field_types' => [ 'headline' ], + ] ); $this->assertEquals( 1, did_action( 'woocommerce_gla_ads_client_exception' ) ); } @@ -121,6 +128,13 @@ public function test_generate_text_no_ads_id() { $this->service->generate_text( [] ); } + public function test_generate_text_no_types_provided() { + $this->expectException( Exception::class ); + $this->expectExceptionMessage( 'Asset field types are required for text generation' ); + + $this->service->generate_text( [ 'final_url' => 'https://example.com' ] ); + } + public function test_generate_images_with_defaults() { $expected_image_assets = [ [ From 3030629bd06d432af4cb81909ebc14779a2cdd49 Mon Sep 17 00:00:00 2001 From: James Morrison Date: Tue, 20 Jan 2026 11:54:38 +0000 Subject: [PATCH 056/123] PHPCS fixes. --- .../Ads/AdsAssetGenerationServiceTest.php | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/tests/Unit/Ads/AdsAssetGenerationServiceTest.php b/tests/Unit/Ads/AdsAssetGenerationServiceTest.php index d4a5c9495c..b3ebf15c03 100644 --- a/tests/Unit/Ads/AdsAssetGenerationServiceTest.php +++ b/tests/Unit/Ads/AdsAssetGenerationServiceTest.php @@ -64,9 +64,12 @@ public function test_generate_text_with_defaults() { $this->generate_text_assets_mock( $expected_text_assets ); - $result = $this->service->generate_text( [ - 'asset_field_types' => [ 'headline', 'long_headline', 'description' ], - ] ); + $result = $this->service->generate_text( + [ + 'final_url' => self::TEST_SITE_URL, + 'asset_field_types' => [ 'headline', 'long_headline', 'description' ], + ] + ); $this->assertEquals( $expected_text_assets, $result ); } @@ -82,10 +85,16 @@ public function test_generate_text_with_custom_final_url() { $this->generate_text_assets_mock( $expected_text_assets ); - $result = $this->service->generate_text( [ - 'final_url' => $final_url, - 'asset_field_types' => [ 'headline', 'long_headline', 'description' ], - ] ); + $result = $this->service->generate_text( + [ + 'final_url' => $final_url, + 'asset_field_types' => [ + 'headline', + 'long_headline', + 'description', + ], + ] + ); $this->assertEquals( $expected_text_assets, $result ); } @@ -113,9 +122,11 @@ public function test_generate_text_exception() { $this->expectException( Exception::class ); $this->expectExceptionMessage( 'Unable to generate text assets' ); - $this->service->generate_text( [ - 'asset_field_types' => [ 'headline' ], - ] ); + $this->service->generate_text( + [ + 'asset_field_types' => [ 'headline' ], + ] + ); $this->assertEquals( 1, did_action( 'woocommerce_gla_ads_client_exception' ) ); } From 2fb2a74ed3ae9fcc5d3caedabaeefcda4541904f Mon Sep 17 00:00:00 2001 From: asvinb Date: Tue, 20 Jan 2026 18:51:37 +0400 Subject: [PATCH 057/123] Add loading logic. --- .../asset-group-images-section.js | 2 +- .../asset-group-header/asset-group-header.js | 16 ++- .../paid-ads/asset-group/asset-group.js | 131 ++++++++++-------- .../paid-ads/campaign-assets-form.js | 73 ++++++++++ .../pages/create-paid-ads-campaign/index.js | 25 ++-- 5 files changed, 177 insertions(+), 70 deletions(-) diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/asset-group-images-section.js b/js/src/components/paid-ads/asset-group/asset-group-editor/asset-group-images-section.js index 6bd7c5701d..fdfee677cb 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/asset-group-images-section.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/asset-group-images-section.js @@ -35,7 +35,7 @@ const AssetGroupImagesSection = ( { renderErrors, } ) => { const { values, getInputProps, adapter } = useAdaptiveFormContext(); - const showTip = adapter.hasImportedAssets; + const showTip = adapter.hasAISuggestedMediaAssets; return (
- { showTip && ( + { hasImportedAssets && ( { __( @@ -92,9 +96,11 @@ export default function AssetGroupHeader() { - - - + { hasAISuggestedTextAssets && hasAISuggestedMediaAssets && ( + + + + ) }
diff --git a/js/src/components/paid-ads/asset-group/asset-group.js b/js/src/components/paid-ads/asset-group/asset-group.js index 8278a07e50..d04998a973 100644 --- a/js/src/components/paid-ads/asset-group/asset-group.js +++ b/js/src/components/paid-ads/asset-group/asset-group.js @@ -72,7 +72,13 @@ export default function AssetGroup( { campaign } ) { const { isValidForm, handleSubmit, adapter, values } = useAdaptiveFormContext(); const { data: countryCodes } = useTargetAudienceFinalCountryCodes(); - const { isValidAssetGroup, isSubmitting, isSubmitted, submitter } = adapter; + const { + isValidAssetGroup, + isSubmitting, + isSubmitted, + submitter, + isFetchingGenAIAssets, + } = adapter; const currentAction = submitter?.dataset.action; const hasRaiseBudgetRecommendation = () => { @@ -155,60 +161,77 @@ export default function AssetGroup( { campaign } ) { ) } /> - - - - - - - { ( isCreation || adapter.isEmptyAssetEntityGroup ) && ( - // Currently, the PMax Assets feature in this extension doesn't offer the function - // to delete the asset entity group, so it needs to hide the skip button if the editing - // asset group is not considered empty. - - { __( - 'Skip this step', - 'google-listings-and-ads' + { isFetchingGenAIAssets && } + + { ! isFetchingGenAIAssets && ( + <> + + + + + + { ( isCreation || + adapter.isEmptyAssetEntityGroup ) && ( + // Currently, the PMax Assets feature in this extension doesn't offer the function + // to delete the asset entity group, so it needs to hide the skip button if the editing + // asset group is not considered empty. + + { __( + 'Skip this step', + 'google-listings-and-ads' + ) } + ) } - - ) } - - { isCreation - ? __( 'Create campaign', 'google-listings-and-ads' ) - : __( 'Save changes', 'google-listings-and-ads' ) } - - - - + + { isCreation + ? __( + 'Create campaign', + 'google-listings-and-ads' + ) + : __( + 'Save changes', + 'google-listings-and-ads' + ) } + + + + + + ) } ); } diff --git a/js/src/components/paid-ads/campaign-assets-form.js b/js/src/components/paid-ads/campaign-assets-form.js index b49d43f491..5a1ca039a4 100644 --- a/js/src/components/paid-ads/campaign-assets-form.js +++ b/js/src/components/paid-ads/campaign-assets-form.js @@ -16,6 +16,7 @@ import useAdsCurrency from '~/hooks/useAdsCurrency'; import useBudgetRecommendation from '~/hooks/useBudgetRecommendation'; import useRaiseBudgetRecommendations from '~/hooks/useRaiseBudgetRecommendations'; import useEventPropertiesFilter from '~/hooks/useEventPropertiesFilter'; +import { useAppDispatch } from '~/data'; import { FILTER_BUDGET_RECOMMENDATIONS } from '~/utils/tracks'; import round from '~/utils/round'; @@ -41,6 +42,18 @@ const emptyAssetGroup = { [ ASSET_FORM_KEY.YOUTUBE_VIDEO ]: [], }; +const REQUIRED_TEXT_ASSET_KEYS = [ + ASSET_FORM_KEY.LONG_HEADLINE, + ASSET_FORM_KEY.HEADLINE, + ASSET_FORM_KEY.DESCRIPTION, +]; + +const REQUIRED_MEDIA_ASSET_KEYS = [ + ASSET_FORM_KEY.MARKETING_IMAGE, + ASSET_FORM_KEY.SQUARE_MARKETING_IMAGE, + ASSET_FORM_KEY.PORTRAIT_MARKETING_IMAGE, +]; + /** * Converts the asset entity group data to the assets form values. * @@ -143,6 +156,30 @@ function resolveInitialCampaign( return injectDailyBudget( values, budgetRecommendation ); } +function hasValidAIGeneratedAssets( assetKeys, data ) { + if ( ! data || typeof data !== 'object' ) { + return false; + } + + // Ensure object isn't empty + if ( Object.keys( data ).length === 0 ) { + return false; + } + + // Ensure required keys exist + contain at least 1 non-empty string + return assetKeys.every( ( key ) => { + const value = data[ key ]; + + return ( + Array.isArray( value ) && + value.length > 0 && + value.some( + ( item ) => typeof item === 'string' && item.trim().length > 0 + ) + ); + } ); +} + /** * Renders a form based on AdaptiveForm for managing campaign and assets. * @@ -158,12 +195,19 @@ export default function CampaignAssetsForm( { countryCodes, ...adaptiveFormProps } ) { + const { fetchGenAIMediaAssets, fetchGenAITextAssets } = useAppDispatch(); + const [ isFetchingGenAIAssets, setIsFetchingGenAIAssets ] = + useState( false ); const initialAssetGroup = useMemo( () => { return convertAssetEntityGroupToFormValues( assetEntityGroup ); }, [ assetEntityGroup ] ); const [ baseAssetGroup, setBaseAssetGroup ] = useState( initialAssetGroup ); const [ hasImportedAssets, setHasImportedAssets ] = useState( false ); + const [ hasAISuggestedTextAssets, setHasAISuggestedTextAssets ] = + useState( false ); + const [ hasAISuggestedMediaAssets, setHasAISuggestedMediaAssets ] = + useState( false ); const { formatAmount } = useAdsCurrency(); const { data: budgetRecommendationData, hasResolved } = useBudgetRecommendation( countryCodes ); @@ -234,8 +278,37 @@ export default function CampaignAssetsForm( { setHasImportedAssets( hasNonEmptyAssets ); setBaseAssetGroup( nextAssetGroup ); + setHasAISuggestedTextAssets( false ); + setHasAISuggestedMediaAssets( false ); formContext.adapter.hideValidation(); }, + isFetchingGenAIAssets, + hasAISuggestedTextAssets, + hasAISuggestedMediaAssets, + async fetchGenAIAssets() { + try { + setIsFetchingGenAIAssets( true ); + const { data: textAssetsData } = + await fetchGenAITextAssets( finalUrl ); + const { data: mediaAssetsData } = + await fetchGenAIMediaAssets( finalUrl ); + + const hasSuggestedTextAssets = hasValidAIGeneratedAssets( + REQUIRED_TEXT_ASSET_KEYS, + textAssetsData + ); + + const hasSuggestedMediaAssets = hasValidAIGeneratedAssets( + REQUIRED_MEDIA_ASSET_KEYS, + mediaAssetsData + ); + + setHasAISuggestedTextAssets( hasSuggestedTextAssets ); + setHasAISuggestedMediaAssets( hasSuggestedMediaAssets ); + } finally { + setIsFetchingGenAIAssets( false ); + } + }, }; }; diff --git a/js/src/pages/create-paid-ads-campaign/index.js b/js/src/pages/create-paid-ads-campaign/index.js index c714408811..5ab3a57395 100644 --- a/js/src/pages/create-paid-ads-campaign/index.js +++ b/js/src/pages/create-paid-ads-campaign/index.js @@ -152,16 +152,21 @@ const CreatePaidAdsCampaign = () => { 'google-listings-and-ads' ) } context={ eventContext } - continueButton={ ( formContext ) => ( - { - handleContinueClick( - STEP.ASSET_GROUP - ); - } } - /> - ) } + continueButton={ ( formContext ) => { + const { adapter } = formContext; + + return ( + { + adapter.fetchGenAIAssets(); + handleContinueClick( + STEP.ASSET_GROUP + ); + } } + /> + ); + } } /> ), onClick: handleStepperClick, From c3bb96884d6da9ea5e6dc4a442f4899d940f21d1 Mon Sep 17 00:00:00 2001 From: Ankit Gade Date: Tue, 20 Jan 2026 20:28:42 +0530 Subject: [PATCH 058/123] Add E2E test coverage. --- .../add-paid-campaigns.test.js | 92 ++++++++++++++++++- tests/e2e/utils/mock-requests.js | 9 ++ 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js b/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js index 9ca407bf4b..5ae1a3d10c 100644 --- a/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js +++ b/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js @@ -6,7 +6,12 @@ import { expect, test } from '@playwright/test'; /** * Internal dependencies */ -import { clearOnboardedMerchant, setOnboardedMerchant } from '../../utils/api'; +import { + clearCompletedAdsSetup, + clearOnboardedMerchant, + setOnboardedMerchant, + setCompletedAdsSetup, +} from '../../utils/api'; import DashboardPage from '../../utils/pages/dashboard'; import SetupAdsAccountsPage from '../../utils/pages/ads-onboarding/setup-ads-accounts'; import SetupBudgetPage from '../../utils/pages/ads-onboarding/setup-budget'; @@ -498,6 +503,91 @@ test.describe( 'Set up Ads account', () => { name: 'Got It', } ) .click(); + + await expect( page.getByRole( 'dialog' ) ).not.toBeVisible(); + } ); + } ); + + test.describe( 'Create campaign after ads setup is completed', async () => { + test.beforeAll( async () => { + await setCompletedAdsSetup(); + await dashboardPage.mockAdsAccountConnected(); + await dashboardPage.fulfillAdsCampaignsRequest( [ + { + id: 111111111, + name: 'Test Campaign', + status: 'enabled', + type: 'performance_max', + amount: 1, + country: 'US', + targeted_locations: [ 'US' ], + }, + ] ); + await dashboardPage.fulfillAssetsSuggestions( { + logo: [], + business_name: 'test-business-entity-01', + square_marketing_image: [ 'https://placehold.co/600x600.png' ], + marketing_image: [ 'https://placehold.co/1200x628.png' ], + portrait_marketing_image: [ + 'https://placehold.co/600x750.png', + 'https://placehold.co/600x750.png', + ], + call_to_action_selection: 'contact_us', + final_url: 'https://example.com', + youtube_video: [], + display_url_path: [ 'test', 'path' ], + headline: [ + 'Sample Headline One', + 'Sample Headline Two', + 'Sample Headline Three', + ], + description: [ + 'This is a primary test description for the asset.', + 'This is a secondary test description for the asset.', + ], + long_headline: [ + 'This is a sample long headline for testing purposes', + ], + } ); + await page.addInitScript( () => { + // Ensure global exists and set the flag as early as possible. + window.glaData = window.glaData || {}; + window.glaData.adsSetupComplete = true; + } ); + await dashboardPage.goto(); + await page.reload( { waitUntil: LOAD_STATE.DOM_CONTENT_LOADED } ); + } ); + + test.afterAll( async () => { + await clearCompletedAdsSetup(); + } ); + + test( 'Heading should be "Create your campaign"', async () => { + await dashboardPage.addPaidCampaignButton.click(); + await expect( + page.getByRole( 'heading', { + level: 1, + name: 'Create your campaign', + } ) + ).toBeVisible(); + } ); + + test( 'Next step should be "Optimize your campaign"', async () => { + await page.getByRole( 'button', { name: 'Continue' } ).click(); + await page.waitForLoadState( LOAD_STATE.DOM_CONTENT_LOADED ); + + await expect( + page.getByRole( 'heading', { + level: 1, + name: 'Optimize your campaign', + } ) + ).toBeVisible(); + + // Get Final URL card having class `gla-final-url-card` + const finalUrlCard = page.locator( '.gla-final-url-card' ); + + // Card should contain the link with url `https://example.com` + await expect( finalUrlCard ).toContainText( 'https://example.com' ); } ); } ); } ); diff --git a/tests/e2e/utils/mock-requests.js b/tests/e2e/utils/mock-requests.js index ab83eaa7fa..4ff0cc9a57 100644 --- a/tests/e2e/utils/mock-requests.js +++ b/tests/e2e/utils/mock-requests.js @@ -1111,4 +1111,13 @@ export default class MockRequests { [ 'GET' ] ); } + + async fulfillAssetsSuggestions( payload, status = 200 ) { + await this.fulfillRequest( + /\/wc\/gla\/assets\/suggestions\b/, + payload, + status, + [ 'GET' ] + ); + } } From f11217adadeaa1827fbadf6fa476191fed70f5a1 Mon Sep 17 00:00:00 2001 From: Ankit Gade Date: Tue, 20 Jan 2026 20:29:20 +0530 Subject: [PATCH 059/123] Add docblock. --- tests/e2e/utils/mock-requests.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/e2e/utils/mock-requests.js b/tests/e2e/utils/mock-requests.js index 4ff0cc9a57..6501db3f2d 100644 --- a/tests/e2e/utils/mock-requests.js +++ b/tests/e2e/utils/mock-requests.js @@ -1112,6 +1112,13 @@ export default class MockRequests { ); } + /** + * Mocks the API request for asset suggestions. + * + * @param {Object} payload - The mock response payload to return. + * @param {number} [status=200] - The HTTP status code to return. + * @return {Promise} Resolves when the mock request has been fulfilled. + */ async fulfillAssetsSuggestions( payload, status = 200 ) { await this.fulfillRequest( /\/wc\/gla\/assets\/suggestions\b/, From 781cac9d4d4984b5a30a01848f91989e7990545a Mon Sep 17 00:00:00 2001 From: asvinb Date: Wed, 21 Jan 2026 13:38:33 +0400 Subject: [PATCH 060/123] refactor(paid-ads): extract GenAI asset fetch and trigger on change --- .../paid-ads/campaign-assets-form.js | 70 ++++++++++++------- 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/js/src/components/paid-ads/campaign-assets-form.js b/js/src/components/paid-ads/campaign-assets-form.js index 5a1ca039a4..c9e1cb6fde 100644 --- a/js/src/components/paid-ads/campaign-assets-form.js +++ b/js/src/components/paid-ads/campaign-assets-form.js @@ -240,6 +240,43 @@ export default function CampaignAssetsForm( { const assetGroupErrors = validateAssetGroup( formContext.values ); const finalUrl = assetEntityGroup?.[ ASSET_GROUP_KEY.FINAL_URL ]; + const fetchGenAIAssets = async ( url, assetGroupValues ) => { + try { + setIsFetchingGenAIAssets( true ); + const { data: textAssetsData } = + await fetchGenAITextAssets( url ); + const { data: mediaAssetsData } = + await fetchGenAIMediaAssets( url ); + + const hasSuggestedTextAssets = hasValidAIGeneratedAssets( + REQUIRED_TEXT_ASSET_KEYS, + textAssetsData + ); + + const hasSuggestedMediaAssets = hasValidAIGeneratedAssets( + REQUIRED_MEDIA_ASSET_KEYS, + mediaAssetsData + ); + + const nextValues = { + ...( hasSuggestedTextAssets ? textAssetsData : {} ), + ...( hasSuggestedMediaAssets ? mediaAssetsData : {} ), + }; + + if ( Object.keys( nextValues ).length ) { + formContext.setValues( { + ...assetGroupValues, + ...nextValues, + } ); + } + + setHasAISuggestedTextAssets( hasSuggestedTextAssets ); + setHasAISuggestedMediaAssets( hasSuggestedMediaAssets ); + } finally { + setIsFetchingGenAIAssets( false ); + } + }; + return { countryCodes, budgetRecommendation: selectedBudgetRecommendation, @@ -280,35 +317,20 @@ export default function CampaignAssetsForm( { setBaseAssetGroup( nextAssetGroup ); setHasAISuggestedTextAssets( false ); setHasAISuggestedMediaAssets( false ); + + if ( nextAssetGroup.final_url ) { + fetchGenAIAssets( + nextAssetGroup.final_url, + updatedContextValues + ); + } + formContext.adapter.hideValidation(); }, isFetchingGenAIAssets, hasAISuggestedTextAssets, hasAISuggestedMediaAssets, - async fetchGenAIAssets() { - try { - setIsFetchingGenAIAssets( true ); - const { data: textAssetsData } = - await fetchGenAITextAssets( finalUrl ); - const { data: mediaAssetsData } = - await fetchGenAIMediaAssets( finalUrl ); - - const hasSuggestedTextAssets = hasValidAIGeneratedAssets( - REQUIRED_TEXT_ASSET_KEYS, - textAssetsData - ); - - const hasSuggestedMediaAssets = hasValidAIGeneratedAssets( - REQUIRED_MEDIA_ASSET_KEYS, - mediaAssetsData - ); - - setHasAISuggestedTextAssets( hasSuggestedTextAssets ); - setHasAISuggestedMediaAssets( hasSuggestedMediaAssets ); - } finally { - setIsFetchingGenAIAssets( false ); - } - }, + fetchGenAIAssets, }; }; From de395f7109519cf1c6c4d1d6ade7d90a3eb5917c Mon Sep 17 00:00:00 2001 From: asvinb Date: Wed, 21 Jan 2026 16:32:42 +0400 Subject: [PATCH 061/123] refactor(asset-group): Extract asset slot filling utility --- .../asset-group-editor/texts-editor.js | 65 ++----------------- .../utils/fill-empty-asset-slots.js | 51 +++++++++++++++ .../utils/fill-empty-asset-slots.test.js | 46 +++++++++++++ 3 files changed, 102 insertions(+), 60 deletions(-) create mode 100644 js/src/components/paid-ads/asset-group/asset-group-editor/utils/fill-empty-asset-slots.js create mode 100644 js/src/components/paid-ads/asset-group/asset-group-editor/utils/fill-empty-asset-slots.test.js diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js b/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js index 2d1f7deabc..e3474c246b 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js @@ -16,6 +16,7 @@ import AppInputControl from '~/components/app-input-control'; import AssetItemActionButton, { ACTION_TYPES, } from './asset-item-action-button'; +import fillEmptyAssetSlots from './utils/fill-empty-asset-slots'; import './texts-editor.scss'; function normalizeNumberOfTexts( texts, minNumberOfTexts, maxNumberOfTexts ) { @@ -28,61 +29,6 @@ function normalizeNumberOfTexts( texts, minNumberOfTexts, maxNumberOfTexts ) { return texts.concat( supplement ).slice( ...sliceArgs ); } -/** - * Result returned by fillEmptyAssetSlotsWithUniqueValues. - * - * @typedef {Object} FillEmptyAssetSlotsResult - * @property {string[]} assets Updated asset list. - * @property {number} updatedCount Number of empty ("") slots that were filled. - */ - -/** - * Fill empty asset slots (represented by empty strings "") with unique - * generated values. - * - * Existing non-empty values are preserved. - * Empty slots that cannot be filled remain as "". - * - * @param {string[]} currentAssets Current asset values, where "" represents an empty slot. - * @param {string[]} generatedAssets Newly generated candidate asset values. - * - * @return {FillEmptyAssetSlotsResult} Result containing updated assets and count of filled slots. - */ -export function fillEmptyAssetSlotsWithUniqueValues( - currentAssets, - generatedAssets -) { - const existingAssetValues = new Set( currentAssets.filter( Boolean ) ); - - let generatedIndex = 0; - let updatedCount = 0; - - const assets = currentAssets.map( ( assetValue ) => { - if ( assetValue !== '' ) { - return assetValue; - } - - while ( - generatedIndex < generatedAssets.length && - existingAssetValues.has( generatedAssets[ generatedIndex ] ) - ) { - generatedIndex++; - } - - if ( generatedIndex < generatedAssets.length ) { - const nextGeneratedValue = generatedAssets[ generatedIndex ]; - existingAssetValues.add( nextGeneratedValue ); - generatedIndex++; - updatedCount++; - return nextGeneratedValue; - } - - return ''; - } ); - - return { assets, updatedCount }; -} - /** * Renders a list of text inputs for managing the single type of asset texts. * @@ -166,11 +112,10 @@ export default function TextsEditor( { const response = await fetchGenAITextAssets( finalUrl, assetKey ); const generatedTextAssets = response?.data?.[ assetKey ] ?? []; - const { assets: updatedTexts, updatedCount } = - fillEmptyAssetSlotsWithUniqueValues( - texts, - generatedTextAssets - ); + const { assets: updatedTexts, updatedCount } = fillEmptyAssetSlots( + texts, + generatedTextAssets + ); if ( updatedCount > 0 ) { updateTexts( updatedTexts ); diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/utils/fill-empty-asset-slots.js b/js/src/components/paid-ads/asset-group/asset-group-editor/utils/fill-empty-asset-slots.js new file mode 100644 index 0000000000..0b96eb7e44 --- /dev/null +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/utils/fill-empty-asset-slots.js @@ -0,0 +1,51 @@ +/** + * Result returned by fillEmptyAssetSlots. + * + * @typedef {Object} FillEmptyAssetSlotsResult + * @property {string[]} assets Updated asset list. + * @property {number} updatedCount Number of empty ("") slots that were filled. + */ + +/** + * Fill empty asset slots (represented by empty strings "") with unique + * generated values. + * + * Existing non-empty values are preserved. + * Empty slots that cannot be filled remain as "". + * + * @param {string[]} currentAssets Current asset values, where "" represents an empty slot. + * @param {string[]} generatedAssets Newly generated candidate asset values. + * + * @return {FillEmptyAssetSlotsResult} Result containing updated assets and count of filled slots. + */ +export default function fillEmptyAssetSlots( currentAssets, generatedAssets ) { + const existingAssetValues = new Set( currentAssets.filter( Boolean ) ); + + let generatedIndex = 0; + let updatedCount = 0; + + const assets = currentAssets.map( ( assetValue ) => { + if ( assetValue !== '' ) { + return assetValue; + } + + while ( + generatedIndex < generatedAssets.length && + existingAssetValues.has( generatedAssets[ generatedIndex ] ) + ) { + generatedIndex++; + } + + if ( generatedIndex < generatedAssets.length ) { + const nextGeneratedValue = generatedAssets[ generatedIndex ]; + existingAssetValues.add( nextGeneratedValue ); + generatedIndex++; + updatedCount++; + return nextGeneratedValue; + } + + return ''; + } ); + + return { assets, updatedCount }; +} diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/utils/fill-empty-asset-slots.test.js b/js/src/components/paid-ads/asset-group/asset-group-editor/utils/fill-empty-asset-slots.test.js new file mode 100644 index 0000000000..2bd58a6862 --- /dev/null +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/utils/fill-empty-asset-slots.test.js @@ -0,0 +1,46 @@ +/** + * Internal dependencies + */ +import fillEmptyAssetSlots from './fill-empty-asset-slots'; + +describe( 'fillEmptyAssetSlots', () => { + it( 'fills empty slots with generated values preserving non-empty values', () => { + const current = [ 'a', '', 'b', '' ]; + const generated = [ 'x', 'y' ]; + const result = fillEmptyAssetSlots( current, generated ); + expect( result.assets ).toEqual( [ 'a', 'x', 'b', 'y' ] ); + expect( result.updatedCount ).toBe( 2 ); + } ); + + it( 'skips generated values that duplicate existing assets and duplicates in generated list', () => { + const current = [ 'a', '', '', 'b' ]; + const generated = [ 'a', 'c', 'c', 'd' ]; + const result = fillEmptyAssetSlots( current, generated ); + expect( result.assets ).toEqual( [ 'a', 'c', 'd', 'b' ] ); + expect( result.updatedCount ).toBe( 2 ); + } ); + + it( 'leaves slots empty when not enough unique generated assets', () => { + const current = [ '', '', '' ]; + const generated = [ 'a' ]; + const result = fillEmptyAssetSlots( current, generated ); + expect( result.assets ).toEqual( [ 'a', '', '' ] ); + expect( result.updatedCount ).toBe( 1 ); + } ); + + it( 'returns unchanged when there are no empty slots', () => { + const current = [ 'a', 'b' ]; + const generated = [ 'x', 'y' ]; + const result = fillEmptyAssetSlots( current, generated ); + expect( result.assets ).toEqual( [ 'a', 'b' ] ); + expect( result.updatedCount ).toBe( 0 ); + } ); + + it( 'does not reuse the same generated value when it appears multiple times in generatedAssets', () => { + const current = [ '', '' ]; + const generated = [ 'x', 'x' ]; + const result = fillEmptyAssetSlots( current, generated ); + expect( result.assets ).toEqual( [ 'x', '' ] ); + expect( result.updatedCount ).toBe( 1 ); + } ); +} ); From 3b05c76c7beed51a29acf8c788cb8149b9d3673b Mon Sep 17 00:00:00 2001 From: Ankit Gade Date: Wed, 21 Jan 2026 18:48:19 +0530 Subject: [PATCH 062/123] Fix: Select another URL triggers homepage url selection. --- .../asset-group-header/assets-loader.js | 46 ++++++++++++------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/js/src/components/paid-ads/asset-group/asset-group-header/assets-loader.js b/js/src/components/paid-ads/asset-group/asset-group-header/assets-loader.js index d28d1e7562..f340de7d49 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-header/assets-loader.js +++ b/js/src/components/paid-ads/asset-group/asset-group-header/assets-loader.js @@ -4,7 +4,7 @@ import { __ } from '@wordpress/i18n'; import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; -import { useState, useRef, useEffect } from '@wordpress/element'; +import { useState, useRef, useEffect, useCallback } from '@wordpress/element'; import { Spinner } from '@woocommerce/components'; /** @@ -16,6 +16,8 @@ import SearchableSelectControl from '~/components/searchable-select-control'; import { API_NAMESPACE } from '~/data/constants'; import './assets-loader.scss'; +let HAS_LOADED_HOMEPAGE_ASSETS = false; + /** * @typedef {import('~/data/types.js').SuggestedAssets} SuggestedAssets */ @@ -105,24 +107,34 @@ export default function AssetsLoader( { onAssetsLoaded } ) { const [ fetching, setFetching ] = useState( false ); const { createNotice } = useDispatchCoreNotices(); - const loadSuggestedAssets = async ( { id, type } ) => { - setFetching( true ); - try { - const assets = await fetchSuggestedAssets( id, type ); - onAssetsLoaded( assets ); - } catch ( error ) { - setFetching( false ); - createNotice( - 'error', - __( 'Unable to load assets data.', 'google-listings-and-ads' ) - ); - } - }; + const loadSuggestedAssets = useCallback( + async ( { id, type } ) => { + setFetching( true ); + try { + const assets = await fetchSuggestedAssets( id, type ); + onAssetsLoaded( assets ); + } catch ( error ) { + setFetching( false ); + createNotice( + 'error', + __( + 'Unable to load assets data.', + 'google-listings-and-ads' + ) + ); + } + }, + [ onAssetsLoaded, createNotice ] + ); useEffect( () => { - loadSuggestedAssets( { id: -1, type: 'homepage' } ); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [] ); + if ( HAS_LOADED_HOMEPAGE_ASSETS ) { + return; + } + + HAS_LOADED_HOMEPAGE_ASSETS = true; + loadSuggestedAssets( { id: 0, type: 'homepage' } ); + }, [ loadSuggestedAssets ] ); // To have the searching state and keep the entered search value, this handler needs to // be called immediately after keying values. Therefore, it also needs to implement the From 1c4060a54237f0d75c651122a0ae1623e4e275cd Mon Sep 17 00:00:00 2001 From: Ankit Gade Date: Thu, 22 Jan 2026 15:17:32 +0530 Subject: [PATCH 063/123] Update composer lock. --- composer.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/composer.lock b/composer.lock index 526d4be5f4..669add3c5e 100644 --- a/composer.lock +++ b/composer.lock @@ -441,12 +441,12 @@ "source": { "type": "git", "url": "https://github.com/googleads/google-ads-php.git", - "reference": "73a755b69f8088a22da27fd0124e2a2d43ec7b82" + "reference": "21ca3b959893a027145fa72b4c882ecd463e2de3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/googleads/google-ads-php/zipball/73a755b69f8088a22da27fd0124e2a2d43ec7b82", - "reference": "73a755b69f8088a22da27fd0124e2a2d43ec7b82", + "url": "https://api.github.com/repos/googleads/google-ads-php/zipball/21ca3b959893a027145fa72b4c882ecd463e2de3", + "reference": "21ca3b959893a027145fa72b4c882ecd463e2de3", "shasum": "" }, "require": { @@ -497,7 +497,7 @@ "issues": "https://github.com/googleads/google-ads-php/issues", "source": "https://github.com/googleads/google-ads-php/tree/legacy-v31.1.0" }, - "time": "2026-01-09T20:17:46+00:00" + "time": "2026-01-21T18:06:16+00:00" }, { "name": "guzzlehttp/guzzle", @@ -5151,16 +5151,16 @@ }, { "name": "wp-cli/php-cli-tools", - "version": "v0.12.6", + "version": "v0.12.7", "source": { "type": "git", "url": "https://github.com/wp-cli/php-cli-tools.git", - "reference": "f12b650d3738e471baed6dd47982d53c5c0ab1c3" + "reference": "5cc6ef2e93cfcd939813eb420ae23bc116d9be2a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wp-cli/php-cli-tools/zipball/f12b650d3738e471baed6dd47982d53c5c0ab1c3", - "reference": "f12b650d3738e471baed6dd47982d53c5c0ab1c3", + "url": "https://api.github.com/repos/wp-cli/php-cli-tools/zipball/5cc6ef2e93cfcd939813eb420ae23bc116d9be2a", + "reference": "5cc6ef2e93cfcd939813eb420ae23bc116d9be2a", "shasum": "" }, "require": { @@ -5173,7 +5173,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "0.12.x-dev" + "dev-main": "0.12.x-dev" } }, "autoload": { @@ -5208,9 +5208,9 @@ ], "support": { "issues": "https://github.com/wp-cli/php-cli-tools/issues", - "source": "https://github.com/wp-cli/php-cli-tools/tree/v0.12.6" + "source": "https://github.com/wp-cli/php-cli-tools/tree/v0.12.7" }, - "time": "2025-09-11T12:43:04+00:00" + "time": "2026-01-20T20:31:49+00:00" }, { "name": "wp-cli/wp-cli", From f04eb6d171c5ba2354927bde5adc8b28f3f442b6 Mon Sep 17 00:00:00 2001 From: James Morrison Date: Thu, 22 Jan 2026 10:39:41 +0000 Subject: [PATCH 064/123] PHPCS fixes. --- src/API/Site/Controllers/Ads/SetupCompleteController.php | 2 +- src/API/Site/Controllers/DisconnectController.php | 2 +- .../Controllers/MerchantCenter/SettingsSyncController.php | 2 +- .../MerchantCenter/SyncableProductsCountController.php | 2 +- src/Coupon/WCCouponAdapter.php | 2 +- tests/Unit/API/ClientTest.php | 4 ++-- tests/Unit/API/Google/MiddlewareTest.php | 2 +- tests/Unit/Product/ProductFilterTest.php | 4 ++-- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/API/Site/Controllers/Ads/SetupCompleteController.php b/src/API/Site/Controllers/Ads/SetupCompleteController.php index e6585d59a6..6f67a0b426 100644 --- a/src/API/Site/Controllers/Ads/SetupCompleteController.php +++ b/src/API/Site/Controllers/Ads/SetupCompleteController.php @@ -62,7 +62,7 @@ public function register_routes() { * @return callable */ protected function get_setup_complete_callback(): callable { - return function ( Request $request ) { + return function ( Request $request ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found do_action( 'woocommerce_gla_ads_setup_completed' ); /** diff --git a/src/API/Site/Controllers/DisconnectController.php b/src/API/Site/Controllers/DisconnectController.php index c0d48efc60..78cc20f7eb 100644 --- a/src/API/Site/Controllers/DisconnectController.php +++ b/src/API/Site/Controllers/DisconnectController.php @@ -40,7 +40,7 @@ public function register_routes() { * @return callable */ protected function get_disconnect_callback(): callable { - return function ( Request $request ) { + return function ( Request $request ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found $endpoints = [ 'ads/connection', 'mc/connection', diff --git a/src/API/Site/Controllers/MerchantCenter/SettingsSyncController.php b/src/API/Site/Controllers/MerchantCenter/SettingsSyncController.php index a49c7b82b0..5784a6c350 100644 --- a/src/API/Site/Controllers/MerchantCenter/SettingsSyncController.php +++ b/src/API/Site/Controllers/MerchantCenter/SettingsSyncController.php @@ -61,7 +61,7 @@ public function register_routes() { * @return callable */ protected function get_sync_endpoint_callback(): callable { - return function ( Request $request ) { + return function ( Request $request ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found try { $this->settings->sync_taxes(); $this->settings->sync_shipping(); diff --git a/src/API/Site/Controllers/MerchantCenter/SyncableProductsCountController.php b/src/API/Site/Controllers/MerchantCenter/SyncableProductsCountController.php index ccf4e03798..889d96ca20 100644 --- a/src/API/Site/Controllers/MerchantCenter/SyncableProductsCountController.php +++ b/src/API/Site/Controllers/MerchantCenter/SyncableProductsCountController.php @@ -86,7 +86,7 @@ protected function get_syncable_products_count_callback(): callable { * @return callable */ protected function update_syncable_products_count_callback(): callable { - return function ( Request $request ) { + return function ( Request $request ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found $this->options->delete( OptionsInterface::SYNCABLE_PRODUCTS_COUNT ); $this->options->delete( OptionsInterface::SYNCABLE_PRODUCTS_COUNT_INTERMEDIATE_DATA ); diff --git a/src/Coupon/WCCouponAdapter.php b/src/Coupon/WCCouponAdapter.php index 9851f26534..cc1a8a7b67 100644 --- a/src/Coupon/WCCouponAdapter.php +++ b/src/Coupon/WCCouponAdapter.php @@ -420,7 +420,7 @@ private function get_product_ids_in_brand( WC_Coupon $wc_coupon, bool $is_exclud $meta_key = $is_exclude ? 'exclude_product_brands' : 'product_brands'; // Get the brand term IDs if brand restriction is set. - $brand_term_ids = get_post_meta( $coupon_id, $meta_key ); + $brand_term_ids = get_post_meta( $coupon_id, $meta_key, true ); if ( ! is_array( $brand_term_ids ) ) { return []; diff --git a/tests/Unit/API/ClientTest.php b/tests/Unit/API/ClientTest.php index 732329e785..62f5a11a4f 100644 --- a/tests/Unit/API/ClientTest.php +++ b/tests/Unit/API/ClientTest.php @@ -206,7 +206,7 @@ public function test_add_auth_header() { $this->note->expects( $this->once() )->method( 'delete' ); $this->invoke_handler( 'add_auth_header' )( - function ( $request, $options ) { + function ( $request, $options ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed $this->assertStringStartsWith( 'X_JP_Auth token=', $request->getHeader( 'Authorization' )[0] ); } )( $request, [] ); @@ -238,7 +238,7 @@ public function test_plugin_version_headers(): void { $request = new Request( 'GET', 'https://testing.local' ); $this->invoke_handler( 'add_plugin_version_header' )( - function ( $request, $options ) { + function ( $request, $options ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed $this->assertEquals( $this->get_client_name(), $request->getHeader( 'x-client-name' )[0] ); $this->assertEquals( $this->get_version(), $request->getHeader( 'x-client-version' )[0] ); } diff --git a/tests/Unit/API/Google/MiddlewareTest.php b/tests/Unit/API/Google/MiddlewareTest.php index 51f2b4b8fe..a9f52877dd 100644 --- a/tests/Unit/API/Google/MiddlewareTest.php +++ b/tests/Unit/API/Google/MiddlewareTest.php @@ -427,7 +427,7 @@ public function test_get_sdi_merchant_update_endpoint() { public function test_get_sdi_merchant_update_endpoint_with_site_url_having_path() { add_filter( 'woocommerce_gla_site_url', - function ( $home_url ) { + function ( $home_url ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found return 'http://example.org/shop'; } ); diff --git a/tests/Unit/Product/ProductFilterTest.php b/tests/Unit/Product/ProductFilterTest.php index 851270bfd0..7e6e727140 100644 --- a/tests/Unit/Product/ProductFilterTest.php +++ b/tests/Unit/Product/ProductFilterTest.php @@ -79,7 +79,7 @@ public function test_filter_sync_ready_products_with_no_filters_but_failed_sync( public function test_filter_sync_ready_products_with_pre_filter() { add_filter( 'woocommerce_gla_get_sync_ready_products_pre_filter', - function ( $products ) { + function ( $products ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found return []; } ); @@ -96,7 +96,7 @@ function ( $products ) { public function test_filter_sync_ready_products_with_post_filter() { add_filter( 'woocommerce_gla_get_sync_ready_products_filter', - function ( $products ) { + function ( $products ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found return []; } ); From 13bdf4aac0b44a8ae9d1d191f7438c457e4ee0a0 Mon Sep 17 00:00:00 2001 From: James Morrison Date: Thu, 22 Jan 2026 10:40:35 +0000 Subject: [PATCH 065/123] PHPCS fixes. --- tests/Unit/Coupon/WCCouponAdapterTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Unit/Coupon/WCCouponAdapterTest.php b/tests/Unit/Coupon/WCCouponAdapterTest.php index e8e0932aef..d00adc985c 100644 --- a/tests/Unit/Coupon/WCCouponAdapterTest.php +++ b/tests/Unit/Coupon/WCCouponAdapterTest.php @@ -280,13 +280,13 @@ public function test_brand_restrictions() { $coupon->set_product_ids( [ $product_3_id ] ); // Include brand 1 (product 1 and 2) for the coupon. - update_post_meta( $coupon->get_id(), 'product_brands', $brand_1['term_id'] ); + update_post_meta( $coupon->get_id(), 'product_brands', [ $brand_1['term_id'] ] ); // Exclude product 2 for the coupon. $coupon->set_excluded_product_ids( [ $product_2_id ] ); // Exclude brand 2 (product 3) for the coupon. - update_post_meta( $coupon->get_id(), 'exclude_product_brands', $brand_2['term_id'] ); + update_post_meta( $coupon->get_id(), 'exclude_product_brands', [ $brand_2['term_id'] ] ); $coupon->save(); From 61b8ca1e5f7eeb9caef9ba634fc97c84cffe5ed0 Mon Sep 17 00:00:00 2001 From: James Morrison Date: Thu, 22 Jan 2026 10:52:21 +0000 Subject: [PATCH 066/123] Composer update. --- composer.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.lock b/composer.lock index 669add3c5e..76f49a8b7b 100644 --- a/composer.lock +++ b/composer.lock @@ -5422,9 +5422,9 @@ "php": ">=7.4", "ext-json": "*" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "7.4.30" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.9.0" } From 220584a5f836445a60167e51d41c5b7ac56fbf8d Mon Sep 17 00:00:00 2001 From: asvinb Date: Thu, 22 Jan 2026 16:05:09 +0400 Subject: [PATCH 067/123] refactor(paid-ads): Refine asset generation logic and tests --- .../paid-ads/campaign-assets-form.js | 1 - .../add-paid-campaigns.test.js | 145 ++++-------------- tests/e2e/utils/pages/create-campaign.js | 2 +- 3 files changed, 32 insertions(+), 116 deletions(-) diff --git a/js/src/components/paid-ads/campaign-assets-form.js b/js/src/components/paid-ads/campaign-assets-form.js index c9e1cb6fde..4822b38fa2 100644 --- a/js/src/components/paid-ads/campaign-assets-form.js +++ b/js/src/components/paid-ads/campaign-assets-form.js @@ -260,7 +260,6 @@ export default function CampaignAssetsForm( { const nextValues = { ...( hasSuggestedTextAssets ? textAssetsData : {} ), - ...( hasSuggestedMediaAssets ? mediaAssetsData : {} ), }; if ( Object.keys( nextValues ).length ) { diff --git a/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js b/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js index b6bd4b2922..a35355db17 100644 --- a/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js +++ b/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js @@ -537,6 +537,8 @@ test.describe( 'Add paid campaign', () => { await setCompletedAdsSetup(); await createCampaignPage.mockRequests(); await createCampaignPage.mockOptimizeCampaignRequests(); + await createCampaignPage.mockGenerateTextAssetsSuccess(); + await createCampaignPage.mockGenerateImageAssetsSuccess(); createCampaignPage.goto(); } ); @@ -617,34 +619,20 @@ test.describe( 'Add paid campaign', () => { } ); test( 'Generate headline button is visible when at least one input is empty', async () => { - const addHeadlineButton = - createCampaignPage.getAddHeadlineButton(); - await addHeadlineButton.click(); - const generateHeadlineButton = createCampaignPage.getGenerateHeadlineButton(); await expect( generateHeadlineButton - ).toBeVisible(); - - const generateHeadlinesButton = - createCampaignPage.getGenerateHeadlinesButton(); - await expect( - generateHeadlinesButton ).not.toBeVisible(); - const headlineInputsValues = - await createCampaignPage.getHeadlineInputsValues(); + const headlineInputs = + await createCampaignPage.getHeadlineInputs(); + const lastHeadlineInput = headlineInputs.last(); + await lastHeadlineInput.fill( '' ); await expect( - headlineInputsValues - ).toHaveLength( 4 ); - - const lastValue = - headlineInputsValues[ - headlineInputsValues.length - 1 - ]; - expect( lastValue ).toBe( '' ); + generateHeadlineButton + ).toBeVisible(); } ); } ); @@ -676,7 +664,7 @@ test.describe( 'Add paid campaign', () => { headlineInputsValues.length - 1 ]; expect( lastValue ).toBe( - 'Shop the Latest Deals' + 'Fast Shipping Available' ); } ); } ); @@ -693,34 +681,21 @@ test.describe( 'Add paid campaign', () => { } ); test( 'Generate long headline button is visible when at least one input is empty', async () => { - const addLongHeadlineButton = - createCampaignPage.getAddLongHeadlineButton(); - await addLongHeadlineButton.click(); - const generateLongHeadlineButton = createCampaignPage.getGenerateLongHeadlineButton(); await expect( generateLongHeadlineButton - ).toBeVisible(); - - const generateLongHeadlinesButton = - createCampaignPage.getGenerateLongHeadlinesButton(); - await expect( - generateLongHeadlinesButton ).not.toBeVisible(); - const longHeadlineInputsValues = - await createCampaignPage.getLongHeadlineInputsValues(); + const longHeadlineInputs = + await createCampaignPage.getLongHeadlineInputs(); + const lastLongHeadlineInput = + longHeadlineInputs.last(); + await lastLongHeadlineInput.fill( '' ); await expect( - longHeadlineInputsValues - ).toHaveLength( 2 ); - - const lastValue = - longHeadlineInputsValues[ - longHeadlineInputsValues.length - 1 - ]; - expect( lastValue ).toBe( '' ); + generateLongHeadlineButton + ).toBeVisible(); } ); } ); @@ -752,7 +727,7 @@ test.describe( 'Add paid campaign', () => { longHeadlineInputsValues.length - 1 ]; expect( lastValue ).toBe( - 'Discover quality products at great prices' + 'Smart shopping starts right here' ); } ); } ); @@ -769,33 +744,21 @@ test.describe( 'Add paid campaign', () => { } ); test( 'Generate description button is visible when at least one input is empty', async () => { - const addDescriptionButton = - createCampaignPage.getAddDescriptionButton(); - await addDescriptionButton.click(); - const generateDescriptionButton = createCampaignPage.getGenerateDescriptionButton(); await expect( generateDescriptionButton - ).toBeVisible(); - - const generateDescriptionsButton = - createCampaignPage.getGenerateDescriptionsButton(); - await expect( - generateDescriptionsButton ).not.toBeVisible(); - const descriptionInputsValues = - await createCampaignPage.getDescriptionInputsValues(); - await expect( - descriptionInputsValues - ).toHaveLength( 3 ); + const descriptionInputs = + await createCampaignPage.getDescriptionInputs(); + const lastDescriptionInput = + descriptionInputs.last(); + await lastDescriptionInput.fill( '' ); - const lastValue = - descriptionInputsValues[ - descriptionInputsValues.length - 1 - ]; - expect( lastValue ).toBe( '' ); + await expect( + generateDescriptionButton + ).toBeVisible(); } ); } ); @@ -827,7 +790,7 @@ test.describe( 'Add paid campaign', () => { descriptionInputsValues.length - 1 ]; expect( lastValue ).toBe( - 'Browse top picks and enjoy exclusive savings.' + 'Quality products backed by great support.' ); } ); } ); @@ -862,9 +825,11 @@ test.describe( 'Add paid campaign', () => { } ); test( 'Displays error message when there are no more generated text', async () => { - const addDescriptionButton = - createCampaignPage.getAddDescriptionButton(); - await addDescriptionButton.click(); + const descriptionInputs = + await createCampaignPage.getDescriptionInputs(); + const lastDescriptionInput = + descriptionInputs.last(); + await lastDescriptionInput.fill( '' ); const generateDescriptionButton = createCampaignPage.getGenerateDescriptionButton(); @@ -930,22 +895,6 @@ test.describe( 'Add paid campaign', () => { ); } ); } ); - - test.describe( 'No generated assets', () => { - test.beforeEach( async () => { - createCampaignPage.mockEmptyGenerateImageAssets(); - } ); - - test( 'Hides the image picker if there are no generated images', async () => { - const generateLandscapeImagesButton = - createCampaignPage.getGenerateLandscapeImagesButton(); - await generateLandscapeImagesButton.click(); - - const imagePicker = - createCampaignPage.getLandscapeImagesSectionImagePicker(); - await expect( imagePicker ).not.toBeVisible(); - } ); - } ); } ); test.describe( 'Image Picker', () => { @@ -1106,22 +1055,6 @@ test.describe( 'Add paid campaign', () => { await expect( generatedImages ).toHaveCount( 3 ); } ); } ); - - test.describe( 'No generated assets', () => { - test.beforeEach( async () => { - createCampaignPage.mockEmptyGenerateImageAssets(); - } ); - - test( 'Hides the image picker if there are no generated images', async () => { - const generateSquareImagesButton = - createCampaignPage.getGenerateSquareImagesButton(); - await generateSquareImagesButton.click(); - - const imagePicker = - createCampaignPage.getSquareImagesSectionImagePicker(); - await expect( imagePicker ).not.toBeVisible(); - } ); - } ); } ); test.describe( 'Portrait images', () => { @@ -1173,22 +1106,6 @@ test.describe( 'Add paid campaign', () => { await expect( generatedImages ).toHaveCount( 2 ); } ); } ); - - test.describe( 'No generated assets', () => { - test.beforeEach( async () => { - createCampaignPage.mockEmptyGenerateImageAssets(); - } ); - - test( 'Hides the image picker if there are no generated images', async () => { - const generatePortraitImagesButton = - createCampaignPage.getGeneratePortraitImagesButton(); - await generatePortraitImagesButton.click(); - - const imagePicker = - createCampaignPage.getPortraitImagesSectionImagePicker(); - await expect( imagePicker ).not.toBeVisible(); - } ); - } ); } ); } ); } ); diff --git a/tests/e2e/utils/pages/create-campaign.js b/tests/e2e/utils/pages/create-campaign.js index 1ce6773beb..9495a57fef 100644 --- a/tests/e2e/utils/pages/create-campaign.js +++ b/tests/e2e/utils/pages/create-campaign.js @@ -625,7 +625,7 @@ export default class CreateCampaignPage extends MockRequests { items: [ // Headlines { - text: 'Shop the Latest Deals', + text: 'Latest Deals', type: 'headline', }, { From 8b849fa4a8990a46f28fe1c0991378da406d0004 Mon Sep 17 00:00:00 2001 From: asvinb Date: Thu, 22 Jan 2026 16:11:16 +0400 Subject: [PATCH 068/123] build: Update commons.js maxSize --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1a8ae93de2..e364c39f94 100644 --- a/package.json +++ b/package.json @@ -153,7 +153,7 @@ }, { "path": "./js/build/commons.js", - "maxSize": "63.6 kB" + "maxSize": "63.9 kB" }, { "path": "./js/build/vendors.js", From 228f378e7366eeedbc196c3e7b0c848bad94665f Mon Sep 17 00:00:00 2001 From: James Morrison Date: Thu, 22 Jan 2026 14:51:31 +0000 Subject: [PATCH 069/123] Merged 406 / feature/GOOWOO-406-create-an-AdsAssetGenerationService; set defaults, added missing type. --- .../Controllers/Ads/AssetGenerationController.php | 7 ++++++- .../Ads/AssetGenerationControllerTest.php | 14 +++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/API/Site/Controllers/Ads/AssetGenerationController.php b/src/API/Site/Controllers/Ads/AssetGenerationController.php index d541a8b78c..99b313cc57 100644 --- a/src/API/Site/Controllers/Ads/AssetGenerationController.php +++ b/src/API/Site/Controllers/Ads/AssetGenerationController.php @@ -80,12 +80,14 @@ protected function get_generate_text_params(): array { 'final_url' => [ 'description' => __( 'The final URL for asset generation', 'google-listings-and-ads' ), 'type' => 'string', + 'default' => '', 'sanitize_callback' => 'esc_url_raw', 'validate_callback' => 'rest_validate_request_arg', ], 'types' => [ 'description' => __( 'Asset types to generate', 'google-listings-and-ads' ), 'type' => 'array', + 'default' => [], 'items' => [ 'type' => 'string', 'enum' => [ @@ -112,18 +114,21 @@ protected function get_generate_images_params(): array { 'final_url' => [ 'description' => __( 'The final URL for asset generation', 'google-listings-and-ads' ), 'type' => 'string', + 'default' => '', 'sanitize_callback' => 'esc_url_raw', 'validate_callback' => 'rest_validate_request_arg', ], 'types' => [ 'description' => __( 'Asset types to generate', 'google-listings-and-ads' ), 'type' => 'array', + 'default' => [], 'items' => [ 'type' => 'string', 'enum' => [ AssetFieldType::MARKETING_IMAGE, AssetFieldType::SQUARE_MARKETING_IMAGE, AssetFieldType::PORTRAIT_MARKETING_IMAGE, + AssetFieldType::TALL_PORTRAIT_MARKETING_IMAGE, ], ], 'sanitize_callback' => function ( $types ) { @@ -143,7 +148,7 @@ protected function get_generate_text_callback(): callable { return function ( Request $request ) { try { $final_url = $request->get_param( 'final_url' ) ?: $this->get_site_url(); - $types = $request->get_param( 'types' ) ?: [ 'headline', 'long_headline', 'description' ]; + $types = $request->get_param( 'types' ) ?: []; // Call service with lowercase types. $items = $this->service->generate_text( diff --git a/tests/Unit/API/Site/Controllers/Ads/AssetGenerationControllerTest.php b/tests/Unit/API/Site/Controllers/Ads/AssetGenerationControllerTest.php index 873437b395..f8ba6dbddc 100644 --- a/tests/Unit/API/Site/Controllers/Ads/AssetGenerationControllerTest.php +++ b/tests/Unit/API/Site/Controllers/Ads/AssetGenerationControllerTest.php @@ -32,19 +32,27 @@ class AssetGenerationControllerTest extends RESTControllerUnitTest { public function setUp(): void { parent::setUp(); + // Mock the site URL filter to return TEST_SITE_URL. + add_filter( + 'woocommerce_gla_site_url', + function () { + return self::TEST_SITE_URL; + } + ); + $this->service = $this->createMock( AdsAssetGenerationService::class ); $this->controller = new AssetGenerationController( $this->server, $this->service ); $this->controller->register(); } public function test_generate_text_with_defaults() { - // Service expects lowercase types. + // Service expects empty array when no types provided (service handles defaults). $this->service->expects( $this->once() ) ->method( 'generate_text' ) ->with( [ 'final_url' => self::TEST_SITE_URL, - 'asset_field_types' => [ 'headline', 'long_headline', 'description' ], + 'asset_field_types' => [], ] ) ->willReturn( @@ -87,7 +95,7 @@ public function test_generate_text_with_custom_url() { ->with( [ 'final_url' => 'https://custom-url.com', - 'asset_field_types' => [ 'headline', 'long_headline', 'description' ], + 'asset_field_types' => [], ] ) ->willReturn( From cc679255bdc090c11fd52e0848e7e9cb7cc92038 Mon Sep 17 00:00:00 2001 From: James Morrison Date: Thu, 22 Jan 2026 15:27:21 +0000 Subject: [PATCH 070/123] Added 90 second timeout for AI generated content; tests timed out with default (30s) --- src/API/Site/Controllers/Ads/AssetGenerationController.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/API/Site/Controllers/Ads/AssetGenerationController.php b/src/API/Site/Controllers/Ads/AssetGenerationController.php index 99b313cc57..499636dd2a 100644 --- a/src/API/Site/Controllers/Ads/AssetGenerationController.php +++ b/src/API/Site/Controllers/Ads/AssetGenerationController.php @@ -146,6 +146,8 @@ protected function get_generate_images_params(): array { */ protected function get_generate_text_callback(): callable { return function ( Request $request ) { + set_time_limit( 90 ); // AI text generation can take time. + try { $final_url = $request->get_param( 'final_url' ) ?: $this->get_site_url(); $types = $request->get_param( 'types' ) ?: []; @@ -173,6 +175,8 @@ protected function get_generate_text_callback(): callable { */ protected function get_generate_images_callback(): callable { return function ( Request $request ) { + set_time_limit( 90 ); // AI image generation can take time. + try { $final_url = $request->get_param( 'final_url' ) ?: $this->get_site_url(); $types = $request->get_param( 'types' ) ?: []; From f118c84a05e7480e2c5a73c47634a2e4158bc09a Mon Sep 17 00:00:00 2001 From: Ankit Gade Date: Fri, 23 Jan 2026 15:01:39 +0530 Subject: [PATCH 071/123] Use the useRef in parent component. --- .../asset-group-header/assets-loader.js | 53 +++------------- .../asset-group-header/final-url-card.js | 61 +++++++++++++++++-- 2 files changed, 62 insertions(+), 52 deletions(-) diff --git a/js/src/components/paid-ads/asset-group/asset-group-header/assets-loader.js b/js/src/components/paid-ads/asset-group/asset-group-header/assets-loader.js index f340de7d49..983f83dcd0 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-header/assets-loader.js +++ b/js/src/components/paid-ads/asset-group/asset-group-header/assets-loader.js @@ -4,20 +4,17 @@ import { __ } from '@wordpress/i18n'; import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; -import { useState, useRef, useEffect, useCallback } from '@wordpress/element'; +import { useState, useRef } from '@wordpress/element'; import { Spinner } from '@woocommerce/components'; /** * Internal dependencies */ -import useDispatchCoreNotices from '~/hooks/useDispatchCoreNotices'; import AppButton from '~/components/app-button'; import SearchableSelectControl from '~/components/searchable-select-control'; import { API_NAMESPACE } from '~/data/constants'; import './assets-loader.scss'; -let HAS_LOADED_HOMEPAGE_ASSETS = false; - /** * @typedef {import('~/data/types.js').SuggestedAssets} SuggestedAssets */ @@ -34,12 +31,6 @@ function fetchFinalUrls( search ) { return apiFetch( { path: addQueryArgs( endPoint, query ) } ); } -function fetchSuggestedAssets( id, type ) { - const endPoint = `${ API_NAMESPACE }/assets/suggestions`; - const query = { id, type }; - return apiFetch( { path: addQueryArgs( endPoint, query ) } ); -} - function mapFinalUrlsToOptions( finalUrls, search ) { const options = finalUrls.map( ( finalUrl ) => ( { finalUrl, @@ -91,11 +82,12 @@ function mapFinalUrlsToOptions( finalUrls, search ) { * and then loading the suggested assets. * * @param {Object} props React props. - * @param {(suggestedAssets: SuggestedAssets) => void} props.onAssetsLoaded Callback function when the suggested assets are loaded. + * @param {boolean} props.isFetching Whether the assets are currently being fetched. + * @param {Function} props.loadSuggestedAssets Function to load suggested assets. * * @fires gla_import_assets_by_final_url_button_click */ -export default function AssetsLoader( { onAssetsLoaded } ) { +export default function AssetsLoader( { isFetching, loadSuggestedAssets } ) { const cacheRef = useRef( {} ); const latestSearchRef = useRef(); @@ -104,37 +96,6 @@ export default function AssetsLoader( { onAssetsLoaded } ) { // Ref: https://github.com/woocommerce/woocommerce/blob/6.9.0/packages/js/components/src/select-control/index.js#L137-L141 const [ selectedOptions, setSelectedOptions ] = useState( [] ); const [ searching, setSearching ] = useState( false ); - const [ fetching, setFetching ] = useState( false ); - const { createNotice } = useDispatchCoreNotices(); - - const loadSuggestedAssets = useCallback( - async ( { id, type } ) => { - setFetching( true ); - try { - const assets = await fetchSuggestedAssets( id, type ); - onAssetsLoaded( assets ); - } catch ( error ) { - setFetching( false ); - createNotice( - 'error', - __( - 'Unable to load assets data.', - 'google-listings-and-ads' - ) - ); - } - }, - [ onAssetsLoaded, createNotice ] - ); - - useEffect( () => { - if ( HAS_LOADED_HOMEPAGE_ASSETS ) { - return; - } - - HAS_LOADED_HOMEPAGE_ASSETS = true; - loadSuggestedAssets( { id: 0, type: 'homepage' } ); - }, [ loadSuggestedAssets ] ); // To have the searching state and keep the entered search value, this handler needs to // be called immediately after keying values. Therefore, it also needs to implement the @@ -204,7 +165,7 @@ export default function AssetsLoader( { onAssetsLoaded } ) { isSearchable hideBeforeSearch excludeSelectedOptions={ false } - disabled={ fetching } + disabled={ isFetching } options={ [] } // The actual options will be provided via the callback results of `onSearch`. selected={ selectedOptions } onSearch={ debouncedHandleSearch } @@ -214,12 +175,12 @@ export default function AssetsLoader( { onAssetsLoaded } ) { diff --git a/js/src/components/paid-ads/asset-group/asset-group-header/final-url-card.js b/js/src/components/paid-ads/asset-group/asset-group-header/final-url-card.js index 2a7076cb87..6fe6661b06 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-header/final-url-card.js +++ b/js/src/components/paid-ads/asset-group/asset-group-header/final-url-card.js @@ -1,9 +1,11 @@ /** * External dependencies */ +import apiFetch from '@wordpress/api-fetch'; import { __ } from '@wordpress/i18n'; +import { addQueryArgs } from '@wordpress/url'; import classnames from 'classnames'; -import { useState } from '@wordpress/element'; +import { useCallback, useEffect, useRef, useState } from '@wordpress/element'; import { ExternalLink } from '@wordpress/components'; /** @@ -14,7 +16,9 @@ import Section from '~/components/section'; import AccountCard, { APPEARANCE } from '~/components/account-card'; import AppButton from '~/components/app-button'; import AssetsLoader from './assets-loader'; +import { API_NAMESPACE } from '~/data/constants'; import './final-url-card.scss'; +import useDispatchCoreNotices from '~/hooks/useDispatchCoreNotices'; /** * @typedef {import('~/data/types.js').SuggestedAssets} SuggestedAssets @@ -26,6 +30,12 @@ import './final-url-card.scss'; * @event gla_reselect_another_final_url_button_click */ +function fetchSuggestedAssets( id, type ) { + const endPoint = `${ API_NAMESPACE }/assets/suggestions`; + const query = { id, type }; + return apiFetch( { path: addQueryArgs( endPoint, query ) } ); +} + /** * Renders the Card UI for managing the final URL and getting the suggested assets. * @@ -41,7 +51,10 @@ export default function FinalUrlCard( { initialFinalUrl, hideFooter = false, } ) { + const [ fetching, setFetching ] = useState( false ); const [ finalUrl, setFinalUrl ] = useState( initialFinalUrl || null ); + const didInitialLoadRef = useRef( false ); + const { createNotice } = useDispatchCoreNotices(); const description = finalUrl ? ( { finalUrl } @@ -52,10 +65,13 @@ export default function FinalUrlCard( { ) ); - const handleAssetsLoaded = ( suggestedAssets ) => { - setFinalUrl( suggestedAssets[ ASSET_GROUP_KEY.FINAL_URL ] ); - onAssetsChange( suggestedAssets ); - }; + const handleAssetsLoaded = useCallback( + ( suggestedAssets ) => { + setFinalUrl( suggestedAssets[ ASSET_GROUP_KEY.FINAL_URL ] ); + onAssetsChange( suggestedAssets ); + }, + [ onAssetsChange ] + ); const handleReselectClick = () => { setFinalUrl( null ); @@ -67,6 +83,36 @@ export default function FinalUrlCard( { 'gla-final-url-card--has-selected-url': finalUrl, } ); + const loadSuggestedAssets = useCallback( + async ( { id, type } ) => { + setFetching( true ); + try { + const assets = await fetchSuggestedAssets( id, type ); + handleAssetsLoaded( assets ); + } catch ( error ) { + createNotice( + 'error', + __( + 'Unable to load assets data.', + 'google-listings-and-ads' + ) + ); + } finally { + setFetching( false ); + } + }, + [ createNotice, handleAssetsLoaded ] + ); + + useEffect( () => { + if ( didInitialLoadRef.current ) { + return; + } + + didInitialLoadRef.current = true; + loadSuggestedAssets( { id: 0, type: 'homepage' } ); + }, [ loadSuggestedAssets ] ); + return ( ) : ( - + ) } From d95803b4d3fd231ee9651a215307654eda7e8a12 Mon Sep 17 00:00:00 2001 From: Ankit Gade Date: Fri, 23 Jan 2026 15:04:59 +0530 Subject: [PATCH 072/123] Remove redundant code --- tests/e2e/utils/mock-requests.js | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/tests/e2e/utils/mock-requests.js b/tests/e2e/utils/mock-requests.js index 39e217131e..ba6b549884 100644 --- a/tests/e2e/utils/mock-requests.js +++ b/tests/e2e/utils/mock-requests.js @@ -1213,20 +1213,4 @@ export default class MockRequests { [ 'POST' ] ); } - - /** - * Mocks the API request for asset suggestions. - * - * @param {Object} payload - The mock response payload to return. - * @param {number} [status=200] - The HTTP status code to return. - * @return {Promise} Resolves when the mock request has been fulfilled. - */ - async fulfillAssetsSuggestions( payload, status = 200 ) { - await this.fulfillRequest( - /\/wc\/gla\/assets\/suggestions\b/, - payload, - status, - [ 'GET' ] - ); - } } From 00594c842eef122050acb82570c7109512e8ef12 Mon Sep 17 00:00:00 2001 From: Ankit Gade Date: Fri, 23 Jan 2026 19:04:43 +0530 Subject: [PATCH 073/123] Fix: JS test. --- .../asset-group-header.test.js | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/js/src/components/paid-ads/asset-group/asset-group-header/asset-group-header.test.js b/js/src/components/paid-ads/asset-group/asset-group-header/asset-group-header.test.js index 571a903773..cdb563c942 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-header/asset-group-header.test.js +++ b/js/src/components/paid-ads/asset-group/asset-group-header/asset-group-header.test.js @@ -18,7 +18,7 @@ jest.mock( '~/components/adaptive-form', () => ( { * External dependencies */ import '@testing-library/jest-dom'; -import { render, screen } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; /** * Internal dependencies @@ -27,23 +27,27 @@ import AssetGroupHeader from './asset-group-header'; import { useAdaptiveFormContext } from '~/components/adaptive-form'; describe( 'AssetGroupHeader', () => { - test( 'Component renders', () => { + test( 'Component renders', async () => { render( ); - expect( - screen.getByText( /Add additional assets/i ) - ).toBeInTheDocument(); + await waitFor( () => { + expect( + screen.getByText( /Add additional assets/i ) + ).toBeInTheDocument(); + } ); } ); - test( 'Component not showing Tip if there are no imported assets', () => { + test( 'Component not showing Tip if there are no imported assets', async () => { render( ); - expect( - screen.queryByText( - "We've used your final URL to auto-populate some assets for you. For the best results, we recommend that you add more assets." - ) - ).not.toBeInTheDocument(); + await waitFor( () => { + expect( + screen.queryByText( + "We've used your final URL to auto-populate some assets for you. For the best results, we recommend that you add more assets." + ) + ).not.toBeInTheDocument(); + } ); } ); - test( 'Component showing Tip if there are imported assets', () => { + test( 'Component showing Tip if there are imported assets', async () => { useAdaptiveFormContext.mockImplementation( () => { return { adapter: { @@ -55,10 +59,12 @@ describe( 'AssetGroupHeader', () => { }; } ); render( ); - expect( - screen.getByText( - "We've used your final URL to auto-populate some assets for you. For the best results, we recommend that you add more assets." - ) - ).toBeInTheDocument(); + await waitFor( () => { + expect( + screen.getByText( + "We've used your final URL to auto-populate some assets for you. For the best results, we recommend that you add more assets." + ) + ).toBeInTheDocument(); + } ); } ); } ); From 481ae7ab4dc47b846c2f6f4ac3043037cda46c97 Mon Sep 17 00:00:00 2001 From: Ankit Gade Date: Fri, 23 Jan 2026 19:11:10 +0530 Subject: [PATCH 074/123] Add comment --- .../paid-ads/asset-group/asset-group-header/final-url-card.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/js/src/components/paid-ads/asset-group/asset-group-header/final-url-card.js b/js/src/components/paid-ads/asset-group/asset-group-header/final-url-card.js index 6fe6661b06..9c18c8850c 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-header/final-url-card.js +++ b/js/src/components/paid-ads/asset-group/asset-group-header/final-url-card.js @@ -110,6 +110,8 @@ export default function FinalUrlCard( { } didInitialLoadRef.current = true; + + // When the type `homepage` is passed, `id` is ignore, but because of typing, we need to pass it as zero. loadSuggestedAssets( { id: 0, type: 'homepage' } ); }, [ loadSuggestedAssets ] ); From 72f18680cc95ebbae337c837d4c2a8e66c900bbe Mon Sep 17 00:00:00 2001 From: asvinb Date: Fri, 23 Jan 2026 19:53:02 +0400 Subject: [PATCH 075/123] refactor(asset-group): Refactor AssetsLoader props and selection logic --- .../asset-group-header/assets-loader.js | 14 +++++------ .../asset-group-header/final-url-card.js | 23 ++++++++++++++----- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/js/src/components/paid-ads/asset-group/asset-group-header/assets-loader.js b/js/src/components/paid-ads/asset-group/asset-group-header/assets-loader.js index 983f83dcd0..435eeaa5f3 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-header/assets-loader.js +++ b/js/src/components/paid-ads/asset-group/asset-group-header/assets-loader.js @@ -82,12 +82,12 @@ function mapFinalUrlsToOptions( finalUrls, search ) { * and then loading the suggested assets. * * @param {Object} props React props. - * @param {boolean} props.isFetching Whether the assets are currently being fetched. - * @param {Function} props.loadSuggestedAssets Function to load suggested assets. + * @param {boolean} props.loading Whether the assets are being loaded. + * @param {Function} props.onSelectFinalUrl Callback when a final URL is selected. * * @fires gla_import_assets_by_final_url_button_click */ -export default function AssetsLoader( { isFetching, loadSuggestedAssets } ) { +export default function AssetsLoader( { loading, onSelectFinalUrl } ) { const cacheRef = useRef( {} ); const latestSearchRef = useRef(); @@ -146,7 +146,7 @@ export default function AssetsLoader( { isFetching, loadSuggestedAssets } ) { const handleClick = async () => { const { finalUrl } = selectedOptions[ 0 ]; - loadSuggestedAssets( { id: finalUrl.id, type: finalUrl.type } ); + onSelectFinalUrl( finalUrl ); }; const { finalUrl } = selectedOptions[ 0 ] || {}; @@ -165,7 +165,7 @@ export default function AssetsLoader( { isFetching, loadSuggestedAssets } ) { isSearchable hideBeforeSearch excludeSelectedOptions={ false } - disabled={ isFetching } + disabled={ loading } options={ [] } // The actual options will be provided via the callback results of `onSearch`. selected={ selectedOptions } onSearch={ debouncedHandleSearch } @@ -175,12 +175,12 @@ export default function AssetsLoader( { isFetching, loadSuggestedAssets } ) { diff --git a/js/src/components/paid-ads/asset-group/asset-group-header/final-url-card.js b/js/src/components/paid-ads/asset-group/asset-group-header/final-url-card.js index 9c18c8850c..24e5079a6c 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-header/final-url-card.js +++ b/js/src/components/paid-ads/asset-group/asset-group-header/final-url-card.js @@ -53,7 +53,7 @@ export default function FinalUrlCard( { } ) { const [ fetching, setFetching ] = useState( false ); const [ finalUrl, setFinalUrl ] = useState( initialFinalUrl || null ); - const didInitialLoadRef = useRef( false ); + const hasLoadedInitialHomepageAssetsRef = useRef( false ); const { createNotice } = useDispatchCoreNotices(); const description = finalUrl ? ( @@ -105,16 +105,27 @@ export default function FinalUrlCard( { ); useEffect( () => { - if ( didInitialLoadRef.current ) { + if ( hasLoadedInitialHomepageAssetsRef.current ) { return; } - didInitialLoadRef.current = true; + hasLoadedInitialHomepageAssetsRef.current = true; - // When the type `homepage` is passed, `id` is ignore, but because of typing, we need to pass it as zero. + // Load homepage assets on first render by passing `id: 0` and a `type` other than `post` or `term`. + // `id` is a required parameter, but it is ignored when loading homepage assets. + // Related: https://github.com/woocommerce/google-listings-and-ads/blob/d23bdb504bce1ed8a10a4bd92608aeb5137fbe60/src/Ads/AssetSuggestionsService.php#L210-L216 loadSuggestedAssets( { id: 0, type: 'homepage' } ); }, [ loadSuggestedAssets ] ); + const handleSelectFinalUrl = ( selectedFinalUrl ) => { + const { id, type } = selectedFinalUrl; + + loadSuggestedAssets( { + id, + type, + } ); + }; + return ( ) : ( ) } From 8ec402cb01d8dca9e4adfb189cd7dbd356bd83c6 Mon Sep 17 00:00:00 2001 From: asvinb Date: Fri, 23 Jan 2026 20:22:42 +0400 Subject: [PATCH 076/123] test: Mock FinalUrlCard and remove waitFor from tests --- .../asset-group-header.test.js | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/js/src/components/paid-ads/asset-group/asset-group-header/asset-group-header.test.js b/js/src/components/paid-ads/asset-group/asset-group-header/asset-group-header.test.js index cdb563c942..7ed7e6feae 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-header/asset-group-header.test.js +++ b/js/src/components/paid-ads/asset-group/asset-group-header/asset-group-header.test.js @@ -14,11 +14,16 @@ jest.mock( '~/components/adaptive-form', () => ( { } ), } ) ); +jest.mock( + '~/components/paid-ads/asset-group/asset-group-header/final-url-card', + () => () =>
+); + /** * External dependencies */ import '@testing-library/jest-dom'; -import { render, screen, waitFor } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; /** * Internal dependencies @@ -27,27 +32,23 @@ import AssetGroupHeader from './asset-group-header'; import { useAdaptiveFormContext } from '~/components/adaptive-form'; describe( 'AssetGroupHeader', () => { - test( 'Component renders', async () => { + test( 'Component renders', () => { render( ); - await waitFor( () => { - expect( - screen.getByText( /Add additional assets/i ) - ).toBeInTheDocument(); - } ); + expect( + screen.getByText( /Add additional assets/i ) + ).toBeInTheDocument(); } ); - test( 'Component not showing Tip if there are no imported assets', async () => { + test( 'Component not showing Tip if there are no imported assets', () => { render( ); - await waitFor( () => { - expect( - screen.queryByText( - "We've used your final URL to auto-populate some assets for you. For the best results, we recommend that you add more assets." - ) - ).not.toBeInTheDocument(); - } ); + expect( + screen.queryByText( + "We've used your final URL to auto-populate some assets for you. For the best results, we recommend that you add more assets." + ) + ).not.toBeInTheDocument(); } ); - test( 'Component showing Tip if there are imported assets', async () => { + test( 'Component showing Tip if there are imported assets', () => { useAdaptiveFormContext.mockImplementation( () => { return { adapter: { @@ -59,12 +60,10 @@ describe( 'AssetGroupHeader', () => { }; } ); render( ); - await waitFor( () => { - expect( - screen.getByText( - "We've used your final URL to auto-populate some assets for you. For the best results, we recommend that you add more assets." - ) - ).toBeInTheDocument(); - } ); + expect( + screen.getByText( + "We've used your final URL to auto-populate some assets for you. For the best results, we recommend that you add more assets." + ) + ).toBeInTheDocument(); } ); } ); From 46a69f0483501d91d994ed5f2084c60142cbbd50 Mon Sep 17 00:00:00 2001 From: asvinb Date: Fri, 23 Jan 2026 20:40:19 +0400 Subject: [PATCH 077/123] test(e2e): Streamline 'Optimize your campaign' final URL tests --- .../add-paid-campaigns.test.js | 34 ++----------------- tests/e2e/utils/pages/create-campaign.js | 9 +++++ 2 files changed, 11 insertions(+), 32 deletions(-) diff --git a/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js b/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js index 76b525e88d..43e6e81358 100644 --- a/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js +++ b/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js @@ -575,40 +575,10 @@ test.describe( 'Add paid campaign', () => { test.describe( 'Optimize your campaign step', async () => { test( 'Final URL should be selected to homepage by default', async () => { - await expect( - page.getByRole( 'heading', { - level: 1, - name: 'Optimize your campaign', - } ) - ).toBeVisible(); - - const finalUrlCard = page.locator( '.gla-final-url-card' ); + const finalUrlCard = createCampaignPage.getFinalUrlCard(); await expect( finalUrlCard ).toContainText( 'https://woo.com/shop/' ); - - const selectDifferentFinalUrlButton = page.getByRole( - 'button', - { - name: 'Or, select a different Final URL', - } - ); - - await selectDifferentFinalUrlButton.click(); - } ); - - test( 'Create Campaign button should be disabled if no URL selected', async () => { - const createCampaignButton = - createCampaignPage.getCreateCampaignButton(); - await expect( createCampaignButton ).toBeDisabled(); - } ); - - test( 'Selecting final URL enables Create Campaign button', async () => { - await createCampaignPage.selectUrlOption(); - - const createCampaignButton = - createCampaignPage.getCreateCampaignButton(); - await expect( createCampaignButton ).toBeEnabled(); } ); test( 'Selecting the "Or, select a different Final URL" button disables the Create Campaign button', async () => { @@ -621,7 +591,7 @@ test.describe( 'Add paid campaign', () => { await expect( createCampaignButton ).toBeDisabled(); } ); - test( 'Selecting the Final URL again enables the Create Campaign button', async () => { + test( 'Selecting final URL enables Create Campaign button', async () => { await createCampaignPage.selectUrlOption(); const createCampaignButton = diff --git a/tests/e2e/utils/pages/create-campaign.js b/tests/e2e/utils/pages/create-campaign.js index 1ce6773beb..81635ab3c7 100644 --- a/tests/e2e/utils/pages/create-campaign.js +++ b/tests/e2e/utils/pages/create-campaign.js @@ -520,6 +520,15 @@ export default class CreateCampaignPage extends MockRequests { ); } + /** + * Get final URL card. + * + * @return {import('@playwright/test').Locator} Get final URL card. + */ + getFinalUrlCard() { + return this.page.locator( '.gla-final-url-card' ); + } + /** * Select URL option. * From 30947ca51307e578fd3f1fe188424e922add49ac Mon Sep 17 00:00:00 2001 From: asvinb Date: Fri, 23 Jan 2026 20:47:54 +0400 Subject: [PATCH 078/123] style: Reorder import statements --- .../paid-ads/asset-group/asset-group-header/final-url-card.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/src/components/paid-ads/asset-group/asset-group-header/final-url-card.js b/js/src/components/paid-ads/asset-group/asset-group-header/final-url-card.js index 24e5079a6c..fcdd0635cb 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-header/final-url-card.js +++ b/js/src/components/paid-ads/asset-group/asset-group-header/final-url-card.js @@ -17,8 +17,8 @@ import AccountCard, { APPEARANCE } from '~/components/account-card'; import AppButton from '~/components/app-button'; import AssetsLoader from './assets-loader'; import { API_NAMESPACE } from '~/data/constants'; -import './final-url-card.scss'; import useDispatchCoreNotices from '~/hooks/useDispatchCoreNotices'; +import './final-url-card.scss'; /** * @typedef {import('~/data/types.js').SuggestedAssets} SuggestedAssets From c2366320a8cd39b67549a2b8c0fd1ffa3fc2b347 Mon Sep 17 00:00:00 2001 From: asvinb Date: Fri, 23 Jan 2026 20:49:10 +0400 Subject: [PATCH 079/123] refactor: Inline API path variables in fetchSuggestedAssets --- .../asset-group/asset-group-header/final-url-card.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/js/src/components/paid-ads/asset-group/asset-group-header/final-url-card.js b/js/src/components/paid-ads/asset-group/asset-group-header/final-url-card.js index fcdd0635cb..641919ce4d 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-header/final-url-card.js +++ b/js/src/components/paid-ads/asset-group/asset-group-header/final-url-card.js @@ -31,9 +31,12 @@ import './final-url-card.scss'; */ function fetchSuggestedAssets( id, type ) { - const endPoint = `${ API_NAMESPACE }/assets/suggestions`; - const query = { id, type }; - return apiFetch( { path: addQueryArgs( endPoint, query ) } ); + const path = addQueryArgs( `${ API_NAMESPACE }/assets/suggestions`, { + id, + type, + } ); + + return apiFetch( { path } ); } /** From 9a03ce57426f00d875739a6568b96ea63c3253d5 Mon Sep 17 00:00:00 2001 From: asvinb Date: Fri, 23 Jan 2026 20:52:28 +0400 Subject: [PATCH 080/123] refactor(asset-group): Inline fetchSuggestedAssets function --- .../asset-group-header/final-url-card.js | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/js/src/components/paid-ads/asset-group/asset-group-header/final-url-card.js b/js/src/components/paid-ads/asset-group/asset-group-header/final-url-card.js index 641919ce4d..40219aa4a8 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-header/final-url-card.js +++ b/js/src/components/paid-ads/asset-group/asset-group-header/final-url-card.js @@ -30,15 +30,6 @@ import './final-url-card.scss'; * @event gla_reselect_another_final_url_button_click */ -function fetchSuggestedAssets( id, type ) { - const path = addQueryArgs( `${ API_NAMESPACE }/assets/suggestions`, { - id, - type, - } ); - - return apiFetch( { path } ); -} - /** * Renders the Card UI for managing the final URL and getting the suggested assets. * @@ -89,8 +80,17 @@ export default function FinalUrlCard( { const loadSuggestedAssets = useCallback( async ( { id, type } ) => { setFetching( true ); + try { - const assets = await fetchSuggestedAssets( id, type ); + const path = addQueryArgs( + `${ API_NAMESPACE }/assets/suggestions`, + { + id, + type, + } + ); + + const assets = await apiFetch( { path } ); handleAssetsLoaded( assets ); } catch ( error ) { createNotice( From f2dd36f2ca08428ff986bd9483bc8407532d2241 Mon Sep 17 00:00:00 2001 From: asvinb Date: Fri, 23 Jan 2026 21:05:05 +0400 Subject: [PATCH 081/123] docs: Adds JSDoc for FinalUrl type and updates onSelectFinalUrl prop --- .../asset-group/asset-group-header/assets-loader.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/js/src/components/paid-ads/asset-group/asset-group-header/assets-loader.js b/js/src/components/paid-ads/asset-group/asset-group-header/assets-loader.js index 435eeaa5f3..1e79114270 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-header/assets-loader.js +++ b/js/src/components/paid-ads/asset-group/asset-group-header/assets-loader.js @@ -19,6 +19,16 @@ import './assets-loader.scss'; * @typedef {import('~/data/types.js').SuggestedAssets} SuggestedAssets */ +/** + * A selectable final URL option. + * + * @typedef {Object} FinalUrl + * @property {number} id The entity ID (e.g. post/term ID). + * @property {string} type The final URL type. + * @property {string} title Display title for the final URL. + * @property {string} url Absolute final URL. + */ + function allowAllResults() { // Make it result in `new RegExp('.', 'i')` to avoid any custom results in // the mapFinalUrlsToOptions function being filtered out. @@ -83,7 +93,7 @@ function mapFinalUrlsToOptions( finalUrls, search ) { * * @param {Object} props React props. * @param {boolean} props.loading Whether the assets are being loaded. - * @param {Function} props.onSelectFinalUrl Callback when a final URL is selected. + * @param {(finalUrl: FinalUrl) => void} props.onSelectFinalUrl Callback fired when a final URL is selected. Receives the selected final URL as the first argument. * * @fires gla_import_assets_by_final_url_button_click */ From 5f0371561e36bd1d961a387d60c63f84ae76ef3e Mon Sep 17 00:00:00 2001 From: James Morrison Date: Tue, 27 Jan 2026 11:26:26 +0000 Subject: [PATCH 082/123] PR feedback. --- src/API/Google/AssetFieldType.php | 36 ++++++---------- src/Ads/AdsAssetGenerationService.php | 43 +++++++++---------- .../HelperTrait/GoogleAdsClientTrait.php | 7 ++- 3 files changed, 38 insertions(+), 48 deletions(-) diff --git a/src/API/Google/AssetFieldType.php b/src/API/Google/AssetFieldType.php index 0cef4d9a5b..160298f6ce 100644 --- a/src/API/Google/AssetFieldType.php +++ b/src/API/Google/AssetFieldType.php @@ -95,13 +95,6 @@ class AssetFieldType extends StatusMapping { */ public const PORTRAIT_MARKETING_IMAGE = 'portrait_marketing_image'; - /** - * The asset is linked for use as a tall portrait marketing image. - * - * @var string - */ - public const TALL_PORTRAIT_MARKETING_IMAGE = 'tall_portrait_marketing_image'; - /** * The asset is linked for use as a landscape logo. * @@ -129,21 +122,20 @@ class AssetFieldType extends StatusMapping { * @var string */ protected const MAPPING = [ - AdsAssetFieldType::UNSPECIFIED => self::UNSPECIFIED, - AdsAssetFieldType::UNKNOWN => self::UNKNOWN, - AdsAssetFieldType::HEADLINE => self::HEADLINE, - AdsAssetFieldType::DESCRIPTION => self::DESCRIPTION, - AdsAssetFieldType::MARKETING_IMAGE => self::MARKETING_IMAGE, - AdsAssetFieldType::LONG_HEADLINE => self::LONG_HEADLINE, - AdsAssetFieldType::BUSINESS_NAME => self::BUSINESS_NAME, - AdsAssetFieldType::SQUARE_MARKETING_IMAGE => self::SQUARE_MARKETING_IMAGE, - AdsAssetFieldType::LOGO => self::LOGO, - AdsAssetFieldType::CALL_TO_ACTION_SELECTION => self::CALL_TO_ACTION_SELECTION, - AdsAssetFieldType::PORTRAIT_MARKETING_IMAGE => self::PORTRAIT_MARKETING_IMAGE, - AdsAssetFieldType::TALL_PORTRAIT_MARKETING_IMAGE => self::TALL_PORTRAIT_MARKETING_IMAGE, - AdsAssetFieldType::LANDSCAPE_LOGO => self::LANDSCAPE_LOGO, - AdsAssetFieldType::YOUTUBE_VIDEO => self::YOUTUBE_VIDEO, - AdsAssetFieldType::MEDIA_BUNDLE => self::MEDIA_BUNDLE, + AdsAssetFieldType::UNSPECIFIED => self::UNSPECIFIED, + AdsAssetFieldType::UNKNOWN => self::UNKNOWN, + AdsAssetFieldType::HEADLINE => self::HEADLINE, + AdsAssetFieldType::DESCRIPTION => self::DESCRIPTION, + AdsAssetFieldType::MARKETING_IMAGE => self::MARKETING_IMAGE, + AdsAssetFieldType::LONG_HEADLINE => self::LONG_HEADLINE, + AdsAssetFieldType::BUSINESS_NAME => self::BUSINESS_NAME, + AdsAssetFieldType::SQUARE_MARKETING_IMAGE => self::SQUARE_MARKETING_IMAGE, + AdsAssetFieldType::LOGO => self::LOGO, + AdsAssetFieldType::CALL_TO_ACTION_SELECTION => self::CALL_TO_ACTION_SELECTION, + AdsAssetFieldType::PORTRAIT_MARKETING_IMAGE => self::PORTRAIT_MARKETING_IMAGE, + AdsAssetFieldType::LANDSCAPE_LOGO => self::LANDSCAPE_LOGO, + AdsAssetFieldType::YOUTUBE_VIDEO => self::YOUTUBE_VIDEO, + AdsAssetFieldType::MEDIA_BUNDLE => self::MEDIA_BUNDLE, ]; diff --git a/src/Ads/AdsAssetGenerationService.php b/src/Ads/AdsAssetGenerationService.php index 91b2dbf812..1809cdb4b7 100644 --- a/src/Ads/AdsAssetGenerationService.php +++ b/src/Ads/AdsAssetGenerationService.php @@ -45,18 +45,25 @@ class AdsAssetGenerationService implements OptionsAwareInterface, Service { protected $google_ads_client; /** - * Mapping from lowercase input strings to AssetFieldType constants. + * Valid text asset field types. * * @var array */ - protected const TYPE_MAPPING = [ - 'headline' => AssetFieldType::HEADLINE, - 'long_headline' => AssetFieldType::LONG_HEADLINE, - 'description' => AssetFieldType::DESCRIPTION, - 'marketing_image' => AssetFieldType::MARKETING_IMAGE, - 'square_marketing_image' => AssetFieldType::SQUARE_MARKETING_IMAGE, - 'portrait_marketing_image' => AssetFieldType::PORTRAIT_MARKETING_IMAGE, - 'tall_portrait_marketing_image' => AssetFieldType::TALL_PORTRAIT_MARKETING_IMAGE, + protected const VALID_TEXT_TYPES = [ + AssetFieldType::HEADLINE, + AssetFieldType::LONG_HEADLINE, + AssetFieldType::DESCRIPTION, + ]; + + /** + * Valid image asset field types. + * + * @var array + */ + protected const VALID_IMAGE_TYPES = [ + AssetFieldType::MARKETING_IMAGE, + AssetFieldType::SQUARE_MARKETING_IMAGE, + AssetFieldType::PORTRAIT_MARKETING_IMAGE, ]; /** @@ -95,8 +102,7 @@ public function generate_text( array $args = [] ): array { } // Convert asset field types from lowercase strings to enum numbers. - $allowed_types = [ AssetFieldType::HEADLINE, AssetFieldType::LONG_HEADLINE, AssetFieldType::DESCRIPTION ]; - $asset_field_types = $this->convert_types_to_enums( $args['asset_field_types'], $allowed_types ); + $asset_field_types = $this->convert_types_to_enums( $args['asset_field_types'], self::VALID_TEXT_TYPES ); $request = new GenerateTextRequest( [ @@ -134,7 +140,7 @@ public function generate_text( array $args = [] ): array { * Optional. Arguments for generating image assets. * * @type string $final_url The final URL - defaults to the Site URL. - * @type array $asset_field_types Can be one or more of: marketing_image, square_marketing_image, portrait_marketing_image, tall_portrait_marketing_image. + * @type array $asset_field_types Can be one or more of: marketing_image, square_marketing_image, portrait_marketing_image. * } * @return array Array of generated image objects with 'temporary_image_url' and 'type' keys. * @throws Exception If the image assets can't be generated. @@ -150,8 +156,7 @@ public function generate_images( array $args = [] ): array { // Convert asset field types from lowercase strings to enum numbers (if provided). $asset_field_types = []; if ( ! empty( $args['asset_field_types'] ) ) { - $allowed_types = [ AssetFieldType::MARKETING_IMAGE, AssetFieldType::SQUARE_MARKETING_IMAGE, AssetFieldType::PORTRAIT_MARKETING_IMAGE, AssetFieldType::TALL_PORTRAIT_MARKETING_IMAGE ]; - $asset_field_types = $this->convert_types_to_enums( $args['asset_field_types'], $allowed_types ); + $asset_field_types = $this->convert_types_to_enums( $args['asset_field_types'], self::VALID_IMAGE_TYPES ); } $request_data = [ @@ -201,18 +206,12 @@ public function generate_images( array $args = [] ): array { protected function convert_types_to_enums( array $types, array $allowed_types = [] ): array { $enums = []; foreach ( $types as $type ) { - if ( ! isset( self::TYPE_MAPPING[ $type ] ) ) { - continue; - } - - $internal_type = self::TYPE_MAPPING[ $type ]; - // Filter by allowed types if specified. - if ( ! empty( $allowed_types ) && ! in_array( $internal_type, $allowed_types, true ) ) { + if ( ! empty( $allowed_types ) && ! in_array( $type, $allowed_types, true ) ) { continue; } - $enums[] = AssetFieldType::number( $internal_type ); + $enums[] = AssetFieldType::number( $type ); } return $enums; diff --git a/tests/Tools/HelperTrait/GoogleAdsClientTrait.php b/tests/Tools/HelperTrait/GoogleAdsClientTrait.php index 1b600e419a..1c897caba4 100644 --- a/tests/Tools/HelperTrait/GoogleAdsClientTrait.php +++ b/tests/Tools/HelperTrait/GoogleAdsClientTrait.php @@ -1212,10 +1212,9 @@ protected function generate_text_assets_mock_exception( ApiException $exception */ protected function generate_image_assets_mock( array $image_assets ) { $type_mapping = [ - 'MARKETING_IMAGE' => AssetFieldType::MARKETING_IMAGE, - 'SQUARE_MARKETING_IMAGE' => AssetFieldType::SQUARE_MARKETING_IMAGE, - 'PORTRAIT_MARKETING_IMAGE' => AssetFieldType::PORTRAIT_MARKETING_IMAGE, - 'TALL_PORTRAIT_MARKETING_IMAGE' => AssetFieldType::TALL_PORTRAIT_MARKETING_IMAGE, + 'MARKETING_IMAGE' => AssetFieldType::MARKETING_IMAGE, + 'SQUARE_MARKETING_IMAGE' => AssetFieldType::SQUARE_MARKETING_IMAGE, + 'PORTRAIT_MARKETING_IMAGE' => AssetFieldType::PORTRAIT_MARKETING_IMAGE, ]; $image_asset_objects = []; From 497cea809809aae3fa7859ef37ed7f777df92179 Mon Sep 17 00:00:00 2001 From: James Morrison Date: Tue, 27 Jan 2026 12:20:41 +0000 Subject: [PATCH 083/123] Removed TALL_PORTRAIT_MARKETING_IMAGE / fix tests. --- src/API/Site/Controllers/Ads/AssetGenerationController.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/API/Site/Controllers/Ads/AssetGenerationController.php b/src/API/Site/Controllers/Ads/AssetGenerationController.php index 499636dd2a..af1b520a39 100644 --- a/src/API/Site/Controllers/Ads/AssetGenerationController.php +++ b/src/API/Site/Controllers/Ads/AssetGenerationController.php @@ -128,7 +128,6 @@ protected function get_generate_images_params(): array { AssetFieldType::MARKETING_IMAGE, AssetFieldType::SQUARE_MARKETING_IMAGE, AssetFieldType::PORTRAIT_MARKETING_IMAGE, - AssetFieldType::TALL_PORTRAIT_MARKETING_IMAGE, ], ], 'sanitize_callback' => function ( $types ) { From 7639885c8f8c0c968d57267b76310f7ef1191a2c Mon Sep 17 00:00:00 2001 From: James Morrison Date: Tue, 27 Jan 2026 13:30:54 +0000 Subject: [PATCH 084/123] PR feedback. --- .../Ads/AssetGenerationController.php | 46 +++++-------------- src/Ads/AdsAssetGenerationService.php | 8 ++-- .../HelperTrait/GoogleAdsClientTrait.php | 16 +++---- .../Ads/AssetGenerationControllerTest.php | 28 +++++------ .../Ads/AdsAssetGenerationServiceTest.php | 26 +++++------ 5 files changed, 51 insertions(+), 73 deletions(-) diff --git a/src/API/Site/Controllers/Ads/AssetGenerationController.php b/src/API/Site/Controllers/Ads/AssetGenerationController.php index af1b520a39..54f8f07263 100644 --- a/src/API/Site/Controllers/Ads/AssetGenerationController.php +++ b/src/API/Site/Controllers/Ads/AssetGenerationController.php @@ -4,7 +4,6 @@ namespace Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\Ads; use Automattic\WooCommerce\GoogleListingsAndAds\Ads\AdsAssetGenerationService; -use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\AssetFieldType; use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\BaseController; use Automattic\WooCommerce\GoogleListingsAndAds\API\Site\Controllers\ResponseFromExceptionTrait; use Automattic\WooCommerce\GoogleListingsAndAds\API\TransportMethods; @@ -54,6 +53,7 @@ public function register_routes(): void { 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_generate_text_params(), ], + 'schema' => $this->get_api_response_schema_callback(), ] ); @@ -66,6 +66,7 @@ public function register_routes(): void { 'permission_callback' => $this->get_permission_callback(), 'args' => $this->get_generate_images_params(), ], + 'schema' => $this->get_api_response_schema_callback(), ] ); } @@ -90,11 +91,7 @@ protected function get_generate_text_params(): array { 'default' => [], 'items' => [ 'type' => 'string', - 'enum' => [ - AssetFieldType::HEADLINE, - AssetFieldType::LONG_HEADLINE, - AssetFieldType::DESCRIPTION, - ], + 'enum' => AdsAssetGenerationService::VALID_TEXT_TYPES, ], 'sanitize_callback' => function ( $types ) { return array_map( 'sanitize_text_field', $types ); @@ -124,11 +121,7 @@ protected function get_generate_images_params(): array { 'default' => [], 'items' => [ 'type' => 'string', - 'enum' => [ - AssetFieldType::MARKETING_IMAGE, - AssetFieldType::SQUARE_MARKETING_IMAGE, - AssetFieldType::PORTRAIT_MARKETING_IMAGE, - ], + 'enum' => AdsAssetGenerationService::VALID_IMAGE_TYPES, ], 'sanitize_callback' => function ( $types ) { return array_map( 'sanitize_text_field', $types ); @@ -159,8 +152,10 @@ protected function get_generate_text_callback(): callable { ] ); - // Format response with lowercase types. - return $this->format_response( $final_url, $items ); + return [ + 'final_url' => $final_url, + 'items' => $items, + ]; } catch ( Exception $e ) { return $this->response_from_exception( $e ); } @@ -187,33 +182,16 @@ protected function get_generate_images_callback(): callable { } $items = $this->service->generate_images( $args ); - // Format response with lowercase types. - return $this->format_response( $final_url, $items ); + return [ + 'final_url' => $final_url, + 'items' => $items, + ]; } catch ( Exception $e ) { return $this->response_from_exception( $e ); } }; } - /** - * Format the response with final_url and items. - * - * @param string $final_url The final URL. - * @param array $service_items Items from the service (with uppercase types). - * @return array Formatted response with lowercase types. - */ - protected function format_response( string $final_url, array $service_items ): array { - $items = []; - foreach ( $service_items as $item ) { - $item['type'] = strtolower( $item['type'] ); - $items[] = $item; - } - - return [ - 'final_url' => $final_url, - 'items' => $items, - ]; - } /** * Get the item schema properties for the controller. diff --git a/src/Ads/AdsAssetGenerationService.php b/src/Ads/AdsAssetGenerationService.php index 1809cdb4b7..4b7b055b65 100644 --- a/src/Ads/AdsAssetGenerationService.php +++ b/src/Ads/AdsAssetGenerationService.php @@ -49,7 +49,7 @@ class AdsAssetGenerationService implements OptionsAwareInterface, Service { * * @var array */ - protected const VALID_TEXT_TYPES = [ + public const VALID_TEXT_TYPES = [ AssetFieldType::HEADLINE, AssetFieldType::LONG_HEADLINE, AssetFieldType::DESCRIPTION, @@ -60,7 +60,7 @@ class AdsAssetGenerationService implements OptionsAwareInterface, Service { * * @var array */ - protected const VALID_IMAGE_TYPES = [ + public const VALID_IMAGE_TYPES = [ AssetFieldType::MARKETING_IMAGE, AssetFieldType::SQUARE_MARKETING_IMAGE, AssetFieldType::PORTRAIT_MARKETING_IMAGE, @@ -122,7 +122,7 @@ public function generate_text( array $args = [] ): array { $asset_field_type_label = AssetFieldType::label( $asset_field_type_number ); $results[] = [ 'text' => $text_asset->getText(), - 'type' => AssetFieldType::name( $asset_field_type_label ), + 'type' => $asset_field_type_label, ]; } @@ -185,7 +185,7 @@ public function generate_images( array $args = [] ): array { $asset_field_type_label = AssetFieldType::label( $asset_field_type_number ); $results[] = [ 'temporary_image_url' => $image_asset->getImageTemporaryUrl(), - 'type' => AssetFieldType::name( $asset_field_type_label ), + 'type' => $asset_field_type_label, ]; } diff --git a/tests/Tools/HelperTrait/GoogleAdsClientTrait.php b/tests/Tools/HelperTrait/GoogleAdsClientTrait.php index 1c897caba4..abaf4f8aaa 100644 --- a/tests/Tools/HelperTrait/GoogleAdsClientTrait.php +++ b/tests/Tools/HelperTrait/GoogleAdsClientTrait.php @@ -1170,13 +1170,13 @@ protected function generate_location_ids_mock( array $locations ) { /** * Generates a mocked response for text asset generation. * - * @param array $text_assets Array of text assets with 'text' and 'type' keys (type in uppercase like 'HEADLINE'). + * @param array $text_assets Array of text assets with 'text' and 'type' keys (type in lowercase like 'headline'). */ protected function generate_text_assets_mock( array $text_assets ) { $type_mapping = [ - 'HEADLINE' => AssetFieldType::HEADLINE, - 'LONG_HEADLINE' => AssetFieldType::LONG_HEADLINE, - 'DESCRIPTION' => AssetFieldType::DESCRIPTION, + 'headline' => AssetFieldType::HEADLINE, + 'long_headline' => AssetFieldType::LONG_HEADLINE, + 'description' => AssetFieldType::DESCRIPTION, ]; $text_asset_objects = []; @@ -1208,13 +1208,13 @@ protected function generate_text_assets_mock_exception( ApiException $exception /** * Generates a mocked response for image asset generation. * - * @param array $image_assets Array of image assets with 'temporary_image_url' and 'type' keys (type in uppercase like 'MARKETING_IMAGE'). + * @param array $image_assets Array of image assets with 'temporary_image_url' and 'type' keys (type in lowercase like 'marketing_image'). */ protected function generate_image_assets_mock( array $image_assets ) { $type_mapping = [ - 'MARKETING_IMAGE' => AssetFieldType::MARKETING_IMAGE, - 'SQUARE_MARKETING_IMAGE' => AssetFieldType::SQUARE_MARKETING_IMAGE, - 'PORTRAIT_MARKETING_IMAGE' => AssetFieldType::PORTRAIT_MARKETING_IMAGE, + 'marketing_image' => AssetFieldType::MARKETING_IMAGE, + 'square_marketing_image' => AssetFieldType::SQUARE_MARKETING_IMAGE, + 'portrait_marketing_image' => AssetFieldType::PORTRAIT_MARKETING_IMAGE, ]; $image_asset_objects = []; diff --git a/tests/Unit/API/Site/Controllers/Ads/AssetGenerationControllerTest.php b/tests/Unit/API/Site/Controllers/Ads/AssetGenerationControllerTest.php index f8ba6dbddc..c7a8768599 100644 --- a/tests/Unit/API/Site/Controllers/Ads/AssetGenerationControllerTest.php +++ b/tests/Unit/API/Site/Controllers/Ads/AssetGenerationControllerTest.php @@ -59,15 +59,15 @@ public function test_generate_text_with_defaults() { [ [ 'text' => 'Test headline', - 'type' => 'HEADLINE', + 'type' => 'headline', ], [ 'text' => 'Test long headline', - 'type' => 'LONG_HEADLINE', + 'type' => 'long_headline', ], [ 'text' => 'Test description', - 'type' => 'DESCRIPTION', + 'type' => 'description', ], ] ); @@ -102,7 +102,7 @@ public function test_generate_text_with_custom_url() { [ [ 'text' => 'Custom headline', - 'type' => 'HEADLINE', + 'type' => 'headline', ], ] ); @@ -132,7 +132,7 @@ public function test_generate_text_with_specific_types() { [ [ 'text' => 'Headline only', - 'type' => 'HEADLINE', + 'type' => 'headline', ], ] ); @@ -164,11 +164,11 @@ function ( $args ) { [ [ 'text' => 'Test', - 'type' => 'HEADLINE', + 'type' => 'headline', ], [ 'text' => 'Test', - 'type' => 'DESCRIPTION', + 'type' => 'description', ], ] ); @@ -205,15 +205,15 @@ public function test_generate_images_with_defaults() { [ [ 'temporary_image_url' => 'https://example.com/image-marketing.jpg', - 'type' => 'MARKETING_IMAGE', + 'type' => 'marketing_image', ], [ 'temporary_image_url' => 'https://example.com/image-square.jpg', - 'type' => 'SQUARE_MARKETING_IMAGE', + 'type' => 'square_marketing_image', ], [ 'temporary_image_url' => 'https://example.com/image-portrait.jpg', - 'type' => 'PORTRAIT_MARKETING_IMAGE', + 'type' => 'portrait_marketing_image', ], ] ); @@ -246,7 +246,7 @@ public function test_generate_images_with_custom_url() { [ [ 'temporary_image_url' => 'https://example.com/custom-image.jpg', - 'type' => 'MARKETING_IMAGE', + 'type' => 'marketing_image', ], ] ); @@ -277,7 +277,7 @@ public function test_generate_images_with_specific_types() { [ [ 'temporary_image_url' => 'https://example.com/image.jpg', - 'type' => 'MARKETING_IMAGE', + 'type' => 'marketing_image', ], ] ); @@ -309,11 +309,11 @@ function ( $args ) { [ [ 'temporary_image_url' => 'https://example.com/image1.jpg', - 'type' => 'MARKETING_IMAGE', + 'type' => 'marketing_image', ], [ 'temporary_image_url' => 'https://example.com/image2.jpg', - 'type' => 'SQUARE_MARKETING_IMAGE', + 'type' => 'square_marketing_image', ], ] ); diff --git a/tests/Unit/Ads/AdsAssetGenerationServiceTest.php b/tests/Unit/Ads/AdsAssetGenerationServiceTest.php index b54e7db7d0..bf56872435 100644 --- a/tests/Unit/Ads/AdsAssetGenerationServiceTest.php +++ b/tests/Unit/Ads/AdsAssetGenerationServiceTest.php @@ -50,15 +50,15 @@ public function test_generate_text_with_defaults() { $expected_text_assets = [ [ 'text' => 'Generated headline text example.', - 'type' => 'HEADLINE', + 'type' => 'headline', ], [ 'text' => 'Generated long headline text example.', - 'type' => 'LONG_HEADLINE', + 'type' => 'long_headline', ], [ 'text' => 'Generated description text example.', - 'type' => 'DESCRIPTION', + 'type' => 'description', ], ]; @@ -79,7 +79,7 @@ public function test_generate_text_with_custom_final_url() { $expected_text_assets = [ [ 'text' => 'Custom headline', - 'type' => 'HEADLINE', + 'type' => 'headline', ], ]; @@ -103,7 +103,7 @@ public function test_generate_text_with_specific_types() { $expected_text_assets = [ [ 'text' => 'Headline only', - 'type' => 'HEADLINE', + 'type' => 'headline', ], ]; @@ -146,15 +146,15 @@ public function test_generate_text_uses_defaults_when_no_types_provided() { $expected_text_assets = [ [ 'text' => 'Default headline', - 'type' => 'HEADLINE', + 'type' => 'headline', ], [ 'text' => 'Default long headline', - 'type' => 'LONG_HEADLINE', + 'type' => 'long_headline', ], [ 'text' => 'Default description', - 'type' => 'DESCRIPTION', + 'type' => 'description', ], ]; @@ -169,15 +169,15 @@ public function test_generate_images_with_defaults() { $expected_image_assets = [ [ 'temporary_image_url' => 'https://example.com/temporary_image_url-marketing.jpg', - 'type' => 'MARKETING_IMAGE', + 'type' => 'marketing_image', ], [ 'temporary_image_url' => 'https://example.com/temporary_image_url-square.jpg', - 'type' => 'SQUARE_MARKETING_IMAGE', + 'type' => 'square_marketing_image', ], [ 'temporary_image_url' => 'https://example.com/temporary_image_url-portrait.jpg', - 'type' => 'PORTRAIT_MARKETING_IMAGE', + 'type' => 'portrait_marketing_image', ], ]; @@ -193,7 +193,7 @@ public function test_generate_images_with_custom_final_url() { $expected_image_assets = [ [ 'temporary_image_url' => 'https://example.com/custom-image.jpg', - 'type' => 'MARKETING_IMAGE', + 'type' => 'marketing_image', ], ]; @@ -208,7 +208,7 @@ public function test_generate_images_with_specific_types() { $expected_image_assets = [ [ 'temporary_image_url' => 'https://example.com/marketing-image.jpg', - 'type' => 'MARKETING_IMAGE', + 'type' => 'marketing_image', ], ]; From 8fbda495efbaf01b9a5e0b139c384a648640845f Mon Sep 17 00:00:00 2001 From: asvinb Date: Tue, 27 Jan 2026 18:53:13 +0400 Subject: [PATCH 085/123] refactor(asset-group): Centralize asset fetching and integrate loading state --- .../asset-group-header/asset-group-header.js | 44 +++++++++- .../asset-group-header/assets-loader.js | 9 +-- .../asset-group-header/final-url-card.js | 81 +++++-------------- .../paid-ads/asset-group/asset-group.js | 7 +- .../paid-ads/campaign-assets-form.js | 71 +++++++++------- .../pages/create-paid-ads-campaign/index.js | 3 - js/src/pages/edit-paid-ads-campaign/index.js | 1 + 7 files changed, 111 insertions(+), 105 deletions(-) diff --git a/js/src/components/paid-ads/asset-group/asset-group-header/asset-group-header.js b/js/src/components/paid-ads/asset-group/asset-group-header/asset-group-header.js index e920346800..8515ebbcc5 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-header/asset-group-header.js +++ b/js/src/components/paid-ads/asset-group/asset-group-header/asset-group-header.js @@ -2,7 +2,12 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { createInterpolateElement } from '@wordpress/element'; +import { + createInterpolateElement, + useRef, + useEffect, + useCallback, +} from '@wordpress/element'; import { Tip, Flex, FlexItem } from '@wordpress/components'; /** @@ -14,6 +19,7 @@ import Section from '~/components/section'; import FinalUrlCard from './final-url-card'; import AppDocumentationLink from '~/components/app-documentation-link'; import GenAICard from '../../gen-ai-card'; +import GenAIProgress from '../../gen-ai-progress'; /** * Renders the header section for the asset group form where the user selects the URL to manage the assets for. @@ -22,13 +28,49 @@ import GenAICard from '../../gen-ai-card'; * so it expects a `CampaignAssetsForm` to existing in its parents. */ export default function AssetGroupHeader() { + const hasLoadedInitialHomepageAssetsRef = useRef( false ); const { adapter } = useAdaptiveFormContext(); const { hasImportedAssets, hasAISuggestedTextAssets, hasAISuggestedMediaAssets, + fetchAssets, + isFetchingAssets, } = adapter; + const fetchCampaignAssets = useCallback( + async ( id, type ) => { + const suggestedAssets = await fetchAssets( id, type ); + adapter.resetAssetGroup( suggestedAssets ); + }, + [ fetchAssets, adapter ] + ); + + useEffect( () => { + console.log( adapter.baseAssetGroup[ ASSET_FORM_KEY.FINAL_URL ] ); + async function loadAssets() { + if ( + hasLoadedInitialHomepageAssetsRef.current || + adapter.baseAssetGroup[ ASSET_FORM_KEY.FINAL_URL ] + ) { + return; + } + + hasLoadedInitialHomepageAssetsRef.current = true; + + // Load homepage assets on first render by passing `id: 0` and a `type` other than `post` or `term`. + // `id` is a required parameter, but it is ignored when loading homepage assets. + // Related: https://github.com/woocommerce/google-listings-and-ads/blob/d23bdb504bce1ed8a10a4bd92608aeb5137fbe60/src/Ads/AssetSuggestionsService.php#L210-L216 + await fetchCampaignAssets( 0, 'homepage' ); + } + + loadAssets(); + }, [ fetchCampaignAssets, adapter.baseAssetGroup ] ); + + if ( isFetchingAssets ) { + return ; + } + return (
void} props.onSelectFinalUrl Callback fired when a final URL is selected. Receives the selected final URL as the first argument. * * @fires gla_import_assets_by_final_url_button_click */ -export default function AssetsLoader( { loading, onSelectFinalUrl } ) { +export default function AssetsLoader( { onSelectFinalUrl } ) { const cacheRef = useRef( {} ); const latestSearchRef = useRef(); @@ -175,7 +174,6 @@ export default function AssetsLoader( { loading, onSelectFinalUrl } ) { isSearchable hideBeforeSearch excludeSelectedOptions={ false } - disabled={ loading } options={ [] } // The actual options will be provided via the callback results of `onSearch`. selected={ selectedOptions } onSearch={ debouncedHandleSearch } @@ -184,13 +182,10 @@ export default function AssetsLoader( { loading, onSelectFinalUrl } ) { /> diff --git a/js/src/components/paid-ads/asset-group/asset-group-header/final-url-card.js b/js/src/components/paid-ads/asset-group/asset-group-header/final-url-card.js index 40219aa4a8..4367fa9df8 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-header/final-url-card.js +++ b/js/src/components/paid-ads/asset-group/asset-group-header/final-url-card.js @@ -1,23 +1,20 @@ /** * External dependencies */ -import apiFetch from '@wordpress/api-fetch'; import { __ } from '@wordpress/i18n'; -import { addQueryArgs } from '@wordpress/url'; import classnames from 'classnames'; -import { useCallback, useEffect, useRef, useState } from '@wordpress/element'; +import { useCallback, useEffect, useState } from '@wordpress/element'; import { ExternalLink } from '@wordpress/components'; /** * Internal dependencies */ import { ASSET_GROUP_KEY } from '~/constants'; +import { useAdaptiveFormContext } from '~/components/adaptive-form'; import Section from '~/components/section'; import AccountCard, { APPEARANCE } from '~/components/account-card'; import AppButton from '~/components/app-button'; import AssetsLoader from './assets-loader'; -import { API_NAMESPACE } from '~/data/constants'; -import useDispatchCoreNotices from '~/hooks/useDispatchCoreNotices'; import './final-url-card.scss'; /** @@ -45,10 +42,9 @@ export default function FinalUrlCard( { initialFinalUrl, hideFooter = false, } ) { - const [ fetching, setFetching ] = useState( false ); - const [ finalUrl, setFinalUrl ] = useState( initialFinalUrl || null ); - const hasLoadedInitialHomepageAssetsRef = useRef( false ); - const { createNotice } = useDispatchCoreNotices(); + const { adapter } = useAdaptiveFormContext(); + const [ finalUrl, setFinalUrl ] = useState( null ); + const { fetchAssets } = adapter; const description = finalUrl ? ( { finalUrl } @@ -59,6 +55,10 @@ export default function FinalUrlCard( { ) ); + useEffect( () => { + setFinalUrl( initialFinalUrl ); + }, [ initialFinalUrl ] ); + const handleAssetsLoaded = useCallback( ( suggestedAssets ) => { setFinalUrl( suggestedAssets[ ASSET_GROUP_KEY.FINAL_URL ] ); @@ -72,61 +72,23 @@ export default function FinalUrlCard( { onAssetsChange( null ); }; + const fetchCampaignAssets = useCallback( + async ( id, type ) => { + const suggestedAssets = await fetchAssets( id, type ); + handleAssetsLoaded( suggestedAssets ); + }, + [ fetchAssets, handleAssetsLoaded ] + ); + const className = classnames( { 'gla-final-url-card': true, 'gla-final-url-card--has-selected-url': finalUrl, } ); - const loadSuggestedAssets = useCallback( - async ( { id, type } ) => { - setFetching( true ); - - try { - const path = addQueryArgs( - `${ API_NAMESPACE }/assets/suggestions`, - { - id, - type, - } - ); - - const assets = await apiFetch( { path } ); - handleAssetsLoaded( assets ); - } catch ( error ) { - createNotice( - 'error', - __( - 'Unable to load assets data.', - 'google-listings-and-ads' - ) - ); - } finally { - setFetching( false ); - } - }, - [ createNotice, handleAssetsLoaded ] - ); - - useEffect( () => { - if ( hasLoadedInitialHomepageAssetsRef.current ) { - return; - } - - hasLoadedInitialHomepageAssetsRef.current = true; - - // Load homepage assets on first render by passing `id: 0` and a `type` other than `post` or `term`. - // `id` is a required parameter, but it is ignored when loading homepage assets. - // Related: https://github.com/woocommerce/google-listings-and-ads/blob/d23bdb504bce1ed8a10a4bd92608aeb5137fbe60/src/Ads/AssetSuggestionsService.php#L210-L216 - loadSuggestedAssets( { id: 0, type: 'homepage' } ); - }, [ loadSuggestedAssets ] ); - - const handleSelectFinalUrl = ( selectedFinalUrl ) => { + const handleSelectFinalUrl = async ( selectedFinalUrl ) => { const { id, type } = selectedFinalUrl; - loadSuggestedAssets( { - id, - type, - } ); + await fetchCampaignAssets( id, type ); }; return ( @@ -148,10 +110,7 @@ export default function FinalUrlCard( { onClick={ handleReselectClick } /> ) : ( - + ) } diff --git a/js/src/components/paid-ads/asset-group/asset-group.js b/js/src/components/paid-ads/asset-group/asset-group.js index d04998a973..92a39b6a57 100644 --- a/js/src/components/paid-ads/asset-group/asset-group.js +++ b/js/src/components/paid-ads/asset-group/asset-group.js @@ -77,7 +77,7 @@ export default function AssetGroup( { campaign } ) { isSubmitting, isSubmitted, submitter, - isFetchingGenAIAssets, + isFetchingAssets, } = adapter; const currentAction = submitter?.dataset.action; @@ -161,11 +161,10 @@ export default function AssetGroup( { campaign } ) { ) } /> - { isFetchingGenAIAssets && } + - { ! isFetchingGenAIAssets && ( + { ! isFetchingAssets && ( <> - diff --git a/js/src/components/paid-ads/campaign-assets-form.js b/js/src/components/paid-ads/campaign-assets-form.js index 4822b38fa2..d10656d4af 100644 --- a/js/src/components/paid-ads/campaign-assets-form.js +++ b/js/src/components/paid-ads/campaign-assets-form.js @@ -1,6 +1,9 @@ /** * External dependencies */ +import apiFetch from '@wordpress/api-fetch'; +import { __ } from '@wordpress/i18n'; +import { addQueryArgs } from '@wordpress/url'; import { useState, useMemo } from '@wordpress/element'; import { isPlainObject } from 'lodash'; @@ -16,8 +19,10 @@ import useAdsCurrency from '~/hooks/useAdsCurrency'; import useBudgetRecommendation from '~/hooks/useBudgetRecommendation'; import useRaiseBudgetRecommendations from '~/hooks/useRaiseBudgetRecommendations'; import useEventPropertiesFilter from '~/hooks/useEventPropertiesFilter'; +import useDispatchCoreNotices from '~/hooks/useDispatchCoreNotices'; import { useAppDispatch } from '~/data'; import { FILTER_BUDGET_RECOMMENDATIONS } from '~/utils/tracks'; +import { API_NAMESPACE } from '~/data/constants'; import round from '~/utils/round'; /** @@ -196,8 +201,7 @@ export default function CampaignAssetsForm( { ...adaptiveFormProps } ) { const { fetchGenAIMediaAssets, fetchGenAITextAssets } = useAppDispatch(); - const [ isFetchingGenAIAssets, setIsFetchingGenAIAssets ] = - useState( false ); + const [ isFetchingAssets, setIsFetchingAssets ] = useState( false ); const initialAssetGroup = useMemo( () => { return convertAssetEntityGroupToFormValues( assetEntityGroup ); }, [ assetEntityGroup ] ); @@ -211,6 +215,7 @@ export default function CampaignAssetsForm( { const { formatAmount } = useAdsCurrency(); const { data: budgetRecommendationData, hasResolved } = useBudgetRecommendation( countryCodes ); + const { createNotice } = useDispatchCoreNotices(); const budgetRecommendation = budgetRecommendationData || {}; @@ -240,13 +245,26 @@ export default function CampaignAssetsForm( { const assetGroupErrors = validateAssetGroup( formContext.values ); const finalUrl = assetEntityGroup?.[ ASSET_GROUP_KEY.FINAL_URL ]; - const fetchGenAIAssets = async ( url, assetGroupValues ) => { + const fetchAssets = async ( id, type ) => { try { - setIsFetchingGenAIAssets( true ); - const { data: textAssetsData } = - await fetchGenAITextAssets( url ); - const { data: mediaAssetsData } = - await fetchGenAIMediaAssets( url ); + setIsFetchingAssets( true ); + + const path = addQueryArgs( + `${ API_NAMESPACE }/assets/suggestions`, + { + id, + type, + } + ); + + const assetSuggestions = await apiFetch( { path } ); + const url = assetSuggestions[ ASSET_GROUP_KEY.FINAL_URL ]; + + const [ { data: textAssetsData }, { data: mediaAssetsData } ] = + await Promise.all( [ + fetchGenAITextAssets( url ), + fetchGenAIMediaAssets( url ), + ] ); const hasSuggestedTextAssets = hasValidAIGeneratedAssets( REQUIRED_TEXT_ASSET_KEYS, @@ -258,21 +276,23 @@ export default function CampaignAssetsForm( { mediaAssetsData ); - const nextValues = { - ...( hasSuggestedTextAssets ? textAssetsData : {} ), - }; - - if ( Object.keys( nextValues ).length ) { - formContext.setValues( { - ...assetGroupValues, - ...nextValues, - } ); - } - setHasAISuggestedTextAssets( hasSuggestedTextAssets ); setHasAISuggestedMediaAssets( hasSuggestedMediaAssets ); + + return { + ...assetSuggestions, + ...( hasSuggestedTextAssets ? textAssetsData : {} ), + }; + } catch ( error ) { + createNotice( + 'error', + __( + 'Unable to load assets data.', + 'google-listings-and-ads' + ) + ); } finally { - setIsFetchingGenAIAssets( false ); + setIsFetchingAssets( false ); } }; @@ -317,19 +337,12 @@ export default function CampaignAssetsForm( { setHasAISuggestedTextAssets( false ); setHasAISuggestedMediaAssets( false ); - if ( nextAssetGroup.final_url ) { - fetchGenAIAssets( - nextAssetGroup.final_url, - updatedContextValues - ); - } - formContext.adapter.hideValidation(); }, - isFetchingGenAIAssets, + isFetchingAssets, hasAISuggestedTextAssets, hasAISuggestedMediaAssets, - fetchGenAIAssets, + fetchAssets, }; }; diff --git a/js/src/pages/create-paid-ads-campaign/index.js b/js/src/pages/create-paid-ads-campaign/index.js index 5ab3a57395..873ca2db3b 100644 --- a/js/src/pages/create-paid-ads-campaign/index.js +++ b/js/src/pages/create-paid-ads-campaign/index.js @@ -153,13 +153,10 @@ const CreatePaidAdsCampaign = () => { ) } context={ eventContext } continueButton={ ( formContext ) => { - const { adapter } = formContext; - return ( { - adapter.fetchGenAIAssets(); handleContinueClick( STEP.ASSET_GROUP ); diff --git a/js/src/pages/edit-paid-ads-campaign/index.js b/js/src/pages/edit-paid-ads-campaign/index.js index 7de3f8fdb8..f544c7e6cc 100644 --- a/js/src/pages/edit-paid-ads-campaign/index.js +++ b/js/src/pages/edit-paid-ads-campaign/index.js @@ -220,6 +220,7 @@ const EditPaidAdsCampaign = () => { getHistory().push( getDashboardUrl() ); }; + assetEntityGroup.final_url = 'https://asvin-10upv2.ngrok.app/shop/'; return ( <> Date: Wed, 28 Jan 2026 10:38:03 +0400 Subject: [PATCH 086/123] fix(paid-ads): Adjust asset group editor logic for editing state --- .../paid-ads/asset-group/asset-group-editor/texts-editor.js | 2 +- .../asset-group/asset-group-header/asset-group-header.js | 5 +++-- js/src/pages/edit-paid-ads-campaign/index.js | 1 - 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js b/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js index 5c1feebe95..e2b510048d 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js @@ -221,7 +221,7 @@ export default function TextsEditor( { onClick={ handleAddClick } /> - { emptyFieldsCount > 0 && ( + { emptyFieldsCount > 0 && generateButtonText && ( { - console.log( adapter.baseAssetGroup[ ASSET_FORM_KEY.FINAL_URL ] ); async function loadAssets() { if ( hasLoadedInitialHomepageAssetsRef.current || - adapter.baseAssetGroup[ ASSET_FORM_KEY.FINAL_URL ] + adapter.baseAssetGroup[ ASSET_FORM_KEY.FINAL_URL ] || + isEditing ) { return; } diff --git a/js/src/pages/edit-paid-ads-campaign/index.js b/js/src/pages/edit-paid-ads-campaign/index.js index f544c7e6cc..7de3f8fdb8 100644 --- a/js/src/pages/edit-paid-ads-campaign/index.js +++ b/js/src/pages/edit-paid-ads-campaign/index.js @@ -220,7 +220,6 @@ const EditPaidAdsCampaign = () => { getHistory().push( getDashboardUrl() ); }; - assetEntityGroup.final_url = 'https://asvin-10upv2.ngrok.app/shop/'; return ( <> Date: Wed, 28 Jan 2026 10:58:36 +0400 Subject: [PATCH 087/123] build: Remove unused import and adjust commons.js maxSize --- js/src/components/paid-ads/asset-group/asset-group.js | 1 - package.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/js/src/components/paid-ads/asset-group/asset-group.js b/js/src/components/paid-ads/asset-group/asset-group.js index 92a39b6a57..2b2c0b45f3 100644 --- a/js/src/components/paid-ads/asset-group/asset-group.js +++ b/js/src/components/paid-ads/asset-group/asset-group.js @@ -18,7 +18,6 @@ import { recordGlaEvent } from '~/utils/tracks'; import useTargetAudienceFinalCountryCodes from '~/hooks/useTargetAudienceFinalCountryCodes'; import AssetGroupHeader from './asset-group-header'; import AssetGroupEditor from './asset-group-editor'; -import GenAIProgress from '../gen-ai-progress'; import { upsertActionedCampaign } from '~/utils/actionedCampaignsCache'; import './asset-group.scss'; diff --git a/package.json b/package.json index e364c39f94..71481adc27 100644 --- a/package.json +++ b/package.json @@ -153,7 +153,7 @@ }, { "path": "./js/build/commons.js", - "maxSize": "63.9 kB" + "maxSize": "64 kB" }, { "path": "./js/build/vendors.js", From 6a8b5a794884803378ceea93b04a567364dcab6c Mon Sep 17 00:00:00 2001 From: asvinb Date: Thu, 29 Jan 2026 12:49:55 +0400 Subject: [PATCH 088/123] fix(asset-group-header): Add isEditing to effect dependencies --- .../asset-group/asset-group-header/asset-group-header.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/src/components/paid-ads/asset-group/asset-group-header/asset-group-header.js b/js/src/components/paid-ads/asset-group/asset-group-header/asset-group-header.js index 72f436d5b0..c67e30e18d 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-header/asset-group-header.js +++ b/js/src/components/paid-ads/asset-group/asset-group-header/asset-group-header.js @@ -66,7 +66,7 @@ export default function AssetGroupHeader() { } loadAssets(); - }, [ fetchCampaignAssets, adapter.baseAssetGroup ] ); + }, [ fetchCampaignAssets, adapter.baseAssetGroup, isEditing ] ); if ( isFetchingAssets ) { return ; From a5d02f6f93f7c0e535721c13a8d0776efdc955a2 Mon Sep 17 00:00:00 2001 From: jjgrainger Date: Thu, 29 Jan 2026 15:16:02 +0000 Subject: [PATCH 089/123] Provide response data in exceptions --- src/Ads/AdsAssetGenerationService.php | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/Ads/AdsAssetGenerationService.php b/src/Ads/AdsAssetGenerationService.php index 4b7b055b65..7dd90a2231 100644 --- a/src/Ads/AdsAssetGenerationService.php +++ b/src/Ads/AdsAssetGenerationService.php @@ -9,6 +9,8 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait; use Automattic\WooCommerce\GoogleListingsAndAds\PluginHelper; use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\AssetFieldType; +use Automattic\WooCommerce\GoogleListingsAndAds\API\Google\ExceptionTrait; +use Automattic\WooCommerce\GoogleListingsAndAds\Exception\ExceptionWithResponseData; use Google\Ads\GoogleAds\V22\Services\GenerateTextRequest; use Google\Ads\GoogleAds\V22\Services\GenerateImagesRequest; use Google\Ads\GoogleAds\V22\Services\FinalUrlImageGenerationInput; @@ -29,6 +31,7 @@ class AdsAssetGenerationService implements OptionsAwareInterface, Service { use OptionsAwareTrait; use PluginHelper; + use ExceptionTrait; /** * The Asset Generation Service Client. @@ -129,7 +132,16 @@ public function generate_text( array $args = [] ): array { return $results; } catch ( ApiException $e ) { do_action( 'woocommerce_gla_ads_client_exception', $e, __METHOD__ ); - throw new Exception( __( 'Unable to generate text assets.', 'google-listings-and-ads' ) . ' ' . $e->getMessage(), $e->getCode() ); + + $errors = $this->get_exception_errors( $e ); + + throw new ExceptionWithResponseData( + /* translators: %s Error message */ + sprintf( __( 'Unable to generate text assets: %s', 'google-listings-and-ads' ), reset( $errors ) ), + $this->map_grpc_code_to_http_status_code( $e ), + $e, + [ 'errors' => $errors ] + ); } } @@ -192,7 +204,16 @@ public function generate_images( array $args = [] ): array { return $results; } catch ( ApiException $e ) { do_action( 'woocommerce_gla_ads_client_exception', $e, __METHOD__ ); - throw new Exception( __( 'Unable to generate image assets.', 'google-listings-and-ads' ) . ' ' . $e->getMessage(), $e->getCode() ); + + $errors = $this->get_exception_errors( $e ); + + throw new ExceptionWithResponseData( + /* translators: %s Error message */ + sprintf( __( 'Unable to generate image assets: %s', 'google-listings-and-ads' ), reset( $errors ) ), + $this->map_grpc_code_to_http_status_code( $e ), + $e, + [ 'errors' => $errors ] + ); } } From 4185b9f3fa65f10d879dc382eb194cc5e05c94a8 Mon Sep 17 00:00:00 2001 From: asvinb Date: Fri, 30 Jan 2026 17:55:24 +0400 Subject: [PATCH 090/123] feat(data): Apply character limits to GenAI text assets --- js/src/data/adapters.js | 11 +++++- js/src/data/utils.js | 84 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/js/src/data/adapters.js b/js/src/data/adapters.js index 35a51d73a6..925236e3a3 100644 --- a/js/src/data/adapters.js +++ b/js/src/data/adapters.js @@ -3,7 +3,10 @@ */ import { ASSET_TEXT_SPECS } from '~/components/paid-ads/assetSpecs'; import getCharacterCounter from '~/utils/getCharacterCounter'; -import { convertKeysFromSnakeCaseToCamelCase } from './utils'; +import { + convertKeysFromSnakeCaseToCamelCase, + applyAssetTextCharacterLimits, +} from './utils'; /** * @typedef {import('~/data/actions').Campaign} Campaign @@ -267,7 +270,7 @@ export function adaptRaiseAdsBudgetRecommendations( rawData ) { * Formats raw API items into a grouped object by type. * @param {Array} items The raw items array from API. * @param {string} valueKey The key to extract (e.g., 'text' or 'temporary_image_url'). - * @param {string} [filterType] Optional type to filter by. + * @param {string} [filterType] Optional type to filter by. Possible values can be headline, description, long_headline, marketing_image, square_marketing_image, portrait_marketing_image. * @return {Object} Groups of assets keyed by their type. */ export function adaptGenAIAssets( items = [], valueKey, filterType ) { @@ -290,5 +293,9 @@ export function adaptGenAIAssets( items = [], valueKey, filterType ) { data[ type ].push( value ); } + if ( valueKey === 'text' ) { + return applyAssetTextCharacterLimits( data, ASSET_TEXT_SPECS ); + } + return data; } diff --git a/js/src/data/utils.js b/js/src/data/utils.js index 7aef933ca1..57cf06f65e 100644 --- a/js/src/data/utils.js +++ b/js/src/data/utils.js @@ -263,6 +263,90 @@ export function convertKeysFromSnakeCaseToCamelCase( data ) { }, {} ); } +/** + * Applies character limits to asset texts based on provided specifications. + * + * @param {Object} assets The asset texts to apply character limits to. + * @param {Array} specs The specifications defining character limits for each asset type. + * @return {Object} The asset texts with character limits applied. + */ +export function applyAssetTextCharacterLimits( assets, specs ) { + return Object.fromEntries( + Object.entries( assets ).map( ( [ type, values ] ) => { + const spec = specs.find( ( s ) => s.key === type ); + if ( ! spec ) { + return [ type, values ]; + } + + const limits = Array.isArray( spec.maxCharacterCounts ) + ? spec.maxCharacterCounts + : Array.from( + { length: values.length }, + () => spec.maxCharacterCounts + ); + + const ellipsis = '…'; + + // Prepare positions with numeric limits (we’ll fill these first). + const positions = limits + .map( ( max, index ) => + typeof max === 'number' ? { index, max } : null + ) + .filter( Boolean ); + + // Sort positions by max ascending (tightest slots first). + positions.sort( ( a, b ) => a.max - b.max ); + + // Keep texts with their original index so ties preserve original order. + const texts = values.map( ( text, index ) => ( { text, index } ) ); + + // Sort texts by length ascending (shortest first). + // Tie-breaker keeps original order. + texts.sort( + ( a, b ) => a.text.length - b.text.length || a.index - b.index + ); + + const out = new Array( values.length ); + const usedTextIndexes = new Set(); + + // Assign shortest texts to tightest positions. + for ( let i = 0; i < positions.length; i++ ) { + const { index: posIndex, max } = positions[ i ]; + const picked = texts[ i ]; + + if ( ! picked ) { + break; + } + + usedTextIndexes.add( picked.index ); + + if ( picked.text.length <= max ) { + out[ posIndex ] = picked.text; + } else { + const sliceLength = Math.max( max - ellipsis.length, 0 ); + out[ posIndex ] = + picked.text.slice( 0, sliceLength ) + ellipsis; + } + } + + // Fill any remaining slots (including positions without max limits) + // with remaining texts in original order. + const remainingTexts = values.filter( + ( _, i ) => ! usedTextIndexes.has( i ) + ); + + let r = 0; + for ( let i = 0; i < out.length; i++ ) { + if ( out[ i ] === undefined ) { + out[ i ] = remainingTexts[ r++ ]; + } + } + + return [ type, out ]; + } ) + ); +} + /** * Report fields fetched from report API. * From 67e3823e330d579ff5a81caf155fdb46f9677f15 Mon Sep 17 00:00:00 2001 From: asvinb Date: Fri, 30 Jan 2026 18:54:35 +0400 Subject: [PATCH 091/123] fix(paid-ads): Reset AI asset flags on import failure --- js/src/components/paid-ads/campaign-assets-form.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/js/src/components/paid-ads/campaign-assets-form.js b/js/src/components/paid-ads/campaign-assets-form.js index d10656d4af..52c77205c9 100644 --- a/js/src/components/paid-ads/campaign-assets-form.js +++ b/js/src/components/paid-ads/campaign-assets-form.js @@ -284,6 +284,9 @@ export default function CampaignAssetsForm( { ...( hasSuggestedTextAssets ? textAssetsData : {} ), }; } catch ( error ) { + setHasAISuggestedTextAssets( false ); + setHasAISuggestedMediaAssets( false ); + createNotice( 'error', __( @@ -334,8 +337,6 @@ export default function CampaignAssetsForm( { setHasImportedAssets( hasNonEmptyAssets ); setBaseAssetGroup( nextAssetGroup ); - setHasAISuggestedTextAssets( false ); - setHasAISuggestedMediaAssets( false ); formContext.adapter.hideValidation(); }, From 1e5f7b3fc386fbfb9d8edb46486ec9452505cb3a Mon Sep 17 00:00:00 2001 From: Mukesh Panchal Date: Tue, 3 Feb 2026 14:57:02 +0530 Subject: [PATCH 092/123] Use __return_empty_array function for filter --- tests/Unit/Product/ProductFilterTest.php | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/tests/Unit/Product/ProductFilterTest.php b/tests/Unit/Product/ProductFilterTest.php index 7e6e727140..dad296db08 100644 --- a/tests/Unit/Product/ProductFilterTest.php +++ b/tests/Unit/Product/ProductFilterTest.php @@ -77,12 +77,7 @@ public function test_filter_sync_ready_products_with_no_filters_but_failed_sync( } public function test_filter_sync_ready_products_with_pre_filter() { - add_filter( - 'woocommerce_gla_get_sync_ready_products_pre_filter', - function ( $products ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found - return []; - } - ); + add_filter( 'woocommerce_gla_get_sync_ready_products_pre_filter', '__return_empty_array' ); $this->product_helper->expects( $this->never() )->method( 'is_sync_ready' ); $this->product_helper->expects( $this->never() )->method( 'is_sync_failed_recently' ); @@ -94,12 +89,7 @@ function ( $products ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionPara } public function test_filter_sync_ready_products_with_post_filter() { - add_filter( - 'woocommerce_gla_get_sync_ready_products_filter', - function ( $products ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found - return []; - } - ); + add_filter( 'woocommerce_gla_get_sync_ready_products_filter', '__return_empty_array' ); [ $product_a, $product_b, $product_c ] = $this->products; From e514030ef163a0663a983f463d98327b5c9935cd Mon Sep 17 00:00:00 2001 From: Mukesh Panchal Date: Tue, 3 Feb 2026 14:58:21 +0530 Subject: [PATCH 093/123] Revert unwanted changes --- tests/Unit/Coupon/WCCouponAdapterTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Unit/Coupon/WCCouponAdapterTest.php b/tests/Unit/Coupon/WCCouponAdapterTest.php index d00adc985c..e8e0932aef 100644 --- a/tests/Unit/Coupon/WCCouponAdapterTest.php +++ b/tests/Unit/Coupon/WCCouponAdapterTest.php @@ -280,13 +280,13 @@ public function test_brand_restrictions() { $coupon->set_product_ids( [ $product_3_id ] ); // Include brand 1 (product 1 and 2) for the coupon. - update_post_meta( $coupon->get_id(), 'product_brands', [ $brand_1['term_id'] ] ); + update_post_meta( $coupon->get_id(), 'product_brands', $brand_1['term_id'] ); // Exclude product 2 for the coupon. $coupon->set_excluded_product_ids( [ $product_2_id ] ); // Exclude brand 2 (product 3) for the coupon. - update_post_meta( $coupon->get_id(), 'exclude_product_brands', [ $brand_2['term_id'] ] ); + update_post_meta( $coupon->get_id(), 'exclude_product_brands', $brand_2['term_id'] ); $coupon->save(); From c316bac3491f35620b54bc9006b6e518e50aa8bc Mon Sep 17 00:00:00 2001 From: Mukesh Panchal Date: Tue, 3 Feb 2026 14:59:16 +0530 Subject: [PATCH 094/123] Update doc --- bin/GoogleAdsCleanupServices.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/GoogleAdsCleanupServices.php b/bin/GoogleAdsCleanupServices.php index 6f28316c37..097d972357 100644 --- a/bin/GoogleAdsCleanupServices.php +++ b/bin/GoogleAdsCleanupServices.php @@ -55,7 +55,7 @@ class GoogleAdsCleanupServices { // ConversionValueRuleService is now used in `ResourceNames::forGeoTargetConstant` in V22. // instead of the previous BatchJobServiceClient. See: // - https://github.com/googleads/google-ads-php/blob/v28.0.0/src/Google/Ads/GoogleAds/Util/V20/ResourceNames.php#L1433-L1439 - // - https://github.com/googleads/google-ads-php/blob/v31.1.0/src/Google/Ads/GoogleAds/Util/V22/ResourceNames.php#L1460 + // - https://github.com/googleads/google-ads-php/blob/v31.1.0/src/Google/Ads/GoogleAds/Util/V22/ResourceNames.php#L1457-L1463 'ConversionValueRule', ]; From 5b694f3f684f2247889af61907a0570258a48f14 Mon Sep 17 00:00:00 2001 From: Mukesh Panchal Date: Tue, 3 Feb 2026 15:02:27 +0530 Subject: [PATCH 095/123] Revert unwanted changes --- src/Coupon/WCCouponAdapter.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Coupon/WCCouponAdapter.php b/src/Coupon/WCCouponAdapter.php index cc1a8a7b67..18e53b5bf6 100644 --- a/src/Coupon/WCCouponAdapter.php +++ b/src/Coupon/WCCouponAdapter.php @@ -385,7 +385,6 @@ public function get_wc_coupon_id(): int { /** * * @param string $targetCountry - * * phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase */ public function setTargetCountry( $targetCountry ) { From 4cc5288d1dd91530f53c4ea9a2fc1f6c8291ee48 Mon Sep 17 00:00:00 2001 From: Mukesh Panchal Date: Tue, 3 Feb 2026 15:12:48 +0530 Subject: [PATCH 096/123] Minor fix --- src/Coupon/WCCouponAdapter.php | 4 +++- tests/Unit/Coupon/WCCouponAdapterTest.php | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Coupon/WCCouponAdapter.php b/src/Coupon/WCCouponAdapter.php index 18e53b5bf6..f33359090f 100644 --- a/src/Coupon/WCCouponAdapter.php +++ b/src/Coupon/WCCouponAdapter.php @@ -383,9 +383,11 @@ public function get_wc_coupon_id(): int { } /** + * Set the target country for the coupon. * * @param string $targetCountry - * phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase + * + * phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase */ public function setTargetCountry( $targetCountry ) { // set the new target country diff --git a/tests/Unit/Coupon/WCCouponAdapterTest.php b/tests/Unit/Coupon/WCCouponAdapterTest.php index e8e0932aef..d00adc985c 100644 --- a/tests/Unit/Coupon/WCCouponAdapterTest.php +++ b/tests/Unit/Coupon/WCCouponAdapterTest.php @@ -280,13 +280,13 @@ public function test_brand_restrictions() { $coupon->set_product_ids( [ $product_3_id ] ); // Include brand 1 (product 1 and 2) for the coupon. - update_post_meta( $coupon->get_id(), 'product_brands', $brand_1['term_id'] ); + update_post_meta( $coupon->get_id(), 'product_brands', [ $brand_1['term_id'] ] ); // Exclude product 2 for the coupon. $coupon->set_excluded_product_ids( [ $product_2_id ] ); // Exclude brand 2 (product 3) for the coupon. - update_post_meta( $coupon->get_id(), 'exclude_product_brands', $brand_2['term_id'] ); + update_post_meta( $coupon->get_id(), 'exclude_product_brands', [ $brand_2['term_id'] ] ); $coupon->save(); From 717c3bd78b21aa602c4f0a866b13b68ee7675591 Mon Sep 17 00:00:00 2001 From: Mukesh Panchal Date: Tue, 3 Feb 2026 15:46:53 +0530 Subject: [PATCH 097/123] Increase maxSize for build index.js --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index eb4a60c468..a7ecd51e09 100644 --- a/package.json +++ b/package.json @@ -149,7 +149,7 @@ }, { "path": "./js/build/index.js", - "maxSize": "19.2 kB" + "maxSize": "19.42 kB" }, { "path": "./js/build/commons.js", From 762d56ef4cfb5b7191fe20ff7ae571feeafb072a Mon Sep 17 00:00:00 2001 From: Alejandro Perez Martin Date: Wed, 4 Feb 2026 13:54:06 +0100 Subject: [PATCH 098/123] Reset selected images after adding them --- .../asset-group/asset-group-editor/gen-ai-image-picker/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.js b/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.js index 3c7fb5b6ea..d472121eac 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.js @@ -36,6 +36,7 @@ export default function GenAIImagePicker( { assetKey, onAddSelectedImages } ) { const handleOnAddSelectedImages = () => { onAddSelectedImages( selectedImages ); + setSelectedImages( [] ); }; const toggleImageSelection = ( src ) => { From 79fe9814d2cff6a8f8bdbf787ba91eb0fbb01415 Mon Sep 17 00:00:00 2001 From: asvinb Date: Thu, 5 Feb 2026 23:05:56 +0400 Subject: [PATCH 099/123] refactor(gen-ai): Consolidate asset generation into new hook --- .../paid-ads/campaign-assets-form.js | 28 ++- js/src/constants.js | 5 + js/src/data/actions.js | 44 +++-- js/src/hooks/useCreateGenAIAssets.js | 167 ++++++++++++++++++ 4 files changed, 226 insertions(+), 18 deletions(-) create mode 100644 js/src/hooks/useCreateGenAIAssets.js diff --git a/js/src/components/paid-ads/campaign-assets-form.js b/js/src/components/paid-ads/campaign-assets-form.js index 52c77205c9..1b62a35d13 100644 --- a/js/src/components/paid-ads/campaign-assets-form.js +++ b/js/src/components/paid-ads/campaign-assets-form.js @@ -10,7 +10,11 @@ import { isPlainObject } from 'lodash'; /** * Internal dependencies */ -import { ASSET_GROUP_KEY, ASSET_FORM_KEY } from '~/constants'; +import { + ASSET_GROUP_KEY, + ASSET_FORM_KEY, + GEN_AI_ASSET_TYPES, +} from '~/constants'; import AdaptiveForm from '~/components/adaptive-form'; import AppSpinner from '~/components/app-spinner'; import validateCampaign from '~/components/paid-ads/validateCampaign'; @@ -20,7 +24,7 @@ import useBudgetRecommendation from '~/hooks/useBudgetRecommendation'; import useRaiseBudgetRecommendations from '~/hooks/useRaiseBudgetRecommendations'; import useEventPropertiesFilter from '~/hooks/useEventPropertiesFilter'; import useDispatchCoreNotices from '~/hooks/useDispatchCoreNotices'; -import { useAppDispatch } from '~/data'; +import useCreateGenAIAssets from '~/hooks/useCreateGenAIAssets'; import { FILTER_BUDGET_RECOMMENDATIONS } from '~/utils/tracks'; import { API_NAMESPACE } from '~/data/constants'; import round from '~/utils/round'; @@ -200,7 +204,7 @@ export default function CampaignAssetsForm( { countryCodes, ...adaptiveFormProps } ) { - const { fetchGenAIMediaAssets, fetchGenAITextAssets } = useAppDispatch(); + const [ generateGenAIAssets ] = useCreateGenAIAssets(); const [ isFetchingAssets, setIsFetchingAssets ] = useState( false ); const initialAssetGroup = useMemo( () => { return convertAssetEntityGroupToFormValues( assetEntityGroup ); @@ -260,11 +264,19 @@ export default function CampaignAssetsForm( { const assetSuggestions = await apiFetch( { path } ); const url = assetSuggestions[ ASSET_GROUP_KEY.FINAL_URL ]; - const [ { data: textAssetsData }, { data: mediaAssetsData } ] = - await Promise.all( [ - fetchGenAITextAssets( url ), - fetchGenAIMediaAssets( url ), - ] ); + if ( ! url ) { + return assetSuggestions; + } + + const generatedGenAIAssets = await generateGenAIAssets( url, [ + { type: GEN_AI_ASSET_TYPES.TEXT }, + { type: GEN_AI_ASSET_TYPES.MEDIA }, + ] ); + + const textAssetsData = + generatedGenAIAssets[ GEN_AI_ASSET_TYPES.TEXT ]; + const mediaAssetsData = + generatedGenAIAssets[ GEN_AI_ASSET_TYPES.MEDIA ]; const hasSuggestedTextAssets = hasValidAIGeneratedAssets( REQUIRED_TEXT_ASSET_KEYS, diff --git a/js/src/constants.js b/js/src/constants.js index b6dd22737d..5f59eaa0ea 100644 --- a/js/src/constants.js +++ b/js/src/constants.js @@ -145,3 +145,8 @@ export const CAMPAIGN_BUDGET = 'CAMPAIGN_BUDGET'; export const MARGINAL_ROI_CAMPAIGN_BUDGET = 'MARGINAL_ROI_CAMPAIGN_BUDGET'; export const PMAX_IMPROVE_PERFORMANCE_MAX_AD_STRENGTH = 'IMPROVE_PERFORMANCE_MAX_AD_STRENGTH'; + +export const GEN_AI_ASSET_TYPES = { + TEXT: 'text', + MEDIA: 'media', +}; diff --git a/js/src/data/actions.js b/js/src/data/actions.js index 678d2f4bf6..c4d42d2282 100644 --- a/js/src/data/actions.js +++ b/js/src/data/actions.js @@ -1284,6 +1284,22 @@ export function* receiveAdsRecommendations( }; } +export function* receiveGenAIMediaAssets( url, data, assetType ) { + if ( ! data?.items ) { + return { + type: TYPES.RECEIVE_GEN_AI_MEDIA_ASSETS, + url, + data: {}, + }; + } + + return { + type: TYPES.RECEIVE_GEN_AI_MEDIA_ASSETS, + url, + data: adaptGenAIAssets( data.items, 'temporary_image_url', assetType ), + }; +} + /** * Fetches Gen AI media assets. If no asset type is provided, it will fetch all asset types. * @@ -1309,11 +1325,7 @@ export function* fetchGenAIMediaAssets( url, assetType ) { assetType ); - return { - type: TYPES.RECEIVE_GEN_AI_MEDIA_ASSETS, - url, - data: formattedData, - }; + return yield receiveGenAIMediaAssets( url, formattedData ); } catch ( error ) { handleApiError( error, @@ -1326,6 +1338,22 @@ export function* fetchGenAIMediaAssets( url, assetType ) { } } +export function* receiveGenAITextAssets( url, data, assetType ) { + if ( ! data?.items ) { + return { + type: TYPES.RECEIVE_GEN_AI_TEXT_ASSETS, + url, + data: {}, + }; + } + + return { + type: TYPES.RECEIVE_GEN_AI_TEXT_ASSETS, + url, + data: adaptGenAIAssets( data.items, 'text', assetType ), + }; +} + /** * Fetches Gen AI text assets. If no asset type is provided, it will fetch all asset types. * @@ -1350,11 +1378,7 @@ export function* fetchGenAITextAssets( url, assetType ) { assetType ); - return { - type: TYPES.RECEIVE_GEN_AI_TEXT_ASSETS, - url, - data: formattedData, - }; + return yield receiveGenAITextAssets( url, formattedData ); } catch ( error ) { handleApiError( error, diff --git a/js/src/hooks/useCreateGenAIAssets.js b/js/src/hooks/useCreateGenAIAssets.js new file mode 100644 index 0000000000..a55f4536a2 --- /dev/null +++ b/js/src/hooks/useCreateGenAIAssets.js @@ -0,0 +1,167 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useCallback, useState } from '@wordpress/element'; +import apiFetch from '@wordpress/api-fetch'; + +/** + * Internal dependencies + */ +import { GEN_AI_ASSET_TYPES } from '~/constants'; +import { useAppDispatch } from '~/data'; +import { API_NAMESPACE, REQUEST_ACTIONS } from '~/data/constants'; +import useDispatchCoreNotices from '~/hooks/useDispatchCoreNotices'; + +const useCreateGenAIAssets = () => { + const [ loading, setLoading ] = useState( false ); + const { createNotice } = useDispatchCoreNotices(); + const { receiveGenAITextAssets, receiveGenAIMediaAssets } = + useAppDispatch(); + + /** + * Helper function to process Gen AI API responses, handling both success and error cases. + * + * @param {Object} result - The result object from Promise.allSettled. + * @return {Object|null} - The parsed JSON data from the response, or null if there was an error. + */ + const processGenAIResponse = useCallback( + async ( result ) => { + // Handle rejected promises (Network errors or apiFetch-thrown errors) + if ( result.status === 'rejected' ) { + const errorResponse = result.reason; + + // Silently handle 400 errors (URL not eligible for suggestions) + if ( errorResponse && errorResponse.status === 400 ) { + return null; + } + + createNotice( + 'error', + errorResponse?.statusText || + __( + 'Unable to load AI-generated assets suggestions.', + 'google-listings-and-ads' + ) + ); + + return null; + } + + const response = result.value; + try { + const responseClone = response.clone(); + return await responseClone.json(); + } catch ( e ) { + createNotice( + 'error', + __( + 'An error occurred while processing AI-generated assets suggestions.', + 'google-listings-and-ads' + ) + ); + return null; + } + }, + [ createNotice ] + ); + + /** + * Generates Gen AI assets based on the provided URL and asset requests. + * + * @param {string} url - The final URL for which to generate assets. + * @param {Array} requests - An array of asset generation requests, each containing a type and an optional assetKey. type can be 'text' or 'media'. assetKey can be 'headline' for text or 'marketing_image' for media, or it can be undefined to fetch all types. + * @return {Promise} - A promise that resolves to the generated assets data, or undefined if no requests are processed. + */ + const generateAssets = useCallback( + async ( url, requests = [] ) => { + if ( ! url || requests.length === 0 ) { + return; + } + + setLoading( true ); + + // Initialize as empty arrays to avoid overwriting multiple requests of same type + const generatedAssets = { + [ GEN_AI_ASSET_TYPES.TEXT ]: [], + [ GEN_AI_ASSET_TYPES.MEDIA ]: [], + }; + + try { + const promises = requests.map( ( request ) => { + const isText = request.type === GEN_AI_ASSET_TYPES.TEXT; + const path = isText + ? `${ API_NAMESPACE }/ads/assets/generate-text` + : `${ API_NAMESPACE }/ads/assets/generate-images`; + + return apiFetch( { + path, + method: REQUEST_ACTIONS.POST, + parse: false, + data: { + final_url: url, + ...( request.assetKey + ? { types: [ request.assetKey ] } + : {} ), + }, + } ); + } ); + + const results = await Promise.allSettled( promises ); + + for ( let index = 0; index < results.length; index++ ) { + const { type, assetKey } = requests[ index ]; + const data = await processGenAIResponse( results[ index ] ); + + if ( ! data || ! data.items ) { + continue; + } + + if ( type === GEN_AI_ASSET_TYPES.TEXT ) { + const { data: textData } = receiveGenAITextAssets( + url, + data, + assetKey + ); + // Spread into array to keep results from multiple requests + generatedAssets[ GEN_AI_ASSET_TYPES.TEXT ].push( + ...( textData || [] ) + ); + } else if ( type === GEN_AI_ASSET_TYPES.MEDIA ) { + const { data: mediaData } = receiveGenAIMediaAssets( + url, + data, + assetKey + ); + generatedAssets[ GEN_AI_ASSET_TYPES.MEDIA ].push( + ...( mediaData || [] ) + ); + } + } + + return generatedAssets; + } catch ( error ) { + // Catch unexpected runtime errors + createNotice( + 'error', + __( + 'An unexpected error occurred.', + 'google-listings-and-ads' + ) + ); + } finally { + setLoading( false ); + } + }, + [ + processGenAIResponse, + receiveGenAITextAssets, + receiveGenAIMediaAssets, + createNotice, + ] + ); + + return [ generateAssets, loading ]; +}; + +export default useCreateGenAIAssets; From fa90d7826255fa8b18791366ff07c5fabe6f3616 Mon Sep 17 00:00:00 2001 From: asvinb Date: Thu, 5 Feb 2026 23:14:15 +0400 Subject: [PATCH 100/123] refactor: Use new hook for GenAI asset generation --- .../asset-group-editor/images-selector.js | 16 ++++++------- .../asset-group-editor/texts-editor.js | 24 +++++++++++-------- js/src/hooks/useCreateGenAIAssets.js | 8 +++---- 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/images-selector.js b/js/src/components/paid-ads/asset-group/asset-group-editor/images-selector.js index 9e5b0bbc64..51d40595b0 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/images-selector.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/images-selector.js @@ -8,9 +8,10 @@ import { useState, useEffect, useRef } from '@wordpress/element'; /** * Internal dependencies */ -import { useAppDispatch } from '~/data'; +import { GEN_AI_ASSET_TYPES } from '~/constants'; import useDispatchCoreNotices from '~/hooks/useDispatchCoreNotices'; import { useAdaptiveFormContext } from '~/components/adaptive-form'; +import useCreateGenAIAssets from '~/hooks/useCreateGenAIAssets'; import useCroppedImageSelector from '~/hooks/useCroppedImageSelector'; import AppTooltip from '~/components/app-tooltip'; import AssetItemActionButton, { @@ -52,9 +53,8 @@ export default function ImagesSelector( { } ) { const { values } = useAdaptiveFormContext(); const updateImagesRef = useRef(); - const [ isGeneratingAssets, setIsGeneratingAssets ] = useState( false ); const [ awaitingActionImage, setAwaitingActionImage ] = useState( null ); - const { fetchGenAIMediaAssets } = useAppDispatch(); + const [ generateAssets, isGenerating ] = useCreateGenAIAssets(); const { createNotice } = useDispatchCoreNotices(); const [ images, setImages ] = useState( () => // The asset images fetched from Google Ads are only URLs. @@ -152,11 +152,11 @@ export default function ImagesSelector( { }; const handleGenerateClick = async () => { - setIsGeneratingAssets( true ); - try { const { final_url: finalUrl } = values; - await fetchGenAIMediaAssets( finalUrl, assetKey ); + await generateAssets( finalUrl, [ + { type: GEN_AI_ASSET_TYPES.MEDIA, assetKey }, + ] ); } catch ( error ) { createNotice( 'error', @@ -165,8 +165,6 @@ export default function ImagesSelector( { 'google-listings-and-ads' ) ); - } finally { - setIsGeneratingAssets( false ); } }; @@ -191,7 +189,7 @@ export default function ImagesSelector( { action={ ACTION_TYPES.GENERATE } text={ generateButtonText } onClick={ handleGenerateClick } - loading={ isGeneratingAssets } + loading={ isGenerating } /> ) } diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js b/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js index e2b510048d..6bc72ecf98 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js @@ -9,9 +9,10 @@ import GridiconCrossSmall from 'gridicons/dist/cross-small'; /** * Internal dependencies */ -import { useAppDispatch } from '~/data'; +import { GEN_AI_ASSET_TYPES } from '~/constants'; import useDispatchCoreNotices from '~/hooks/useDispatchCoreNotices'; import useGenAITextAssets from '~/hooks/useGenAITextAssets'; +import useCreateGenAIAssets from '~/hooks/useCreateGenAIAssets'; import AppButton from '~/components/app-button'; import AppInputControl from '~/components/app-input-control'; import AssetItemActionButton, { @@ -64,9 +65,8 @@ export default function TextsEditor( { } ) { const updateTextsRef = useRef(); const { createNotice } = useDispatchCoreNotices(); - const { fetchGenAITextAssets } = useAppDispatch(); + const [ generateAssets, isGenerating ] = useCreateGenAIAssets(); const [ texts, setTexts ] = useState( initialTexts ); - const [ isGeneratingAssets, setIsGeneratingAssets ] = useState( false ); const { assets: genAITextAssets } = useGenAITextAssets( finalUrl, assetKey @@ -112,11 +112,17 @@ export default function TextsEditor( { }; const handleGenerateClick = async () => { - setIsGeneratingAssets( true ); - try { - const response = await fetchGenAITextAssets( finalUrl, assetKey ); - const generatedTextAssets = response?.data?.[ assetKey ] ?? []; + const generatedAssets = await generateAssets( finalUrl, [ + { + type: GEN_AI_ASSET_TYPES.TEXT, + assetKey, + }, + ] ); + + const generatedTextAssets = + generatedAssets?.[ GEN_AI_ASSET_TYPES.TEXT ]?.[ assetKey ] ?? + []; const { assets: updatedTexts, updatedCount } = fillEmptyAssetSlots( texts, @@ -142,8 +148,6 @@ export default function TextsEditor( { 'google-listings-and-ads' ) ); - } finally { - setIsGeneratingAssets( false ); } }; @@ -226,7 +230,7 @@ export default function TextsEditor( { action={ ACTION_TYPES.GENERATE } text={ generateButtonText } onClick={ handleGenerateClick } - loading={ isGeneratingAssets } + loading={ isGenerating } /> ) } diff --git a/js/src/hooks/useCreateGenAIAssets.js b/js/src/hooks/useCreateGenAIAssets.js index a55f4536a2..2ca45e7ae1 100644 --- a/js/src/hooks/useCreateGenAIAssets.js +++ b/js/src/hooks/useCreateGenAIAssets.js @@ -14,7 +14,7 @@ import { API_NAMESPACE, REQUEST_ACTIONS } from '~/data/constants'; import useDispatchCoreNotices from '~/hooks/useDispatchCoreNotices'; const useCreateGenAIAssets = () => { - const [ loading, setLoading ] = useState( false ); + const [ isGenerating, setIsGenerating ] = useState( false ); const { createNotice } = useDispatchCoreNotices(); const { receiveGenAITextAssets, receiveGenAIMediaAssets } = useAppDispatch(); @@ -79,7 +79,7 @@ const useCreateGenAIAssets = () => { return; } - setLoading( true ); + setIsGenerating( true ); // Initialize as empty arrays to avoid overwriting multiple requests of same type const generatedAssets = { @@ -150,7 +150,7 @@ const useCreateGenAIAssets = () => { ) ); } finally { - setLoading( false ); + setIsGenerating( false ); } }, [ @@ -161,7 +161,7 @@ const useCreateGenAIAssets = () => { ] ); - return [ generateAssets, loading ]; + return [ generateAssets, isGenerating ]; }; export default useCreateGenAIAssets; From 2996bc853088c94497211746ea9cca87ae0e67fd Mon Sep 17 00:00:00 2001 From: asvinb Date: Thu, 5 Feb 2026 23:15:49 +0400 Subject: [PATCH 101/123] refactor: Remove unused Gen AI asset fetching functions --- js/src/data/actions.js | 75 ------------------------------------------ 1 file changed, 75 deletions(-) diff --git a/js/src/data/actions.js b/js/src/data/actions.js index c4d42d2282..620104b16b 100644 --- a/js/src/data/actions.js +++ b/js/src/data/actions.js @@ -1300,44 +1300,6 @@ export function* receiveGenAIMediaAssets( url, data, assetType ) { }; } -/** - * Fetches Gen AI media assets. If no asset type is provided, it will fetch all asset types. - * - * @param {string} url The final URL for which to generate media assets. - * @param {'marketing_image'|'square_marketing_image'|'portrait_marketing_image'|undefined} [assetType] - The type of media asset to retrieve. - * @return {Object} Action object to save generated media assets. - * @throws Will throw an error if the request failed. - */ -export function* fetchGenAIMediaAssets( url, assetType ) { - try { - const response = yield apiFetch( { - path: `${ API_NAMESPACE }/ads/assets/generate-images`, - method: REQUEST_ACTIONS.POST, - data: { - final_url: url, - types: assetType ? [ assetType ] : undefined, - }, - } ); - - const formattedData = adaptGenAIAssets( - response.items, - 'temporary_image_url', - assetType - ); - - return yield receiveGenAIMediaAssets( url, formattedData ); - } catch ( error ) { - handleApiError( - error, - __( - 'There was an error generating media assets.', - 'google-listings-and-ads' - ) - ); - throw error; - } -} - export function* receiveGenAITextAssets( url, data, assetType ) { if ( ! data?.items ) { return { @@ -1353,40 +1315,3 @@ export function* receiveGenAITextAssets( url, data, assetType ) { data: adaptGenAIAssets( data.items, 'text', assetType ), }; } - -/** - * Fetches Gen AI text assets. If no asset type is provided, it will fetch all asset types. - * - * @param {string} url The final URL for which to generate text assets. - * @param {'headline'|'long_headline'|'description'|undefined} [assetType] - The type of text asset to retrieve. - * @return {Object} Action object to save generated text assets. - */ -export function* fetchGenAITextAssets( url, assetType ) { - try { - const response = yield apiFetch( { - path: `${ API_NAMESPACE }/ads/assets/generate-text`, - method: REQUEST_ACTIONS.POST, - data: { - final_url: url, - types: assetType ? [ assetType ] : undefined, - }, - } ); - - const formattedData = adaptGenAIAssets( - response.items, - 'text', - assetType - ); - - return yield receiveGenAITextAssets( url, formattedData ); - } catch ( error ) { - handleApiError( - error, - __( - 'There was an error generating text assets.', - 'google-listings-and-ads' - ) - ); - throw error; - } -} From 82c200f1ced854e3d25fab55a28d373f68f2001a Mon Sep 17 00:00:00 2001 From: asvinb Date: Thu, 5 Feb 2026 23:59:31 +0400 Subject: [PATCH 102/123] refactor(gen-ai-assets): Store assets by type, deduplicate, and handle errors --- .../asset-group-editor/texts-editor.js | 10 +++ .../paid-ads/campaign-assets-form.js | 67 ++++++++++++------- js/src/data/actions.js | 4 ++ js/src/data/reducer.js | 61 ++++++++++++++--- js/src/hooks/useCreateGenAIAssets.js | 30 +++++---- 5 files changed, 125 insertions(+), 47 deletions(-) diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js b/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js index 6bc72ecf98..2a5b36e1e1 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js @@ -123,6 +123,16 @@ export default function TextsEditor( { const generatedTextAssets = generatedAssets?.[ GEN_AI_ASSET_TYPES.TEXT ]?.[ assetKey ] ?? []; + console.log( + 'Generated text assets:', + generatedTextAssets, + 'for URL:', + finalUrl, + 'and asset key:', + assetKey, + 'with response:', + generatedAssets + ); // Debug log const { assets: updatedTexts, updatedCount } = fillEmptyAssetSlots( texts, diff --git a/js/src/components/paid-ads/campaign-assets-form.js b/js/src/components/paid-ads/campaign-assets-form.js index 1b62a35d13..4b16dc3ab8 100644 --- a/js/src/components/paid-ads/campaign-assets-form.js +++ b/js/src/components/paid-ads/campaign-assets-form.js @@ -268,33 +268,48 @@ export default function CampaignAssetsForm( { return assetSuggestions; } - const generatedGenAIAssets = await generateGenAIAssets( url, [ - { type: GEN_AI_ASSET_TYPES.TEXT }, - { type: GEN_AI_ASSET_TYPES.MEDIA }, - ] ); - - const textAssetsData = - generatedGenAIAssets[ GEN_AI_ASSET_TYPES.TEXT ]; - const mediaAssetsData = - generatedGenAIAssets[ GEN_AI_ASSET_TYPES.MEDIA ]; - - const hasSuggestedTextAssets = hasValidAIGeneratedAssets( - REQUIRED_TEXT_ASSET_KEYS, - textAssetsData - ); - - const hasSuggestedMediaAssets = hasValidAIGeneratedAssets( - REQUIRED_MEDIA_ASSET_KEYS, - mediaAssetsData - ); + try { + const generatedGenAIAssets = await generateGenAIAssets( + url, + [ + { type: GEN_AI_ASSET_TYPES.TEXT }, + { type: GEN_AI_ASSET_TYPES.MEDIA }, + ] + ); + + const textAssetsData = + generatedGenAIAssets[ GEN_AI_ASSET_TYPES.TEXT ]; + const mediaAssetsData = + generatedGenAIAssets[ GEN_AI_ASSET_TYPES.MEDIA ]; + + const hasSuggestedTextAssets = hasValidAIGeneratedAssets( + REQUIRED_TEXT_ASSET_KEYS, + textAssetsData + ); + + const hasSuggestedMediaAssets = hasValidAIGeneratedAssets( + REQUIRED_MEDIA_ASSET_KEYS, + mediaAssetsData + ); + + setHasAISuggestedTextAssets( hasSuggestedTextAssets ); + setHasAISuggestedMediaAssets( hasSuggestedMediaAssets ); + + return { + ...assetSuggestions, + ...( hasSuggestedTextAssets ? textAssetsData : {} ), + }; + } catch ( genAIError ) { + createNotice( + 'error', + __( + 'Unable to generate AI suggested assets.', + 'google-listings-and-ads' + ) + ); - setHasAISuggestedTextAssets( hasSuggestedTextAssets ); - setHasAISuggestedMediaAssets( hasSuggestedMediaAssets ); - - return { - ...assetSuggestions, - ...( hasSuggestedTextAssets ? textAssetsData : {} ), - }; + return assetSuggestions; + } } catch ( error ) { setHasAISuggestedTextAssets( false ); setHasAISuggestedMediaAssets( false ); diff --git a/js/src/data/actions.js b/js/src/data/actions.js index 620104b16b..bcbc6ec0e1 100644 --- a/js/src/data/actions.js +++ b/js/src/data/actions.js @@ -1289,6 +1289,7 @@ export function* receiveGenAIMediaAssets( url, data, assetType ) { return { type: TYPES.RECEIVE_GEN_AI_MEDIA_ASSETS, url, + assetType, data: {}, }; } @@ -1296,6 +1297,7 @@ export function* receiveGenAIMediaAssets( url, data, assetType ) { return { type: TYPES.RECEIVE_GEN_AI_MEDIA_ASSETS, url, + assetType, data: adaptGenAIAssets( data.items, 'temporary_image_url', assetType ), }; } @@ -1305,6 +1307,7 @@ export function* receiveGenAITextAssets( url, data, assetType ) { return { type: TYPES.RECEIVE_GEN_AI_TEXT_ASSETS, url, + assetType, data: {}, }; } @@ -1312,6 +1315,7 @@ export function* receiveGenAITextAssets( url, data, assetType ) { return { type: TYPES.RECEIVE_GEN_AI_TEXT_ASSETS, url, + assetType, data: adaptGenAIAssets( data.items, 'text', assetType ), }; } diff --git a/js/src/data/reducer.js b/js/src/data/reducer.js index 85afadcc19..7f09db3e07 100644 --- a/js/src/data/reducer.js +++ b/js/src/data/reducer.js @@ -633,20 +633,65 @@ const reducer = ( state = DEFAULT_STATE, action ) => { } case TYPES.RECEIVE_GEN_AI_MEDIA_ASSETS: { - const { url, data } = action; - + const { url, data, assetType } = action; const existingMedia = state.gen_ai_assets?.[ url ]?.media ?? {}; - return setIn( state, [ 'gen_ai_assets', url, 'media' ], { - ...existingMedia, - ...data, - } ); + let updatedMedia = {}; + + if ( assetType ) { + const currentList = existingMedia[ assetType ] ?? []; + const newList = data[ assetType ] ?? []; + + // De-duplicate based on the 'url' property of the media item + const deDuplicated = [ + ...new Map( + [ ...currentList, ...newList ].map( ( item ) => [ + item.url, + item, + ] ) + ).values(), + ]; + + updatedMedia = { + ...existingMedia, + [ assetType ]: deDuplicated, + }; + } else { + updatedMedia = { + ...existingMedia, + ...data, + }; + } + + return setIn( + state, + [ 'gen_ai_assets', url, 'media' ], + updatedMedia + ); } case TYPES.RECEIVE_GEN_AI_TEXT_ASSETS: { - const { url, data } = action; + const { url, data, assetType } = action; + const existingText = state.gen_ai_assets?.[ url ]?.text ?? {}; + + const updatedText = assetType + ? { + ...existingText, + [ assetType ]: [ + ...( existingText[ assetType ] ?? [] ), + ...( data[ assetType ] ?? [] ), + ], + } + : { + ...existingText, + ...data, + }; - return setIn( state, [ 'gen_ai_assets', url, 'text' ], data ); + return setIn( + state, + [ 'gen_ai_assets', url, 'text' ], + updatedText + ); } // Page will be reloaded after all accounts have been disconnected, so no need to mutate state. diff --git a/js/src/hooks/useCreateGenAIAssets.js b/js/src/hooks/useCreateGenAIAssets.js index 2ca45e7ae1..dbe7e3f8d5 100644 --- a/js/src/hooks/useCreateGenAIAssets.js +++ b/js/src/hooks/useCreateGenAIAssets.js @@ -118,24 +118,28 @@ const useCreateGenAIAssets = () => { } if ( type === GEN_AI_ASSET_TYPES.TEXT ) { - const { data: textData } = receiveGenAITextAssets( + const { data: textData } = await receiveGenAITextAssets( url, data, assetKey ); - // Spread into array to keep results from multiple requests - generatedAssets[ GEN_AI_ASSET_TYPES.TEXT ].push( - ...( textData || [] ) - ); + + generatedAssets[ GEN_AI_ASSET_TYPES.TEXT ] = { + ...generatedAssets[ GEN_AI_ASSET_TYPES.TEXT ], + ...textData, + }; } else if ( type === GEN_AI_ASSET_TYPES.MEDIA ) { - const { data: mediaData } = receiveGenAIMediaAssets( - url, - data, - assetKey - ); - generatedAssets[ GEN_AI_ASSET_TYPES.MEDIA ].push( - ...( mediaData || [] ) - ); + const { data: mediaData } = + await receiveGenAIMediaAssets( + url, + data, + assetKey + ); + + generatedAssets[ GEN_AI_ASSET_TYPES.MEDIA ] = { + ...generatedAssets[ GEN_AI_ASSET_TYPES.MEDIA ], + ...mediaData, + }; } } From 87772a1063d0e869fd21b49a6b048467066b2710 Mon Sep 17 00:00:00 2001 From: asvinb Date: Fri, 6 Feb 2026 10:48:39 +0400 Subject: [PATCH 103/123] refactor(data): Streamline Gen AI asset merging and cleanup --- .../asset-group-editor/texts-editor.js | 10 ----- js/src/data/adapters.js | 4 +- js/src/data/reducer.js | 40 +++++++------------ 3 files changed, 16 insertions(+), 38 deletions(-) diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js b/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js index 2a5b36e1e1..6bc72ecf98 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js @@ -123,16 +123,6 @@ export default function TextsEditor( { const generatedTextAssets = generatedAssets?.[ GEN_AI_ASSET_TYPES.TEXT ]?.[ assetKey ] ?? []; - console.log( - 'Generated text assets:', - generatedTextAssets, - 'for URL:', - finalUrl, - 'and asset key:', - assetKey, - 'with response:', - generatedAssets - ); // Debug log const { assets: updatedTexts, updatedCount } = fillEmptyAssetSlots( texts, diff --git a/js/src/data/adapters.js b/js/src/data/adapters.js index 925236e3a3..6861f11cf3 100644 --- a/js/src/data/adapters.js +++ b/js/src/data/adapters.js @@ -280,8 +280,8 @@ export function adaptGenAIAssets( items = [], valueKey, filterType ) { const { type, [ valueKey ]: value } = item; // Skip if: - // 1. We have a filter and it doesn't match - // 2. The value for the specified key is empty/null + // We have a filter and it doesn't match + // The value for the specified key is empty/null if ( ( filterType && type !== filterType ) || ! value ) { continue; } diff --git a/js/src/data/reducer.js b/js/src/data/reducer.js index 7f09db3e07..19f14d8ca2 100644 --- a/js/src/data/reducer.js +++ b/js/src/data/reducer.js @@ -636,32 +636,20 @@ const reducer = ( state = DEFAULT_STATE, action ) => { const { url, data, assetType } = action; const existingMedia = state.gen_ai_assets?.[ url ]?.media ?? {}; - let updatedMedia = {}; - - if ( assetType ) { - const currentList = existingMedia[ assetType ] ?? []; - const newList = data[ assetType ] ?? []; - - // De-duplicate based on the 'url' property of the media item - const deDuplicated = [ - ...new Map( - [ ...currentList, ...newList ].map( ( item ) => [ - item.url, - item, - ] ) - ).values(), - ]; - - updatedMedia = { - ...existingMedia, - [ assetType ]: deDuplicated, - }; - } else { - updatedMedia = { - ...existingMedia, - ...data, - }; - } + const updatedMedia = assetType + ? { + ...existingMedia, + [ assetType ]: [ + ...new Set( [ + ...( existingMedia[ assetType ] ?? [] ), + ...( data[ assetType ] ?? [] ), + ] ), + ], + } + : { + ...existingMedia, + ...data, + }; return setIn( state, From 58296970aa06c5017a6ce0d48cd44f1722e64750 Mon Sep 17 00:00:00 2001 From: asvinb Date: Fri, 6 Feb 2026 10:53:14 +0400 Subject: [PATCH 104/123] refactor(gen-ai): Rename asset generation state and function variables --- .../asset-group-editor/images-selector.js | 4 ++-- .../asset-group/asset-group-editor/texts-editor.js | 4 ++-- js/src/components/paid-ads/campaign-assets-form.js | 13 +++++-------- js/src/hooks/useCreateGenAIAssets.js | 8 ++++---- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/images-selector.js b/js/src/components/paid-ads/asset-group/asset-group-editor/images-selector.js index 51d40595b0..491b6fab09 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/images-selector.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/images-selector.js @@ -54,7 +54,7 @@ export default function ImagesSelector( { const { values } = useAdaptiveFormContext(); const updateImagesRef = useRef(); const [ awaitingActionImage, setAwaitingActionImage ] = useState( null ); - const [ generateAssets, isGenerating ] = useCreateGenAIAssets(); + const [ generateAssets, isGeneratingAssets ] = useCreateGenAIAssets(); const { createNotice } = useDispatchCoreNotices(); const [ images, setImages ] = useState( () => // The asset images fetched from Google Ads are only URLs. @@ -189,7 +189,7 @@ export default function ImagesSelector( { action={ ACTION_TYPES.GENERATE } text={ generateButtonText } onClick={ handleGenerateClick } - loading={ isGenerating } + loading={ isGeneratingAssets } /> ) } diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js b/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js index 6bc72ecf98..b0096108d0 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js @@ -65,7 +65,7 @@ export default function TextsEditor( { } ) { const updateTextsRef = useRef(); const { createNotice } = useDispatchCoreNotices(); - const [ generateAssets, isGenerating ] = useCreateGenAIAssets(); + const [ generateAssets, isGeneratingAssets ] = useCreateGenAIAssets(); const [ texts, setTexts ] = useState( initialTexts ); const { assets: genAITextAssets } = useGenAITextAssets( finalUrl, @@ -230,7 +230,7 @@ export default function TextsEditor( { action={ ACTION_TYPES.GENERATE } text={ generateButtonText } onClick={ handleGenerateClick } - loading={ isGenerating } + loading={ isGeneratingAssets } /> ) } diff --git a/js/src/components/paid-ads/campaign-assets-form.js b/js/src/components/paid-ads/campaign-assets-form.js index 4b16dc3ab8..da31f17e7c 100644 --- a/js/src/components/paid-ads/campaign-assets-form.js +++ b/js/src/components/paid-ads/campaign-assets-form.js @@ -204,7 +204,7 @@ export default function CampaignAssetsForm( { countryCodes, ...adaptiveFormProps } ) { - const [ generateGenAIAssets ] = useCreateGenAIAssets(); + const [ generateAssets ] = useCreateGenAIAssets(); const [ isFetchingAssets, setIsFetchingAssets ] = useState( false ); const initialAssetGroup = useMemo( () => { return convertAssetEntityGroupToFormValues( assetEntityGroup ); @@ -269,13 +269,10 @@ export default function CampaignAssetsForm( { } try { - const generatedGenAIAssets = await generateGenAIAssets( - url, - [ - { type: GEN_AI_ASSET_TYPES.TEXT }, - { type: GEN_AI_ASSET_TYPES.MEDIA }, - ] - ); + const generatedGenAIAssets = await generateAssets( url, [ + { type: GEN_AI_ASSET_TYPES.TEXT }, + { type: GEN_AI_ASSET_TYPES.MEDIA }, + ] ); const textAssetsData = generatedGenAIAssets[ GEN_AI_ASSET_TYPES.TEXT ]; diff --git a/js/src/hooks/useCreateGenAIAssets.js b/js/src/hooks/useCreateGenAIAssets.js index dbe7e3f8d5..95f7191467 100644 --- a/js/src/hooks/useCreateGenAIAssets.js +++ b/js/src/hooks/useCreateGenAIAssets.js @@ -14,7 +14,7 @@ import { API_NAMESPACE, REQUEST_ACTIONS } from '~/data/constants'; import useDispatchCoreNotices from '~/hooks/useDispatchCoreNotices'; const useCreateGenAIAssets = () => { - const [ isGenerating, setIsGenerating ] = useState( false ); + const [ isGeneratingAssets, setIsGeneratingAssets ] = useState( false ); const { createNotice } = useDispatchCoreNotices(); const { receiveGenAITextAssets, receiveGenAIMediaAssets } = useAppDispatch(); @@ -79,7 +79,7 @@ const useCreateGenAIAssets = () => { return; } - setIsGenerating( true ); + setIsGeneratingAssets( true ); // Initialize as empty arrays to avoid overwriting multiple requests of same type const generatedAssets = { @@ -154,7 +154,7 @@ const useCreateGenAIAssets = () => { ) ); } finally { - setIsGenerating( false ); + setIsGeneratingAssets( false ); } }, [ @@ -165,7 +165,7 @@ const useCreateGenAIAssets = () => { ] ); - return [ generateAssets, isGenerating ]; + return [ generateAssets, isGeneratingAssets ]; }; export default useCreateGenAIAssets; From 9c248291aed488a0f137ff3db74a3f2a535c4d3b Mon Sep 17 00:00:00 2001 From: asvinb Date: Fri, 6 Feb 2026 11:01:55 +0400 Subject: [PATCH 105/123] docs(hooks): Add JSDoc to useCreateGenAIAssets hook --- js/src/hooks/useCreateGenAIAssets.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/js/src/hooks/useCreateGenAIAssets.js b/js/src/hooks/useCreateGenAIAssets.js index 95f7191467..38c06206cd 100644 --- a/js/src/hooks/useCreateGenAIAssets.js +++ b/js/src/hooks/useCreateGenAIAssets.js @@ -1,18 +1,23 @@ /** * External dependencies */ +import apiFetch from '@wordpress/api-fetch'; import { __ } from '@wordpress/i18n'; import { useCallback, useState } from '@wordpress/element'; -import apiFetch from '@wordpress/api-fetch'; /** * Internal dependencies */ -import { GEN_AI_ASSET_TYPES } from '~/constants'; import { useAppDispatch } from '~/data'; +import { GEN_AI_ASSET_TYPES } from '~/constants'; import { API_NAMESPACE, REQUEST_ACTIONS } from '~/data/constants'; import useDispatchCoreNotices from '~/hooks/useDispatchCoreNotices'; +/** + * Custom hook to generate Gen AI assets for a given URL and asset requests. + * + * @return {Array} - An array containing the generateAssets function and a boolean indicating if assets are currently being generated. + */ const useCreateGenAIAssets = () => { const [ isGeneratingAssets, setIsGeneratingAssets ] = useState( false ); const { createNotice } = useDispatchCoreNotices(); From 919b355697ecf94becce1edbaa43877a9de07d8d Mon Sep 17 00:00:00 2001 From: asvinb Date: Fri, 6 Feb 2026 11:21:08 +0400 Subject: [PATCH 106/123] build: Update bundle max sizes --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a7ecd51e09..1ddfe5948d 100644 --- a/package.json +++ b/package.json @@ -149,11 +149,11 @@ }, { "path": "./js/build/index.js", - "maxSize": "19.42 kB" + "maxSize": "19.6 kB" }, { "path": "./js/build/commons.js", - "maxSize": "64 kB" + "maxSize": "65 kB" }, { "path": "./js/build/vendors.js", From e540522878a7b78f9ede36c94fc9ef629fe71f9e Mon Sep 17 00:00:00 2001 From: asvinb Date: Fri, 6 Feb 2026 11:49:55 +0400 Subject: [PATCH 107/123] test(e2e): Update expected ad copy in paid campaign tests --- tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js b/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js index bbba8a43a5..708a9469a0 100644 --- a/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js +++ b/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js @@ -722,7 +722,7 @@ test.describe( 'Add paid campaign', () => { longHeadlineInputsValues.length - 1 ]; expect( lastValue ).toBe( - 'Smart shopping starts right here' + 'Upgrade your everyday shopping experience' ); } ); } ); @@ -785,7 +785,7 @@ test.describe( 'Add paid campaign', () => { descriptionInputsValues.length - 1 ]; expect( lastValue ).toBe( - 'Quality products backed by great support.' + 'Browse top picks and enjoy exclusive savings.' ); } ); } ); From 86df4ff4480b0537a29840d58bfc73b18e5b06e2 Mon Sep 17 00:00:00 2001 From: asvinb Date: Mon, 23 Feb 2026 12:45:31 +0400 Subject: [PATCH 108/123] add(paid-ads): Increase headline asset max character count --- js/src/components/paid-ads/assetSpecs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/src/components/paid-ads/assetSpecs.js b/js/src/components/paid-ads/assetSpecs.js index cd6c488e2e..2eb452c231 100644 --- a/js/src/components/paid-ads/assetSpecs.js +++ b/js/src/components/paid-ads/assetSpecs.js @@ -212,7 +212,7 @@ const ASSET_TEXT_SPECS = [ key: ASSET_FORM_KEY.HEADLINE, min: 3, max: 5, - maxCharacterCounts: [ 15, 30, 30, 30, 30 ], + maxCharacterCounts: [ 30, 30, 30, 30, 30 ], heading: _x( 'Headlines', 'Plural asset field name as the heading', From d4d747ebc29419669acbcb801a75c21c46369ae7 Mon Sep 17 00:00:00 2001 From: asvinb Date: Mon, 23 Feb 2026 18:44:12 +0400 Subject: [PATCH 109/123] refactor(paid-ads): simplify headline max character counts --- js/src/components/paid-ads/assetSpecs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/src/components/paid-ads/assetSpecs.js b/js/src/components/paid-ads/assetSpecs.js index 2eb452c231..0bea983862 100644 --- a/js/src/components/paid-ads/assetSpecs.js +++ b/js/src/components/paid-ads/assetSpecs.js @@ -212,7 +212,7 @@ const ASSET_TEXT_SPECS = [ key: ASSET_FORM_KEY.HEADLINE, min: 3, max: 5, - maxCharacterCounts: [ 30, 30, 30, 30, 30 ], + maxCharacterCounts: 30, heading: _x( 'Headlines', 'Plural asset field name as the heading', From e0dda579a22d8e1cac9557fa65fe5853365d3729 Mon Sep 17 00:00:00 2001 From: asvinb Date: Mon, 23 Feb 2026 18:52:06 +0400 Subject: [PATCH 110/123] test: Remove obsolete headline validation test snapshot --- .../paid-ads/__snapshots__/validateAssetGroup.test.js.snap | 6 ------ 1 file changed, 6 deletions(-) diff --git a/js/src/components/paid-ads/__snapshots__/validateAssetGroup.test.js.snap b/js/src/components/paid-ads/__snapshots__/validateAssetGroup.test.js.snap index 1013e6a8ba..d7fb6b0b7d 100644 --- a/js/src/components/paid-ads/__snapshots__/validateAssetGroup.test.js.snap +++ b/js/src/components/paid-ads/__snapshots__/validateAssetGroup.test.js.snap @@ -52,12 +52,6 @@ exports[`validateAssetGroup Text assets When the first value of description is a ] `; -exports[`validateAssetGroup Text assets When the first value of headline is an empty string, it should not pass 1`] = ` -[ - "The headline in the first field is required", -] -`; - exports[`validateAssetGroup Text assets When the length of values.business_name is less than 1 after omitting empty strings, it should not pass 1`] = ` [ "The business name is required", From efff038c8431ecf4aed7b876ffc933f558553680 Mon Sep 17 00:00:00 2001 From: asvinb Date: Mon, 23 Feb 2026 19:02:59 +0400 Subject: [PATCH 111/123] test: Remove irrelevant headline assertions from adaptAssetGroup test --- js/src/data/adapters.test.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/js/src/data/adapters.test.js b/js/src/data/adapters.test.js index 0e59a3acc0..9dd00ecd4d 100644 --- a/js/src/data/adapters.test.js +++ b/js/src/data/adapters.test.js @@ -345,23 +345,10 @@ describe( 'adaptAssetGroup', () => { it( 'When the first text has an invalid character count, it should move the valid one to the first', () => { assetGroup.assets[ DESCRIPTION ].reverse(); - assetGroup.assets[ HEADLINE ] = [ - { content: text20Count }, - { content: text30Count }, - { content: text15Count }, - { content: text10Count }, - ]; const { assets } = adaptAssetGroup( assetGroup ); const descriptions = assets[ DESCRIPTION ].map( mapContent ); - const headlines = assets[ HEADLINE ].map( mapContent ); expect( descriptions ).toEqual( [ text60Count, text90Count ] ); - expect( headlines ).toEqual( [ - text15Count, - text20Count, - text30Count, - text10Count, - ] ); } ); } ); } ); From 0853366cc76d3fd08c7cb3d7b955938176d9c725 Mon Sep 17 00:00:00 2001 From: James Morrison Date: Mon, 9 Mar 2026 11:27:27 +0000 Subject: [PATCH 112/123] Increase max bundle size for '/js/build/index.js'. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d8f400e5b3..e6a6916c90 100644 --- a/package.json +++ b/package.json @@ -149,7 +149,7 @@ }, { "path": "./js/build/index.js", - "maxSize": "19.6 kB" + "maxSize": "19.68 kB" }, { "path": "./js/build/commons.js", From b494517f43c1789e532ff0ee1311ec29cd945689 Mon Sep 17 00:00:00 2001 From: asvinb Date: Tue, 10 Mar 2026 13:32:33 +0400 Subject: [PATCH 113/123] Revert changes during merge conflict resolution --- .../paid-ads/asset-group/asset-group.js | 110 +++++++++++------- .../add-paid-campaigns.test.js | 2 +- .../step-3-optimize-campaign.test.js | 24 ++-- ...step-2-create-campaign-ads-account-only.js | 32 +++++ 4 files changed, 113 insertions(+), 55 deletions(-) diff --git a/js/src/components/paid-ads/asset-group/asset-group.js b/js/src/components/paid-ads/asset-group/asset-group.js index c41d839eac..ccdc8d8b12 100644 --- a/js/src/components/paid-ads/asset-group/asset-group.js +++ b/js/src/components/paid-ads/asset-group/asset-group.js @@ -18,6 +18,7 @@ import Faqs from './faqs'; import { recordGlaEvent, CONTEXT_ADS_ONLY_ONBOARDING } from '~/utils/tracks'; import useTargetAudienceFinalCountryCodes from '~/hooks/useTargetAudienceFinalCountryCodes'; import AssetGroupHeader from './asset-group-header'; +import AssetGroupEditor from './asset-group-editor'; import { upsertActionedCampaign } from '~/utils/actionedCampaignsCache'; import useGoogleMCAccount from '~/hooks/useGoogleMCAccount'; import './asset-group.scss'; @@ -78,7 +79,13 @@ export default function AssetGroup( { const { isValidForm, handleSubmit, adapter, values } = useAdaptiveFormContext(); const { data: countryCodes } = useTargetAudienceFinalCountryCodes(); - const { isValidAssetGroup, isSubmitting, isSubmitted, submitter } = adapter; + const { + isValidAssetGroup, + isSubmitting, + isSubmitted, + submitter, + isFetchingAssets, + } = adapter; const { hasGoogleMCConnection } = useGoogleMCAccount(); const currentAction = submitter?.dataset.action; @@ -168,58 +175,77 @@ export default function AssetGroup( { - - - { ( isCreation || adapter.isEmptyAssetEntityGroup ) && - hasGoogleMCConnection && ( - // Currently, the PMax Assets feature in this extension doesn't offer the function - // to delete the asset entity group, so it needs to hide the skip button if the editing - // asset group is not considered empty. + { ! isFetchingAssets && ( + <> + + + + + { ( isCreation || + adapter.isEmptyAssetEntityGroup ) && + hasGoogleMCConnection && ( + // Currently, the PMax Assets feature in this extension doesn't offer the function + // to delete the asset entity group, so it needs to hide the skip button if the editing + // asset group is not considered empty. + + { __( + 'Skip this step', + 'google-listings-and-ads' + ) } + + ) } - { __( - 'Skip this step', - 'google-listings-and-ads' - ) } + { isCreation + ? __( + 'Create campaign', + 'google-listings-and-ads' + ) + : __( + 'Save changes', + 'google-listings-and-ads' + ) } - ) } - - { isCreation - ? __( 'Create campaign', 'google-listings-and-ads' ) - : __( 'Save changes', 'google-listings-and-ads' ) } - - - - + + + + + ) } ); } diff --git a/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js b/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js index 644df1c476..9448e3e148 100644 --- a/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js +++ b/tests/e2e/specs/add-paid-campaigns/add-paid-campaigns.test.js @@ -577,7 +577,7 @@ test.describe( 'Add paid campaign', () => { } ); test.describe( 'Optimize your campaign step', async () => { - test( 'Final URL should be selected to homepage by default', async () => { + test( 'Final URL should be selected by default', async () => { const finalUrlCard = createCampaignPage.getFinalUrlCard(); await expect( finalUrlCard ).toContainText( 'https://woo.com/shop/' diff --git a/tests/e2e/specs/onboarding-ads-only/step-3-optimize-campaign.test.js b/tests/e2e/specs/onboarding-ads-only/step-3-optimize-campaign.test.js index 4668f323ee..c4251a33b6 100644 --- a/tests/e2e/specs/onboarding-ads-only/step-3-optimize-campaign.test.js +++ b/tests/e2e/specs/onboarding-ads-only/step-3-optimize-campaign.test.js @@ -108,7 +108,18 @@ test.describe( 'Optimize campaign for Ads only merchants', () => { } ); test.describe( 'Optimize campaign', () => { - test( 'Create Campaign button should be disabled if no URL selected', async () => { + test( 'Final URL should be selected by default', async () => { + const finalUrlCard = createCampaignPage.getFinalUrlCard(); + await expect( finalUrlCard ).toContainText( + 'https://woo.com/shop/' + ); + } ); + + test( 'Selecting the "Or, select a different Final URL" button disables the Create Campaign button', async () => { + const selectDifferentFinalUrlButton = + optimizeCampaignPage.getSelectDifferentFinalUrlButton(); + await selectDifferentFinalUrlButton.click(); + const createCampaignButton = optimizeCampaignPage.getCreateCampaignButton(); await expect( createCampaignButton ).toBeDisabled(); @@ -122,23 +133,12 @@ test.describe( 'Optimize campaign for Ads only merchants', () => { await expect( createCampaignButton ).toBeEnabled(); } ); - test( 'Selecting the "Or, select a different Final URL" button disables the Create Campaign button', async () => { - const selectDifferentFinalUrlButton = - optimizeCampaignPage.getSelectDifferentFinalUrlButton(); - await selectDifferentFinalUrlButton.click(); - - const createCampaignButton = - optimizeCampaignPage.getCreateCampaignButton(); - await expect( createCampaignButton ).toBeDisabled(); - } ); - test( '"Skip this step" button should not be present in the last step of onboarding', async () => { const skipThisStepButton = page.locator( 'text="Skip this step"' ); await expect( skipThisStepButton ).toHaveCount( 0 ); } ); test( 'Clicking the "Create Campaign" button navigates to the dashboard and should see the setup success modal', async () => { - await optimizeCampaignPage.selectUrlOption(); const createCampaignButton = optimizeCampaignPage.getCreateCampaignButton(); diff --git a/tests/e2e/utils/pages/onboarding/step-2-create-campaign-ads-account-only.js b/tests/e2e/utils/pages/onboarding/step-2-create-campaign-ads-account-only.js index bc0f9c19cb..2590aec4ad 100644 --- a/tests/e2e/utils/pages/onboarding/step-2-create-campaign-ads-account-only.js +++ b/tests/e2e/utils/pages/onboarding/step-2-create-campaign-ads-account-only.js @@ -38,4 +38,36 @@ export default class CreateCampaign extends CompleteCampaign { await button.click(); await this.page.waitForLoadState( LOAD_STATE.DOM_CONTENT_LOADED ); } + + /** + * Get final URL card. + * + * @return {import('@playwright/test').Locator} Get final URL card. + */ + getFinalUrlCard() { + return this.page.locator( '.gla-final-url-card' ); + } + + /** + * Get select different final URL button. + * + * @return {import('@playwright/test').Locator} Get select different final URL button. + */ + getSelectDifferentFinalUrlButton() { + return this.page.getByRole( 'button', { + name: 'Or, select a different Final URL', + } ); + } + + /** + * Get create campaign button. + * + * @return {import('@playwright/test').Locator} Get create campaign button. + */ + getCreateCampaignButton() { + // Intentionally not using getByRole here, as another button with the same accessible name exists in the Stepper header. + return this.page.locator( + 'button[data-action="submit-campaign-and-assets"]' + ); + } } From 0a9a59b382164835fda354047bc405b27ea18283 Mon Sep 17 00:00:00 2001 From: asvinb Date: Tue, 10 Mar 2026 13:57:12 +0400 Subject: [PATCH 114/123] build: Update commons.js max bundle size --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e6a6916c90..2c8bce56d6 100644 --- a/package.json +++ b/package.json @@ -153,7 +153,7 @@ }, { "path": "./js/build/commons.js", - "maxSize": "65 kB" + "maxSize": "68 kB" }, { "path": "./js/build/vendors.js", From 51f8d3847dcdab02fb4a1b7c84f6182165e9d792 Mon Sep 17 00:00:00 2001 From: Joe McGill Date: Thu, 12 Mar 2026 16:21:02 -0500 Subject: [PATCH 115/123] Handle additional v22 uprades --- src/API/Google/AdsCampaignAsset.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/API/Google/AdsCampaignAsset.php b/src/API/Google/AdsCampaignAsset.php index e4e13a61a1..67ad3ecb98 100644 --- a/src/API/Google/AdsCampaignAsset.php +++ b/src/API/Google/AdsCampaignAsset.php @@ -6,11 +6,11 @@ use Automattic\WooCommerce\GoogleListingsAndAds\Google\Ads\GoogleAdsClient; use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareInterface; use Automattic\WooCommerce\GoogleListingsAndAds\Options\OptionsAwareTrait; -use Google\Ads\GoogleAds\Util\V20\ResourceNames; -use Google\Ads\GoogleAds\V20\Resources\CampaignAsset; -use Google\Ads\GoogleAds\V20\Services\CampaignAssetOperation; -use Google\Ads\GoogleAds\V20\Services\MutateOperation; -use Google\Ads\GoogleAds\V20\Enums\AssetFieldTypeEnum\AssetFieldType as AssetFieldTypeEnum; +use Google\Ads\GoogleAds\Util\V22\ResourceNames; +use Google\Ads\GoogleAds\V22\Resources\CampaignAsset; +use Google\Ads\GoogleAds\V22\Services\CampaignAssetOperation; +use Google\Ads\GoogleAds\V22\Services\MutateOperation; +use Google\Ads\GoogleAds\V22\Enums\AssetFieldTypeEnum\AssetFieldType as AssetFieldTypeEnum; /** * Class AdsCampaignAsset From 012299bd025b78cd655b3733ef160df45b608bed Mon Sep 17 00:00:00 2001 From: asvinb Date: Fri, 13 Mar 2026 17:17:52 +0400 Subject: [PATCH 116/123] feat(gen-ai): Allow users to skip asset generation --- .../asset-group-editor/images-selector.js | 2 +- .../asset-group-editor/texts-editor.js | 2 +- .../paid-ads/campaign-assets-form.js | 9 ++++- .../index.js} | 12 ++++++- .../index.scss} | 4 +++ .../paid-ads/gen-ai-progress/skip-button.js | 34 +++++++++++++++++++ js/src/hooks/useCreateGenAIAssets.js | 28 +++++++++++++-- 7 files changed, 84 insertions(+), 7 deletions(-) rename js/src/components/paid-ads/{gen-ai-progress.js => gen-ai-progress/index.js} (76%) rename js/src/components/paid-ads/{gen-ai-progress.scss => gen-ai-progress/index.scss} (84%) create mode 100644 js/src/components/paid-ads/gen-ai-progress/skip-button.js diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/images-selector.js b/js/src/components/paid-ads/asset-group/asset-group-editor/images-selector.js index 491b6fab09..2de634ba3c 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/images-selector.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/images-selector.js @@ -54,7 +54,7 @@ export default function ImagesSelector( { const { values } = useAdaptiveFormContext(); const updateImagesRef = useRef(); const [ awaitingActionImage, setAwaitingActionImage ] = useState( null ); - const [ generateAssets, isGeneratingAssets ] = useCreateGenAIAssets(); + const { generateAssets, isGeneratingAssets } = useCreateGenAIAssets(); const { createNotice } = useDispatchCoreNotices(); const [ images, setImages ] = useState( () => // The asset images fetched from Google Ads are only URLs. diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js b/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js index b0096108d0..9328007fb8 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js @@ -65,7 +65,7 @@ export default function TextsEditor( { } ) { const updateTextsRef = useRef(); const { createNotice } = useDispatchCoreNotices(); - const [ generateAssets, isGeneratingAssets ] = useCreateGenAIAssets(); + const { generateAssets, isGeneratingAssets } = useCreateGenAIAssets(); const [ texts, setTexts ] = useState( initialTexts ); const { assets: genAITextAssets } = useGenAITextAssets( finalUrl, diff --git a/js/src/components/paid-ads/campaign-assets-form.js b/js/src/components/paid-ads/campaign-assets-form.js index da31f17e7c..1ea1a7b6e4 100644 --- a/js/src/components/paid-ads/campaign-assets-form.js +++ b/js/src/components/paid-ads/campaign-assets-form.js @@ -204,7 +204,8 @@ export default function CampaignAssetsForm( { countryCodes, ...adaptiveFormProps } ) { - const [ generateAssets ] = useCreateGenAIAssets(); + const { generateAssets, isGeneratingAssets, abortGenerateAssets } = + useCreateGenAIAssets(); const [ isFetchingAssets, setIsFetchingAssets ] = useState( false ); const initialAssetGroup = useMemo( () => { return convertAssetEntityGroupToFormValues( assetEntityGroup ); @@ -274,6 +275,10 @@ export default function CampaignAssetsForm( { { type: GEN_AI_ASSET_TYPES.MEDIA }, ] ); + if ( ! generatedGenAIAssets ) { + return assetSuggestions; + } + const textAssetsData = generatedGenAIAssets[ GEN_AI_ASSET_TYPES.TEXT ]; const mediaAssetsData = @@ -365,9 +370,11 @@ export default function CampaignAssetsForm( { formContext.adapter.hideValidation(); }, isFetchingAssets, + isGeneratingAssets, hasAISuggestedTextAssets, hasAISuggestedMediaAssets, fetchAssets, + abortGenerateAssets, }; }; diff --git a/js/src/components/paid-ads/gen-ai-progress.js b/js/src/components/paid-ads/gen-ai-progress/index.js similarity index 76% rename from js/src/components/paid-ads/gen-ai-progress.js rename to js/src/components/paid-ads/gen-ai-progress/index.js index a1dadd304d..7784880860 100644 --- a/js/src/components/paid-ads/gen-ai-progress.js +++ b/js/src/components/paid-ads/gen-ai-progress/index.js @@ -9,8 +9,14 @@ import { ProgressBar } from '@wordpress/components'; * Internal dependencies */ import ProgressGraphics from '~/images/pmax-assets-improvements/gen-ai-progress.svg'; -import './gen-ai-progress.scss'; +import SkipButton from './skip-button'; +import './index.scss'; +/** + * Component to display the progress of Gen AI asset generation, including a progress bar and a skip button. + * + * @return {JSX.Element} The GenAIProgress component. + */ const GenAIProgress = () => { return (
@@ -34,6 +40,10 @@ const GenAIProgress = () => { 'google-listings-and-ads' ) }

+ +
+ +
); diff --git a/js/src/components/paid-ads/gen-ai-progress.scss b/js/src/components/paid-ads/gen-ai-progress/index.scss similarity index 84% rename from js/src/components/paid-ads/gen-ai-progress.scss rename to js/src/components/paid-ads/gen-ai-progress/index.scss index 50c4b0d225..d3ca3757fa 100644 --- a/js/src/components/paid-ads/gen-ai-progress.scss +++ b/js/src/components/paid-ads/gen-ai-progress/index.scss @@ -21,4 +21,8 @@ margin: $grid-unit-20 0 0; } } + + .gen-ai-progress__actions { + min-height: $gla-size-control-height; + } } diff --git a/js/src/components/paid-ads/gen-ai-progress/skip-button.js b/js/src/components/paid-ads/gen-ai-progress/skip-button.js new file mode 100644 index 0000000000..5f2a330600 --- /dev/null +++ b/js/src/components/paid-ads/gen-ai-progress/skip-button.js @@ -0,0 +1,34 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { useAdaptiveFormContext } from '~/components/adaptive-form'; +import AppButton from '~/components/app-button'; + +/** + * Component for the skip button displayed during Gen AI asset generation progress. + * + * This button allows users to abort the asset generation process if they choose to skip it. + * + * @return {JSX.Element|null} The SkipButton component, or null if not currently generating assets. + */ +const SkipButton = () => { + const { adapter } = useAdaptiveFormContext(); + const { abortGenerateAssets, isGeneratingAssets } = adapter; + + if ( ! isGeneratingAssets ) { + return null; + } + + return ( + + { __( 'Skip', 'google-listings-and-ads' ) } + + ); +}; + +export default SkipButton; diff --git a/js/src/hooks/useCreateGenAIAssets.js b/js/src/hooks/useCreateGenAIAssets.js index 38c06206cd..866a46a9b9 100644 --- a/js/src/hooks/useCreateGenAIAssets.js +++ b/js/src/hooks/useCreateGenAIAssets.js @@ -3,7 +3,7 @@ */ import apiFetch from '@wordpress/api-fetch'; import { __ } from '@wordpress/i18n'; -import { useCallback, useState } from '@wordpress/element'; +import { useCallback, useState, useRef } from '@wordpress/element'; /** * Internal dependencies @@ -16,14 +16,24 @@ import useDispatchCoreNotices from '~/hooks/useDispatchCoreNotices'; /** * Custom hook to generate Gen AI assets for a given URL and asset requests. * - * @return {Array} - An array containing the generateAssets function and a boolean indicating if assets are currently being generated. + * @return {Object} An object containing the generateAssets function, isGeneratingAssets boolean, and abortGenerateAssets function. */ const useCreateGenAIAssets = () => { const [ isGeneratingAssets, setIsGeneratingAssets ] = useState( false ); const { createNotice } = useDispatchCoreNotices(); + const abortControllerRef = useRef( null ); const { receiveGenAITextAssets, receiveGenAIMediaAssets } = useAppDispatch(); + /** + * Aborts any ongoing Gen AI asset generation requests. + */ + const abortGenerateAssets = useCallback( () => { + if ( abortControllerRef.current ) { + abortControllerRef.current.abort(); + } + }, [] ); + /** * Helper function to process Gen AI API responses, handling both success and error cases. * @@ -84,6 +94,9 @@ const useCreateGenAIAssets = () => { return; } + abortControllerRef.current = new AbortController(); + const { signal } = abortControllerRef.current; + setIsGeneratingAssets( true ); // Initialize as empty arrays to avoid overwriting multiple requests of same type @@ -101,6 +114,7 @@ const useCreateGenAIAssets = () => { return apiFetch( { path, + signal, method: REQUEST_ACTIONS.POST, parse: false, data: { @@ -114,6 +128,10 @@ const useCreateGenAIAssets = () => { const results = await Promise.allSettled( promises ); + if ( signal.aborted ) { + return; + } + for ( let index = 0; index < results.length; index++ ) { const { type, assetKey } = requests[ index ]; const data = await processGenAIResponse( results[ index ] ); @@ -150,6 +168,10 @@ const useCreateGenAIAssets = () => { return generatedAssets; } catch ( error ) { + if ( signal.aborted ) { + return; + } + // Catch unexpected runtime errors createNotice( 'error', @@ -170,7 +192,7 @@ const useCreateGenAIAssets = () => { ] ); - return [ generateAssets, isGeneratingAssets ]; + return { generateAssets, isGeneratingAssets, abortGenerateAssets }; }; export default useCreateGenAIAssets; From d8aba267e8ab56de475b86f578a884029d110e37 Mon Sep 17 00:00:00 2001 From: asvinb Date: Fri, 13 Mar 2026 17:19:35 +0400 Subject: [PATCH 117/123] style(gen-ai-progress): Add top margin to actions --- js/src/components/paid-ads/gen-ai-progress/index.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/js/src/components/paid-ads/gen-ai-progress/index.scss b/js/src/components/paid-ads/gen-ai-progress/index.scss index d3ca3757fa..4822ecf9b7 100644 --- a/js/src/components/paid-ads/gen-ai-progress/index.scss +++ b/js/src/components/paid-ads/gen-ai-progress/index.scss @@ -23,6 +23,7 @@ } .gen-ai-progress__actions { + margin-block-start: $grid-unit-10; min-height: $gla-size-control-height; } } From e4e5946187a1d2b1d4e8277d5a06563f31b40720 Mon Sep 17 00:00:00 2001 From: asvinb Date: Fri, 13 Mar 2026 17:51:56 +0400 Subject: [PATCH 118/123] feat: Add analytics events to Gen AI buttons --- .../gen-ai-image-picker/index.js | 17 +++++++++++++++++ .../asset-group-editor/texts-editor.js | 15 +++++++++++++++ .../paid-ads/gen-ai-progress/skip-button.js | 14 +++++++++++++- 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.js b/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.js index d472121eac..635406df0c 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.js @@ -19,10 +19,21 @@ import AppButton from '~/components/app-button'; import AIIcon from '~/images/ai-icon.svg?inline'; import './index.scss'; +/** + * Triggered when the "Add selected images" button is clicked. + * + * @event gla_gen_ai_image_picker_add_selected_images_click + * @property {string} finalUrl The final URL for which the images were generated. + * @property {string} assetKey The asset key for which the images were generated. + * @property {number} numberOfSelectedImages The number of images that were selected to be added. + */ + /** * GenAIImagePicker component. * Allows users to pick AI-generated images based on the final URL and the spec type. * + * @fires gla_gen_ai_image_picker_add_selected_images_click when the "Add selected images" button is clicked. + * * @param {Object} props Component props. * @param {string} props.assetKey Asset key. * @param {Function} props.onAddSelectedImages Callback to add selected images. @@ -124,6 +135,12 @@ export default function GenAIImagePicker( { assetKey, onAddSelectedImages } ) { ) } onClick={ handleOnAddSelectedImages } disabled={ selectedImages.length === 0 } + eventName="gla_gen_ai_image_picker_add_selected_images_click" + eventProps={ { + finalUrl, + assetKey, + numberOfSelectedImages: selectedImages.length, + } } /> diff --git a/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js b/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js index 9328007fb8..cf5cb02cc6 100644 --- a/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js +++ b/js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js @@ -32,9 +32,19 @@ function normalizeNumberOfTexts( texts, minNumberOfTexts, maxNumberOfTexts ) { return texts.concat( supplement ).slice( ...sliceArgs ); } +/** + * Triggered when the generate texts button is clicked in the TextsEditor component. Event properties include finalUrl and assetKey. + * + * @event gla_texts_editor_generate_button_click + * @property {string} finalUrl The final URL for the ad. + * @property {string} assetKey The key of the text asset. + */ + /** * Renders a list of text inputs for managing the single type of asset texts. * + * @fires gla_texts_editor_generate_button_click when the generate texts button is clicked in the TextsEditor component. + * * @param {Object} props React props. * @param {string} props.assetKey Key of the text asset. * @param {string} props.finalUrl The final URL for the ad. @@ -231,6 +241,11 @@ export default function TextsEditor( { text={ generateButtonText } onClick={ handleGenerateClick } loading={ isGeneratingAssets } + eventName="gla_texts_editor_generate_button_click" + eventProps={ { + finalUrl, + assetKey, + } } /> ) } diff --git a/js/src/components/paid-ads/gen-ai-progress/skip-button.js b/js/src/components/paid-ads/gen-ai-progress/skip-button.js index 5f2a330600..cd4e079930 100644 --- a/js/src/components/paid-ads/gen-ai-progress/skip-button.js +++ b/js/src/components/paid-ads/gen-ai-progress/skip-button.js @@ -9,11 +9,19 @@ import { __ } from '@wordpress/i18n'; import { useAdaptiveFormContext } from '~/components/adaptive-form'; import AppButton from '~/components/app-button'; +/** + * Triggered when the skip button is clicked during Gen AI asset generation progress. + * + * @event gla_gen_ai_progress_skip_button_click + */ + /** * Component for the skip button displayed during Gen AI asset generation progress. * * This button allows users to abort the asset generation process if they choose to skip it. * + * @fires gla_gen_ai_progress_skip_button_click when the skip button is clicked. + * * @return {JSX.Element|null} The SkipButton component, or null if not currently generating assets. */ const SkipButton = () => { @@ -25,7 +33,11 @@ const SkipButton = () => { } return ( - + { __( 'Skip', 'google-listings-and-ads' ) } ); From b165c3fa4114278c54f4fd436a5fda4c9475c011 Mon Sep 17 00:00:00 2001 From: asvinb Date: Fri, 13 Mar 2026 17:52:17 +0400 Subject: [PATCH 119/123] docs: Add Gen AI tracking event descriptions --- src/Tracking/README.md | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/src/Tracking/README.md b/src/Tracking/README.md index 93c77ba775..f7f0ea073c 100644 --- a/src/Tracking/README.md +++ b/src/Tracking/README.md @@ -552,6 +552,22 @@ Saving changes of audience and/or shipping settings to the product feed. #### Emitters - [`exports`](../../js/src/pages/shipping/index.js#L46) +### [`gla_gen_ai_image_picker_add_selected_images_click`](../../js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.js#L22) +Triggered when the "Add selected images" button is clicked. +#### Properties +| name | type | description | +| ---- | ---- | ----------- | +`finalUrl` | `string` | The final URL for which the images were generated. +`assetKey` | `string` | The asset key for which the images were generated. +`numberOfSelectedImages` | `number` | The number of images that were selected to be added. +#### Emitters +- [`exports`](../../js/src/components/paid-ads/asset-group/asset-group-editor/gen-ai-image-picker/index.js#L41) when the "Add selected images" button is clicked. + +### [`gla_gen_ai_progress_skip_button_click`](../../js/src/components/paid-ads/gen-ai-progress/skip-button.js#L12) +Triggered when the skip button is clicked during Gen AI asset generation progress. +#### Emitters +- [`SkipButton`](../../js/src/components/paid-ads/gen-ai-progress/skip-button.js#L27) when the skip button is clicked. + ### [`gla_google_account_connect_button_click`](../../js/src/utils/tracks.js#L185) Clicking on the button to connect Google account. #### Properties @@ -592,14 +608,14 @@ Clicking on a Google Merchant Center link. #### Emitters - [`HelpIconButton`](../../js/src/components/help-icon-button/index.js#L31) -### [`gla_import_assets_by_final_url_button_click`](../../js/src/components/paid-ads/asset-group/asset-group-header/assets-loader.js#L80) +### [`gla_import_assets_by_final_url_button_click`](../../js/src/components/paid-ads/asset-group/asset-group-header/assets-loader.js#L83) Clicking on the "Scan for assets" button. #### Properties | name | type | description | | ---- | ---- | ----------- | `type` | `string` | The type of the selected Final URL suggestion to be imported. Possible values: `post`, `term`, `homepage`. #### Emitters -- [`exports`](../../js/src/components/paid-ads/asset-group/asset-group-header/assets-loader.js#L96) +- [`exports`](../../js/src/components/paid-ads/asset-group/asset-group-header/assets-loader.js#L99) ### [`gla_launch_paid_campaign_button_click`](../../js/src/utils/tracks.js#L173) Triggered when the "Launch paid campaign" button is clicked to add a new paid campaign in the Google Ads setup flow. @@ -912,10 +928,10 @@ Triggered when the request review is successful #### Emitters - [`ReviewRequestModal`](../../js/src/pages/product-feed/review-request/review-request-modal.js#L58) -### [`gla_reselect_another_final_url_button_click`](../../js/src/components/paid-ads/asset-group/asset-group-header/final-url-card.js#L23) +### [`gla_reselect_another_final_url_button_click`](../../js/src/components/paid-ads/asset-group/asset-group-header/final-url-card.js#L24) Clicking on the "Or, select another page" button. #### Emitters -- [`exports`](../../js/src/components/paid-ads/asset-group/asset-group-header/final-url-card.js#L39) +- [`exports`](../../js/src/components/paid-ads/asset-group/asset-group-header/final-url-card.js#L40) ### [`gla_setup_ads`](../../js/src/utils/tracks.js#L203) Triggered on events during ads onboarding @@ -1052,6 +1068,16 @@ Sorting table - [`AppTableCard`](../../js/src/components/app-table-card/index.js#L74) upon sorting table by column - [`recordTableSortEvent`](../../js/src/components/app-table-card/index.js#L55) with given props. +### [`gla_texts_editor_generate_button_click`](../../js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js#L35) +Triggered when the generate texts button is clicked in the TextsEditor component. Event properties include finalUrl and assetKey. +#### Properties +| name | type | description | +| ---- | ---- | ----------- | +`finalUrl` | `string` | The final URL for the ad. +`assetKey` | `string` | The key of the text asset. +#### Emitters +- [`exports`](../../js/src/components/paid-ads/asset-group/asset-group-editor/texts-editor.js#L62) when the generate texts button is clicked in the TextsEditor component. + ### [`gla_tooltip_viewed`](../../js/src/components/help-popover/index.js#L16) Viewing tooltip #### Properties From 728841769150ec754a9136951e490457f3b4199a Mon Sep 17 00:00:00 2001 From: asvinb Date: Fri, 13 Mar 2026 18:12:47 +0400 Subject: [PATCH 120/123] build: Update max size for Google Listings & Ads zip --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2c8bce56d6..3c1938519c 100644 --- a/package.json +++ b/package.json @@ -169,7 +169,7 @@ }, { "path": "./google-listings-and-ads.zip", - "maxSize": "8.26 mB", + "maxSize": "8.27 mB", "compression": "none" } ], From 2335f11cb1278b0d6eef71f7519cc81085f6f772 Mon Sep 17 00:00:00 2001 From: asvinb Date: Fri, 13 Mar 2026 18:18:15 +0400 Subject: [PATCH 121/123] build: Update google-listings-and-ads.zip maxSize --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3c1938519c..89a392cf13 100644 --- a/package.json +++ b/package.json @@ -169,7 +169,7 @@ }, { "path": "./google-listings-and-ads.zip", - "maxSize": "8.27 mB", + "maxSize": "8.28 mB", "compression": "none" } ], From 37d369d1dfbcb31a2ee54c779dd50531e81b9259 Mon Sep 17 00:00:00 2001 From: Joe McGill Date: Fri, 13 Mar 2026 11:18:11 -0500 Subject: [PATCH 122/123] Disable brand guidelines for non-shopping campaigns. --- src/API/Google/AdsCampaign.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/API/Google/AdsCampaign.php b/src/API/Google/AdsCampaign.php index 67fa903d4c..ebd969de67 100644 --- a/src/API/Google/AdsCampaign.php +++ b/src/API/Google/AdsCampaign.php @@ -559,6 +559,9 @@ protected function create_operation( string $campaign_name, ?string $country, bo 'feed_label' => $country, ] ); + } else { + // Turn off brand guidelines for non-shopping campaigns. + $campaign_data['brand_guidelines_enabled'] = false; } $campaign = new Campaign( $campaign_data ); From c7655af80f53812c12f279991e5318bf1dab61f8 Mon Sep 17 00:00:00 2001 From: Joe McGill Date: Mon, 16 Mar 2026 15:33:19 -0500 Subject: [PATCH 123/123] Bump bundlewatch --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 89a392cf13..fd88af507c 100644 --- a/package.json +++ b/package.json @@ -149,11 +149,11 @@ }, { "path": "./js/build/index.js", - "maxSize": "19.68 kB" + "maxSize": "19.85 kB" }, { "path": "./js/build/commons.js", - "maxSize": "68 kB" + "maxSize": "68.1 kB" }, { "path": "./js/build/vendors.js", @@ -169,7 +169,7 @@ }, { "path": "./google-listings-and-ads.zip", - "maxSize": "8.28 mB", + "maxSize": "8.30 mB", "compression": "none" } ],