@aventum/hooks
Version:
A universal, lightweight & efficient EventManager/PluginsSystem/MiddlewareManager/ExtendabilitySystem for JavaScript
776 lines (630 loc) • 23.9 kB
JavaScript
/* eslint-disable no-console */
/**
* Internal dependencies
*/
import {
createHooks,
addAction,
addFilter,
removeAction,
removeFilter,
hasAction,
hasFilter,
removeAllActions,
removeAllFilters,
doActionSync,
applyFiltersSync,
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 reseting 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(applyFiltersSync('test.filter', 42)).toBe(42)
})
test('add and remove a filter', () => {
addFilter('test.filter', 'my_callback', filterA)
expect(removeAllFilters('test.filter')).toBe(1)
expect(applyFiltersSync('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(applyFiltersSync('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(applyFiltersSync('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(applyFiltersSync('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(applyFiltersSync('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(applyFiltersSync('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(applyFiltersSync('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(doActionSync('test.action')).toBe(undefined)
expect(window.actionValue).toBe('')
})
test('add an action and run it', () => {
addAction('test.action', 'my_callback', actionA)
doActionSync('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)
doActionSync('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)
doActionSync('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)
})
doActionSync('test.action', arg1, arg2)
})
test('fire action multiple times', () => {
expect.assertions(2)
function func() {
expect(true).toBe(true)
}
addAction('test.action', 'my_callback', func)
doActionSync('test.action')
doActionSync('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(applyFiltersSync('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(applyFiltersSync('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(applyFiltersSync('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)
doActionSync('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)
doActionSync('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(applyFiltersSync('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(applyFiltersSync('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(applyFiltersSync('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(applyFiltersSync('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(applyFiltersSync('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(applyFiltersSync('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(applyFiltersSync('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(applyFiltersSync('test.filter', 'test')).toBe('test')
})
// Test doingAction, didAction, hasAction.
test('Test doingAction, didAction and hasAction.', () => {
let actionCalls = 0
addAction('another.action', 'my_callback', () => {})
doActionSync('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)
doActionSync('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)
doActionSync('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)
doActionSync('another.action')
expect(doingAction('test.action')).toBe(false)
// Verify an action with no handlers is still counted
expect(didAction('unattached.action')).toBe(0)
doActionSync('unattached.action')
expect(doingAction('unattached.action')).toBe(false)
expect(didAction('unattached.action')).toBe(1)
doActionSync('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 = applyFiltersSync('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 applyFiltersSync('test.filter', value + 'X')
})
expect(applyFiltersSync('test.filter', 'test')).toBe('testXXX')
})
test('current filter when multiple filters are running', () => {
addFilter('test.filter1', 'my_callback', (value) => {
return applyFiltersSync('test.filter2', value.concat(currentFilter()))
})
addFilter('test.filter2', 'my_callback', (value) => {
return value.concat(currentFilter())
})
expect(currentFilter()).toBe(null)
expect(applyFiltersSync('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 += '-' + applyFiltersSync('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(applyFiltersSync('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)
})
doActionSync('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 = applyFiltersSync('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.applyFiltersSync).toBe('function')
})
// Test adding as a mixin.
test('adding hooks as a mixin', () => {
const testObject = {}
Object.assign(testObject, createHooks())
expect(typeof testObject.applyFiltersSync).toBe('function')
})
// Test context.
test('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))
doActionSync('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
)
})
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
)
})
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')
})
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')
})
test('add an all filter and run it any hook to trigger it', () => {
addFilter('all', 'my_callback', filterA)
expect(applyFiltersSync('test.filter', 'test')).toBe('testa')
expect(applyFiltersSync('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.
doActionSync('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(applyFiltersSync('test.filter', 'test')).toBe('testab')
expect(applyFiltersSync('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.
doActionSync('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(applyFiltersSync('test.filter', 'test')).toBe('testba')
expect(applyFiltersSync('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.
doActionSync('test.action-anything')
expect(window.actionValue).toBe('ba')
})