UNPKG

@wordpress/hooks

Version:
1,096 lines (907 loc) 33.4 kB
/** * Internal dependencies */ import { createHooks, addAction, addFilter, removeAction, removeFilter, hasAction, hasFilter, removeAllActions, removeAllFilters, doAction, doActionAsync, applyFilters, applyFiltersAsync, currentAction, currentFilter, doingAction, doingFilter, didAction, didFilter, actions, filters, } from '..'; function filterA( str ) { return str + 'a'; } function filterB( str ) { return str + 'b'; } function filterC( str ) { return str + 'c'; } function filterCRemovesSelf( str ) { removeFilter( 'test.filter', 'my_callback_filter_c_removes_self' ); return str + 'b'; } function filterRemovesB( str ) { removeFilter( 'test.filter', 'my_callback_filter_b' ); return str; } function filterRemovesC( str ) { removeFilter( 'test.filter', 'my_callback_filter_c' ); return str; } function actionA() { window.actionValue += 'a'; } function actionB() { window.actionValue += 'b'; } function actionC() { window.actionValue += 'c'; } beforeEach( () => { window.actionValue = ''; // Reset state in between tests (clear all callbacks, `didAction` counts, // etc.) Just resetting actions and filters is not enough // because the internal functions have references to the original objects. [ actions, filters ].forEach( ( hooks ) => { for ( const k in hooks ) { if ( '__current' === k ) { continue; } delete hooks[ k ]; } delete hooks.all; } ); } ); test( 'hooks can be instantiated', () => { const hooks = createHooks(); expect( typeof hooks ).toBe( 'object' ); } ); test( 'run a filter with no callbacks', () => { expect( applyFilters( 'test.filter', 42 ) ).toBe( 42 ); } ); test( 'add and remove a filter', () => { addFilter( 'test.filter', 'my_callback', filterA ); expect( removeAllFilters( 'test.filter' ) ).toBe( 1 ); expect( applyFilters( 'test.filter', 'test' ) ).toBe( 'test' ); expect( removeAllFilters( 'test.filter' ) ).toBe( 0 ); } ); test( 'add a filter and run it', () => { addFilter( 'test.filter', 'my_callback', filterA ); expect( applyFilters( 'test.filter', 'test' ) ).toBe( 'testa' ); } ); test( 'add 2 filters in a row and run them', () => { addFilter( 'test.filter', 'my_callback', filterA ); addFilter( 'test.filter', 'my_callback', filterB ); expect( applyFilters( 'test.filter', 'test' ) ).toBe( 'testab' ); } ); test( 'remove a non-existent filter', () => { expect( removeFilter( 'test.filter', 'my_callback', filterA ) ).toBe( 0 ); expect( removeAllFilters( 'test.filter' ) ).toBe( 0 ); } ); test( 'remove an invalid namespace from a filter', () => { expect( removeFilter( 'test.filter', 42 ) ).toBe( undefined ); expect( console ).toHaveErroredWith( 'The namespace must be a non-empty string.' ); } ); test( 'cannot add filters with non-string hook names', () => { addFilter( 42, 'my_callback', () => null ); expect( console ).toHaveErroredWith( 'The hook name must be a non-empty string.' ); } ); test( 'cannot add filters with empty-string hook names', () => { addFilter( '', 'my_callback', () => null ); expect( console ).toHaveErroredWith( 'The hook name must be a non-empty string.' ); } ); test( 'cannot add filters with empty-string namespaces', () => { addFilter( 'hook_name', '', () => null ); expect( console ).toHaveErroredWith( 'The namespace must be a non-empty string.' ); } ); test( 'cannot add filters with invalid namespaces', () => { addFilter( 'hook_name', 'invalid_%&name', () => null ); expect( console ).toHaveErroredWith( 'The namespace can only contain numbers, letters, dashes, periods, underscores and slashes.' ); } ); test( 'cannot add filters with namespaces starting with a slash', () => { addFilter( 'hook_name', '/invalid_name', () => null ); expect( console ).toHaveErroredWith( 'The namespace can only contain numbers, letters, dashes, periods, underscores and slashes.' ); } ); test( 'Can add filters with dashes in namespaces', () => { addFilter( 'hook_name', 'with-dashes', () => null ); expect( console ).not.toHaveErrored(); } ); test( 'Can add filters with capitals in namespaces', () => { addFilter( 'hook_name', 'My_Name-OhNoaction', () => null ); expect( console ).not.toHaveErrored(); } ); test( 'Can add filters with slashes in namespaces', () => { addFilter( 'hook_name', 'my/name/action', () => null ); expect( console ).not.toHaveErrored(); } ); test( 'Can add filters with periods in namespaces', () => { addFilter( 'hook_name', 'my.name.action', () => null ); expect( console ).not.toHaveErrored(); } ); test( 'Can add filters with capitals in hookName', () => { addFilter( 'hookName', 'action', () => null ); expect( console ).not.toHaveErrored(); } ); test( 'Can add filters with periods in hookName', () => { addFilter( 'hook.name', 'action', () => null ); expect( console ).not.toHaveErrored(); } ); test( 'cannot add filters with namespace containing backslash', () => { addFilter( 'hook_name', 'i\n\valid\name', () => null ); expect( console ).toHaveErroredWith( 'The namespace can only contain numbers, letters, dashes, periods, underscores and slashes.' ); } ); test( 'cannot add filters named with __ prefix', () => { addFilter( '__test', 'my_callback', () => null ); expect( console ).toHaveErroredWith( 'The hook name cannot begin with `__`.' ); } ); test( 'cannot add filters with non-function callbacks', () => { addFilter( 'test', 'my_callback', '42' ); expect( console ).toHaveErroredWith( 'The hook callback must be a function.' ); } ); test( 'cannot add filters with non-numeric priorities', () => { addFilter( 'test', 'my_callback', () => null, '42' ); expect( console ).toHaveErroredWith( 'If specified, the hook priority must be a number.' ); } ); test( 'add 3 filters with different priorities and run them', () => { addFilter( 'test.filter', 'my_callback_filter_a', filterA ); addFilter( 'test.filter', 'my_callback_filter_b', filterB, 2 ); addFilter( 'test.filter', 'my_callback_filter_c', filterC, 8 ); expect( applyFilters( 'test.filter', 'test' ) ).toBe( 'testbca' ); } ); test( 'filters with the same and different priorities', () => { const callbacks = {}; [ 1, 2, 3, 4 ].forEach( ( priority ) => { [ 'a', 'b', 'c', 'd' ].forEach( ( string ) => { callbacks[ 'fn_' + priority + string ] = ( value ) => { return value.concat( priority + string ); }; } ); } ); addFilter( 'test_order', 'my_callback_fn_3a', callbacks.fn_3a, 3 ); addFilter( 'test_order', 'my_callback_fn_3b', callbacks.fn_3b, 3 ); addFilter( 'test_order', 'my_callback_fn_3c', callbacks.fn_3c, 3 ); addFilter( 'test_order', 'my_callback_fn_2a', callbacks.fn_2a, 2 ); addFilter( 'test_order', 'my_callback_fn_2b', callbacks.fn_2b, 2 ); addFilter( 'test_order', 'my_callback_fn_2c', callbacks.fn_2c, 2 ); expect( applyFilters( 'test_order', [] ) ).toEqual( [ '2a', '2b', '2c', '3a', '3b', '3c', ] ); removeFilter( 'test_order', 'my_callback_fn_2b', callbacks.fn_2b ); removeFilter( 'test_order', 'my_callback_fn_3a', callbacks.fn_3a ); expect( applyFilters( 'test_order', [] ) ).toEqual( [ '2a', '2c', '3b', '3c', ] ); addFilter( 'test_order', 'my_callback_fn_4a', callbacks.fn_4a, 4 ); addFilter( 'test_order', 'my_callback_fn_4b', callbacks.fn_4b, 4 ); addFilter( 'test_order', 'my_callback_fn_1a', callbacks.fn_1a, 1 ); addFilter( 'test_order', 'my_callback_fn_4c', callbacks.fn_4c, 4 ); addFilter( 'test_order', 'my_callback_fn_1b', callbacks.fn_1b, 1 ); addFilter( 'test_order', 'my_callback_fn_3d', callbacks.fn_3d, 3 ); addFilter( 'test_order', 'my_callback_fn_4d', callbacks.fn_4d, 4 ); addFilter( 'test_order', 'my_callback_fn_1c', callbacks.fn_1c, 1 ); addFilter( 'test_order', 'my_callback_fn_2d', callbacks.fn_2d, 2 ); addFilter( 'test_order', 'my_callback_fn_1d', callbacks.fn_1d, 1 ); expect( applyFilters( 'test_order', [] ) ).toEqual( [ // All except 2b and 3a, which we removed earlier. '1a', '1b', '1c', '1d', '2a', '2c', '2d', '3b', '3c', '3d', '4a', '4b', '4c', '4d', ] ); } ); test( 'add and remove an action', () => { addAction( 'test.action', 'my_callback', actionA ); expect( removeAllActions( 'test.action' ) ).toBe( 1 ); expect( doAction( 'test.action' ) ).toBe( undefined ); expect( window.actionValue ).toBe( '' ); } ); test( 'add an action and run it', () => { addAction( 'test.action', 'my_callback', actionA ); doAction( 'test.action' ); expect( window.actionValue ).toBe( 'a' ); } ); test( 'add 2 actions in a row and then run them', () => { addAction( 'test.action', 'my_callback', actionA ); addAction( 'test.action', 'my_callback', actionB ); doAction( 'test.action' ); expect( window.actionValue ).toBe( 'ab' ); } ); test( 'add 3 actions with different priorities and run them', () => { addAction( 'test.action', 'my_callback', actionA ); addAction( 'test.action', 'my_callback', actionB, 2 ); addAction( 'test.action', 'my_callback', actionC, 8 ); doAction( 'test.action' ); expect( window.actionValue ).toBe( 'bca' ); } ); test( 'pass in two arguments to an action', () => { const arg1 = { a: 10 }; const arg2 = { b: 20 }; addAction( 'test.action', 'my_callback', ( a, b ) => { expect( a ).toBe( arg1 ); expect( b ).toBe( arg2 ); } ); doAction( 'test.action', arg1, arg2 ); } ); test( 'fire action multiple times', () => { expect.assertions( 2 ); function func() { expect( true ).toBe( true ); } addAction( 'test.action', 'my_callback', func ); doAction( 'test.action' ); doAction( 'test.action' ); } ); test( 'add a filter before the one currently executing', () => { addFilter( 'test.filter', 'my_callback', ( outerValue ) => { addFilter( 'test.filter', 'my_callback', ( innerValue ) => innerValue + 'a', 1 ); return outerValue + 'b'; }, 2 ); expect( applyFilters( 'test.filter', 'test_' ) ).toBe( 'test_b' ); } ); test( 'add a filter after the one currently executing', () => { addFilter( 'test.filter', 'my_callback', ( outerValue ) => { addFilter( 'test.filter', 'my_callback', ( innerValue ) => innerValue + 'b', 2 ); return outerValue + 'a'; }, 1 ); expect( applyFilters( 'test.filter', 'test_' ) ).toBe( 'test_ab' ); } ); test( 'add a filter immediately after the one currently executing', () => { addFilter( 'test.filter', 'my_callback', ( outerValue ) => { addFilter( 'test.filter', 'my_callback', ( innerValue ) => innerValue + 'b', 1 ); return outerValue + 'a'; }, 1 ); expect( applyFilters( 'test.filter', 'test_' ) ).toBe( 'test_ab' ); } ); test( 'remove specific action callback', () => { addAction( 'test.action', 'my_callback_action_a', actionA ); addAction( 'test.action', 'my_callback_action_b', actionB, 2 ); addAction( 'test.action', 'my_callback_action_c', actionC, 8 ); expect( removeAction( 'test.action', 'my_callback_action_b' ) ).toBe( 1 ); doAction( 'test.action' ); expect( window.actionValue ).toBe( 'ca' ); } ); test( 'remove all action callbacks', () => { addAction( 'test.action', 'my_callback_action_a', actionA ); addAction( 'test.action', 'my_callback_action_b', actionB, 2 ); addAction( 'test.action', 'my_callback_action_c', actionC, 8 ); expect( removeAllActions( 'test.action' ) ).toBe( 3 ); doAction( 'test.action' ); expect( window.actionValue ).toBe( '' ); } ); test( 'remove specific filter callback', () => { addFilter( 'test.filter', 'my_callback_filter_a', filterA ); addFilter( 'test.filter', 'my_callback_filter_b', filterB, 2 ); addFilter( 'test.filter', 'my_callback_filter_c', filterC, 8 ); expect( removeFilter( 'test.filter', 'my_callback_filter_b' ) ).toBe( 1 ); expect( applyFilters( 'test.filter', 'test' ) ).toBe( 'testca' ); } ); test( 'filter removes a callback that has already executed', () => { addFilter( 'test.filter', 'my_callback_filter_a', filterA, 1 ); addFilter( 'test.filter', 'my_callback_filter_b', filterB, 3 ); addFilter( 'test.filter', 'my_callback_filter_c', filterC, 5 ); addFilter( 'test.filter', 'my_callback_filter_removes_b', filterRemovesB, 4 ); expect( applyFilters( 'test.filter', 'test' ) ).toBe( 'testabc' ); } ); test( 'filter removes a callback that has already executed (same priority)', () => { addFilter( 'test.filter', 'my_callback_filter_a', filterA, 1 ); addFilter( 'test.filter', 'my_callback_filter_b', filterB, 2 ); addFilter( 'test.filter', 'my_callback_filter_removes_b', filterRemovesB, 2 ); addFilter( 'test.filter', 'my_callback_filter_c', filterC, 4 ); expect( applyFilters( 'test.filter', 'test' ) ).toBe( 'testabc' ); } ); test( 'filter removes the current callback', () => { addFilter( 'test.filter', 'my_callback_filter_a', filterA, 1 ); addFilter( 'test.filter', 'my_callback_filter_c_removes_self', filterCRemovesSelf, 3 ); addFilter( 'test.filter', 'my_callback_filter_c', filterC, 5 ); expect( applyFilters( 'test.filter', 'test' ) ).toBe( 'testabc' ); } ); test( 'filter removes a callback that has not yet executed (last)', () => { addFilter( 'test.filter', 'my_callback_filter_a', filterA, 1 ); addFilter( 'test.filter', 'my_callback_filter_b', filterB, 3 ); addFilter( 'test.filter', 'my_callback_filter_c', filterC, 5 ); addFilter( 'test.filter', 'my_callback_filter_removes_c', filterRemovesC, 4 ); expect( applyFilters( 'test.filter', 'test' ) ).toBe( 'testab' ); } ); test( 'filter removes a callback that has not yet executed (middle)', () => { addFilter( 'test.filter', 'my_callback_filter_a', filterA, 1 ); addFilter( 'test.filter', 'my_callback_filter_b', filterB, 3 ); addFilter( 'test.filter', 'my_callback_filter_c', filterC, 4 ); addFilter( 'test.filter', 'my_callback_filter_removes_b', filterRemovesB, 2 ); expect( applyFilters( 'test.filter', 'test' ) ).toBe( 'testac' ); } ); test( 'filter removes a callback that has not yet executed (same priority)', () => { addFilter( 'test.filter', 'my_callback_filter_a', filterA, 1 ); addFilter( 'test.filter', 'my_callback_filter_removes_b', filterRemovesB, 2 ); addFilter( 'test.filter', 'my_callback_filter_b', filterB, 2 ); addFilter( 'test.filter', 'my_callback_filter_c', filterC, 4 ); expect( applyFilters( 'test.filter', 'test' ) ).toBe( 'testac' ); } ); test( 'remove all filter callbacks', () => { addFilter( 'test.filter', 'my_callback_filter_a', filterA ); addFilter( 'test.filter', 'my_callback_filter_b', filterB, 2 ); addFilter( 'test.filter', 'my_callback_filter_c', filterC, 8 ); expect( removeAllFilters( 'test.filter' ) ).toBe( 3 ); expect( applyFilters( 'test.filter', 'test' ) ).toBe( 'test' ); } ); // Test doingAction, didAction, hasAction. test( 'doingAction, didAction and hasAction.', () => { let actionCalls = 0; addAction( 'another.action', 'my_callback', () => {} ); doAction( 'another.action' ); // Verify no action is running yet. expect( doingAction( 'test.action' ) ).toBe( false ); expect( didAction( 'test.action' ) ).toBe( 0 ); expect( hasAction( 'test.action' ) ).toBe( false ); addAction( 'test.action', 'my_callback', () => { actionCalls++; expect( currentAction() ).toBe( 'test.action' ); expect( doingAction() ).toBe( true ); expect( doingAction( 'test.action' ) ).toBe( true ); } ); // Verify action added, not running yet. expect( doingAction( 'test.action' ) ).toBe( false ); expect( didAction( 'test.action' ) ).toBe( 0 ); expect( hasAction( 'test.action' ) ).toBe( true ); doAction( 'test.action' ); // Verify action added and running. expect( actionCalls ).toBe( 1 ); expect( doingAction( 'test.action' ) ).toBe( false ); expect( didAction( 'test.action' ) ).toBe( 1 ); expect( hasAction( 'test.action' ) ).toBe( true ); expect( doingAction() ).toBe( false ); expect( doingAction( 'test.action' ) ).toBe( false ); expect( doingAction( 'notatest.action' ) ).toBe( false ); expect( currentAction() ).toBe( null ); doAction( 'test.action' ); expect( actionCalls ).toBe( 2 ); expect( didAction( 'test.action' ) ).toBe( 2 ); expect( removeAllActions( 'test.action' ) ).toBe( 1 ); // Verify state is reset appropriately. expect( doingAction( 'test.action' ) ).toBe( false ); expect( didAction( 'test.action' ) ).toBe( 2 ); expect( hasAction( 'test.action' ) ).toBe( true ); doAction( 'another.action' ); expect( doingAction( 'test.action' ) ).toBe( false ); // Verify an action with no handlers is still counted. expect( didAction( 'unattached.action' ) ).toBe( 0 ); doAction( 'unattached.action' ); expect( doingAction( 'unattached.action' ) ).toBe( false ); expect( didAction( 'unattached.action' ) ).toBe( 1 ); doAction( 'unattached.action' ); expect( doingAction( 'unattached.action' ) ).toBe( false ); expect( didAction( 'unattached.action' ) ).toBe( 2 ); // Verify hasAction returns 0 when no matching action. expect( hasAction( 'notatest.action' ) ).toBe( false ); } ); test( 'Verify doingFilter, didFilter and hasFilter.', () => { let filterCalls = 0; addFilter( 'runtest.filter', 'my_callback', ( arg ) => { filterCalls++; expect( currentFilter() ).toBe( 'runtest.filter' ); expect( doingFilter() ).toBe( true ); expect( doingFilter( 'runtest.filter' ) ).toBe( true ); return arg; } ); // Verify filter added and running. const test = applyFilters( 'runtest.filter', 'someValue' ); expect( test ).toBe( 'someValue' ); expect( filterCalls ).toBe( 1 ); expect( didFilter( 'runtest.filter' ) ).toBe( 1 ); expect( hasFilter( 'runtest.filter' ) ).toBe( true ); expect( hasFilter( 'notatest.filter' ) ).toBe( false ); expect( doingFilter() ).toBe( false ); expect( doingFilter( 'runtest.filter' ) ).toBe( false ); expect( doingFilter( 'notatest.filter' ) ).toBe( false ); expect( currentFilter() ).toBe( null ); expect( removeAllFilters( 'runtest.filter' ) ).toBe( 1 ); expect( hasFilter( 'runtest.filter' ) ).toBe( true ); expect( didFilter( 'runtest.filter' ) ).toBe( 1 ); } ); test( 'recursively calling a filter', () => { addFilter( 'test.filter', 'my_callback', ( value ) => { if ( value.length === 7 ) { return value; } return applyFilters( 'test.filter', value + 'X' ); } ); expect( applyFilters( 'test.filter', 'test' ) ).toBe( 'testXXX' ); } ); test( 'current filter when multiple filters are running', () => { addFilter( 'test.filter1', 'my_callback', ( value ) => { return applyFilters( 'test.filter2', value.concat( currentFilter() ) ); } ); addFilter( 'test.filter2', 'my_callback', ( value ) => { return value.concat( currentFilter() ); } ); expect( currentFilter() ).toBe( null ); expect( applyFilters( 'test.filter1', [ 'test' ] ) ).toEqual( [ 'test', 'test.filter1', 'test.filter2', ] ); expect( currentFilter() ).toBe( null ); } ); test( 'adding and removing filters with recursion', () => { function removeRecurseAndAdd2( val ) { expect( removeFilter( 'remove_and_add', 'my_callback_recurse' ) ).toBe( 1 ); val += '-' + applyFilters( 'remove_and_add', '' ) + '-'; addFilter( 'remove_and_add', 'my_callback_recurse', removeRecurseAndAdd2, 10 ); return val + '2'; } addFilter( 'remove_and_add', 'my_callback', ( val ) => val + '1', 11 ); addFilter( 'remove_and_add', 'my_callback_recurse', removeRecurseAndAdd2, 12 ); addFilter( 'remove_and_add', 'my_callback', ( val ) => val + '3', 13 ); addFilter( 'remove_and_add', 'my_callback', ( val ) => val + '4', 14 ); expect( applyFilters( 'remove_and_add', '' ) ).toBe( '1-134-234' ); } ); test( 'actions preserve arguments across handlers without return value', () => { const arg1 = { a: 10 }; const arg2 = { b: 20 }; addAction( 'test.action', 'my_callback1', ( a, b ) => { expect( a ).toBe( arg1 ); expect( b ).toBe( arg2 ); } ); addAction( 'test.action', 'my_callback2', ( a, b ) => { expect( a ).toBe( arg1 ); expect( b ).toBe( arg2 ); } ); doAction( 'test.action', arg1, arg2 ); } ); test( 'filters pass first argument across handlers', () => { addFilter( 'test.filter', 'my_callback1', ( count ) => count + 1 ); addFilter( 'test.filter', 'my_callback2', ( count ) => count + 1 ); const result = applyFilters( 'test.filter', 0 ); expect( result ).toBe( 2 ); } ); // Test adding via composition. test( 'adding hooks via composition', () => { const testObject = {}; testObject.hooks = createHooks(); expect( typeof testObject.hooks.applyFilters ).toBe( 'function' ); } ); // Test adding as a mixin. test( 'adding hooks as a mixin', () => { const testObject = {}; Object.assign( testObject, createHooks() ); expect( typeof testObject.applyFilters ).toBe( 'function' ); } ); // Test context. test( '`this` context via composition', () => { const testObject = { test: 'test this' }; testObject.hooks = createHooks(); const theCallback = function () { expect( this.test ).toBe( 'test this' ); }; addAction( 'test.action', 'my_callback', theCallback.bind( testObject ) ); doAction( 'test.action' ); const testObject2 = {}; Object.assign( testObject2, createHooks() ); } ); const setupActionListener = ( hookName, callback ) => addAction( hookName, 'my_callback', callback ); test( 'adding an action triggers a hookAdded action passing all callback details', () => { const hookAddedSpy = jest.fn(); setupActionListener( 'hookAdded', hookAddedSpy ); addAction( 'testAction', 'my_callback2', actionA, 9 ); expect( hookAddedSpy ).toHaveBeenCalledTimes( 1 ); expect( hookAddedSpy ).toHaveBeenCalledWith( 'testAction', 'my_callback2', actionA, 9 ); // Private instance. const hooksPrivateInstance = createHooks(); removeAction( 'hookAdded', 'my_callback' ); hookAddedSpy.mockClear(); hooksPrivateInstance.addAction( 'hookAdded', 'my_callback', hookAddedSpy ); hooksPrivateInstance.addAction( 'testAction', 'my_callback2', actionA, 9 ); expect( hookAddedSpy ).toHaveBeenCalledTimes( 1 ); expect( hookAddedSpy ).toHaveBeenCalledWith( 'testAction', 'my_callback2', actionA, 9 ); } ); test( 'adding a filter triggers a hookAdded action passing all callback details', () => { const hookAddedSpy = jest.fn(); setupActionListener( 'hookAdded', hookAddedSpy ); addFilter( 'testFilter', 'my_callback3', filterA, 8 ); expect( hookAddedSpy ).toHaveBeenCalledTimes( 1 ); expect( hookAddedSpy ).toHaveBeenCalledWith( 'testFilter', 'my_callback3', filterA, 8 ); // Private instance. const hooksPrivateInstance = createHooks(); removeAction( 'hookAdded', 'my_callback' ); hookAddedSpy.mockClear(); hooksPrivateInstance.addAction( 'hookAdded', 'my_callback', hookAddedSpy ); hooksPrivateInstance.addFilter( 'testFilter', 'my_callback3', filterA, 8 ); expect( hookAddedSpy ).toHaveBeenCalledTimes( 1 ); expect( hookAddedSpy ).toHaveBeenCalledWith( 'testFilter', 'my_callback3', filterA, 8 ); } ); test( 'removing an action triggers a hookRemoved action passing all callback details', () => { const hookRemovedSpy = jest.fn(); setupActionListener( 'hookRemoved', hookRemovedSpy ); addAction( 'testAction', 'my_callback2', actionA, 9 ); removeAction( 'testAction', 'my_callback2' ); expect( hookRemovedSpy ).toHaveBeenCalledTimes( 1 ); expect( hookRemovedSpy ).toHaveBeenCalledWith( 'testAction', 'my_callback2' ); // Private instance. const hooksPrivateInstance = createHooks(); removeAction( 'hookRemoved', 'my_callback' ); hookRemovedSpy.mockClear(); hooksPrivateInstance.addAction( 'hookRemoved', 'my_callback', hookRemovedSpy ); hooksPrivateInstance.addAction( 'testAction', 'my_callback2', actionA, 9 ); hooksPrivateInstance.removeAction( 'testAction', 'my_callback2' ); expect( hookRemovedSpy ).toHaveBeenCalledTimes( 1 ); expect( hookRemovedSpy ).toHaveBeenCalledWith( 'testAction', 'my_callback2' ); } ); test( 'removing a filter triggers a hookRemoved action passing all callback details', () => { const hookRemovedSpy = jest.fn(); setupActionListener( 'hookRemoved', hookRemovedSpy ); addFilter( 'testFilter', 'my_callback3', filterA, 8 ); removeFilter( 'testFilter', 'my_callback3' ); expect( hookRemovedSpy ).toHaveBeenCalledTimes( 1 ); expect( hookRemovedSpy ).toHaveBeenCalledWith( 'testFilter', 'my_callback3' ); // Private instance. const hooksPrivateInstance = createHooks(); removeAction( 'hookRemoved', 'my_callback' ); hookRemovedSpy.mockClear(); hooksPrivateInstance.addAction( 'hookRemoved', 'my_callback', hookRemovedSpy ); hooksPrivateInstance.addFilter( 'testFilter', 'my_callback3', filterA, 8 ); hooksPrivateInstance.removeFilter( 'testFilter', 'my_callback3' ); expect( hookRemovedSpy ).toHaveBeenCalledTimes( 1 ); expect( hookRemovedSpy ).toHaveBeenCalledWith( 'testFilter', 'my_callback3' ); } ); test( 'add an all filter and run it any hook to trigger it', () => { addFilter( 'all', 'my_callback', filterA ); expect( applyFilters( 'test.filter', 'test' ) ).toBe( 'testa' ); expect( applyFilters( 'test.filter-anything', 'test' ) ).toBe( 'testa' ); } ); test( 'add an all action and run it any hook to trigger it', () => { addAction( 'all', 'my_callback', actionA ); addAction( 'test.action', 'my_callback', actionA ); // Doesn't get triggered. doAction( 'test.action-anything' ); expect( window.actionValue ).toBe( 'a' ); } ); test( 'add multiple all filters and run it any hook to trigger them', () => { addFilter( 'all', 'my_callback', filterA ); addFilter( 'all', 'my_callback', filterB ); expect( applyFilters( 'test.filter', 'test' ) ).toBe( 'testab' ); expect( applyFilters( 'test.filter-anything', 'test' ) ).toBe( 'testab' ); } ); test( 'add multiple all actions and run it any hook to trigger them', () => { addAction( 'all', 'my_callback', actionA ); addAction( 'all', 'my_callback', actionB ); addAction( 'test.action', 'my_callback', actionA ); // Doesn't get triggered. doAction( 'test.action-anything' ); expect( window.actionValue ).toBe( 'ab' ); } ); test( 'add multiple all filters and run it any hook to trigger them by priority', () => { addFilter( 'all', 'my_callback', filterA, 11 ); addFilter( 'all', 'my_callback', filterB, 10 ); expect( applyFilters( 'test.filter', 'test' ) ).toBe( 'testba' ); expect( applyFilters( 'test.filter-anything', 'test' ) ).toBe( 'testba' ); } ); test( 'add multiple all actions and run it any hook to trigger them by priority', () => { addAction( 'all', 'my_callback', actionA, 11 ); addAction( 'all', 'my_callback', actionB, 10 ); addAction( 'test.action', 'my_callback', actionA ); // Doesn't get triggered. doAction( 'test.action-anything' ); expect( window.actionValue ).toBe( 'ba' ); } ); test( 'checking hasAction with named callbacks and removing', () => { addAction( 'test.action', 'my_callback', () => {} ); expect( hasAction( 'test.action', 'not_my_callback' ) ).toBe( false ); expect( hasAction( 'test.action', 'my_callback' ) ).toBe( true ); removeAction( 'test.action', 'my_callback' ); expect( hasAction( 'test.action', 'my_callback' ) ).toBe( false ); } ); test( 'checking hasAction with named callbacks and removeAllActions', () => { addAction( 'test.action', 'my_callback', () => {} ); addAction( 'test.action', 'my_second_callback', () => {} ); expect( hasAction( 'test.action', 'my_callback' ) ).toBe( true ); expect( hasAction( 'test.action', 'my_callback' ) ).toBe( true ); removeAllActions( 'test.action' ); expect( hasAction( 'test.action', 'my_callback' ) ).toBe( false ); expect( hasAction( 'test.action', 'my_callback' ) ).toBe( false ); } ); test( 'checking hasFilter with named callbacks and removing', () => { addFilter( 'test.filter', 'my_callback', () => {} ); expect( hasFilter( 'test.filter', 'not_my_callback' ) ).toBe( false ); expect( hasFilter( 'test.filter', 'my_callback' ) ).toBe( true ); removeFilter( 'test.filter', 'my_callback' ); expect( hasFilter( 'test.filter', 'my_callback' ) ).toBe( false ); } ); test( 'checking hasFilter with named callbacks and removeAllActions', () => { addFilter( 'test.filter', 'my_callback', () => {} ); addFilter( 'test.filter', 'my_second_callback', () => {} ); expect( hasFilter( 'test.filter', 'my_callback' ) ).toBe( true ); expect( hasFilter( 'test.filter', 'my_second_callback' ) ).toBe( true ); removeAllFilters( 'test.filter' ); expect( hasFilter( 'test.filter', 'my_callback' ) ).toBe( false ); expect( hasFilter( 'test.filter', 'my_second_callback' ) ).toBe( false ); } ); describe( 'async filter', () => { test( 'runs all registered handlers', async () => { addFilter( 'test.async.filter', 'callback_plus1', ( value ) => { return new Promise( ( r ) => setTimeout( () => r( value + 1 ), 10 ) ); } ); addFilter( 'test.async.filter', 'callback_times2', ( value ) => { return new Promise( ( r ) => setTimeout( () => r( value * 2 ), 10 ) ); } ); expect( await applyFiltersAsync( 'test.async.filter', 2 ) ).toBe( 6 ); } ); test( 'aborts when handler throws an error', async () => { const sqrt = jest.fn( async ( value ) => { if ( value < 0 ) { throw new Error( 'cannot pass negative value to sqrt' ); } return Math.sqrt( value ); } ); const plus1 = jest.fn( async ( value ) => { return value + 1; } ); addFilter( 'test.async.filter', 'callback_sqrt', sqrt ); addFilter( 'test.async.filter', 'callback_plus1', plus1 ); await expect( applyFiltersAsync( 'test.async.filter', -1 ) ).rejects.toThrow( 'cannot pass negative value to sqrt' ); expect( sqrt ).toHaveBeenCalledTimes( 1 ); expect( plus1 ).not.toHaveBeenCalled(); } ); test( 'is correctly tracked by doingFilter and didFilter', async () => { addFilter( 'test.async.filter', 'callback_doing', async ( value ) => { await new Promise( ( r ) => setTimeout( () => r(), 10 ) ); expect( doingFilter( 'test.async.filter' ) ).toBe( true ); return value; } ); expect( doingFilter( 'test.async.filter' ) ).toBe( false ); expect( didFilter( 'test.async.filter' ) ).toBe( 0 ); await applyFiltersAsync( 'test.async.filter', 0 ); expect( doingFilter( 'test.async.filter' ) ).toBe( false ); expect( didFilter( 'test.async.filter' ) ).toBe( 1 ); } ); test( 'is correctly tracked when multiple filters run at once', async () => { addFilter( 'test.async.filter1', 'callback_doing', async ( value ) => { await new Promise( ( r ) => setTimeout( () => r(), 10 ) ); expect( doingFilter( 'test.async.filter1' ) ).toBe( true ); await new Promise( ( r ) => setTimeout( () => r(), 10 ) ); return value; } ); addFilter( 'test.async.filter2', 'callback_doing', async ( value ) => { await new Promise( ( r ) => setTimeout( () => r(), 10 ) ); expect( doingFilter( 'test.async.filter2' ) ).toBe( true ); await new Promise( ( r ) => setTimeout( () => r(), 10 ) ); return value; } ); await Promise.all( [ applyFiltersAsync( 'test.async.filter1', 0 ), applyFiltersAsync( 'test.async.filter2', 0 ), ] ); } ); } ); describe( 'async action', () => { test( 'runs all registered handlers sequentially', async () => { const outputs = []; const action1 = async () => { outputs.push( 1 ); await new Promise( ( r ) => setTimeout( () => r(), 10 ) ); outputs.push( 2 ); }; const action2 = async () => { outputs.push( 3 ); await new Promise( ( r ) => setTimeout( () => r(), 10 ) ); outputs.push( 4 ); }; addAction( 'test.async.action', 'action1', action1 ); addAction( 'test.async.action', 'action2', action2 ); await doActionAsync( 'test.async.action' ); expect( outputs ).toEqual( [ 1, 2, 3, 4 ] ); } ); test( 'aborts when handler throws an error', async () => { const outputs = []; const action1 = async () => { throw new Error( 'aborting' ); }; const action2 = async () => { outputs.push( 3 ); await new Promise( ( r ) => setTimeout( () => r(), 10 ) ); outputs.push( 4 ); }; addAction( 'test.async.action', 'action1', action1 ); addAction( 'test.async.action', 'action2', action2 ); await expect( doActionAsync( 'test.async.action' ) ).rejects.toThrow( 'aborting' ); expect( outputs ).toEqual( [] ); } ); test( 'is correctly tracked by doingAction and didAction', async () => { addAction( 'test.async.action', 'callback_doing', async () => { await new Promise( ( r ) => setTimeout( () => r(), 10 ) ); expect( doingAction( 'test.async.action' ) ).toBe( true ); } ); expect( doingAction( 'test.async.action' ) ).toBe( false ); expect( didAction( 'test.async.action' ) ).toBe( 0 ); await doActionAsync( 'test.async.action', 0 ); expect( doingAction( 'test.async.action' ) ).toBe( false ); expect( didAction( 'test.async.action' ) ).toBe( 1 ); } ); test( 'is correctly tracked when multiple actions run at once', async () => { addAction( 'test.async.action1', 'callback_doing', async () => { await new Promise( ( r ) => setTimeout( () => r(), 10 ) ); expect( doingAction( 'test.async.action1' ) ).toBe( true ); await new Promise( ( r ) => setTimeout( () => r(), 10 ) ); } ); addAction( 'test.async.action2', 'callback_doing', async () => { await new Promise( ( r ) => setTimeout( () => r(), 10 ) ); expect( doingAction( 'test.async.action2' ) ).toBe( true ); await new Promise( ( r ) => setTimeout( () => r(), 10 ) ); } ); await Promise.all( [ doActionAsync( 'test.async.action1', 0 ), doActionAsync( 'test.async.action2', 0 ), ] ); } ); } );