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]>
+  }>
+}