Skip to content

Commit

Permalink
attempt at actionListenerMiddleware
Browse files Browse the repository at this point in the history
  • Loading branch information
phryneas committed May 9, 2020
1 parent cb1a287 commit cf44723
Show file tree
Hide file tree
Showing 3 changed files with 505 additions and 1 deletion.
2 changes: 1 addition & 1 deletion src/createAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export type _ActionCreatorWithPreparedPayload<
*
* @inheritdoc {redux#ActionCreator}
*/
interface BaseActionCreator<P, T extends string, M = never, E = never> {
export interface BaseActionCreator<P, T extends string, M = never, E = never> {
type: T
match(action: Action<unknown>): action is PayloadAction<P, T, M, E>
}
Expand Down
236 changes: 236 additions & 0 deletions src/entities/createActionListenerMiddleware.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
import { configureStore } from '../configureStore'
import {
createActionListenerMiddleware,
addListenerAction,
removeListenerAction
} from './createActionListenerMiddleware'
import { createAction } from '../createAction'

const middlewareApi = {
getState: expect.any(Function),
dispatch: expect.any(Function)
}

describe('createActionListenerMiddleware', () => {
let store = configureStore({
reducer: () => ({}),
middleware: [createActionListenerMiddleware()] as const
})
let reducer: jest.Mock
let middleware: ReturnType<typeof createActionListenerMiddleware>

const testAction1 = createAction<string>('testAction1')
type TestAction1 = ReturnType<typeof testAction1>
const testAction2 = createAction<string>('testAction2')

beforeEach(() => {
middleware = createActionListenerMiddleware()
reducer = jest.fn(() => ({}))
store = configureStore({
reducer,
middleware: [middleware] as const
})
})

test('directly subscribing', () => {
const listener = jest.fn((_: TestAction1) => {})

middleware.addListener(testAction1, listener)

store.dispatch(testAction1('a'))
store.dispatch(testAction2('b'))
store.dispatch(testAction1('c'))

expect(listener.mock.calls).toEqual([
[testAction1('a'), middlewareApi],
[testAction1('c'), middlewareApi]
])
})

test('subscribing with the same listener will not make it trigger twice (like EventTarget.addEventListener())', () => {
/**
* thoughts: allow to use this to override the options for a listener?
* right now it's just exiting if the listener is already registered
*/

const listener = jest.fn((_: TestAction1) => {})

middleware.addListener(testAction1, listener)
middleware.addListener(testAction1, listener)

store.dispatch(testAction1('a'))
store.dispatch(testAction2('b'))
store.dispatch(testAction1('c'))

expect(listener.mock.calls).toEqual([
[testAction1('a'), middlewareApi],
[testAction1('c'), middlewareApi]
])
})

test('unsubscribing via callback', () => {
const listener = jest.fn((_: TestAction1) => {})

const unsubscribe = middleware.addListener(testAction1, listener)

store.dispatch(testAction1('a'))
unsubscribe()
store.dispatch(testAction2('b'))
store.dispatch(testAction1('c'))

expect(listener.mock.calls).toEqual([[testAction1('a'), middlewareApi]])
})

test('directly unsubscribing', () => {
const listener = jest.fn((_: TestAction1) => {})

middleware.addListener(testAction1, listener)

store.dispatch(testAction1('a'))

middleware.removeListener(testAction1, listener)
store.dispatch(testAction2('b'))
store.dispatch(testAction1('c'))

expect(listener.mock.calls).toEqual([[testAction1('a'), middlewareApi]])
})

test('subscribing via action', () => {
const listener = jest.fn((_: TestAction1) => {})

store.dispatch(addListenerAction(testAction1, listener))

store.dispatch(testAction1('a'))
store.dispatch(testAction2('b'))
store.dispatch(testAction1('c'))

expect(listener.mock.calls).toEqual([
[testAction1('a'), middlewareApi],
[testAction1('c'), middlewareApi]
])
})

test('unsubscribing via callback from dispatch', () => {
const listener = jest.fn((_: TestAction1) => {})

const unsubscribe = store.dispatch(addListenerAction(testAction1, listener))

store.dispatch(testAction1('a'))
// @ts-ignore TODO types
unsubscribe()
store.dispatch(testAction2('b'))
store.dispatch(testAction1('c'))

expect(listener.mock.calls).toEqual([[testAction1('a'), middlewareApi]])
})

test('unsubscribing via action', () => {
const listener = jest.fn((_: TestAction1) => {})

middleware.addListener(testAction1, listener)

store.dispatch(testAction1('a'))

store.dispatch(removeListenerAction(testAction1, listener))
store.dispatch(testAction2('b'))
store.dispatch(testAction1('c'))

expect(listener.mock.calls).toEqual([[testAction1('a'), middlewareApi]])
})

test('"condition" allows to skip the listener', () => {
const listener = jest.fn((_: TestAction1) => {})

middleware.addListener(testAction1, listener, {
condition(action) {
return action.payload !== 'b'
}
})

store.dispatch(testAction1('a'))
store.dispatch(testAction1('b'))
store.dispatch(testAction1('c'))

expect(listener.mock.calls).toEqual([
[testAction1('a'), middlewareApi],
[testAction1('c'), middlewareApi]
])
})

test('"once" unsubscribes the listener automatically after one use', () => {
const listener = jest.fn((_: TestAction1) => {})

middleware.addListener(testAction1, listener, {
once: true
})

store.dispatch(testAction1('a'))
store.dispatch(testAction1('b'))
store.dispatch(testAction1('c'))

expect(listener.mock.calls).toEqual([[testAction1('a'), middlewareApi]])
})

test('combining "once" with "condition', () => {
const listener = jest.fn((_: TestAction1) => {})

middleware.addListener(testAction1, listener, {
once: true,
condition(action) {
return action.payload === 'b'
}
})

store.dispatch(testAction1('a'))
store.dispatch(testAction1('b'))
store.dispatch(testAction1('c'))

expect(listener.mock.calls).toEqual([[testAction1('b'), middlewareApi]])
})

test('by default, actions are forwarded to the store', () => {
reducer.mockClear()

const listener = jest.fn((_: TestAction1) => {})

middleware.addListener(testAction1, listener)

store.dispatch(testAction1('a'))

expect(reducer.mock.calls).toEqual([[{}, testAction1('a')]])
})

test('"preventPropagation" prevents actions from being forwarded to the store', () => {
reducer.mockClear()

const listener = jest.fn((_: TestAction1) => {})

middleware.addListener(testAction1, listener, { preventPropagation: true })

store.dispatch(testAction1('a'))

expect(reducer.mock.calls).toEqual([])
})

test('combining "preventPropagation" and "condition', () => {
reducer.mockClear()

const listener = jest.fn((_: TestAction1) => {})

middleware.addListener(testAction1, listener, {
preventPropagation: true,
condition(action) {
return action.payload === 'b'
}
})

store.dispatch(testAction1('a'))
store.dispatch(testAction1('b'))
store.dispatch(testAction1('c'))

expect(reducer.mock.calls).toEqual([
[{}, testAction1('a')],
[{}, testAction1('c')]
])
})
})
Loading

0 comments on commit cf44723

Please sign in to comment.