feat: inula-testing-library

This commit is contained in:
小豪 2024-10-15 13:42:50 +08:00
parent 847fbd5bc0
commit 2047bb27db
12 changed files with 933 additions and 0 deletions

View File

@ -0,0 +1,3 @@
/node_modules
.vscode
package-lock.json

View File

@ -0,0 +1,194 @@
木兰宽松许可证第2版
木兰宽松许可证第2版
2020年1月 http://license.coscl.org.cn/MulanPSL2
您对“软件”的复制、使用、修改及分发受木兰宽松许可证第2版“本许可证”的如下条款的约束
0. 定义
“软件” 是指由“贡献”构成的许可在“本许可证”下的程序和相关文档的集合。
“贡献” 是指由任一“贡献者”许可在“本许可证”下的受版权法保护的作品。
“贡献者” 是指将受版权法保护的作品许可在“本许可证”下的自然人或“法人实体”。
“法人实体” 是指提交贡献的机构及其“关联实体”。
“关联实体” 是指,对“本许可证”下的行为方而言,控制、受控制或与其共同受控制的机构,此处的控制是
指有受控方或共同受控方至少50%直接或间接的投票权、资金或其他有价证券。
1. 授予版权许可
每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的版权许可,您可
以复制、使用、修改、分发其“贡献”,不论修改与否。
2. 授予专利许可
每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的(根据本条规定
撤销除外)专利许可,供您制造、委托制造、使用、许诺销售、销售、进口其“贡献”或以其他方式转移其“贡
献”。前述专利许可仅限于“贡献者”现在或将来拥有或控制的其“贡献”本身或其“贡献”与许可“贡献”时的“软
件”结合而将必然会侵犯的专利权利要求,不包括对“贡献”的修改或包含“贡献”的其他结合。如果您或您的“
关联实体”直接或间接地,就“软件”或其中的“贡献”对任何人发起专利侵权诉讼(包括反诉或交叉诉讼)或
其他专利维权行动,指控其侵犯专利权,则“本许可证”授予您对“软件”的专利许可自您提起诉讼或发起维权
行动之日终止。
3. 无商标许可
“本许可证”不提供对“贡献者”的商品名称、商标、服务标志或产品名称的商标许可但您为满足第4条规定
的声明义务而必须使用除外。
4. 分发限制
您可以在任何媒介中将“软件”以源程序形式或可执行形式重新分发,不论修改与否,但您必须向接收者提供“
本许可证”的副本,并保留“软件”中的版权、商标、专利及免责声明。
5. 免责声明与责任限制
“软件”及其中的“贡献”在提供时不带任何明示或默示的担保。在任何情况下,“贡献者”或版权所有者不对
任何人因使用“软件”或其中的“贡献”而引发的任何直接或间接损失承担责任,不论因何种原因导致或者基于
何种法律理论,即使其曾被建议有此种损失的可能性。
6. 语言
“本许可证”以中英文双语表述,中英文版本具有同等法律效力。如果中英文版本存在任何冲突不一致,以中文
版为准。
条款结束
如何将木兰宽松许可证第2版应用到您的软件
如果您希望将木兰宽松许可证第2版应用到您的新软件为了方便接收者查阅建议您完成如下三步
1 请您补充如下声明中的空白,包括软件名、软件的首次发表年份以及您作为版权人的名字;
2 请您在软件包的一级目录下创建以“LICENSE”为名的文件将整个许可证文本放入该文件中
3 请将如下声明文本放入每个源文件的头部注释中。
Copyright (c) [Year] [name of copyright holder]
[Software Name] is licensed under Mulan PSL v2.
You can use this software according to the terms and conditions of the Mulan
PSL v2.
You may obtain a copy of Mulan PSL v2 at:
http://license.coscl.org.cn/MulanPSL2
THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
See the Mulan PSL v2 for more details.
Mulan Permissive Software LicenseVersion 2
Mulan Permissive Software LicenseVersion 2 (Mulan PSL v2)
January 2020 http://license.coscl.org.cn/MulanPSL2
Your reproduction, use, modification and distribution of the Software shall
be subject to Mulan PSL v2 (this License) with the following terms and
conditions:
0. Definition
Software means the program and related documents which are licensed under
this License and comprise all Contribution(s).
Contribution means the copyrightable work licensed by a particular
Contributor under this License.
Contributor means the Individual or Legal Entity who licenses its
copyrightable work under this License.
Legal Entity means the entity making a Contribution and all its
Affiliates.
Affiliates means entities that control, are controlled by, or are under
common control with the acting entity under this License, control means
direct or indirect ownership of at least fifty percent (50%) of the voting
power, capital or other securities of controlled or commonly controlled
entity.
1. Grant of Copyright License
Subject to the terms and conditions of this License, each Contributor hereby
grants to you a perpetual, worldwide, royalty-free, non-exclusive,
irrevocable copyright license to reproduce, use, modify, or distribute its
Contribution, with modification or not.
2. Grant of Patent License
Subject to the terms and conditions of this License, each Contributor hereby
grants to you a perpetual, worldwide, royalty-free, non-exclusive,
irrevocable (except for revocation under this Section) patent license to
make, have made, use, offer for sale, sell, import or otherwise transfer its
Contribution, where such patent license is only limited to the patent claims
owned or controlled by such Contributor now or in future which will be
necessarily infringed by its Contribution alone, or by combination of the
Contribution with the Software to which the Contribution was contributed.
The patent license shall not apply to any modification of the Contribution,
and any other combination which includes the Contribution. If you or your
Affiliates directly or indirectly institute patent litigation (including a
cross claim or counterclaim in a litigation) or other patent enforcement
activities against any individual or entity by alleging that the Software or
any Contribution in it infringes patents, then any patent license granted to
you under this License for the Software shall terminate as of the date such
litigation or activity is filed or taken.
3. No Trademark License
No trademark license is granted to use the trade names, trademarks, service
marks, or product names of Contributor, except as required to fulfill notice
requirements in section 4.
4. Distribution Restriction
You may distribute the Software in any medium with or without modification,
whether in source or executable forms, provided that you provide recipients
with a copy of this License and retain copyright, patent, trademark and
disclaimer statements in the Software.
5. Disclaimer of Warranty and Limitation of Liability
THE SOFTWARE AND CONTRIBUTION IN IT ARE PROVIDED WITHOUT WARRANTIES OF ANY
KIND, EITHER EXPRESS OR IMPLIED. IN NO EVENT SHALL ANY CONTRIBUTOR OR
COPYRIGHT HOLDER BE LIABLE TO YOU FOR ANY DAMAGES, INCLUDING, BUT NOT
LIMITED TO ANY DIRECT, OR INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING
FROM YOUR USE OR INABILITY TO USE THE SOFTWARE OR THE CONTRIBUTION IN IT, NO
MATTER HOW ITS CAUSED OR BASED ON WHICH LEGAL THEORY, EVEN IF ADVISED OF
THE POSSIBILITY OF SUCH DAMAGES.
6. Language
THIS LICENSE IS WRITTEN IN BOTH CHINESE AND ENGLISH, AND THE CHINESE VERSION
AND ENGLISH VERSION SHALL HAVE THE SAME LEGAL EFFECT. IN THE CASE OF
DIVERGENCE BETWEEN THE CHINESE AND ENGLISH VERSIONS, THE CHINESE VERSION
SHALL PREVAIL.
END OF THE TERMS AND CONDITIONS
How to Apply the Mulan Permissive Software LicenseVersion 2
(Mulan PSL v2) to Your Software
To apply the Mulan PSL v2 to your work, for easy identification by
recipients, you are suggested to complete following three steps:
i. Fill in the blanks in following statement, including insert your software
name, the year of the first publication of your software, and your name
identified as the copyright owner;
ii. Create a file named "LICENSE" which contains the whole context of this
License in the first directory of your software package;
iii. Attach the statement to the appropriate annotated syntax at the
beginning of each source file.
Copyright (c) [Year] [name of copyright holder]
[Software Name] is licensed under Mulan PSL v2.
You can use this software according to the terms and conditions of the Mulan
PSL v2.
You may obtain a copy of Mulan PSL v2 at:
http://license.coscl.org.cn/MulanPSL2
THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
See the Mulan PSL v2 for more details.

View File

@ -0,0 +1,19 @@
const {jest: jestConfig} = require('kcd-scripts/config')
module.exports = Object.assign(jestConfig, {
coverageThreshold: {
...jestConfig.coverageThreshold,
// Full coverage across the build matrix (React 18, 19) but not in a single job
// Ful coverage is checked via codecov
'./src/act-compat': {
branches: 90,
},
'./src/pure': {
// minimum coverage of jobs using React 18 and 19
branches: 95,
functions: 88,
lines: 92,
statements: 92,
},
},
})

View File

@ -0,0 +1,31 @@
{
"name": "inula-testing-library-demo",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"scripts": {
"test": "kcd-scripts test"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@babel/runtime": "^7.12.5"
},
"devDependencies": {
"@testing-library/dom": "^10.3.1",
"@testing-library/jest-dom": "^6.4.6",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"chalk": "^4.1.2",
"dotenv-cli": "^4.0.0",
"jest-diff": "^29.7.0",
"npm-run-all": "^4.1.5",
"kcd-scripts": "^16.0.0",
"rimraf": "^5.0.8",
"typescript": "^4.1.2",
"openinula": "0.1.3",
"react": "^18.3.1",
"react-dom": "^18.3.1"
}
}

View File

@ -0,0 +1,62 @@
import { useState, useEffect, createRef } from "openinula";
import React from "react";
import { render, configure } from "../../rtl";
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, info) {
console.error("ErrorBoundary caught an error", error);
}
render() {
if (this.state.hasError) {
return <div>Error</div>;
}
return this.props.children;
}
}
const Foo = () => {
const [text, setText] = useState("text1");
useEffect(() => {
setTimeout(() => {
setText("text2");
}, 300);
}, []);
return <div>{text}</div>;
};
describe("render API", () => {
let originalConfig
beforeEach(() => {
// Grab the existing configuration so we can restore
// it at the end of the test
configure(existingConfig => {
originalConfig = existingConfig
// Don't change the existing config
return {}
})
})
afterEach(() => {
configure(originalConfig)
})
test("renders without crashing", () => {
render(<Foo />);
});
test('renders div into document', () => {
const ref = createRef()
const { container } = render(<div ref={ref} />)
expect(container.firstChild).toBe(ref.current)
})
});

View File

@ -0,0 +1,6 @@
import * as rtlAPI from './rtl'
const { render, renderHook, cleanup, act, fireEvent, getConfig, configure } = rtlAPI
export * from '@testing-library/dom'
export { render, renderHook, cleanup, act, fireEvent, getConfig, configure }

View File

@ -0,0 +1,91 @@
import * as React from 'react'
import * as DeprecatedReactTestUtils from 'react-dom/test-utils'
const reactAct =
typeof React.act === 'function' ? React.act : DeprecatedReactTestUtils.act
function getGlobalThis() {
/* istanbul ignore else */
if (typeof globalThis !== 'undefined') {
return globalThis
}
/* istanbul ignore next */
if (typeof self !== 'undefined') {
return self
}
/* istanbul ignore next */
if (typeof window !== 'undefined') {
return window
}
/* istanbul ignore next */
if (typeof global !== 'undefined') {
return global
}
/* istanbul ignore next */
throw new Error('unable to locate global object')
}
function setIsReactActEnvironment(isReactActEnvironment) {
getGlobalThis().IS_REACT_ACT_ENVIRONMENT = isReactActEnvironment
}
function getIsReactActEnvironment() {
return getGlobalThis().IS_REACT_ACT_ENVIRONMENT
}
function withGlobalActEnvironment(actImplementation) {
return callback => {
const previousActEnvironment = getIsReactActEnvironment()
setIsReactActEnvironment(true)
try {
// The return value of `act` is always a thenable.
let callbackNeedsToBeAwaited = false
const actResult = actImplementation(() => {
const result = callback()
if (
result !== null &&
typeof result === 'object' &&
typeof result.then === 'function'
) {
callbackNeedsToBeAwaited = true
}
return result
})
if (callbackNeedsToBeAwaited) {
const thenable = actResult
return {
then: (resolve, reject) => {
thenable.then(
returnValue => {
setIsReactActEnvironment(previousActEnvironment)
resolve(returnValue)
},
error => {
setIsReactActEnvironment(previousActEnvironment)
reject(error)
},
)
},
}
} else {
setIsReactActEnvironment(previousActEnvironment)
return actResult
}
} catch (error) {
// Can't be a `finally {}` block since we don't know if we have to immediately restore IS_REACT_ACT_ENVIRONMENT
// or if we have to await the callback first.
setIsReactActEnvironment(previousActEnvironment)
throw error
}
}
}
const act = withGlobalActEnvironment(reactAct)
export default act
export {
setIsReactActEnvironment as setReactActEnvironment,
getIsReactActEnvironment,
}
/* eslint no-console:0 */

View File

@ -0,0 +1,34 @@
import {
getConfig as getConfigDTL,
configure as configureDTL,
} from '@testing-library/dom'
let configForRTL = {
reactStrictMode: false,
}
function getConfig() {
return {
...getConfigDTL(),
...configForRTL,
}
}
function configure(newConfig) {
if (typeof newConfig === 'function') {
// Pass the existing config out to the provided function
// and accept a delta in return
newConfig = newConfig(getConfig())
}
const {reactStrictMode, ...configForDTL} = newConfig
configureDTL(configForDTL)
configForRTL = {
...configForRTL,
reactStrictMode,
}
}
export {getConfig, configure}

View File

@ -0,0 +1,69 @@
import {fireEvent as dtlFireEvent} from '@testing-library/dom'
// react-testing-library's version of fireEvent will call
// dom-testing-library's version of fireEvent. The reason
// we make this distinction however is because we have
// a few extra events that work a bit differently
const fireEvent = (...args) => dtlFireEvent(...args)
Object.keys(dtlFireEvent).forEach(key => {
fireEvent[key] = (...args) => dtlFireEvent[key](...args)
})
// React event system tracks native mouseOver/mouseOut events for
// running onMouseEnter/onMouseLeave handlers
// @link https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/react-dom/src/events/EnterLeaveEventPlugin.js#L24-L31
const mouseEnter = fireEvent.mouseEnter
const mouseLeave = fireEvent.mouseLeave
fireEvent.mouseEnter = (...args) => {
mouseEnter(...args)
return fireEvent.mouseOver(...args)
}
fireEvent.mouseLeave = (...args) => {
mouseLeave(...args)
return fireEvent.mouseOut(...args)
}
const pointerEnter = fireEvent.pointerEnter
const pointerLeave = fireEvent.pointerLeave
fireEvent.pointerEnter = (...args) => {
pointerEnter(...args)
return fireEvent.pointerOver(...args)
}
fireEvent.pointerLeave = (...args) => {
pointerLeave(...args)
return fireEvent.pointerOut(...args)
}
const select = fireEvent.select
fireEvent.select = (node, init) => {
select(node, init)
// React tracks this event only on focused inputs
node.focus()
// React creates this event when one of the following native events happens
// - contextMenu
// - mouseUp
// - dragEnd
// - keyUp
// - keyDown
// so we can use any here
// @link https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/react-dom/src/events/SelectEventPlugin.js#L203-L224
fireEvent.keyUp(node, init)
}
// React event system tracks native focusout/focusin events for
// running blur/focus handlers
// @link https://github.com/facebook/react/pull/19186
const blur = fireEvent.blur
const focus = fireEvent.focus
fireEvent.blur = (...args) => {
fireEvent.focusOut(...args)
return blur(...args)
}
fireEvent.focus = (...args) => {
fireEvent.focusIn(...args)
return focus(...args)
}
export {fireEvent}

View File

@ -0,0 +1,41 @@
import { getIsReactActEnvironment, setReactActEnvironment } from './act-compat'
import { cleanup } from './pure'
// if we're running in a test runner that supports afterEach
// or teardown then we'll automatically run cleanup afterEach test
// this ensures that tests run in isolation from each other
// if you don't like this then either import the `pure` module
// or set the RTL_SKIP_AUTO_CLEANUP env variable to 'true'.
if (typeof process === 'undefined' || !process.env?.RTL_SKIP_AUTO_CLEANUP) {
// ignore teardown() in code coverage because Jest does not support it
/* istanbul ignore else */
if (typeof afterEach === 'function') {
afterEach(() => {
cleanup()
})
} else if (typeof teardown === 'function') {
// Block is guarded by `typeof` check.
// eslint does not support `typeof` guards.
// eslint-disable-next-line no-undef
teardown(() => {
cleanup()
})
}
// No test setup with other test runners available
/* istanbul ignore else */
if (typeof beforeAll === 'function' && typeof afterAll === 'function') {
// This matches the behavior of React < 18.
let previousIsReactActEnvironment = getIsReactActEnvironment()
beforeAll(() => {
previousIsReactActEnvironment = getIsReactActEnvironment()
setReactActEnvironment(true)
})
afterAll(() => {
setReactActEnvironment(previousIsReactActEnvironment)
})
}
}
export * from './pure'

View File

@ -0,0 +1,331 @@
import * as React from 'react'
import ReactDOM from 'react-dom'
import * as ReactDOMClient from 'react-dom/client'
import {
getQueriesForElement,
prettyDOM,
configure as configureDTL,
} from '@testing-library/dom'
import act, {
getIsReactActEnvironment,
setReactActEnvironment,
} from './act-compat'
import { fireEvent } from './fire-event'
import { getConfig, configure } from './config'
/**
* 检查jest的模拟定时器是否启用
*
* 本函数旨在确定当前环境下是否使用了jest的模拟定时器这在测试环境中尤为重要
* 因为模拟定时器可以帮助我们更好地控制异步行为而不受实际时间流逝的影响
*
* @returns {boolean} 如果jest的模拟定时器已启用则返回true否则返回false
*/
function jestFakeTimersAreEnabled() {
/* istanbul ignore else */ // 忽略此条件块以进行代码覆盖率计算因为jest的定义状态会导致条件不总是可用
if (typeof jest !== 'undefined' && jest !== null) { // 检查jest是否已定义且不为null这通常意味着jest环境已被加载
return (
// legacy timers
setTimeout._isMockFunction === true || // 检查setTimeout是否被标记为mock函数这是旧版jest模拟定时器的一个标志
// eslint-disable-next-line prefer-object-has-own -- No Object.hasOwn in all target environments we support.
Object.prototype.hasOwnProperty.call(setTimeout, 'clock') // 检查setTimeout对象是否有'cock'属性这是新版jest模拟定时器的标志
)
} /* istanbul ignore next */
return false // 如果jest未定义或为null则返回false表示没有启用模拟定时器
}
// 配置DTLDelay The Load策略用于优化测试环境下的异步操作和事件处理
configureDTL({
// 包装不稳定的定时器操作,确保在非测试环境下正常运行
unstable_advanceTimersWrapper: cb => {
return act(cb)
},
// 定义异步操作的包装器,用于处理异步测试逻辑
asyncWrapper: async cb => {
// 保存当前的React Act环境状态
const previousActEnvironment = getIsReactActEnvironment()
// 设置React Act环境为false以模拟非测试环境
setReactActEnvironment(false)
try {
// 执行回调函数,并返回结果
const result = await cb()
// 清空微任务队列以确保在resolve `waitFor`调用之前恢复之前的Act环境
await new Promise(resolve => {
// 使用setTimeout来确保微任务队列被清空
setTimeout(() => {
resolve()
}, 0)
// 如果使用的是Jest假定时器则前进定时器以清空队列
if (jestFakeTimersAreEnabled()) {
jest.advanceTimersByTime(0)
}
})
return result
} finally {
// 恢复到原始的React Act环境状态
setReactActEnvironment(previousActEnvironment)
}
},
// 为事件处理提供包装确保事件在Act环境下执行
eventWrapper: cb => {
let result
// 在Act环境下执行回调以确保事件立即执行
act(() => {
result = cb()
})
return result
},
})
// Ideally we'd just use a WeakMap where containers are keys and roots are values.
// We use two variables so that we can bail out in constant time when we render with a new container (most common use case)
/**
* @type {Set<import('react-dom').Container>}
*/
const mountedContainers = new Set()
/**
* @type Array<{container: import('react-dom').Container, root: ReturnType<typeof createConcurrentRoot>}>
*/
const mountedRootEntries = []
function strictModeIfNeeded(innerElement) {
return getConfig().reactStrictMode
? React.createElement(React.StrictMode, null, innerElement)
: innerElement
}
function wrapUiIfNeeded(innerElement, wrapperComponent) {
return wrapperComponent
? React.createElement(wrapperComponent, null, innerElement)
: innerElement
}
function createConcurrentRoot(
container,
{ hydrate, ui, wrapper: WrapperComponent },
) {
let root
if (hydrate) {
act(() => {
root = ReactDOMClient.hydrateRoot(
container,
strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)),
)
})
} else {
root = ReactDOMClient.createRoot(container)
}
return {
hydrate() {
/* istanbul ignore if */
if (!hydrate) {
throw new Error(
'Attempted to hydrate a non-hydrateable root. This is a bug in `@testing-library/react`.',
)
}
// Nothing to do since hydration happens when creating the root object.
},
render(element) {
root.render(element)
},
unmount() {
root.unmount()
},
}
}
function createLegacyRoot(container) {
return {
hydrate(element) {
ReactDOM.hydrate(element, container)
},
render(element) {
ReactDOM.render(element, container)
},
unmount() {
ReactDOM.unmountComponentAtNode(container)
},
}
}
function renderRoot(
ui,
{ baseElement, container, hydrate, queries, root, wrapper: WrapperComponent },
) {
act(() => {
if (hydrate) {
root.hydrate(
strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)),
container,
)
} else {
root.render(
strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)),
container,
)
}
})
return {
container,
baseElement,
debug: (el = baseElement, maxLength, options) =>
Array.isArray(el)
? // eslint-disable-next-line no-console
el.forEach(e => console.log(prettyDOM(e, maxLength, options)))
: // eslint-disable-next-line no-console,
console.log(prettyDOM(el, maxLength, options)),
unmount: () => {
act(() => {
root.unmount()
})
},
rerender: rerenderUi => {
renderRoot(rerenderUi, {
container,
baseElement,
root,
wrapper: WrapperComponent,
})
// Intentionally do not return anything to avoid unnecessarily complicating the API.
// folks can use all the same utilities we return in the first place that are bound to the container
},
asFragment: () => {
/* istanbul ignore else (old jsdom limitation) */
if (typeof document.createRange === 'function') {
return document
.createRange()
.createContextualFragment(container.innerHTML)
} else {
const template = document.createElement('template')
template.innerHTML = container.innerHTML
return template.content
}
},
...getQueriesForElement(baseElement, queries),
}
}
function render(
ui,
{
container,
baseElement = container,
legacyRoot = false,
queries,
hydrate = false,
wrapper,
} = {},
) {
if (legacyRoot && typeof ReactDOM.render !== 'function') {
const error = new Error(
'`legacyRoot: true` is not supported in this version of React. ' +
'If your app runs React 19 or later, you should remove this flag. ' +
'If your app runs React 18 or earlier, visit https://react.dev/blog/2022/03/08/react-18-upgrade-guide for upgrade instructions.',
)
Error.captureStackTrace(error, render)
throw error
}
if (!baseElement) {
// default to document.body instead of documentElement to avoid output of potentially-large
// head elements (such as JSS style blocks) in debug output
baseElement = document.body
}
if (!container) {
container = baseElement.appendChild(document.createElement('div'))
}
let root
// eslint-disable-next-line no-negated-condition -- we want to map the evolution of this over time. The root is created first. Only later is it re-used so we don't want to read the case that happens later first.
if (!mountedContainers.has(container)) {
const createRootImpl = legacyRoot ? createLegacyRoot : createConcurrentRoot
root = createRootImpl(container, { hydrate, ui, wrapper })
mountedRootEntries.push({ container, root })
// we'll add it to the mounted containers regardless of whether it's actually
// added to document.body so the cleanup method works regardless of whether
// they're passing us a custom container or not.
mountedContainers.add(container)
} else {
mountedRootEntries.forEach(rootEntry => {
// Else is unreachable since `mountedContainers` has the `container`.
// Only reachable if one would accidentally add the container to `mountedContainers` but not the root to `mountedRootEntries`
/* istanbul ignore else */
if (rootEntry.container === container) {
root = rootEntry.root
}
})
}
return renderRoot(ui, {
container,
baseElement,
queries,
hydrate,
wrapper,
root,
})
}
function cleanup() {
mountedRootEntries.forEach(({ root, container }) => {
act(() => {
root.unmount()
})
if (container.parentNode === document.body) {
document.body.removeChild(container)
}
})
mountedRootEntries.length = 0
mountedContainers.clear()
}
function renderHook(renderCallback, options = {}) {
const { initialProps, ...renderOptions } = options
if (renderOptions.legacyRoot && typeof ReactDOM.render !== 'function') {
const error = new Error(
'`legacyRoot: true` is not supported in this version of React. ' +
'If your app runs React 19 or later, you should remove this flag. ' +
'If your app runs React 18 or earlier, visit https://react.dev/blog/2022/03/08/react-18-upgrade-guide for upgrade instructions.',
)
Error.captureStackTrace(error, renderHook)
throw error
}
const result = React.createRef()
function TestComponent({ renderCallbackProps }) {
const pendingResult = renderCallback(renderCallbackProps)
React.useEffect(() => {
result.current = pendingResult
})
return null
}
const { rerender: baseRerender, unmount } = render(
<TestComponent renderCallbackProps={initialProps} />,
renderOptions,
)
function rerender(rerenderCallbackProps) {
return baseRerender(
<TestComponent renderCallbackProps={rerenderCallbackProps} />,
)
}
return { result, rerender, unmount }
}
// just re-export everything from dom-testing-library
export * from '@testing-library/dom'
export { render, renderHook, cleanup, act, fireEvent, getConfig, configure }
/* eslint func-name-matching:0 */

View File

@ -0,0 +1,52 @@
import React from "react";
import { queries as defaultQueries } from "@testing-library/dom";
import * as ReactDOMClient from "react-dom/client";
export interface RenderOptions {
container?: HTMLElement;
baseElement?: HTMLElement;
legacyRoot?: boolean;
queries?: typeof defaultQueries;
hydrate?: boolean;
wrapper?: React.ComponentType;
}
export interface RenderRootOptions {
baseElement?: HTMLElement;
container?: HTMLElement;
hydrate?: boolean;
queries?: typeof defaultQueries;
root: ReactDOMClient.Root;
wrapper?: React.ComponentType;
}
export interface RenderResult {
container: HTMLElement;
baseElement: HTMLElement;
debug: (
el?: HTMLElement | HTMLElement[],
maxLength?: number,
options?: object
) => void;
unmount: () => void;
rerender: (rerenderUi: React.ReactElement) => void;
asFragment: () => DocumentFragment;
[key: string]: any;
}
export function strictModeIfNeeded(ui: React.ReactElement): React.ReactElement {
if (process.env.NODE_ENV === "production") {
return ui;
}
return <React.StrictMode>{ui}</React.StrictMode>;
}
export function wrapUiIfNeeded(
ui: React.ReactElement,
WrapperComponent?: React.ComponentType
): React.ReactElement {
if (!WrapperComponent) {
return ui;
}
return <WrapperComponent>{ui}</WrapperComponent>;
}