From 0bc4da872c898eb54ce573685ac1ec5bb071fc6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 6 Sep 2021 13:41:09 +0200 Subject: [PATCH] Fix integration tests --- packages/core-data/src/resolvers.js | 41 ++++---- packages/core-data/src/test/integration.js | 57 ++++------- packages/core-data/src/test/resolvers.js | 113 +++++++++++++-------- 3 files changed, 118 insertions(+), 93 deletions(-) diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js index 7026937858b474..f675e091524292 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -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 */ @@ -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 } @@ -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". @@ -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 @@ -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 ( diff --git a/packages/core-data/src/test/integration.js b/packages/core-data/src/test/integration.js index 6eb1645c506fc7..7411e560436c0d 100644 --- a/packages/core-data/src/test/integration.js +++ b/packages/core-data/src/test/integration.js @@ -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 { @@ -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', () => { @@ -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( @@ -82,12 +79,12 @@ describe( 'receiveEntityRecord', () => { getEntityRecord, getEntityRecords: resolvers.getEntityRecords, }, + __experimentalUseThunks: true, } ); return registry; } beforeEach( async () => { - apiFetch.mockReset(); triggerFetch.mockReset(); jest.useFakeTimers(); } ); @@ -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( @@ -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( @@ -165,7 +162,6 @@ describe( 'saveEntityRecord', () => { } beforeEach( async () => { - apiFetch.mockReset(); triggerFetch.mockReset(); jest.useFakeTimers( 'modern' ); } ); @@ -173,34 +169,30 @@ describe( 'saveEntityRecord', () => { 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 ) => { @@ -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. @@ -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 diff --git a/packages/core-data/src/test/resolvers.js b/packages/core-data/src/test/resolvers.js index 2ff24b126ab8d7..7cc3ab5b55f9ff 100644 --- a/packages/core-data/src/test/resolvers.js +++ b/packages/core-data/src/test/resolvers.js @@ -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 @@ -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 ] ],