|
import { DeepPartial } from 'mock-typed'; |
|
import { isValidElement } from 'react'; |
|
import { equals, Tester } from '@jest/expect-utils'; |
|
import React from 'react'; |
|
import { prettyDOM, PrettyDOMOptions } from '@testing-library/dom'; |
|
import invariant from 'tiny-invariant'; |
|
|
|
/** |
|
* Utility function to deeply apply `expect.objectContaining` and `expect.arrayContaining` |
|
*/ |
|
export const deepContaining = <T>(input: DeepPartial<T>): any => { |
|
if (input instanceof RegExp) { |
|
return expect.stringMatching(input); |
|
} |
|
|
|
if (Array.isArray(input)) { |
|
return expect.arrayContaining(input.map((item) => deepContaining(item))); |
|
} |
|
|
|
if (typeof input === 'object' && input !== null) { |
|
if ((input as any)['$$typeof']?.toString() === 'Symbol(jest.asymmetricMatcher)') { |
|
// AsymmetricMatcher |
|
return input; |
|
} |
|
|
|
// If input is an object, map its values with `objectContainingDeep` and wrap in `expect.objectContaining` |
|
return expect.objectContaining( |
|
Object.entries(input).reduce((acc, [key, value]) => { |
|
acc[key as keyof T] = deepContaining(value); |
|
return acc; |
|
}, {} as any) |
|
); |
|
} |
|
|
|
// For primitive values, return them directly (no transformation needed) |
|
return input; |
|
}; |
|
|
|
type Replacer = (input: any) => any | undefined; |
|
|
|
export const replaceProps = (obj: any, replacers: Replacer[]): any => { |
|
const process = (input: any): { output: any; isChanged: boolean } => { |
|
for (const replacer of replacers) { |
|
const output = replacer(input); |
|
if (output !== undefined) return { output, isChanged: true }; |
|
} |
|
|
|
// Handle arrays |
|
if (Array.isArray(input)) { |
|
const mapped = input.map(process); |
|
return mapped.some((item) => item.isChanged) |
|
? { isChanged: true, output: mapped.map((item) => item.output) } |
|
: { isChanged: false, output: input }; |
|
} |
|
|
|
// Handle objects |
|
if (input && typeof input === 'object') { |
|
const mapped = Object.entries(input).map(([key, val]) => { |
|
const { isChanged, output } = process(val); |
|
return { isChanged, key, output }; |
|
}); |
|
return !mapped.some((item) => item.isChanged) |
|
? { isChanged: false, output: input } |
|
: { |
|
isChanged: true, |
|
output: mapped.reduce((acc, { key, output }) => { |
|
acc[key] = output; |
|
return acc; |
|
}, {} as any), |
|
}; |
|
} |
|
|
|
return { isChanged: false, output: input }; |
|
}; |
|
|
|
const { isChanged, output } = process(obj); |
|
return isChanged ? output : obj; |
|
}; |
|
|
|
export const reactElementMatcher: Tester = (a, b) => |
|
isValidElement(a) && isValidElement(b) |
|
? a.type === b.type && equals(a.props, b.props, [reactElementMatcher]) |
|
: undefined; |
|
|
|
const serializeReactElements = (input: any) => |
|
React.isValidElement(input) |
|
? { |
|
type: (input.type as React.FC)?.displayName ?? (input.type as React.FC)?.name ?? input.type ?? 'Unknown', |
|
props: ensureReactElementsForStringifying(input.props), |
|
} |
|
: undefined; |
|
|
|
export const ensureReactElementsForStringifying = (input: any) => replaceProps(input, [serializeReactElements]); |
|
|
|
export const printElementHtml = ( |
|
element: Element | HTMLElement, |
|
{ maxLength = 500_000, ...options }: { maxLength?: number } & PrettyDOMOptions = {}, |
|
...rest: any[] |
|
) => { |
|
invariant( |
|
process.env.npm_lifecycle_event === 'test:watch', |
|
'Sorry, printing DOM to console is only available in test:watch' |
|
); |
|
// eslint-disable-next-line no-console |
|
console.log(...rest.map((item) => `${item}\n`), prettyDOM(element, maxLength, options)); |
|
}; |