diff --git a/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceSelectionPanel/ErrorNotice.test.js b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceSelectionPanel/ErrorNotice.test.js index c8625c96bcc..33bc572aa5f 100644 --- a/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceSelectionPanel/ErrorNotice.test.js +++ b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceSelectionPanel/ErrorNotice.test.js @@ -42,6 +42,9 @@ describe( 'ErrorNotice', () => { const syncAvailableAudiencesEndpoint = new RegExp( '^/google-site-kit/v1/modules/analytics-4/data/sync-audiences' ); + const audienceSettingsEndpoint = new RegExp( + '^/google-site-kit/v1/modules/analytics-4/data/audience-settings' + ); const reportOptions = { endDate: '2024-03-27', @@ -265,6 +268,14 @@ describe( 'ErrorNotice', () => { status: 200, } ); + fetchMock.getOnce( audienceSettingsEndpoint, { + body: { + data: { + configuredAudiences: [], + }, + }, + } ); + expect( registry .select( MODULES_ANALYTICS_4 ) diff --git a/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/AudienceTile/AudienceTileLoading.js b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/AudienceTile/AudienceTileLoading.js new file mode 100644 index 00000000000..298d51957f5 --- /dev/null +++ b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/AudienceTile/AudienceTileLoading.js @@ -0,0 +1,37 @@ +/** + * AudienceTileLoading component. + * + * Site Kit by Google, Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Internal dependencies + */ +import PreviewBlock from '../../../../../../../components/PreviewBlock'; + +export default function AudienceTileLoading() { + return ( +
+ { /* The first preview block is only visible on desktop to preview the header which is hidden on other screens. */ } + + + + + + + +
+ ); +} diff --git a/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/AudienceTile/index.js b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/AudienceTile/index.js index d3ca2243fe0..f25ef05f511 100644 --- a/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/AudienceTile/index.js +++ b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/AudienceTile/index.js @@ -46,7 +46,6 @@ import AudienceMetricIconTopContent from '../../../../../../../../svg/icons/audi import AudienceTileMetric from './AudienceTileMetric'; import AudienceTileCitiesMetric from './AudienceTileCitiesMetric'; import AudienceTilePagesMetric from './AudienceTilePagesMetric'; -import PreviewBlock from '../../../../../../../components/PreviewBlock'; import ChangeBadge from '../../../../../../../components/ChangeBadge'; import InfoTooltip from '../../../../../../../components/InfoTooltip'; import PartialDataBadge from './PartialDataBadge'; @@ -54,6 +53,7 @@ import PartialDataNotice from './PartialDataNotice'; import { numFmt } from '../../../../../../../util'; import AudienceTileCollectingData from './AudienceTileCollectingData'; import AudienceTileCollectingDataHideable from './AudienceTileCollectingDataHideable'; +import AudienceTileLoading from './AudienceTileLoading'; // TODO: as part of #8484 the report props should be updated to expect // the full report rows for the current tile to reduce data manipulation @@ -72,6 +72,7 @@ export default function AudienceTile( { topContentTitles, Widget, audienceResourceName, + isLoading, isZeroData, isPartialData, isTileHideable, @@ -107,9 +108,17 @@ export default function AudienceTile( { breakpoint ); - // TODO: Loading states will be implemented as part of https://github.com/google/site-kit-wp/issues/8145. - if ( ! loaded || isZeroData === undefined || isPartialData === undefined ) { - return ; + if ( + ! loaded || + isLoading || + isZeroData === undefined || + isPartialData === undefined + ) { + return ( + + + + ); } if ( isPartialData && isZeroData ) { @@ -283,6 +292,7 @@ AudienceTile.propTypes = { topContentTitles: PropTypes.object, Widget: PropTypes.elementType.isRequired, audienceResourceName: PropTypes.string.isRequired, + isLoading: PropTypes.bool, isZeroData: PropTypes.bool, isPartialData: PropTypes.bool, isTileHideable: PropTypes.bool, diff --git a/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/AudienceTile/index.stories.js b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/AudienceTile/index.stories.js index e9b67fec88c..95f18b9d9f1 100644 --- a/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/AudienceTile/index.stories.js +++ b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/AudienceTile/index.stories.js @@ -199,6 +199,26 @@ ReadyLongCityNames.scenario = { label: 'Modules/Analytics4/Components/AudienceSegmentation/Dashboard/AudienceTile/ReadyLongCityNames', }; +export const Loading = Template.bind( {} ); +Loading.storyName = 'Loading'; +Loading.args = { + ...readyProps, + isLoading: true, +}; +Loading.decorators = [ + ( Story ) => { + // Ensure the animation is paused for VRT tests to correctly capture the loading state. + return ( +
+ +
+ ); + }, +]; +Loading.scenario = { + label: 'Modules/Analytics4/Components/AudienceSegmentation/Dashboard/AudienceTile/Loading', +}; + export const NoData = Template.bind( {} ); NoData.storyName = 'NoData'; NoData.args = { diff --git a/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/AudienceTiles.js b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/AudienceTiles.js index e1340c2c523..f372b60b4ee 100644 --- a/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/AudienceTiles.js +++ b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/AudienceTiles.js @@ -59,7 +59,7 @@ const hasZeroDataForAudience = ( report, audienceResourceName ) => { return totalUsers === 0; }; -export default function AudienceTiles( { Widget } ) { +export default function AudienceTiles( { Widget, widgetLoading } ) { const [ activeTile, setActiveTile ] = useState( 0 ); const breakpoint = useBreakpoint(); const isTabbedBreakpoint = @@ -248,7 +248,7 @@ export default function AudienceTiles( { Widget } ) { const visible = []; const tempAudiences = configuredAudiences.slice(); - while ( reportLoaded && tempAudiences.length > 0 ) { + while ( tempAudiences.length > 0 ) { const audienceResourceName = tempAudiences.shift(); const isDismissed = dismissedItems?.includes( @@ -278,7 +278,7 @@ export default function AudienceTiles( { Widget } ) { } return [ toClear, visible ]; - }, [ configuredAudiences, dismissedItems, reportLoaded, report ] ); + }, [ configuredAudiences, dismissedItems, report ] ); // Re-dismiss with a short expiry time to clear any previously dismissed tiles. // This ensures that the tile will reappear when it is populated with data again. @@ -300,6 +300,7 @@ export default function AudienceTiles( { Widget } ) { }, [ audiencesToClearDismissal, dismissItem, isDismissingItem ] ); const loading = + widgetLoading || ! reportLoaded || ! totalPageviewsReportLoaded || ! topCitiesReportLoaded || @@ -515,4 +516,5 @@ export default function AudienceTiles( { Widget } ) { AudienceTiles.propTypes = { Widget: PropTypes.elementType.isRequired, + widgetLoading: PropTypes.bool.isRequired, }; diff --git a/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/__snapshots__/index.test.js.snap b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..cd7038ec757 --- /dev/null +++ b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/__snapshots__/index.test.js.snap @@ -0,0 +1,370 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AudienceTilesWidget should render when all configured audiences are matching available audiences 1`] = ` +
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+ +

+ Site Kit is collecting data for this group. +

+

+ You can hide this group until data is available. +

+ +
+
+
+
+
+
+
+
+
+`; + +exports[`AudienceTilesWidget should render when configured audience is matching available audiences 1`] = ` +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+ +

+ Site Kit is collecting data for this group. +

+
+
+
+
+
+
+
+
+
+`; + +exports[`AudienceTilesWidget should render when some configured audiences are matching available audiences 1`] = ` +
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+ +

+ Site Kit is collecting data for this group. +

+

+ You can hide this group until data is available. +

+ +
+
+
+
+
+
+
+
+
+`; diff --git a/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/index.js b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/index.js index 16e7db33323..0c81e558fcf 100644 --- a/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/index.js +++ b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/index.js @@ -20,14 +20,16 @@ * External dependencies */ import PropTypes from 'prop-types'; +import { useEffect, useState } from '@wordpress/element'; /** * Internal dependencies */ -import { useSelect } from 'googlesitekit-data'; +import { useDispatch, useSelect } from 'googlesitekit-data'; import whenActive from '../../../../../../util/when-active'; import { MODULES_ANALYTICS_4 } from '../../../../datastore/constants'; import AudienceTiles from './AudienceTiles'; +import { useInView } from '../../../../../../hooks/useInView'; function AudienceTilesWidget( { Widget, WidgetNull } ) { const availableAudiences = useSelect( ( select ) => { @@ -38,15 +40,36 @@ function AudienceTilesWidget( { Widget, WidgetNull } ) { select( MODULES_ANALYTICS_4 ).getConfiguredAudiences() ); + const [ availableAudiencesSynced, setAvailableAudiencesSynced ] = + useState( false ); + const { maybeSyncAvailableAudiences } = useDispatch( MODULES_ANALYTICS_4 ); + + const inView = useInView(); + useEffect( () => { + if ( inView && ! availableAudiencesSynced ) { + maybeSyncAvailableAudiences(); + setAvailableAudiencesSynced( true ); + } + }, [ inView, availableAudiencesSynced, maybeSyncAvailableAudiences ] ); + const hasMatchingAudience = configuredAudiences?.some( ( audience ) => availableAudiences?.includes( audience ) ); - if ( hasMatchingAudience ) { - return ; + if ( ! hasMatchingAudience ) { + return ; } - return ; + return ( + + ); } AudienceTilesWidget.propTypes = { diff --git a/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/index.test.js b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/index.test.js index fc66fd75549..491b09c2396 100644 --- a/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/index.test.js +++ b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/AudienceTilesWidget/index.test.js @@ -61,44 +61,59 @@ describe( 'AudienceTilesWidget', () => { jest.clearAllMocks(); } ); - it( 'should not render when availableAudiences and configuredAudiences are not loaded', () => { + it( 'should not render when availableAudiences and configuredAudiences are not loaded', async () => { muteFetch( audienceSettingsRegExp ); - const { container } = render( , { - registry, - } ); + const { container, waitForRegistry } = render( + , + { + registry, + } + ); + + await waitForRegistry(); expect( container ).toBeEmptyDOMElement(); } ); - it( 'should not render when availableAudiences is not loaded', () => { + it( 'should not render when availableAudiences is not loaded', async () => { registry.dispatch( MODULES_ANALYTICS_4 ).receiveGetAudienceSettings( { configuredAudiences: [ 'properties/12345/audiences/1' ], isAudienceSegmentationWidgetHidden: false, } ); - const { container } = render( , { - registry, - } ); + const { container, waitForRegistry } = render( + , + { + registry, + } + ); + + await waitForRegistry(); expect( container ).toBeEmptyDOMElement(); } ); - it( 'should not render when configuredAudiences is not loaded', () => { + it( 'should not render when configuredAudiences is not loaded', async () => { muteFetch( audienceSettingsRegExp ); registry .dispatch( MODULES_ANALYTICS_4 ) .setAvailableAudiences( availableAudiences ); - const { container } = render( , { - registry, - } ); + const { container, waitForRegistry } = render( + , + { + registry, + } + ); + + await waitForRegistry(); expect( container ).toBeEmptyDOMElement(); } ); - it( 'should not render when there is no available audience', () => { + it( 'should not render when there is no available audience', async () => { registry.dispatch( MODULES_ANALYTICS_4 ).setAvailableAudiences( [] ); registry.dispatch( MODULES_ANALYTICS_4 ).receiveGetAudienceSettings( { @@ -106,14 +121,19 @@ describe( 'AudienceTilesWidget', () => { isAudienceSegmentationWidgetHidden: false, } ); - const { container } = render( , { - registry, - } ); + const { container, waitForRegistry } = render( + , + { + registry, + } + ); + + await waitForRegistry(); expect( container ).toBeEmptyDOMElement(); } ); - it( 'should not render when there is no configured audience', () => { + it( 'should not render when there is no configured audience', async () => { registry .dispatch( MODULES_ANALYTICS_4 ) .setAvailableAudiences( availableAudiences ); @@ -123,14 +143,19 @@ describe( 'AudienceTilesWidget', () => { isAudienceSegmentationWidgetHidden: false, } ); - const { container } = render( , { - registry, - } ); + const { container, waitForRegistry } = render( + , + { + registry, + } + ); + + await waitForRegistry(); expect( container ).toBeEmptyDOMElement(); } ); - it( 'should not render when configuredAudiences is null (not set)', () => { + it( 'should not render when configuredAudiences is null (not set)', async () => { registry .dispatch( MODULES_ANALYTICS_4 ) .setAvailableAudiences( availableAudiences ); @@ -140,14 +165,19 @@ describe( 'AudienceTilesWidget', () => { isAudienceSegmentationWidgetHidden: false, } ); - const { container } = render( , { - registry, - } ); + const { container, waitForRegistry } = render( + , + { + registry, + } + ); + + await waitForRegistry(); expect( container ).toBeEmptyDOMElement(); } ); - it( 'should not render when there is no matching audience', () => { + it( 'should not render when there is no matching audience', async () => { registry .dispatch( MODULES_ANALYTICS_4 ) .setAvailableAudiences( availableAudiences ); @@ -157,14 +187,23 @@ describe( 'AudienceTilesWidget', () => { isAudienceSegmentationWidgetHidden: false, } ); - const { container } = render( , { - registry, - } ); + const { container, waitForRegistry } = render( + , + { + registry, + } + ); + + await waitForRegistry(); expect( container ).toBeEmptyDOMElement(); } ); it( 'should render when configured audience is matching available audiences', async () => { + registry.dispatch( MODULES_ANALYTICS_4 ).receiveGetSettings( { + availableAudiencesLastSyncedAt: ( Date.now() - 1000 ) / 1000, + } ); + registry .dispatch( MODULES_ANALYTICS_4 ) .setAvailableAudiences( availableAudiences ); @@ -183,10 +222,14 @@ describe( 'AudienceTilesWidget', () => { await waitForRegistry(); - expect( container ).not.toBeEmptyDOMElement(); + expect( container ).toMatchSnapshot(); } ); it( 'should render when all configured audiences are matching available audiences', async () => { + registry.dispatch( MODULES_ANALYTICS_4 ).receiveGetSettings( { + availableAudiencesLastSyncedAt: ( Date.now() - 1000 ) / 1000, + } ); + registry .dispatch( MODULES_ANALYTICS_4 ) .setAvailableAudiences( availableAudiences ); @@ -208,10 +251,14 @@ describe( 'AudienceTilesWidget', () => { await waitForRegistry(); - expect( container ).not.toBeEmptyDOMElement(); + expect( container ).toMatchSnapshot(); } ); it( 'should render when some configured audiences are matching available audiences', async () => { + registry.dispatch( MODULES_ANALYTICS_4 ).receiveGetSettings( { + availableAudiencesLastSyncedAt: ( Date.now() - 1000 ) / 1000, + } ); + registry .dispatch( MODULES_ANALYTICS_4 ) .setAvailableAudiences( availableAudiences ); @@ -233,6 +280,6 @@ describe( 'AudienceTilesWidget', () => { await waitForRegistry(); - expect( container ).not.toBeEmptyDOMElement(); + expect( container ).toMatchSnapshot(); } ); } ); diff --git a/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/NoAudienceBannerWidget/__snapshots__/index.test.js.snap b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/NoAudienceBannerWidget/__snapshots__/index.test.js.snap index b3bfa719f8b..8718913bdd6 100644 --- a/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/NoAudienceBannerWidget/__snapshots__/index.test.js.snap +++ b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/NoAudienceBannerWidget/__snapshots__/index.test.js.snap @@ -1,5 +1,66 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`NoAudienceBannerWidget should render when there is no configured audience. 1`] = ` +
+
+
+
+
+
+ +
+
+

+ It looks like your visitor groups aren’t available anymore. + + . +

+

+ You can deactivate this widget in + + . +

+
+
+
+ +
+
+
+
+
+`; + exports[`NoAudienceBannerWidget should render with correct message when there are additional configurable audiences available 1`] = `
- Array.isArray( availableAudiences ) && - ! availableAudiences.includes( audience ) - ); - - const configurableAudiences = availableAudiences?.filter( - ( element ) => - Array.isArray( configuredAudiences ) && - ! configuredAudiences.includes( element ) + const hasNoMatchingAudience = configuredAudiences?.every( + ( audience ) => + Array.isArray( availableAudiences ) && + ! availableAudiences.includes( audience ) ); - - if ( hasNoMatchingAudience ) { + if ( + !! configuredAudiences && + ( configuredAudiences?.length === 0 || hasNoMatchingAudience ) + ) { return ( ); diff --git a/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/NoAudienceBannerWidget/index.test.js b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/NoAudienceBannerWidget/index.test.js index 77b1331d79f..69ed8ad3a26 100644 --- a/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/NoAudienceBannerWidget/index.test.js +++ b/assets/js/modules/analytics-4/components/audience-segmentation/dashboard/NoAudienceBannerWidget/index.test.js @@ -61,29 +61,6 @@ describe( 'NoAudienceBannerWidget', () => { jest.clearAllMocks(); } ); - it( 'should not render when availableAudiences and configuredAudiences are not loaded', () => { - muteFetch( audienceSettingsRegExp ); - - const { container } = render( , { - registry, - } ); - - expect( container ).toBeEmptyDOMElement(); - } ); - - it( 'should not render when availableAudiences is not loaded', () => { - registry.dispatch( MODULES_ANALYTICS_4 ).receiveGetAudienceSettings( { - configuredAudiences: [ 'properties/12345/audiences/1' ], - isAudienceSegmentationWidgetHidden: false, - } ); - - const { container } = render( , { - registry, - } ); - - expect( container ).toBeEmptyDOMElement(); - } ); - it( 'should not render when configuredAudiences is not loaded', () => { muteFetch( audienceSettingsRegExp ); @@ -115,7 +92,7 @@ describe( 'NoAudienceBannerWidget', () => { expect( container ).toBeEmptyDOMElement(); } ); - it( 'should not render when there is no configured audience.', () => { + it( 'should render when there is no configured audience.', () => { registry .dispatch( MODULES_ANALYTICS_4 ) .setAvailableAudiences( availableAudiences ); @@ -129,7 +106,7 @@ describe( 'NoAudienceBannerWidget', () => { registry, } ); - expect( container ).toBeEmptyDOMElement(); + expect( container ).toMatchSnapshot(); } ); it( 'should not render when configured audience is matching available audiences', () => { diff --git a/assets/js/modules/analytics-4/datastore/audiences.js b/assets/js/modules/analytics-4/datastore/audiences.js index e15a4a79e43..16c59331848 100644 --- a/assets/js/modules/analytics-4/datastore/audiences.js +++ b/assets/js/modules/analytics-4/datastore/audiences.js @@ -166,10 +166,64 @@ const baseActions = { * @return {Object} Object with `response` and `error`. */ *syncAvailableAudiences() { - const { response, error } = + const { response: availableAudiences, error } = yield fetchSyncAvailableAudiencesStore.actions.fetchSyncAvailableAudiences(); - return { response, error }; + if ( error ) { + return { response: availableAudiences, error }; + } + + const registry = yield commonActions.getRegistry(); + const { select, dispatch } = registry; + + // Remove any configuredAudiences that are no longer available in availableAudiences. + const configuredAudiences = + select( MODULES_ANALYTICS_4 ).getConfiguredAudiences(); + const newConfiguredAudiences = configuredAudiences?.filter( + ( configuredAudience ) => + availableAudiences?.some( + ( { name } ) => name === configuredAudience + ) + ); + + if ( + configuredAudiences && + newConfiguredAudiences && + newConfiguredAudiences !== configuredAudiences + ) { + dispatch( MODULES_ANALYTICS_4 ).setConfiguredAudiences( + newConfiguredAudiences || [] + ); + } + + return { response: availableAudiences, error }; + }, + + /** + * Syncs available audiences older than 1 hour. + * + * @since n.e.x.t + * + * @return {void} + */ + *maybeSyncAvailableAudiences() { + const registry = yield commonActions.getRegistry(); + const { select, dispatch } = registry; + + const availableAudiencesLastSyncedAt = + select( MODULES_ANALYTICS_4 ).getAvailableAudiencesLastSyncedAt(); + + // Update the audience cache if the availableAudiencesLastSyncedAt setting is older than 1 hour. + if ( + ! availableAudiencesLastSyncedAt || + availableAudiencesLastSyncedAt * 1000 < + // eslint-disable-next-line sitekit/no-direct-date + Date.now() - 1 * 60 * 60 * 1000 + ) { + yield commonActions.await( + dispatch( MODULES_ANALYTICS_4 ).syncAvailableAudiences() + ); + } }, /** diff --git a/assets/js/modules/analytics-4/datastore/audiences.test.js b/assets/js/modules/analytics-4/datastore/audiences.test.js index d268284060a..ed08c7f4cff 100644 --- a/assets/js/modules/analytics-4/datastore/audiences.test.js +++ b/assets/js/modules/analytics-4/datastore/audiences.test.js @@ -53,6 +53,9 @@ describe( 'modules/analytics-4 audiences', () => { const syncAvailableAudiencesEndpoint = new RegExp( '^/google-site-kit/v1/modules/analytics-4/data/sync-audiences' ); + const audienceSettingsEndpoint = new RegExp( + '^/google-site-kit/v1/modules/analytics-4/data/audience-settings' + ); const audience = { displayName: 'Recently active users', @@ -267,10 +270,20 @@ describe( 'modules/analytics-4 audiences', () => { status: 500, } ); + fetchMock.getOnce( audienceSettingsEndpoint, { + body: { + data: { + configuredAudiences: [], + }, + }, + } ); + const { response, error } = await registry .dispatch( MODULES_ANALYTICS_4 ) .syncAvailableAudiences(); + await waitForDefaultTimeouts(); + expect( response ).toBeUndefined(); expect( error ).toEqual( errorResponse ); @@ -289,10 +302,20 @@ describe( 'modules/analytics-4 audiences', () => { status: 200, } ); + fetchMock.get( audienceSettingsEndpoint, { + body: { + data: { + configuredAudiences: [], + }, + }, + } ); + const { response, error } = await registry .dispatch( MODULES_ANALYTICS_4 ) .syncAvailableAudiences(); + await waitForDefaultTimeouts(); + expect( response ).toEqual( availableAudiences ); expect( error ).toBeUndefined(); @@ -302,6 +325,127 @@ describe( 'modules/analytics-4 audiences', () => { .getAvailableAudiences() ).toEqual( availableAudiences ); } ); + + it( 'should remove configured audiences which are no longer available', async () => { + const availableAudiencesSubset = [ + availableAudiencesFixture[ 0 ], + availableAudiencesFixture[ 2 ], + ]; + + fetchMock.post( syncAvailableAudiencesEndpoint, { + body: availableAudiencesSubset, + status: 200, + } ); + + const settings = { + configuredAudiences: availableAudiencesFixture.reduce( + ( acc, { name } ) => [ ...acc, name ], + [] + ), + isAudienceSegmentationWidgetHidden: false, + }; + + registry + .dispatch( MODULES_ANALYTICS_4 ) + .receiveGetAudienceSettings( settings ); + + await registry + .dispatch( MODULES_ANALYTICS_4 ) + .syncAvailableAudiences(); + + expect( fetchMock ).toHaveFetchedTimes( 1 ); + expect( fetchMock ).toHaveFetched( + syncAvailableAudiencesEndpoint + ); + + expect( + registry + .select( MODULES_ANALYTICS_4 ) + .getConfiguredAudiences() + ).toEqual( + availableAudiencesSubset.reduce( + ( acc, { name } ) => [ ...acc, name ], + [] + ) + ); + } ); + } ); + + describe( 'maybeSyncAvailableAudiences', () => { + it( 'should call syncAvailableAudiences if the availableAudiencesLastSyncedAt setting is undefined', async () => { + fetchMock.postOnce( syncAvailableAudiencesEndpoint, { + body: availableAudiencesFixture, + status: 200, + } ); + + fetchMock.getOnce( audienceSettingsEndpoint, { + body: { + data: { + configuredAudiences: [], + }, + }, + } ); + + await registry + .dispatch( MODULES_ANALYTICS_4 ) + .maybeSyncAvailableAudiences(); + + await waitForDefaultTimeouts(); + + expect( fetchMock ).toHaveFetchedTimes( 2 ); + expect( fetchMock ).toHaveFetched( + syncAvailableAudiencesEndpoint + ); + } ); + + it( 'should not call syncAvailableAudiences if the availableAudiencesLastSyncedAt setting is within the last hour', async () => { + fetchMock.post( syncAvailableAudiencesEndpoint, { + body: availableAudiencesFixture, + status: 200, + } ); + + registry.dispatch( MODULES_ANALYTICS_4 ).receiveGetSettings( { + availableAudiencesLastSyncedAt: + ( Date.now() - 1000 ) / 1000, // Value expected to be a PHP date so divide by 1000. + } ); + + await registry + .dispatch( MODULES_ANALYTICS_4 ) + .maybeSyncAvailableAudiences(); + + expect( fetchMock ).toHaveFetchedTimes( 0 ); + } ); + + it( 'should call syncAvailableAudiences if the availableAudiencesLastSyncedAt setting is not within the last hour', async () => { + fetchMock.postOnce( syncAvailableAudiencesEndpoint, { + body: availableAudiencesFixture, + status: 200, + } ); + + fetchMock.getOnce( audienceSettingsEndpoint, { + body: { + data: { + configuredAudiences: [], + }, + }, + } ); + + registry.dispatch( MODULES_ANALYTICS_4 ).receiveGetSettings( { + availableAudiencesLastSyncedAt: + ( Date.now() - 2 * 60 * 60 * 1000 ) / 1000, // Value expected to be a PHP date so divide by 1000. + } ); + + await registry + .dispatch( MODULES_ANALYTICS_4 ) + .maybeSyncAvailableAudiences(); + + await waitForDefaultTimeouts(); + + expect( fetchMock ).toHaveFetchedTimes( 2 ); + expect( fetchMock ).toHaveFetched( + syncAvailableAudiencesEndpoint + ); + } ); } ); describe( 'enableAudienceGroup', () => { @@ -408,10 +552,6 @@ describe( 'modules/analytics-4 audiences', () => { }; } - const audienceSettingsEndpoint = new RegExp( - '^/google-site-kit/v1/modules/analytics-4/data/audience-settings' - ); - const testPropertyID = propertiesFixture[ 0 ]._id; const referenceDate = '2024-05-10'; @@ -1005,10 +1145,20 @@ describe( 'modules/analytics-4 audiences', () => { status: 500, } ); + fetchMock.get( audienceSettingsEndpoint, { + body: { + data: { + configuredAudiences: [], + }, + }, + } ); + const { response, error } = await registry .dispatch( MODULES_ANALYTICS_4 ) .enableAudienceGroup(); + await waitForDefaultTimeouts(); + expect( response ).toBeUndefined(); expect( error ).toEqual( errorResponse ); diff --git a/assets/js/modules/analytics-4/index.js b/assets/js/modules/analytics-4/index.js index a1280ec1817..54a8d5a6d8d 100644 --- a/assets/js/modules/analytics-4/index.js +++ b/assets/js/modules/analytics-4/index.js @@ -154,7 +154,7 @@ export const registerWidgets = ( widgets ) => { isActive: ( select ) => { const configuredAudiences = select( MODULES_ANALYTICS_4 ).getConfiguredAudiences(); - return configuredAudiences?.length > 0; + return !! configuredAudiences; }, }, [ AREA_MAIN_DASHBOARD_TRAFFIC_AUDIENCE_SEGMENTATION ] diff --git a/assets/sass/components/analytics-4/_index.scss b/assets/sass/components/analytics-4/_index.scss index e139b957ad3..57e85b51094 100644 --- a/assets/sass/components/analytics-4/_index.scss +++ b/assets/sass/components/analytics-4/_index.scss @@ -23,6 +23,7 @@ @import "googlesitekit-analytics-enhanced-measurement"; @import "audience-segmentation/googlesitekit-audience-segmentation-setup-cta"; @import "audience-segmentation/googlesitekit-audience-segmentation-tile"; +@import "audience-segmentation/googlesitekit-audience-segmentation-tile-loading"; @import "audience-segmentation/googlesitekit-audience-segmentation-tile-error"; @import "audience-segmentation/googlesitekit-audience-segmentation-info-notice"; @import "audience-segmentation/googlesitekit-audience-segmentation-error"; diff --git a/assets/sass/components/analytics-4/audience-segmentation/_googlesitekit-audience-segmentation-tile-loading.scss b/assets/sass/components/analytics-4/audience-segmentation/_googlesitekit-audience-segmentation-tile-loading.scss new file mode 100644 index 00000000000..557357ad96e --- /dev/null +++ b/assets/sass/components/analytics-4/audience-segmentation/_googlesitekit-audience-segmentation-tile-loading.scss @@ -0,0 +1,50 @@ +/** + * Audience Segmentation Loading styles. + * + * Site Kit by Google, Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.googlesitekit-plugin { + .googlesitekit-audience-segmentation-tile-loading { + margin: $grid-gap-phone $grid-gap-phone $grid-gap-phone + 4px; + + @media (min-width: $bp-desktop) { + margin: $grid-gap-desktop $grid-gap-desktop $grid-gap-desktop + 4px; + } + + .googlesitekit-preview-block { + margin: 22px 0; + + &:first-of-type { + display: none; + } + + &:last-of-type { + // Match the no data height of the final metric block + // on tablet and desktop to minimize layout shift. + margin-bottom: 31px; + } + + @media (min-width: $bp-desktop) { + margin: 24px 0; + + &:first-of-type { + display: flex; + margin-bottom: 35px; + } + } + } + } +} diff --git a/tests/backstop/reference/google-site-kit_Modules_Analytics4_Components_AudienceSegmentation_Dashboard_AudienceTile_Loading_0_document_0_small.png b/tests/backstop/reference/google-site-kit_Modules_Analytics4_Components_AudienceSegmentation_Dashboard_AudienceTile_Loading_0_document_0_small.png new file mode 100644 index 00000000000..38a6c1013f7 Binary files /dev/null and b/tests/backstop/reference/google-site-kit_Modules_Analytics4_Components_AudienceSegmentation_Dashboard_AudienceTile_Loading_0_document_0_small.png differ diff --git a/tests/backstop/reference/google-site-kit_Modules_Analytics4_Components_AudienceSegmentation_Dashboard_AudienceTile_Loading_0_document_1_medium.png b/tests/backstop/reference/google-site-kit_Modules_Analytics4_Components_AudienceSegmentation_Dashboard_AudienceTile_Loading_0_document_1_medium.png new file mode 100644 index 00000000000..e6502866221 Binary files /dev/null and b/tests/backstop/reference/google-site-kit_Modules_Analytics4_Components_AudienceSegmentation_Dashboard_AudienceTile_Loading_0_document_1_medium.png differ diff --git a/tests/backstop/reference/google-site-kit_Modules_Analytics4_Components_AudienceSegmentation_Dashboard_AudienceTile_Loading_0_document_2_large.png b/tests/backstop/reference/google-site-kit_Modules_Analytics4_Components_AudienceSegmentation_Dashboard_AudienceTile_Loading_0_document_2_large.png new file mode 100644 index 00000000000..9acbdde09e6 Binary files /dev/null and b/tests/backstop/reference/google-site-kit_Modules_Analytics4_Components_AudienceSegmentation_Dashboard_AudienceTile_Loading_0_document_2_large.png differ