diff --git a/src/utils/run-ability.ts b/src/utils/run-ability.ts index f97d9c3ef..be6589f54 100644 --- a/src/utils/run-ability.ts +++ b/src/utils/run-ability.ts @@ -95,30 +95,37 @@ const buildFetchOptions = ( input: AbilityInput, method: Method ) => { - const normalizedInput = input ?? null; - if ( method === 'GET' || method === 'DELETE' ) { return { path: - normalizedInput === null + input === undefined || input === null ? `/wp-abilities/v1/abilities/${ ability }/run` : addQueryArgs( `/wp-abilities/v1/abilities/${ ability }/run`, { - input: normalizedInput, + input, } ), method, }; } - return { + const options: { + path: string; + method: 'POST'; + data?: { + input: AbilityInput; + }; + } = { path: `/wp-abilities/v1/abilities/${ ability }/run`, method: 'POST' as const, - data: { - input: normalizedInput, - }, }; + + if ( input !== undefined ) { + options.data = { input }; + } + + return options; }; export async function runAbility< T = unknown >( @@ -131,7 +138,7 @@ export async function runAbility< T = unknown >( if ( typeof abilitiesModule?.executeAbility === 'function' ) { return ( await abilitiesModule.executeAbility( ability, - input ?? null + input ) ) as T; } diff --git a/tests/unit/run-ability.test.ts b/tests/unit/run-ability.test.ts new file mode 100644 index 000000000..77b82e947 --- /dev/null +++ b/tests/unit/run-ability.test.ts @@ -0,0 +1,82 @@ +/** + * WordPress dependencies + */ +import { executeAbility } from '@wordpress/abilities'; +import apiFetch from '@wordpress/api-fetch'; + +/** + * Internal dependencies + */ +import { runAbility } from '../../src/utils/run-ability'; + +jest.mock( '@wordpress/api-fetch', () => jest.fn() ); +jest.mock( '@wordpress/core-abilities', () => ( { + ready: Promise.resolve(), +} ) ); +jest.mock( '@wordpress/abilities', () => ( { + executeAbility: jest.fn(), +} ) ); + +describe( 'runAbility', () => { + const mockedApiFetch = apiFetch as jest.Mock; + const mockedExecuteAbility = executeAbility as jest.Mock; + + beforeEach( () => { + jest.clearAllMocks(); + jest.spyOn( console, 'error' ).mockImplementation( () => undefined ); + jest.spyOn( console, 'warn' ).mockImplementation( () => undefined ); + + mockedApiFetch.mockResolvedValue( 'fallback result' ); + mockedExecuteAbility.mockResolvedValue( 'client result' ); + } ); + + afterEach( () => { + jest.restoreAllMocks(); + } ); + + it( 'passes omitted input to the client ability as undefined', async () => { + const result = await runAbility( 'core/users' ); + + expect( result ).toBe( 'client result' ); + expect( mockedExecuteAbility ).toHaveBeenCalledWith( + 'core/users', + undefined + ); + expect( mockedApiFetch ).not.toHaveBeenCalled(); + } ); + + it( 'omits REST input data when fallback runs without input', async () => { + mockedExecuteAbility.mockRejectedValueOnce( + Object.assign( new Error( 'Ability not found' ), { + code: 'ability_not_found', + } ) + ); + + const result = await runAbility( 'core/users' ); + + expect( result ).toBe( 'fallback result' ); + expect( mockedApiFetch ).toHaveBeenCalledWith( { + path: '/wp-abilities/v1/abilities/core/users/run', + method: 'POST', + } ); + } ); + + it( 'keeps explicit empty-object input in REST fallback data', async () => { + mockedExecuteAbility.mockRejectedValueOnce( + Object.assign( new Error( 'Ability not found' ), { + code: 'ability_not_found', + } ) + ); + + const result = await runAbility( 'core/users', {} ); + + expect( result ).toBe( 'fallback result' ); + expect( mockedApiFetch ).toHaveBeenCalledWith( { + path: '/wp-abilities/v1/abilities/core/users/run', + method: 'POST', + data: { + input: {}, + }, + } ); + } ); +} );