| 1 | import * as DevTools from '@internal/devtoolsExtension'
|
|---|
| 2 | import type { Middleware, StoreEnhancer } from '@reduxjs/toolkit'
|
|---|
| 3 | import { Tuple } from '@reduxjs/toolkit'
|
|---|
| 4 | import type * as Redux from 'redux'
|
|---|
| 5 | import { vi } from 'vitest'
|
|---|
| 6 |
|
|---|
| 7 | vi.doMock('redux', async (importOriginal) => {
|
|---|
| 8 | const redux = await importOriginal<typeof import('redux')>()
|
|---|
| 9 |
|
|---|
| 10 | vi.spyOn(redux, 'applyMiddleware')
|
|---|
| 11 | vi.spyOn(redux, 'combineReducers')
|
|---|
| 12 | vi.spyOn(redux, 'compose')
|
|---|
| 13 | vi.spyOn(redux, 'createStore')
|
|---|
| 14 |
|
|---|
| 15 | return redux
|
|---|
| 16 | })
|
|---|
| 17 |
|
|---|
| 18 | describe('configureStore', async () => {
|
|---|
| 19 | const composeWithDevToolsSpy = vi.spyOn(DevTools, 'composeWithDevTools')
|
|---|
| 20 |
|
|---|
| 21 | const redux = await import('redux')
|
|---|
| 22 |
|
|---|
| 23 | const { configureStore } = await import('@reduxjs/toolkit')
|
|---|
| 24 |
|
|---|
| 25 | const reducer: Redux.Reducer = (state = {}, _action) => state
|
|---|
| 26 |
|
|---|
| 27 | beforeEach(() => {
|
|---|
| 28 | vi.clearAllMocks()
|
|---|
| 29 | })
|
|---|
| 30 |
|
|---|
| 31 | describe('given a function reducer', () => {
|
|---|
| 32 | it('calls createStore with the reducer', () => {
|
|---|
| 33 | configureStore({ reducer })
|
|---|
| 34 | expect(configureStore({ reducer })).toBeInstanceOf(Object)
|
|---|
| 35 |
|
|---|
| 36 | expect(redux.createStore).toHaveBeenCalledWith(
|
|---|
| 37 | reducer,
|
|---|
| 38 | undefined,
|
|---|
| 39 | expect.any(Function),
|
|---|
| 40 | )
|
|---|
| 41 | expect(redux.applyMiddleware).toHaveBeenCalled()
|
|---|
| 42 | if (process.env.TEST_DIST) {
|
|---|
| 43 | expect(composeWithDevToolsSpy).not.toHaveBeenCalled()
|
|---|
| 44 | } else {
|
|---|
| 45 | expect(composeWithDevToolsSpy).toHaveBeenCalledTimes(2)
|
|---|
| 46 | }
|
|---|
| 47 | })
|
|---|
| 48 | })
|
|---|
| 49 |
|
|---|
| 50 | describe('given an object of reducers', () => {
|
|---|
| 51 | it('calls createStore with the combined reducers', () => {
|
|---|
| 52 | const reducer = {
|
|---|
| 53 | reducer() {
|
|---|
| 54 | return true
|
|---|
| 55 | },
|
|---|
| 56 | }
|
|---|
| 57 | expect(configureStore({ reducer })).toBeInstanceOf(Object)
|
|---|
| 58 | expect(redux.combineReducers).toHaveBeenCalledWith(reducer)
|
|---|
| 59 | expect(redux.applyMiddleware).toHaveBeenCalled()
|
|---|
| 60 | if (process.env.TEST_DIST) {
|
|---|
| 61 | expect(composeWithDevToolsSpy).not.toHaveBeenCalled()
|
|---|
| 62 | } else {
|
|---|
| 63 | expect(composeWithDevToolsSpy).toHaveBeenCalledOnce()
|
|---|
| 64 | }
|
|---|
| 65 | expect(redux.createStore).toHaveBeenCalledWith(
|
|---|
| 66 | expect.any(Function),
|
|---|
| 67 | undefined,
|
|---|
| 68 | expect.any(Function),
|
|---|
| 69 | )
|
|---|
| 70 | })
|
|---|
| 71 | })
|
|---|
| 72 |
|
|---|
| 73 | describe('given no reducer', () => {
|
|---|
| 74 | it('throws', () => {
|
|---|
| 75 | expect(configureStore).toThrow(
|
|---|
| 76 | '`reducer` is a required argument, and must be a function or an object of functions that can be passed to combineReducers',
|
|---|
| 77 | )
|
|---|
| 78 | })
|
|---|
| 79 | })
|
|---|
| 80 |
|
|---|
| 81 | describe('given no middleware', () => {
|
|---|
| 82 | it('calls createStore without any middleware', () => {
|
|---|
| 83 | expect(
|
|---|
| 84 | configureStore({ middleware: () => new Tuple(), reducer }),
|
|---|
| 85 | ).toBeInstanceOf(Object)
|
|---|
| 86 | expect(redux.applyMiddleware).toHaveBeenCalledWith()
|
|---|
| 87 | if (process.env.TEST_DIST) {
|
|---|
| 88 | expect(composeWithDevToolsSpy).not.toHaveBeenCalled()
|
|---|
| 89 | } else {
|
|---|
| 90 | expect(composeWithDevToolsSpy).toHaveBeenCalledOnce()
|
|---|
| 91 | }
|
|---|
| 92 | expect(redux.createStore).toHaveBeenCalledWith(
|
|---|
| 93 | reducer,
|
|---|
| 94 | undefined,
|
|---|
| 95 | expect.any(Function),
|
|---|
| 96 | )
|
|---|
| 97 | })
|
|---|
| 98 | })
|
|---|
| 99 |
|
|---|
| 100 | describe('given an array of middleware', () => {
|
|---|
| 101 | it('throws an error requiring a callback', () => {
|
|---|
| 102 | // @ts-expect-error
|
|---|
| 103 | expect(() => configureStore({ middleware: [], reducer })).toThrow(
|
|---|
| 104 | '`middleware` field must be a callback',
|
|---|
| 105 | )
|
|---|
| 106 | })
|
|---|
| 107 | })
|
|---|
| 108 |
|
|---|
| 109 | describe('given undefined middleware', () => {
|
|---|
| 110 | it('calls createStore with default middleware', () => {
|
|---|
| 111 | expect(configureStore({ middleware: undefined, reducer })).toBeInstanceOf(
|
|---|
| 112 | Object,
|
|---|
| 113 | )
|
|---|
| 114 | expect(redux.applyMiddleware).toHaveBeenCalledWith(
|
|---|
| 115 | expect.any(Function), // immutableCheck
|
|---|
| 116 | expect.any(Function), // thunk
|
|---|
| 117 | expect.any(Function), // serializableCheck
|
|---|
| 118 | expect.any(Function), // actionCreatorCheck
|
|---|
| 119 | )
|
|---|
| 120 | if (process.env.TEST_DIST) {
|
|---|
| 121 | expect(composeWithDevToolsSpy).not.toHaveBeenCalled()
|
|---|
| 122 | } else {
|
|---|
| 123 | expect(composeWithDevToolsSpy).toHaveBeenCalledOnce()
|
|---|
| 124 | }
|
|---|
| 125 | expect(redux.createStore).toHaveBeenCalledWith(
|
|---|
| 126 | reducer,
|
|---|
| 127 | undefined,
|
|---|
| 128 | expect.any(Function),
|
|---|
| 129 | )
|
|---|
| 130 | })
|
|---|
| 131 | })
|
|---|
| 132 |
|
|---|
| 133 | describe('given any middleware', () => {
|
|---|
| 134 | const exampleMiddleware: Middleware<any, any> = () => (next) => (action) =>
|
|---|
| 135 | next(action)
|
|---|
| 136 | it('throws an error by default if there are duplicate middleware', () => {
|
|---|
| 137 | const makeStore = () => {
|
|---|
| 138 | return configureStore({
|
|---|
| 139 | reducer,
|
|---|
| 140 | middleware: (gDM) =>
|
|---|
| 141 | gDM().concat(exampleMiddleware, exampleMiddleware),
|
|---|
| 142 | })
|
|---|
| 143 | }
|
|---|
| 144 |
|
|---|
| 145 | expect(makeStore).toThrowError(
|
|---|
| 146 | 'Duplicate middleware references found when creating the store. Ensure that each middleware is only included once.',
|
|---|
| 147 | )
|
|---|
| 148 | })
|
|---|
| 149 |
|
|---|
| 150 | it('does not throw a duplicate middleware error if duplicateMiddlewareCheck is disabled', () => {
|
|---|
| 151 | const makeStore = () => {
|
|---|
| 152 | return configureStore({
|
|---|
| 153 | reducer,
|
|---|
| 154 | middleware: (gDM) =>
|
|---|
| 155 | gDM().concat(exampleMiddleware, exampleMiddleware),
|
|---|
| 156 | duplicateMiddlewareCheck: false,
|
|---|
| 157 | })
|
|---|
| 158 | }
|
|---|
| 159 |
|
|---|
| 160 | expect(makeStore).not.toThrowError()
|
|---|
| 161 | })
|
|---|
| 162 | })
|
|---|
| 163 |
|
|---|
| 164 | describe('given a middleware creation function that returns undefined', () => {
|
|---|
| 165 | it('throws an error', () => {
|
|---|
| 166 | const invalidBuilder = vi.fn((getDefaultMiddleware) => undefined as any)
|
|---|
| 167 | expect(() =>
|
|---|
| 168 | configureStore({ middleware: invalidBuilder, reducer }),
|
|---|
| 169 | ).toThrow(
|
|---|
| 170 | 'when using a middleware builder function, an array of middleware must be returned',
|
|---|
| 171 | )
|
|---|
| 172 | })
|
|---|
| 173 | })
|
|---|
| 174 |
|
|---|
| 175 | describe('given a middleware creation function that returns an array with non-functions', () => {
|
|---|
| 176 | it('throws an error', () => {
|
|---|
| 177 | const invalidBuilder = vi.fn((getDefaultMiddleware) => [true] as any)
|
|---|
| 178 | expect(() =>
|
|---|
| 179 | configureStore({ middleware: invalidBuilder, reducer }),
|
|---|
| 180 | ).toThrow('each middleware provided to configureStore must be a function')
|
|---|
| 181 | })
|
|---|
| 182 | })
|
|---|
| 183 |
|
|---|
| 184 | describe('given custom middleware', () => {
|
|---|
| 185 | it('calls createStore with custom middleware and without default middleware', () => {
|
|---|
| 186 | const thank: Redux.Middleware = (_store) => (next) => (action) =>
|
|---|
| 187 | next(action)
|
|---|
| 188 | expect(
|
|---|
| 189 | configureStore({ middleware: () => new Tuple(thank), reducer }),
|
|---|
| 190 | ).toBeInstanceOf(Object)
|
|---|
| 191 | expect(redux.applyMiddleware).toHaveBeenCalledWith(thank)
|
|---|
| 192 | if (process.env.TEST_DIST) {
|
|---|
| 193 | expect(composeWithDevToolsSpy).not.toHaveBeenCalled()
|
|---|
| 194 | } else {
|
|---|
| 195 | expect(composeWithDevToolsSpy).toHaveBeenCalledOnce()
|
|---|
| 196 | }
|
|---|
| 197 | expect(redux.createStore).toHaveBeenCalledWith(
|
|---|
| 198 | reducer,
|
|---|
| 199 | undefined,
|
|---|
| 200 | expect.any(Function),
|
|---|
| 201 | )
|
|---|
| 202 | })
|
|---|
| 203 | })
|
|---|
| 204 |
|
|---|
| 205 | describe('middleware builder notation', () => {
|
|---|
| 206 | it('calls builder, passes getDefaultMiddleware and uses returned middlewares', () => {
|
|---|
| 207 | const thank = vi.fn(
|
|---|
| 208 | ((_store) => (next) => (action) => 'foobar') as Redux.Middleware,
|
|---|
| 209 | )
|
|---|
| 210 |
|
|---|
| 211 | const builder = vi.fn((getDefaultMiddleware) => {
|
|---|
| 212 | expect(getDefaultMiddleware).toEqual(expect.any(Function))
|
|---|
| 213 | expect(getDefaultMiddleware()).toEqual(expect.any(Array))
|
|---|
| 214 |
|
|---|
| 215 | return new Tuple(thank)
|
|---|
| 216 | })
|
|---|
| 217 |
|
|---|
| 218 | const store = configureStore({ middleware: builder, reducer })
|
|---|
| 219 |
|
|---|
| 220 | expect(builder).toHaveBeenCalled()
|
|---|
| 221 |
|
|---|
| 222 | expect(store.dispatch({ type: 'test' })).toBe('foobar')
|
|---|
| 223 | })
|
|---|
| 224 | })
|
|---|
| 225 |
|
|---|
| 226 | describe('with devTools disabled', () => {
|
|---|
| 227 | it('calls createStore without devTools enhancer', () => {
|
|---|
| 228 | expect(configureStore({ devTools: false, reducer })).toBeInstanceOf(
|
|---|
| 229 | Object,
|
|---|
| 230 | )
|
|---|
| 231 | expect(redux.applyMiddleware).toHaveBeenCalled()
|
|---|
| 232 | expect(redux.compose).toHaveBeenCalled()
|
|---|
| 233 | expect(redux.createStore).toHaveBeenCalledWith(
|
|---|
| 234 | reducer,
|
|---|
| 235 | undefined,
|
|---|
| 236 | expect.any(Function),
|
|---|
| 237 | )
|
|---|
| 238 | })
|
|---|
| 239 | })
|
|---|
| 240 |
|
|---|
| 241 | describe('with devTools options', () => {
|
|---|
| 242 | it('calls createStore with devTools enhancer and option', () => {
|
|---|
| 243 | const options = {
|
|---|
| 244 | name: 'myApp',
|
|---|
| 245 | trace: true,
|
|---|
| 246 | }
|
|---|
| 247 | expect(configureStore({ devTools: options, reducer })).toBeInstanceOf(
|
|---|
| 248 | Object,
|
|---|
| 249 | )
|
|---|
| 250 | expect(redux.applyMiddleware).toHaveBeenCalled()
|
|---|
| 251 | if (process.env.TEST_DIST) {
|
|---|
| 252 | expect(composeWithDevToolsSpy).not.toHaveBeenCalled()
|
|---|
| 253 | } else {
|
|---|
| 254 | expect(composeWithDevToolsSpy).toHaveBeenCalledOnce()
|
|---|
| 255 |
|
|---|
| 256 | expect(composeWithDevToolsSpy).toHaveBeenLastCalledWith(options)
|
|---|
| 257 | }
|
|---|
| 258 | expect(redux.createStore).toHaveBeenCalledWith(
|
|---|
| 259 | reducer,
|
|---|
| 260 | undefined,
|
|---|
| 261 | expect.any(Function),
|
|---|
| 262 | )
|
|---|
| 263 | })
|
|---|
| 264 | })
|
|---|
| 265 |
|
|---|
| 266 | describe('given preloadedState', () => {
|
|---|
| 267 | it('calls createStore with preloadedState', () => {
|
|---|
| 268 | expect(configureStore({ reducer })).toBeInstanceOf(Object)
|
|---|
| 269 | expect(redux.applyMiddleware).toHaveBeenCalled()
|
|---|
| 270 | if (process.env.TEST_DIST) {
|
|---|
| 271 | expect(composeWithDevToolsSpy).not.toHaveBeenCalled()
|
|---|
| 272 | } else {
|
|---|
| 273 | expect(composeWithDevToolsSpy).toHaveBeenCalledOnce()
|
|---|
| 274 | }
|
|---|
| 275 | expect(redux.createStore).toHaveBeenCalledWith(
|
|---|
| 276 | reducer,
|
|---|
| 277 | undefined,
|
|---|
| 278 | expect.any(Function),
|
|---|
| 279 | )
|
|---|
| 280 | })
|
|---|
| 281 | })
|
|---|
| 282 |
|
|---|
| 283 | describe('given enhancers', () => {
|
|---|
| 284 | let dummyEnhancerCalled = false
|
|---|
| 285 |
|
|---|
| 286 | const dummyEnhancer: StoreEnhancer =
|
|---|
| 287 | (createStore) => (reducer, preloadedState) => {
|
|---|
| 288 | dummyEnhancerCalled = true
|
|---|
| 289 |
|
|---|
| 290 | return createStore(reducer, preloadedState)
|
|---|
| 291 | }
|
|---|
| 292 |
|
|---|
| 293 | beforeEach(() => {
|
|---|
| 294 | dummyEnhancerCalled = false
|
|---|
| 295 | })
|
|---|
| 296 |
|
|---|
| 297 | it('calls createStore with enhancers', () => {
|
|---|
| 298 | expect(
|
|---|
| 299 | configureStore({
|
|---|
| 300 | enhancers: (gDE) => gDE().concat(dummyEnhancer),
|
|---|
| 301 | reducer,
|
|---|
| 302 | }),
|
|---|
| 303 | ).toBeInstanceOf(Object)
|
|---|
| 304 | expect(redux.applyMiddleware).toHaveBeenCalled()
|
|---|
| 305 | if (process.env.TEST_DIST) {
|
|---|
| 306 | expect(composeWithDevToolsSpy).not.toHaveBeenCalled()
|
|---|
| 307 | } else {
|
|---|
| 308 | expect(composeWithDevToolsSpy).toHaveBeenCalledOnce()
|
|---|
| 309 | }
|
|---|
| 310 | expect(redux.createStore).toHaveBeenCalledWith(
|
|---|
| 311 | reducer,
|
|---|
| 312 | undefined,
|
|---|
| 313 | expect.any(Function),
|
|---|
| 314 | )
|
|---|
| 315 |
|
|---|
| 316 | expect(dummyEnhancerCalled).toBe(true)
|
|---|
| 317 | })
|
|---|
| 318 |
|
|---|
| 319 | describe('invalid arguments', () => {
|
|---|
| 320 | test('enhancers is not a callback', () => {
|
|---|
| 321 | expect(() => configureStore({ reducer, enhancers: [] as any })).toThrow(
|
|---|
| 322 | '`enhancers` field must be a callback',
|
|---|
| 323 | )
|
|---|
| 324 | })
|
|---|
| 325 |
|
|---|
| 326 | test('callback fails to return array', () => {
|
|---|
| 327 | expect(() =>
|
|---|
| 328 | configureStore({ reducer, enhancers: (() => {}) as any }),
|
|---|
| 329 | ).toThrow('`enhancers` callback must return an array')
|
|---|
| 330 | })
|
|---|
| 331 |
|
|---|
| 332 | test('array contains non-function', () => {
|
|---|
| 333 | expect(() =>
|
|---|
| 334 | configureStore({ reducer, enhancers: (() => ['']) as any }),
|
|---|
| 335 | ).toThrow('each enhancer provided to configureStore must be a function')
|
|---|
| 336 | })
|
|---|
| 337 | })
|
|---|
| 338 |
|
|---|
| 339 | const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|---|
| 340 | beforeEach(() => {
|
|---|
| 341 | consoleSpy.mockClear()
|
|---|
| 342 | })
|
|---|
| 343 | afterAll(() => {
|
|---|
| 344 | consoleSpy.mockRestore()
|
|---|
| 345 | })
|
|---|
| 346 |
|
|---|
| 347 | it('warns if middleware enhancer is excluded from final array when middlewares are provided', () => {
|
|---|
| 348 | const store = configureStore({
|
|---|
| 349 | reducer,
|
|---|
| 350 | enhancers: () => new Tuple(dummyEnhancer),
|
|---|
| 351 | })
|
|---|
| 352 |
|
|---|
| 353 | expect(dummyEnhancerCalled).toBe(true)
|
|---|
| 354 |
|
|---|
| 355 | expect(consoleSpy).toHaveBeenCalledWith(
|
|---|
| 356 | 'middlewares were provided, but middleware enhancer was not included in final enhancers - make sure to call `getDefaultEnhancers`',
|
|---|
| 357 | )
|
|---|
| 358 | })
|
|---|
| 359 | it("doesn't warn when middleware enhancer is excluded if no middlewares provided", () => {
|
|---|
| 360 | const store = configureStore({
|
|---|
| 361 | reducer,
|
|---|
| 362 | middleware: () => new Tuple(),
|
|---|
| 363 | enhancers: () => new Tuple(dummyEnhancer),
|
|---|
| 364 | })
|
|---|
| 365 |
|
|---|
| 366 | expect(dummyEnhancerCalled).toBe(true)
|
|---|
| 367 |
|
|---|
| 368 | expect(consoleSpy).not.toHaveBeenCalled()
|
|---|
| 369 | })
|
|---|
| 370 | })
|
|---|
| 371 | })
|
|---|