diff --git a/js/src/components/audience-country-select.js b/js/src/components/audience-country-select.js deleted file mode 100644 index 02a39ef4bc..0000000000 --- a/js/src/components/audience-country-select.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Internal dependencies - */ -import SupportedCountrySelect from '.~/components/supported-country-select'; -import AppSpinner from '.~/components/app-spinner'; -import useTargetAudienceFinalCountryCodes from '.~/hooks/useTargetAudienceFinalCountryCodes'; - -/** - * @typedef {import('.~/data/actions').CountryCode} CountryCode - */ - -/** - * Returns a SupportedCountrySelect component with list of countries grouped by continents. - * And SupportedCountrySelect will be rendered via TreeSelectControl component. - * - * This component is for selecting countries under the merchant selected targeting audiences. - * - * @param {Object} props React props. - * @param {Array} [props.additionalCountryCodes] Additional countries that are not in the target audience countries and need to be selectable. - * @param {Object} props.restProps Props to be forwarded to SupportedCountrySelect. - */ -const AudienceCountrySelect = ( { additionalCountryCodes, ...restProps } ) => { - let { data: countryCodes } = useTargetAudienceFinalCountryCodes(); - - if ( ! countryCodes ) { - return ; - } - - if ( additionalCountryCodes ) { - countryCodes = Array.from( - new Set( countryCodes.concat( additionalCountryCodes ) ) - ); - } - - return ( - - ); -}; - -export default AudienceCountrySelect; diff --git a/js/src/components/paid-ads/ads-campaign/ads-campaign.js b/js/src/components/paid-ads/ads-campaign/ads-campaign.js index 5800d7249b..34324fd40c 100644 --- a/js/src/components/paid-ads/ads-campaign/ads-campaign.js +++ b/js/src/components/paid-ads/ads-campaign/ads-campaign.js @@ -13,10 +13,16 @@ import StepContentFooter from '.~/components/stepper/step-content-footer'; import StepContentActions from '.~/components/stepper/step-content-actions'; import AppDocumentationLink from '.~/components/app-documentation-link'; import { useAdaptiveFormContext } from '.~/components/adaptive-form'; -import AudienceSection from '../audience-section'; +import BillingCard from '.~/components/paid-ads/billing-card'; import BudgetSection from '../budget-section'; import { CampaignPreviewCard } from '../campaign-preview'; -import PaidAdsFaqsPanel from '../faqs-panel'; +import PaidAdsFaqsPanel from './faqs-panel'; +import PaidAdsFeaturesSection from './paid-ads-features-section'; +import useTargetAudienceFinalCountryCodes from '.~/hooks/useTargetAudienceFinalCountryCodes'; + +/** + * @typedef {import('.~/components/adaptive-form/adaptive-form-context').AdaptiveFormContext} AdaptiveFormContext + */ /** * @typedef {import('.~/data/actions').Campaign} Campaign @@ -30,79 +36,83 @@ import PaidAdsFaqsPanel from '../faqs-panel'; * * @fires gla_documentation_link_click with `{ context: 'create-ads' | 'edit-ads' | 'setup-ads', link_id: 'see-what-ads-look-like', href: 'https://support.google.com/google-ads/answer/6275294' }` * @param {Object} props React props. - * @param {Campaign} [props.campaign] Campaign data to be edited. If not provided, this component will show campaign creation UI. - * @param {JSX.Element|Function} props.continueButton Continue button component. - * @param {'create-ads'|'edit-ads'|'setup-ads'} props.trackingContext A context indicating which page this component is used on. This will be the value of `context` in the track event properties. + * @param {Campaign} [props.campaign] Campaign data to be edited. The displayCountries property will be used to fetch budget recommendation data. + * @param {string} props.headerTitle The title of the step. + * @param {'create-ads'|'edit-ads'|'setup-ads'|'setup-mc'} props.context A context indicating which page this component is used on. This will be the value of `context` in the track event properties. + * @param {(formContext: AdaptiveFormContext) => JSX.Element | JSX.Element} [props.skipButton] A React element or function to render the "Skip" button. If a function is passed, it receives the form context and returns the button element. + * @param {(formContext: AdaptiveFormContext) => JSX.Element | JSX.Element} [props.continueButton] A React element or function to render the "Continue" button. If a function is passed, it receives the form context and returns the button element. */ export default function AdsCampaign( { campaign, + headerTitle, + context, + skipButton, continueButton, - trackingContext, } ) { - const isCreation = ! campaign; const formContext = useAdaptiveFormContext(); + const { data: countryCodes } = useTargetAudienceFinalCountryCodes(); + const isOnboardingFlow = context === 'setup-mc'; + const showCampaignPreviewCard = + context === 'setup-ads' || + context === 'create-ads' || + context === 'edit-ads'; + // only show the billing card during onboarding or setup Ads flow. + // For creating/editing a campaign, we assume billing is already set up. + const showBillingCard = context === 'setup-mc' || context === 'setup-ads'; - const disabledBudgetSection = ! formContext.values.countryCodes.length; - const helperText = isCreation - ? __( - 'You can only choose from countries you’ve selected during product listings configuration.', - 'google-listings-and-ads' - ) - : __( - 'Once a campaign has been created, you cannot change the target country(s).', - 'google-listings-and-ads' - ); + let description = createInterpolateElement( + __( + 'Paid Performance Max campaigns are automatically optimized for you by Google. See what your ads will look like.', + 'google-listings-and-ads' + ), + { + link: ( + + ), + } + ); + + if ( isOnboardingFlow ) { + description = __( + 'You’re ready to set up a Performance Max campaign to drive more sales with ads. Your products will be included in the campaign after they’re approved.', + 'google-listings-and-ads' + ); + } return ( See what your ads will look like.', - 'google-listings-and-ads' - ), - { - link: ( - - ), - } - ) } - /> - + + { isOnboardingFlow && } + - + { showBillingCard && } + + { showCampaignPreviewCard && } + { typeof skipButton === 'function' + ? skipButton( formContext ) + : skipButton } + { typeof continueButton === 'function' - ? continueButton( { - formProps: formContext, - } ) + ? continueButton( formContext ) : continueButton } diff --git a/js/src/components/paid-ads/faqs-panel.js b/js/src/components/paid-ads/ads-campaign/faqs-panel.js similarity index 100% rename from js/src/components/paid-ads/faqs-panel.js rename to js/src/components/paid-ads/ads-campaign/faqs-panel.js diff --git a/js/src/setup-mc/setup-stepper/setup-paid-ads/paid-ads-features-section.js b/js/src/components/paid-ads/ads-campaign/paid-ads-features-section.js similarity index 100% rename from js/src/setup-mc/setup-stepper/setup-paid-ads/paid-ads-features-section.js rename to js/src/components/paid-ads/ads-campaign/paid-ads-features-section.js diff --git a/js/src/setup-mc/setup-stepper/setup-paid-ads/paid-ads-features-section.scss b/js/src/components/paid-ads/ads-campaign/paid-ads-features-section.scss similarity index 100% rename from js/src/setup-mc/setup-stepper/setup-paid-ads/paid-ads-features-section.scss rename to js/src/components/paid-ads/ads-campaign/paid-ads-features-section.scss 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 bbceda7497..205f79cd29 100644 --- a/js/src/components/paid-ads/asset-group/asset-group.js +++ b/js/src/components/paid-ads/asset-group/asset-group.js @@ -16,6 +16,7 @@ import AppButton from '.~/components/app-button'; import AssetGroupFaqsPanel from './faqs-panel'; import AssetGroupSection from './asset-group-section'; import { recordGlaEvent } from '.~/utils/tracks'; +import useTargetAudienceFinalCountryCodes from '.~/hooks/useTargetAudienceFinalCountryCodes'; export const ACTION_SUBMIT_CAMPAIGN_AND_ASSETS = 'submit-campaign-and-assets'; export const ACTION_SUBMIT_CAMPAIGN_ONLY = 'submit-campaign-only'; @@ -62,15 +63,17 @@ export default function AssetGroup( { campaign } ) { const isCreation = ! campaign; const { isValidForm, handleSubmit, adapter, values } = useAdaptiveFormContext(); + const { data: countryCodes } = useTargetAudienceFinalCountryCodes(); const { isValidAssetGroup, isSubmitting, isSubmitted, submitter } = adapter; const currentAction = submitter?.dataset.action; function recordSubmissionClickEvent( event ) { + const audiences = isCreation ? countryCodes : campaign.displayCountries; const finalUrl = values[ ASSET_FORM_KEY.FINAL_URL ]; const eventProps = { context: isCreation ? 'campaign-creation' : 'campaign-editing', action: event.target.dataset.action, - audiences: values.countryCodes.join( ',' ), + audiences: audiences.join( ',' ), budget: values.amount.toString(), assets_validation: isValidAssetGroup ? 'valid' : 'invalid', }; diff --git a/js/src/components/paid-ads/audience-section.js b/js/src/components/paid-ads/audience-section.js deleted file mode 100644 index f5ce860dc4..0000000000 --- a/js/src/components/paid-ads/audience-section.js +++ /dev/null @@ -1,84 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; -import { SelectControl } from '@wordpress/components'; - -/** - * Internal dependencies - */ -import useCountryKeyNameMap from '.~/hooks/useCountryKeyNameMap'; -import Section from '.~/wcdl/section'; -import AudienceCountrySelect from '.~/components/audience-country-select'; -import './audience-section.scss'; - -function toCountryOptions( countryCodes, countryNameMap ) { - return countryCodes.map( ( code ) => ( { - label: countryNameMap[ code ], - value: code, - } ) ); -} - -/** - * Renders
and UI with country(s) selector. - * - * @param {Object} props React props. - * @param {Object} props.formProps Form props forwarded from `Form` component. - * @param {boolean} [props.multiple=true] Whether the selector is multi-selected. - * @param {boolean} [props.disabled=false] Whether the selector is disabled. - * @param {JSX.Element} [props.countrySelectHelperText] Helper text to be displayed under the selector. - */ -const AudienceSection = ( props ) => { - const { - formProps: { getInputProps }, - multiple = true, - disabled = false, - countrySelectHelperText, - } = props; - - const countryNameMap = useCountryKeyNameMap(); - const inputProps = getInputProps( 'countryCodes' ); - - const selector = multiple ? ( - - ) : ( - - ); - - return ( -
- { __( - 'Choose where you want your product ads to appear', - 'google-listings-and-ads' - ) } -

- } - > - - { selector } - -
- ); -}; - -export default AudienceSection; diff --git a/js/src/components/paid-ads/audience-section.scss b/js/src/components/paid-ads/audience-section.scss deleted file mode 100644 index 565a4bc70f..0000000000 --- a/js/src/components/paid-ads/audience-section.scss +++ /dev/null @@ -1,15 +0,0 @@ -.gla-audience-section { - // Adjust imported from @wordpress/components. - // Repeat selector to make it higher priority. - .components-input-control__container.components-input-control__container { - .components-select-control__input { - padding-left: $grid-unit-20; - } - } - - // Adjust help text of imported from @wordpress/components. - .components-base-control__help { - margin: 0; - font-style: italic; - } -} diff --git a/js/src/components/paid-ads/audienceSection.test.js b/js/src/components/paid-ads/audienceSection.test.js deleted file mode 100644 index a7eb440a84..0000000000 --- a/js/src/components/paid-ads/audienceSection.test.js +++ /dev/null @@ -1,129 +0,0 @@ -/** - * External dependencies - */ -import '@testing-library/jest-dom'; -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; - -/** - * Internal dependencies - */ -import AudienceSection from '.~/components/paid-ads/audience-section'; - -jest.mock( '.~/hooks/useAppSelectDispatch' ); -jest.mock( '.~/hooks/useCountryKeyNameMap' ); - -jest.mock( '.~/hooks/useTargetAudienceFinalCountryCodes', () => - jest.fn( () => ( { data: [ 'GB', 'US', 'ES' ] } ) ) -); - -describe( 'AudienceSection with multiple countries selector', () => { - let defaultProps; - let onChange; - - beforeEach( () => { - onChange = jest.fn(); - defaultProps = { - formProps: { - getInputProps: () => ( { onChange } ), - }, - }; - } ); - - test( 'If Audience section is disabled the country field should be disabled', async () => { - const user = userEvent.setup(); - - render( ); - - const dropdown = await screen.findByRole( 'combobox' ); - expect( dropdown ).toBeDisabled(); - - //Test that input is not editable - expect( dropdown ).toHaveValue( '' ); - await user.type( dropdown, 'spa' ); - expect( dropdown ).toHaveValue( '' ); - - const options = screen.queryAllByRole( 'checkbox' ); - expect( options.length ).toBe( 0 ); - expect( onChange ).toHaveBeenCalledTimes( 0 ); - } ); - - test( 'If Audience section is enable the country field should be enable & editable', async () => { - const user = userEvent.setup(); - - render( ); - - const dropdown = await screen.findByRole( 'combobox' ); - expect( dropdown ).not.toBeDisabled(); - - //Test that input is editable - expect( dropdown ).toHaveValue( '' ); - await user.type( dropdown, 'spa' ); - expect( dropdown ).toHaveValue( 'spa' ); - - const options = await screen.findAllByRole( 'checkbox' ); - expect( options.length ).toBeGreaterThan( 0 ); - - const firstOption = options[ 0 ]; - await user.click( firstOption ); - expect( onChange ).toHaveBeenCalledTimes( 1 ); - } ); -} ); - -describe( 'AudienceSection with single country selector', () => { - let defaultProps; - let onChange; - - beforeEach( () => { - onChange = jest.fn(); - defaultProps = { - multiple: false, - formProps: { - getInputProps: () => ( { - value: [ 'US', 'ES', 'GB' ], - selected: [ 'ES' ], - onChange, - } ), - }, - }; - } ); - - test( 'When AudienceSection is disabled, the country field should be disabled', () => { - render( ); - const dropdown = screen.queryByRole( 'combobox' ); - - expect( dropdown ).toBeDisabled(); - } ); - - test( 'When AudienceSection is enable, the country field should be enable', () => { - render( ); - const dropdown = screen.queryByRole( 'combobox' ); - - expect( dropdown ).not.toBeDisabled(); - } ); - - test( 'When selecting another option, the country field should trigger `onChange` callback', async () => { - const user = userEvent.setup(); - - render( ); - - const dropdown = screen.queryByRole( 'combobox' ); - await user.selectOptions( dropdown, 'GB' ); - - expect( onChange ).toHaveBeenCalledTimes( 1 ); - } ); - - test( 'The country field should have the given options', () => { - render( ); - const options = screen.queryAllByRole( 'option' ); - - expect( options.length ).toBe( 3 ); - } ); - - test( 'The country field should select the option by given value', () => { - render( ); - const option = screen.queryByRole( 'option', { selected: true } ); - - expect( option.value ).toBe( 'ES' ); - } ); -} ); diff --git a/js/src/components/types.js b/js/src/components/types.js index fa7214953f..50259022b1 100644 --- a/js/src/components/types.js +++ b/js/src/components/types.js @@ -4,7 +4,6 @@ /** * @typedef {Object} CampaignFormValues - * @property {Array} countryCodes Selected country codes for the paid ads campaign. * @property {number} amount The daily average cost amount. */ diff --git a/js/src/pages/create-paid-ads-campaign/index.js b/js/src/pages/create-paid-ads-campaign/index.js index c93a1cbf68..ae16722d76 100644 --- a/js/src/pages/create-paid-ads-campaign/index.js +++ b/js/src/pages/create-paid-ads-campaign/index.js @@ -51,7 +51,7 @@ const CreatePaidAdsCampaign = () => { const createdCampaignIdRef = useRef( null ); const { createAdsCampaign, updateCampaignAssetGroup } = useAppDispatch(); const { createNotice } = useDispatchCoreNotices(); - const { data: initialCountryCodes } = useTargetAudienceFinalCountryCodes(); + const { data: countryCodes } = useTargetAudienceFinalCountryCodes(); const handleStepperClick = ( nextStep ) => { recordStepperChangeEvent( @@ -76,7 +76,7 @@ const CreatePaidAdsCampaign = () => { const { action } = enhancer.submitter.dataset; try { - const { amount, countryCodes } = values; + const { amount } = values; // Avoid re-creating a new campaign if the subsequent asset group update is failed. if ( createdCampaignIdRef.current === null ) { @@ -114,7 +114,7 @@ const CreatePaidAdsCampaign = () => { getHistory().push( getDashboardUrl( { campaign: 'saved' } ) ); }; - if ( ! initialCountryCodes ) { + if ( ! countryCodes ) { return null; } @@ -131,7 +131,6 @@ const CreatePaidAdsCampaign = () => { @@ -146,15 +145,19 @@ const CreatePaidAdsCampaign = () => { ), content: ( ( + headerTitle={ __( + 'Create your paid campaign', + 'google-listings-and-ads' + ) } + context={ eventContext } + continueButton={ ( formContext ) => ( + formProps={ formContext } + onClick={ () => { 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 952c7f875b..ddc58dc64e 100644 --- a/js/src/pages/edit-paid-ads-campaign/index.js +++ b/js/src/pages/edit-paid-ads-campaign/index.js @@ -180,7 +180,6 @@ const EditPaidAdsCampaign = () => { { content: ( ( + context={ eventContext } + headerTitle={ __( + 'Edit your paid campaign', + 'google-listings-and-ads' + ) } + continueButton={ ( formContext ) => ( handleContinueClick( STEP.ASSET_GROUP diff --git a/js/src/setup-ads/ads-stepper/index.js b/js/src/setup-ads/ads-stepper/index.js index 017ae3193e..51fe990ffb 100644 --- a/js/src/setup-ads/ads-stepper/index.js +++ b/js/src/setup-ads/ads-stepper/index.js @@ -9,8 +9,6 @@ import { useState } from '@wordpress/element'; * Internal dependencies */ import SetupAccounts from './setup-accounts'; -import AppButton from '.~/components/app-button'; -import AdsCampaign from '.~/components/paid-ads/ads-campaign'; import useEventPropertiesFilter from '.~/hooks/useEventPropertiesFilter'; import { recordStepperChangeEvent, @@ -18,15 +16,17 @@ import { FILTER_ONBOARDING, CONTEXT_ADS_ONBOARDING, } from '.~/utils/tracks'; +import SetupPaidAds from './setup-paid-ads'; /** * @param {Object} props React props - * @param {Object} props.formProps Form props forwarded from `Form` component. + * @param {boolean} props.isSubmitting When the form in the parent component, i.e SetupAdsForm, is currently being submitted via the useAdsSetupCompleteCallback hook. * @fires gla_setup_ads with `{ triggered_by: 'step1-continue-button', action: 'go-to-step2' }`. * @fires gla_setup_ads with `{ triggered_by: 'stepper-step1-button', action: 'go-to-step1'}`. */ -const AdsStepper = ( { formProps } ) => { +const AdsStepper = ( { isSubmitting } ) => { const [ step, setStep ] = useState( '1' ); + useEventPropertiesFilter( FILTER_ONBOARDING, { context: CONTEXT_ADS_ONBOARDING, step, @@ -57,10 +57,6 @@ const AdsStepper = ( { formProps } ) => { continueStep( '2' ); }; - // @todo: Add check for billing status once billing setup is moved to step 2. - // For now, only disable based on the form being valid for testing purposes. - const isDisabledLaunch = ! formProps.isValidForm; - return ( // This Stepper with this class name // should be refactored into separate shared component. @@ -88,23 +84,7 @@ const AdsStepper = ( { formProps } ) => { 'Create your paid campaign', 'google-listings-and-ads' ), - content: ( - - } - /> - ), + content: , onClick: handleStepClick, }, ] } diff --git a/js/src/setup-ads/ads-stepper/index.test.js b/js/src/setup-ads/ads-stepper/index.test.js index ebfad27dfc..feae4d028b 100644 --- a/js/src/setup-ads/ads-stepper/index.test.js +++ b/js/src/setup-ads/ads-stepper/index.test.js @@ -3,7 +3,6 @@ jest.mock( '@woocommerce/tracks', () => { recordEvent: jest.fn().mockName( 'recordEvent' ), }; } ); - jest.mock( './setup-accounts', () => jest.fn().mockName( 'SetupAccounts' ) ); jest.mock( '.~/components/paid-ads/ads-campaign', () => jest.fn().mockName( 'AdsCampaign' ) @@ -12,7 +11,7 @@ jest.mock( '.~/components/paid-ads/ads-campaign', () => /** * External dependencies */ -import { screen, render, waitFor } from '@testing-library/react'; +import { screen, render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { recordEvent } from '@woocommerce/tracks'; @@ -21,54 +20,32 @@ import { recordEvent } from '@woocommerce/tracks'; */ import AdsStepper from './'; import SetupAccounts from './setup-accounts'; -import AdsCampaign from '.~/components/paid-ads/ads-campaign'; describe( 'AdsStepper', () => { let continueToStep2; - let continueToStep3; beforeEach( () => { SetupAccounts.mockImplementation( ( { onContinue } ) => { continueToStep2 = onContinue; return null; } ); - - AdsCampaign.mockImplementation( ( { onContinue } ) => { - continueToStep3 = onContinue; - return null; - } ); } ); afterEach( () => { jest.clearAllMocks(); } ); - async function continueUntilStep3() { - continueToStep2(); - - // Wait for stepper content to be rendered. - await waitFor( () => { - expect( continueToStep3 ).toBeDefined(); - } ); - - continueToStep3(); - } - describe( 'tracks', () => { it( 'Should record events after calling back to `onContinue`', async () => { render( ); - await continueUntilStep3(); + await continueToStep2(); - expect( recordEvent ).toHaveBeenCalledTimes( 2 ); + expect( recordEvent ).toHaveBeenCalledTimes( 1 ); expect( recordEvent ).toHaveBeenNthCalledWith( 1, 'gla_setup_ads', { action: 'go-to-step2', triggered_by: 'step1-continue-button', } ); - expect( recordEvent ).toHaveBeenNthCalledWith( 2, 'gla_setup_ads', { - action: 'go-to-step3', - triggered_by: 'step2-continue-button', - } ); } ); it( 'Should record events after clicking step navigation buttons', async () => { @@ -77,28 +54,9 @@ describe( 'AdsStepper', () => { render( ); const step1 = screen.getByRole( 'button', { name: /accounts/ } ); - const step2 = screen.getByRole( 'button', { name: /campaign/ } ); - - // Step 3 -> Step 2 -> Step 1 - await continueUntilStep3(); - recordEvent.mockClear(); - expect( recordEvent ).toHaveBeenCalledTimes( 0 ); - - await user.click( step2 ); - await user.click( step1 ); - - expect( recordEvent ).toHaveBeenCalledTimes( 2 ); - expect( recordEvent ).toHaveBeenNthCalledWith( 1, 'gla_setup_ads', { - action: 'go-to-step2', - triggered_by: 'stepper-step2-button', - } ); - expect( recordEvent ).toHaveBeenNthCalledWith( 2, 'gla_setup_ads', { - action: 'go-to-step1', - triggered_by: 'stepper-step1-button', - } ); - // Step 3 -> Step 1 - await continueUntilStep3(); + // Step 2 -> Step 1 + await continueToStep2(); recordEvent.mockClear(); expect( recordEvent ).toHaveBeenCalledTimes( 0 ); diff --git a/js/src/setup-ads/ads-stepper/setup-paid-ads.js b/js/src/setup-ads/ads-stepper/setup-paid-ads.js new file mode 100644 index 0000000000..6e38e09c92 --- /dev/null +++ b/js/src/setup-ads/ads-stepper/setup-paid-ads.js @@ -0,0 +1,51 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import AppButton from '.~/components/app-button'; +import AdsCampaign from '.~/components/paid-ads/ads-campaign'; +import useGoogleAdsAccountBillingStatus from '.~/hooks/useGoogleAdsAccountBillingStatus'; +import { GOOGLE_ADS_BILLING_STATUS } from '.~/constants'; + +const { APPROVED } = GOOGLE_ADS_BILLING_STATUS; + +/** + * Renders the step to setup paid ads + * + * @param {Object} props Component props. + * @param {boolean} props.isSubmitting Indicates if the form is currently being submitted. + */ +const SetupPaidAds = ( { isSubmitting } ) => { + const { billingStatus } = useGoogleAdsAccountBillingStatus(); + + return ( + ( + + ) } + /> + ); +}; + +export default SetupPaidAds; diff --git a/js/src/setup-ads/setup-ads-form.js b/js/src/setup-ads/setup-ads-form.js index 609c915d93..74b298db71 100644 --- a/js/src/setup-ads/setup-ads-form.js +++ b/js/src/setup-ads/setup-ads-form.js @@ -26,11 +26,10 @@ const SetupAdsForm = () => { const [ isSubmitted, setSubmitted ] = useState( false ); const [ handleSetupComplete, isSubmitting ] = useAdsSetupCompleteCallback(); const adminUrl = useAdminUrl(); - const { data: targetAudience } = useTargetAudienceFinalCountryCodes(); + const { data: countryCodes } = useTargetAudienceFinalCountryCodes(); const initialValues = { amount: 0, - countryCodes: targetAudience, }; useEffect( () => { @@ -55,7 +54,7 @@ const SetupAdsForm = () => { ); const handleSubmit = ( values ) => { - const { amount, countryCodes } = values; + const { amount } = values; recordGlaEvent( 'gla_launch_paid_campaign_button_click', { audiences: countryCodes.join( ',' ), @@ -68,17 +67,10 @@ const SetupAdsForm = () => { }; const handleChange = ( _, values ) => { - const args = [ initialValues, values ].map( - ( { countryCodes, ...v } ) => { - v.countrySet = new Set( countryCodes ); - return v; - } - ); - - setFormChanged( ! isEqual( ...args ) ); + setFormChanged( ! isEqual( initialValues, values ) ); }; - if ( ! targetAudience ) { + if ( ! countryCodes ) { return null; } @@ -88,19 +80,8 @@ const SetupAdsForm = () => { onChange={ handleChange } onSubmit={ handleSubmit } > - { ( formProps ) => { - const mixedFormProps = { - ...formProps, - // TODO: maybe move all API calls in useSetupCompleteCallback to ~./data - isSubmitting, - }; - return ( - <> - - - - ); - } } + + ); }; diff --git a/js/src/setup-mc/setup-stepper/setup-paid-ads/clientSession.js b/js/src/setup-mc/setup-stepper/clientSession.js similarity index 100% rename from js/src/setup-mc/setup-stepper/setup-paid-ads/clientSession.js rename to js/src/setup-mc/setup-stepper/clientSession.js diff --git a/js/src/setup-mc/setup-stepper/setup-paid-ads/constants.js b/js/src/setup-mc/setup-stepper/constants.js similarity index 100% rename from js/src/setup-mc/setup-stepper/setup-paid-ads/constants.js rename to js/src/setup-mc/setup-stepper/constants.js diff --git a/js/src/setup-mc/setup-stepper/setup-paid-ads.js b/js/src/setup-mc/setup-stepper/setup-paid-ads.js new file mode 100644 index 0000000000..f5911f282d --- /dev/null +++ b/js/src/setup-mc/setup-stepper/setup-paid-ads.js @@ -0,0 +1,150 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import apiFetch from '@wordpress/api-fetch'; +import { useState } from '@wordpress/element'; +import { noop } from 'lodash'; + +/** + * Internal dependencies + */ +import useAdminUrl from '.~/hooks/useAdminUrl'; +import useDispatchCoreNotices from '.~/hooks/useDispatchCoreNotices'; +import useAdsSetupCompleteCallback from '.~/hooks/useAdsSetupCompleteCallback'; +import useTargetAudienceFinalCountryCodes from '.~/hooks/useTargetAudienceFinalCountryCodes'; +import AdsCampaign from '.~/components/paid-ads/ads-campaign'; +import CampaignAssetsForm from '.~/components/paid-ads/campaign-assets-form'; +import AppButton from '.~/components/app-button'; +import useGoogleAdsAccountBillingStatus from '.~/hooks/useGoogleAdsAccountBillingStatus'; +import { getProductFeedUrl } from '.~/utils/urls'; +import { API_NAMESPACE } from '.~/data/constants'; +import { GUIDE_NAMES, GOOGLE_ADS_BILLING_STATUS } from '.~/constants'; +import { ACTION_COMPLETE, ACTION_SKIP } from './constants'; +import SkipButton from './skip-button'; +import clientSession from './clientSession'; + +/** + * Clicking on the "Complete setup" button to complete the onboarding flow with paid ads. + * + * @event gla_onboarding_complete_with_paid_ads_button_click + * @property {number} budget The budget for the campaign + * @property {string} audiences The targeted audiences for the campaign + */ + +/** + * Renders the onboarding step for setting up the paid ads (Google Ads account and paid campaign) + * or skipping it, and then completing the onboarding flow. + * @fires gla_onboarding_complete_with_paid_ads_button_click + */ +export default function SetupPaidAds() { + const adminUrl = useAdminUrl(); + const [ completing, setCompleting ] = useState( null ); + const { createNotice } = useDispatchCoreNotices(); + const { data: countryCodes } = useTargetAudienceFinalCountryCodes(); + const [ handleSetupComplete ] = useAdsSetupCompleteCallback(); + const { billingStatus } = useGoogleAdsAccountBillingStatus(); + + const isBillingCompleted = + billingStatus?.status === GOOGLE_ADS_BILLING_STATUS.APPROVED; + + const finishOnboardingSetup = async ( onBeforeFinish = noop ) => { + try { + await onBeforeFinish(); + await apiFetch( { + path: `${ API_NAMESPACE }/mc/settings/sync`, + method: 'POST', + } ); + } catch ( e ) { + setCompleting( null ); + + createNotice( + 'error', + __( + 'Unable to complete your setup.', + 'google-listings-and-ads' + ) + ); + } + + // Force reload WC admin page to initiate the relevant dependencies of the Dashboard page. + const query = { guide: GUIDE_NAMES.SUBMISSION_SUCCESS }; + window.location.href = adminUrl + getProductFeedUrl( query ); + }; + + const handleSkipCreatePaidAds = async () => { + setCompleting( ACTION_SKIP ); + await finishOnboardingSetup(); + }; + + const createSkipButton = ( formContext ) => { + const { isValidForm } = formContext; + + return ( + + ); + }; + + const createContinueButton = ( formContext ) => { + const { isValidForm, values } = formContext; + const { amount } = values; + + const disabled = + completing === ACTION_SKIP || ! isValidForm || ! isBillingCompleted; + + const handleCompleteClick = async () => { + setCompleting( ACTION_COMPLETE ); + const onBeforeFinish = handleSetupComplete.bind( + null, + amount, + countryCodes + ); + + await finishOnboardingSetup( onBeforeFinish ); + }; + + return ( + + ); + }; + + const paidAds = { + amount: 0, + ...clientSession.getCampaign(), + }; + + return ( + { + clientSession.setCampaign( { ...values } ); + } } + > + + + ); +} diff --git a/js/src/setup-mc/setup-stepper/setup-paid-ads/index.js b/js/src/setup-mc/setup-stepper/setup-paid-ads/index.js deleted file mode 100644 index b949d024c1..0000000000 --- a/js/src/setup-mc/setup-stepper/setup-paid-ads/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './setup-paid-ads'; diff --git a/js/src/setup-mc/setup-stepper/setup-paid-ads/paid-ads-setup-sections.js b/js/src/setup-mc/setup-stepper/setup-paid-ads/paid-ads-setup-sections.js deleted file mode 100644 index 7072eab023..0000000000 --- a/js/src/setup-mc/setup-stepper/setup-paid-ads/paid-ads-setup-sections.js +++ /dev/null @@ -1,134 +0,0 @@ -/** - * External dependencies - */ -import { useState, useRef, useEffect } from '@wordpress/element'; -import { Form } from '@woocommerce/components'; - -/** - * Internal dependencies - */ -import useGoogleAdsAccountBillingStatus from '.~/hooks/useGoogleAdsAccountBillingStatus'; -import BudgetSection from '.~/components/paid-ads/budget-section'; -import BillingCard from '.~/components/paid-ads/billing-card'; -import SpinnerCard from '.~/components/spinner-card'; -import Section from '.~/wcdl/section'; -import validateCampaign from '.~/components/paid-ads/validateCampaign'; -import clientSession from './clientSession'; -import { GOOGLE_ADS_BILLING_STATUS } from '.~/constants'; - -/** - * @typedef {import('.~/data/actions').CountryCode} CountryCode - */ - -/** - * @typedef {Object} PaidAdsData - * @property {number|undefined} amount Daily average cost of the paid ads campaign. - * @property {boolean} isValid Whether the campaign data are valid values. - * @property {boolean} isReady Whether the campaign data and the billing setting are ready for completing the paid ads setup. - */ - -const defaultPaidAds = { - amount: 0, - isValid: false, - isReady: false, -}; - -/** - * Resolve the initial paid ads data from the given paid ads data. - * Parts of the resolved data are used in the `initialValues` prop of `Form` component. - * - * @param {PaidAdsData} paidAds The paid ads data as the base to be resolved with other states. - * @return {PaidAdsData} The resolved paid ads data. - */ -function resolveInitialPaidAds( paidAds ) { - const nextPaidAds = { ...paidAds }; - nextPaidAds.isValid = ! Object.keys( validateCampaign( nextPaidAds ) ) - .length; - - return nextPaidAds; -} - -/** - * Renders sections of Google Ads account, budget and billing for setting up the paid ads. - * - * @param {Object} props React props. - * @param {(onStatesReceived: PaidAdsData)=>void} props.onStatesReceived Callback to receive the data for setting up paid ads when initial and also when the budget and billing are updated. - * @param {Array|undefined} props.countryCodes Country codes for the campaign. - */ -export default function PaidAdsSetupSections( { - onStatesReceived, - countryCodes, -} ) { - const { billingStatus } = useGoogleAdsAccountBillingStatus(); - - const onStatesReceivedRef = useRef(); - onStatesReceivedRef.current = onStatesReceived; - - const [ paidAds, setPaidAds ] = useState( () => { - // Resolve the starting paid ads data with the campaign data stored in the client session. - const startingPaidAds = { - ...defaultPaidAds, - ...clientSession.getCampaign(), - }; - return resolveInitialPaidAds( startingPaidAds ); - } ); - - const isBillingCompleted = - billingStatus?.status === GOOGLE_ADS_BILLING_STATUS.APPROVED; - - /* - If a merchant has not yet finished the billing setup, the billing status will be - updated by `useAutoCheckBillingStatusEffect` hook in `BillingSetupCard` component - till it gets completed. - - Or, if the billing setup is already finished, the loaded `billingStatus.status` - will already be 'approved' without passing through the above hook and component. - - Therefore, in order to ensure the parent component can continue the setup from - any billing status, it only needs to watch the `isBillingCompleted` eventually - to wait for the fulfilled 'approved' status, and then propagate it to the parent. - - For example, refresh page during onboarding flow after the billing setup is finished. - */ - useEffect( () => { - const nextPaidAds = { - ...paidAds, - isReady: paidAds.isValid && isBillingCompleted, - }; - onStatesReceivedRef.current( nextPaidAds ); - clientSession.setCampaign( nextPaidAds ); - }, [ paidAds, isBillingCompleted ] ); - - if ( ! billingStatus ) { - return ( -
- -
- ); - } - - const initialValues = { - amount: paidAds.amount, - }; - - return ( -
{ - setPaidAds( { ...paidAds, ...values, isValid } ); - } } - validate={ validateCampaign } - > - { ( formProps ) => { - return ( - - - - ); - } } -
- ); -} diff --git a/js/src/setup-mc/setup-stepper/setup-paid-ads/setup-paid-ads.js b/js/src/setup-mc/setup-stepper/setup-paid-ads/setup-paid-ads.js deleted file mode 100644 index 76b6381fb9..0000000000 --- a/js/src/setup-mc/setup-stepper/setup-paid-ads/setup-paid-ads.js +++ /dev/null @@ -1,216 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; -import apiFetch from '@wordpress/api-fetch'; -import { select } from '@wordpress/data'; -import { useState } from '@wordpress/element'; -import { Flex } from '@wordpress/components'; -import { noop } from 'lodash'; - -/** - * Internal dependencies - */ -import useAdminUrl from '.~/hooks/useAdminUrl'; -import useDispatchCoreNotices from '.~/hooks/useDispatchCoreNotices'; -import useGoogleAdsAccount from '.~/hooks/useGoogleAdsAccount'; -import useAdsSetupCompleteCallback from '.~/hooks/useAdsSetupCompleteCallback'; -import useTargetAudienceFinalCountryCodes from '.~/hooks/useTargetAudienceFinalCountryCodes'; -import StepContent from '.~/components/stepper/step-content'; -import StepContentHeader from '.~/components/stepper/step-content-header'; -import StepContentFooter from '.~/components/stepper/step-content-footer'; -import StepContentActions from '.~/components/stepper/step-content-actions'; -import AppButton from '.~/components/app-button'; -import PaidAdsFaqsPanel from '.~/components/paid-ads/faqs-panel'; -import PaidAdsFeaturesSection from './paid-ads-features-section'; -import PaidAdsSetupSections from './paid-ads-setup-sections'; -import SkipPaidAdsConfirmationModal from './skip-paid-ads-confirmation-modal'; -import { getProductFeedUrl } from '.~/utils/urls'; -import { API_NAMESPACE, STORE_KEY } from '.~/data/constants'; -import { GUIDE_NAMES } from '.~/constants'; -import { ACTION_COMPLETE, ACTION_SKIP } from './constants'; -import { recordGlaEvent } from '.~/utils/tracks'; - -/** - * Clicking on the "Create a paid ad campaign" button to open the paid ads setup in the onboarding flow. - * - * @event gla_onboarding_open_paid_ads_setup_button_click - */ - -/** - * Clicking on the "Complete setup" button to complete the onboarding flow with paid ads. - * - * @event gla_onboarding_complete_with_paid_ads_button_click - * @property {number} budget The budget for the campaign - * @property {string} audiences The targeted audiences for the campaign - */ - -/** - * Clicking on the skip paid ads button to complete the onboarding flow. - * The 'unknown' value of properties may means: - * - the final status has not yet been resolved when recording this event - * - the status is not available, for example, the billing status is unknown if Google Ads account is not yet connected - * - * @event gla_onboarding_complete_button_click - * @property {string} google_ads_account_status The connection status of merchant's Google Ads addcount, e.g. 'connected', 'disconnected', 'incomplete' - * @property {string} billing_method_status The status of billing method of merchant's Google Ads addcount e.g. 'unknown', 'pending', 'approved', 'cancelled' - * @property {string} campaign_form_validation Whether the entered paid campaign form data are valid, e.g. 'unknown', 'valid', 'invalid' - */ - -/** - * Renders the onboarding step for setting up the paid ads (Google Ads account and paid campaign) - * or skipping it, and then completing the onboarding flow. - * - * @fires gla_onboarding_open_paid_ads_setup_button_click - * @fires gla_onboarding_complete_with_paid_ads_button_click - * @fires gla_onboarding_complete_button_click - */ -export default function SetupPaidAds() { - const adminUrl = useAdminUrl(); - const { createNotice } = useDispatchCoreNotices(); - const { data: countryCodes } = useTargetAudienceFinalCountryCodes(); - const { googleAdsAccount, hasGoogleAdsConnection } = useGoogleAdsAccount(); - const [ handleSetupComplete ] = useAdsSetupCompleteCallback(); - const [ paidAds, setPaidAds ] = useState( {} ); - const [ completing, setCompleting ] = useState( null ); - const [ - showSkipPaidAdsConfirmationModal, - setShowSkipPaidAdsConfirmationModal, - ] = useState( false ); - - const finishOnboardingSetup = async ( event, onBeforeFinish = noop ) => { - setCompleting( event.target.dataset.action ); - - try { - await onBeforeFinish(); - await apiFetch( { - path: `${ API_NAMESPACE }/mc/settings/sync`, - method: 'POST', - } ); - } catch ( e ) { - setCompleting( null ); - - createNotice( - 'error', - __( - 'Unable to complete your setup.', - 'google-listings-and-ads' - ) - ); - } - - // Force reload WC admin page to initiate the relevant dependencies of the Dashboard page. - const query = { guide: GUIDE_NAMES.SUBMISSION_SUCCESS }; - window.location.href = adminUrl + getProductFeedUrl( query ); - }; - - const handleCompleteClick = async ( event ) => { - const onBeforeFinish = handleSetupComplete.bind( - null, - paidAds.amount, - countryCodes - ); - await finishOnboardingSetup( event, onBeforeFinish ); - }; - - const handleSkipCreatePaidAds = async ( event ) => { - const selector = select( STORE_KEY ); - const billing = selector.getGoogleAdsAccountBillingStatus(); - - setShowSkipPaidAdsConfirmationModal( false ); - - const eventProps = { - google_ads_account_status: googleAdsAccount?.status, - billing_method_status: billing?.status || 'unknown', - campaign_form_validation: paidAds.isValid ? 'valid' : 'invalid', - }; - - recordGlaEvent( 'gla_onboarding_complete_button_click', eventProps ); - - await finishOnboardingSetup( event ); - }; - - const handleShowSkipPaidAdsConfirmationModal = () => { - setShowSkipPaidAdsConfirmationModal( true ); - }; - - const handleCancelSkipPaidAdsClick = () => { - setShowSkipPaidAdsConfirmationModal( false ); - }; - - // The status check of Google Ads account connection is included in `paidAds.isReady`, - // because when there is no connected account, it will disable the budget section and set the `amount` to `undefined`. - const disabledComplete = completing === ACTION_SKIP || ! paidAds.isReady; - - function createSkipButton( text ) { - const disabledSkip = - completing === ACTION_COMPLETE || ! hasGoogleAdsConnection; - - return ( - - ); - } - - return ( - - - - - - { showSkipPaidAdsConfirmationModal && ( - - ) } - - - - - { createSkipButton( - __( - 'Skip paid ads creation', - 'google-listings-and-ads' - ) - ) } - - - - - - - ); -} diff --git a/js/src/setup-mc/setup-stepper/skip-button.js b/js/src/setup-mc/setup-stepper/skip-button.js new file mode 100644 index 0000000000..c5a62f2d54 --- /dev/null +++ b/js/src/setup-mc/setup-stepper/skip-button.js @@ -0,0 +1,84 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useState } from '@wordpress/element'; +import { noop } from 'lodash'; + +/** + * Internal dependencies + */ +import useGoogleAdsAccount from '.~/hooks/useGoogleAdsAccount'; +import useGoogleAdsAccountBillingStatus from '.~/hooks/useGoogleAdsAccountBillingStatus'; +import AppButton from '.~/components/app-button'; +import SkipPaidAdsConfirmationModal from './skip-paid-ads-confirmation-modal'; +import { recordGlaEvent } from '.~/utils/tracks'; + +/** + * Clicking on the skip paid ads button to complete the onboarding flow. + * The 'unknown' value of properties may means: + * - the final status has not yet been resolved when recording this event + * - the status is not available, for example, the billing status is unknown if Google Ads account is not yet connected + * + * @event gla_onboarding_complete_button_click + * @property {string} google_ads_account_status The connection status of merchant's Google Ads addcount, e.g. 'connected', 'disconnected', 'incomplete' + * @property {string} billing_method_status The status of billing method of merchant's Google Ads addcount e.g. 'unknown', 'pending', 'approved', 'cancelled' + * @property {string} campaign_form_validation Whether the entered paid campaign form data are valid, e.g. 'unknown', 'valid', 'invalid' + */ + +export default function SkipButton( { + isValidForm, + onSkipCreatePaidAds = noop, + loading, + disabled, +} ) { + const [ + showSkipPaidAdsConfirmationModal, + setShowSkipPaidAdsConfirmationModal, + ] = useState( false ); + const { googleAdsAccount } = useGoogleAdsAccount(); + const { billingStatus } = useGoogleAdsAccountBillingStatus(); + + const handleOnSkipClick = () => { + setShowSkipPaidAdsConfirmationModal( true ); + }; + + const handleCancelSkipPaidAdsClick = () => { + setShowSkipPaidAdsConfirmationModal( false ); + }; + + const handleSkipCreatePaidAds = () => { + setShowSkipPaidAdsConfirmationModal( false ); + + const eventProps = { + google_ads_account_status: googleAdsAccount?.status, + billing_method_status: billingStatus?.status || 'unknown', + campaign_form_validation: isValidForm ? 'valid' : 'invalid', + }; + recordGlaEvent( 'gla_onboarding_complete_button_click', eventProps ); + + onSkipCreatePaidAds(); + }; + + return ( + <> + + + { showSkipPaidAdsConfirmationModal && ( + + ) } + + ); +} diff --git a/js/src/setup-mc/setup-stepper/setup-paid-ads/skip-paid-ads-confirmation-modal.js b/js/src/setup-mc/setup-stepper/skip-paid-ads-confirmation-modal.js similarity index 96% rename from js/src/setup-mc/setup-stepper/setup-paid-ads/skip-paid-ads-confirmation-modal.js rename to js/src/setup-mc/setup-stepper/skip-paid-ads-confirmation-modal.js index 18de3c6583..b2ebb0a4de 100644 --- a/js/src/setup-mc/setup-stepper/setup-paid-ads/skip-paid-ads-confirmation-modal.js +++ b/js/src/setup-mc/setup-stepper/skip-paid-ads-confirmation-modal.js @@ -9,7 +9,6 @@ import { __ } from '@wordpress/i18n'; import AppModal from '.~/components/app-modal'; import AppButton from '.~/components/app-button'; import AppDocumentationLink from '.~/components/app-documentation-link'; -import { ACTION_SKIP } from './constants'; /** * @fires gla_documentation_link_click with `{ context: 'skip-paid-ads-modal', link_id: 'paid-ads-with-performance-max-campaigns-learn-more', href: 'https://support.google.com/google-ads/answer/10724817' }` @@ -37,7 +36,6 @@ const SkipPaidAdsConfirmationModal = ( { { __( 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 aa8218b58e..987a7111ae 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 @@ -11,8 +11,6 @@ import SetupAdsAccountsPage from '../../utils/pages/setup-ads/setup-ads-accounts import SetupBudgetPage from '../../utils/pages/setup-ads/setup-budget'; import { LOAD_STATE } from '../../utils/constants'; import { - getCountryInputSearchBoxContainer, - getCountryTagsFromInputSearchBoxContainer, getFAQPanelTitle, getFAQPanelRow, checkFAQExpandable, @@ -69,6 +67,9 @@ test.describe( 'Set up Ads account', () => { setupBudgetPage = new SetupBudgetPage( page ); await setOnboardedMerchant(); await setupAdsAccounts.mockAdsAccountsResponse( [] ); + await setupBudgetPage.fulfillBillingStatusRequest( { + status: 'approved', + } ); await dashboardPage.mockRequests(); await dashboardPage.goto(); } ); @@ -280,26 +281,15 @@ test.describe( 'Set up Ads account', () => { } ); test.describe( 'Create your paid campaign', () => { - test.beforeAll( async () => { - await setupBudgetPage.fulfillBillingStatusRequest( { - status: 'approved', - } ); - } ); - test( 'Continue to create paid ad campaign', async () => { await setupAdsAccounts.clickContinue(); await page.waitForLoadState( LOAD_STATE.DOM_CONTENT_LOADED ); - await expect( page.getByRole( 'heading', { name: 'Create your paid campaign', } ) ).toBeVisible(); - await expect( - page.getByRole( 'heading', { name: 'Ads audience' } ) - ).toBeVisible(); - await expect( page.getByRole( 'heading', { name: 'Set your budget' } ) ).toBeVisible(); @@ -357,17 +347,6 @@ test.describe( 'Set up Ads account', () => { } ); } ); - test( 'Audience should be United States', async () => { - const countrySearchBoxContainer = - getCountryInputSearchBoxContainer( page ); - const countryTags = - getCountryTagsFromInputSearchBoxContainer( page ); - await expect( countryTags ).toHaveCount( 1 ); - await expect( countrySearchBoxContainer ).toContainText( - 'United States' - ); - } ); - test( 'Set the budget', async () => { budget = '0'; await setupBudgetPage.fillBudget( budget ); diff --git a/tests/e2e/specs/setup-mc/step-4-complete-campaign.test.js b/tests/e2e/specs/setup-mc/step-4-complete-campaign.test.js index 92c3fb3206..adc31ea614 100644 --- a/tests/e2e/specs/setup-mc/step-4-complete-campaign.test.js +++ b/tests/e2e/specs/setup-mc/step-4-complete-campaign.test.js @@ -247,12 +247,21 @@ test.describe( 'Complete your campaign', () => { } ); test( 'should see billing has been set up successfully when billing status API returns approved', async () => { + await setupBudgetPage.mockAdsAccountsResponse( { + id: 12345, + billing_url: null, + } ); await setupBudgetPage.fulfillBillingStatusRequest( { - status: 'approved', + status: 'pending', } ); await newPage.close(); - await page.reload(); + // return focus to the page. + await setupBudgetPage.focusBudget(); + await setupBudgetPage.fulfillBillingStatusRequest( { + status: 'approved', + } ); + await setupBudgetPage.awaitForBillingStatusRequest(); const billingSetupSuccessSection = setupBudgetPage.getBillingSetupSuccessSection(); diff --git a/tests/e2e/utils/page.js b/tests/e2e/utils/page.js index 54912b00f8..51713bd110 100644 --- a/tests/e2e/utils/page.js +++ b/tests/e2e/utils/page.js @@ -54,19 +54,6 @@ export function getCountryInputSearchBoxContainer( page ) { ); } -/** - * Get country tags from input search box container. - * - * @param {import('@playwright/test').Page} page The current page. - * - * @return {import('@playwright/test').Locator} Get country tags from input search box container. - */ -export function getCountryTagsFromInputSearchBoxContainer( page ) { - return getCountryInputSearchBoxContainer( page ).locator( - '.woocommerce-tag' - ); -} - /** * Get country input search box. * diff --git a/tests/e2e/utils/pages/setup-ads/setup-budget.js b/tests/e2e/utils/pages/setup-ads/setup-budget.js index bf9bab0b71..bb164ff592 100644 --- a/tests/e2e/utils/pages/setup-ads/setup-budget.js +++ b/tests/e2e/utils/pages/setup-ads/setup-budget.js @@ -99,6 +99,16 @@ export default class SetupBudget extends MockRequests { await input.fill( budget ); } + /** + * Focus the budget input. + * + * @return {Promise} + */ + async focusBudget() { + const input = this.getBudgetInput(); + await input.focus(); + } + /** * Click set up billing button. *