From 441319a522214757ff189662d07da5d8409b01e0 Mon Sep 17 00:00:00 2001 From: Ben Durrant <ben.j.durrant@gmail.com> Date: Sat, 30 Nov 2024 16:45:43 +0000 Subject: [PATCH] Add `importWithEnv` util and disposable console spy --- .../toolkit/src/entities/tests/utils.spec.ts | 31 +++++--------- .../toolkit/src/tests/createReducer.test.ts | 40 ++++++------------- .../src/tests/getDefaultMiddleware.test.ts | 30 ++++++-------- packages/toolkit/src/tests/utils/helpers.tsx | 23 +++++++++++ packages/toolkit/src/utils.ts | 16 ++++++++ 5 files changed, 75 insertions(+), 65 deletions(-) diff --git a/packages/toolkit/src/entities/tests/utils.spec.ts b/packages/toolkit/src/entities/tests/utils.spec.ts index cc4053cd18..ed2f72cd51 100644 --- a/packages/toolkit/src/entities/tests/utils.spec.ts +++ b/packages/toolkit/src/entities/tests/utils.spec.ts @@ -1,31 +1,27 @@ -import { vi } from 'vitest' import { AClockworkOrange } from './fixtures/book' +import { consoleSpy, makeImportWithEnv } from '@internal/tests/utils/helpers' describe('Entity utils', () => { describe(`selectIdValue()`, () => { - const OLD_ENV = process.env + const importWithEnv = makeImportWithEnv(() => + import('../utils').then((m) => m.selectIdValue), + ) - beforeEach(() => { - vi.resetModules() // this is important - it clears the cache - process.env = { ...OLD_ENV, NODE_ENV: 'development' } - }) + using spy = consoleSpy('warn') afterEach(() => { - process.env = OLD_ENV - vi.resetAllMocks() + spy.mockReset() }) it('should not warn when key does exist', async () => { - const { selectIdValue } = await import('../utils') - const spy = vi.spyOn(console, 'warn') + using selectIdValue = await importWithEnv('development') selectIdValue(AClockworkOrange, (book: any) => book.id) expect(spy).not.toHaveBeenCalled() }) it('should warn when key does not exist in dev mode', async () => { - const { selectIdValue } = await import('../utils') - const spy = vi.spyOn(console, 'warn') + using selectIdValue = await importWithEnv('development') selectIdValue(AClockworkOrange, (book: any) => book.foo) @@ -33,8 +29,7 @@ describe('Entity utils', () => { }) it('should warn when key is undefined in dev mode', async () => { - const { selectIdValue } = await import('../utils') - const spy = vi.spyOn(console, 'warn') + using selectIdValue = await importWithEnv('development') const undefinedAClockworkOrange = { ...AClockworkOrange, id: undefined } selectIdValue(undefinedAClockworkOrange, (book: any) => book.id) @@ -43,9 +38,7 @@ describe('Entity utils', () => { }) it('should not warn when key does not exist in prod mode', async () => { - process.env.NODE_ENV = 'production' - const { selectIdValue } = await import('../utils') - const spy = vi.spyOn(console, 'warn') + using selectIdValue = await importWithEnv('production') selectIdValue(AClockworkOrange, (book: any) => book.foo) @@ -53,9 +46,7 @@ describe('Entity utils', () => { }) it('should not warn when key is undefined in prod mode', async () => { - process.env.NODE_ENV = 'production' - const { selectIdValue } = await import('../utils') - const spy = vi.spyOn(console, 'warn') + using selectIdValue = await importWithEnv('production') const undefinedAClockworkOrange = { ...AClockworkOrange, id: undefined } selectIdValue(undefinedAClockworkOrange, (book: any) => book.id) diff --git a/packages/toolkit/src/tests/createReducer.test.ts b/packages/toolkit/src/tests/createReducer.test.ts index e5ea7365a3..287cef0577 100644 --- a/packages/toolkit/src/tests/createReducer.test.ts +++ b/packages/toolkit/src/tests/createReducer.test.ts @@ -13,6 +13,7 @@ import { createConsole, getLog, } from 'console-testing-library/pure' +import { makeImportWithEnv } from './utils/helpers' interface Todo { text: string @@ -39,7 +40,9 @@ type ToggleTodoReducer = CaseReducer< PayloadAction<ToggleTodoPayload, 'TOGGLE_TODO'> > -type CreateReducer = typeof createReducer +const importWithEnv = makeImportWithEnv(() => + import('../createReducer').then((m) => m.createReducer), +) describe('createReducer', () => { let restore: () => void @@ -71,21 +74,12 @@ describe('createReducer', () => { }) describe('Deprecation warnings', () => { - let originalNodeEnv = process.env.NODE_ENV - - beforeEach(() => { - vi.resetModules() - }) - - afterEach(() => { - process.env.NODE_ENV = originalNodeEnv - }) - it('Throws an error if the legacy object notation is used', async () => { - const { createReducer } = await import('../createReducer') + using createReducer = await importWithEnv('development') + const wrapper = () => { // @ts-ignore - let dummyReducer = (createReducer as CreateReducer)([] as TodoState, {}) + let dummyReducer = createReducer([], {}) } expect(wrapper).toThrowError( @@ -98,11 +92,11 @@ describe('createReducer', () => { }) it('Crashes in production', async () => { - process.env.NODE_ENV = 'production' - const { createReducer } = await import('../createReducer') + using createReducer = await importWithEnv('production') + const wrapper = () => { // @ts-ignore - let dummyReducer = (createReducer as CreateReducer)([] as TodoState, {}) + let dummyReducer = createReducer([], {}) } expect(wrapper).toThrowError() @@ -110,19 +104,9 @@ describe('createReducer', () => { }) describe('Immer in a production environment', () => { - let originalNodeEnv = process.env.NODE_ENV - - beforeEach(() => { - vi.resetModules() - process.env.NODE_ENV = 'production' - }) - - afterEach(() => { - process.env.NODE_ENV = originalNodeEnv - }) - test('Freezes data in production', async () => { - const { createReducer } = await import('../createReducer') + using createReducer = await importWithEnv('production') + const addTodo: AddTodoReducer = (state, action) => { const { newTodo } = action.payload state.push({ ...newTodo, completed: false }) diff --git a/packages/toolkit/src/tests/getDefaultMiddleware.test.ts b/packages/toolkit/src/tests/getDefaultMiddleware.test.ts index b72a959b0b..b492c85641 100644 --- a/packages/toolkit/src/tests/getDefaultMiddleware.test.ts +++ b/packages/toolkit/src/tests/getDefaultMiddleware.test.ts @@ -1,4 +1,4 @@ -import { Tuple } from '@internal/utils' +import { promiseOwnProperties, Tuple } from '@internal/utils' import type { Action, Middleware, @@ -7,30 +7,26 @@ import type { } from '@reduxjs/toolkit' import { configureStore } from '@reduxjs/toolkit' import { thunk } from 'redux-thunk' -import { vi } from 'vitest' +import { makeImportWithEnv } from './utils/helpers' import { buildGetDefaultMiddleware } from '@internal/getDefaultMiddleware' const getDefaultMiddleware = buildGetDefaultMiddleware() -describe('getDefaultMiddleware', () => { - const ORIGINAL_NODE_ENV = process.env.NODE_ENV - - afterEach(() => { - process.env.NODE_ENV = ORIGINAL_NODE_ENV - }) +const importWithEnv = makeImportWithEnv(() => + promiseOwnProperties({ + buildGetDefaultMiddleware: import('../getDefaultMiddleware').then( + (m) => m.buildGetDefaultMiddleware, + ), + thunk: import('redux-thunk').then((m) => m.thunk), + }), +) +describe('getDefaultMiddleware', () => { describe('Production behavior', () => { - beforeEach(() => { - vi.resetModules() - }) - it('returns an array with only redux-thunk in production', async () => { - process.env.NODE_ENV = 'production' - const { thunk } = await import('redux-thunk') - const { buildGetDefaultMiddleware } = await import( - '@internal/getDefaultMiddleware' - ) + using imports = await importWithEnv('production') + const { buildGetDefaultMiddleware, thunk } = imports const middleware = buildGetDefaultMiddleware()() expect(middleware).toContain(thunk) diff --git a/packages/toolkit/src/tests/utils/helpers.tsx b/packages/toolkit/src/tests/utils/helpers.tsx index 5ccedc434d..62d6f33854 100644 --- a/packages/toolkit/src/tests/utils/helpers.tsx +++ b/packages/toolkit/src/tests/utils/helpers.tsx @@ -268,3 +268,26 @@ export function setupApiStore< return refObj } + +export const makeImportWithEnv = + <T extends {}>(getModule: () => Promise<T>) => + async (env: string) => { + const originalEnv = process.env.NODE_ENV + vi.stubEnv('NODE_ENV', env) + vi.resetModules() + const result = await getModule() + return Object.assign(result, { + [Symbol.dispose]() { + process.env.NODE_ENV = originalEnv + }, + } satisfies Disposable) + } + +export const consoleSpy = <K extends keyof Console>(key: K) => { + const spy = vi.spyOn(console, key) + return Object.assign(spy, { + [Symbol.dispose]() { + spy.mockRestore() + }, + } satisfies Disposable) +} diff --git a/packages/toolkit/src/utils.ts b/packages/toolkit/src/utils.ts index 6607f4b339..bd1d29f73d 100644 --- a/packages/toolkit/src/utils.ts +++ b/packages/toolkit/src/utils.ts @@ -109,3 +109,19 @@ export function getOrInsertComputed<K extends object, V>( return map.set(key, compute(key)).get(key) as V } + +export async function promiseFromEntries<T>( + entries: Iterable<readonly [PropertyKey, T]>, +) { + return Object.fromEntries( + await Promise.all( + Array.from(entries, async ([key, value]) => [key, await value] as const), + ), + ) +} + +export async function promiseOwnProperties<T extends {}>(obj: T) { + return promiseFromEntries(Object.entries(obj)) as Promise<{ + [K in keyof T]: Awaited<T[K]> + }> +}