diff --git a/assets/js/components/DashboardMainApp.js b/assets/js/components/DashboardMainApp.js index 94c7519b775..896b7246abd 100644 --- a/assets/js/components/DashboardMainApp.js +++ b/assets/js/components/DashboardMainApp.js @@ -50,6 +50,7 @@ import SurveyViewTrigger from './surveys/SurveyViewTrigger'; import CurrentSurveyPortal from './surveys/CurrentSurveyPortal'; import MetricsSelectionPanel from './KeyMetrics/MetricsSelectionPanel'; import UserSettingsSelectionPanel from './email-reporting/UserSettingsSelectionPanel'; +import WelcomeModal from './WelcomeModal'; import { useFeature } from '@/js/hooks/useFeature'; import { ANCHOR_ID_CONTENT, @@ -229,6 +230,7 @@ export default function DashboardMainApp() { } ); const emailReportingEnabled = useFeature( 'proactiveUserEngagement' ); + const setupFlowRefreshEnabled = useFeature( 'setupFlowRefresh' ); useMonitorInternetConnection(); @@ -320,7 +322,6 @@ export default function DashboardMainApp() { lastWidgetAnchor === ANCHOR_ID_MONETIZATION, } ) } /> - } + { setupFlowRefreshEnabled && } + ); diff --git a/assets/js/components/WelcomeModal.test.tsx b/assets/js/components/WelcomeModal.test.tsx index 910f24737e5..67ca1a27b68 100644 --- a/assets/js/components/WelcomeModal.test.tsx +++ b/assets/js/components/WelcomeModal.test.tsx @@ -16,10 +16,20 @@ * limitations under the License. */ +/** + * External dependencies + */ +import fetchMock from 'fetch-mock'; + /** * Internal dependencies */ -import { render, createTestRegistry } from '../../../tests/js/test-utils'; +import { + createTestRegistry, + fireEvent, + render, + waitFor, +} from '../../../tests/js/test-utils'; import { provideModules, provideUserAuthentication, @@ -29,11 +39,20 @@ import { MODULE_SLUG_ANALYTICS_4 } from '@/js/modules/analytics-4/constants'; import { MODULE_SLUG_SEARCH_CONSOLE } from '@/js/modules/search-console/constants'; import { MODULES_ANALYTICS_4 } from '@/js/modules/analytics-4/datastore/constants'; import { MODULES_SEARCH_CONSOLE } from '@/js/modules/search-console/datastore/constants'; +import { CORE_USER } from '@/js/googlesitekit/datastore/user/constants'; +import { CORE_UI } from '@/js/googlesitekit/datastore/ui/constants'; import WelcomeModal from './WelcomeModal'; +const WITH_TOUR_DISMISSED_ITEM_SLUG = 'welcome-modal-with-tour'; +const GATHERING_DATA_DISMISSED_ITEM_SLUG = 'welcome-modal-gathering-data'; + describe( 'WelcomeModal', () => { let registry: ReturnType< typeof createTestRegistry >; + const dismissItemEndpoint = new RegExp( + '^/google-site-kit/v1/core/user/data/dismiss-item' + ); + beforeEach( () => { registry = createTestRegistry(); } ); @@ -64,6 +83,8 @@ describe( 'WelcomeModal', () => { .dispatch( MODULES_SEARCH_CONSOLE ) .receiveIsDataAvailableOnLoad( true ); + registry.dispatch( CORE_USER ).receiveGetDismissedItems( [] ); + const { container, getByText, getByRole, waitForRegistry } = render( , { @@ -112,6 +133,8 @@ describe( 'WelcomeModal', () => { .dispatch( MODULES_SEARCH_CONSOLE ) .receiveIsDataAvailableOnLoad( true ); + registry.dispatch( CORE_USER ).receiveGetDismissedItems( [] ); + const { container, getByText, getByRole, waitForRegistry } = render( , { @@ -138,6 +161,293 @@ describe( 'WelcomeModal', () => { ).toBeInTheDocument(); } ); + describe.each( [ 'Start tour', 'Maybe later', 'Close' ] )( + 'when the "%s" button is clicked for the dashboard tour variant', + ( buttonText ) => { + beforeEach( () => { + provideModules( registry, [ + { + slug: MODULE_SLUG_ANALYTICS_4, + active: true, + connected: true, + }, + { + slug: MODULE_SLUG_SEARCH_CONSOLE, + active: true, + connected: true, + }, + ] ); + + provideGatheringDataState( registry, { + [ MODULE_SLUG_ANALYTICS_4 ]: false, + [ MODULE_SLUG_SEARCH_CONSOLE ]: false, + } ); + + registry + .dispatch( MODULES_ANALYTICS_4 ) + .receiveIsDataAvailableOnLoad( true ); + registry + .dispatch( MODULES_SEARCH_CONSOLE ) + .receiveIsDataAvailableOnLoad( true ); + + registry.dispatch( CORE_USER ).receiveGetDismissedItems( [] ); + + // Model the responses for the two POST requests to `dismiss-item`. + fetchMock.postOnce( dismissItemEndpoint, { + body: [ WITH_TOUR_DISMISSED_ITEM_SLUG ], + status: 200, + } ); + + fetchMock.postOnce( dismissItemEndpoint, { + body: [ + WITH_TOUR_DISMISSED_ITEM_SLUG, + GATHERING_DATA_DISMISSED_ITEM_SLUG, + ], + status: 200, + } ); + } ); + + it( 'should close the modal', async () => { + const { container, getByRole, waitForRegistry } = render( + , + { + registry, + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- `render` is not typed yet. + ) as any; + + await waitForRegistry(); + + const closeButton = getByRole( 'button', { name: buttonText } ); + fireEvent.click( closeButton ); + + await waitFor( () => { + // Wait for the dismissal to complete. + expect( fetchMock ).toHaveFetchedTimes( 2 ); + } ); + + expect( container ).toBeEmptyDOMElement(); + } ); + + it( 'should dismiss the items for both the dashboard tour and gathering data variants', async () => { + const { getByRole, waitForRegistry } = render( + , + { + registry, + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- `render` is not typed yet. + ) as any; + + await waitForRegistry(); + + const closeButton = getByRole( 'button', { name: buttonText } ); + fireEvent.click( closeButton ); + + await waitFor( () => { + expect( fetchMock ).toHaveFetchedTimes( 2 ); + } ); + + expect( fetchMock ).toHaveFetched( dismissItemEndpoint, { + body: { + data: { + slug: WITH_TOUR_DISMISSED_ITEM_SLUG, + expiration: 0, + }, + }, + } ); + + expect( fetchMock ).toHaveFetched( dismissItemEndpoint, { + body: { + data: { + slug: GATHERING_DATA_DISMISSED_ITEM_SLUG, + expiration: 0, + }, + }, + } ); + } ); + } + ); + + it.each( [ 'Maybe later', 'Close' ] )( + 'should show a tooltip when the dashboard tour variant is closed by the "%s" button', + async ( buttonText ) => { + provideModules( registry, [ + { + slug: MODULE_SLUG_ANALYTICS_4, + active: true, + connected: true, + }, + { + slug: MODULE_SLUG_SEARCH_CONSOLE, + active: true, + connected: true, + }, + ] ); + + provideGatheringDataState( registry, { + [ MODULE_SLUG_ANALYTICS_4 ]: false, + [ MODULE_SLUG_SEARCH_CONSOLE ]: false, + } ); + + registry + .dispatch( MODULES_ANALYTICS_4 ) + .receiveIsDataAvailableOnLoad( true ); + registry + .dispatch( MODULES_SEARCH_CONSOLE ) + .receiveIsDataAvailableOnLoad( true ); + + registry.dispatch( CORE_USER ).receiveGetDismissedItems( [] ); + + // Model the responses for the two POST requests to `dismiss-item`. + fetchMock.postOnce( dismissItemEndpoint, { + body: [ WITH_TOUR_DISMISSED_ITEM_SLUG ], + status: 200, + } ); + + fetchMock.postOnce( dismissItemEndpoint, { + body: [ + WITH_TOUR_DISMISSED_ITEM_SLUG, + GATHERING_DATA_DISMISSED_ITEM_SLUG, + ], + status: 200, + } ); + + const { getByRole, waitForRegistry } = render( + , + { + registry, + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- `render` is not typed yet. + ) as any; + + await waitForRegistry(); + + const closeButton = getByRole( 'button', { name: buttonText } ); + fireEvent.click( closeButton ); + + await waitFor( () => { + expect( fetchMock ).toHaveFetchedTimes( 2 ); + } ); + + const tooltipState = registry + .select( CORE_UI ) + .getValue( 'admin-screen-tooltip' ); + + expect( tooltipState ).toMatchObject( { + isTooltipVisible: true, + tooltipSlug: 'dashboard-tour', + title: 'You can always take the dashboard tour from the help menu', + dismissLabel: 'Got it', + } ); + } + ); + + it( 'should not show a tooltip when the dashboard tour variant is closed by the "Start tour" button', async () => { + provideModules( registry, [ + { + slug: MODULE_SLUG_ANALYTICS_4, + active: true, + connected: true, + }, + { + slug: MODULE_SLUG_SEARCH_CONSOLE, + active: true, + connected: true, + }, + ] ); + + provideGatheringDataState( registry, { + [ MODULE_SLUG_ANALYTICS_4 ]: false, + [ MODULE_SLUG_SEARCH_CONSOLE ]: false, + } ); + + registry + .dispatch( MODULES_ANALYTICS_4 ) + .receiveIsDataAvailableOnLoad( true ); + registry + .dispatch( MODULES_SEARCH_CONSOLE ) + .receiveIsDataAvailableOnLoad( true ); + + registry.dispatch( CORE_USER ).receiveGetDismissedItems( [] ); + + // Model the responses for the two POST requests to `dismiss-item`. + fetchMock.postOnce( dismissItemEndpoint, { + body: [ WITH_TOUR_DISMISSED_ITEM_SLUG ], + status: 200, + } ); + + fetchMock.postOnce( dismissItemEndpoint, { + body: [ + WITH_TOUR_DISMISSED_ITEM_SLUG, + GATHERING_DATA_DISMISSED_ITEM_SLUG, + ], + status: 200, + } ); + + const { getByRole, waitForRegistry } = render( + , + { + registry, + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- `render` is not typed yet. + ) as any; + + await waitForRegistry(); + + const closeButton = getByRole( 'button', { name: 'Start tour' } ); + fireEvent.click( closeButton ); + + await waitFor( () => { + expect( fetchMock ).toHaveFetchedTimes( 2 ); + } ); + + const tooltipState = registry + .select( CORE_UI ) + .getValue( 'admin-screen-tooltip' ); + + expect( tooltipState ).toBeUndefined(); + } ); + + it( 'should not show the dashboard tour variant when it has been dismissed', async () => { + provideModules( registry, [ + { + slug: MODULE_SLUG_ANALYTICS_4, + active: true, + connected: true, + }, + { + slug: MODULE_SLUG_SEARCH_CONSOLE, + active: true, + connected: true, + }, + ] ); + + provideGatheringDataState( registry, { + [ MODULE_SLUG_ANALYTICS_4 ]: false, + [ MODULE_SLUG_SEARCH_CONSOLE ]: false, + } ); + + registry + .dispatch( MODULES_ANALYTICS_4 ) + .receiveIsDataAvailableOnLoad( true ); + registry + .dispatch( MODULES_SEARCH_CONSOLE ) + .receiveIsDataAvailableOnLoad( true ); + + registry + .dispatch( CORE_USER ) + .receiveGetDismissedItems( [ WITH_TOUR_DISMISSED_ITEM_SLUG ] ); + + const { container, waitForRegistry } = render( , { + registry, + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- `render` is not typed yet. + } ) as any; + + await waitForRegistry(); + + expect( container ).toBeEmptyDOMElement(); + } ); + it( 'should show the gathering data variant when Analytics is connected and gathering data', async () => { provideModules( registry, [ { @@ -175,6 +485,8 @@ describe( 'WelcomeModal', () => { } ); + registry.dispatch( CORE_USER ).receiveGetDismissedItems( [] ); + const { container, getByText, getByRole, waitForRegistry } = render( , { @@ -216,6 +528,8 @@ describe( 'WelcomeModal', () => { [ MODULE_SLUG_SEARCH_CONSOLE ]: true, } ); + registry.dispatch( CORE_USER ).receiveGetDismissedItems( [] ); + const { container, getByText, getByRole, waitForRegistry } = render( , { @@ -238,4 +552,145 @@ describe( 'WelcomeModal', () => { getByRole( 'button', { name: 'Get started' } ) ).toBeInTheDocument(); } ); + + describe.each( [ 'Get started', 'Close' ] )( + 'when the "%s" button is clicked for the gathering data variant', + ( buttonText ) => { + beforeEach( () => { + provideModules( registry, [ + { + slug: MODULE_SLUG_ANALYTICS_4, + active: false, + connected: false, + }, + { + slug: MODULE_SLUG_SEARCH_CONSOLE, + active: true, + connected: true, + }, + ] ); + + provideGatheringDataState( registry, { + [ MODULE_SLUG_SEARCH_CONSOLE ]: true, + } ); + + registry.dispatch( CORE_USER ).receiveGetDismissedItems( [] ); + + fetchMock.postOnce( dismissItemEndpoint, { + body: [ GATHERING_DATA_DISMISSED_ITEM_SLUG ], + status: 200, + } ); + } ); + + // eslint-disable-next-line jest/no-identical-title -- The nested describe block distinguishes the test titles. + it( 'should close the modal', async () => { + const { container, getByRole, waitForRegistry } = render( + , + { + registry, + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- `render` is not typed yet. + ) as any; + + await waitForRegistry(); + + const closeButton = getByRole( 'button', { + name: buttonText, + } ); + fireEvent.click( closeButton ); + + await waitFor( () => { + // Wait for the dismissal to complete. + expect( fetchMock ).toHaveFetchedTimes( 1 ); + } ); + + expect( container ).toBeEmptyDOMElement(); + } ); + + it( 'should dismiss the item for the gathering data variant', async () => { + const { getByRole, waitForRegistry } = render( + , + { + registry, + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- `render` is not typed yet. + ) as any; + + await waitForRegistry(); + + const closeButton = getByRole( 'button', { name: buttonText } ); + fireEvent.click( closeButton ); + + await waitFor( () => { + expect( fetchMock ).toHaveFetchedTimes( 1 ); + } ); + + expect( fetchMock ).toHaveFetched( dismissItemEndpoint, { + body: { + data: { + slug: GATHERING_DATA_DISMISSED_ITEM_SLUG, + expiration: 0, + }, + }, + } ); + } ); + + it( 'should not show a tooltip', async () => { + const { getByRole, waitForRegistry } = render( + , + { + registry, + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- `render` is not typed yet. + ) as any; + + await waitForRegistry(); + + const closeButton = getByRole( 'button', { name: buttonText } ); + fireEvent.click( closeButton ); + + await waitFor( () => { + expect( fetchMock ).toHaveFetchedTimes( 1 ); + } ); + + const tooltipState = registry + .select( CORE_UI ) + .getValue( 'admin-screen-tooltip' ); + + expect( tooltipState ).toBeUndefined(); + } ); + } + ); + + it( 'should not show the gathering data variant when it has been dismissed', async () => { + provideModules( registry, [ + { + slug: MODULE_SLUG_ANALYTICS_4, + active: false, + connected: false, + }, + { + slug: MODULE_SLUG_SEARCH_CONSOLE, + active: true, + connected: true, + }, + ] ); + + provideGatheringDataState( registry, { + [ MODULE_SLUG_SEARCH_CONSOLE ]: true, + } ); + + registry + .dispatch( CORE_USER ) + .receiveGetDismissedItems( [ GATHERING_DATA_DISMISSED_ITEM_SLUG ] ); + + const { container, waitForRegistry } = render( , { + registry, + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- `render` is not typed yet. + } ) as any; + + await waitForRegistry(); + + expect( container ).toBeEmptyDOMElement(); + } ); } ); diff --git a/assets/js/components/WelcomeModal.tsx b/assets/js/components/WelcomeModal.tsx index 37cddd89554..b0abc02e317 100644 --- a/assets/js/components/WelcomeModal.tsx +++ b/assets/js/components/WelcomeModal.tsx @@ -19,14 +19,20 @@ /** * WordPress dependencies */ -import { createInterpolateElement, Fragment } from '@wordpress/element'; +import { + createInterpolateElement, + useCallback, + useState, + Fragment, +} from '@wordpress/element'; import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import { useSelect } from 'googlesitekit-data'; +import { useSelect, useDispatch } from 'googlesitekit-data'; import { CORE_MODULES } from '@/js/googlesitekit/modules/datastore/constants'; +import { CORE_USER } from '@/js/googlesitekit/datastore/user/constants'; import { MODULES_ANALYTICS_4 } from '@/js/modules/analytics-4/datastore/constants'; import { MODULES_SEARCH_CONSOLE } from '@/js/modules/search-console/datastore/constants'; import { MODULE_SLUG_ANALYTICS_4 } from '@/js/modules/analytics-4/constants'; @@ -34,15 +40,21 @@ import { Button } from 'googlesitekit-components'; import { Dialog, DialogContent, DialogFooter } from '@/js/material-components'; import P from '@/js/components/Typography/P'; import Typography from '@/js/components/Typography'; +import { useShowTooltip } from '@/js/components/AdminScreenTooltip'; // @ts-expect-error - We need to add types for imported SVGs. import CloseIcon from '@/svg/icons/close.svg'; // @ts-expect-error - We need to add types for imported SVGs. import WelcomeModalGraphic from '@/svg/graphics/welcome-modal-graphic.svg'; +const WITH_TOUR_DISMISSED_ITEM_SLUG = 'welcome-modal-with-tour'; +const GATHERING_DATA_DISMISSED_ITEM_SLUG = 'welcome-modal-gathering-data'; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- `@wordpress/data` is not typed yet. type SelectFunction = ( select: any ) => any; export default function WelcomeModal() { + const [ isOpen, setIsOpen ] = useState( true ); + const analyticsConnected = useSelect( ( select: SelectFunction ) => select( CORE_MODULES ).isModuleConnected( MODULE_SLUG_ANALYTICS_4 ) ); @@ -62,11 +74,57 @@ export default function WelcomeModal() { ? analyticsGatheringData : searchConsoleGatheringData; + const dismissedItemSlug = showGatheringDataModal + ? GATHERING_DATA_DISMISSED_ITEM_SLUG + : WITH_TOUR_DISMISSED_ITEM_SLUG; + + const isItemDismissed = useSelect( ( select: SelectFunction ) => + select( CORE_USER ).isItemDismissed( dismissedItemSlug ) + ); + + const { dismissItem } = useDispatch( CORE_USER ); + + const tooltipSettings = { + target: '.googlesitekit-help-menu__button', + tooltipSlug: 'dashboard-tour', + title: __( + 'You can always take the dashboard tour from the help menu', + 'google-site-kit' + ), + dismissLabel: __( 'Got it', 'google-site-kit' ), + }; + + const showTooltip = useShowTooltip( tooltipSettings ); + + const closeAndDismissModal = useCallback( async () => { + setIsOpen( false ); + + await dismissItem( dismissedItemSlug ); + + if ( ! showGatheringDataModal ) { + // If we're in the dashboard tour variant, also dismiss the gathering data variant. + await dismissItem( GATHERING_DATA_DISMISSED_ITEM_SLUG ); + } + }, [ dismissItem, dismissedItemSlug, showGatheringDataModal, setIsOpen ] ); + + const closeAndDismissModalWithTooltip = useCallback( async () => { + await closeAndDismissModal(); + + // Don't show the tooltip for the gathering data variant. + if ( ! showGatheringDataModal ) { + showTooltip(); + } + }, [ closeAndDismissModal, showGatheringDataModal, showTooltip ] ); + if ( showGatheringDataModal === undefined ) { // TODO: Implement a loading state when we have a design for it in phase 3 of the Setup Flow Refresh epic. return null; } + if ( isItemDismissed || ! isOpen ) { + return null; + } + const description = showGatheringDataModal ? createInterpolateElement( __( @@ -84,8 +142,8 @@ export default function WelcomeModal() { return ( {} } className="googlesitekit-dialog googlesitekit-welcome-modal" + onClose={ closeAndDismissModalWithTooltip } open > @@ -96,7 +154,7 @@ export default function WelcomeModal() { // @ts-expect-error - The `Button` component is not typed yet. className="googlesitekit-welcome-modal__close-button" icon={ } - onClick={ () => {} } + onClick={ closeAndDismissModalWithTooltip } aria-label={ __( 'Close', 'google-site-kit' ) } hideTooltipTitle /> @@ -125,17 +183,20 @@ export default function WelcomeModal() { { showGatheringDataModal ? ( // @ts-expect-error - The `Button` component is not typed yet. - ) : ( { /* @ts-expect-error - The `Button` component is not typed yet. */ } - { /* @ts-expect-error - The `Button` component is not typed yet. */ } - diff --git a/assets/js/components/key-metrics-setup/KeyMetricsSetupApp.js b/assets/js/components/key-metrics-setup/KeyMetricsSetupApp.js index 19d40160740..c7f9d90ee72 100644 --- a/assets/js/components/key-metrics-setup/KeyMetricsSetupApp.js +++ b/assets/js/components/key-metrics-setup/KeyMetricsSetupApp.js @@ -45,6 +45,7 @@ import { CORE_MODULES } from '@/js/googlesitekit/modules/datastore/constants'; import { MODULE_SLUG_ANALYTICS_4 } from '@/js/modules/analytics-4/constants'; import { Grid, Row, Cell } from '@/js/material-components'; import { MODULES_ANALYTICS_4 } from '@/js/modules/analytics-4/datastore/constants'; +import { MODULES_SEARCH_CONSOLE } from '@/js/modules/search-console/datastore/constants'; import ExitSetup from '@/js/components/setup/ExitSetup'; import Header from '@/js/components/Header'; import HelpMenu from '@/js/components/help/HelpMenu'; @@ -105,6 +106,12 @@ export default function KeyMetricsSetupApp() { const { saveUserInputSettings, saveInitialSetupSettings } = useDispatch( CORE_USER ); + // Trigger resolution of data availability state before the user proceeds to the dashboard. + useSelect( ( select ) => { + select( MODULES_ANALYTICS_4 ).isGatheringData(); + select( MODULES_SEARCH_CONSOLE ).isGatheringData(); + } ); + const isSyncing = useSelect( ( select ) => { const isFetchingSyncAvailableCustomDimensions = select( @@ -114,7 +121,21 @@ export default function KeyMetricsSetupApp() { const isSyncingAudiences = select( MODULES_ANALYTICS_4 ).isSyncingAudiences(); - return isFetchingSyncAvailableCustomDimensions || isSyncingAudiences; + const hasResolvedAnalytics4DataAvailability = + select( MODULES_ANALYTICS_4 ).hasFinishedResolution( + 'isGatheringData' + ); + + const hasResolvedSearchConsoleDataAvailability = select( + MODULES_SEARCH_CONSOLE + ).hasFinishedResolution( 'isGatheringData' ); + + return ( + isFetchingSyncAvailableCustomDimensions || + isSyncingAudiences || + ! hasResolvedAnalytics4DataAvailability || + ! hasResolvedSearchConsoleDataAvailability + ); } ); const { navigateTo } = useDispatch( CORE_LOCATION ); diff --git a/assets/js/components/key-metrics-setup/KeyMetricsSetupApp.test.js b/assets/js/components/key-metrics-setup/KeyMetricsSetupApp.test.js index 4d875c9a77a..3b71dae566a 100644 --- a/assets/js/components/key-metrics-setup/KeyMetricsSetupApp.test.js +++ b/assets/js/components/key-metrics-setup/KeyMetricsSetupApp.test.js @@ -25,6 +25,7 @@ import { freezeFetch, waitForTimeouts, waitFor, + muteFetch, } from '../../../../tests/js/test-utils'; import { mockLocation } from '../../../../tests/js/mock-browser-utils'; import { CORE_USER } from '@/js/googlesitekit/datastore/user/constants'; @@ -33,8 +34,11 @@ import KeyMetricsSetupApp from './KeyMetricsSetupApp'; import { MODULES_ANALYTICS_4 } from '@/js/modules/analytics-4/datastore/constants'; import { CORE_MODULES } from '@/js/googlesitekit/modules/datastore/constants'; import { MODULE_SLUG_ANALYTICS_4 } from '@/js/modules/analytics-4/constants'; +import { MODULES_SEARCH_CONSOLE } from '@/js/modules/search-console/datastore/constants'; import { withConnected } from '@/js/googlesitekit/modules/datastore/__fixtures__'; import * as tracking from '@/js/util/tracking'; +import * as analyticsFixtures from '@/js/modules/analytics-4/datastore/__fixtures__'; +import * as searchConsoleFixtures from '@/js/modules/search-console/datastore/__fixtures__'; const mockTrackEvent = jest.spyOn( tracking, 'trackEvent' ); mockTrackEvent.mockImplementation( () => Promise.resolve() ); @@ -59,6 +63,20 @@ describe( 'KeyMetricsSetupApp', () => { '^/google-site-kit/v1/core/user/data/initial-setup-settings' ); + const searchAnalyticsRegexp = new RegExp( + '^/google-site-kit/v1/modules/search-console/data/searchanalytics' + ); + const analytics4ReportRegexp = new RegExp( + '^/google-site-kit/v1/modules/analytics-4/data/report' + ); + + const searchConsoleDataAvailableRegexp = new RegExp( + '^/google-site-kit/v1/modules/search-console/data/data-available' + ); + const analytics4DataAvailableRegexp = new RegExp( + '^/google-site-kit/v1/modules/analytics-4/data/data-available' + ); + // The `UserInputSelectOptions` automatically focuses the first radio/checkbox // 50 milliseconds after it renders, which causes inconsistencies in snapshots, // so we advance the timer to make sure it's focused before we capture the snapshot. @@ -82,6 +100,15 @@ describe( 'KeyMetricsSetupApp', () => { status: 200, } ); + fetchMock.getOnce( searchAnalyticsRegexp, { + body: searchConsoleFixtures.report, + } ); + fetchMock.getOnce( analytics4ReportRegexp, { + body: analyticsFixtures.report, + } ); + muteFetch( searchConsoleDataAvailableRegexp ); + muteFetch( analytics4DataAvailableRegexp ); + registry.dispatch( CORE_USER ).receiveGetUserAudienceSettings( { configuredAudiences: null, isAudienceSegmentationWidgetHidden: false, @@ -347,6 +374,38 @@ describe( 'KeyMetricsSetupApp', () => { ); } ); + it( 'should resolve data availability for both Analytics and Search Console on render', async () => { + expect( + registry + .select( MODULES_ANALYTICS_4 ) + .hasStartedResolution( 'isGatheringData', [] ) + ).toBe( false ); + + expect( + registry + .select( MODULES_SEARCH_CONSOLE ) + .hasStartedResolution( 'isGatheringData', [] ) + ).toBe( false ); + + const { waitForRegistry } = render( , { + registry, + viewContext: VIEW_CONTEXT_KEY_METRICS_SETUP, + } ); + + await waitForRegistry(); + + expect( + registry + .select( MODULES_ANALYTICS_4 ) + .hasFinishedResolution( 'isGatheringData', [] ) + ).toBe( true ); + expect( + registry + .select( MODULES_SEARCH_CONSOLE ) + .hasFinishedResolution( 'isGatheringData', [] ) + ).toBe( true ); + } ); + it( 'should display the help menu in the header', async () => { const { container, waitForRegistry } = render( , { registry,