Skip to content

Commit

Permalink
Fix integration tests
Browse files Browse the repository at this point in the history
  • Loading branch information
adamziel committed Sep 6, 2021
1 parent 351e52c commit 0bc4da8
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 93 deletions.
41 changes: 24 additions & 17 deletions packages/core-data/src/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { find, includes, get, hasIn, compact, uniq } from 'lodash';
import { addQueryArgs } from '@wordpress/url';
import { controls } from '@wordpress/data';
import { apiFetch } from '@wordpress/data-controls';
import triggerFetch from '@wordpress/api-fetch';

/**
* Internal dependencies
*/
Expand Down Expand Up @@ -159,16 +161,24 @@ export const getEditedEntityRecord = ifNotResolved(
* @param {string} name Entity name.
* @param {Object?} query Query Object.
*/
export function* getEntityRecords( kind, name, query = {} ) {
const entities = yield getKindEntities( kind );

/**
* Requests the entity's records from the REST API.
*
* @param {string} kind Entity kind.
* @param {string} name Entity name.
* @param {Object?} query Query Object.
*/
export const getEntityRecords = ( kind, name, query = {} ) => async ( {
dispatch,
} ) => {
const entities = await dispatch( getKindEntities( kind ) );
const entity = find( entities, { kind, name } );
if ( ! entity ) {
return;
}

const lock = yield controls.dispatch(
STORE_NAME,
'__unstableAcquireStoreLock',
const lock = await dispatch.__unstableAcquireStoreLock(
STORE_NAME,
[ 'entities', 'data', kind, name ],
{ exclusive: false }
Expand All @@ -193,7 +203,7 @@ export function* getEntityRecords( kind, name, query = {} ) {
...query,
} );

let records = Object.values( yield apiFetch( { path } ) );
let records = Object.values( await triggerFetch( { path } ) );
// If we request fields but the result doesn't contain the fields,
// explicitely set these fields as "undefined"
// that way we consider the query "fullfilled".
Expand All @@ -209,7 +219,8 @@ export function* getEntityRecords( kind, name, query = {} ) {
} );
}

yield receiveEntityRecords( kind, name, records, query );
dispatch.receiveEntityRecords( kind, name, records, query );

// When requesting all fields, the list of results can be used to
// resolve the `getEntityRecord` selector in addition to `getEntityRecords`.
// See https://github.com/WordPress/gutenberg/pull/26575
Expand All @@ -219,25 +230,21 @@ export function* getEntityRecords( kind, name, query = {} ) {
.filter( ( record ) => record[ key ] )
.map( ( record ) => [ kind, name, record[ key ] ] );

yield {
dispatch( {
type: 'START_RESOLUTIONS',
selectorName: 'getEntityRecord',
args: resolutionsArgs,
};
yield {
} );
dispatch( {
type: 'FINISH_RESOLUTIONS',
selectorName: 'getEntityRecord',
args: resolutionsArgs,
};
} );
}
} finally {
yield controls.dispatch(
STORE_NAME,
'__unstableReleaseStoreLock',
lock
);
dispatch.__unstableReleaseStoreLock( lock );
}
}
};

getEntityRecords.shouldInvalidate = ( action, kind, name ) => {
return (
Expand Down
57 changes: 22 additions & 35 deletions packages/core-data/src/test/integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,6 @@ jest.mock( '@wordpress/data-controls', () => {
apiFetch: jest.fn(),
};
} );
const { apiFetch: actualApiFetch } = jest.requireActual(
'@wordpress/data-controls'
);
import { apiFetch } from '@wordpress/data-controls';

jest.mock( '@wordpress/api-fetch', () => {
return {
Expand All @@ -38,10 +34,13 @@ const runPromise = async ( promise ) => {
};

const runPendingPromises = async () => {
jest.runAllTimers();
const p = new Promise( ( resolve ) => setTimeout( resolve ) );
jest.runAllTimers();
await p;
// @TODO: find a better way of exhausting the current event loop queue
for ( let i = 0; i < 100; i++ ) {
jest.runAllTimers();
const p = new Promise( ( resolve ) => setTimeout( resolve ) );
jest.runAllTimers();
await p;
}
};

describe( 'receiveEntityRecord', () => {
Expand All @@ -55,13 +54,11 @@ describe( 'receiveEntityRecord', () => {
registry.register( store );
registry.registerStore( 'test/resolution', {
actions: {
__unstableAcquireStoreLock: () => ( { type: 'ACQUIRE_LOCK' } ),
__unstableReleaseStoreLock: () => ( { type: 'RELEASE_LOCK' } ),
receiveEntityRecords: actions.receiveEntityRecords,
*getEntityRecords( ...args ) {
return yield controls.resolveSelect(
'test/resolution',
'getEntityRecords',
...args
);
getEntityRecords( ...args ) {
return resolvers.getEntityRecords( ...args );
},
*getEntityRecord( ...args ) {
return yield controls.resolveSelect(
Expand All @@ -82,12 +79,12 @@ describe( 'receiveEntityRecord', () => {
getEntityRecord,
getEntityRecords: resolvers.getEntityRecords,
},
__experimentalUseThunks: true,
} );
return registry;
}

beforeEach( async () => {
apiFetch.mockReset();
triggerFetch.mockReset();
jest.useFakeTimers();
} );
Expand All @@ -96,8 +93,8 @@ describe( 'receiveEntityRecord', () => {
const getEntityRecord = jest.fn();
const registry = createTestRegistry( getEntityRecord );

// Trigger resolution of postType records
apiFetch.mockImplementation( () => ( {
// // Trigger resolution of postType records
triggerFetch.mockImplementation( () => ( {
2: { slug: 'test', id: 2 },
} ) );
await runPromise(
Expand Down Expand Up @@ -129,7 +126,7 @@ describe( 'receiveEntityRecord', () => {
const registry = createTestRegistry( getEntityRecord );

// Trigger resolution of postType records
apiFetch.mockImplementation( () => ( {
triggerFetch.mockImplementation( () => ( {
'test-1': { slug: 'test-1', id: 2 },
} ) );
await runPromise(
Expand Down Expand Up @@ -165,42 +162,37 @@ describe( 'saveEntityRecord', () => {
}

beforeEach( async () => {
apiFetch.mockReset();
triggerFetch.mockReset();
jest.useFakeTimers( 'modern' );
} );

it( 'should not trigger any GET requests until POST/PUT is finished.', async () => {
const registry = createTestRegistry();
// Fetch post types from the API {{{
apiFetch.mockImplementation( () => ( {
triggerFetch.mockImplementation( () => ( {
'post-1': { slug: 'post-1' },
} ) );

// Trigger fetch
registry.select( 'core' ).getEntityRecords( 'root', 'postType' );
jest.runAllTimers();
await Promise.resolve().then( () => jest.runAllTimers() );
expect( apiFetch ).toBeCalledTimes( 1 );
expect( apiFetch ).toBeCalledWith( {
await runPendingPromises();
expect( triggerFetch ).toBeCalledTimes( 1 );
expect( triggerFetch ).toBeCalledWith( {
path: '/wp/v2/types?context=edit',
} );

// Select fetched results, there should be no subsequent request
apiFetch.mockReset();
triggerFetch.mockReset();
const results = registry
.select( 'core' )
.getEntityRecords( 'root', 'postType' );
expect( apiFetch ).toBeCalledTimes( 0 );
jest.runAllTimers();
expect( apiFetch ).toBeCalledTimes( 0 );
expect( triggerFetch ).toBeCalledTimes( 0 );
expect( results ).toHaveLength( 1 );
expect( results[ 0 ].slug ).toBe( 'post-1' );
// }}} Fetch post types from the API

// Save changes
apiFetch.mockClear();
apiFetch.mockImplementation( actualApiFetch );
triggerFetch.mockClear();
let resolvePromise;
triggerFetch.mockImplementation( function () {
return new Promise( ( resolve ) => {
Expand All @@ -214,10 +206,6 @@ describe( 'saveEntityRecord', () => {
newField: 'a',
} );

// Wait a few ticks – without rungen we have less control over the flow of things.
// @TODO: A better solution
await runPendingPromises();
await runPendingPromises();
await runPendingPromises();

// There should ONLY be a single hanging API call (PUT) by this point.
Expand All @@ -235,7 +223,6 @@ describe( 'saveEntityRecord', () => {
} )
);
triggerFetch.mockClear();
apiFetch.mockClear();

// The PUT is still hanging, let's call a selector now and make sure it won't trigger
// any requests
Expand Down
113 changes: 72 additions & 41 deletions packages/core-data/src/test/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
* WordPress dependencies
*/
import { apiFetch } from '@wordpress/data-controls';
import triggerFetch from '@wordpress/api-fetch';

jest.mock( '@wordpress/api-fetch' );

/**
* Internal dependencies
Expand Down Expand Up @@ -117,69 +120,97 @@ describe( 'getEntityRecords', () => {
},
];

it( 'yields with requested post type', async () => {
const fulfillment = getEntityRecords( 'root', 'postType' );
beforeEach( async () => {
triggerFetch.mockReset();
jest.useFakeTimers();
} );

// Trigger generator
fulfillment.next();
it( 'dispatches the requested post type', async () => {
const dispatch = Object.assign( jest.fn(), {
receiveEntityRecords: jest.fn(),
__unstableAcquireStoreLock: jest.fn(),
__unstableReleaseStoreLock: jest.fn(),
} );
// Provide entities
dispatch.mockReturnValueOnce( ENTITIES );

// Provide entities and acquire lock
fulfillment.next( ENTITIES );
// Provide response
triggerFetch.mockImplementation( () => POST_TYPES );

// trigger apiFetch
const { value: apiFetchAction } = fulfillment.next();
await getEntityRecords( 'root', 'postType' )( { dispatch } );

expect( apiFetchAction.request ).toEqual( {
// Fetch request should have been issued
expect( triggerFetch ).toHaveBeenCalledWith( {
path: '/wp/v2/types?context=edit',
} );
// Provide response and trigger action
const { value: received } = fulfillment.next( POST_TYPES );
expect( received ).toEqual(
receiveEntityRecords(
'root',
'postType',
Object.values( POST_TYPES ),
{}
)

// The record should have been received
expect( dispatch.receiveEntityRecords ).toHaveBeenCalledWith(
'root',
'postType',
Object.values( POST_TYPES ),
{}
);
} );

it( 'Uses state locks', async () => {
const fulfillment = getEntityRecords( 'root', 'postType' );
const dispatch = Object.assign( jest.fn(), {
receiveEntityRecords: jest.fn(),
__unstableAcquireStoreLock: jest.fn(),
__unstableReleaseStoreLock: jest.fn(),
} );
// Provide entities
dispatch.mockReturnValueOnce( ENTITIES );

// Repeat the steps from `yields with requested post type` test
fulfillment.next();
// Provide entities and acquire lock
expect( fulfillment.next( ENTITIES ).value.type ).toEqual(
'@@data/DISPATCH'
);
fulfillment.next();
fulfillment.next( POST_TYPES );
// Provide response
triggerFetch.mockImplementation( () => POST_TYPES );

// Resolve specific entity records
fulfillment.next();
fulfillment.next();
await getEntityRecords( 'root', 'postType' )( { dispatch } );

// Release lock
expect( fulfillment.next().value.type ).toEqual( '@@data/DISPATCH' );
// Fetch request should have been issued
expect( triggerFetch ).toHaveBeenCalledWith( {
path: '/wp/v2/types?context=edit',
} );

// The record should have been received
expect(
dispatch.__unstableAcquireStoreLock
).toHaveBeenCalledWith(
'core',
[ 'entities', 'data', 'root', 'postType' ],
{ exclusive: false }
);
expect( dispatch.__unstableReleaseStoreLock ).toHaveBeenCalledTimes(
1
);
} );

it( 'marks specific entity records as resolved', async () => {
const fulfillment = getEntityRecords( 'root', 'postType' );
const dispatch = Object.assign( jest.fn(), {
receiveEntityRecords: jest.fn(),
__unstableAcquireStoreLock: jest.fn(),
__unstableReleaseStoreLock: jest.fn(),
} );
// Provide entities
dispatch.mockReturnValueOnce( ENTITIES );

// Repeat the steps from `yields with requested post type` test
fulfillment.next();
fulfillment.next( ENTITIES );
fulfillment.next();
fulfillment.next( POST_TYPES );
// Provide response
triggerFetch.mockImplementation( () => POST_TYPES );

await getEntityRecords( 'root', 'postType' )( { dispatch } );

// Fetch request should have been issued
expect( triggerFetch ).toHaveBeenCalledWith( {
path: '/wp/v2/types?context=edit',
} );

// It should mark the entity record that has an ID as resolved
expect( fulfillment.next().value ).toEqual( {
// The record should have been received
expect( dispatch ).toHaveBeenCalledWith( {
type: 'START_RESOLUTIONS',
selectorName: 'getEntityRecord',
args: [ [ ENTITIES[ 1 ].kind, ENTITIES[ 1 ].name, 2 ] ],
} );
expect( fulfillment.next().value ).toEqual( {
expect( dispatch ).toHaveBeenCalledWith( {
type: 'FINISH_RESOLUTIONS',
selectorName: 'getEntityRecord',
args: [ [ ENTITIES[ 1 ].kind, ENTITIES[ 1 ].name, 2 ] ],
Expand Down

0 comments on commit 0bc4da8

Please sign in to comment.