├── .all-contributorsrc ├── .babelrc ├── .babelrc.esm.json ├── .editorconfig ├── .eslintrc.js ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── validate.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── add-extensions.js ├── dont-clean-up-after-each.js ├── other └── poodle.png ├── package.json ├── pure.d.ts ├── pure.js ├── src ├── __mocks__ │ └── axios.js ├── __tests__ │ ├── __snapshots__ │ │ └── render.js.snap │ ├── act.js │ ├── auto-cleanup-skip.js │ ├── auto-cleanup.js │ ├── cleanup.js │ ├── debug.js │ ├── end-to-end.js │ ├── events-compat.js │ ├── events.js │ ├── multi-base.js │ ├── render.js │ ├── renderHook.js │ ├── rerender.js │ └── stopwatch.js ├── fire-event.js ├── index.js └── pure.js └── types ├── index.d.ts └── pure.d.ts /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "preact-testing-library", 3 | "projectOwner": "testing-library", 4 | "repoType": "github", 5 | "files": [ 6 | "README.md" 7 | ], 8 | "imageSize": 100, 9 | "commit": false, 10 | "contributors": [ 11 | { 12 | "login": "kentcdodds", 13 | "name": "Kent C. Dodds", 14 | "avatar_url": "https://avatars.githubusercontent.com/u/1500684?v=3", 15 | "profile": "https://kentcdodds.com", 16 | "contributions": [ 17 | "code", 18 | "doc", 19 | "test" 20 | ] 21 | }, 22 | { 23 | "login": "antsmartian", 24 | "name": "Ants Martian", 25 | "avatar_url": "https://avatars0.githubusercontent.com/u/1241511?s=400&v=4", 26 | "profile": "https://github.com/antsmartian", 27 | "contributions": [ 28 | "code", 29 | "doc", 30 | "test" 31 | ] 32 | }, 33 | { 34 | "login": "mihar-22", 35 | "name": "Rahim Alwer", 36 | "avatar_url": "https://avatars3.githubusercontent.com/u/14304599?s=460&v=4", 37 | "profile": "https://github.com/mihar-22", 38 | "contributions": [ 39 | "code", 40 | "doc", 41 | "test", 42 | "infra" 43 | ] 44 | } 45 | ], 46 | "contributorsPerLine": 7, 47 | "repoHost": "https://github.com", 48 | "commitConvention": "none" 49 | } 50 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { "targets": { "node": "12" } }]], 4 | "plugins": [ 5 | ["@babel/plugin-proposal-class-properties"], 6 | ["@babel/plugin-transform-react-jsx", { "pragma": "h" }], 7 | ["@babel/plugin-transform-modules-commonjs", {"allowTopLevelThis": true}] 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.babelrc.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": false, 7 | "targets": { "node": "12" } 8 | } 9 | ] 10 | ], 11 | "plugins": [ 12 | ["@babel/plugin-proposal-class-properties"], 13 | ["@babel/plugin-transform-react-jsx", { "pragma": "h" }], 14 | ["./add-extensions", { "extension": "mjs" }] 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.md] 11 | max_line_length = off 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | es6: true, 6 | jest: true 7 | }, 8 | parser: 'babel-eslint', 9 | extends: [ 10 | "standard" 11 | ], 12 | plugins: [ 13 | 'simple-import-sort', 14 | 'react-hooks' 15 | ], 16 | rules: { 17 | 'max-len': ['warn', {'code': 100}], 18 | 'react-hooks/rules-of-hooks': 'error', 19 | 'react-hooks/exhaustive-deps': 'warn', 20 | 'no-unused-vars': 'off' 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 43 | 44 | - `preact-testing-library` version: 45 | - `preact` version: 46 | - `node` version: 47 | - `npm` (or `yarn`) version: 48 | 49 | **Relevant code or config** 50 | 51 | **What you did:** 52 | 53 | **What happened:** 54 | 55 | 56 | 57 | Reproduction repository: 58 | 59 | 66 | 67 | Problem description: 68 | 69 | 70 | 71 | Suggested solution: 72 | 73 | 77 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | **What**: 20 | 21 | 22 | 23 | **Why**: 24 | 25 | 26 | 27 | **How**: 28 | 29 | 30 | 31 | **Checklist**: 32 | 33 | 34 | 35 | 36 | 37 | - [ ] Documentation added 38 | - [ ] Tests 39 | - [ ] Typescript definitions updated 40 | - [ ] Ready to be merged 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: validate 2 | on: 3 | push: 4 | branches: 5 | - '+([0-9])?(.{+([0-9]),x}).x' 6 | - 'main' 7 | - 'next' 8 | - 'next-major' 9 | - 'beta' 10 | - 'alpha' 11 | - '!all-contributors/**' 12 | pull_request: 13 | 14 | concurrency: 15 | group: ${{ github.workflow }}-${{ github.ref }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | main: 20 | # ignore all-contributors PRs 21 | if: ${{ !contains(github.head_ref, 'all-contributors') }} 22 | strategy: 23 | matrix: 24 | node: [16, 18, 20] 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: ⬇️ Checkout repo 28 | uses: actions/checkout@v2 29 | 30 | - name: ⎔ Setup node 31 | uses: actions/setup-node@v2 32 | with: 33 | node-version: ${{ matrix.node }} 34 | 35 | - name: 📥 Download deps 36 | uses: bahmutov/npm-install@v1 37 | with: 38 | useLockFile: false 39 | env: 40 | HUSKY_SKIP_INSTALL: true 41 | 42 | - name: ▶️ Run validate script 43 | run: npm run validate 44 | 45 | - name: ⬆️ Upload coverage report 46 | uses: codecov/codecov-action@v2 47 | 48 | release: 49 | needs: main 50 | runs-on: ubuntu-latest 51 | if: 52 | ${{ github.repository == 'testing-library/preact-testing-library' && 53 | contains('refs/heads/main,refs/heads/beta,refs/heads/next,refs/heads/alpha', github.ref) && 54 | github.event_name == 'push' }} 55 | steps: 56 | - name: ⬇️ Checkout repo 57 | uses: actions/checkout@v2 58 | 59 | - name: ⎔ Setup node 60 | uses: actions/setup-node@v2 61 | with: 62 | node-version: 14 63 | 64 | - name: 📥 Download deps 65 | uses: bahmutov/npm-install@v1 66 | with: 67 | useLockFile: false 68 | env: 69 | HUSKY_SKIP_INSTALL: true 70 | 71 | - name: 🏗 Run build script 72 | run: npm run build 73 | 74 | - name: 🚀 Release 75 | uses: cycjimmy/semantic-release-action@v2 76 | with: 77 | semantic_version: 17 78 | branches: | 79 | [ 80 | '+([0-9])?(.{+([0-9]),x}).x', 81 | 'main', 82 | 'next', 83 | 'next-major', 84 | {name: 'beta', prerelease: true}, 85 | {name: 'alpha', prerelease: true} 86 | ] 87 | env: 88 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 89 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | .opt-in 5 | .opt-out 6 | .DS_Store 7 | .eslintcache 8 | .idea 9 | 10 | yarn-error.log 11 | package-lock.json 12 | yarn.lock 13 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | node_modules 3 | dist 4 | coverage 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "all", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "proseWrap": "always" 11 | } 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | The changelog is automatically updated using 4 | [semantic-release](https://github.com/semantic-release/semantic-release). You 5 | can see it on the [releases page](../../releases). 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of 9 | experience, nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or reject 41 | comments, commits, code, wiki edits, issues, and other contributions that are 42 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 43 | contributor for other behaviors that they deem inappropriate, threatening, 44 | offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an 62 | incident. Further details of specific enforcement policies may be posted 63 | separately. 64 | 65 | Project maintainers who do not follow or enforce the Code of Conduct in good 66 | faith may face temporary or permanent repercussions as determined by other 67 | members of the project's leadership. 68 | 69 | ## Attribution 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 72 | version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 73 | 74 | [homepage]: http://contributor-covenant.org 75 | [version]: http://contributor-covenant.org/version/1/4/ 76 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for being willing to contribute! 4 | 5 | **Working on your first Pull Request?** You can learn how from this _free_ series [How to Contribute 6 | to an Open Source Project on GitHub][egghead] 7 | 8 | ## Project setup 9 | 10 | 1. Fork and clone the repo 11 | 2. Run `npm run setup -s` to install dependencies and run validation 12 | 3. Create a branch for your PR with `git checkout -b pr/your-branch-name` 13 | 14 | > Tip: Keep your `main` branch pointing at the original repository and make pull requests from 15 | > branches on your fork. To do this, run: 16 | > 17 | > ``` 18 | > git remote add upstream https://github.com/testing-library/preact-testing-library.git 19 | > git fetch upstream 20 | > git branch --set-upstream-to=upstream/main main 21 | > ``` 22 | > 23 | > This will add the original repository as a "remote" called "upstream," Then fetch the git 24 | > information from that remote, then set your local `main` branch to use the upstream main branch 25 | > whenever you run `git pull`. Then you can make all of your pull request branches based on this 26 | > `main` branch. Whenever you want to update your version of `main`, do a regular `git pull`. 27 | 28 | ## Committing and Pushing changes 29 | 30 | Please make sure to run the tests before you commit your changes. You can run `npm run test:update` 31 | which will update any snapshots that need updating. Make sure to include those changes (if they 32 | exist) in your commit. 33 | 34 | ## Help needed 35 | 36 | Please checkout the [the open issues][issues] 37 | 38 | Also, please watch the repo and respond to questions/bug reports/feature requests! Thanks! 39 | 40 | [egghead]: https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github 41 | [issues]: https://github.com/testing-library/preact-testing-library/issues 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Rahim Alwer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

Preact Testing Library

3 | 4 | 5 | poodle 11 | 12 | 13 |

Simple and complete Preact DOM testing utilities that encourage good testing 14 | practices.

15 | 16 | > Inspired completely by [react-testing-library][react-testing-library] 17 | 18 | [![Build Status][build-badge]][build] [![Code Coverage][coverage-badge]][coverage] 19 | [![All Contributors](https://img.shields.io/badge/all_contributors-3-orange.svg?style=flat-square)](#contributors-) 20 | [![PRs Welcome][prs-badge]][prs] [![Code of Conduct][coc-badge]][coc] 21 | [![version][version-badge]][package] [![downloads][downloads-badge]][package] 22 | [![MIT License][license-badge]][license] 23 | [![Preact Slack Community][preact-slack-badge]][preact-slack] 24 | [![Commitzen][commitzen-badge]][commitzen] [![Discord][discord-badge]][discord] 25 | 26 |
27 | 28 |
29 | 30 | ## Table of Contents 31 | 32 | 33 | 34 | 35 | - [The Problem](#the-problem) 36 | - [The Solution](#the-solution) 37 | - [Installation](#installation) 38 | - [Docs](#docs) 39 | - [Issues](#issues) 40 | - [🐛 Bugs](#-bugs) 41 | - [💡 Feature Requests](#-feature-requests) 42 | - [❓ Questions](#-questions) 43 | - [Contributors](#contributors) 44 | - [LICENSE](#license) 45 | 46 | 47 | 48 | ## The Problem 49 | 50 | You want to write tests for your Preact components so that they avoid including implementation 51 | details, and are maintainable in the long run. 52 | 53 | ## The Solution 54 | 55 | The Preact Testing Library is a very lightweight solution for testing Preact components. It provides 56 | light utility functions on top of preact/test-utils, in a way that encourages better testing 57 | practices. Its primary guiding principle is: 58 | 59 | > [The more your tests resemble the way your software is used, the more confidence they can give you.](https://twitter.com/kentcdodds/status/977018512689455106) 60 | 61 | ## Installation 62 | 63 | This module is distributed via [npm][npm] which is bundled with [node][node] and should be installed 64 | as one of your project's `devDependencies`: 65 | 66 | ``` 67 | npm install --save-dev @testing-library/preact 68 | ``` 69 | 70 | This library has `peerDependencies` listings for `preact >= 10`. 71 | 72 | 💡 You may also be interested in installing `@testing-library/jest-dom` so you can use 73 | [the custom jest matchers](https://github.com/testing-library/jest-dom). 74 | 75 | 📝 This library supports Preact X (10.x). It takes advantage of the `act` test utility in 76 | `preact/test-utils` to enable both Preact Hook and Class components to be easily tested. 77 | 78 | 📝 If you're looking for a solution for Preact 8.x then install `preact-testing-library`. 79 | 80 | ## Docs 81 | 82 | See the [docs](https://testing-library.com/docs/preact-testing-library/intro) over at the Testing 83 | Library website. 84 | 85 | ## Issues 86 | 87 | _Looking to contribute? Look for the [Good First Issue][good-first-issue] label._ 88 | 89 | ### 🐛 Bugs 90 | 91 | Please file an issue for bugs, missing documentation, or unexpected behavior. 92 | 93 | [**See Bugs**][bugs] 94 | 95 | ### 💡 Feature Requests 96 | 97 | Please file an issue to suggest new features. Vote on feature requests by adding a 👍. This helps 98 | maintainers prioritize what to work on. 99 | 100 | [**See Feature Requests**][requests] 101 | 102 | ### ❓ Questions 103 | 104 | For questions related to using the library, please visit a support community instead of filing an 105 | issue on GitHub. 106 | 107 | - [Preact Slack][slack] 108 | - [Stack Overflow][stackoverflow] 109 | 110 | ## Contributors 111 | 112 | Thanks goes to these people ([emoji key][emojis]): 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 |
Kent C. Dodds
Kent C. Dodds

💻 📖 ⚠️
Ants Martian
Ants Martian

💻 📖 ⚠️
Rahim Alwer
Rahim Alwer

💻 📖 ⚠️ 🚇
124 | 125 | 126 | 127 | 128 | 129 | 130 | This project follows the [all-contributors][all-contributors] specification. Contributions of any 131 | kind welcome! 132 | 133 | ## LICENSE 134 | 135 | [MIT](LICENSE) 136 | 137 | 138 | [npm]: https://www.npmjs.com 139 | [node]: https://nodejs.org 140 | [build]: https://github.com/testing-library/preact-testing-library/actions?query=workflow%3Avalidate 141 | [build-badge]: https://img.shields.io/github/workflow/status/testing-library/preact-testing-library/validate?logo=github&style=flat-square 142 | [coverage-badge]: https://img.shields.io/codecov/c/github/testing-library/preact-testing-library.svg?style=flat-square 143 | [coverage]: https://codecov.io/github/testing-library/preact-testing-library 144 | [package]: https://www.npmjs.com/package/@testing-library/preact 145 | [version-badge]: https://img.shields.io/npm/v/@testing-library/preact 146 | [downloads-badge]: https://img.shields.io/npm/dw/@testing-library/preact 147 | [slack]: https://preact-slack.now.sh 148 | [license]: https://github.com/testing-library/preact-testing-library/blob/main/LICENSE 149 | [license-badge]: https://img.shields.io/github/license/testing-library/preact-testing-library?color=b 150 | [emojis]: https://github.com/all-contributors/all-contributors#emoji-key 151 | [all-contributors]: https://github.com/all-contributors/all-contributors 152 | [guiding-principle]: https://twitter.com/kentcdodds/status/977018512689455106 153 | [bugs]: https://github.com/testing-library/preact-testing-library/issues?q=is%3Aissue+is%3Aopen+label%3Abug+sort%3Acreated-desc 154 | [requests]: https://github.com/testing-library/preact-testing-library/issues?q=is%3Aissue+sort%3Areactions-%2B1-desc+label%3Aenhancement+is%3Aopen 155 | [good-first-issue]: https://github.com/testing-library/preact-testing-library/issues?utf8=✓&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3A"good+first+issue"+ 156 | [stackoverflow]: https://stackoverflow.com/questions/tagged/preact-testing-library 157 | [react-testing-library]: https://github.com/testing-library/react-testing-library 158 | [react-testing-library-docs]: https://testing-library.com/docs/react-testing-library/intro 159 | [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square 160 | [prs]: http://makeapullrequest.com 161 | [coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square 162 | [coc]: https://github.com/testing-library/preact-testing-library/blob/main/CODE_OF_CONDUCT.md 163 | [preact-slack]: https://preact-slack.now.sh/ 164 | [preact-slack-badge]: https://preact-slack.now.sh/badge.svg 165 | [commitzen]: http://commitizen.github.io/cz-cli/ 166 | [commitzen-badge]: https://img.shields.io/badge/commitizen-friendly-brightgreen.svg 167 | [discord-badge]: https://img.shields.io/discord/723559267868737556.svg?color=7389D8&labelColor=6A7EC2&logo=discord&logoColor=ffffff 168 | [discord]: https://discord.gg/testing-library 169 | 170 | -------------------------------------------------------------------------------- /add-extensions.js: -------------------------------------------------------------------------------- 1 | module.exports = function (babel, opts) { 2 | return { 3 | visitor: { 4 | ExportAllDeclaration: (path) => { 5 | const { node } = path 6 | if (node.source && node.source.extra && node.source.extra.rawValue.startsWith('./')) { 7 | node.source = babel.types.stringLiteral( 8 | node.source.extra && node.source.extra.rawValue + '.' + opts.extension 9 | ) 10 | } 11 | }, 12 | ImportDeclaration: (path) => { 13 | const { node } = path 14 | if (node.source && node.source.extra && node.source.extra.rawValue.startsWith('./')) { 15 | node.source = babel.types.stringLiteral( 16 | node.source.extra && node.source.extra.rawValue + '.' + opts.extension 17 | ) 18 | } 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /dont-clean-up-after-each.js: -------------------------------------------------------------------------------- 1 | process.env.PTL_SKIP_AUTO_CLEANUP = true 2 | -------------------------------------------------------------------------------- /other/poodle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testing-library/preact-testing-library/3e5394e29975296587f9532ab55a43b0da0fab3d/other/poodle.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@testing-library/preact", 3 | "version": "0.0.0-semantically-released", 4 | "description": "Simple and complete Preact DOM testing utilities that encourage good testing practices.", 5 | "main": "dist/cjs/index.js", 6 | "module": "dist/esm/index.mjs", 7 | "types": "types/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "types": "./types/index.d.ts", 11 | "browser": "./dist/esm/index.mjs", 12 | "import": "./dist/esm/index.mjs", 13 | "require": "./dist/cjs/index.js" 14 | }, 15 | "./pure": { 16 | "types": "./pure.d.ts", 17 | "browser": "./dist/esm/pure.mjs", 18 | "import": "./dist/esm/pure.mjs", 19 | "require": "./dist/cjs/pure.js" 20 | } 21 | }, 22 | "license": "MIT", 23 | "author": "Rahim Alwer ", 24 | "homepage": "https://github.com/testing-library/preact-testing-library#readme", 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/testing-library/preact-testing-library" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/testing-library/preact-testing-library/issues" 31 | }, 32 | "engines": { 33 | "node": ">= 12" 34 | }, 35 | "keywords": [ 36 | "testing", 37 | "preact", 38 | "ui", 39 | "dom", 40 | "jsdom", 41 | "unit", 42 | "integration", 43 | "functional", 44 | "end-to-end", 45 | "e2e" 46 | ], 47 | "files": [ 48 | "dist", 49 | "dont-cleanup-after-each.js", 50 | "pure.js", 51 | "types/index.d.ts", 52 | "pure.d.ts" 53 | ], 54 | "scripts": { 55 | "toc": "doctoc README.md", 56 | "lint": "eslint src/**/*.js --fix", 57 | "clean": "rimraf dist", 58 | "build": "npm run build:cjs && npm run build:esm", 59 | "build:cjs": "babel src --out-dir dist/cjs --config-file ./.babelrc --ignore '**/__tests__/**,**/__mocks__/**'", 60 | "build:esm": "babel src --no-babelrc --out-file-extension .mjs --out-dir dist/esm --config-file ./.babelrc.esm.json --ignore '**/__tests__/**,**/__mocks__/**'", 61 | "test": "jest src/__tests__ ", 62 | "test:watch": "npm test --watch", 63 | "test:update": "npm test --updateSnapshot --coverage", 64 | "setup": "npm install && npm run validate", 65 | "validate": "npm run lint && npm test && npm run clean && npm run build", 66 | "contributors:add": "all-contributors add", 67 | "contributors:generate": "all-contributors generate" 68 | }, 69 | "dependencies": { 70 | "@testing-library/dom": "^8.11.1" 71 | }, 72 | "devDependencies": { 73 | "@babel/cli": "^7.5.5", 74 | "@babel/core": "^7.5.5", 75 | "@babel/plugin-proposal-class-properties": "^7.5.5", 76 | "@babel/plugin-transform-modules-commonjs": "^7.6.0", 77 | "@babel/plugin-transform-react-jsx": "^7.3.0", 78 | "@babel/preset-env": "^7.5.5", 79 | "@commitlint/cli": "^8.2.0", 80 | "@commitlint/config-conventional": "^8.2.0", 81 | "@testing-library/jest-dom": "^5.16.1", 82 | "@types/jest": "^26.0.0", 83 | "all-contributors-cli": "^6.9.0", 84 | "babel-eslint": "^10.0.3", 85 | "babel-jest": "^26.0.1", 86 | "doctoc": "^1.4.0", 87 | "eslint": "^7.3.0", 88 | "eslint-config-standard": "^14.1.0", 89 | "eslint-plugin-import": "^2.18.2", 90 | "eslint-plugin-node": "^11.1.0", 91 | "eslint-plugin-promise": "^4.2.1", 92 | "eslint-plugin-react-hooks": "^4.0.4", 93 | "eslint-plugin-simple-import-sort": "^5.0.3", 94 | "eslint-plugin-standard": "^4.0.1", 95 | "husky": "^4.2.5", 96 | "jest": "^27.0.6", 97 | "jest-watch-typeahead": "^0.6.0", 98 | "lint-staged": "^10.2.11", 99 | "preact": "^10.0.0-rc.1", 100 | "preact-portal": "^1.1.3", 101 | "prettier": "^2.0.5", 102 | "rimraf": "^3.0.0" 103 | }, 104 | "peerDependencies": { 105 | "preact": ">=10 || ^10.0.0-alpha.0 || ^10.0.0-beta.0" 106 | }, 107 | "husky": { 108 | "hooks": { 109 | "pre-commit": "lint-staged", 110 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 111 | } 112 | }, 113 | "lint-staged": { 114 | "README.md": [ 115 | "npm run toc", 116 | "prettier --parser markdown --write", 117 | "git add" 118 | ], 119 | ".all-contributorsrc": [ 120 | "npm run contributors:generate", 121 | "git add" 122 | ], 123 | "**/*.js": [ 124 | "npm run lint", 125 | "npm test", 126 | "git add" 127 | ] 128 | }, 129 | "commitlint": { 130 | "extends": [ 131 | "@commitlint/config-conventional" 132 | ] 133 | }, 134 | "jest": { 135 | "testEnvironment": "jsdom", 136 | "watchPlugins": [ 137 | "typeahead/filename", 138 | "typeahead/testname" 139 | ] 140 | }, 141 | "publishConfig": { 142 | "access": "public" 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /pure.d.ts: -------------------------------------------------------------------------------- 1 | export * from './types/pure' 2 | -------------------------------------------------------------------------------- /pure.js: -------------------------------------------------------------------------------- 1 | // Makes it so people can import from '@testing-library/preact/pure' 2 | module.exports = require('./dist/pure') 3 | -------------------------------------------------------------------------------- /src/__mocks__/axios.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | get: jest.fn(() => Promise.resolve({ data: {} })) 3 | } 4 | 5 | // Note: 6 | // For now we don't need any other method (POST/PUT/PATCH), what we have already works fine. 7 | // We will add more methods only if we need to. 8 | // For reference please read: https://github.com/testing-library/react-testing-library/issues/2 9 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/render.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`supports fragments 1`] = ` 4 | 5 |
6 | 7 | DocumentFragment 8 | 9 | is pretty cool! 10 |
11 |
12 | `; 13 | -------------------------------------------------------------------------------- /src/__tests__/act.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect' 2 | import { createRef, h } from 'preact' 3 | import { useEffect, useState } from 'preact/hooks' 4 | import { fireEvent, render } from '..' 5 | 6 | test('render calls useEffect immediately', () => { 7 | const cb = jest.fn() 8 | 9 | function Comp () { 10 | useEffect(cb) 11 | return null 12 | } 13 | 14 | render() 15 | 16 | expect(cb).toHaveBeenCalledTimes(1) 17 | }) 18 | 19 | test('findByTestId returns the element', async () => { 20 | const ref = createRef() 21 | 22 | const { findByTestId } = render(
) 23 | 24 | expect(await findByTestId('foo')).toBe(ref.current) 25 | }) 26 | 27 | test('fireEvent triggers useEffect calls', () => { 28 | const cb = jest.fn() 29 | 30 | function Counter () { 31 | useEffect(cb) 32 | 33 | const [count, setCount] = useState(0) 34 | 35 | return 36 | } 37 | 38 | const { container: { firstChild: buttonNode } } = render() 39 | 40 | cb.mockClear() 41 | fireEvent.click(buttonNode) 42 | expect(buttonNode).toHaveTextContent('1') 43 | expect(cb).toHaveBeenCalledTimes(1) 44 | }) 45 | 46 | test('calls to hydrate will run useEffects', () => { 47 | const cb = jest.fn() 48 | 49 | function Comp () { 50 | useEffect(cb) 51 | return null 52 | } 53 | 54 | render(, { hydrate: true }) 55 | 56 | expect(cb).toHaveBeenCalledTimes(1) 57 | }) 58 | -------------------------------------------------------------------------------- /src/__tests__/auto-cleanup-skip.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact' 2 | 3 | let render 4 | 5 | beforeAll(() => { 6 | process.env.PTL_SKIP_AUTO_CLEANUP = 'true' 7 | const rtl = require('../') // eslint-disable-line global-require 8 | render = rtl.render 9 | }) 10 | 11 | // This one verifies that if PTL_SKIP_AUTO_CLEANUP is set 12 | // then we DON'T auto-wire up the afterEach for folks 13 | test('first', () => { 14 | render(
hi
) 15 | }) 16 | 17 | test('second', () => { 18 | expect(document.body.innerHTML).toEqual('
hi
') 19 | }) 20 | -------------------------------------------------------------------------------- /src/__tests__/auto-cleanup.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact' 2 | import { render } from '..' 3 | 4 | // This just verifies that by importing PTL in an 5 | // environment which supports afterEach (like jest) 6 | // we'll get automatic cleanup between tests. 7 | test('first', () => { 8 | render(
hi
) 9 | }) 10 | 11 | test('second', () => { 12 | expect(document.body.innerHTML).toEqual('') 13 | }) 14 | -------------------------------------------------------------------------------- /src/__tests__/cleanup.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect' 2 | import { Component, h } from 'preact' 3 | import { cleanup, render } from '..' 4 | 5 | test('cleans up the document', () => { 6 | const spy = jest.fn() 7 | const divId = 'my-div' 8 | 9 | class Test extends Component { 10 | componentWillUnmount () { 11 | expect(document.getElementById(divId)).toBeInTheDocument() 12 | spy() 13 | } 14 | 15 | render () { 16 | return (
) 17 | } 18 | } 19 | 20 | render() 21 | cleanup() 22 | expect(document.body.innerHTML).toBe('') 23 | expect(spy).toHaveBeenCalledTimes(1) 24 | }) 25 | 26 | test('cleanup does not error when an element is not a child', () => { 27 | render(
, { container: document.createElement('div') }) 28 | cleanup() 29 | }) 30 | -------------------------------------------------------------------------------- /src/__tests__/debug.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import '@testing-library/jest-dom/extend-expect' 4 | import { h, Fragment } from 'preact' 5 | import { screen, render } from '..' 6 | 7 | beforeEach(() => { 8 | jest.spyOn(console, 'log').mockImplementation(() => {}) 9 | }) 10 | 11 | afterEach(() => { 12 | console.log.mockRestore() 13 | }) 14 | 15 | test('debug pretty prints the container', () => { 16 | const HelloWorld = () => (

Hello World

) 17 | 18 | const { debug } = render() 19 | 20 | debug() 21 | 22 | expect(console.log).toHaveBeenCalledTimes(1) 23 | expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Hello World')) 24 | }) 25 | 26 | test('debug pretty prints multiple containers', () => { 27 | const HelloWorld = () => ( 28 | 29 |

Hello World

30 |

Hello World

31 |
32 | ) 33 | 34 | const { debug } = render() 35 | const multipleElements = screen.getAllByTestId('testId') 36 | debug(multipleElements) 37 | expect(console.log).toHaveBeenCalledTimes(2) 38 | expect(console.log).toHaveBeenCalledWith( 39 | expect.stringContaining('Hello World') 40 | ) 41 | }) 42 | 43 | test('allows same arguments as prettyDOM', () => { 44 | const HelloWorld = () =>

Hello World

45 | const { debug, container } = render() 46 | debug(container, 6, { highlight: false }) 47 | expect(console.log).toHaveBeenCalledTimes(1) 48 | expect(console.log.mock.calls[0]).toMatchInlineSnapshot(` 49 | Array [ 50 | "
51 | ...", 52 | ] 53 | `) 54 | }) 55 | -------------------------------------------------------------------------------- /src/__tests__/end-to-end.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect' 2 | import { Component, h } from 'preact' 3 | import { screen, render, waitForElementToBeRemoved } from '..' 4 | 5 | const fetchAMessage = () => new Promise((resolve) => { 6 | // we are using random timeout here to simulate a real-time example 7 | // of an async operation calling a callback at a non-deterministic time 8 | const randomTimeout = Math.floor(Math.random() * 100) 9 | 10 | setTimeout(() => { 11 | resolve({ returnedMessage: 'Hello World' }) 12 | }, randomTimeout) 13 | }) 14 | 15 | class ComponentWithLoader extends Component { 16 | state = { loading: true } 17 | 18 | componentDidMount () { 19 | fetchAMessage().then(data => { 20 | this.setState({ data, loading: false }) 21 | }) 22 | } 23 | 24 | render () { 25 | if (this.state.loading) { 26 | return (
Loading...
) 27 | } 28 | 29 | return ( 30 |
31 | Loaded this message: {this.state.data.returnedMessage}! 32 |
33 | ) 34 | } 35 | } 36 | 37 | test('it waits for the data to be loaded', async () => { 38 | render() 39 | const loading = () => screen.getByText('Loading...') 40 | await waitForElementToBeRemoved(loading) 41 | expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/) 42 | }) 43 | -------------------------------------------------------------------------------- /src/__tests__/events-compat.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact' // required by render 2 | import { fireEvent, render } from '..' 3 | import 'preact/compat' 4 | 5 | test('calling `fireEvent` with `preact/compat` and onChange works too', () => { 6 | const handler = jest.fn() 7 | 8 | // Preact only matches React's aliasing of `onChange` when `preact/compat` is used 9 | // This test ensures this is supported properly with `fireEvent.change()` 10 | const { 11 | container: { firstChild: input } 12 | } = render() 13 | 14 | const targetProperties = { value: 'a' } 15 | const otherProperties = { isComposing: true } 16 | const init = { 17 | target: targetProperties, 18 | ...otherProperties 19 | } 20 | 21 | expect(fireEvent.change(input, init)).toBe(true) 22 | 23 | expect(handler).toHaveBeenCalledTimes(1) 24 | expect(handler).toHaveBeenCalledWith(expect.objectContaining(otherProperties)) 25 | }) 26 | -------------------------------------------------------------------------------- /src/__tests__/events.js: -------------------------------------------------------------------------------- 1 | import { createRef, h } from 'preact' 2 | import { fireEvent, render } from '..' 3 | 4 | const eventTypes = [ 5 | { 6 | type: 'Clipboard', 7 | events: ['copy', 'paste'], 8 | elementType: 'input' 9 | }, 10 | { 11 | type: 'Composition', 12 | events: ['compositionEnd', 'compositionStart', 'compositionUpdate'], 13 | elementType: 'input' 14 | }, 15 | { 16 | type: 'Keyboard', 17 | events: ['keyDown', 'keyPress', 'keyUp'], 18 | elementType: 'input', 19 | init: { keyCode: 13 } 20 | }, 21 | { 22 | type: 'Focus', 23 | events: ['focus', 'blur'], 24 | elementType: 'input' 25 | }, 26 | { 27 | type: 'Form', 28 | events: ['focus', 'blur'], 29 | elementType: 'input' 30 | }, 31 | { 32 | type: 'Focus', 33 | events: ['input', 'invalid', 'change'], 34 | elementType: 'input' 35 | }, 36 | { 37 | type: 'Focus', 38 | events: ['submit'], 39 | elementType: 'form' 40 | }, 41 | { 42 | type: 'Mouse', 43 | events: [ 44 | 'click', 45 | 'contextMenu', 46 | 'dblClick', 47 | 'drag', 48 | 'dragEnd', 49 | 'dragEnter', 50 | 'dragExit', 51 | 'dragLeave', 52 | 'dragOver', 53 | 'dragStart', 54 | 'drop', 55 | 'mouseDown', 56 | 'mouseEnter', 57 | 'mouseLeave', 58 | 'mouseMove', 59 | 'mouseOut', 60 | 'mouseOver', 61 | 'mouseUp' 62 | ], 63 | elementType: 'button' 64 | }, 65 | { 66 | type: 'Selection', 67 | events: ['select'], 68 | elementType: 'input' 69 | }, 70 | { 71 | type: 'Touch', 72 | events: ['touchCancel', 'touchEnd', 'touchMove', 'touchStart'], 73 | elementType: 'button' 74 | }, 75 | { 76 | type: 'UI', 77 | events: ['scroll'], 78 | elementType: 'div' 79 | }, 80 | { 81 | type: 'Wheel', 82 | events: ['wheel'], 83 | elementType: 'div' 84 | }, 85 | { 86 | type: 'Media', 87 | events: [ 88 | 'abort', 89 | 'canPlay', 90 | 'canPlayThrough', 91 | 'durationChange', 92 | 'emptied', 93 | 'encrypted', 94 | 'ended', 95 | 'error', 96 | 'loadedData', 97 | 'loadedMetadata', 98 | 'loadStart', 99 | 'pause', 100 | 'play', 101 | 'playing', 102 | 'progress', 103 | 'rateChange', 104 | 'seeked', 105 | 'seeking', 106 | 'stalled', 107 | 'suspend', 108 | 'timeUpdate', 109 | 'volumeChange', 110 | 'waiting' 111 | ], 112 | elementType: 'video' 113 | }, 114 | { 115 | type: 'Image', 116 | events: ['load', 'error'], 117 | elementType: 'img' 118 | }, 119 | { 120 | type: 'Animation', 121 | events: ['animationStart', 'animationEnd', 'animationIteration'], 122 | elementType: 'div' 123 | }, 124 | { 125 | type: 'Transition', 126 | events: ['transitionEnd'], 127 | elementType: 'div' 128 | }, 129 | { 130 | type: 'Pointer', 131 | events: ['pointerEnter', 'pointerLeave'], 132 | elementType: 'div' 133 | } 134 | ] 135 | 136 | eventTypes.forEach(({ type, events, elementType, init }) => { 137 | describe(`${type} Events`, () => { 138 | events.forEach((eventName) => { 139 | const eventProp = `on${eventName[0].toUpperCase() + eventName.slice(1)}` 140 | 141 | it(`triggers ${eventProp}`, () => { 142 | const ref = createRef() 143 | const spy = jest.fn() 144 | 145 | render( 146 | h(elementType, { 147 | [eventProp]: spy, 148 | ref 149 | }) 150 | ) 151 | 152 | expect(fireEvent[eventName](ref.current, init)).toBe(true) 153 | 154 | expect(spy).toHaveBeenCalledTimes(1) 155 | if (init) { 156 | expect(spy).toHaveBeenCalledWith(expect.objectContaining(init)) 157 | } 158 | }) 159 | }) 160 | }) 161 | }) 162 | 163 | test('onInput works', () => { 164 | const handler = jest.fn() 165 | 166 | const { 167 | container: { firstChild: input } 168 | } = render() 169 | 170 | const targetProperties = { value: 'a' } 171 | const otherProperties = { isComposing: true } 172 | const init = { 173 | target: targetProperties, 174 | ...otherProperties 175 | } 176 | 177 | expect(fireEvent.input(input, init)).toBe(true) 178 | 179 | expect(handler).toHaveBeenCalledTimes(1) 180 | expect(handler).toHaveBeenCalledWith(expect.objectContaining(otherProperties)) 181 | }) 182 | 183 | test('calling `fireEvent` directly works too', () => { 184 | const handler = jest.fn() 185 | 186 | const { 187 | container: { firstChild: button } 188 | } = render( 39 | 40 |
41 | ) 42 | } 43 | } 44 | 45 | const wait = (time) => new Promise((resolve) => setTimeout(resolve, time)) 46 | 47 | test('unmounts a component', async () => { 48 | jest.spyOn(console, 'error').mockImplementation(() => {}) 49 | 50 | const { unmount, getByText, container } = render() 51 | 52 | fireEvent.click(getByText('Start')) 53 | 54 | unmount() 55 | 56 | // Hey there reader! You don't need to have an assertion like this one 57 | // this is just me making sure that the unmount function works. 58 | // You don't need to do this in your apps. Just rely on the fact that this works. 59 | expect(container.innerHTML).toBe('') 60 | 61 | // Just wait to see if the interval is cleared or not. 62 | // If it's not, then we'll call setState on an unmounted component and get an error. 63 | await wait(() => expect(console.error).not.toHaveBeenCalled()) 64 | }) 65 | -------------------------------------------------------------------------------- /src/fire-event.js: -------------------------------------------------------------------------------- 1 | import { fireEvent as domFireEvent, createEvent } from '@testing-library/dom' 2 | import { options } from 'preact' 3 | 4 | let isCompat = false 5 | 6 | // Detects if preact/compat is used 7 | const oldHook = options.vnode 8 | options.vnode = (vnode) => { 9 | if (vnode.$$typeof) isCompat = true 10 | if (oldHook) oldHook(vnode) 11 | } 12 | 13 | // Renames event to match React (preact/compat) version 14 | const renameEventCompat = (key) => { 15 | return key === 'change' ? 'input' : key 16 | } 17 | 18 | // Similar to RTL we make are own fireEvent helper that just calls DTL's fireEvent with that 19 | // we can that any specific behaviors to the helpers we need 20 | export const fireEvent = (...args) => domFireEvent(...args) 21 | 22 | Object.keys(domFireEvent).forEach((key) => { 23 | fireEvent[key] = (elem, init) => { 24 | // Preact registers event-listeners in lower-case, so onPointerStart becomes pointerStart 25 | // here we will copy this behavior, when we fire an element we will fire it in lowercase so 26 | // we hit the Preact listeners. 27 | const eventName = `on${key.toLowerCase()}` 28 | const isInElem = eventName in elem 29 | // Preact changes all change events to input events when running 'preact/compat', 30 | // making the event name out of sync. 31 | // The problematic code is in: preact/compat/src/render.js > handleDomVNode() 32 | const keyFiltered = !isCompat ? key : renameEventCompat(key) 33 | 34 | return isInElem 35 | ? domFireEvent[keyFiltered](elem, init) 36 | : domFireEvent( 37 | elem, 38 | createEvent(keyFiltered[0].toUpperCase() + keyFiltered.slice(1), elem, init) 39 | ) 40 | } 41 | }) 42 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { cleanup } from './pure' 2 | 3 | // If we're running in a test runner that supports afterEach 4 | // or teardown then we'll automatically run cleanup afterEach test 5 | // this ensures that tests run in isolation from each other. 6 | // If you don't like this then either import the `pure` module 7 | // or set the PTL_SKIP_AUTO_CLEANUP env variable to 'true'. 8 | if (typeof process === "undefined" || !process.env.PTL_SKIP_AUTO_CLEANUP) { 9 | if (typeof afterEach === 'function') { 10 | afterEach(async () => { 11 | await cleanup() 12 | }) 13 | } else if (typeof teardown === 'function') { 14 | // eslint-disable-next-line no-undef 15 | teardown(async () => { 16 | await cleanup() 17 | }) 18 | } 19 | } 20 | 21 | export * from './pure' 22 | -------------------------------------------------------------------------------- /src/pure.js: -------------------------------------------------------------------------------- 1 | import { getQueriesForElement, prettyDOM, configure as configureDTL } from '@testing-library/dom' 2 | import { h, hydrate as preactHydrate, render as preactRender, createRef } from 'preact' 3 | import { useEffect } from 'preact/hooks' 4 | import { act } from 'preact/test-utils' 5 | import { fireEvent } from './fire-event' 6 | 7 | configureDTL({ 8 | asyncWrapper: async cb => { 9 | let result 10 | await act(() => { 11 | result = cb() 12 | }) 13 | return result 14 | }, 15 | eventWrapper: cb => { 16 | let result 17 | act(() => { 18 | result = cb() 19 | }) 20 | return result 21 | } 22 | }) 23 | 24 | const mountedContainers = new Set() 25 | 26 | function render ( 27 | ui, 28 | { 29 | container, 30 | baseElement = container, 31 | queries, 32 | hydrate = false, 33 | wrapper: WrapperComponent 34 | } = {} 35 | ) { 36 | if (!baseElement) { 37 | // Default to document.body instead of documentElement to avoid output of potentially-large 38 | // head elements (such as JSS style blocks) in debug output. 39 | baseElement = document.body 40 | } 41 | 42 | if (!container) { 43 | container = baseElement.appendChild(document.createElement('div')) 44 | } 45 | 46 | // We'll add it to the mounted containers regardless of whether it's actually 47 | // added to document.body so the cleanup method works regardless of whether 48 | // they're passing us a custom container or not. 49 | mountedContainers.add(container) 50 | 51 | const wrapUiIfNeeded = (innerElement) => (WrapperComponent 52 | ? h(WrapperComponent, null, innerElement) 53 | : innerElement) 54 | 55 | act(() => { 56 | if (hydrate) { 57 | preactHydrate(wrapUiIfNeeded(ui), container) 58 | } else { 59 | preactRender(wrapUiIfNeeded(ui), container) 60 | } 61 | }) 62 | 63 | return { 64 | container, 65 | baseElement, 66 | debug: (el = baseElement, maxLength, options) => 67 | Array.isArray(el) 68 | // eslint-disable-next-line no-console 69 | ? el.forEach(e => console.log(prettyDOM(e, maxLength, options))) 70 | // eslint-disable-next-line no-console, 71 | : console.log(prettyDOM(el, maxLength, options)), 72 | unmount: () => preactRender(null, container), 73 | rerender: (rerenderUi) => { 74 | act(() => {}) 75 | render(wrapUiIfNeeded(rerenderUi), { container, baseElement }) 76 | // Intentionally do not return anything to avoid unnecessarily complicating the API. 77 | // folks can use all the same utilities we return in the first place that are bound to 78 | // the container 79 | }, 80 | asFragment: () => { 81 | if (typeof document.createRange === 'function') { 82 | return document 83 | .createRange() 84 | .createContextualFragment(container.innerHTML) 85 | } else { 86 | const template = document.createElement('template') 87 | template.innerHTML = container.innerHTML 88 | return template.content 89 | } 90 | }, 91 | ...getQueriesForElement(baseElement, queries) 92 | } 93 | } 94 | 95 | // Maybe one day we'll expose this (perhaps even as a utility returned by render). 96 | // but let's wait until someone asks for it. 97 | function cleanupAtContainer (container) { 98 | preactRender(null, container) 99 | 100 | if (container.parentNode === document.body) { 101 | document.body.removeChild(container) 102 | } 103 | 104 | mountedContainers.delete(container) 105 | } 106 | 107 | function cleanup () { 108 | mountedContainers.forEach(cleanupAtContainer) 109 | } 110 | 111 | function renderHook (renderCallback, options) { 112 | const { initialProps, wrapper } = (options || {}) 113 | const result = createRef() 114 | 115 | function TestComponent ({ renderCallbackProps }) { 116 | const pendingResult = renderCallback(renderCallbackProps) 117 | 118 | useEffect(() => { 119 | result.current = pendingResult 120 | }) 121 | 122 | return null 123 | } 124 | 125 | const { rerender: baseRerender, unmount } = render( 126 | , 127 | { wrapper } 128 | ) 129 | 130 | function rerender (rerenderCallbackProps) { 131 | return baseRerender( 132 | 133 | ) 134 | } 135 | 136 | return { result, rerender, unmount } 137 | } 138 | 139 | // eslint-disable-next-line import/export 140 | export * from '@testing-library/dom' 141 | // eslint-disable-next-line import/export 142 | export { render, cleanup, act, fireEvent, renderHook } 143 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { queries, Queries, BoundFunction } from '@testing-library/dom' 2 | import { act as preactAct } from 'preact/test-utils' 3 | import { ComponentChild, ComponentType } from 'preact' 4 | 5 | export * from '@testing-library/dom' 6 | 7 | export type RenderResult = { 8 | container: Element 9 | baseElement: Element 10 | debug: (baseElement?: Element | DocumentFragment) => void 11 | rerender: (ui: ComponentChild) => void 12 | unmount: () => boolean 13 | asFragment: () => DocumentFragment 14 | } & { [P in keyof Q]: BoundFunction } 15 | 16 | export interface RenderOptions { 17 | container?: Element 18 | baseElement?: Element 19 | queries?: Q 20 | wrapper?: ComponentChild 21 | } 22 | 23 | type Omit = Pick> 24 | 25 | /** 26 | * Render into a container which is appended to document.body. It should be used with cleanup. 27 | */ 28 | export function render(ui: ComponentChild, options?: Omit): RenderResult 29 | 30 | export function render( 31 | ui: ComponentChild, 32 | options: RenderOptions, 33 | ): RenderResult 34 | 35 | /** 36 | * Unmounts Preact trees that were mounted with render. 37 | */ 38 | export function cleanup(): void 39 | 40 | /** 41 | * Simply calls preact/test-utils.act(cb) 42 | * 43 | * If that's not available (older version of preact) then it 44 | * simply calls the given callback immediately 45 | */ 46 | export const act: typeof preactAct extends undefined 47 | ? (callback: () => void) => void 48 | : typeof preactAct 49 | 50 | export interface RenderHookResult { 51 | /** 52 | * Triggers a re-render. The props will be passed to your renderHook callback. 53 | */ 54 | rerender: (props?: Props) => void 55 | /** 56 | * This is a stable reference to the latest value returned by your renderHook 57 | * callback 58 | */ 59 | result: { 60 | /** 61 | * The value returned by your renderHook callback 62 | */ 63 | current: Result 64 | } 65 | /** 66 | * Unmounts the test component. This is useful for when you need to test 67 | * any cleanup your useEffects have. 68 | */ 69 | unmount: () => void 70 | } 71 | 72 | export interface RenderHookOptions { 73 | /** 74 | * The argument passed to the renderHook callback. Can be useful if you plan 75 | * to use the rerender utility to change the values passed to your hook. 76 | */ 77 | initialProps?: Props 78 | /** 79 | * Pass a React Component as the wrapper option to have it rendered around the inner element. This is most useful for creating 80 | * reusable custom render functions for common data providers. See setup for examples. 81 | * 82 | * @see https://testing-library.com/docs/react-testing-library/api/#wrapper 83 | */ 84 | wrapper?: ComponentType<{ children: Element }> 85 | } 86 | 87 | /** 88 | * Allows you to render a hook within a test React component without having to 89 | * create that component yourself. 90 | */ 91 | export function renderHook( 92 | render: (initialProps: Props) => Result, 93 | options?: RenderHookOptions, 94 | ): RenderHookResult 95 | -------------------------------------------------------------------------------- /types/pure.d.ts: -------------------------------------------------------------------------------- 1 | export * from './' 2 | --------------------------------------------------------------------------------