/
githubmirror
/
wp-calypso
Обзор
Документация
Войти
/
githubmirror
/
wp-calypso
Код
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
trunk
client/state/test/initial-state.js
832 строки
22 KB
Griffith Chen
Trim redundant lodash references from js-utils comments (#112267)
03 июл 2026, 17:11
Не верифицирован
03 июл 2026, 17:11
f3399af
Код
Авторство
О чём код?
/** * @jest-environment jsdom */ import { withStorageKey } from '@automattic/state-utils'; import * as browserStorage from 'calypso/lib/browser-storage'; import { isSupportSession } from 'calypso/lib/user/support-user-interop'; import { createReduxStore } from 'calypso/state'; import { addReducerToStore } from 'calypso/state/add-reducer'; import { MAX_AGE, SERIALIZE_THROTTLE } from 'calypso/state/constants'; import currentUser from 'calypso/state/current-user/reducer'; import { getInitialState, getStateFromCache, persistOnChange } from 'calypso/state/initial-state'; import { loadPersistedState } from 'calypso/state/persisted-state'; import postTypes from 'calypso/state/post-types/reducer'; import signupReducer from 'calypso/state/signup/reducer'; import { combineReducers, withPersistence } from 'calypso/state/utils'; // Create a legacy initial reducer, with no modularization. const initialReducer = combineReducers( { currentUser, postTypes, } ); jest.mock( 'calypso/lib/user/support-user-interop', () => ( { isSupportSession: jest.fn().mockReturnValue( false ), } ) ); describe( 'initial-state', () => { describe( 'getInitialState', () => { describe( 'retrieve persisted state from the cached client data', () => { describe( 'switched user', () => { describe( 'with recently persisted data and initial server data', () => { let state; let consoleErrorSpy; let getStoredItemSpy; const savedState = { 'redux-state-123456789': { currentUser: { id: 123456789 }, postTypes: { items: { 2916284: { post: { name: 'post', label: 'Posts' }, page: { name: 'page', label: 'Pages' }, }, }, }, _timestamp: Date.now(), }, }; beforeAll( async () => { isSupportSession.mockReturnValue( true ); window.initialReduxState = { currentUser: { currencyCode: 'USD' } }; consoleErrorSpy = jest.spyOn( global.console, 'error' ); getStoredItemSpy = jest .spyOn( browserStorage, 'getAllStoredItems' ) .mockResolvedValue( savedState ); await loadPersistedState(); state = getInitialState( initialReducer, 123456789 ); } ); afterAll( () => { isSupportSession.mockReturnValue( false ); window.initialReduxState = null; consoleErrorSpy.mockRestore(); getStoredItemSpy.mockRestore(); } ); test( 'builds initial state without errors', () => { expect( consoleErrorSpy ).not.toHaveBeenCalled(); } ); test( 'does not build initial state using IndexedDB state', () => { expect( state.postTypes ).toBeUndefined(); } ); test( 'does not add timestamp to initial state', () => { expect( state._timestamp ).toBeUndefined(); } ); test( 'does not build initial state using server state', () => { expect( state.currentUser ).toBeUndefined(); } ); } ); } ); describe( 'with recently persisted data and initial server data', () => { let state; let consoleErrorSpy; let getStoredItemSpy; const savedState = { 'redux-state-123456789': { currentUser: { id: 123456789 }, postTypes: { items: { 2916284: { post: { name: 'post', label: 'Posts' }, page: { name: 'page', label: 'Pages' }, }, }, }, _timestamp: Date.now(), }, }; const serverState = { postTypes: { items: { 77203074: { post: { name: 'post', label: 'Posts' }, }, }, }, }; beforeAll( async () => { window.initialReduxState = serverState; consoleErrorSpy = jest.spyOn( global.console, 'error' ); getStoredItemSpy = jest .spyOn( browserStorage, 'getAllStoredItems' ) .mockResolvedValue( savedState ); await loadPersistedState(); state = getInitialState( initialReducer, 123456789 ); } ); afterAll( () => { window.initialReduxState = null; consoleErrorSpy.mockRestore(); getStoredItemSpy.mockRestore(); } ); test( 'builds initial state without errors', () => { expect( consoleErrorSpy ).not.toHaveBeenCalled(); } ); test( 'builds initial state using IndexedDB state', () => { expect( state.currentUser.id ).toBe( 123456789 ); } ); test( 'does not add timestamp to initial state', () => { expect( state._timestamp ).toBeUndefined(); } ); test( 'server state shallowly overrides IndexedDB state', () => { expect( state.postTypes.items ).toEqual( serverState.postTypes.items ); } ); } ); describe( 'with stale persisted data and initial server data', () => { let state; let consoleErrorSpy; let getStoredItemSpy; const savedState = { 'redux-state-123456789': { currentUser: { id: 123456789 }, postTypes: { items: { 2916284: { post: { name: 'post', label: 'Posts' }, page: { name: 'page', label: 'Pages' }, }, }, }, _timestamp: Date.now() - MAX_AGE - 1, }, }; const serverState = { postTypes: { items: { 77203074: { post: { name: 'post', label: 'Posts' }, }, }, }, }; beforeAll( async () => { window.initialReduxState = serverState; consoleErrorSpy = jest.spyOn( global.console, 'error' ); getStoredItemSpy = jest .spyOn( browserStorage, 'getAllStoredItems' ) .mockResolvedValue( savedState ); await loadPersistedState(); state = getInitialState( initialReducer, 123456789 ); } ); afterAll( () => { window.initialReduxState = null; consoleErrorSpy.mockRestore(); getStoredItemSpy.mockRestore(); } ); test( 'builds store without errors', () => { expect( consoleErrorSpy ).not.toHaveBeenCalled(); } ); test( 'builds initial state using server state', () => { expect( state.postTypes.items ).toEqual( serverState.postTypes.items ); } ); test( 'does not build initial state using IndexedDB state', () => { expect( state.currentUser ).toBeUndefined(); } ); test( 'does not add timestamp to store', () => { expect( state._timestamp ).toBeUndefined(); } ); } ); describe( 'with recently persisted data and no initial server data', () => { let state; let consoleErrorSpy; let getStoredItemSpy; const savedState = { 'redux-state-123456789': { currentUser: { id: 123456789 }, postTypes: { items: { 2916284: { post: { name: 'post', label: 'Posts' }, page: { name: 'page', label: 'Pages' }, }, }, }, _timestamp: Date.now(), }, }; const serverState = {}; beforeAll( async () => { window.initialReduxState = serverState; consoleErrorSpy = jest.spyOn( global.console, 'error' ); getStoredItemSpy = jest .spyOn( browserStorage, 'getAllStoredItems' ) .mockResolvedValue( savedState ); await loadPersistedState(); state = getInitialState( initialReducer, 123456789 ); } ); afterAll( () => { window.initialReduxState = null; consoleErrorSpy.mockRestore(); getStoredItemSpy.mockRestore(); } ); test( 'builds initial state without errors', () => { expect( consoleErrorSpy ).not.toHaveBeenCalled(); } ); test( 'builds state using local forage state', () => { expect( state.currentUser.id ).toBe( 123456789 ); expect( state.postTypes.items ).toEqual( savedState[ 'redux-state-123456789' ].postTypes.items ); } ); test( 'does not add timestamp to store', () => { expect( state._timestamp ).toBeUndefined(); } ); } ); describe( 'with invalid persisted data and no initial server data', () => { let state; let consoleErrorSpy; let getStoredItemSpy; const userId = 123456789 + 1; const savedState = { [ `redux-state-${ userId }` ]: { // Create an invalid state by forcing the user ID // stored in the state to differ from the current // mocked user ID. currentUser: { id: userId }, postTypes: { items: { 2916284: { post: { name: 'post', label: 'Posts' }, page: { name: 'page', label: 'Pages' }, }, }, }, _timestamp: Date.now(), }, }; const serverState = {}; beforeAll( async () => { window.initialReduxState = serverState; consoleErrorSpy = jest.spyOn( global.console, 'error' ); getStoredItemSpy = jest .spyOn( browserStorage, 'getAllStoredItems' ) .mockResolvedValue( savedState ); await loadPersistedState(); state = getInitialState( initialReducer, 123456789 ); } ); afterAll( () => { window.initialReduxState = null; consoleErrorSpy.mockRestore(); getStoredItemSpy.mockRestore(); } ); test( 'builds initial state without errors', () => { expect( consoleErrorSpy ).not.toHaveBeenCalled(); } ); test( 'does not build initial state using IndexedDB state', () => { expect( state.postTypes ).toBeUndefined(); } ); test( 'does not add timestamp to initial state', () => { expect( state._timestamp ).toBeUndefined(); } ); } ); } ); } ); describe( 'getStateFromCache', () => { describe( 'with empty persisted signup state for logged in user, and persisted state for logged out user', () => { let state; let consoleErrorSpy; let getStoredItemSpy; const _timestamp = Date.now(); const storedState = { 'redux-state-logged-out:signup': { dependencyStore: { siteType: 'blog', siteTitle: 'Logged out test title', }, progress: { 'logged-out-step': { stepName: 'logged-out-step', status: 'completed', }, }, _timestamp, }, }; const serverState = {}; beforeAll( async () => { window.initialReduxState = serverState; consoleErrorSpy = jest.spyOn( global.console, 'error' ); getStoredItemSpy = jest .spyOn( browserStorage, 'getAllStoredItems' ) .mockResolvedValue( storedState ); await loadPersistedState(); state = getStateFromCache()( signupReducer, 'signup' ); } ); afterAll( () => { window.initialReduxState = null; consoleErrorSpy.mockRestore(); getStoredItemSpy.mockRestore(); } ); test( 'builds initial state without errors', () => { expect( consoleErrorSpy ).not.toHaveBeenCalled(); } ); test( 'builds initial signup state from logged out state', () => { expect( state.dependencyStore ).toEqual( storedState[ 'redux-state-logged-out:signup' ].dependencyStore ); expect( state.progress ).toEqual( storedState[ 'redux-state-logged-out:signup' ].progress ); } ); test( 'does not add timestamp to initial state', () => { expect( state._timestamp ).toBeUndefined(); } ); } ); describe( 'with existing persisted signup state for logged in user, and persisted state for logged out user', () => { let state; let consoleErrorSpy; let getStoredItemSpy; const _timestamp = Date.now(); const storedState = { 'redux-state-123456789:signup': { dependencyStore: { siteType: 'blog', siteTitle: 'Logged in test title', }, progress: { 'logged-in-step': { stepName: 'logged-in-step', status: 'completed', }, }, _timestamp, }, 'redux-state-logged-out:signup': { dependencyStore: { siteType: 'blog', siteTitle: 'Logged out test title', }, progress: { 'logged-out-step': { stepName: 'logged-out-step', status: 'completed', }, }, _timestamp, }, }; const serverState = {}; beforeAll( async () => { window.initialReduxState = serverState; consoleErrorSpy = jest.spyOn( global.console, 'error' ); getStoredItemSpy = jest .spyOn( browserStorage, 'getAllStoredItems' ) .mockResolvedValue( storedState ); await loadPersistedState(); state = getStateFromCache( 123456789 )( signupReducer, 'signup' ); } ); afterAll( () => { window.initialReduxState = null; consoleErrorSpy.mockRestore(); getStoredItemSpy.mockRestore(); } ); test( 'builds initial state without errors', () => { expect( consoleErrorSpy ).not.toHaveBeenCalled(); } ); test( 'builds initial signup state from logged in state', () => { expect( state.dependencyStore ).toEqual( storedState[ 'redux-state-123456789:signup' ].dependencyStore ); expect( state.progress ).toEqual( storedState[ 'redux-state-123456789:signup' ].progress ); } ); test( 'does not add timestamp to initial state', () => { expect( state._timestamp ).toBeUndefined(); } ); } ); } ); describe( '#persistOnChange()', () => { let store; let setStoredItemSpy; let stopPersisting; const dataReducer = withPersistence( ( state = null, { data } ) => { if ( data && data !== state ) { return data; } return state; } ); const currentUserReducer = withPersistence( ( state = null, { userId } ) => { if ( userId && userId !== state.id ) { return { ...state, id: userId }; } return state; } ); const reducer = combineReducers( { data: dataReducer, currentUser: currentUserReducer } ); // Create a valid initial state (with a stored user ID that matches the // current mocked user ID). const initialState = { currentUser: { id: 123456789 } }; beforeEach( async () => { // we use fake timers from Sinon (aka Lolex) because `throttle` also uses `Date.now()` // and relies on it returning a mocked value. Jest fake timers don't mock `Date`, Lolex does. jest.useFakeTimers(); setStoredItemSpy = jest .spyOn( browserStorage, 'setStoredItem' ) .mockImplementation( ( value ) => Promise.resolve( value ) ); store = createReduxStore( initialState, reducer ); stopPersisting = persistOnChange( store, 123456789 ); } ); afterEach( () => { setStoredItemSpy.mockRestore(); } ); test( 'should persist state for first dispatch', () => { store.dispatch( { type: 'foo', data: 1, } ); jest.advanceTimersByTime( SERIALIZE_THROTTLE ); expect( setStoredItemSpy ).toHaveBeenCalledTimes( 1 ); } ); test( 'should persist state for changed state', () => { store.dispatch( { type: 'foo', data: 1, } ); jest.advanceTimersByTime( SERIALIZE_THROTTLE ); store.dispatch( { type: 'foo', data: 2, } ); jest.advanceTimersByTime( SERIALIZE_THROTTLE ); expect( setStoredItemSpy ).toHaveBeenCalledTimes( 2 ); } ); test( 'should not persist state for unchanged state', () => { store.dispatch( { type: 'foo', data: 1, } ); jest.advanceTimersByTime( SERIALIZE_THROTTLE ); store.dispatch( { type: 'foo', data: 1, } ); jest.advanceTimersByTime( SERIALIZE_THROTTLE ); expect( setStoredItemSpy ).toHaveBeenCalledTimes( 1 ); } ); test( 'should throttle', () => { store.dispatch( { type: 'foo', data: 1, } ); store.dispatch( { type: 'foo', data: 2, } ); store.dispatch( { type: 'foo', data: 3, } ); jest.advanceTimersByTime( SERIALIZE_THROTTLE ); store.dispatch( { type: 'foo', data: 4, } ); store.dispatch( { type: 'foo', data: 5, } ); jest.advanceTimersByTime( SERIALIZE_THROTTLE ); expect( setStoredItemSpy ).toHaveBeenCalledTimes( 2 ); expect( setStoredItemSpy ).toHaveBeenCalledWith( 'redux-state-123456789', expect.objectContaining( { data: 3 } ) ); expect( setStoredItemSpy ).toHaveBeenCalledWith( 'redux-state-123456789', expect.objectContaining( { data: 5 } ) ); } ); test( 'should not persist after calling the stop function', () => { store.dispatch( { type: 'foo', data: 1, } ); jest.advanceTimersByTime( SERIALIZE_THROTTLE ); expect( setStoredItemSpy ).toHaveBeenCalledTimes( 1 ); stopPersisting(); store.dispatch( { type: 'foo', data: 1, } ); jest.advanceTimersByTime( SERIALIZE_THROTTLE ); expect( setStoredItemSpy ).toHaveBeenCalledTimes( 1 ); // no new call } ); } ); } ); describe( 'loading stored state with dynamic reducers', () => { // Creates a reducer that serializes objects by prefixing all keys with a given prefix. // For example, `withKeyPrefix( 'A' )` serializes `{ x: 1, y: 2 }` into `{ 'A:x': 1, 'A:y': 2 }` const withKeyPrefix = ( keyPrefix ) => { const keyPrefixRe = new RegExp( `^${ keyPrefix }:` ); return withPersistence( ( state = {} ) => state, { serialize: ( state ) => Object.fromEntries( Object.entries( state ).map( ( [ key, value ] ) => [ `${ keyPrefix }:${ key }`, value ] ) ), deserialize: ( persisted ) => persisted ? Object.fromEntries( Object.entries( persisted ).map( ( [ key, value ] ) => [ key.replace( keyPrefixRe, '' ), value, ] ) ) : {}, } ); }; const currentUserReducer = withPersistence( ( state = { id: null } ) => state ); let getStoredItemSpy; beforeEach( () => { // state stored in IndexedDB const _timestamp = Date.now(); const storedState = { 'redux-state-123456789': { currentUser: { id: 123456789 }, _timestamp, }, 'redux-state-123456789:A': { 'A:city': 'London', 'A:country': 'UK', _timestamp, }, 'redux-state-123456789:B': { 'B:city': 'Paris', 'B:country': 'France', _timestamp, }, 'redux-state-123456789:CD': { 'CD:city': 'Lisbon', 'CD:country': 'Portugal', _timestamp, }, 'redux-state-123456789:E': { 'E:city': 'Madrid', 'E:country': 'Spain', _timestamp, }, }; // `lib/browser-storage` mock to return mock IndexedDB state getStoredItemSpy = jest .spyOn( browserStorage, 'getAllStoredItems' ) .mockResolvedValue( storedState ); } ); afterEach( () => { getStoredItemSpy.mockRestore(); } ); test( 'loads state from multiple storage keys', async () => { // initial reducer. includes several subreducers with storageKey property const reducer = combineReducers( { currentUser: currentUserReducer, a: withStorageKey( 'A', withKeyPrefix( 'A' ) ), b: withStorageKey( 'B', withKeyPrefix( 'B' ) ), } ); // load initial state and create Redux store with it await loadPersistedState(); const state = getInitialState( reducer, 123456789 ); const store = createReduxStore( state, reducer ); // verify that state from all storageKey's was loaded expect( store.getState() ).toEqual( { currentUser: { id: 123456789, }, a: { city: 'London', country: 'UK', }, b: { city: 'Paris', country: 'France', }, } ); } ); test( 'loads state after adding a reducer', async () => { // initial reducer. includes only the `currentUser` subreducer. const reducer = combineReducers( { currentUser: currentUserReducer, } ); // load initial state and create Redux store with it await loadPersistedState(); const userId = 123456789; const state = getInitialState( reducer, userId ); const store = createReduxStore( state, reducer ); // verify that the initial Redux store loaded state only for `currentUser` expect( store.getState() ).toEqual( { currentUser: { id: 123456789, }, } ); // load a reducer dynamically const aReducer = withStorageKey( 'A', withKeyPrefix( 'A' ) ); addReducerToStore( store, getStateFromCache( userId ) )( [ 'a' ], aReducer ); // verify that the Redux store contains the stored state for `A` now expect( store.getState() ).toEqual( { currentUser: { id: 123456789, }, a: { city: 'London', country: 'UK', }, } ); } ); test( 'loads state after adding a nested reducer', async () => { // initial reducer. includes only the `currentUser` subreducer. const reducer = combineReducers( { currentUser: currentUserReducer, } ); // load initial state and create Redux store with it await loadPersistedState(); const userId = 123456789; const state = getInitialState( reducer, userId ); const store = createReduxStore( state, reducer ); // verify that the initial Redux store loaded state only for `currentUser` expect( store.getState() ).toEqual( { currentUser: { id: 123456789, }, } ); // load a reducer dynamically const cdReducer = withStorageKey( 'CD', withKeyPrefix( 'CD' ) ); addReducerToStore( store, getStateFromCache( userId ) )( [ 'c', 'd' ], cdReducer ); // verify that the Redux store contains the stored state for `A` now expect( store.getState() ).toEqual( { currentUser: { id: 123456789, }, c: { d: { city: 'Lisbon', country: 'Portugal', }, }, } ); } ); test( 'loads state a single time after adding a reducer twice', async () => { // initial reducer. includes only the `currentUser` subreducer. const reducer = combineReducers( { currentUser: currentUserReducer, } ); // load initial state and create Redux store with it await loadPersistedState(); const userId = 123456789; const state = getInitialState( reducer, userId ); const store = createReduxStore( state, reducer ); // verify that the initial Redux store loaded state only for `currentUser` expect( store.getState() ).toEqual( { currentUser: { id: 123456789, }, } ); // load a reducer dynamically const eReducer = withStorageKey( 'E', withKeyPrefix( 'E' ) ); addReducerToStore( store, getStateFromCache( userId ) )( [ 'e' ], eReducer ); addReducerToStore( store, getStateFromCache( userId ) )( [ 'e' ], eReducer ); // verify that the Redux store contains the stored state for `E` now expect( store.getState() ).toEqual( { currentUser: { id: 123456789, }, e: { city: 'Madrid', country: 'Spain', }, } ); } ); test( 'throws an error when adding two different reducers to the same key', async () => { // initial reducer. includes only the `currentUser` subreducer. const reducer = combineReducers( { currentUser: currentUserReducer, } ); // load initial state and create Redux store with it await loadPersistedState(); const userId = 123456789; const state = getInitialState( reducer, userId ); const store = createReduxStore( state, reducer ); // verify that the initial Redux store loaded state only for `currentUser` expect( store.getState() ).toEqual( { currentUser: { id: 123456789, }, } ); // load a reducer dynamically const bReducer = withStorageKey( 'B', withKeyPrefix( 'B' ) ); const cReducer = withStorageKey( 'C', withKeyPrefix( 'C' ) ); expect( () => { addReducerToStore( store, getStateFromCache( userId ) )( [ 'b' ], bReducer ); addReducerToStore( store, getStateFromCache( userId ) )( [ 'b' ], cReducer ); } ).toThrow(); } ); } );