From 88d43125526b48ad9554584ba38cba859f6f6e38 Mon Sep 17 00:00:00 2001 From: Ben Bowler Date: Wed, 10 Sep 2025 11:10:30 +0100 Subject: [PATCH 1/2] Implement PUE site-level settings datastore partial. --- .../js/googlesitekit/datastore/site/index.js | 2 + .../site/proactive-user-engagement.js | 188 +++++++++++++++ .../site/proactive-user-engagement.test.js | 225 ++++++++++++++++++ 3 files changed, 415 insertions(+) create mode 100644 assets/js/googlesitekit/datastore/site/proactive-user-engagement.js create mode 100644 assets/js/googlesitekit/datastore/site/proactive-user-engagement.test.js diff --git a/assets/js/googlesitekit/datastore/site/index.js b/assets/js/googlesitekit/datastore/site/index.js index 7d5babe9d01..9371115062f 100644 --- a/assets/js/googlesitekit/datastore/site/index.js +++ b/assets/js/googlesitekit/datastore/site/index.js @@ -25,6 +25,7 @@ import cache from './cache'; import connection from './connection'; import consentMode from './consent-mode'; import conversionTracking from './conversion-tracking'; +import proactiveUserEngagement from './proactive-user-engagement'; import errors from './errors'; import googleTagGateway from './google-tag-gateway'; import html from './html'; @@ -42,6 +43,7 @@ const store = combineStores( connection, consentMode, conversionTracking, + proactiveUserEngagement, errors, googleTagGateway, html, diff --git a/assets/js/googlesitekit/datastore/site/proactive-user-engagement.js b/assets/js/googlesitekit/datastore/site/proactive-user-engagement.js new file mode 100644 index 00000000000..4a8dca5a39e --- /dev/null +++ b/assets/js/googlesitekit/datastore/site/proactive-user-engagement.js @@ -0,0 +1,188 @@ +/** + * Site Kit by Google, Copyright 2025 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. + */ + +/** + * External dependencies + */ +import invariant from 'invariant'; +import { isPlainObject } from 'lodash'; + +/** + * Internal dependencies + */ +import { get, set } from 'googlesitekit-api'; +import { + commonActions, + combineStores, + createRegistrySelector, + createReducer, +} from 'googlesitekit-data'; +import { createFetchStore } from '@/js/googlesitekit/data/create-fetch-store'; +import { CORE_SITE } from './constants'; + +const { getRegistry } = commonActions; + +const SET_PROACTIVE_USER_ENGAGEMENT_ENABLED = + 'SET_PROACTIVE_USER_ENGAGEMENT_ENABLED'; + +const settingsReducerCallback = createReducer( ( state, settings ) => { + state.proactiveUserEngagementSettings = settings; +} ); + +const fetchGetProactiveUserEngagementSettingsStore = createFetchStore( { + baseName: 'getProactiveUserEngagementSettings', + controlCallback: () => { + return get( 'core', 'site', 'proactive-user-engagement', null, { + useCache: false, + } ); + }, + reducerCallback: settingsReducerCallback, +} ); + +const fetchSaveProactiveUserEngagementSettingsStore = createFetchStore( { + baseName: 'saveProactiveUserEngagementSettings', + controlCallback: ( { settings } ) => { + return set( 'core', 'site', 'proactive-user-engagement', { settings } ); + }, + reducerCallback: settingsReducerCallback, + argsToParams: ( settings ) => { + return { settings }; + }, + validateParams: ( { settings } ) => { + invariant( + isPlainObject( settings ), + 'settings must be a plain object.' + ); + }, +} ); + +const baseInitialState = { + // Holds the PUE settings object, e.g. { enabled: boolean } once loaded. + proactiveUserEngagementSettings: undefined, +}; + +const baseActions = { + /** + * Saves the Proactive User Engagement settings. + * + * @since n.e.x.t + * + * @return {Object} Object with `response` and `error`. + */ + *saveProactiveUserEngagementSettings() { + const { select } = yield getRegistry(); + const settings = + select( CORE_SITE ).getProactiveUserEngagementSettings(); + + return yield fetchSaveProactiveUserEngagementSettingsStore.actions.fetchSaveProactiveUserEngagementSettings( + settings + ); + }, + + /** + * Sets the Proactive User Engagement enabled status. + * + * @since n.e.x.t + * + * @param {string} enabled PUE enabled status. + * @return {Object} Redux-style action. + */ + setProactiveUserEngagementEnabled( enabled ) { + return { + type: SET_PROACTIVE_USER_ENGAGEMENT_ENABLED, + payload: { enabled }, + }; + }, +}; + +const baseControls = {}; + +const baseReducer = createReducer( ( state, { type, payload } ) => { + switch ( type ) { + case SET_PROACTIVE_USER_ENGAGEMENT_ENABLED: + state.proactiveUserEngagementSettings = + state.proactiveUserEngagementSettings || {}; + state.proactiveUserEngagementSettings.enabled = !! payload.enabled; + break; + + default: + break; + } +} ); + +const baseSelectors = { + /** + * Gets the Proactive User Engagement settings. + * + * @since n.e.x.t + * + * @param {Object} state Data store's state. + * @return {Object|undefined} PUE settings, or `undefined` if not loaded. + */ + getProactiveUserEngagementSettings: ( state ) => { + return state.proactiveUserEngagementSettings; + }, + + /** + * Gets the Proactive User Engagement enabled status. + * + * @since n.e.x.t + * + * @return {boolean|undefined} PUE enabled status, or `undefined` if not loaded. + */ + isProactiveUserEngagementEnabled: createRegistrySelector( + ( select ) => () => { + const { enabled } = + select( CORE_SITE ).getProactiveUserEngagementSettings() || {}; + + return enabled; + } + ), +}; + +const baseResolvers = { + *getProactiveUserEngagementSettings() { + const { select } = yield getRegistry(); + + if ( select( CORE_SITE ).getProactiveUserEngagementSettings() ) { + return; + } + + yield fetchGetProactiveUserEngagementSettingsStore.actions.fetchGetProactiveUserEngagementSettings(); + }, +}; + +const store = combineStores( + fetchGetProactiveUserEngagementSettingsStore, + fetchSaveProactiveUserEngagementSettingsStore, + { + initialState: baseInitialState, + actions: baseActions, + controls: baseControls, + reducer: baseReducer, + resolvers: baseResolvers, + selectors: baseSelectors, + } +); + +export const initialState = store.initialState; +export const actions = store.actions; +export const controls = store.controls; +export const reducer = store.reducer; +export const resolvers = store.resolvers; +export const selectors = store.selectors; + +export default store; diff --git a/assets/js/googlesitekit/datastore/site/proactive-user-engagement.test.js b/assets/js/googlesitekit/datastore/site/proactive-user-engagement.test.js new file mode 100644 index 00000000000..68261c2bee0 --- /dev/null +++ b/assets/js/googlesitekit/datastore/site/proactive-user-engagement.test.js @@ -0,0 +1,225 @@ +/** + * Site Kit by Google, Copyright 2025 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 { setUsingCache } from 'googlesitekit-api'; +import { + createTestRegistry, + untilResolved, + waitForDefaultTimeouts, +} from '../../../../../tests/js/utils'; +import { CORE_SITE } from './constants'; + +describe( 'core/site Proactive User Engagement', () => { + let registry; + + const pueSettingsEndpointRegExp = new RegExp( + '^/google-site-kit/v1/core/site/data/proactive-user-engagement' + ); + + beforeAll( () => { + setUsingCache( false ); + } ); + + beforeEach( () => { + registry = createTestRegistry(); + } ); + + afterAll( () => { + setUsingCache( true ); + } ); + + describe( 'actions', () => { + describe( 'saveProactiveUserEngagementSettings', () => { + it( 'saves the settings and returns the response', async () => { + const updatedSettings = { + enabled: true, + }; + + fetchMock.postOnce( pueSettingsEndpointRegExp, { + body: updatedSettings, + status: 200, + } ); + + registry + .dispatch( CORE_SITE ) + .receiveGetProactiveUserEngagementSettings( { + enabled: false, + } ); + + registry + .dispatch( CORE_SITE ) + .setProactiveUserEngagementEnabled( true ); + + const { response } = await registry + .dispatch( CORE_SITE ) + .saveProactiveUserEngagementSettings(); + + expect( fetchMock ).toHaveFetched( pueSettingsEndpointRegExp, { + body: { + data: { + settings: updatedSettings, + }, + }, + } ); + + expect( response ).toEqual( updatedSettings ); + } ); + + it( 'returns an error if the request fails', async () => { + const errorResponse = { + code: 'internal_server_error', + message: 'Internal server error', + data: { status: 500 }, + }; + + fetchMock.postOnce( pueSettingsEndpointRegExp, { + body: errorResponse, + status: 500, + } ); + + registry + .dispatch( CORE_SITE ) + .setProactiveUserEngagementEnabled( true ); + + const { error } = await registry + .dispatch( CORE_SITE ) + .saveProactiveUserEngagementSettings(); + + expect( fetchMock ).toHaveFetched( pueSettingsEndpointRegExp, { + body: { + data: { + settings: { enabled: true }, + }, + }, + } ); + + expect( error ).toEqual( errorResponse ); + + expect( console ).toHaveErrored(); + } ); + } ); + + describe( 'setProactiveUserEngagementEnabled', () => { + it( 'sets the enabled status', () => { + registry + .dispatch( CORE_SITE ) + .receiveGetProactiveUserEngagementSettings( { + enabled: false, + } ); + + expect( + registry + .select( CORE_SITE ) + .isProactiveUserEngagementEnabled() + ).toBe( false ); + + registry + .dispatch( CORE_SITE ) + .setProactiveUserEngagementEnabled( true ); + + expect( + registry + .select( CORE_SITE ) + .isProactiveUserEngagementEnabled() + ).toBe( true ); + } ); + } ); + } ); + + describe( 'selectors', () => { + describe( 'getProactiveUserEngagementSettings', () => { + it( 'uses a resolver to make a network request', async () => { + const settingsResponse = { + enabled: false, + }; + + fetchMock.getOnce( pueSettingsEndpointRegExp, { + body: settingsResponse, + status: 200, + } ); + + const initialSettings = registry + .select( CORE_SITE ) + .getProactiveUserEngagementSettings(); + + expect( initialSettings ).toBeUndefined(); + + await untilResolved( + registry, + CORE_SITE + ).getProactiveUserEngagementSettings(); + + const settings = registry + .select( CORE_SITE ) + .getProactiveUserEngagementSettings(); + + expect( settings ).toEqual( settingsResponse ); + + expect( fetchMock ).toHaveFetched( pueSettingsEndpointRegExp ); + } ); + + it( 'returns undefined if the request fails', async () => { + fetchMock.getOnce( pueSettingsEndpointRegExp, { + body: { error: 'something went wrong' }, + status: 500, + } ); + + const initialSettings = registry + .select( CORE_SITE ) + .getProactiveUserEngagementSettings(); + + expect( initialSettings ).toBeUndefined(); + + await untilResolved( + registry, + CORE_SITE + ).getProactiveUserEngagementSettings(); + + const settings = registry + .select( CORE_SITE ) + .getProactiveUserEngagementSettings(); + + // Verify the settings are still undefined after the selector is resolved. + expect( settings ).toBeUndefined(); + + await waitForDefaultTimeouts(); + + expect( fetchMock ).toHaveFetched( pueSettingsEndpointRegExp ); + + expect( console ).toHaveErrored(); + } ); + } ); + + describe( 'isProactiveUserEngagementEnabled', () => { + it( 'returns the enabled status', () => { + registry + .dispatch( CORE_SITE ) + .receiveGetProactiveUserEngagementSettings( { + enabled: true, + } ); + + expect( + registry + .select( CORE_SITE ) + .isProactiveUserEngagementEnabled() + ).toBe( true ); + } ); + } ); + } ); +} ); From 84baa377d08cb16d0f87d66362f8b78e8b784033 Mon Sep 17 00:00:00 2001 From: Matthew Riley MacPherson Date: Mon, 15 Sep 2025 23:56:03 +0100 Subject: [PATCH 2/2] Update comment text. --- .../googlesitekit/datastore/site/proactive-user-engagement.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/googlesitekit/datastore/site/proactive-user-engagement.js b/assets/js/googlesitekit/datastore/site/proactive-user-engagement.js index 4a8dca5a39e..55ede5ffe42 100644 --- a/assets/js/googlesitekit/datastore/site/proactive-user-engagement.js +++ b/assets/js/googlesitekit/datastore/site/proactive-user-engagement.js @@ -70,7 +70,7 @@ const fetchSaveProactiveUserEngagementSettingsStore = createFetchStore( { } ); const baseInitialState = { - // Holds the PUE settings object, e.g. { enabled: boolean } once loaded. + // Holds the Proactive User Engagement settings object, e.g. `{ enabled: boolean }` once loaded. proactiveUserEngagementSettings: undefined, };