diff --git a/js/src/components/ads-account-select-control/index.js b/js/src/components/ads-account-select-control/index.js index ccb26c3447..12de497df3 100644 --- a/js/src/components/ads-account-select-control/index.js +++ b/js/src/components/ads-account-select-control/index.js @@ -1,8 +1,15 @@ +/** + * External dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { getSetting } from '@woocommerce/settings'; // eslint-disable-line import/no-unresolved + /** * Internal dependencies */ import AppSelectControl from '.~/components/app-select-control'; import useExistingGoogleAdsAccounts from '.~/hooks/useExistingGoogleAdsAccounts'; +import useGoogleAdsAccount from '.~/hooks/useGoogleAdsAccount'; /** * @param {Object} props The component props @@ -10,6 +17,35 @@ import useExistingGoogleAdsAccounts from '.~/hooks/useExistingGoogleAdsAccounts' */ const AdsAccountSelectControl = ( props ) => { const { existingAccounts } = useExistingGoogleAdsAccounts(); + const { googleAdsAccount, hasGoogleAdsConnection } = useGoogleAdsAccount(); + + const accountIdExists = existingAccounts?.some( + ( existingAccount ) => existingAccount.id === googleAdsAccount?.id + ); + + // If the account ID is not in the list of existing accounts, fake the select options by displaying the connected account ID only. + if ( ! accountIdExists && hasGoogleAdsConnection ) { + const domain = new URL( getSetting( 'homeUrl' ) ).host; + + return ( + + ); + } const options = existingAccounts?.map( ( acc ) => ( { value: acc.id, diff --git a/js/src/components/google-account-card/connected-google-account-card.js b/js/src/components/google-account-card/connected-google-account-card.js index be55f49d0e..4a3b833ea5 100644 --- a/js/src/components/google-account-card/connected-google-account-card.js +++ b/js/src/components/google-account-card/connected-google-account-card.js @@ -1,22 +1,10 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; - /** * Internal dependencies */ import AccountCard, { APPEARANCE } from '.~/components/account-card'; -import AppButton from '.~/components/app-button'; import ConnectedIconLabel from '.~/components/connected-icon-label'; import Section from '.~/wcdl/section'; -import useSwitchGoogleAccount from './useSwitchGoogleAccount'; - -/** - * Clicking on the "connect to a different Google account" button. - * - * @event gla_google_account_connect_different_account_button_click - */ +import SwitchAccountButton from './switch-account-button'; /** * Renders a Google account card UI with connected account information. @@ -26,16 +14,12 @@ import useSwitchGoogleAccount from './useSwitchGoogleAccount'; * @param {{ email: string }} props.googleAccount A data payload object containing the user's Google account email. * @param {JSX.Element} [props.helper] Helper content below the Google account email. * @param {boolean} [props.hideAccountSwitch=false] Indicate whether hide the account switch block at the card footer. - * - * @fires gla_google_account_connect_different_account_button_click */ const ConnectedGoogleAccountCard = ( { googleAccount, helper, hideAccountSwitch = false, } ) => { - const [ handleSwitch, { loading } ] = useSwitchGoogleAccount(); - return ( { ! hideAccountSwitch && ( - + ) } diff --git a/js/src/components/google-account-card/switch-account-button.js b/js/src/components/google-account-card/switch-account-button.js new file mode 100644 index 0000000000..39838c702a --- /dev/null +++ b/js/src/components/google-account-card/switch-account-button.js @@ -0,0 +1,46 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import AppButton from '.~/components/app-button'; +import useSwitchGoogleAccount from './useSwitchGoogleAccount'; + +/** + * Clicking on the "connect to a different Google account" button. + * + * @event gla_google_account_connect_different_account_button_click + */ + +/** + * Renders a switch button that lets user connect with another Google account. + * + * @fires gla_google_account_connect_different_account_button_click + * @param {Object} props React props + * @param {string} [props.text="Or, connect to a different Google account"] Text to display on the button + */ +const SwitchAccountButton = ( { + text = __( + 'Or, connect to a different Google account', + 'google-listings-and-ads' + ), + ...restProps +} ) => { + const [ handleSwitch, { loading } ] = useSwitchGoogleAccount(); + + return ( + + ); +}; + +export default SwitchAccountButton; diff --git a/js/src/components/google-account-card/useSwitchGoogleAccount.js b/js/src/components/google-account-card/useSwitchGoogleAccount.js index 2e38f10f94..c151d86311 100644 --- a/js/src/components/google-account-card/useSwitchGoogleAccount.js +++ b/js/src/components/google-account-card/useSwitchGoogleAccount.js @@ -37,6 +37,14 @@ const useSwitchGoogleAccount = () => { method: 'DELETE', } ); + const [ + fetchGoogleAdsDisconnect, + { loading: loadingGoogleAdsDisconnect }, + ] = useApiFetchCallback( { + path: `${ API_NAMESPACE }/ads/connection`, + method: 'DELETE', + } ); + /** * Note: we are manually calling `DELETE /google/connect` instead of using * `disconnectGoogleAccount` action from wp-data store @@ -66,6 +74,7 @@ const useSwitchGoogleAccount = () => { try { await fetchGoogleMCDisconnect(); + await fetchGoogleAdsDisconnect(); await fetchGoogleDisconnect(); const { url } = await fetchGoogleConnect(); window.location.href = url; @@ -83,6 +92,7 @@ const useSwitchGoogleAccount = () => { const loading = loadingGoogleMCDisconnect || + loadingGoogleAdsDisconnect || loadingGoogleDisconnect || loadingGoogleConnect || dataGoogleConnect; diff --git a/js/src/components/google-combo-account-card/claim-ads-account/claim-ads-account.js b/js/src/components/google-combo-account-card/claim-ads-account/claim-ads-account.js index 298c7bc42a..28e0df8e27 100644 --- a/js/src/components/google-combo-account-card/claim-ads-account/claim-ads-account.js +++ b/js/src/components/google-combo-account-card/claim-ads-account/claim-ads-account.js @@ -2,6 +2,8 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; +import { Icon } from '@wordpress/components'; +import { external as externalIcon } from '@wordpress/icons'; /** * Internal dependencies @@ -40,13 +42,13 @@ const ClaimAdsAccount = () => { 'google-listings-and-ads' ) }

- + { __( 'Claim account in Google Ads', 'google-listings-and-ads' ) } - isPrimary - /> + + ); }; diff --git a/js/src/components/google-combo-account-card/connect-ads/connect-ads-footer.js b/js/src/components/google-combo-account-card/connect-ads/connect-ads-footer.js index 6c2cd912c7..df8ed23192 100644 --- a/js/src/components/google-combo-account-card/connect-ads/connect-ads-footer.js +++ b/js/src/components/google-combo-account-card/connect-ads/connect-ads-footer.js @@ -8,6 +8,9 @@ import { __ } from '@wordpress/i18n'; */ import AppButton from '.~/components/app-button'; import DisconnectAccount from '.~/components/google-ads-account-card/disconnect-account'; +import useExistingGoogleAdsAccounts from '.~/hooks/useExistingGoogleAdsAccounts'; +import useGoogleAdsAccountStatus from '.~/hooks/useGoogleAdsAccountStatus'; +import useGoogleAdsAccount from '.~/hooks/useGoogleAdsAccount'; /** * Footer component. @@ -15,20 +18,37 @@ import DisconnectAccount from '.~/components/google-ads-account-card/disconnect- * @param {Object} props Props. * @param {boolean} props.isConnected Whether the account is connected. * @param {Function} props.onCreateNewClick Callback when clicking on the button to create a new account. + * @param {boolean} props.disabled Whether to disable the create account button. * @param {Object} props.restProps Rest props. Passed to AppButton. * @return {JSX.Element} Footer component. */ const ConnectAdsFooter = ( { isConnected, onCreateNewClick, + disabled, ...restProps } ) => { - if ( isConnected ) { + const { existingAccounts } = useExistingGoogleAdsAccounts(); + const { googleAdsAccount } = useGoogleAdsAccount(); + const { hasAccess } = useGoogleAdsAccountStatus(); + const shouldClaimGoogleAdsAccount = Boolean( + googleAdsAccount?.id && hasAccess === false + ); + + if ( isConnected && existingAccounts.length > 0 ) { return ; } + const disabledButton = + disabled || + ( shouldClaimGoogleAdsAccount && ! existingAccounts.length ); return ( - + { __( 'Or, create a new Google Ads account', 'google-listings-and-ads' diff --git a/js/src/components/google-combo-account-card/connect-ads/connect-existing-account.js b/js/src/components/google-combo-account-card/connect-ads/connect-existing-account.js index a42679f6ff..e074405a00 100644 --- a/js/src/components/google-combo-account-card/connect-ads/connect-existing-account.js +++ b/js/src/components/google-combo-account-card/connect-ads/connect-existing-account.js @@ -31,8 +31,12 @@ const ConnectExistingAccount = ( { onCreateClick } ) => { const { createNotice } = useDispatchCoreNotices(); const { fetchGoogleAdsAccountStatus } = useAppDispatch(); const isConnected = useGoogleAdsAccountReady(); - const { googleAdsAccount, hasFinishedResolution, refetchGoogleAdsAccount } = - useGoogleAdsAccount(); + const { + googleAdsAccount, + hasFinishedResolution, + hasGoogleAdsConnection, + refetchGoogleAdsAccount, + } = useGoogleAdsAccount(); const [ connectGoogleAdsAccount ] = useApiFetchCallback( { path: '/wc/gla/ads/accounts', method: 'POST', @@ -40,10 +44,10 @@ const ConnectExistingAccount = ( { onCreateClick } ) => { } ); useEffect( () => { - if ( isConnected ) { + if ( hasGoogleAdsConnection ) { setValue( googleAdsAccount.id ); } - }, [ googleAdsAccount, isConnected ] ); + }, [ googleAdsAccount, hasGoogleAdsConnection ] ); const handleConnectClick = async () => { if ( ! value ) { @@ -86,7 +90,11 @@ const ConnectExistingAccount = ( { onCreateClick } ) => { } return ( - + ); }; @@ -108,13 +116,13 @@ const ConnectExistingAccount = ( { onCreateClick } ) => { value={ value } onChange={ setValue } autoSelectFirstOption - nonInteractive={ isConnected } + nonInteractive={ hasGoogleAdsConnection } /> } actions={ } diff --git a/js/src/components/google-combo-account-card/connect-ads/upserting-account.js b/js/src/components/google-combo-account-card/connect-ads/upserting-account.js index 61e22ca118..063daac196 100644 --- a/js/src/components/google-combo-account-card/connect-ads/upserting-account.js +++ b/js/src/components/google-combo-account-card/connect-ads/upserting-account.js @@ -37,7 +37,7 @@ const UpsertingAccount = ( { upsertingAction } ) => { className="gla-google-combo-service-account-card--ads" title={ title } helper={ __( - 'This may take a few minutes, please wait a moment…', + 'This may take a few moments, please wait…', 'google-listings-and-ads' ) } indicator={ } diff --git a/js/src/components/google-combo-account-card/connected-google-combo-account-card.js b/js/src/components/google-combo-account-card/connected-google-combo-account-card.js index cd7864c70b..9acba1c41d 100644 --- a/js/src/components/google-combo-account-card/connected-google-combo-account-card.js +++ b/js/src/components/google-combo-account-card/connected-google-combo-account-card.js @@ -1,7 +1,8 @@ /** * External dependencies */ -import { useEffect } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { useState, useEffect } from '@wordpress/element'; /** * Internal dependencies @@ -20,8 +21,9 @@ import useGoogleMCAccount from '.~/hooks/useGoogleMCAccount'; import useExistingGoogleMCAccounts from '.~/hooks/useExistingGoogleMCAccounts'; import useCreateMCAccount from '.~/hooks/useCreateMCAccount'; import ConnectMC from '.~/components/google-mc-account-card/connect-mc'; -import useGoogleAdsAccountReady from '.~/hooks/useGoogleAdsAccountReady'; import useExistingGoogleAdsAccounts from '.~/hooks/useExistingGoogleAdsAccounts'; +import AppButton from '.~/components/app-button'; +import SwitchAccountButton from '.~/components/google-account-card/switch-account-button'; import useGoogleAdsAccountStatus from '.~/hooks/useGoogleAdsAccountStatus'; import useGoogleAdsAccount from '.~/hooks/useGoogleAdsAccount'; import useUpsertAdsAccount from '.~/hooks/useUpsertAdsAccount'; @@ -33,26 +35,34 @@ import './connected-google-combo-account-card.scss'; * It will also kickoff Ads and Merchant Center account creation if the user does not have accounts. */ const ConnectedGoogleComboAccountCard = () => { + const [ editMode, setEditMode ] = useState( false ); + // We use a single instance of the hook to create a MC (Merchant Center) account, // ensuring consistent results across both the main component (ConnectedGoogleComboAccountCard) and its child component (ConnectMC). // This approach is especially useful when an MC account is automatically created, and the URL needs to be reclaimed. // The URL reclaim component is rendered within the ConnectMC component. const [ createMCAccount, resultCreateMCAccount ] = useCreateMCAccount(); const { data: existingGoogleMCAccounts } = useExistingGoogleMCAccounts(); - const { isReady: isGoogleMCReady } = useGoogleMCAccount(); const { hasDetermined, creatingWhich } = useAutoCreateAdsMCAccounts( createMCAccount ); const { text, subText } = getAccountCreationTexts( creatingWhich ); const { existingAccounts: existingGoogleAdsAccounts } = useExistingGoogleAdsAccounts(); - const isConnected = useGoogleAdsAccountReady(); - const { hasGoogleMCConnection, hasFinishedResolution } = - useGoogleMCAccount(); + const { + isReady: isGoogleMCReady, + hasGoogleMCConnection, + hasFinishedResolution, + } = useGoogleMCAccount(); const { invalidateResolution } = useAppDispatch(); - const { googleAdsAccount } = useGoogleAdsAccount(); + const { googleAdsAccount, hasGoogleAdsConnection } = useGoogleAdsAccount(); const { hasAccess, step } = useGoogleAdsAccountStatus(); const [ upsertAdsAccount, { action, loading } ] = useUpsertAdsAccount(); + const hasExistingGoogleMCAccounts = existingGoogleMCAccounts?.length > 0; + const hasExistingGoogleAdsAccounts = existingGoogleAdsAccounts?.length > 0; + const shouldClaimGoogleAdsAccount = Boolean( + ! loading && googleAdsAccount?.id && hasAccess === false + ); const finalizeAdsAccountCreation = hasAccess === true && step === 'conversion_action'; @@ -69,26 +79,95 @@ const ConnectedGoogleComboAccountCard = () => { upsertAccount(); }, [ finalizeAdsAccountCreation, upsertAdsAccount, invalidateResolution ] ); + const handleCancelClick = () => { + setEditMode( false ); + }; + + const handleEditClick = () => { + setEditMode( true ); + }; + + // During MC account creation, we need to show the ConnectMC component + // when the account creation needs to show the Switch or Reclaim flow. + const googleMCHasError = [ 409, 403 ].includes( + resultCreateMCAccount.response?.status + ); + + // After creating a new account, it may be connected but not ready + // (e.g., needing to reclaim the URL). In this case, we show the ConnectMC + // component, even if the existing accounts list has not yet updated. + // + // The last `hasGoogleMCConnection` condition exists for the scenario with these steps: + // 1. Automatically creating a Google Merchant Center account and reclaiming URL is required. + // 2. The merchant interrupts the onboarding flow and then resumes it. + // This is also the case for refreshing webpage. + // 3. The newly created account is not yet among the existing accounts. + // 4. The condition enables the merchant to resume the Google Merchant Center connection + // from the step of connecting the newly created account + const canShowConnectMC = + googleMCHasError || + hasExistingGoogleMCAccounts || + hasGoogleMCConnection; + const showConnectMC = canShowConnectMC && ( editMode || ! isGoogleMCReady ); + + // After creating a new account, it may not show up in the existing accounts list + // immediately. In this case, we show the ConnectAds component in edit mode unless + // we're showing the claim notice in the upper card. + const canShowConnectAds = + hasGoogleAdsConnection || hasExistingGoogleAdsAccounts; + const showConnectAds = + canShowConnectAds && ( editMode || ! hasGoogleAdsConnection ); + + // When Ads and MC are disconnected in edit mode, exit edit mode. + useEffect( () => { + if ( editMode && ! hasGoogleMCConnection && ! hasGoogleAdsConnection ) { + setEditMode( false ); + } + }, [ editMode, hasGoogleAdsConnection, hasGoogleMCConnection ] ); + if ( ! hasDetermined ) { return ; } - // @TODO: edit mode implementation in 2605 - const editMode = false; - const shouldClaimGoogleAdsAccount = Boolean( - ! loading && googleAdsAccount?.id && hasAccess === false + const switchAccountButton = ( + ); - const hasExistingGoogleMCAccounts = existingGoogleMCAccounts?.length > 0; - const showConnectMC = - ( editMode && hasExistingGoogleMCAccounts ) || - ( ! isGoogleMCReady && hasExistingGoogleMCAccounts ); - - const hasExistingGoogleAdsAccounts = existingGoogleAdsAccounts?.length > 0; - const showConnectAds = - ( ( editMode && hasExistingGoogleAdsAccounts ) || - ( ! isConnected && hasExistingGoogleAdsAccounts ) ) && - ! shouldClaimGoogleAdsAccount; + const getCardActions = () => { + if ( editMode ) { + return ( +
+ { switchAccountButton } + + { __( 'Cancel', 'google-listings-and-ads' ) } + +
+ ); + } + + // When not in edit mode, only show the edit button if clicking the + // button would change the visibility of the ConnectAds or ConnectMC cards. + return ( +
+ { ( showConnectAds || ! canShowConnectAds ) && + ( showConnectMC || ! canShowConnectMC ) ? ( + switchAccountButton + ) : ( + + ) } +
+ ); + }; // Show the spinner if there's an account creation in progress and account should not be claimed. // If we are not showing the ConnectMC screen, for e.g when we are creating the first account, @@ -100,15 +179,16 @@ const ConnectedGoogleComboAccountCard = () => { const showConversionMeasurementNotice = showAdsConversionNotice( googleAdsAccount ); - const showAddressCard = hasFinishedResolution && hasGoogleMCConnection; + const showAddressCard = hasFinishedResolution && isGoogleMCReady; return ( -
+
} + actions={ getCardActions() } helper={ subText } indicator={ } detail={ diff --git a/js/src/components/google-combo-account-card/connected-google-combo-account-card.scss b/js/src/components/google-combo-account-card/connected-google-combo-account-card.scss index 9c65e53adf..2fa9639124 100644 --- a/js/src/components/google-combo-account-card/connected-google-combo-account-card.scss +++ b/js/src/components/google-combo-account-card/connected-google-combo-account-card.scss @@ -1,6 +1,26 @@ +.gla-google-combo-account-card-wrapper { + + .components-card { + &:not(:last-child) { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + + &:not(:first-child) { + border-top-left-radius: 0; + border-top-right-radius: 0; + } + } +} + .gla-google-combo-account-card--connected { .gla-account-card__description { gap: 0; } + + .gla-google-combo-account-card__description-actions { + display: flex; + gap: var(--large-gap); + } } diff --git a/js/src/components/google-mc-account-card/connect-mc/actions.js b/js/src/components/google-mc-account-card/connect-mc/actions.js index 30517e839c..c6932ae911 100644 --- a/js/src/components/google-mc-account-card/connect-mc/actions.js +++ b/js/src/components/google-mc-account-card/connect-mc/actions.js @@ -8,6 +8,7 @@ import { __ } from '@wordpress/i18n'; */ import CreateAccountButton from '../create-account-button'; import DisconnectAccountButton from '../disconnect-account-button'; +import useExistingGoogleMCAccounts from '.~/hooks/useExistingGoogleMCAccounts'; /** * Actions component. @@ -27,7 +28,9 @@ const Actions = ( { resultCreateAccount, onCreateAccount, } ) => { - if ( isConnected ) { + const { data: existingGoogleMCAccounts } = useExistingGoogleMCAccounts(); + + if ( isConnected && existingGoogleMCAccounts.length > 0 ) { const handleOnDisconnected = () => { resultConnectMC.reset(); resultCreateAccount.reset(); diff --git a/js/src/components/google-mc-account-card/connect-mc/index.js b/js/src/components/google-mc-account-card/connect-mc/index.js index d8eb62d52e..751dbf46c3 100644 --- a/js/src/components/google-mc-account-card/connect-mc/index.js +++ b/js/src/components/google-mc-account-card/connect-mc/index.js @@ -52,13 +52,14 @@ const ConnectMC = ( { createAccount, resultCreateAccount, className } ) => { googleMCAccount, hasFinishedResolution, isReady: isGoogleMCReady, + hasGoogleMCConnection, } = useGoogleMCAccount(); useEffect( () => { - if ( isGoogleMCReady ) { + if ( hasGoogleMCConnection ) { setValue( googleMCAccount.id ); } - }, [ googleMCAccount, isGoogleMCReady ] ); + }, [ googleMCAccount, hasGoogleMCConnection ] ); if ( ! isGoogleMCReady ) { if ( resultConnectMC.response?.status === 409 ) { @@ -152,14 +153,14 @@ const ConnectMC = ( { createAccount, resultCreateAccount, className } ) => { indicator={ getIndicator() } detail={ } actions={ { { data: { id }, } ); const homeUrl = getSetting( 'homeUrl' ); + const { data: existingGoogleMCAccounts } = useExistingGoogleMCAccounts(); + const hasExistingGoogleMCAccounts = existingGoogleMCAccounts?.length > 0; const handleReclaimClick = async () => { reset(); @@ -65,17 +68,19 @@ const ReclaimUrlCard = ( { id, websiteUrl, onSwitchAccount = noop } ) => { id ) } indicator={ - - { __( 'Switch account', 'google-listings-and-ads' ) } - + hasExistingGoogleMCAccounts ? ( + + { __( 'Switch account', 'google-listings-and-ads' ) } + + ) : null } > diff --git a/js/src/hooks/useGoogleMCAccount.js b/js/src/hooks/useGoogleMCAccount.js index 22a4ecbc24..aabc752315 100644 --- a/js/src/hooks/useGoogleMCAccount.js +++ b/js/src/hooks/useGoogleMCAccount.js @@ -59,10 +59,12 @@ const useGoogleMCAccount = () => { googleMCAccountSelector ); - const hasGoogleMCConnection = [ - GOOGLE_MC_ACCOUNT_STATUS.CONNECTED, - GOOGLE_MC_ACCOUNT_STATUS.INCOMPLETE, - ].includes( acc?.status ); + const hasGoogleMCConnection = + Boolean( acc?.id ) && + [ + GOOGLE_MC_ACCOUNT_STATUS.CONNECTED, + GOOGLE_MC_ACCOUNT_STATUS.INCOMPLETE, + ].includes( acc?.status ); const isReady = acc?.status === GOOGLE_MC_ACCOUNT_STATUS.CONNECTED || diff --git a/tests/e2e/global-setup.js b/tests/e2e/global-setup.js index 26870e5aaa..6cf36e5a4a 100644 --- a/tests/e2e/global-setup.js +++ b/tests/e2e/global-setup.js @@ -55,7 +55,7 @@ module.exports = async ( config ) => { await adminPage .locator( 'input[name="pwd"]' ) .fill( admin.password ); - await adminPage.locator( 'text=Log In' ).click(); + await adminPage.getByRole( 'button', { name: 'Log In' } ).click(); await adminPage.waitForLoadState( LOAD_STATE.DOM_CONTENT_LOADED ); await adminPage.goto( `/wp-admin` ); await adminPage.waitForLoadState( LOAD_STATE.DOM_CONTENT_LOADED ); diff --git a/tests/e2e/specs/setup-mc/step-1-accounts.test.js b/tests/e2e/specs/setup-mc/step-1-accounts.test.js index a8bc132a12..0abda36ce5 100644 --- a/tests/e2e/specs/setup-mc/step-1-accounts.test.js +++ b/tests/e2e/specs/setup-mc/step-1-accounts.test.js @@ -233,9 +233,11 @@ test.describe( 'Set up accounts', () => { await once.mockAdsHasNoAccounts(); await once.mockMCHasNoAccounts(); await once.mockAdsAccountDisconnected(); + await once.mockAdsStatusDisconnected(); await once.mockMCNotConnected(); await setUpAccountsPage.goto(); + const googleAccountCard = setUpAccountsPage.getGoogleAccountCard(); await expect( @@ -248,6 +250,37 @@ test.describe( 'Set up accounts', () => { ).toBeVisible(); } ); + test( 'should show Ads claim and MC Reclaim after auto-creation, when appropriate', async () => { + await setUpAccountsPage.mockJetpackConnected(); + await setUpAccountsPage.mockGoogleConnected(); + await setUpAccountsPage.mockMCCreateAccountWebsiteClaimed(); + await setUpAccountsPage.mockAdsCreateAccount(); + await setUpAccountsPage.mockAdsAccountIncomplete( 'claim_account' ); + await setUpAccountsPage.mockAdsStatusNotClaimed(); + + const once = setUpAccountsPage.fulfillTimes( 1 ); + + await once.mockAdsHasNoAccounts(); + await once.mockMCHasNoAccounts(); + await once.mockAdsAccountDisconnected(); + await once.mockAdsStatusDisconnected(); + await once.mockMCNotConnected(); + + await setUpAccountsPage.goto(); + + const googleAccountCard = setUpAccountsPage.getGoogleAccountCard(); + const googleMCReclaimButton = + setUpAccountsPage.getReclaimMyURLButton(); + + await expect( + googleAccountCard.getByRole( 'button', { + name: 'Claim account in Google Ads', + } ) + ).toBeVisible(); + + await expect( googleMCReclaimButton ).toBeVisible(); + } ); + test.describe( 'After connecting Google account', () => { test.beforeEach( async () => { await setUpAccountsPage.mockJetpackConnected(); @@ -886,6 +919,7 @@ test.describe( 'Set up accounts', () => { test.beforeAll( async () => { await setUpAccountsPage.mockAdsAccountConnected(); await setUpAccountsPage.mockMCConnected(); + await setUpAccountsPage.mockAdsStatusClaimed(); await setUpAccountsPage.mockContactInformation( {} ); await setUpAccountsPage.goto(); @@ -921,6 +955,142 @@ test.describe( 'Set up accounts', () => { } ); } ); + test.describe( 'Edit button', () => { + test.beforeAll( async () => { + await setUpAccountsPage.mockJetpackConnected(); + await setUpAccountsPage.mockGoogleConnected(); + await setUpAccountsPage.mockMCConnected(); + await setUpAccountsPage.mockMCHasAccounts(); + await setUpAccountsPage.mockContactInformation(); + await setUpAccountsPage.mockAdsAccountConnected(); + await setUpAccountsPage.fulfillAdsAccounts( ADS_ACCOUNTS ); + await setUpAccountsPage.mockAdsStatusClaimed(); + await setUpAccountsPage.goto(); + } ); + + test( 'should display the Edit button and the Ads and MC account cards are not visible', async () => { + const editButton = setUpAccountsPage.getEditButton(); + await expect( editButton ).toBeVisible(); + + const googleMcAccountCard = setUpAccountsPage.getMCAccountCard(); + await expect( googleMcAccountCard ).not.toBeVisible(); + + const googleAdsAccountCard = + setUpAccountsPage.getGoogleAdsAccountCard(); + await expect( googleAdsAccountCard ).not.toBeVisible(); + } ); + + test.describe( 'clicking the Edit button', async () => { + test( 'the "Or, connect to a different Google account" button is visible', async () => { + const editButton = setUpAccountsPage.getEditButton(); + await editButton.click(); + + const connectDifferentGoogleAccountButton = + setUpAccountsPage.getConnectDifferentGoogleAccountButton(); + await expect( + connectDifferentGoogleAccountButton + ).toBeVisible(); + } ); + + test( 'the "Cancel" button is visible', async () => { + const cancelButton = setUpAccountsPage.getCancelButton(); + await expect( cancelButton ).toBeVisible(); + } ); + + test( 'MC and Ads account cards are visible', async () => { + const googleMcAccountCard = + setUpAccountsPage.getMCAccountCard(); + await expect( googleMcAccountCard ).toBeVisible(); + + const googleAdsAccountCard = + setUpAccountsPage.getGoogleAdsAccountCard(); + await expect( googleAdsAccountCard ).toBeVisible(); + } ); + } ); + + test.describe( 'clicking the Cancel button', async () => { + test( 'the "Edit" button is visible', async () => { + const cancelButton = setUpAccountsPage.getCancelButton(); + await cancelButton.click(); + + const editButton = setUpAccountsPage.getEditButton(); + await expect( editButton ).toBeVisible(); + } ); + + test( 'MC and Ads account cards are not visible', async () => { + const googleMcAccountCard = + setUpAccountsPage.getMCAccountCard(); + await expect( googleMcAccountCard ).not.toBeVisible(); + + const googleAdsAccountCard = + setUpAccountsPage.getGoogleAdsAccountCard(); + await expect( googleAdsAccountCard ).not.toBeVisible(); + } ); + } ); + + test.describe( 'clicking "Edit" when an Ads account is being claimed', async () => { + test( 'should let you connect to a different account', async () => { + await setUpAccountsPage.mockAdsStatusNotClaimed(); + await setUpAccountsPage.mockAdsAccountIncomplete( + 'claim_account' + ); + + await setUpAccountsPage.goto(); + + const editButton = setUpAccountsPage.getEditButton(); + await editButton.click(); + + const connectDifferentGoogleAdsAccountButton = + setUpAccountsPage.getConnectDifferentAdsAccountButton(); + + await expect( + connectDifferentGoogleAdsAccountButton + ).toBeVisible(); + } ); + + test( 'should disable the create new account button if there are no other existing accounts', async () => { + await setUpAccountsPage.mockAdsStatusNotClaimed(); + await setUpAccountsPage.mockAdsHasNoAccounts(); + await setUpAccountsPage.mockAdsAccountIncomplete( + 'claim_account' + ); + + await setUpAccountsPage.goto(); + + const editButton = setUpAccountsPage.getEditButton(); + await editButton.click(); + + const createNewAdsAccountButton = + setUpAccountsPage.getCreateNewAdsAccountButton(); + + await expect( createNewAdsAccountButton ).toBeDisabled(); + } ); + } ); + + test.describe( 'clicking "Edit" when there are no other existing accounts', async () => { + test( 'should let you create new accounts', async () => { + await setUpAccountsPage.mockAdsStatusClaimed(); + await setUpAccountsPage.mockAdsAccountConnected(); + await setUpAccountsPage.mockAdsHasNoAccounts(); + await setUpAccountsPage.mockMCHasNoAccounts(); + await setUpAccountsPage.mockMCConnected(); + + await setUpAccountsPage.goto(); + + const editButton = setUpAccountsPage.getEditButton(); + await editButton.click(); + + const createNewAdsAccountButton = + setUpAccountsPage.getCreateNewAdsAccountButton(); + const createNewMCAccountButton = + setUpAccountsPage.getCreateNewMCAccountButton(); + + await expect( createNewAdsAccountButton ).toBeVisible(); + await expect( createNewMCAccountButton ).toBeVisible(); + } ); + } ); + } ); + test.describe( 'Links', () => { test( 'should contain the correct URL for "Google Merchant Center Help" link', async () => { await setUpAccountsPage.goto(); diff --git a/tests/e2e/utils/mock-requests.js b/tests/e2e/utils/mock-requests.js index 29306ad059..a1f7d52869 100644 --- a/tests/e2e/utils/mock-requests.js +++ b/tests/e2e/utils/mock-requests.js @@ -241,10 +241,17 @@ export default class MockRequests { * Fulfill the Ads Account request. * * @param {Object} payload + * @param {number} [status=200] + * @param {string[]} [methods] * @return {Promise} */ - async fulfillAdsAccounts( payload ) { - await this.fulfillRequest( /\/wc\/gla\/ads\/accounts\b/, payload ); + async fulfillAdsAccounts( payload, status = 200, methods ) { + await this.fulfillRequest( + /\/wc\/gla\/ads\/accounts\b/, + payload, + status, + methods + ); } /** @@ -525,13 +532,13 @@ export default class MockRequests { * * @return {Promise} */ - async mockAdsAccountIncomplete() { + async mockAdsAccountIncomplete( step = 'billing' ) { await this.fulfillAdsConnection( { id: 12345, currency: 'TWD', symbol: 'NT$', status: 'incomplete', - step: 'billing', + step, } ); } @@ -550,6 +557,23 @@ export default class MockRequests { } ); } + /** + * Mock Ads create account. + * + * @return {Promise} + */ + async mockAdsCreateAccount() { + await this.fulfillAdsAccounts( + { + has_access: false, + invite_link: 'https://test.com', + step: 'claim_account', + }, + 200, + [ 'POST' ] + ); + } + /** * Mock the Ads accounts response. * diff --git a/tests/e2e/utils/pages/setup-mc/step-1-set-up-accounts.js b/tests/e2e/utils/pages/setup-mc/step-1-set-up-accounts.js index 4411e96f63..c11ea2caf3 100644 --- a/tests/e2e/utils/pages/setup-mc/step-1-set-up-accounts.js +++ b/tests/e2e/utils/pages/setup-mc/step-1-set-up-accounts.js @@ -299,6 +299,18 @@ export default class SetUpAccountsPage extends MockRequests { return this.getMCCardFooter().getByRole( 'button' ); } + /** + * Get create a new Merchant Center account button. + * + * @return {import('@playwright/test').Locator} Get create a new Merchant Center account button. + */ + getCreateNewMCAccountButton() { + return this.page.getByRole( 'button', { + name: 'Or, create a new Merchant Center account', + exact: true, + } ); + } + /** * Get "Continue" button. * @@ -383,6 +395,30 @@ export default class SetUpAccountsPage extends MockRequests { ); } + /** + * Get connect to a different Google Ads account button. + * + * @return {import('@playwright/test').Locator} Get connect to a different Google Ads account button. + */ + getConnectDifferentAdsAccountButton() { + return this.page.getByRole( 'button', { + name: 'Or, connect to a different Google Ads account', + exact: true, + } ); + } + + /** + * Get create a new Google Ads account button. + * + * @return {import('@playwright/test').Locator} Get create a new Google Ads account button. + */ + getCreateNewAdsAccountButton() { + return this.page.getByRole( 'button', { + name: 'Or, create a new Google Ads account', + exact: true, + } ); + } + /** * Get terms checkbox. * @@ -392,6 +428,18 @@ export default class SetUpAccountsPage extends MockRequests { return this.page.getByLabel( /I accept the terms and conditions/ ); } + /** + * Get "Edit" button. + * + * @return {import('@playwright/test').Locator} Get "Edit" button. + */ + getEditButton() { + return this.page.getByRole( 'button', { + name: 'Edit', + exact: true, + } ); + } + /** * Get store address card. * @@ -414,6 +462,30 @@ export default class SetUpAccountsPage extends MockRequests { } /** + * Get "Or, connect to a different Google account" button. + * + * @return {import('@playwright/test').Locator} Get "Or, connect to a different Google account" button. + */ + getConnectDifferentGoogleAccountButton() { + return this.page.getByRole( 'button', { + name: 'Or, connect to a different Google account', + exact: true, + } ); + } + + /** + * Get "Cancel" button. + * + * @return {import('@playwright/test').Locator} Get "Cancel" button. + */ + getCancelButton() { + return this.page.getByRole( 'button', { + name: 'Cancel', + exact: true, + } ); + } + + /* * Register the response when connecting an Ads account. * * @return {Promise} The response.