jest fixes

This commit is contained in:
karilint
2026-03-18 09:49:02 +02:00
parent c38b3f280a
commit b3f4464b11
9 changed files with 104 additions and 10 deletions
+1
View File
@@ -12,6 +12,7 @@ lerna-debug.log*
node_modules
dist
dist-ssr
.jest-cache
*.local
# Editor directories and files
+6 -1
View File
@@ -3,11 +3,16 @@ export default {
'^.+\\.tsx?$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.json',
isolatedModules: true,
tsconfig: '<rootDir>/tsconfig.jest.json',
},
],
},
moduleNameMapper: {
'^.+\\.css$': '<rootDir>/src/tests/mocks/styleMock.ts',
'^lodash-es$': '<rootDir>/src/tests/mocks/lodashEs.ts',
'^@/util/config$': '<rootDir>/src/tests/mocks/config.ts',
'^\\.\\.?/util/config$': '<rootDir>/src/tests/mocks/config.ts',
'^@/(.*)$': '<rootDir>/src/$1',
'^@components/Table$': '<rootDir>/src/components/table',
'^@components/(.*)$': '<rootDir>/src/components/$1',
@@ -11,7 +11,11 @@ import type { EditDataType } from '@/shared/types'
jest.mock('lodash-es', () => ({
cloneDeep: <T,>(value: T) => structuredClone(value),
isEqual: (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b),
isEqualWith: (a: unknown, b: unknown, customizer?: (left: unknown, right: unknown) => boolean | undefined) => {
const customizedResult = customizer?.(a, b)
if (customizedResult !== undefined) return customizedResult
return JSON.stringify(a) === JSON.stringify(b)
},
}))
type TestData = { name: string; count: number }
@@ -1,14 +1,34 @@
import { renderHook } from '@testing-library/react'
import { ReactNode } from 'react'
import { UnsavedChangesProvider } from '@/components/UnsavedChangesProvider'
import { useUnsavedChangesPrompt } from '../../hooks/useUnsavedChangesPrompt'
import { useBlocker } from 'react-router-dom'
jest.mock('react-router-dom', () => {
const actual = jest.requireActual<typeof import('react-router-dom')>('react-router-dom')
return {
...actual,
useBlocker: jest.fn(),
}
})
const wrapper = ({ children }: { children: ReactNode }) => <UnsavedChangesProvider>{children}</UnsavedChangesProvider>
describe('useUnsavedChangesPrompt', () => {
let addEventListenerSpy: jest.SpyInstance
let removeEventListenerSpy: jest.SpyInstance
const mockUseBlocker = useBlocker as jest.MockedFunction<typeof useBlocker>
beforeEach(() => {
// Mock window.addEventListener and removeEventListener
addEventListenerSpy = jest.spyOn(window, 'addEventListener')
removeEventListenerSpy = jest.spyOn(window, 'removeEventListener')
mockUseBlocker.mockReturnValue({
state: 'unblocked',
proceed: undefined,
reset: undefined,
location: undefined,
} as ReturnType<typeof useBlocker>)
})
afterEach(() => {
@@ -18,19 +38,19 @@ describe('useUnsavedChangesPrompt', () => {
})
it('should add beforeunload event listener when hasUnsavedChanges is true', () => {
renderHook(() => useUnsavedChangesPrompt(true))
renderHook(() => useUnsavedChangesPrompt(true), { wrapper })
expect(addEventListenerSpy).toHaveBeenCalledWith('beforeunload', expect.any(Function))
})
it('should not add beforeunload event listener when hasUnsavedChanges is false', () => {
renderHook(() => useUnsavedChangesPrompt(false))
renderHook(() => useUnsavedChangesPrompt(false), { wrapper })
expect(addEventListenerSpy).not.toHaveBeenCalled()
})
it('should remove event listener on cleanup when hasUnsavedChanges was true', () => {
const { unmount } = renderHook(() => useUnsavedChangesPrompt(true))
const { unmount } = renderHook(() => useUnsavedChangesPrompt(true), { wrapper })
// Get the handler that was added
const handler = (addEventListenerSpy.mock.calls[0] as unknown[])?.[1] as (event: BeforeUnloadEvent) => void
@@ -43,6 +63,7 @@ describe('useUnsavedChangesPrompt', () => {
it('should update event listener when hasUnsavedChanges changes from false to true', () => {
const { rerender } = renderHook(({ hasUnsavedChanges }) => useUnsavedChangesPrompt(hasUnsavedChanges), {
initialProps: { hasUnsavedChanges: false },
wrapper,
})
expect(addEventListenerSpy).not.toHaveBeenCalled()
@@ -55,6 +76,7 @@ describe('useUnsavedChangesPrompt', () => {
it('should remove event listener when hasUnsavedChanges changes from true to false', () => {
const { rerender } = renderHook(({ hasUnsavedChanges }) => useUnsavedChangesPrompt(hasUnsavedChanges), {
initialProps: { hasUnsavedChanges: true },
wrapper,
})
const handler = (addEventListenerSpy.mock.calls[0] as unknown[])?.[1] as (event: BeforeUnloadEvent) => void
@@ -65,7 +87,7 @@ describe('useUnsavedChangesPrompt', () => {
})
it('should call preventDefault on the event when hasUnsavedChanges is true', () => {
renderHook(() => useUnsavedChangesPrompt(true))
renderHook(() => useUnsavedChangesPrompt(true), { wrapper })
// Get the handler function that was registered
const handler = (addEventListenerSpy.mock.calls[0] as unknown[])?.[1] as (event: BeforeUnloadEvent) => void
@@ -81,11 +103,11 @@ describe('useUnsavedChangesPrompt', () => {
// eslint-disable-next-line @typescript-eslint/unbound-method
expect(mockEvent.preventDefault).toHaveBeenCalled()
expect(mockEvent.returnValue).toBe('')
expect(mockEvent.returnValue).toBe('You have unsaved changes. Do you want to leave this page without saving?')
})
it('should set returnValue to empty string to trigger browser prompt', () => {
renderHook(() => useUnsavedChangesPrompt(true))
it('should set returnValue to the active prompt message', () => {
renderHook(() => useUnsavedChangesPrompt(true), { wrapper })
const handler = (addEventListenerSpy.mock.calls[0] as unknown[])?.[1] as (event: BeforeUnloadEvent) => void
@@ -96,12 +118,13 @@ describe('useUnsavedChangesPrompt', () => {
handler(mockEvent)
expect(mockEvent.returnValue).toBe('')
expect(mockEvent.returnValue).toBe('You have unsaved changes. Do you want to leave this page without saving?')
})
it('should not add multiple event listeners when re-rendered with same hasUnsavedChanges value', () => {
const { rerender } = renderHook(({ hasUnsavedChanges }) => useUnsavedChangesPrompt(hasUnsavedChanges), {
initialProps: { hasUnsavedChanges: true },
wrapper,
})
expect(addEventListenerSpy).toHaveBeenCalledTimes(1)
+3
View File
@@ -0,0 +1,3 @@
export const ENV = 'dev' as const
export const BACKEND_URL = 'http://localhost'
export const ENABLE_WRITE = true
+39
View File
@@ -0,0 +1,39 @@
const cloneValue = <T,>(value: T): T => {
if (typeof structuredClone === 'function') {
return structuredClone(value)
}
return JSON.parse(JSON.stringify(value)) as T
}
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
typeof value === 'object' && value !== null && !Array.isArray(value)
export const cloneDeep = cloneValue
export const isEqualWith = (
value: unknown,
other: unknown,
customizer?: (value: unknown, other: unknown) => boolean | undefined
): boolean => {
const customizedResult = customizer?.(value, other)
if (customizedResult !== undefined) return customizedResult
if (Object.is(value, other)) return true
if (Array.isArray(value) && Array.isArray(other)) {
if (value.length !== other.length) return false
return value.every((item, index) => isEqualWith(item, other[index], customizer))
}
if (isPlainObject(value) && isPlainObject(other)) {
const valueKeys = Object.keys(value)
const otherKeys = Object.keys(other)
if (valueKeys.length !== otherKeys.length) return false
return valueKeys.every(key => isEqualWith(value[key], other[key], customizer))
}
return false
}
+1
View File
@@ -0,0 +1 @@
export default {}
+6
View File
@@ -1,5 +1,7 @@
// Polyfill for React Router's data router in Jest environment
// See: https://github.com/remix-run/react-router/discussions/9851
import '@testing-library/jest-dom'
if (typeof global.Request === 'undefined') {
global.Request = class Request {
constructor(
@@ -17,3 +19,7 @@ if (typeof global.Response === 'undefined') {
) {}
} as typeof Response
}
if (typeof global.structuredClone === 'undefined') {
global.structuredClone = (<T,>(value: T) => JSON.parse(JSON.stringify(value)) as T) as typeof structuredClone
}
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"moduleResolution": "node",
"isolatedModules": true,
"noEmit": false,
"allowImportingTsExtensions": false,
"types": ["jest", "node"]
},
"include": ["src", "tests", "jest.config.ts"]
}