Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
229 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,229 @@ | ||
import * as Sentry from '@sentry/browser'; | ||
import { Scope } from '@sentry/types'; | ||
import * as Redux from 'redux'; | ||
|
||
import { createReduxEnhancer } from '../src/redux'; | ||
|
||
const mockAddBreadcrumb = jest.fn(); | ||
const mockSetExtra = jest.fn(); | ||
|
||
jest.mock('@sentry/browser', () => ({ | ||
configureScope: (callback: (scope: any) => Partial<Scope>) => | ||
callback({ | ||
addBreadcrumb: mockAddBreadcrumb, | ||
setExtra: mockSetExtra, | ||
}), | ||
})); | ||
|
||
afterEach(() => { | ||
mockAddBreadcrumb.mockReset(); | ||
mockSetExtra.mockReset(); | ||
}); | ||
|
||
describe('createReduxEnhancer', () => { | ||
it('logs redux action as breadcrumb', () => { | ||
const enhancer = createReduxEnhancer(); | ||
|
||
const initialState = {}; | ||
|
||
const store = Redux.createStore(() => initialState, enhancer); | ||
|
||
const action = { type: 'TEST_ACTION' }; | ||
store.dispatch(action); | ||
|
||
expect(mockAddBreadcrumb).toBeCalledWith({ | ||
category: 'redux.action', | ||
data: action, | ||
type: 'info', | ||
}); | ||
}); | ||
|
||
it('sets latest state on to scope', () => { | ||
const enhancer = createReduxEnhancer(); | ||
|
||
const initialState = { | ||
value: 'initial', | ||
}; | ||
const ACTION_TYPE = 'UPDATE_VALUE'; | ||
|
||
const store = Redux.createStore((state: object = initialState, action: { type: string; newValue: any }) => { | ||
if (action.type === ACTION_TYPE) { | ||
return { | ||
...state, | ||
value: action.newValue, | ||
}; | ||
} | ||
return state; | ||
}, enhancer); | ||
|
||
const updateAction = { type: ACTION_TYPE, newValue: 'updated' }; | ||
store.dispatch(updateAction); | ||
|
||
expect(mockSetExtra).toBeCalledWith('redux.state', { | ||
value: 'updated', | ||
}); | ||
}); | ||
|
||
describe('transformers', () => { | ||
it('transforms state', () => { | ||
const enhancer = createReduxEnhancer({ | ||
stateTransformer: state => ({ | ||
...state, | ||
superSecret: 'REDACTED', | ||
}), | ||
}); | ||
|
||
const initialState = { | ||
superSecret: 'SECRET!', | ||
value: 123, | ||
}; | ||
|
||
Redux.createStore((state = initialState) => state, enhancer); | ||
|
||
expect(mockSetExtra).toBeCalledWith('redux.state', { | ||
superSecret: 'REDACTED', | ||
value: 123, | ||
}); | ||
}); | ||
|
||
it('clears state if transformer returns null', () => { | ||
const enhancer = createReduxEnhancer({ | ||
stateTransformer: () => null, | ||
}); | ||
|
||
const initialState = { | ||
superSecret: 'SECRET!', | ||
value: 123, | ||
}; | ||
|
||
Redux.createStore((state = initialState) => state, enhancer); | ||
|
||
// Check that state is cleared | ||
expect(mockSetExtra).toBeCalledWith('redux.state', undefined); | ||
}); | ||
|
||
it('transforms actions', () => { | ||
const ACTION_TYPES = { | ||
SAFE: 'SAFE_ACTION', | ||
SECRET: 'SUPER_SECRET_ACTION', | ||
}; | ||
|
||
const enhancer = createReduxEnhancer({ | ||
actionTransformer: action => { | ||
if (action.type === ACTION_TYPES.SECRET) { | ||
return { | ||
...action, | ||
secret: 'I love pizza', | ||
}; | ||
} | ||
return action; | ||
}, | ||
}); | ||
|
||
const initialState = {}; | ||
|
||
const store = Redux.createStore(() => initialState, enhancer); | ||
|
||
store.dispatch({ | ||
secret: 'The Nuclear Launch Code is: Pizza', | ||
type: ACTION_TYPES.SECRET, | ||
}); | ||
|
||
expect(mockAddBreadcrumb).toBeCalledWith({ | ||
category: 'redux.action', | ||
data: { | ||
secret: 'I love pizza', | ||
type: ACTION_TYPES.SECRET, | ||
}, | ||
type: 'info', | ||
}); | ||
|
||
const safeAction = { | ||
secret: 'Not really a secret am I', | ||
type: ACTION_TYPES.SAFE, | ||
}; | ||
store.dispatch(safeAction); | ||
|
||
expect(mockAddBreadcrumb).toBeCalledWith({ | ||
category: 'redux.action', | ||
data: safeAction, | ||
type: 'info', | ||
}); | ||
|
||
// first time is redux initialize | ||
expect(mockAddBreadcrumb).toBeCalledTimes(3); | ||
}); | ||
|
||
it("doesn't send action if transformer returns null", () => { | ||
const enhancer = createReduxEnhancer({ | ||
actionTransformer: action => { | ||
if (action.type === 'COCA_COLA_RECIPE') { | ||
return null; | ||
} | ||
return action; | ||
}, | ||
}); | ||
|
||
const initialState = {}; | ||
|
||
const store = Redux.createStore((state = initialState) => state, enhancer); | ||
|
||
const safeAction = { | ||
type: 'SAFE', | ||
}; | ||
store.dispatch(safeAction); | ||
|
||
const secretAction = { | ||
cocaColaRecipe: { | ||
everythingElse: '10ml', | ||
sugar: '990ml', | ||
}, | ||
type: 'COCA_COLA_RECIPE', | ||
}; | ||
store.dispatch(secretAction); | ||
|
||
// first time is redux initialize | ||
expect(mockAddBreadcrumb).toBeCalledTimes(2); | ||
expect(mockAddBreadcrumb).toBeCalledWith({ | ||
category: 'redux.action', | ||
data: safeAction, | ||
type: 'info', | ||
}); | ||
}); | ||
}); | ||
|
||
it('configureScopeWithState is passed latest state', () => { | ||
const configureScopeWithState = jest.fn(); | ||
const enhancer = createReduxEnhancer({ | ||
configureScopeWithState, | ||
}); | ||
|
||
const initialState = { | ||
value: 'outdated', | ||
}; | ||
|
||
const UPDATE_VALUE = 'UPDATE_VALUE'; | ||
|
||
const store = Redux.createStore((state: object = initialState, action: { type: string; value: any }) => { | ||
if (action.type === UPDATE_VALUE) { | ||
return { | ||
...state, | ||
value: action.value, | ||
}; | ||
} | ||
return state; | ||
}, enhancer); | ||
|
||
store.dispatch({ | ||
type: UPDATE_VALUE, | ||
value: 'latest', | ||
}); | ||
|
||
let scopeRef; | ||
Sentry.configureScope(scope => (scopeRef = scope)); | ||
|
||
expect(configureScopeWithState).toBeCalledWith(scopeRef, { | ||
value: 'latest', | ||
}); | ||
}); | ||
}); |