fix: prevent asset conflicts between React and Grid.js versions

Add coexistence checks to all enqueue methods to prevent loading
both React and Grid.js assets simultaneously.

Changes:
- ReactAdmin.php: Only enqueue React assets when ?react=1
- Init.php: Skip Grid.js when React active on admin pages
- Form.php, Coupon.php, Access.php: Restore classic assets when ?react=0
- Customer.php, Product.php, License.php: Add coexistence checks

Now the toggle between Classic and React versions works correctly.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
dwindown
2026-04-18 17:02:14 +07:00
parent bd9cdac02e
commit e8fbfb14c1
74973 changed files with 6658406 additions and 71 deletions

173
node_modules/@wordpress/data/src/test/controls.js generated vendored Normal file
View File

@@ -0,0 +1,173 @@
/**
* Internal dependencies
*/
import { createRegistry, controls } from '..';
describe( 'controls', () => {
// Create a registry with store to test select controls.
function createSelectTestRegistry() {
const registry = createRegistry();
// State is initially null and can receive data.
// Typical for fetching data from remote locations.
const reducer = ( state = null, action ) => {
switch ( action.type ) {
case 'RECEIVE':
return action.data;
default:
return state;
}
};
// Select state both without and with a resolver.
const selectors = {
selectorWithoutResolver: ( state ) => state,
selectorWithResolver: ( state ) => state,
};
// The resolver receives data after a little delay.
const resolvers = {
*selectorWithResolver() {
yield new Promise( ( r ) => setTimeout( r, 10 ) );
return { type: 'RECEIVE', data: 'resolved-data' };
},
};
// actions that call the tested controls and return the selected value.
const actions = {
*resolveWithoutResolver() {
const value = yield controls.resolveSelect(
'test/select',
'selectorWithoutResolver'
);
return value;
},
*resolveWithResolver() {
const value = yield controls.resolveSelect(
'test/select',
'selectorWithResolver'
);
return value;
},
*selectWithoutResolver() {
const value = yield controls.select(
'test/select',
'selectorWithoutResolver'
);
return value;
},
*selectWithResolver() {
const value = yield controls.select(
'test/select',
'selectorWithResolver'
);
return value;
},
};
registry.registerStore( 'test/select', {
reducer,
actions,
selectors,
resolvers,
} );
return registry;
}
describe( 'resolveSelect', () => {
it( 'invokes selector without a resolver', async () => {
const registry = createSelectTestRegistry();
const value = await registry
.dispatch( 'test/select' )
.resolveWithoutResolver();
// Returns the state value without waiting for any resolver.
expect( value ).toBe( null );
} );
it( 'resolves selector with a resolver', async () => {
const registry = createSelectTestRegistry();
const value = await registry
.dispatch( 'test/select' )
.resolveWithResolver();
// Waits for the resolver to resolve and returns the resolved data.
// Never returns the initial `null` state.
expect( value ).toBe( 'resolved-data' );
} );
} );
describe( 'select', () => {
it( 'invokes selector without a resolver', async () => {
const registry = createSelectTestRegistry();
const value = await registry
.dispatch( 'test/select' )
.selectWithoutResolver();
// Returns the state value without waiting for any resolver.
expect( value ).toBe( null );
} );
it( 'invokes selector with a resolver', async () => {
const registry = createSelectTestRegistry();
// Check that the action with a control returns the initial state
// without waiting for any resolver.
const value = await registry
.dispatch( 'test/select' )
.selectWithResolver();
expect( value ).toBe( null );
// Check that re-running the action immediately still returns
// the initial state, as the resolution is still running.
const value2 = await registry
.dispatch( 'test/select' )
.selectWithResolver();
expect( value2 ).toBe( null );
} );
} );
describe( 'dispatch', () => {
function createDispatchTestRegistry() {
const registry = createRegistry();
// Store stores a counter that can be incremented.
const reducer = ( state = 0, action ) => {
switch ( action.type ) {
case 'INC':
return state + 1;
default:
return state;
}
};
const actions = {
// increment the counter.
inc() {
return { type: 'INC' };
},
// Increment the counter twice in an async routine with controls.
*doubleInc() {
yield controls.dispatch( 'test/dispatch', 'inc' );
yield controls.dispatch( 'test/dispatch', 'inc' );
},
};
const selectors = {
get: ( state ) => state,
};
registry.registerStore( 'test/dispatch', {
reducer,
actions,
selectors,
} );
return registry;
}
it( 'invokes dispatch action', async () => {
const registry = createDispatchTestRegistry();
expect( registry.select( 'test/dispatch' ).get() ).toBe( 0 );
await registry.dispatch( 'test/dispatch' ).doubleInc();
expect( registry.select( 'test/dispatch' ).get() ).toBe( 2 );
} );
} );
} );

420
node_modules/@wordpress/data/src/test/privateAPIs.js generated vendored Normal file
View File

@@ -0,0 +1,420 @@
/**
* Internal dependencies
*/
import { createRegistry } from '../registry';
import createReduxStore from '../redux-store';
import { unlock } from '../lock-unlock';
import { createRegistrySelector } from '../factory';
describe( 'Private data APIs', () => {
let registry;
beforeEach( () => {
registry = createRegistry();
} );
function getPublicPrice( state ) {
return state.price;
}
function getSecretDiscount( state ) {
return state.secretDiscount;
}
function setSecretDiscount( price ) {
return { type: 'SET_PRIVATE_PRICE', price };
}
function setPublicPrice( price ) {
return { type: 'SET_PUBLIC_PRICE', price };
}
const storeName = 'grocer';
const storeDescriptor = {
selectors: {
getPublicPrice,
getState: ( state ) => state,
},
actions: { setPublicPrice },
reducer: ( state = { price: 1000, secretDiscount: 800 }, action ) => {
if ( action.type === 'SET_PRIVATE_PRICE' ) {
return {
...state,
secretDiscount: action.price,
};
} else if ( action.type === 'SET_PUBLIC_PRICE' ) {
return {
...state,
price: action.price,
};
}
return state;
},
};
function createStore() {
const groceryStore = createReduxStore( storeName, storeDescriptor );
registry.register( groceryStore );
return groceryStore;
}
describe( 'private selectors', () => {
it( 'should expose public selectors by default', () => {
const groceryStore = createStore();
unlock( groceryStore ).registerPrivateSelectors( groceryStore, {
getSecretDiscount,
} );
const publicSelectors = registry.select( groceryStore );
expect( publicSelectors.getPublicPrice ).toEqual(
expect.any( Function )
);
} );
it( 'should not expose private selectors by default', () => {
const groceryStore = createStore();
unlock( groceryStore ).registerPrivateSelectors( {
getSecretDiscount,
} );
const publicSelectors = registry.select( groceryStore );
expect( publicSelectors.getSecretDiscount ).toEqual( undefined );
} );
it( 'should make private selectors available via unlock()', () => {
const groceryStore = createStore();
unlock( groceryStore ).registerPrivateSelectors( {
getSecretDiscount,
} );
const privateSelectors = unlock( registry.select( groceryStore ) );
expect( privateSelectors.getSecretDiscount ).toEqual(
expect.any( Function )
);
// The public selector is still accessible:
expect( privateSelectors.getPublicPrice ).toEqual(
expect.any( Function )
);
} );
it( 'should support combination of private selectors and resolvers', async () => {
const testStore = createReduxStore( 'test', {
reducer: ( state = {}, action ) => {
if ( action.type === 'RECEIVE' ) {
return { ...state, [ action.key ]: action.value };
}
return state;
},
selectors: {
get: ( state, key ) => state[ key ],
},
resolvers: {
get:
( key ) =>
async ( { dispatch } ) => {
const value = await ( 'resolved-' + key );
dispatch( { type: 'RECEIVE', key, value } );
},
},
} );
unlock( testStore ).registerPrivateSelectors( {
peek: ( state, key ) => state[ key ],
} );
registry.register( testStore );
await registry.resolveSelect( testStore ).get( 'x' );
expect( unlock( registry.select( testStore ) ).peek( 'x' ) ).toBe(
'resolved-x'
);
} );
it( 'should give private selectors access to the state', () => {
const groceryStore = createStore();
unlock( groceryStore ).registerPrivateSelectors( {
getSecretDiscount,
} );
const privateSelectors = unlock( registry.select( groceryStore ) );
expect( privateSelectors.getSecretDiscount() ).toEqual( 800 );
} );
it( 'should support public selectors accessed via unlock()', () => {
const groceryStore = createStore();
unlock( groceryStore ).registerPrivateSelectors( {
getSecretDiscount,
} );
const unlockedSelectors = unlock( registry.select( groceryStore ) );
expect( unlockedSelectors.getPublicPrice() ).toEqual( 1000 );
} );
it( 'should return stable references to selectors', () => {
const groceryStore = createStore();
unlock( groceryStore ).registerPrivateSelectors( {
getSecretDiscount,
} );
const select = unlock( registry.select( groceryStore ) );
expect( select.getPublicPrice ).toBe( select.getPublicPrice );
expect( select.getSecretDiscount ).toBe( select.getSecretDiscount );
} );
it( 'should support registerStore', () => {
const groceryStore = registry.registerStore(
storeName,
storeDescriptor
);
unlock( groceryStore ).registerPrivateSelectors( {
getSecretDiscount,
} );
const privateSelectors = unlock( registry.select( storeName ) );
expect( privateSelectors.getSecretDiscount() ).toEqual( 800 );
} );
it( 'should support mixing createReduxStore and registerStore', () => {
createReduxStore( storeName, storeDescriptor );
const groceryStore2 = registry.registerStore(
storeName,
storeDescriptor
);
unlock( groceryStore2 ).registerPrivateSelectors( {
getSecretDiscount,
} );
const privateSelectors = unlock( registry.select( storeName ) );
expect( privateSelectors.getSecretDiscount() ).toEqual( 800 );
} );
it( 'should support sub registries', () => {
const groceryStore = registry.registerStore(
storeName,
storeDescriptor
);
unlock( groceryStore ).registerPrivateSelectors( {
getSecretDiscount,
} );
const subRegistry = createRegistry( {}, registry );
subRegistry.registerStore( storeName, storeDescriptor );
const parentPrivateSelectors = unlock(
registry.select( storeName )
);
expect( parentPrivateSelectors.getSecretDiscount() ).toEqual( 800 );
const subPrivateSelectors = unlock(
subRegistry.select( storeName )
);
expect( subPrivateSelectors.getSecretDiscount() ).toEqual( 800 );
} );
it( 'should support private registry selectors', () => {
const groceryStore = createStore();
const otherStore = createReduxStore( 'other', {
reducer: ( state = {} ) => state,
} );
unlock( otherStore ).registerPrivateSelectors( {
getPrice: createRegistrySelector(
( select ) => () => select( groceryStore ).getPublicPrice()
),
} );
registry.register( otherStore );
const privateSelectors = unlock( registry.select( otherStore ) );
expect( privateSelectors.getPrice() ).toEqual( 1000 );
} );
it( 'should support calling a private registry selector from a public selector', () => {
const groceryStore = createStore();
const getPriceWithShipping = createRegistrySelector(
( select ) => () => select( groceryStore ).getPublicPrice() + 5
);
const store = createReduxStore( 'a', {
reducer: ( state = {} ) => state,
selectors: {
getPriceWithShippingAndTax: ( state ) =>
getPriceWithShipping( state ) * 1.1,
},
} );
unlock( store ).registerPrivateSelectors( {
getPriceWithShipping,
} );
registry.register( store );
expect(
registry.select( store ).getPriceWithShippingAndTax()
).toEqual( 1105.5 );
} );
} );
describe( 'private actions', () => {
it( 'should expose public actions by default', () => {
const groceryStore = createStore();
unlock( groceryStore ).registerPrivateActions( groceryStore, {
setSecretDiscount,
} );
const publicActions = registry.dispatch( groceryStore );
expect( publicActions.setPublicPrice ).toEqual(
expect.any( Function )
);
} );
it( 'should not expose private actions by default', () => {
const groceryStore = createStore();
unlock( groceryStore ).registerPrivateActions( {
setSecretDiscount,
} );
const publicActions = registry.dispatch( groceryStore );
expect( publicActions.setSecretDiscount ).toEqual( undefined );
} );
it( 'should make private actions available via unlock)', () => {
const groceryStore = createStore();
unlock( groceryStore ).registerPrivateActions( {
setSecretDiscount,
} );
const privateActions = unlock( registry.dispatch( groceryStore ) );
expect( privateActions.setSecretDiscount ).toEqual(
expect.any( Function )
);
// The public action is still accessible:
expect( privateActions.setPublicPrice ).toEqual(
expect.any( Function )
);
} );
it( 'should work with both private actions and private selectors at the same time', () => {
const groceryStore = createStore();
unlock( groceryStore ).registerPrivateActions( {
setSecretDiscount,
} );
unlock( groceryStore ).registerPrivateSelectors( {
getSecretDiscount,
} );
const privateActions = unlock( registry.dispatch( groceryStore ) );
const privateSelectors = unlock( registry.select( groceryStore ) );
expect( privateActions.setSecretDiscount ).toEqual(
expect.any( Function )
);
expect( privateSelectors.getSecretDiscount ).toEqual(
expect.any( Function )
);
} );
it( 'should dispatch private actions like regular actions', () => {
const groceryStore = createStore();
unlock( groceryStore ).registerPrivateActions( {
setSecretDiscount,
} );
const privateActions = unlock( registry.dispatch( groceryStore ) );
privateActions.setSecretDiscount( 400 );
expect(
registry.select( groceryStore ).getState().secretDiscount
).toEqual( 400 );
} );
it( 'should return stable references to actions', () => {
const groceryStore = createStore();
unlock( groceryStore ).registerPrivateActions( {
setSecretDiscount,
} );
const disp = unlock( registry.dispatch( groceryStore ) );
expect( disp.setPublicPrice ).toBe( disp.setPublicPrice );
expect( disp.setSecretDiscount ).toBe( disp.setSecretDiscount );
} );
it( 'should dispatch public actions on the unlocked store', () => {
const groceryStore = createStore();
unlock( groceryStore ).registerPrivateActions( {
setSecretDiscount,
} );
const privateActions = unlock( registry.dispatch( groceryStore ) );
privateActions.setPublicPrice( 400 );
expect( registry.select( groceryStore ).getState().price ).toEqual(
400
);
} );
it( 'should dispatch private action thunks like regular actions', () => {
const groceryStore = createStore();
unlock( groceryStore ).registerPrivateActions( {
setSecretDiscountThunk:
( price ) =>
( { dispatch } ) => {
dispatch( { type: 'SET_PRIVATE_PRICE', price } );
},
} );
unlock( groceryStore ).registerPrivateSelectors( {
getSecretDiscount,
} );
const privateActions = unlock( registry.dispatch( groceryStore ) );
privateActions.setSecretDiscountThunk( 100 );
expect(
unlock( registry.select( groceryStore ) ).getSecretDiscount()
).toEqual( 100 );
} );
it( 'should expose unlocked private selectors and actions to thunks', () => {
const groceryStore = createStore();
unlock( groceryStore ).registerPrivateSelectors( {
getSecretDiscount,
} );
unlock( groceryStore ).registerPrivateActions( {
setSecretDiscount,
doubleSecretDiscount() {
return ( { dispatch, select } ) => {
dispatch.setSecretDiscount(
select.getSecretDiscount() * 2
);
};
},
} );
const privateActions = unlock( registry.dispatch( groceryStore ) );
privateActions.setSecretDiscount( 100 );
privateActions.doubleSecretDiscount();
expect(
unlock( registry.select( groceryStore ) ).getSecretDiscount()
).toEqual( 200 );
} );
it( 'should support registerStore', () => {
const groceryStore = registry.registerStore(
storeName,
storeDescriptor
);
unlock( groceryStore ).registerPrivateActions( {
setSecretDiscount,
} );
const privateActions = unlock( registry.dispatch( storeName ) );
privateActions.setSecretDiscount( 400 );
expect(
registry.select( storeName ).getState().secretDiscount
).toEqual( 400 );
} );
it( 'should support sub registries', () => {
const groceryStore = createStore();
unlock( groceryStore ).registerPrivateSelectors( {
getSecretDiscount,
} );
unlock( groceryStore ).registerPrivateActions( {
setSecretDiscount,
} );
const subRegistry = createRegistry( {}, registry );
subRegistry.registerStore( storeName, storeDescriptor );
const parentPrivateActions = unlock(
registry.dispatch( storeName )
);
const parentPrivateSelectors = unlock(
registry.select( storeName )
);
const subPrivateActions = unlock(
subRegistry.dispatch( storeName )
);
const subPrivateSelectors = unlock(
subRegistry.select( storeName )
);
parentPrivateActions.setSecretDiscount( 400 );
subPrivateActions.setSecretDiscount( 478 );
expect( parentPrivateSelectors.getSecretDiscount() ).toEqual( 400 );
expect( subPrivateSelectors.getSecretDiscount() ).toEqual( 478 );
} );
} );
} );

View File

@@ -0,0 +1,150 @@
/**
* Internal dependencies
*/
import { createRegistry } from '../registry';
import { createRegistrySelector } from '../factory';
import { createSelector } from '..';
import createReduxStore from '../redux-store';
const getElementCount = createRegistrySelector( ( select ) => () => {
return select( 'elements' ).getElements().length;
} );
const getFilterValue = ( state ) => state;
const getFilteredElements = createRegistrySelector( ( select ) =>
createSelector(
( state ) => {
const filter = getFilterValue( state );
const elements = select( 'elements' ).getElements();
if ( ! filter ) {
return elements;
}
return elements.filter( ( el ) => el.startsWith( filter ) );
},
( state ) => [
select( 'elements' ).getElements(),
getFilterValue( state ),
]
)
);
const elementsStore = createReduxStore( 'elements', {
reducer( state = [], action ) {
if ( action.type === 'ADD' ) {
return [ ...state, ...action.elements ];
}
return state;
},
actions: {
add: ( ...elements ) => ( { type: 'ADD', elements } ),
},
selectors: {
getElements: ( state ) => state,
},
} );
const uiStore = createReduxStore( 'ui', {
reducer( state = '', action ) {
if ( action.type === 'FILTER' ) {
return action.value;
}
return state;
},
actions: {
filter: ( value ) => ( { type: 'FILTER', value } ),
},
selectors: {
getFilterValue,
getElementCount,
getFilteredElements,
},
} );
describe( 'createRegistrySelector', () => {
it( 'can bind one selector to a registry', () => {
const registry = createRegistry();
registry.register( elementsStore );
registry.register( uiStore );
expect( registry.select( uiStore ).getElementCount() ).toBe( 0 );
registry.dispatch( elementsStore ).add( 'Carbon' );
expect( registry.select( uiStore ).getElementCount() ).toBe( 1 );
} );
it( 'can bind one selector to multiple registries (createRegistrySelector)', () => {
const registry1 = createRegistry();
const registry2 = createRegistry();
registry1.register( elementsStore );
registry1.register( uiStore );
registry2.register( elementsStore );
registry2.register( uiStore );
expect( registry1.select( uiStore ).getElementCount() ).toBe( 0 );
expect( registry2.select( uiStore ).getElementCount() ).toBe( 0 );
registry1.dispatch( elementsStore ).add( 'Carbon' );
expect( registry1.select( uiStore ).getElementCount() ).toBe( 1 );
expect( registry2.select( uiStore ).getElementCount() ).toBe( 0 );
registry2.dispatch( elementsStore ).add( 'Helium' );
expect( registry1.select( uiStore ).getElementCount() ).toBe( 1 );
expect( registry2.select( uiStore ).getElementCount() ).toBe( 1 );
} );
it( 'can bind one selector to multiple registries (createRegistrySelector + createSelector)', () => {
const registry1 = createRegistry();
registry1.register( elementsStore );
registry1.register( uiStore );
registry1.dispatch( elementsStore ).add( 'Carbon' );
const registry2 = createRegistry();
registry2.register( elementsStore );
registry2.register( uiStore );
registry2.dispatch( elementsStore ).add( 'Helium' );
// Expects the `getFilteredElements` to be bound separately and independently to the two registries
expect( registry1.select( uiStore ).getFilteredElements() ).toEqual( [
'Carbon',
] );
expect( registry2.select( uiStore ).getFilteredElements() ).toEqual( [
'Helium',
] );
} );
it( 'can bind a memoized selector to a registry', () => {
const registry = createRegistry();
registry.register( elementsStore );
registry.register( uiStore );
registry.dispatch( elementsStore ).add( 'Carbon', 'Nitrogen' );
registry.dispatch( uiStore ).filter( 'Ca' );
// check that `getFilteredElements` return value is a memoized array
const elements1 = registry.select( uiStore ).getFilteredElements();
expect( elements1 ).toEqual( [ 'Carbon' ] );
const elements2 = registry.select( uiStore ).getFilteredElements();
expect( elements2 ).toBe( elements1 ); // memoized
// check that adding a new element triggers recalculation
registry.dispatch( elementsStore ).add( 'Calcium' );
const elements3 = registry.select( uiStore ).getFilteredElements();
expect( elements3 ).toEqual( [ 'Carbon', 'Calcium' ] );
const elements4 = registry.select( uiStore ).getFilteredElements();
expect( elements4 ).toBe( elements3 ); // memoized
// check that changing the filter triggers recalculation
registry.dispatch( uiStore ).filter( 'Ni' );
const elements5 = registry.select( uiStore ).getFilteredElements();
expect( elements5 ).toEqual( [ 'Nitrogen' ] );
const elements6 = registry.select( uiStore ).getFilteredElements();
expect( elements6 ).toBe( elements5 ); // memoized
} );
} );

872
node_modules/@wordpress/data/src/test/registry.js generated vendored Normal file
View File

@@ -0,0 +1,872 @@
/* eslint jest/expect-expect: ["warn", { "assertFunctionNames": ["expect", "subscribeUntil"] }] */
/**
* Internal dependencies
*/
import { createRegistry } from '../registry';
import { createRegistrySelector } from '../factory';
import createReduxStore from '../redux-store';
import coreDataStore from '../store';
jest.useFakeTimers( { legacyFakeTimers: true } );
describe( 'createRegistry', () => {
let registry;
const unsubscribes = [];
function subscribeWithUnsubscribe( ...args ) {
const unsubscribe = registry.subscribe( ...args );
unsubscribes.push( unsubscribe );
return unsubscribe;
}
function subscribeUntil( predicates ) {
predicates = Array.from( predicates );
return new Promise( ( resolve ) => {
subscribeWithUnsubscribe( () => {
if ( predicates.every( ( predicate ) => predicate() ) ) {
resolve();
}
} );
} );
}
beforeEach( () => {
registry = createRegistry();
} );
afterEach( () => {
let unsubscribe;
while ( ( unsubscribe = unsubscribes.shift() ) ) {
unsubscribe();
}
} );
describe( 'registerGenericStore', () => {
let getSelectors;
let getActions;
let subscribe;
beforeEach( () => {
getSelectors = () => ( {} );
getActions = () => ( {} );
subscribe = () => ( {} );
} );
it( 'should throw if not all required config elements are present', () => {
expect( () =>
registry.registerGenericStore( 'grocer', {} )
).toThrow();
expect( () =>
registry.registerGenericStore( 'grocer', {
getSelectors,
getActions,
} )
).toThrow();
expect( () =>
registry.registerGenericStore( 'grocer', {
getActions,
subscribe,
} )
).toThrow();
expect( console ).toHaveWarned();
} );
describe( 'getSelectors', () => {
it( 'should make selectors available via registry.select', () => {
const items = {
broccoli: { price: 2, quantity: 15 },
lettuce: { price: 1, quantity: 12 },
};
function getPrice( itemName ) {
const item = items[ itemName ];
return item && item.price;
}
function getQuantity( itemName ) {
const item = items[ itemName ];
return item && item.quantity;
}
getSelectors = () => ( { getPrice, getQuantity } );
registry.registerGenericStore( 'grocer', {
getSelectors,
getActions,
subscribe,
} );
expect( registry.select( 'grocer' ).getPrice ).toEqual(
getPrice
);
expect( registry.select( 'grocer' ).getQuantity ).toEqual(
getQuantity
);
} );
} );
describe( 'getActions', () => {
it( 'should make actions available via registry.dispatch', () => {
const dispatch = jest.fn();
function setPrice( itemName, price ) {
return { type: 'SET_PRICE', itemName, price };
}
function setQuantity( itemName, quantity ) {
return { type: 'SET_QUANTITY', itemName, quantity };
}
getActions = () => {
return {
setPrice: ( ...args ) =>
dispatch( setPrice( ...args ) ),
setQuantity: ( ...args ) =>
dispatch( setQuantity( ...args ) ),
};
};
registry.registerGenericStore( 'grocer', {
getSelectors,
getActions,
subscribe,
} );
expect( dispatch ).not.toHaveBeenCalled();
registry.dispatch( 'grocer' ).setPrice( 'broccoli', 3 );
expect( dispatch ).toHaveBeenCalledTimes( 1 );
expect( dispatch ).toHaveBeenCalledWith( {
type: 'SET_PRICE',
itemName: 'broccoli',
price: 3,
} );
registry.dispatch( 'grocer' ).setQuantity( 'lettuce', 8 );
expect( dispatch ).toHaveBeenCalledTimes( 2 );
expect( dispatch ).toHaveBeenCalledWith( {
type: 'SET_QUANTITY',
itemName: 'lettuce',
quantity: 8,
} );
} );
} );
describe( 'subscribe', () => {
it( 'should send out updates to listeners of the registry', () => {
const registryListener = jest.fn();
let listener = () => {};
const storeChanged = () => {
listener();
};
subscribe = ( newListener ) => {
listener = newListener;
};
const unsubscribe = registry.subscribe( registryListener );
registry.registerGenericStore( 'grocer', {
getSelectors,
getActions,
subscribe,
} );
expect( registryListener ).not.toHaveBeenCalled();
storeChanged();
expect( registryListener ).toHaveBeenCalledTimes( 1 );
storeChanged();
expect( registryListener ).toHaveBeenCalledTimes( 2 );
unsubscribe();
storeChanged();
expect( registryListener ).toHaveBeenCalledTimes( 2 );
} );
} );
} );
describe( 'registerStore', () => {
it( 'should be shorthand for reducer, actions, selectors registration', () => {
const store = registry.registerStore( 'butcher', {
reducer( state = {}, action ) {
switch ( action.type ) {
case 'sale':
return {
...state,
[ action.meat ]: state[ action.meat ] / 2,
};
}
return state;
},
initialState: { ribs: 6, chicken: 4 },
selectors: {
getPrice: ( state, meat ) => state[ meat ],
},
actions: {
startSale: ( meat ) => ( { type: 'sale', meat } ),
},
} );
expect( store.getState() ).toEqual( { ribs: 6, chicken: 4 } );
expect( registry.dispatch( 'butcher' ) ).toHaveProperty(
'startSale'
);
expect( registry.select( 'butcher' ) ).toHaveProperty( 'getPrice' );
expect( registry.select( 'butcher' ).getPrice( 'chicken' ) ).toBe(
4
);
expect( registry.select( 'butcher' ).getPrice( 'ribs' ) ).toBe( 6 );
registry.dispatch( 'butcher' ).startSale( 'chicken' );
expect( registry.select( 'butcher' ).getPrice( 'chicken' ) ).toBe(
2
);
expect( registry.select( 'butcher' ).getPrice( 'ribs' ) ).toBe( 6 );
} );
it( 'Should append reducers to the state', () => {
const reducer1 = () => 'chicken';
const reducer2 = () => 'ribs';
const store = registry.registerStore( 'red1', {
reducer: reducer1,
} );
expect( store.getState() ).toEqual( 'chicken' );
const store2 = registry.registerStore( 'red2', {
reducer: reducer2,
} );
expect( store2.getState() ).toEqual( 'ribs' );
} );
it( 'should not do anything for selectors which do not have resolvers', () => {
registry.registerStore( 'demo', {
reducer: ( state = 'OK' ) => state,
selectors: {
getValue: ( state ) => state,
},
resolvers: {},
} );
expect( registry.select( 'demo' ).getValue() ).toBe( 'OK' );
} );
it( 'should behave as a side effect for the given selector, with arguments', () => {
const resolver = jest.fn();
registry.registerStore( 'demo', {
reducer: ( state = 'OK' ) => state,
selectors: {
getValue: ( state ) => state,
},
resolvers: {
getValue: resolver,
},
} );
const value = registry.select( 'demo' ).getValue( 'arg1', 'arg2' );
jest.runAllTimers();
expect( value ).toBe( 'OK' );
expect( resolver ).toHaveBeenCalledWith( 'arg1', 'arg2' );
registry.select( 'demo' ).getValue( 'arg1', 'arg2' );
jest.runAllTimers();
expect( resolver ).toHaveBeenCalledTimes( 1 );
registry.select( 'demo' ).getValue( 'arg3', 'arg4' );
jest.runAllTimers();
expect( resolver ).toHaveBeenCalledTimes( 2 );
} );
it( 'should support the object resolver descriptor', () => {
const resolver = jest.fn();
registry.registerStore( 'demo', {
reducer: ( state = 'OK' ) => state,
selectors: {
getValue: ( state ) => state,
},
resolvers: {
getValue: { fulfill: resolver },
},
} );
const value = registry.select( 'demo' ).getValue( 'arg1', 'arg2' );
jest.runAllTimers();
expect( value ).toBe( 'OK' );
} );
it( 'should use isFulfilled definition before calling the side effect', () => {
const fulfill = jest.fn().mockImplementation( ( state, page ) => {
return { type: 'SET_PAGE', page, result: [] };
} );
const store = registry.registerStore( 'demo', {
reducer: ( state = {}, action ) => {
switch ( action.type ) {
case 'SET_PAGE':
return {
...state,
[ action.page ]: action.result,
};
}
return state;
},
selectors: {
getPage: ( state, page ) => state[ page ],
},
resolvers: {
getPage: {
fulfill,
isFulfilled( state, page ) {
return state.hasOwnProperty( page );
},
},
},
} );
store.dispatch( { type: 'SET_PAGE', page: 4, result: [] } );
registry.select( 'demo' ).getPage( 1 );
jest.runAllTimers();
registry.select( 'demo' ).getPage( 2 );
jest.runAllTimers();
expect( fulfill ).toHaveBeenCalledTimes( 2 );
registry.select( 'demo' ).getPage( 1 );
jest.runAllTimers();
registry.select( 'demo' ).getPage( 2 );
jest.runAllTimers();
registry.select( 'demo' ).getPage( 3, {} );
jest.runAllTimers();
// Expected: First and second page fulfillments already triggered, so
// should only be one more than previous assertion set.
expect( fulfill ).toHaveBeenCalledTimes( 3 );
registry.select( 'demo' ).getPage( 1 );
jest.runAllTimers();
registry.select( 'demo' ).getPage( 2 );
jest.runAllTimers();
registry.select( 'demo' ).getPage( 3, {} );
jest.runAllTimers();
registry.select( 'demo' ).getPage( 4 );
// Expected:
// - Fourth page was pre-filled. Necessary to determine via
// isFulfilled, but fulfillment resolver should not be triggered.
// - Third page arguments are not strictly equal but are equivalent,
// so fulfillment should already be satisfied.
expect( fulfill ).toHaveBeenCalledTimes( 3 );
registry.select( 'demo' ).getPage( 4, {} );
} );
it( 'should resolve action to dispatch', () => {
registry.registerStore( 'demo', {
reducer: ( state = 'NOTOK', action ) => {
return action.type === 'SET_OK' ? 'OK' : state;
},
selectors: {
getValue: ( state ) => state,
},
resolvers: {
getValue: () => ( { type: 'SET_OK' } ),
},
} );
const promise = subscribeUntil( [
() => registry.select( 'demo' ).getValue() === 'OK',
() =>
registry
.select( coreDataStore )
.hasFinishedResolution( 'demo', 'getValue' ),
] );
registry.select( 'demo' ).getValue();
jest.runAllTimers();
return promise;
} );
it( 'should resolve promise action to dispatch', () => {
registry.registerStore( 'demo', {
reducer: ( state = 'NOTOK', action ) => {
return action.type === 'SET_OK' ? 'OK' : state;
},
selectors: {
getValue: ( state ) => state,
},
resolvers: {
getValue: () => Promise.resolve( { type: 'SET_OK' } ),
},
} );
const promise = subscribeUntil( [
() => registry.select( 'demo' ).getValue() === 'OK',
() =>
registry
.select( coreDataStore )
.hasFinishedResolution( 'demo', 'getValue' ),
] );
registry.select( 'demo' ).getValue();
jest.runAllTimers();
return promise;
} );
it( 'should not dispatch resolved promise action on subsequent selector calls', () => {
registry.registerStore( 'demo', {
reducer: ( state = 'NOTOK', action ) => {
return action.type === 'SET_OK' && state === 'NOTOK'
? 'OK'
: 'NOTOK';
},
selectors: {
getValue: ( state ) => state,
},
resolvers: {
getValue: () => Promise.resolve( { type: 'SET_OK' } ),
},
} );
const promise = subscribeUntil(
() => registry.select( 'demo' ).getValue() === 'OK'
);
registry.select( 'demo' ).getValue();
jest.runAllTimers();
registry.select( 'demo' ).getValue();
jest.runAllTimers();
return promise;
} );
it( "should invalidate the resolver's resolution cache", async () => {
registry.registerStore( 'demo', {
reducer: ( state = 'NOTOK', action ) => {
return action.type === 'SET_OK' && state === 'NOTOK'
? 'OK'
: 'NOTOK';
},
selectors: {
getValue: ( state ) => state,
},
resolvers: {
getValue: {
fulfill: () => Promise.resolve( { type: 'SET_OK' } ),
shouldInvalidate: ( action ) =>
action.type === 'INVALIDATE',
},
},
actions: {
invalidate: () => ( { type: 'INVALIDATE' } ),
},
} );
let promise = subscribeUntil(
() => registry.select( 'demo' ).getValue() === 'OK'
);
registry.select( 'demo' ).getValue(); // Triggers resolver switches to OK.
jest.runAllTimers();
await promise;
// Invalidate the cache
registry.dispatch( 'demo' ).invalidate();
promise = subscribeUntil(
() => registry.select( 'demo' ).getValue() === 'NOTOK'
);
registry.select( 'demo' ).getValue(); // Triggers the resolver again and switch to NOTOK.
jest.runAllTimers();
await promise;
} );
} );
describe( 'register', () => {
const store = createReduxStore( 'demo', {
reducer( state = 'OK', action ) {
if ( action.type === 'UPDATE' ) {
return 'UPDATED';
}
return state;
},
actions: {
update: () => ( { type: 'UPDATE' } ),
},
selectors: {
getValue: ( state ) => state,
},
} );
it( 'should work with the store descriptor as param for select', () => {
registry.register( store );
expect( registry.select( store ).getValue() ).toBe( 'OK' );
} );
it( 'should work with the store descriptor as param for dispatch', async () => {
registry.register( store );
expect( registry.select( store ).getValue() ).toBe( 'OK' );
await registry.dispatch( store ).update();
expect( registry.select( store ).getValue() ).toBe( 'UPDATED' );
} );
it( 'should keep the existing store instance on duplicate registration', async () => {
registry.register( store );
await registry.dispatch( store ).update();
expect( registry.select( store ).getValue() ).toBe( 'UPDATED' );
registry.register( store );
// check that the state hasn't been reset back to `OK`, as a re-registration would do
expect( registry.select( store ).getValue() ).toBe( 'UPDATED' );
expect( console ).toHaveErroredWith(
'Store "demo" is already registered.'
);
} );
} );
describe( 'select', () => {
it( 'registers multiple selectors to the public API', () => {
const selector1 = jest.fn( () => 'result1' );
const selector2 = jest.fn( () => 'result2' );
const store = registry.registerStore( 'reducer1', {
reducer: () => 'state1',
selectors: {
selector1,
selector2,
},
} );
expect( registry.select( 'reducer1' ).selector1() ).toEqual(
'result1'
);
expect( selector1 ).toHaveBeenCalledWith( store.getState() );
expect( registry.select( 'reducer1' ).selector2() ).toEqual(
'result2'
);
expect( selector2 ).toHaveBeenCalledWith( store.getState() );
} );
it( 'should run the registry selectors properly', () => {
const selector1 = () => 'result1';
const selector2 = createRegistrySelector(
( select ) => () => select( 'reducer1' ).selector1()
);
registry.registerStore( 'reducer1', {
reducer: () => 'state1',
selectors: {
selector1,
},
} );
registry.registerStore( 'reducer2', {
reducer: () => 'state1',
selectors: {
selector2,
},
} );
expect( registry.select( 'reducer2' ).selector2() ).toEqual(
'result1'
);
} );
it( 'should run the registry selector from a non-registry selector', () => {
const selector1 = () => 'result1';
const selector2 = createRegistrySelector(
( select ) => () => select( 'reducer1' ).selector1()
);
const selector3 = () => selector2();
registry.registerStore( 'reducer1', {
reducer: () => 'state1',
selectors: {
selector1,
},
} );
registry.registerStore( 'reducer2', {
reducer: () => 'state1',
selectors: {
selector2,
selector3,
},
} );
expect( registry.select( 'reducer2' ).selector3() ).toEqual(
'result1'
);
} );
} );
describe( 'subscribe', () => {
it( 'registers multiple selectors to the public API', () => {
let incrementedValue = null;
const store = registry.registerStore( 'myAwesomeReducer', {
reducer: ( state = 0 ) => state + 1,
selectors: {
globalSelector: ( state ) => state,
},
} );
const unsubscribe = registry.subscribe( () => {
incrementedValue = registry
.select( 'myAwesomeReducer' )
.globalSelector();
} );
const action = { type: 'dummy' };
store.dispatch( action ); // Increment the data by => data = 2.
expect( incrementedValue ).toBe( 2 );
store.dispatch( action ); // Increment the data by => data = 3.
expect( incrementedValue ).toBe( 3 );
unsubscribe(); // Store subscribe to changes, the data variable stops upgrading.
store.dispatch( action );
store.dispatch( action );
expect( incrementedValue ).toBe( 3 );
} );
it( 'snapshots listeners on change, avoiding a later listener if subscribed during earlier callback', () => {
const store = registry.registerStore( 'myAwesomeReducer', {
reducer: ( state = 0 ) => state + 1,
} );
const secondListener = jest.fn();
const firstListener = jest.fn( () => {
subscribeWithUnsubscribe( secondListener );
} );
subscribeWithUnsubscribe( firstListener );
store.dispatch( { type: 'dummy' } );
expect( secondListener ).not.toHaveBeenCalled();
} );
it( 'snapshots listeners on change, calling a later listener even if unsubscribed during earlier callback', () => {
const store = registry.registerStore( 'myAwesomeReducer', {
reducer: ( state = 0 ) => state + 1,
} );
const firstListener = jest.fn( () => {
secondUnsubscribe();
} );
const secondListener = jest.fn();
subscribeWithUnsubscribe( firstListener );
const secondUnsubscribe =
subscribeWithUnsubscribe( secondListener );
store.dispatch( { type: 'dummy' } );
expect( secondListener ).toHaveBeenCalled();
} );
it( 'does not call listeners if state has not changed', () => {
const store = registry.registerStore( 'unchanging', {
reducer: ( state = {} ) => state,
} );
const listener = jest.fn();
subscribeWithUnsubscribe( listener );
store.dispatch( { type: 'dummy' } );
expect( listener ).not.toHaveBeenCalled();
} );
} );
describe( 'dispatch', () => {
it( 'registers actions to the public API', async () => {
const increment = ( count = 1 ) => ( { type: 'increment', count } );
const store = registry.registerStore( 'counter', {
reducer: ( state = 0, action ) => {
if ( action.type === 'increment' ) {
return state + action.count;
}
return state;
},
actions: {
increment,
},
} );
// State = 1.
const dispatchResult = await registry
.dispatch( 'counter' )
.increment();
await expect( dispatchResult ).toEqual( {
type: 'increment',
count: 1,
} );
registry.dispatch( 'counter' ).increment( 4 ); // State = 5.
expect( store.getState() ).toBe( 5 );
} );
} );
describe( 'batch', () => {
it( 'should batch callbacks and only run the subscriber once', () => {
const store = registry.registerStore( 'myAwesomeReducer', {
reducer: ( state = 0 ) => state + 1,
} );
const listener = jest.fn();
subscribeWithUnsubscribe( listener );
registry.batch( () => {} );
expect( listener ).not.toHaveBeenCalled();
registry.batch( () => {
store.dispatch( { type: 'dummy' } );
store.dispatch( { type: 'dummy' } );
} );
expect( listener ).toHaveBeenCalledTimes( 1 );
const listener2 = jest.fn();
// useSelect subscribes to the stores differently,
// This test ensures batching works in this case as well.
const unsubscribe = registry.subscribe(
listener2,
'myAwesomeReducer'
);
registry.batch( () => {
store.dispatch( { type: 'dummy' } );
store.dispatch( { type: 'dummy' } );
} );
unsubscribe();
expect( listener2 ).toHaveBeenCalledTimes( 1 );
} );
it( 'should support nested batches', () => {
const store = registry.registerStore( 'myAwesomeReducer', {
reducer: ( state = 0 ) => state + 1,
} );
const listener = jest.fn();
subscribeWithUnsubscribe( listener );
registry.batch( () => {} );
expect( listener ).not.toHaveBeenCalled();
registry.batch( () => {
store.dispatch( { type: 'dummy' } );
registry.batch( () => {
store.dispatch( { type: 'dummy' } );
store.dispatch( { type: 'dummy' } );
} );
store.dispatch( { type: 'dummy' } );
} );
expect( listener ).toHaveBeenCalledTimes( 1 );
} );
} );
describe( 'use', () => {
it( 'should pass through options object to plugin', () => {
const expectedOptions = {};
const anyObject = expect.any( Object );
let actualOptions;
function plugin( _registry, options ) {
// The registry passed to a plugin is not the same as the one
// returned by createRegistry, as the former uses the internal
// representation of the object, the latter applying its
// function proxying.
expect( _registry ).toMatchObject(
Object.fromEntries(
Object.entries( registry ).map( ( [ key ] ) => {
if ( key === 'stores' ) {
return [ key, anyObject ];
}
// TODO: Remove this after namsespaces is removed.
if ( key === 'namespaces' ) {
return [ key, registry.stores ];
}
return [ key, expect.any( Function ) ];
} )
)
);
actualOptions = options;
return {};
}
registry.use( plugin, expectedOptions );
expect( actualOptions ).toBe( expectedOptions );
} );
it( 'should override base method', () => {
function plugin( _registry, options ) {
return { select: () => options.value };
}
registry.use( plugin, { value: 10 } );
expect( registry.select() ).toBe( 10 );
} );
} );
describe( 'parent registry', () => {
it( 'should call parent registry selectors/actions if defined', () => {
const mySelector = jest.fn();
const myAction = jest.fn();
const getSelectors = () => ( { mySelector } );
const getActions = () => ( { myAction } );
const subscribe = () => {};
const myStore = {
name: 'store',
instantiate: () => ( {
getSelectors,
getActions,
subscribe,
} ),
};
registry.register( myStore );
const subRegistry = createRegistry( {}, registry );
subRegistry.select( myStore ).mySelector();
subRegistry.dispatch( myStore ).myAction();
expect( mySelector ).toHaveBeenCalled();
expect( myAction ).toHaveBeenCalled();
} );
it( 'should override existing store in parent registry', () => {
const mySelector = jest.fn();
const myAction = jest.fn();
const getSelectors = () => ( { mySelector } );
const getActions = () => ( { myAction } );
const subscribe = () => {};
registry.register( {
name: 'store',
instantiate: () => ( {
getSelectors,
getActions,
subscribe,
} ),
} );
const subRegistry = createRegistry( {}, registry );
const mySelector2 = jest.fn();
const myAction2 = jest.fn();
const getSelectors2 = () => ( { mySelector: mySelector2 } );
const getActions2 = () => ( { myAction: myAction2 } );
const subscribe2 = () => {};
subRegistry.register( {
name: 'store',
instantiate: () => ( {
getSelectors: getSelectors2,
getActions: getActions2,
subscribe: subscribe2,
} ),
} );
subRegistry.select( 'store' ).mySelector();
subRegistry.dispatch( 'store' ).myAction();
expect( mySelector ).not.toHaveBeenCalled();
expect( myAction ).not.toHaveBeenCalled();
expect( mySelector2 ).toHaveBeenCalled();
expect( myAction2 ).toHaveBeenCalled();
} );
} );
} );