Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .changeset/rtl-overrides.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
'@leafygreen-ui/testing-lib': minor
---

Exports types `RenderHookOptions` & `RenderHookResult`.
Updates type signature of `renderHook`
- Exports types `RenderHookOptions` & `RenderHookResult`.
- Updates type signature of `renderHook`
- Updates internal structure of RTL override files
13 changes: 7 additions & 6 deletions packages/testing-lib/rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { esmConfig, umdConfig } from '@lg-tools/build/config/rollup.config.mjs';
export default [
esmConfig,
umdConfig,
...['./src/renderHookServer.tsx', './src/renderHookServerV17.tsx'].map(
input => ({
...umdConfig,
input,
}),
),
...[
'./src/ReactTestingLibrary/renderHookServer/renderHookServer18.tsx',
'./src/ReactTestingLibrary/renderHookServer/renderHookServer17.tsx',
].map(input => ({
...umdConfig,
input,
})),
];
21 changes: 21 additions & 0 deletions packages/testing-lib/src/ReactTestingLibrary/act.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { isReact17 } from './utils/isReact17';

/**
* Type of `act` from `@testing-library/react` if it exists,
* otherwise defaults to compatible type.
*/
export type ActType = (cb: () => void | Promise<void>) => void | Promise<void>;

/**
* Re-exports `act` from `"@testing-library/react"` in React18+
* or from `"@testing-library/react-hooks"` in React17 environments
*/
export const act: ActType = (() => {
if (isReact17()) {
const RHTL = require('@testing-library/react-hooks');
return RHTL.act;
} else {
const RTL = require('@testing-library/react');
return RTL.act;
}
})();
11 changes: 11 additions & 0 deletions packages/testing-lib/src/ReactTestingLibrary/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export { act, type ActType } from './act';
export {
renderHook,
type RenderHookOptions,
type RenderHookResult,
} from './renderHook';
export {
renderHookServer,
type RenderHookServerOptions,
type RenderHookServerResult,
} from './renderHookServer';
50 changes: 50 additions & 0 deletions packages/testing-lib/src/ReactTestingLibrary/renderHook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import * as RTL from '@testing-library/react';

import { Exists } from './utils/types';

/**
* Returns the type of `RenderHookOptions` from `@testing-library/react` if it exists,
* otherwise defaults to a compatible type.
*/
export type RenderHookOptions<TProps> = Exists<
typeof RTL,
'RenderHookOptions',
RTL.RenderOptions & {
initialProps?: TProps;
}
>;

/**
* Returns the type of `RenderHookResult` from `@testing-library/react` if it exists,
* otherwise defaults to a compatible type.
*/
export type RenderHookResult<TResult, TProps> = Exists<
typeof RTL,
'RenderHookResult',
{
current: TResult;
rerender: (props?: TProps) => void;
unmount: () => void;
result: {
all: Array<TResult>;
current: TResult;
error: Error;
};
}
>;

/**
* Re-exports `renderHook` from `"@testing-library/react"` if it exists,
* or from `"@testing-library/react-hooks"`
*
* (used when running in a React 17 test environment)
*/
export const renderHook: <TResult = any, TProps = any>(
render: (initialProps: TProps) => TResult,
options?: RenderHookOptions<TProps>,
) => RenderHookResult<TResult, TProps> =
(RTL as any).renderHook ??
(() => {
const RHTL = require('@testing-library/react-hooks');
return RHTL.renderHook;
})();
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import path from 'path';

import { isReact17 } from '../utils/isReact17';

import type {
RenderHookServerOptions,
RenderHookServerResult,
} from './renderHookServer.types';

export type { RenderHookServerOptions, RenderHookServerResult };

/**
* Correct `renderHookServer` method based on React version.
*/
export const renderHookServer: <Hook extends () => any>(
useHook: Hook,
options?: RenderHookServerOptions,
) => RenderHookServerResult<Hook> = (() => {
const filename = isReact17() ? 'renderHookServer17' : 'renderHookServer18';
const RHS = require(path.resolve(__dirname, filename));
return RHS.renderHookServer;
})();
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ReactNode } from 'react';

export interface RenderHookServerOptions {
wrapper?: ({ children }: { children: ReactNode }) => JSX.Element;
}

export interface RenderHookServerResult<Hook extends () => any> {
result: { current: ReturnType<Hook> };
hydrate: () => void;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { renderHook } from '@testing-library/react-hooks/server';

import type {
RenderHookServerOptions,
RenderHookServerResult,
} from './renderHookServer.types';

/**
* Allows you to mock the server side rendering of a hook in pre React 18 versions.
* For versions >=18, use `@testing-lib/renderHookServer`.
*
* e.g.
* ```typescript
* it('should return true when server-side rendered and false after hydration', () => {
* const { result, hydrate } = renderHookServer(useMyHook);
* expect(result.current).toBe(true);
* hydrate();
* expect(result.current).toBe(false);
* });
* ```
}
*/
export function renderHookServer<Hook extends () => any>(
useHook: Hook,
{ wrapper }: RenderHookServerOptions = {},
): RenderHookServerResult<Hook> {
// @ts-ignore Type 'undefined' is not assignable to type 'Window'.
jest.spyOn(global, 'window', 'get').mockImplementation(() => undefined);
const response = renderHook(useHook, { wrapper });
jest.spyOn(global, 'window', 'get').mockRestore();
return response;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React from 'react';
// @ts-ignore Cannot find module 'react-dom/client' or its corresponding type declarations
import { hydrateRoot } from 'react-dom/client';
import { renderToString } from 'react-dom/server';
import { act } from 'react-dom/test-utils';

import type {
RenderHookServerOptions,
RenderHookServerResult,
} from './renderHookServer.types';

/**
* Allows you to mock the server side rendering of a hook.
*
* @testing-library/react-hooks/server exposed a `renderHook` method
* that allowed for one to render hooks as if SSR, and control
* hydration. This is no longer supported in versions >=18.
*
* This code was extracted from @testing-library/react-hooks/server and
* updated to be compatible with React version >= 18 using `hydrateRoot`.
*
* More context found here:
* https://github.com/testing-library/react-testing-library/issues/1120
*
* e.g.
* ```typescript
* it('should return true when server-side rendered and false after hydration', () => {
* const { result, hydrate } = renderHookServer(useMyHook);
* expect(result.current).toBe(true);
* hydrate();
* expect(result.current).toBe(false);
* });
* ```
}
*/
export function renderHookServer18<Hook extends () => any>(
useHook: Hook,
{ wrapper: Wrapper }: RenderHookServerOptions = {},
): RenderHookServerResult<Hook> {
// Store hook return value
const results: Array<ReturnType<Hook>> = [];
const result = {
get current() {
return results.slice(-1)[0];
},
};

// Test component to render hook in
const Component = ({ useHook }: { useHook: Hook }) => {
results.push(useHook());
return null;
};

// Add wrapper if necessary
const component = Wrapper ? (
<Wrapper>
<Component useHook={useHook} />
</Wrapper>
) : (
<Component useHook={useHook} />
);

// Running tests in an environment that simulates a browser (like Jest with jsdom),
// the window object will still be available even when server rendered. To ensure
// that window is not available during SSR we need to explicitly mock or remove the
// window object.
// @ts-ignore Type 'undefined' is not assignable to type 'Window'.
jest.spyOn(global, 'window', 'get').mockImplementation(() => undefined);

// Render hook on server
const serverOutput = renderToString(component);

// Restore window
jest.spyOn(global, 'window', 'get').mockRestore();

// Render hook on client
const hydrate = () => {
const root = document.createElement('div');
root.innerHTML = serverOutput;
act(() => {
hydrateRoot(root, component);
});
};

return {
result,
hydrate,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import React from 'react';

export const getReactVersion = () => {
return parseInt(React.version.split('.')[0], 10);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { getReactVersion } from './getReactVersion';

export const isReact17 = () => {
return getReactVersion() === 17;
};
11 changes: 11 additions & 0 deletions packages/testing-lib/src/ReactTestingLibrary/utils/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Utility type that returns `X.Y` if it exists, otherwise defaults to fallback type `Z`, or `any`
*
* @internal
* Redeclared from /lib to avoid circular dependency issues
*/
export type Exists<
X,
Y extends keyof X | string,
Z = unknown,
> = Y extends keyof X ? X[Y] : Z;
6 changes: 5 additions & 1 deletion packages/testing-lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ import * as jest from './jest';
import * as JestDOM from './jest-dom';
export {
act,
type ActType,
renderHook,
type RenderHookOptions,
type RenderHookResult,
renderHookServer,
} from './RTLOverrides';
type RenderHookServerOptions,
type RenderHookServerResult,
} from './ReactTestingLibrary';
export { useTraceUpdate } from './useTraceUpdate';
export { waitForState } from './waitForState';
export { waitForTransition } from './waitForTransition';
Expand Down
4 changes: 2 additions & 2 deletions packages/testing-lib/src/waitForState.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { act } from './RTLOverrides';
import { act } from './ReactTestingLibrary';

/**
* Wrapper around `act`.
Expand All @@ -10,7 +10,7 @@ export const waitForState = async <T extends any>(
callback: () => T,
): Promise<T> => {
let val: T;
await act(() => {
await act(async () => {
val = callback();
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { fireEvent, waitFor } from '@testing-library/react';

import { act } from '../RTLOverrides';
import { act } from '../ReactTestingLibrary';

/**
* Fires the `transitionEnd` event on the provided element,
Expand All @@ -15,7 +15,7 @@ export async function waitForTransition(
) {
if (element) {
await waitFor(() => {
act(() => {
act(async () => {
fireEvent.transitionEnd(element, options);
});
});
Expand Down
Loading