├── .all-contributorsrc ├── .bundle.main.env ├── .bundle.pure.env ├── .codesandbox └── ci.json ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── Bug_Report.md │ ├── Feature_Request.md │ └── Question.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── validate.yml ├── .gitignore ├── .huskyrc.js ├── .npmrc ├── .prettierignore ├── .prettierrc.js ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── codecov.yml ├── dont-cleanup-after-each.js ├── jest.config.js ├── other ├── MAINTAINING.md ├── USERS.md ├── cheat-sheet.pdf ├── design files │ ├── README.txt │ └── cheat-sheet.afpub ├── goat.png ├── manual-releases.md └── testingjavascript.jpg ├── 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 │ ├── config.js │ ├── debug.js │ ├── end-to-end.js │ ├── error-handlers.js │ ├── events.js │ ├── multi-base.js │ ├── new-act.js │ ├── render.js │ ├── renderHook.js │ ├── rerender.js │ └── stopwatch.js ├── act-compat.js ├── config.js ├── fire-event.js ├── index.js └── pure.js ├── tests ├── failOnUnexpectedConsoleCalls.js ├── setup-env.js ├── shouldIgnoreConsoleError.js └── toWarnDev.js └── types ├── index.d.ts ├── pure.d.ts ├── test.tsx └── tsconfig.json /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "react-testing-library", 3 | "projectOwner": "testing-library", 4 | "repoType": "github", 5 | "files": [ 6 | "README.md" 7 | ], 8 | "imageSize": 100, 9 | "commit": false, 10 | "skipCi": false, 11 | "contributors": [ 12 | { 13 | "login": "kentcdodds", 14 | "name": "Kent C. Dodds", 15 | "avatar_url": "https://avatars.githubusercontent.com/u/1500684?v=3", 16 | "profile": "https://kentcdodds.com", 17 | "contributions": [ 18 | "code", 19 | "doc", 20 | "infra", 21 | "test" 22 | ] 23 | }, 24 | { 25 | "login": "audiolion", 26 | "name": "Ryan Castner", 27 | "avatar_url": "https://avatars1.githubusercontent.com/u/2430381?v=4", 28 | "profile": "http://audiolion.github.io", 29 | "contributions": [ 30 | "doc" 31 | ] 32 | }, 33 | { 34 | "login": "dnlsandiego", 35 | "name": "Daniel Sandiego", 36 | "avatar_url": "https://avatars0.githubusercontent.com/u/8008023?v=4", 37 | "profile": "https://www.dnlsandiego.com", 38 | "contributions": [ 39 | "code" 40 | ] 41 | }, 42 | { 43 | "login": "Miklet", 44 | "name": "Paweł Mikołajczyk", 45 | "avatar_url": "https://avatars2.githubusercontent.com/u/12592677?v=4", 46 | "profile": "https://github.com/Miklet", 47 | "contributions": [ 48 | "code" 49 | ] 50 | }, 51 | { 52 | "login": "alejandronanez", 53 | "name": "Alejandro Ñáñez Ortiz", 54 | "avatar_url": "https://avatars3.githubusercontent.com/u/464978?v=4", 55 | "profile": "http://co.linkedin.com/in/alejandronanez/", 56 | "contributions": [ 57 | "doc" 58 | ] 59 | }, 60 | { 61 | "login": "pbomb", 62 | "name": "Matt Parrish", 63 | "avatar_url": "https://avatars0.githubusercontent.com/u/1402095?v=4", 64 | "profile": "https://github.com/pbomb", 65 | "contributions": [ 66 | "bug", 67 | "code", 68 | "doc", 69 | "test" 70 | ] 71 | }, 72 | { 73 | "login": "wKovacs64", 74 | "name": "Justin Hall", 75 | "avatar_url": "https://avatars1.githubusercontent.com/u/1288694?v=4", 76 | "profile": "https://github.com/wKovacs64", 77 | "contributions": [ 78 | "platform" 79 | ] 80 | }, 81 | { 82 | "login": "antoaravinth", 83 | "name": "Anto Aravinth", 84 | "avatar_url": "https://avatars1.githubusercontent.com/u/1241511?s=460&v=4", 85 | "profile": "https://github.com/antoaravinth", 86 | "contributions": [ 87 | "code", 88 | "test", 89 | "doc" 90 | ] 91 | }, 92 | { 93 | "login": "JonahMoses", 94 | "name": "Jonah Moses", 95 | "avatar_url": "https://avatars2.githubusercontent.com/u/3462296?v=4", 96 | "profile": "https://github.com/JonahMoses", 97 | "contributions": [ 98 | "doc" 99 | ] 100 | }, 101 | { 102 | "login": "lgandecki", 103 | "name": "Łukasz Gandecki", 104 | "avatar_url": "https://avatars1.githubusercontent.com/u/4002543?v=4", 105 | "profile": "http://team.thebrain.pro", 106 | "contributions": [ 107 | "code", 108 | "test", 109 | "doc" 110 | ] 111 | }, 112 | { 113 | "login": "sompylasar", 114 | "name": "Ivan Babak", 115 | "avatar_url": "https://avatars2.githubusercontent.com/u/498274?v=4", 116 | "profile": "https://sompylasar.github.io", 117 | "contributions": [ 118 | "bug", 119 | "ideas" 120 | ] 121 | }, 122 | { 123 | "login": "jday3", 124 | "name": "Jesse Day", 125 | "avatar_url": "https://avatars3.githubusercontent.com/u/4439618?v=4", 126 | "profile": "https://github.com/jday3", 127 | "contributions": [ 128 | "code" 129 | ] 130 | }, 131 | { 132 | "login": "gnapse", 133 | "name": "Ernesto García", 134 | "avatar_url": "https://avatars0.githubusercontent.com/u/15199?v=4", 135 | "profile": "http://gnapse.github.io", 136 | "contributions": [ 137 | "question", 138 | "code", 139 | "doc" 140 | ] 141 | }, 142 | { 143 | "login": "jomaxx", 144 | "name": "Josef Maxx Blake", 145 | "avatar_url": "https://avatars2.githubusercontent.com/u/2747424?v=4", 146 | "profile": "http://jomaxx.com", 147 | "contributions": [ 148 | "code", 149 | "doc", 150 | "test" 151 | ] 152 | }, 153 | { 154 | "login": "mbaranovski", 155 | "name": "Michal Baranowski", 156 | "avatar_url": "https://avatars1.githubusercontent.com/u/29602306?v=4", 157 | "profile": "https://twitter.com/baranovskim", 158 | "contributions": [ 159 | "blog", 160 | "tutorial" 161 | ] 162 | }, 163 | { 164 | "login": "aputhin", 165 | "name": "Arthur Puthin", 166 | "avatar_url": "https://avatars3.githubusercontent.com/u/13985684?v=4", 167 | "profile": "https://github.com/aputhin", 168 | "contributions": [ 169 | "doc" 170 | ] 171 | }, 172 | { 173 | "login": "thchia", 174 | "name": "Thomas Chia", 175 | "avatar_url": "https://avatars2.githubusercontent.com/u/21194045?v=4", 176 | "profile": "https://github.com/thchia", 177 | "contributions": [ 178 | "code", 179 | "doc" 180 | ] 181 | }, 182 | { 183 | "login": "thiagopaiva99", 184 | "name": "Thiago Galvani", 185 | "avatar_url": "https://avatars3.githubusercontent.com/u/20430611?v=4", 186 | "profile": "http://ilegra.com/", 187 | "contributions": [ 188 | "doc" 189 | ] 190 | }, 191 | { 192 | "login": "ChrisWcs", 193 | "name": "Christian", 194 | "avatar_url": "https://avatars1.githubusercontent.com/u/19828824?v=4", 195 | "profile": "http://Chriswcs.github.io", 196 | "contributions": [ 197 | "test" 198 | ] 199 | }, 200 | { 201 | "login": "alexkrolick", 202 | "name": "Alex Krolick", 203 | "avatar_url": "https://avatars3.githubusercontent.com/u/1571667?v=4", 204 | "profile": "https://alexkrolick.com", 205 | "contributions": [ 206 | "question", 207 | "doc", 208 | "example", 209 | "ideas" 210 | ] 211 | }, 212 | { 213 | "login": "johann-sonntagbauer", 214 | "name": "Johann Hubert Sonntagbauer", 215 | "avatar_url": "https://avatars3.githubusercontent.com/u/1239401?v=4", 216 | "profile": "https://github.com/johann-sonntagbauer", 217 | "contributions": [ 218 | "code", 219 | "doc", 220 | "test" 221 | ] 222 | }, 223 | { 224 | "login": "maddijoyce", 225 | "name": "Maddi Joyce", 226 | "avatar_url": "https://avatars2.githubusercontent.com/u/2224291?v=4", 227 | "profile": "http://www.maddijoyce.com", 228 | "contributions": [ 229 | "code" 230 | ] 231 | }, 232 | { 233 | "login": "RyanAtViceSoftware", 234 | "name": "Ryan Vice", 235 | "avatar_url": "https://avatars2.githubusercontent.com/u/10080111?v=4", 236 | "profile": "http://www.vicesoftware.com", 237 | "contributions": [ 238 | "doc" 239 | ] 240 | }, 241 | { 242 | "login": "iwilsonq", 243 | "name": "Ian Wilson", 244 | "avatar_url": "https://avatars1.githubusercontent.com/u/7942604?v=4", 245 | "profile": "https://ianwilson.io", 246 | "contributions": [ 247 | "blog", 248 | "tutorial" 249 | ] 250 | }, 251 | { 252 | "login": "InExtremaRes", 253 | "name": "Daniel", 254 | "avatar_url": "https://avatars2.githubusercontent.com/u/1635491?v=4", 255 | "profile": "https://github.com/InExtremaRes", 256 | "contributions": [ 257 | "bug", 258 | "code" 259 | ] 260 | }, 261 | { 262 | "login": "Gpx", 263 | "name": "Giorgio Polvara", 264 | "avatar_url": "https://avatars0.githubusercontent.com/u/767959?v=4", 265 | "profile": "https://twitter.com/Gpx", 266 | "contributions": [ 267 | "bug", 268 | "ideas" 269 | ] 270 | }, 271 | { 272 | "login": "jgoz", 273 | "name": "John Gozde", 274 | "avatar_url": "https://avatars2.githubusercontent.com/u/132233?v=4", 275 | "profile": "https://github.com/jgoz", 276 | "contributions": [ 277 | "code" 278 | ] 279 | }, 280 | { 281 | "login": "SavePointSam", 282 | "name": "Sam Horton", 283 | "avatar_url": "https://avatars0.githubusercontent.com/u/8203211?v=4", 284 | "profile": "https://twitter.com/SavePointSam", 285 | "contributions": [ 286 | "doc", 287 | "example", 288 | "ideas" 289 | ] 290 | }, 291 | { 292 | "login": "rkotze", 293 | "name": "Richard Kotze (mobile)", 294 | "avatar_url": "https://avatars2.githubusercontent.com/u/10452163?v=4", 295 | "profile": "http://www.richardkotze.com", 296 | "contributions": [ 297 | "doc" 298 | ] 299 | }, 300 | { 301 | "login": "sotobuild", 302 | "name": "Brahian E. Soto Mercedes", 303 | "avatar_url": "https://avatars2.githubusercontent.com/u/10819833?v=4", 304 | "profile": "https://github.com/sotobuild", 305 | "contributions": [ 306 | "doc" 307 | ] 308 | }, 309 | { 310 | "login": "bdelaforest", 311 | "name": "Benoit de La Forest", 312 | "avatar_url": "https://avatars2.githubusercontent.com/u/7151559?v=4", 313 | "profile": "https://github.com/bdelaforest", 314 | "contributions": [ 315 | "doc" 316 | ] 317 | }, 318 | { 319 | "login": "thesalah", 320 | "name": "Salah", 321 | "avatar_url": "https://avatars3.githubusercontent.com/u/6624197?v=4", 322 | "profile": "https://github.com/thesalah", 323 | "contributions": [ 324 | "code", 325 | "test" 326 | ] 327 | }, 328 | { 329 | "login": "icfantv", 330 | "name": "Adam Gordon", 331 | "avatar_url": "https://avatars2.githubusercontent.com/u/370054?v=4", 332 | "profile": "http://gordonizer.com", 333 | "contributions": [ 334 | "bug", 335 | "code" 336 | ] 337 | }, 338 | { 339 | "login": "silvenon", 340 | "name": "Matija Marohnić", 341 | "avatar_url": "https://avatars2.githubusercontent.com/u/471278?v=4", 342 | "profile": "https://silvenon.com", 343 | "contributions": [ 344 | "doc" 345 | ] 346 | }, 347 | { 348 | "login": "Dajust", 349 | "name": "Justice Mba", 350 | "avatar_url": "https://avatars3.githubusercontent.com/u/8015514?v=4", 351 | "profile": "https://github.com/Dajust", 352 | "contributions": [ 353 | "doc" 354 | ] 355 | }, 356 | { 357 | "login": "MarkPollmann", 358 | "name": "Mark Pollmann", 359 | "avatar_url": "https://avatars2.githubusercontent.com/u/5286559?v=4", 360 | "profile": "https://markpollmann.com/", 361 | "contributions": [ 362 | "doc" 363 | ] 364 | }, 365 | { 366 | "login": "ehteshamkafeel", 367 | "name": "Ehtesham Kafeel", 368 | "avatar_url": "https://avatars1.githubusercontent.com/u/1213123?v=4", 369 | "profile": "https://github.com/ehteshamkafeel", 370 | "contributions": [ 371 | "code", 372 | "doc" 373 | ] 374 | }, 375 | { 376 | "login": "jpavon", 377 | "name": "Julio Pavón", 378 | "avatar_url": "https://avatars2.githubusercontent.com/u/1493505?v=4", 379 | "profile": "http://jpavon.com", 380 | "contributions": [ 381 | "code" 382 | ] 383 | }, 384 | { 385 | "login": "duncanleung", 386 | "name": "Duncan L", 387 | "avatar_url": "https://avatars3.githubusercontent.com/u/1765048?v=4", 388 | "profile": "http://www.duncanleung.com/", 389 | "contributions": [ 390 | "doc", 391 | "example" 392 | ] 393 | }, 394 | { 395 | "login": "tyagow", 396 | "name": "Tiago Almeida", 397 | "avatar_url": "https://avatars1.githubusercontent.com/u/700778?v=4", 398 | "profile": "https://www.linkedin.com/in/tyagow/?locale=en_US", 399 | "contributions": [ 400 | "doc" 401 | ] 402 | }, 403 | { 404 | "login": "rbrtsmith", 405 | "name": "Robert Smith", 406 | "avatar_url": "https://avatars2.githubusercontent.com/u/4982001?v=4", 407 | "profile": "http://rbrtsmith.com/", 408 | "contributions": [ 409 | "bug" 410 | ] 411 | }, 412 | { 413 | "login": "zgreen", 414 | "name": "Zach Green", 415 | "avatar_url": "https://avatars0.githubusercontent.com/u/1700355?v=4", 416 | "profile": "https://offbyone.tech", 417 | "contributions": [ 418 | "doc" 419 | ] 420 | }, 421 | { 422 | "login": "dadamssg", 423 | "name": "dadamssg", 424 | "avatar_url": "https://avatars3.githubusercontent.com/u/881986?v=4", 425 | "profile": "https://github.com/dadamssg", 426 | "contributions": [ 427 | "doc" 428 | ] 429 | }, 430 | { 431 | "login": "YazanAabeed", 432 | "name": "Yazan Aabed", 433 | "avatar_url": "https://avatars0.githubusercontent.com/u/8734097?v=4", 434 | "profile": "https://www.yaabed.com/", 435 | "contributions": [ 436 | "blog" 437 | ] 438 | }, 439 | { 440 | "login": "timbonicus", 441 | "name": "Tim", 442 | "avatar_url": "https://avatars0.githubusercontent.com/u/556258?v=4", 443 | "profile": "https://github.com/timbonicus", 444 | "contributions": [ 445 | "bug", 446 | "code", 447 | "doc", 448 | "test" 449 | ] 450 | }, 451 | { 452 | "login": "divyanshu013", 453 | "name": "Divyanshu Maithani", 454 | "avatar_url": "https://avatars3.githubusercontent.com/u/6682655?v=4", 455 | "profile": "http://divyanshu.xyz", 456 | "contributions": [ 457 | "tutorial", 458 | "video" 459 | ] 460 | }, 461 | { 462 | "login": "metagrover", 463 | "name": "Deepak Grover", 464 | "avatar_url": "https://avatars2.githubusercontent.com/u/9116042?v=4", 465 | "profile": "https://www.linkedin.com/in/metagrover", 466 | "contributions": [ 467 | "tutorial", 468 | "video" 469 | ] 470 | }, 471 | { 472 | "login": "eyalcohen4", 473 | "name": "Eyal Cohen", 474 | "avatar_url": "https://avatars0.githubusercontent.com/u/16276358?v=4", 475 | "profile": "https://github.com/eyalcohen4", 476 | "contributions": [ 477 | "doc" 478 | ] 479 | }, 480 | { 481 | "login": "petermakowski", 482 | "name": "Peter Makowski", 483 | "avatar_url": "https://avatars3.githubusercontent.com/u/7452681?v=4", 484 | "profile": "https://github.com/petermakowski", 485 | "contributions": [ 486 | "doc" 487 | ] 488 | }, 489 | { 490 | "login": "Michielnuyts", 491 | "name": "Michiel Nuyts", 492 | "avatar_url": "https://avatars2.githubusercontent.com/u/20361668?v=4", 493 | "profile": "https://github.com/Michielnuyts", 494 | "contributions": [ 495 | "doc" 496 | ] 497 | }, 498 | { 499 | "login": "joeynimu", 500 | "name": "Joe Ng'ethe", 501 | "avatar_url": "https://avatars0.githubusercontent.com/u/1195863?v=4", 502 | "profile": "https://github.com/joeynimu", 503 | "contributions": [ 504 | "code", 505 | "doc" 506 | ] 507 | }, 508 | { 509 | "login": "Enikol", 510 | "name": "Kate", 511 | "avatar_url": "https://avatars3.githubusercontent.com/u/19998290?v=4", 512 | "profile": "https://github.com/Enikol", 513 | "contributions": [ 514 | "doc" 515 | ] 516 | }, 517 | { 518 | "login": "SeanRParker", 519 | "name": "Sean", 520 | "avatar_url": "https://avatars1.githubusercontent.com/u/11980217?v=4", 521 | "profile": "http://www.seanrparker.com", 522 | "contributions": [ 523 | "doc" 524 | ] 525 | }, 526 | { 527 | "login": "jlongster", 528 | "name": "James Long", 529 | "avatar_url": "https://avatars2.githubusercontent.com/u/17031?v=4", 530 | "profile": "http://jlongster.com", 531 | "contributions": [ 532 | "ideas", 533 | "platform" 534 | ] 535 | }, 536 | { 537 | "login": "hhagely", 538 | "name": "Herb Hagely", 539 | "avatar_url": "https://avatars1.githubusercontent.com/u/10118777?v=4", 540 | "profile": "https://github.com/hhagely", 541 | "contributions": [ 542 | "example" 543 | ] 544 | }, 545 | { 546 | "login": "themostcolm", 547 | "name": "Alex Wendte", 548 | "avatar_url": "https://avatars2.githubusercontent.com/u/5779538?v=4", 549 | "profile": "http://www.wendtedesigns.com/", 550 | "contributions": [ 551 | "example" 552 | ] 553 | }, 554 | { 555 | "login": "M0nica", 556 | "name": "Monica Powell", 557 | "avatar_url": "https://avatars0.githubusercontent.com/u/6998954?v=4", 558 | "profile": "http://www.aboutmonica.com", 559 | "contributions": [ 560 | "doc" 561 | ] 562 | }, 563 | { 564 | "login": "sivkoff", 565 | "name": "Vitaly Sivkov", 566 | "avatar_url": "https://avatars1.githubusercontent.com/u/2699953?v=4", 567 | "profile": "http://sivkoff.com", 568 | "contributions": [ 569 | "code" 570 | ] 571 | }, 572 | { 573 | "login": "weyert", 574 | "name": "Weyert de Boer", 575 | "avatar_url": "https://avatars3.githubusercontent.com/u/7049?v=4", 576 | "profile": "https://github.com/weyert", 577 | "contributions": [ 578 | "ideas", 579 | "review", 580 | "design" 581 | ] 582 | }, 583 | { 584 | "login": "EstebanMarin", 585 | "name": "EstebanMarin", 586 | "avatar_url": "https://avatars3.githubusercontent.com/u/13613037?v=4", 587 | "profile": "https://github.com/EstebanMarin", 588 | "contributions": [ 589 | "doc" 590 | ] 591 | }, 592 | { 593 | "login": "vctormb", 594 | "name": "Victor Martins", 595 | "avatar_url": "https://avatars2.githubusercontent.com/u/13953703?v=4", 596 | "profile": "https://github.com/vctormb", 597 | "contributions": [ 598 | "doc" 599 | ] 600 | }, 601 | { 602 | "login": "RoystonS", 603 | "name": "Royston Shufflebotham", 604 | "avatar_url": "https://avatars0.githubusercontent.com/u/19773?v=4", 605 | "profile": "https://github.com/RoystonS", 606 | "contributions": [ 607 | "bug", 608 | "doc", 609 | "example" 610 | ] 611 | }, 612 | { 613 | "login": "chrbala", 614 | "name": "chrbala", 615 | "avatar_url": "https://avatars0.githubusercontent.com/u/6834804?v=4", 616 | "profile": "https://github.com/chrbala", 617 | "contributions": [ 618 | "code" 619 | ] 620 | }, 621 | { 622 | "login": "donavon", 623 | "name": "Donavon West", 624 | "avatar_url": "https://avatars3.githubusercontent.com/u/887639?v=4", 625 | "profile": "http://donavon.com", 626 | "contributions": [ 627 | "code", 628 | "doc", 629 | "ideas", 630 | "test" 631 | ] 632 | }, 633 | { 634 | "login": "maisano", 635 | "name": "Richard Maisano", 636 | "avatar_url": "https://avatars2.githubusercontent.com/u/689081?v=4", 637 | "profile": "https://github.com/maisano", 638 | "contributions": [ 639 | "code" 640 | ] 641 | }, 642 | { 643 | "login": "marcobiedermann", 644 | "name": "Marco Biedermann", 645 | "avatar_url": "https://avatars0.githubusercontent.com/u/5244986?v=4", 646 | "profile": "https://www.marcobiedermann.com", 647 | "contributions": [ 648 | "code", 649 | "maintenance", 650 | "test" 651 | ] 652 | }, 653 | { 654 | "login": "alexzherdev", 655 | "name": "Alex Zherdev", 656 | "avatar_url": "https://avatars3.githubusercontent.com/u/93752?v=4", 657 | "profile": "https://github.com/alexzherdev", 658 | "contributions": [ 659 | "bug", 660 | "code" 661 | ] 662 | }, 663 | { 664 | "login": "Andrewmat", 665 | "name": "André Matulionis dos Santos", 666 | "avatar_url": "https://avatars0.githubusercontent.com/u/5133846?v=4", 667 | "profile": "https://twitter.com/Andrewmat", 668 | "contributions": [ 669 | "code", 670 | "example", 671 | "test" 672 | ] 673 | }, 674 | { 675 | "login": "FredyC", 676 | "name": "Daniel K.", 677 | "avatar_url": "https://avatars0.githubusercontent.com/u/1096340?v=4", 678 | "profile": "https://github.com/FredyC", 679 | "contributions": [ 680 | "bug", 681 | "code", 682 | "ideas", 683 | "test", 684 | "review" 685 | ] 686 | }, 687 | { 688 | "login": "mohamedmagdy17593", 689 | "name": "mohamedmagdy17593", 690 | "avatar_url": "https://avatars0.githubusercontent.com/u/40938625?v=4", 691 | "profile": "https://github.com/mohamedmagdy17593", 692 | "contributions": [ 693 | "code" 694 | ] 695 | }, 696 | { 697 | "login": "lorensr", 698 | "name": "Loren ☺️", 699 | "avatar_url": "https://avatars2.githubusercontent.com/u/251288?v=4", 700 | "profile": "http://lorensr.me", 701 | "contributions": [ 702 | "doc" 703 | ] 704 | }, 705 | { 706 | "login": "MarkFalconbridge", 707 | "name": "MarkFalconbridge", 708 | "avatar_url": "https://avatars1.githubusercontent.com/u/20678943?v=4", 709 | "profile": "https://github.com/MarkFalconbridge", 710 | "contributions": [ 711 | "bug", 712 | "code" 713 | ] 714 | }, 715 | { 716 | "login": "viniciusavieira", 717 | "name": "Vinicius", 718 | "avatar_url": "https://avatars0.githubusercontent.com/u/2073019?v=4", 719 | "profile": "https://github.com/viniciusavieira", 720 | "contributions": [ 721 | "doc", 722 | "example" 723 | ] 724 | }, 725 | { 726 | "login": "pschyma", 727 | "name": "Peter Schyma", 728 | "avatar_url": "https://avatars2.githubusercontent.com/u/2489928?v=4", 729 | "profile": "https://github.com/pschyma", 730 | "contributions": [ 731 | "code" 732 | ] 733 | }, 734 | { 735 | "login": "ianschmitz", 736 | "name": "Ian Schmitz", 737 | "avatar_url": "https://avatars1.githubusercontent.com/u/6355370?v=4", 738 | "profile": "https://github.com/ianschmitz", 739 | "contributions": [ 740 | "doc" 741 | ] 742 | }, 743 | { 744 | "login": "joual", 745 | "name": "Joel Marcotte", 746 | "avatar_url": "https://avatars0.githubusercontent.com/u/157877?v=4", 747 | "profile": "https://github.com/joual", 748 | "contributions": [ 749 | "bug", 750 | "test", 751 | "code" 752 | ] 753 | }, 754 | { 755 | "login": "aledustet", 756 | "name": "Alejandro Dustet", 757 | "avatar_url": "https://avatars3.githubusercontent.com/u/2413802?v=4", 758 | "profile": "http://aledustet.com", 759 | "contributions": [ 760 | "bug" 761 | ] 762 | }, 763 | { 764 | "login": "bcarroll22", 765 | "name": "Brandon Carroll", 766 | "avatar_url": "https://avatars2.githubusercontent.com/u/11020406?v=4", 767 | "profile": "https://github.com/bcarroll22", 768 | "contributions": [ 769 | "doc" 770 | ] 771 | }, 772 | { 773 | "login": "lucas0707", 774 | "name": "Lucas Machado", 775 | "avatar_url": "https://avatars1.githubusercontent.com/u/26284338?v=4", 776 | "profile": "https://github.com/lucas0707", 777 | "contributions": [ 778 | "doc" 779 | ] 780 | }, 781 | { 782 | "login": "pascalduez", 783 | "name": "Pascal Duez", 784 | "avatar_url": "https://avatars3.githubusercontent.com/u/335467?v=4", 785 | "profile": "http://pascalduez.me", 786 | "contributions": [ 787 | "platform" 788 | ] 789 | }, 790 | { 791 | "login": "NMinhNguyen", 792 | "name": "Minh Nguyen", 793 | "avatar_url": "https://avatars3.githubusercontent.com/u/2852660?v=4", 794 | "profile": "https://twitter.com/minh_ngvyen", 795 | "contributions": [ 796 | "code" 797 | ] 798 | }, 799 | { 800 | "login": "LiaoJimmy", 801 | "name": "LiaoJimmy", 802 | "avatar_url": "https://avatars0.githubusercontent.com/u/11155585?v=4", 803 | "profile": "http://iababy46.blogspot.tw/", 804 | "contributions": [ 805 | "doc" 806 | ] 807 | }, 808 | { 809 | "login": "threepointone", 810 | "name": "Sunil Pai", 811 | "avatar_url": "https://avatars2.githubusercontent.com/u/18808?v=4", 812 | "profile": "https://github.com/threepointone", 813 | "contributions": [ 814 | "code", 815 | "test" 816 | ] 817 | }, 818 | { 819 | "login": "gaearon", 820 | "name": "Dan Abramov", 821 | "avatar_url": "https://avatars0.githubusercontent.com/u/810438?v=4", 822 | "profile": "http://twitter.com/dan_abramov", 823 | "contributions": [ 824 | "review" 825 | ] 826 | }, 827 | { 828 | "login": "ChristianMurphy", 829 | "name": "Christian Murphy", 830 | "avatar_url": "https://avatars3.githubusercontent.com/u/3107513?v=4", 831 | "profile": "https://github.com/ChristianMurphy", 832 | "contributions": [ 833 | "infra" 834 | ] 835 | }, 836 | { 837 | "login": "jeetiss", 838 | "name": "Ivakhnenko Dmitry", 839 | "avatar_url": "https://avatars1.githubusercontent.com/u/6726016?v=4", 840 | "profile": "https://jeetiss.github.io/", 841 | "contributions": [ 842 | "code" 843 | ] 844 | }, 845 | { 846 | "login": "jamesgeorge007", 847 | "name": "James George", 848 | "avatar_url": "https://avatars2.githubusercontent.com/u/25279263?v=4", 849 | "profile": "https://ghuser.io/jamesgeorge007", 850 | "contributions": [ 851 | "doc" 852 | ] 853 | }, 854 | { 855 | "login": "JSFernandes", 856 | "name": "João Fernandes", 857 | "avatar_url": "https://avatars1.githubusercontent.com/u/1075053?v=4", 858 | "profile": "https://joaofernandes.me/", 859 | "contributions": [ 860 | "doc" 861 | ] 862 | }, 863 | { 864 | "login": "alejandroperea", 865 | "name": "Alejandro Perea", 866 | "avatar_url": "https://avatars3.githubusercontent.com/u/6084749?v=4", 867 | "profile": "https://github.com/alejandroperea", 868 | "contributions": [ 869 | "review" 870 | ] 871 | }, 872 | { 873 | "login": "nickmccurdy", 874 | "name": "Nick McCurdy", 875 | "avatar_url": "https://avatars0.githubusercontent.com/u/927220?v=4", 876 | "profile": "https://nickmccurdy.com/", 877 | "contributions": [ 878 | "review", 879 | "question", 880 | "infra" 881 | ] 882 | }, 883 | { 884 | "login": "eps1lon", 885 | "name": "Sebastian Silbermann", 886 | "avatar_url": "https://avatars3.githubusercontent.com/u/12292047?v=4", 887 | "profile": "https://twitter.com/sebsilbermann", 888 | "contributions": [ 889 | "review" 890 | ] 891 | }, 892 | { 893 | "login": "afontcu", 894 | "name": "Adrià Fontcuberta", 895 | "avatar_url": "https://avatars0.githubusercontent.com/u/9197791?v=4", 896 | "profile": "https://afontcu.dev", 897 | "contributions": [ 898 | "review", 899 | "doc" 900 | ] 901 | }, 902 | { 903 | "login": "johnnyreilly", 904 | "name": "John Reilly", 905 | "avatar_url": "https://avatars0.githubusercontent.com/u/1010525?v=4", 906 | "profile": "https://blog.johnnyreilly.com/", 907 | "contributions": [ 908 | "review" 909 | ] 910 | }, 911 | { 912 | "login": "MichaelDeBoey", 913 | "name": "Michaël De Boey", 914 | "avatar_url": "https://avatars3.githubusercontent.com/u/6643991?v=4", 915 | "profile": "https://michaeldeboey.be", 916 | "contributions": [ 917 | "review", 918 | "code" 919 | ] 920 | }, 921 | { 922 | "login": "cimbul", 923 | "name": "Tim Yates", 924 | "avatar_url": "https://avatars2.githubusercontent.com/u/927923?v=4", 925 | "profile": "https://cimbul.com", 926 | "contributions": [ 927 | "review" 928 | ] 929 | }, 930 | { 931 | "login": "eventualbuddha", 932 | "name": "Brian Donovan", 933 | "avatar_url": "https://avatars3.githubusercontent.com/u/1938?v=4", 934 | "profile": "https://github.com/eventualbuddha", 935 | "contributions": [ 936 | "code" 937 | ] 938 | }, 939 | { 940 | "login": "JaysQubeXon", 941 | "name": "Noam Gabriel Jacobson", 942 | "avatar_url": "https://avatars1.githubusercontent.com/u/18309230?v=4", 943 | "profile": "https://github.com/JaysQubeXon", 944 | "contributions": [ 945 | "doc" 946 | ] 947 | }, 948 | { 949 | "login": "rvdkooy", 950 | "name": "Ronald van der Kooij", 951 | "avatar_url": "https://avatars1.githubusercontent.com/u/4119960?v=4", 952 | "profile": "https://github.com/rvdkooy", 953 | "contributions": [ 954 | "test", 955 | "code" 956 | ] 957 | }, 958 | { 959 | "login": "aayushrajvanshi", 960 | "name": "Aayush Rajvanshi", 961 | "avatar_url": "https://avatars0.githubusercontent.com/u/14968551?v=4", 962 | "profile": "https://github.com/aayushrajvanshi", 963 | "contributions": [ 964 | "doc" 965 | ] 966 | }, 967 | { 968 | "login": "ely-alamillo", 969 | "name": "Ely Alamillo", 970 | "avatar_url": "https://avatars2.githubusercontent.com/u/24350492?v=4", 971 | "profile": "https://elyalamillo.com", 972 | "contributions": [ 973 | "code", 974 | "test" 975 | ] 976 | }, 977 | { 978 | "login": "danieljcafonso", 979 | "name": "Daniel Afonso", 980 | "avatar_url": "https://avatars3.githubusercontent.com/u/35337607?v=4", 981 | "profile": "https://github.com/danieljcafonso", 982 | "contributions": [ 983 | "code", 984 | "test" 985 | ] 986 | }, 987 | { 988 | "login": "LaurensBosscher", 989 | "name": "Laurens Bosscher", 990 | "avatar_url": "https://avatars0.githubusercontent.com/u/13363196?v=4", 991 | "profile": "http://www.laurensbosscher.nl", 992 | "contributions": [ 993 | "code" 994 | ] 995 | }, 996 | { 997 | "login": "sakito21", 998 | "name": "Sakito Mukai", 999 | "avatar_url": "https://avatars1.githubusercontent.com/u/15010907?v=4", 1000 | "profile": "https://twitter.com/__sakito__", 1001 | "contributions": [ 1002 | "doc" 1003 | ] 1004 | }, 1005 | { 1006 | "login": "tteke", 1007 | "name": "Türker Teke", 1008 | "avatar_url": "https://avatars3.githubusercontent.com/u/12457162?v=4", 1009 | "profile": "http://turkerteke.com", 1010 | "contributions": [ 1011 | "doc" 1012 | ] 1013 | }, 1014 | { 1015 | "login": "zbrogz", 1016 | "name": "Zach Brogan", 1017 | "avatar_url": "https://avatars1.githubusercontent.com/u/319162?v=4", 1018 | "profile": "http://linkedin.com/in/zachbrogan", 1019 | "contributions": [ 1020 | "code", 1021 | "test" 1022 | ] 1023 | }, 1024 | { 1025 | "login": "ryota-murakami", 1026 | "name": "Ryota Murakami", 1027 | "avatar_url": "https://avatars2.githubusercontent.com/u/5501268?v=4", 1028 | "profile": "https://ryota-murakami.github.io/", 1029 | "contributions": [ 1030 | "doc" 1031 | ] 1032 | }, 1033 | { 1034 | "login": "hottmanmichael", 1035 | "name": "Michael Hottman", 1036 | "avatar_url": "https://avatars3.githubusercontent.com/u/10534502?v=4", 1037 | "profile": "https://github.com/hottmanmichael", 1038 | "contributions": [ 1039 | "ideas" 1040 | ] 1041 | }, 1042 | { 1043 | "login": "stevenfitzpatrick", 1044 | "name": "Steven Fitzpatrick", 1045 | "avatar_url": "https://avatars0.githubusercontent.com/u/23268855?v=4", 1046 | "profile": "https://github.com/stevenfitzpatrick", 1047 | "contributions": [ 1048 | "bug" 1049 | ] 1050 | }, 1051 | { 1052 | "login": "juangl", 1053 | "name": "Juan Je García", 1054 | "avatar_url": "https://avatars0.githubusercontent.com/u/1887029?v=4", 1055 | "profile": "https://github.com/juangl", 1056 | "contributions": [ 1057 | "doc" 1058 | ] 1059 | }, 1060 | { 1061 | "login": "Ishaan28malik", 1062 | "name": "Championrunner", 1063 | "avatar_url": "https://avatars3.githubusercontent.com/u/27343592?v=4", 1064 | "profile": "https://ghuser.io/Ishaan28malik", 1065 | "contributions": [ 1066 | "doc" 1067 | ] 1068 | }, 1069 | { 1070 | "login": "samtsai", 1071 | "name": "Sam Tsai", 1072 | "avatar_url": "https://avatars0.githubusercontent.com/u/225526?v=4", 1073 | "profile": "https://github.com/samtsai", 1074 | "contributions": [ 1075 | "code", 1076 | "test", 1077 | "doc" 1078 | ] 1079 | }, 1080 | { 1081 | "login": "screendriver", 1082 | "name": "Christian Rackerseder", 1083 | "avatar_url": "https://avatars0.githubusercontent.com/u/149248?v=4", 1084 | "profile": "https://www.echooff.dev", 1085 | "contributions": [ 1086 | "code" 1087 | ] 1088 | }, 1089 | { 1090 | "login": "NiGhTTraX", 1091 | "name": "Andrei Picus", 1092 | "avatar_url": "https://avatars0.githubusercontent.com/u/485061?v=4", 1093 | "profile": "https://github.com/NiGhTTraX", 1094 | "contributions": [ 1095 | "bug", 1096 | "review" 1097 | ] 1098 | }, 1099 | { 1100 | "login": "kettanaito", 1101 | "name": "Artem Zakharchenko", 1102 | "avatar_url": "https://avatars3.githubusercontent.com/u/14984911?v=4", 1103 | "profile": "https://redd.one", 1104 | "contributions": [ 1105 | "doc" 1106 | ] 1107 | }, 1108 | { 1109 | "login": "michael-siek", 1110 | "name": "Michael", 1111 | "avatar_url": "https://avatars0.githubusercontent.com/u/45568605?v=4", 1112 | "profile": "http://michaelsiek.com", 1113 | "contributions": [ 1114 | "doc" 1115 | ] 1116 | }, 1117 | { 1118 | "login": "2dubbing", 1119 | "name": "Braden Lee", 1120 | "avatar_url": "https://avatars2.githubusercontent.com/u/15885679?v=4", 1121 | "profile": "http://2dubbing.tistory.com", 1122 | "contributions": [ 1123 | "doc" 1124 | ] 1125 | }, 1126 | { 1127 | "login": "kamranayub", 1128 | "name": "Kamran Ayub", 1129 | "avatar_url": "https://avatars1.githubusercontent.com/u/563819?v=4", 1130 | "profile": "http://kamranicus.com/", 1131 | "contributions": [ 1132 | "code", 1133 | "test" 1134 | ] 1135 | }, 1136 | { 1137 | "login": "MatanBobi", 1138 | "name": "Matan Borenkraout", 1139 | "avatar_url": "https://avatars2.githubusercontent.com/u/12711091?v=4", 1140 | "profile": "https://twitter.com/matanbobi", 1141 | "contributions": [ 1142 | "code" 1143 | ] 1144 | }, 1145 | { 1146 | "login": "radar", 1147 | "name": "Ryan Bigg", 1148 | "avatar_url": "https://avatars3.githubusercontent.com/u/2687?v=4", 1149 | "profile": "http://ryanbigg.com", 1150 | "contributions": [ 1151 | "maintenance" 1152 | ] 1153 | }, 1154 | { 1155 | "login": "antonhalim", 1156 | "name": "Anton Halim", 1157 | "avatar_url": "https://avatars1.githubusercontent.com/u/10498035?v=4", 1158 | "profile": "https://antonhalim.com", 1159 | "contributions": [ 1160 | "doc" 1161 | ] 1162 | }, 1163 | { 1164 | "login": "artem-malko", 1165 | "name": "Artem Malko", 1166 | "avatar_url": "https://avatars0.githubusercontent.com/u/1823689?v=4", 1167 | "profile": "http://artmalko.ru", 1168 | "contributions": [ 1169 | "code" 1170 | ] 1171 | }, 1172 | { 1173 | "login": "ljosberinn", 1174 | "name": "Gerrit Alex", 1175 | "avatar_url": "https://avatars1.githubusercontent.com/u/29307652?v=4", 1176 | "profile": "http://gerritalex.de", 1177 | "contributions": [ 1178 | "code" 1179 | ] 1180 | }, 1181 | { 1182 | "login": "karthick3018", 1183 | "name": "Karthick Raja", 1184 | "avatar_url": "https://avatars1.githubusercontent.com/u/47154512?v=4", 1185 | "profile": "https://github.com/karthick3018", 1186 | "contributions": [ 1187 | "code" 1188 | ] 1189 | }, 1190 | { 1191 | "login": "theashraf", 1192 | "name": "Abdelrahman Ashraf", 1193 | "avatar_url": "https://avatars1.githubusercontent.com/u/39750790?v=4", 1194 | "profile": "https://github.com/theashraf", 1195 | "contributions": [ 1196 | "code" 1197 | ] 1198 | }, 1199 | { 1200 | "login": "lidoravitan", 1201 | "name": "Lidor Avitan", 1202 | "avatar_url": "https://avatars0.githubusercontent.com/u/35113398?v=4", 1203 | "profile": "https://github.com/lidoravitan", 1204 | "contributions": [ 1205 | "doc" 1206 | ] 1207 | }, 1208 | { 1209 | "login": "ljharb", 1210 | "name": "Jordan Harband", 1211 | "avatar_url": "https://avatars1.githubusercontent.com/u/45469?v=4", 1212 | "profile": "https://github.com/ljharb", 1213 | "contributions": [ 1214 | "review", 1215 | "ideas" 1216 | ] 1217 | }, 1218 | { 1219 | "login": "marcosvega91", 1220 | "name": "Marco Moretti", 1221 | "avatar_url": "https://avatars2.githubusercontent.com/u/5365582?v=4", 1222 | "profile": "https://github.com/marcosvega91", 1223 | "contributions": [ 1224 | "code" 1225 | ] 1226 | }, 1227 | { 1228 | "login": "sanchit121", 1229 | "name": "sanchit121", 1230 | "avatar_url": "https://avatars2.githubusercontent.com/u/30828115?v=4", 1231 | "profile": "https://github.com/sanchit121", 1232 | "contributions": [ 1233 | "bug", 1234 | "code" 1235 | ] 1236 | }, 1237 | { 1238 | "login": "solufa", 1239 | "name": "Solufa", 1240 | "avatar_url": "https://avatars.githubusercontent.com/u/9402912?v=4", 1241 | "profile": "https://github.com/solufa", 1242 | "contributions": [ 1243 | "bug", 1244 | "code" 1245 | ] 1246 | }, 1247 | { 1248 | "login": "AriPerkkio", 1249 | "name": "Ari Perkkiö", 1250 | "avatar_url": "https://avatars.githubusercontent.com/u/14806298?v=4", 1251 | "profile": "https://codepen.io/ariperkkio/", 1252 | "contributions": [ 1253 | "test" 1254 | ] 1255 | }, 1256 | { 1257 | "login": "jhnns", 1258 | "name": "Johannes Ewald", 1259 | "avatar_url": "https://avatars.githubusercontent.com/u/781746?v=4", 1260 | "profile": "https://github.com/jhnns", 1261 | "contributions": [ 1262 | "code" 1263 | ] 1264 | }, 1265 | { 1266 | "login": "anpaopao", 1267 | "name": "Angus J. Pope", 1268 | "avatar_url": "https://avatars.githubusercontent.com/u/44686792?v=4", 1269 | "profile": "https://github.com/anpaopao", 1270 | "contributions": [ 1271 | "doc" 1272 | ] 1273 | }, 1274 | { 1275 | "login": "leschdom", 1276 | "name": "Dominik Lesch", 1277 | "avatar_url": "https://avatars.githubusercontent.com/u/62334278?v=4", 1278 | "profile": "https://github.com/leschdom", 1279 | "contributions": [ 1280 | "doc" 1281 | ] 1282 | }, 1283 | { 1284 | "login": "ImADrafter", 1285 | "name": "Marcos Gómez", 1286 | "avatar_url": "https://avatars.githubusercontent.com/u/44379989?v=4", 1287 | "profile": "https://github.com/ImADrafter", 1288 | "contributions": [ 1289 | "doc" 1290 | ] 1291 | }, 1292 | { 1293 | "login": "akashshyamdev", 1294 | "name": "Akash Shyam", 1295 | "avatar_url": "https://avatars.githubusercontent.com/u/56759828?v=4", 1296 | "profile": "https://www.akashshyam.online/", 1297 | "contributions": [ 1298 | "bug" 1299 | ] 1300 | }, 1301 | { 1302 | "login": "fmeum", 1303 | "name": "Fabian Meumertzheim", 1304 | "avatar_url": "https://avatars.githubusercontent.com/u/4312191?v=4", 1305 | "profile": "https://hen.ne.ke", 1306 | "contributions": [ 1307 | "code", 1308 | "bug" 1309 | ] 1310 | }, 1311 | { 1312 | "login": "Nokel81", 1313 | "name": "Sebastian Malton", 1314 | "avatar_url": "https://avatars.githubusercontent.com/u/8225332?v=4", 1315 | "profile": "https://github.com/Nokel81", 1316 | "contributions": [ 1317 | "bug", 1318 | "code" 1319 | ] 1320 | }, 1321 | { 1322 | "login": "mboettcher", 1323 | "name": "Martin Böttcher", 1324 | "avatar_url": "https://avatars.githubusercontent.com/u/2325337?v=4", 1325 | "profile": "https://github.com/mboettcher", 1326 | "contributions": [ 1327 | "code" 1328 | ] 1329 | }, 1330 | { 1331 | "login": "TkDodo", 1332 | "name": "Dominik Dorfmeister", 1333 | "avatar_url": "https://avatars.githubusercontent.com/u/1021430?v=4", 1334 | "profile": "http://tkdodo.eu", 1335 | "contributions": [ 1336 | "code" 1337 | ] 1338 | }, 1339 | { 1340 | "login": "stephensauceda", 1341 | "name": "Stephen Sauceda", 1342 | "avatar_url": "https://avatars.githubusercontent.com/u/1017723?v=4", 1343 | "profile": "https://stephensauceda.com", 1344 | "contributions": [ 1345 | "doc" 1346 | ] 1347 | }, 1348 | { 1349 | "login": "cmdcolin", 1350 | "name": "Colin Diesh", 1351 | "avatar_url": "https://avatars.githubusercontent.com/u/6511937?v=4", 1352 | "profile": "http://cmdcolin.github.io", 1353 | "contributions": [ 1354 | "doc" 1355 | ] 1356 | }, 1357 | { 1358 | "login": "yinm", 1359 | "name": "Yusuke Iinuma", 1360 | "avatar_url": "https://avatars.githubusercontent.com/u/13295106?v=4", 1361 | "profile": "http://yinm.info", 1362 | "contributions": [ 1363 | "code" 1364 | ] 1365 | }, 1366 | { 1367 | "login": "trappar", 1368 | "name": "Jeff Way", 1369 | "avatar_url": "https://avatars.githubusercontent.com/u/525726?v=4", 1370 | "profile": "https://github.com/trappar", 1371 | "contributions": [ 1372 | "code" 1373 | ] 1374 | }, 1375 | { 1376 | "login": "bernardobelchior", 1377 | "name": "Bernardo Belchior", 1378 | "avatar_url": "https://avatars.githubusercontent.com/u/12778398?v=4", 1379 | "profile": "http://belchior.me", 1380 | "contributions": [ 1381 | "code", 1382 | "doc" 1383 | ] 1384 | } 1385 | ], 1386 | "contributorsPerLine": 7, 1387 | "repoHost": "https://github.com", 1388 | "commitType": "docs", 1389 | "commitConvention": "angular" 1390 | } 1391 | -------------------------------------------------------------------------------- /.bundle.main.env: -------------------------------------------------------------------------------- 1 | BUILD_GLOBALS={"react-dom/test-utils":"ReactTestUtils","react":"React","react-dom":"ReactDOM"} 2 | 3 | -------------------------------------------------------------------------------- /.bundle.pure.env: -------------------------------------------------------------------------------- 1 | BUILD_FILENAME_SUFFIX=.pure 2 | BUILD_INPUT=src/pure.js 3 | 4 | -------------------------------------------------------------------------------- /.codesandbox/ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "installCommand": "install:csb", 3 | "sandboxes": ["new", "github/kentcdodds/react-testing-library-examples"], 4 | "node": "18" 5 | } 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 44 | 45 | - `@testing-library/react` version: 46 | - Testing Framework and version: 47 | 48 | - DOM Environment: 49 | 50 | 51 | 60 | 61 | Relevant code or config 62 | 63 | ```javascript 64 | 65 | ``` 66 | 67 | What you did: 68 | 69 | What happened: 70 | 71 | 72 | 73 | Reproduction repository: 74 | 75 | 83 | 84 | Problem description: 85 | 86 | 87 | 88 | Suggested solution: 89 | 90 | 94 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_Report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug Report 3 | about: Bugs, missing documentation, or unexpected behavior 🤔. 4 | --- 5 | 6 | 24 | 25 | - `@testing-library/react` version: 26 | - Testing Framework and version: 27 | 28 | - DOM Environment: 29 | 30 | 31 | 40 | 41 | ### Relevant code or config: 42 | 43 | ```js 44 | var your => (code) => here; 45 | ``` 46 | 47 | 51 | 52 | ### What you did: 53 | 54 | 55 | 56 | ### What happened: 57 | 58 | 59 | 60 | ### Reproduction: 61 | 62 | 66 | 67 | ### Problem description: 68 | 69 | 70 | 71 | ### Suggested solution: 72 | 73 | 77 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_Request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 💡 Feature Request 3 | about: I have a suggestion (and might want to implement myself 🙂)! 4 | --- 5 | 6 | 27 | 28 | ### Describe the feature you'd like: 29 | 30 | 34 | 35 | ### Suggested implementation: 36 | 37 | 38 | 39 | ### Describe alternatives you've considered: 40 | 41 | 45 | 46 | ### Teachability, Documentation, Adoption, Migration Strategy: 47 | 48 | 52 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: ❓ Support Question 3 | about: 🛑 If you have a question 💬, please check out our support channels! 4 | --- 5 | 6 | -------------- 👆 Click "Preview"! 7 | 8 | Issues on GitHub are intended to be related to problems with the library itself 9 | and feature requests so we recommend not using this medium to ask them here 😁. 10 | 11 | --- 12 | 13 | ## ❓ Support Forums 14 | 15 | For questions related to using the library, please visit a support community 16 | instead of filing an issue on GitHub. You can follow the instructions in this 17 | codesandbox to make a reproduction of your issue: https://kcd.im/rtl-help 18 | 19 | - Discord https://discord.gg/testing-library 20 | - Stack Overflow 21 | https://stackoverflow.com/questions/tagged/react-testing-library 22 | - Documentation: https://github.com/testing-library/testing-library-docs 23 | 24 | **ISSUES WHICH ARE QUESTIONS WILL BE CLOSED** 25 | -------------------------------------------------------------------------------- /.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 to the 38 | [docs site](https://github.com/testing-library/testing-library-docs) 39 | - [ ] Tests 40 | - [ ] TypeScript definitions updated 41 | - [ ] Ready to be merged 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: validate 2 | on: 3 | push: 4 | branches: 5 | # Match SemVer major release branches 6 | # e.g. "12.x" or "8.x" 7 | - '[0-9]+.x' 8 | - 'main' 9 | - 'next' 10 | - 'next-major' 11 | - 'beta' 12 | - 'alpha' 13 | - '!all-contributors/**' 14 | pull_request: 15 | 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.ref }} 18 | cancel-in-progress: true 19 | 20 | permissions: 21 | actions: write # to cancel/stop running workflows (styfle/cancel-workflow-action) 22 | contents: read # to fetch code (actions/checkout) 23 | 24 | jobs: 25 | main: 26 | continue-on-error: ${{ matrix.react != 'latest' }} 27 | # ignore all-contributors PRs 28 | if: ${{ !contains(github.head_ref, 'all-contributors') }} 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | node: [18, 20] 33 | react: ['18.x', latest, canary, experimental] 34 | runs-on: ubuntu-latest 35 | steps: 36 | - name: ⬇️ Checkout repo 37 | uses: actions/checkout@v4 38 | 39 | - name: ⎔ Setup node 40 | uses: actions/setup-node@v4 41 | with: 42 | node-version: ${{ matrix.node }} 43 | 44 | - name: 📥 Download deps 45 | uses: bahmutov/npm-install@v1 46 | with: 47 | useLockFile: false 48 | 49 | # TODO: Can be removed if https://github.com/kentcdodds/kcd-scripts/pull/146 is released 50 | - name: Verify format (`npm run format` committed?) 51 | run: npm run format -- --check --no-write 52 | 53 | # as requested by the React team :) 54 | # https://reactjs.org/blog/2019/10/22/react-release-channels.html#using-the-next-channel-for-integration-testing 55 | - name: ⚛️ Setup react 56 | run: npm install react@${{ matrix.react }} react-dom@${{ matrix.react }} 57 | 58 | - name: ⚛️ Setup react types 59 | if: ${{ matrix.react != 'canary' && matrix.react != 'experimental' }} 60 | run: 61 | npm install @types/react@${{ matrix.react }} @types/react-dom@${{ 62 | matrix.react }} 63 | 64 | - name: ▶️ Run validate script 65 | run: npm run validate 66 | 67 | - name: ⬆️ Upload coverage report 68 | uses: codecov/codecov-action@v5 69 | with: 70 | fail_ci_if_error: true 71 | flags: ${{ matrix.react }} 72 | token: ${{ secrets.CODECOV_TOKEN }} 73 | 74 | release: 75 | permissions: 76 | actions: write # to cancel/stop running workflows (styfle/cancel-workflow-action) 77 | contents: write # to create release tags (cycjimmy/semantic-release-action) 78 | issues: write # to post release that resolves an issue (cycjimmy/semantic-release-action) 79 | 80 | needs: main 81 | runs-on: ubuntu-latest 82 | if: 83 | ${{ github.repository == 'testing-library/react-testing-library' && 84 | github.event_name == 'push' }} 85 | steps: 86 | - name: ⬇️ Checkout repo 87 | uses: actions/checkout@v4 88 | 89 | - name: ⎔ Setup node 90 | uses: actions/setup-node@v4 91 | with: 92 | node-version: 14 93 | 94 | - name: 📥 Download deps 95 | uses: bahmutov/npm-install@v1 96 | with: 97 | useLockFile: false 98 | 99 | - name: 🏗 Run build script 100 | run: npm run build 101 | 102 | - name: 🚀 Release 103 | uses: cycjimmy/semantic-release-action@v2 104 | with: 105 | semantic_version: 17 106 | branches: | 107 | [ 108 | '+([0-9])?(.{+([0-9]),x}).x', 109 | 'main', 110 | 'next', 111 | 'next-major', 112 | {name: 'beta', prerelease: true}, 113 | {name: 'alpha', prerelease: true} 114 | ] 115 | env: 116 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 117 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 118 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | .DS_Store 5 | 6 | # these cause more harm than good 7 | # when working with contributors 8 | package-lock.json 9 | yarn.lock 10 | -------------------------------------------------------------------------------- /.huskyrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('kcd-scripts/husky') 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('kcd-scripts/prettier') 2 | -------------------------------------------------------------------------------- /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 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | me+coc@kentcdodds.com. All complaints will be reviewed and investigated promptly 64 | and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /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_ 6 | series [How to Contribute 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 15 | > requests from branches on your fork. To do this, run: 16 | > 17 | > ``` 18 | > git remote add upstream https://github.com/testing-library/react-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 24 | > fetch the git information from that remote, then set your local `main` branch 25 | > to use the upstream main branch whenever you run `git pull`. Then you can make 26 | > all of your pull request branches based on this `main` branch. Whenever you 27 | > want to update your version of `main`, do a regular `git pull`. 28 | 29 | ## Committing and Pushing changes 30 | 31 | Please make sure to run the tests before you commit your changes. You can run 32 | `npm run test:update` which will update any snapshots that need updating. Make 33 | sure to include those changes (if they exist) in your commit. 34 | 35 | ### Update Typings 36 | 37 | If your PR introduced some changes in the API, you are more than welcome to 38 | modify the TypeScript type definition to reflect those changes. Just modify the 39 | `/types/index.d.ts` file accordingly. If you have never seen TypeScript 40 | definitions before, you can read more about it in its 41 | [documentation pages](https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html). 42 | Though this library itself is not written in TypeScript we use 43 | [dtslint](https://github.com/microsoft/dtslint) to lint our typings. 44 | 45 | ## Help needed 46 | 47 | Please checkout the [the open issues][issues] 48 | 49 | Also, please watch the repo and respond to questions/bug reports/feature 50 | requests! Thanks! 51 | 52 | [egghead]: 53 | https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github 54 | [issues]: https://github.com/testing-library/react-testing-library/issues 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017-Present Kent C. Dodds 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | # basic 6 | target: 100% 7 | threshold: 0% 8 | flags: 9 | - canary 10 | - experimental 11 | - latest 12 | branches: 13 | - main 14 | - 12.x 15 | if_ci_failed: success 16 | if_not_found: failure 17 | informational: false 18 | only_pulls: false 19 | github_checks: 20 | annotations: true 21 | -------------------------------------------------------------------------------- /dont-cleanup-after-each.js: -------------------------------------------------------------------------------- 1 | process.env.RTL_SKIP_AUTO_CLEANUP = true 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const {jest: jestConfig} = require('kcd-scripts/config') 2 | 3 | module.exports = Object.assign(jestConfig, { 4 | coverageThreshold: { 5 | ...jestConfig.coverageThreshold, 6 | // Full coverage across the build matrix (React 18, 19) but not in a single job 7 | // Ful coverage is checked via codecov 8 | './src/act-compat': { 9 | branches: 90, 10 | }, 11 | './src/pure': { 12 | // minimum coverage of jobs using React 18 and 19 13 | branches: 95, 14 | functions: 88, 15 | lines: 92, 16 | statements: 92, 17 | }, 18 | }, 19 | }) 20 | -------------------------------------------------------------------------------- /other/MAINTAINING.md: -------------------------------------------------------------------------------- 1 | # Maintaining 2 | 3 | This is documentation for maintainers of this project. 4 | 5 | ## Code of Conduct 6 | 7 | Please review, understand, and be an example of it. Violations of the code of 8 | conduct are taken seriously, even (especially) for maintainers. 9 | 10 | ## Issues 11 | 12 | We want to support and build the community. We do that best by helping people 13 | learn to solve their own problems. We have an issue template and hopefully most 14 | folks follow it. If it's not clear what the issue is, invite them to create a 15 | minimal reproduction of what they're trying to accomplish or the bug they think 16 | they've found. 17 | 18 | Once it's determined that a code change is necessary, point people to 19 | [makeapullrequest.com](http://makeapullrequest.com) and invite them to make a 20 | pull request. If they're the one who needs the feature, they're the one who can 21 | build it. If they need some hand holding and you have time to lend a hand, 22 | please do so. It's an investment into another human being, and an investment 23 | into a potential maintainer. 24 | 25 | Remember that this is open source, so the code is not yours, it's ours. If 26 | someone needs a change in the codebase, you don't have to make it happen 27 | yourself. Commit as much time to the project as you want/need to. Nobody can ask 28 | any more of you than that. 29 | 30 | ## Pull Requests 31 | 32 | As a maintainer, you're fine to make your branches on the main repo or on your 33 | own fork. Either way is fine. 34 | 35 | When we receive a pull request, a github action is kicked off automatically (see 36 | the `.github/workflows/validate.yml` for what runs in the action). We avoid 37 | merging anything that breaks the validate action. 38 | 39 | Please review PRs and focus on the code rather than the individual. You never 40 | know when this is someone's first ever PR and we want their experience to be as 41 | positive as possible, so be uplifting and constructive. 42 | 43 | When you merge the pull request, 99% of the time you should use the 44 | [Squash and merge](https://help.github.com/articles/merging-a-pull-request/) 45 | feature. This keeps our git history clean, but more importantly, this allows us 46 | to make any necessary changes to the commit message so we release what we want 47 | to release. See the next section on Releases for more about that. 48 | 49 | ## Release 50 | 51 | Our releases are automatic. They happen whenever code lands into `main`. A 52 | github action gets kicked off and if it's successful, a tool called 53 | [`semantic-release`](https://github.com/semantic-release/semantic-release) is 54 | used to automatically publish a new release to npm as well as a changelog to 55 | GitHub. It is only able to determine the version and whether a release is 56 | necessary by the git commit messages. With this in mind, **please brush up on 57 | [the commit message convention][commit] which drives our releases.** 58 | 59 | > One important note about this: Please make sure that commit messages do NOT 60 | > contain the words "BREAKING CHANGE" in them unless we want to push a major 61 | > version. I've been burned by this more than once where someone will include 62 | > "BREAKING CHANGE: None" and it will end up releasing a new major version. Not 63 | > a huge deal honestly, but kind of annoying... 64 | 65 | ## Thanks! 66 | 67 | Thank you so much for helping to maintain this project! 68 | 69 | [commit]: 70 | https://github.com/conventional-changelog-archived-repos/conventional-changelog-angular/blob/ed32559941719a130bb0327f886d6a32a8cbc2ba/convention.md 71 | -------------------------------------------------------------------------------- /other/USERS.md: -------------------------------------------------------------------------------- 1 | # Users 2 | 3 | If you or your company uses this project, add your name to this list! Eventually 4 | we may have a website to showcase these (wanna build it!?) 5 | 6 | > No users have been added yet! 7 | 8 | 13 | -------------------------------------------------------------------------------- /other/cheat-sheet.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testing-library/react-testing-library/9fc6a75d74bb8e03a48d3339efde4dd83cd5328b/other/cheat-sheet.pdf -------------------------------------------------------------------------------- /other/design files/README.txt: -------------------------------------------------------------------------------- 1 | # Cheat sheet design 2 | 3 | The cheat sheet document has been created using the desktop publishing software called Affinity Publisher, the original source file can be found in this folder 4 | 5 | ## Fonts used 6 | 7 | - Menlo 8 | - Arial 9 | 10 | ## Standard distances 11 | 12 | 15pt between boxes 13 | 8pt margin from box edge to inner text -------------------------------------------------------------------------------- /other/design files/cheat-sheet.afpub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testing-library/react-testing-library/9fc6a75d74bb8e03a48d3339efde4dd83cd5328b/other/design files/cheat-sheet.afpub -------------------------------------------------------------------------------- /other/goat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testing-library/react-testing-library/9fc6a75d74bb8e03a48d3339efde4dd83cd5328b/other/goat.png -------------------------------------------------------------------------------- /other/manual-releases.md: -------------------------------------------------------------------------------- 1 | # manual-releases 2 | 3 | This project has an automated release set up. So things are only released when 4 | there are useful changes in the code that justify a release. But sometimes 5 | things get messed up one way or another and we need to trigger the release 6 | ourselves. When this happens, simply bump the number below and commit that with 7 | the following commit message based on your needs: 8 | 9 | **Major** 10 | 11 | ``` 12 | fix(release): manually release a major version 13 | 14 | There was an issue with a major release, so this manual-releases.md 15 | change is to release a new major version. 16 | 17 | Reference: # 18 | 19 | BREAKING CHANGE: 20 | ``` 21 | 22 | **Minor** 23 | 24 | ``` 25 | feat(release): manually release a minor version 26 | 27 | There was an issue with a minor release, so this manual-releases.md 28 | change is to release a new minor version. 29 | 30 | Reference: # 31 | ``` 32 | 33 | **Patch** 34 | 35 | ``` 36 | fix(release): manually release a patch version 37 | 38 | There was an issue with a patch release, so this manual-releases.md 39 | change is to release a new patch version. 40 | 41 | Reference: # 42 | ``` 43 | 44 | The number of times we've had to do a manual release is: 5 45 | -------------------------------------------------------------------------------- /other/testingjavascript.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testing-library/react-testing-library/9fc6a75d74bb8e03a48d3339efde4dd83cd5328b/other/testingjavascript.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@testing-library/react", 3 | "version": "0.0.0-semantically-released", 4 | "description": "Simple and complete React DOM testing utilities that encourage good testing practices.", 5 | "main": "dist/index.js", 6 | "types": "types/index.d.ts", 7 | "module": "dist/@testing-library/react.esm.js", 8 | "engines": { 9 | "node": ">=18" 10 | }, 11 | "scripts": { 12 | "prebuild": "rimraf dist", 13 | "build": "npm-run-all --parallel build:main build:bundle:main build:bundle:pure", 14 | "build:bundle:main": "dotenv -e .bundle.main.env kcd-scripts build -- --bundle --no-clean", 15 | "build:bundle:pure": "dotenv -e .bundle.main.env -e .bundle.pure.env kcd-scripts build -- --bundle --no-clean", 16 | "build:main": "kcd-scripts build --no-clean", 17 | "format": "kcd-scripts format", 18 | "install:csb": "npm install", 19 | "lint": "kcd-scripts lint", 20 | "setup": "npm install && npm run validate -s", 21 | "test": "kcd-scripts test", 22 | "test:update": "npm test -- --updateSnapshot --coverage", 23 | "typecheck": "kcd-scripts typecheck --build types", 24 | "validate": "kcd-scripts validate" 25 | }, 26 | "files": [ 27 | "dist", 28 | "dont-cleanup-after-each.js", 29 | "pure.js", 30 | "pure.d.ts", 31 | "types/*.d.ts" 32 | ], 33 | "keywords": [ 34 | "testing", 35 | "react", 36 | "ui", 37 | "dom", 38 | "jsdom", 39 | "unit", 40 | "integration", 41 | "functional", 42 | "end-to-end", 43 | "e2e" 44 | ], 45 | "author": "Kent C. Dodds (https://kentcdodds.com)", 46 | "license": "MIT", 47 | "dependencies": { 48 | "@babel/runtime": "^7.12.5" 49 | }, 50 | "devDependencies": { 51 | "@testing-library/dom": "^10.0.0", 52 | "@testing-library/jest-dom": "^5.11.6", 53 | "@types/react": "^19.0.0", 54 | "@types/react-dom": "^19.0.0", 55 | "chalk": "^4.1.2", 56 | "dotenv-cli": "^4.0.0", 57 | "jest-diff": "^29.7.0", 58 | "kcd-scripts": "^13.0.0", 59 | "npm-run-all": "^4.1.5", 60 | "react": "^19.0.0", 61 | "react-dom": "^19.0.0", 62 | "rimraf": "^3.0.2", 63 | "typescript": "^4.1.2" 64 | }, 65 | "peerDependencies": { 66 | "@testing-library/dom": "^10.0.0", 67 | "@types/react": "^18.0.0 || ^19.0.0", 68 | "@types/react-dom": "^18.0.0 || ^19.0.0", 69 | "react": "^18.0.0 || ^19.0.0", 70 | "react-dom": "^18.0.0 || ^19.0.0" 71 | }, 72 | "peerDependenciesMeta": { 73 | "@types/react": { 74 | "optional": true 75 | }, 76 | "@types/react-dom": { 77 | "optional": true 78 | } 79 | }, 80 | "eslintConfig": { 81 | "extends": "./node_modules/kcd-scripts/eslint.js", 82 | "parserOptions": { 83 | "ecmaVersion": 2022 84 | }, 85 | "globals": { 86 | "globalThis": "readonly" 87 | }, 88 | "rules": { 89 | "react/prop-types": "off", 90 | "react/no-adjacent-inline-elements": "off", 91 | "import/no-unassigned-import": "off", 92 | "import/named": "off", 93 | "testing-library/no-container": "off", 94 | "testing-library/no-debugging-utils": "off", 95 | "testing-library/no-dom-import": "off", 96 | "testing-library/no-unnecessary-act": "off", 97 | "testing-library/prefer-explicit-assert": "off", 98 | "testing-library/prefer-find-by": "off", 99 | "testing-library/prefer-user-event": "off" 100 | } 101 | }, 102 | "eslintIgnore": [ 103 | "node_modules", 104 | "coverage", 105 | "dist", 106 | "*.d.ts" 107 | ], 108 | "repository": { 109 | "type": "git", 110 | "url": "https://github.com/testing-library/react-testing-library" 111 | }, 112 | "bugs": { 113 | "url": "https://github.com/testing-library/react-testing-library/issues" 114 | }, 115 | "homepage": "https://github.com/testing-library/react-testing-library#readme" 116 | } 117 | -------------------------------------------------------------------------------- /pure.d.ts: -------------------------------------------------------------------------------- 1 | export * from './types/pure' 2 | -------------------------------------------------------------------------------- /pure.js: -------------------------------------------------------------------------------- 1 | // makes it so people can import from '@testing-library/react/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[`render API supports fragments 1`] = ` 4 | 5 |
6 | 7 | DocumentFragment 8 | 9 | is pretty cool! 10 |
11 |
12 | `; 13 | -------------------------------------------------------------------------------- /src/__tests__/act.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {act, render, fireEvent, screen} from '../' 3 | 4 | test('render calls useEffect immediately', () => { 5 | const effectCb = jest.fn() 6 | function MyUselessComponent() { 7 | React.useEffect(effectCb) 8 | return null 9 | } 10 | render() 11 | expect(effectCb).toHaveBeenCalledTimes(1) 12 | }) 13 | 14 | test('findByTestId returns the element', async () => { 15 | const ref = React.createRef() 16 | render(
) 17 | expect(await screen.findByTestId('foo')).toBe(ref.current) 18 | }) 19 | 20 | test('fireEvent triggers useEffect calls', () => { 21 | const effectCb = jest.fn() 22 | function Counter() { 23 | React.useEffect(effectCb) 24 | const [count, setCount] = React.useState(0) 25 | return 26 | } 27 | const { 28 | container: {firstChild: buttonNode}, 29 | } = render() 30 | 31 | effectCb.mockClear() 32 | fireEvent.click(buttonNode) 33 | expect(buttonNode).toHaveTextContent('1') 34 | expect(effectCb).toHaveBeenCalledTimes(1) 35 | }) 36 | 37 | test('calls to hydrate will run useEffects', () => { 38 | const effectCb = jest.fn() 39 | function MyUselessComponent() { 40 | React.useEffect(effectCb) 41 | return null 42 | } 43 | render(, {hydrate: true}) 44 | expect(effectCb).toHaveBeenCalledTimes(1) 45 | }) 46 | 47 | test('cleans up IS_REACT_ACT_ENVIRONMENT if its callback throws', () => { 48 | global.IS_REACT_ACT_ENVIRONMENT = false 49 | 50 | expect(() => 51 | act(() => { 52 | throw new Error('threw') 53 | }), 54 | ).toThrow('threw') 55 | 56 | expect(global.IS_REACT_ACT_ENVIRONMENT).toEqual(false) 57 | }) 58 | 59 | test('cleans up IS_REACT_ACT_ENVIRONMENT if its async callback throws', async () => { 60 | global.IS_REACT_ACT_ENVIRONMENT = false 61 | 62 | await expect(() => 63 | act(async () => { 64 | throw new Error('thenable threw') 65 | }), 66 | ).rejects.toThrow('thenable threw') 67 | 68 | expect(global.IS_REACT_ACT_ENVIRONMENT).toEqual(false) 69 | }) 70 | -------------------------------------------------------------------------------- /src/__tests__/auto-cleanup-skip.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | let render 4 | beforeAll(() => { 5 | process.env.RTL_SKIP_AUTO_CLEANUP = 'true' 6 | const rtl = require('../') 7 | render = rtl.render 8 | }) 9 | 10 | // This one verifies that if RTL_SKIP_AUTO_CLEANUP is set 11 | // then we DON'T auto-wire up the afterEach for folks 12 | test('first', () => { 13 | render(
hi
) 14 | }) 15 | 16 | test('second', () => { 17 | expect(document.body.innerHTML).toEqual('
hi
') 18 | }) 19 | -------------------------------------------------------------------------------- /src/__tests__/auto-cleanup.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {render} from '../' 3 | 4 | // This just verifies that by importing RTL 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).toBeEmptyDOMElement() 13 | }) 14 | -------------------------------------------------------------------------------- /src/__tests__/cleanup.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {render, cleanup} from '../' 3 | 4 | test('cleans up the document', () => { 5 | const spy = jest.fn() 6 | const divId = 'my-div' 7 | 8 | class Test extends React.Component { 9 | componentWillUnmount() { 10 | expect(document.getElementById(divId)).toBeInTheDocument() 11 | spy() 12 | } 13 | 14 | render() { 15 | return
16 | } 17 | } 18 | 19 | render() 20 | cleanup() 21 | expect(document.body).toBeEmptyDOMElement() 22 | expect(spy).toHaveBeenCalledTimes(1) 23 | }) 24 | 25 | test('cleanup does not error when an element is not a child', () => { 26 | render(
, {container: document.createElement('div')}) 27 | cleanup() 28 | }) 29 | 30 | test('cleanup runs effect cleanup functions', () => { 31 | const spy = jest.fn() 32 | 33 | const Test = () => { 34 | React.useEffect(() => spy) 35 | 36 | return null 37 | } 38 | 39 | render() 40 | cleanup() 41 | expect(spy).toHaveBeenCalledTimes(1) 42 | }) 43 | 44 | describe('fake timers and missing act warnings', () => { 45 | beforeEach(() => { 46 | jest.resetAllMocks() 47 | jest.spyOn(console, 'error').mockImplementation(() => { 48 | // assert messages explicitly 49 | }) 50 | jest.useFakeTimers() 51 | }) 52 | 53 | afterEach(() => { 54 | jest.restoreAllMocks() 55 | jest.useRealTimers() 56 | }) 57 | 58 | test('cleanup does not flush microtasks', () => { 59 | const microTaskSpy = jest.fn() 60 | function Test() { 61 | const counter = 1 62 | const [, setDeferredCounter] = React.useState(null) 63 | React.useEffect(() => { 64 | let cancelled = false 65 | Promise.resolve().then(() => { 66 | microTaskSpy() 67 | // eslint-disable-next-line jest/no-if, jest/no-conditional-in-test -- false positive 68 | if (!cancelled) { 69 | setDeferredCounter(counter) 70 | } 71 | }) 72 | 73 | return () => { 74 | cancelled = true 75 | } 76 | }, [counter]) 77 | 78 | return null 79 | } 80 | render() 81 | 82 | cleanup() 83 | 84 | expect(microTaskSpy).toHaveBeenCalledTimes(0) 85 | // console.error is mocked 86 | // eslint-disable-next-line no-console 87 | expect(console.error).toHaveBeenCalledTimes(0) 88 | }) 89 | 90 | test('cleanup does not swallow missing act warnings', () => { 91 | const deferredStateUpdateSpy = jest.fn() 92 | function Test() { 93 | const counter = 1 94 | const [, setDeferredCounter] = React.useState(null) 95 | React.useEffect(() => { 96 | let cancelled = false 97 | setTimeout(() => { 98 | deferredStateUpdateSpy() 99 | // eslint-disable-next-line jest/no-conditional-in-test -- false-positive 100 | if (!cancelled) { 101 | setDeferredCounter(counter) 102 | } 103 | }, 0) 104 | 105 | return () => { 106 | cancelled = true 107 | } 108 | }, [counter]) 109 | 110 | return null 111 | } 112 | render() 113 | 114 | jest.runAllTimers() 115 | cleanup() 116 | 117 | expect(deferredStateUpdateSpy).toHaveBeenCalledTimes(1) 118 | // console.error is mocked 119 | // eslint-disable-next-line no-console 120 | expect(console.error).toHaveBeenCalledTimes(1) 121 | // eslint-disable-next-line no-console 122 | expect(console.error.mock.calls[0][0]).toMatch( 123 | 'a test was not wrapped in act(...)', 124 | ) 125 | }) 126 | }) 127 | -------------------------------------------------------------------------------- /src/__tests__/config.js: -------------------------------------------------------------------------------- 1 | import {configure, getConfig} from '../' 2 | 3 | describe('configuration API', () => { 4 | let originalConfig 5 | beforeEach(() => { 6 | // Grab the existing configuration so we can restore 7 | // it at the end of the test 8 | configure(existingConfig => { 9 | originalConfig = existingConfig 10 | // Don't change the existing config 11 | return {} 12 | }) 13 | }) 14 | 15 | afterEach(() => { 16 | configure(originalConfig) 17 | }) 18 | 19 | describe('DTL options', () => { 20 | test('configure can set by a plain JS object', () => { 21 | const testIdAttribute = 'not-data-testid' 22 | configure({testIdAttribute}) 23 | 24 | expect(getConfig().testIdAttribute).toBe(testIdAttribute) 25 | }) 26 | 27 | test('configure can set by a function', () => { 28 | // setup base option 29 | const baseTestIdAttribute = 'data-testid' 30 | configure({testIdAttribute: baseTestIdAttribute}) 31 | 32 | const modifiedPrefix = 'modified-' 33 | configure(existingConfig => ({ 34 | testIdAttribute: `${modifiedPrefix}${existingConfig.testIdAttribute}`, 35 | })) 36 | 37 | expect(getConfig().testIdAttribute).toBe( 38 | `${modifiedPrefix}${baseTestIdAttribute}`, 39 | ) 40 | }) 41 | }) 42 | 43 | describe('RTL options', () => { 44 | test('configure can set by a plain JS object', () => { 45 | configure({reactStrictMode: true}) 46 | 47 | expect(getConfig().reactStrictMode).toBe(true) 48 | }) 49 | 50 | test('configure can set by a function', () => { 51 | configure(existingConfig => ({ 52 | reactStrictMode: !existingConfig.reactStrictMode, 53 | })) 54 | 55 | expect(getConfig().reactStrictMode).toBe(true) 56 | }) 57 | }) 58 | 59 | test('configure can set DTL and RTL options at once', () => { 60 | const testIdAttribute = 'not-data-testid' 61 | configure({testIdAttribute, reactStrictMode: true}) 62 | 63 | expect(getConfig().testIdAttribute).toBe(testIdAttribute) 64 | expect(getConfig().reactStrictMode).toBe(true) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /src/__tests__/debug.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {render, screen} from '../' 3 | 4 | beforeEach(() => { 5 | jest.spyOn(console, 'log').mockImplementation(() => {}) 6 | }) 7 | 8 | afterEach(() => { 9 | console.log.mockRestore() 10 | }) 11 | 12 | test('debug pretty prints the container', () => { 13 | const HelloWorld = () =>

Hello World

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

Hello World

26 |

Hello World

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

Hello World

41 | const {debug, container} = render() 42 | debug(container, 6, {highlight: false}) 43 | expect(console.log).toHaveBeenCalledTimes(1) 44 | expect(console.log.mock.calls[0]).toMatchInlineSnapshot(` 45 | [ 46 |
47 | ..., 48 | ] 49 | `) 50 | }) 51 | 52 | /* 53 | eslint 54 | no-console: "off", 55 | */ 56 | -------------------------------------------------------------------------------- /src/__tests__/end-to-end.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {render, waitForElementToBeRemoved, screen, waitFor} from '../' 3 | 4 | describe.each([ 5 | ['real timers', () => jest.useRealTimers()], 6 | ['fake legacy timers', () => jest.useFakeTimers('legacy')], 7 | ['fake modern timers', () => jest.useFakeTimers('modern')], 8 | ])( 9 | 'it waits for the data to be loaded in a macrotask using %s', 10 | (label, useTimers) => { 11 | beforeEach(() => { 12 | useTimers() 13 | }) 14 | 15 | afterEach(() => { 16 | jest.useRealTimers() 17 | }) 18 | 19 | const fetchAMessageInAMacrotask = () => 20 | new Promise(resolve => { 21 | // we are using random timeout here to simulate a real-time example 22 | // of an async operation calling a callback at a non-deterministic time 23 | const randomTimeout = Math.floor(Math.random() * 100) 24 | setTimeout(() => { 25 | resolve({returnedMessage: 'Hello World'}) 26 | }, randomTimeout) 27 | }) 28 | 29 | function ComponentWithMacrotaskLoader() { 30 | const [state, setState] = React.useState({data: undefined, loading: true}) 31 | React.useEffect(() => { 32 | let cancelled = false 33 | fetchAMessageInAMacrotask().then(data => { 34 | if (!cancelled) { 35 | setState({data, loading: false}) 36 | } 37 | }) 38 | 39 | return () => { 40 | cancelled = true 41 | } 42 | }, []) 43 | 44 | if (state.loading) { 45 | return
Loading...
46 | } 47 | 48 | return ( 49 |
50 | Loaded this message: {state.data.returnedMessage}! 51 |
52 | ) 53 | } 54 | 55 | test('waitForElementToBeRemoved', async () => { 56 | render() 57 | const loading = () => screen.getByText('Loading...') 58 | await waitForElementToBeRemoved(loading) 59 | expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/) 60 | }) 61 | 62 | test('waitFor', async () => { 63 | render() 64 | await waitFor(() => screen.getByText(/Loading../)) 65 | await waitFor(() => screen.getByText(/Loaded this message:/)) 66 | expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/) 67 | }) 68 | 69 | test('findBy', async () => { 70 | render() 71 | await expect(screen.findByTestId('message')).resolves.toHaveTextContent( 72 | /Hello World/, 73 | ) 74 | }) 75 | }, 76 | ) 77 | 78 | describe.each([ 79 | ['real timers', () => jest.useRealTimers()], 80 | ['fake legacy timers', () => jest.useFakeTimers('legacy')], 81 | ['fake modern timers', () => jest.useFakeTimers('modern')], 82 | ])( 83 | 'it waits for the data to be loaded in many microtask using %s', 84 | (label, useTimers) => { 85 | beforeEach(() => { 86 | useTimers() 87 | }) 88 | 89 | afterEach(() => { 90 | jest.useRealTimers() 91 | }) 92 | 93 | const fetchAMessageInAMicrotask = () => 94 | Promise.resolve({ 95 | status: 200, 96 | json: () => Promise.resolve({title: 'Hello World'}), 97 | }) 98 | 99 | function ComponentWithMicrotaskLoader() { 100 | const [fetchState, setFetchState] = React.useState({fetching: true}) 101 | 102 | React.useEffect(() => { 103 | if (fetchState.fetching) { 104 | fetchAMessageInAMicrotask().then(res => { 105 | return ( 106 | res 107 | .json() 108 | // By spec, the runtime can only yield back to the event loop once 109 | // the microtask queue is empty. 110 | // So we ensure that we actually wait for that as well before yielding back from `waitFor`. 111 | .then(data => data) 112 | .then(data => data) 113 | .then(data => data) 114 | .then(data => data) 115 | .then(data => data) 116 | .then(data => data) 117 | .then(data => data) 118 | .then(data => data) 119 | .then(data => data) 120 | .then(data => data) 121 | .then(data => data) 122 | .then(data => { 123 | setFetchState({todo: data.title, fetching: false}) 124 | }) 125 | ) 126 | }) 127 | } 128 | }, [fetchState]) 129 | 130 | if (fetchState.fetching) { 131 | return

Loading..

132 | } 133 | 134 | return ( 135 |
Loaded this message: {fetchState.todo}
136 | ) 137 | } 138 | 139 | test('waitForElementToBeRemoved', async () => { 140 | render() 141 | const loading = () => screen.getByText('Loading..') 142 | await waitForElementToBeRemoved(loading) 143 | expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/) 144 | }) 145 | 146 | test('waitFor', async () => { 147 | render() 148 | await waitFor(() => { 149 | screen.getByText('Loading..') 150 | }) 151 | await waitFor(() => { 152 | screen.getByText(/Loaded this message:/) 153 | }) 154 | expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/) 155 | }) 156 | 157 | test('findBy', async () => { 158 | render() 159 | await expect(screen.findByTestId('message')).resolves.toHaveTextContent( 160 | /Hello World/, 161 | ) 162 | }) 163 | }, 164 | ) 165 | 166 | describe.each([ 167 | ['real timers', () => jest.useRealTimers()], 168 | ['fake legacy timers', () => jest.useFakeTimers('legacy')], 169 | ['fake modern timers', () => jest.useFakeTimers('modern')], 170 | ])( 171 | 'it waits for the data to be loaded in a microtask using %s', 172 | (label, useTimers) => { 173 | beforeEach(() => { 174 | useTimers() 175 | }) 176 | 177 | afterEach(() => { 178 | jest.useRealTimers() 179 | }) 180 | 181 | const fetchAMessageInAMicrotask = () => 182 | Promise.resolve({ 183 | status: 200, 184 | json: () => Promise.resolve({title: 'Hello World'}), 185 | }) 186 | 187 | function ComponentWithMicrotaskLoader() { 188 | const [fetchState, setFetchState] = React.useState({fetching: true}) 189 | 190 | React.useEffect(() => { 191 | if (fetchState.fetching) { 192 | fetchAMessageInAMicrotask().then(res => { 193 | return res.json().then(data => { 194 | setFetchState({todo: data.title, fetching: false}) 195 | }) 196 | }) 197 | } 198 | }, [fetchState]) 199 | 200 | if (fetchState.fetching) { 201 | return

Loading..

202 | } 203 | 204 | return ( 205 |
Loaded this message: {fetchState.todo}
206 | ) 207 | } 208 | 209 | test('waitForElementToBeRemoved', async () => { 210 | render() 211 | const loading = () => screen.getByText('Loading..') 212 | await waitForElementToBeRemoved(loading) 213 | expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/) 214 | }) 215 | 216 | test('waitFor', async () => { 217 | render() 218 | await waitFor(() => { 219 | screen.getByText('Loading..') 220 | }) 221 | await waitFor(() => { 222 | screen.getByText(/Loaded this message:/) 223 | }) 224 | expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/) 225 | }) 226 | 227 | test('findBy', async () => { 228 | render() 229 | await expect(screen.findByTestId('message')).resolves.toHaveTextContent( 230 | /Hello World/, 231 | ) 232 | }) 233 | }, 234 | ) 235 | -------------------------------------------------------------------------------- /src/__tests__/error-handlers.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jest/no-if */ 2 | /* eslint-disable jest/no-conditional-in-test */ 3 | /* eslint-disable jest/no-conditional-expect */ 4 | import * as React from 'react' 5 | import {render, renderHook} from '../' 6 | 7 | const isReact19 = React.version.startsWith('19.') 8 | 9 | const testGateReact19 = isReact19 ? test : test.skip 10 | 11 | test('render errors', () => { 12 | function Thrower() { 13 | throw new Error('Boom!') 14 | } 15 | 16 | if (isReact19) { 17 | expect(() => { 18 | render() 19 | }).toThrow('Boom!') 20 | } else { 21 | expect(() => { 22 | expect(() => { 23 | render() 24 | }).toThrow('Boom!') 25 | }).toErrorDev([ 26 | 'Error: Uncaught [Error: Boom!]', 27 | // React retries on error 28 | 'Error: Uncaught [Error: Boom!]', 29 | ]) 30 | } 31 | }) 32 | 33 | test('onUncaughtError is not supported in render', () => { 34 | function Thrower() { 35 | throw new Error('Boom!') 36 | } 37 | const onUncaughtError = jest.fn(() => {}) 38 | 39 | expect(() => { 40 | render(, { 41 | onUncaughtError(error, errorInfo) { 42 | console.log({error, errorInfo}) 43 | }, 44 | }) 45 | }).toThrow( 46 | 'onUncaughtError is not supported. The `render` call will already throw on uncaught errors.', 47 | ) 48 | 49 | expect(onUncaughtError).toHaveBeenCalledTimes(0) 50 | }) 51 | 52 | testGateReact19('onCaughtError is supported in render', () => { 53 | const thrownError = new Error('Boom!') 54 | const handleComponentDidCatch = jest.fn() 55 | const onCaughtError = jest.fn() 56 | class ErrorBoundary extends React.Component { 57 | state = {error: null} 58 | static getDerivedStateFromError(error) { 59 | return {error} 60 | } 61 | componentDidCatch(error, errorInfo) { 62 | handleComponentDidCatch(error, errorInfo) 63 | } 64 | render() { 65 | if (this.state.error) { 66 | return null 67 | } 68 | return this.props.children 69 | } 70 | } 71 | function Thrower() { 72 | throw thrownError 73 | } 74 | 75 | render( 76 | 77 | 78 | , 79 | { 80 | onCaughtError, 81 | }, 82 | ) 83 | 84 | expect(onCaughtError).toHaveBeenCalledWith(thrownError, { 85 | componentStack: expect.any(String), 86 | errorBoundary: expect.any(Object), 87 | }) 88 | }) 89 | 90 | test('onRecoverableError is supported in render', () => { 91 | const onRecoverableError = jest.fn() 92 | 93 | const container = document.createElement('div') 94 | container.innerHTML = '
server
' 95 | // We just hope we forwarded the callback correctly (which is guaranteed since we just pass it along) 96 | // Frankly, I'm too lazy to assert on React 18 hydration errors since they're a mess. 97 | // eslint-disable-next-line jest/no-conditional-in-test 98 | if (isReact19) { 99 | render(
client
, { 100 | container, 101 | hydrate: true, 102 | onRecoverableError, 103 | }) 104 | expect(onRecoverableError).toHaveBeenCalledTimes(1) 105 | } else { 106 | expect(() => { 107 | render(
client
, { 108 | container, 109 | hydrate: true, 110 | onRecoverableError, 111 | }) 112 | }).toErrorDev(['', ''], {withoutStack: 1}) 113 | expect(onRecoverableError).toHaveBeenCalledTimes(2) 114 | } 115 | }) 116 | 117 | test('onUncaughtError is not supported in renderHook', () => { 118 | function useThrower() { 119 | throw new Error('Boom!') 120 | } 121 | const onUncaughtError = jest.fn(() => {}) 122 | 123 | expect(() => { 124 | renderHook(useThrower, { 125 | onUncaughtError(error, errorInfo) { 126 | console.log({error, errorInfo}) 127 | }, 128 | }) 129 | }).toThrow( 130 | 'onUncaughtError is not supported. The `render` call will already throw on uncaught errors.', 131 | ) 132 | 133 | expect(onUncaughtError).toHaveBeenCalledTimes(0) 134 | }) 135 | 136 | testGateReact19('onCaughtError is supported in renderHook', () => { 137 | const thrownError = new Error('Boom!') 138 | const handleComponentDidCatch = jest.fn() 139 | const onCaughtError = jest.fn() 140 | class ErrorBoundary extends React.Component { 141 | state = {error: null} 142 | static getDerivedStateFromError(error) { 143 | return {error} 144 | } 145 | componentDidCatch(error, errorInfo) { 146 | handleComponentDidCatch(error, errorInfo) 147 | } 148 | render() { 149 | if (this.state.error) { 150 | return null 151 | } 152 | return this.props.children 153 | } 154 | } 155 | function useThrower() { 156 | throw thrownError 157 | } 158 | 159 | renderHook(useThrower, { 160 | onCaughtError, 161 | wrapper: ErrorBoundary, 162 | }) 163 | 164 | expect(onCaughtError).toHaveBeenCalledWith(thrownError, { 165 | componentStack: expect.any(String), 166 | errorBoundary: expect.any(Object), 167 | }) 168 | }) 169 | 170 | // Currently, there's no recoverable error without hydration. 171 | // The option is still supported though. 172 | test('onRecoverableError is supported in renderHook', () => { 173 | const onRecoverableError = jest.fn() 174 | 175 | renderHook( 176 | () => { 177 | // TODO: trigger recoverable error 178 | }, 179 | { 180 | onRecoverableError, 181 | }, 182 | ) 183 | }) 184 | -------------------------------------------------------------------------------- /src/__tests__/events.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {render, fireEvent} 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'], 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 | 'doubleClick', 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: 'Pointer', 67 | events: [ 68 | 'pointerOver', 69 | 'pointerEnter', 70 | 'pointerDown', 71 | 'pointerMove', 72 | 'pointerUp', 73 | 'pointerCancel', 74 | 'pointerOut', 75 | 'pointerLeave', 76 | 'gotPointerCapture', 77 | 'lostPointerCapture', 78 | ], 79 | elementType: 'button', 80 | }, 81 | { 82 | type: 'Selection', 83 | events: ['select'], 84 | elementType: 'input', 85 | }, 86 | { 87 | type: 'Touch', 88 | events: ['touchCancel', 'touchEnd', 'touchMove', 'touchStart'], 89 | elementType: 'button', 90 | }, 91 | { 92 | type: 'UI', 93 | events: ['scroll'], 94 | elementType: 'div', 95 | }, 96 | { 97 | type: 'Wheel', 98 | events: ['wheel'], 99 | elementType: 'div', 100 | }, 101 | { 102 | type: 'Media', 103 | events: [ 104 | 'abort', 105 | 'canPlay', 106 | 'canPlayThrough', 107 | 'durationChange', 108 | 'emptied', 109 | 'encrypted', 110 | 'ended', 111 | 'error', 112 | 'loadedData', 113 | 'loadedMetadata', 114 | 'loadStart', 115 | 'pause', 116 | 'play', 117 | 'playing', 118 | 'progress', 119 | 'rateChange', 120 | 'seeked', 121 | 'seeking', 122 | 'stalled', 123 | 'suspend', 124 | 'timeUpdate', 125 | 'volumeChange', 126 | 'waiting', 127 | ], 128 | elementType: 'video', 129 | }, 130 | { 131 | type: 'Image', 132 | events: ['load', 'error'], 133 | elementType: 'img', 134 | }, 135 | { 136 | type: 'Animation', 137 | events: ['animationStart', 'animationEnd', 'animationIteration'], 138 | elementType: 'div', 139 | }, 140 | { 141 | type: 'Transition', 142 | events: ['transitionEnd'], 143 | elementType: 'div', 144 | }, 145 | ] 146 | 147 | eventTypes.forEach(({type, events, elementType, init}) => { 148 | describe(`${type} Events`, () => { 149 | events.forEach(eventName => { 150 | const propName = `on${eventName.charAt(0).toUpperCase()}${eventName.slice( 151 | 1, 152 | )}` 153 | 154 | it(`triggers ${propName}`, () => { 155 | const ref = React.createRef() 156 | const spy = jest.fn() 157 | 158 | render( 159 | React.createElement(elementType, { 160 | [propName]: spy, 161 | ref, 162 | }), 163 | ) 164 | 165 | fireEvent[eventName](ref.current, init) 166 | expect(spy).toHaveBeenCalledTimes(1) 167 | }) 168 | }) 169 | }) 170 | }) 171 | 172 | eventTypes.forEach(({type, events, elementType, init}) => { 173 | describe(`Native ${type} Events`, () => { 174 | events.forEach(eventName => { 175 | let nativeEventName = eventName.toLowerCase() 176 | 177 | // The doubleClick synthetic event maps to the dblclick native event 178 | if (nativeEventName === 'doubleclick') { 179 | nativeEventName = 'dblclick' 180 | } 181 | 182 | it(`triggers native ${nativeEventName}`, () => { 183 | const ref = React.createRef() 184 | const spy = jest.fn() 185 | const Element = elementType 186 | 187 | const NativeEventElement = () => { 188 | React.useEffect(() => { 189 | const element = ref.current 190 | element.addEventListener(nativeEventName, spy) 191 | return () => { 192 | element.removeEventListener(nativeEventName, spy) 193 | } 194 | }) 195 | return 196 | } 197 | 198 | render() 199 | 200 | fireEvent[eventName](ref.current, init) 201 | expect(spy).toHaveBeenCalledTimes(1) 202 | }) 203 | }) 204 | }) 205 | }) 206 | 207 | test('onChange works', () => { 208 | const handleChange = jest.fn() 209 | const { 210 | container: {firstChild: input}, 211 | } = render() 212 | fireEvent.change(input, {target: {value: 'a'}}) 213 | expect(handleChange).toHaveBeenCalledTimes(1) 214 | }) 215 | 216 | test('calling `fireEvent` directly works too', () => { 217 | const handleEvent = jest.fn() 218 | const { 219 | container: {firstChild: button}, 220 | } = render(
, 240 | ) 241 | const button = container.firstChild.firstChild 242 | 243 | fireEvent.focus(button) 244 | 245 | expect(handleBlur).toHaveBeenCalledTimes(0) 246 | expect(handleBubbledBlur).toHaveBeenCalledTimes(0) 247 | expect(handleFocus).toHaveBeenCalledTimes(1) 248 | expect(handleBubbledFocus).toHaveBeenCalledTimes(1) 249 | 250 | fireEvent.blur(button) 251 | 252 | expect(handleBlur).toHaveBeenCalledTimes(1) 253 | expect(handleBubbledBlur).toHaveBeenCalledTimes(1) 254 | expect(handleFocus).toHaveBeenCalledTimes(1) 255 | expect(handleBubbledFocus).toHaveBeenCalledTimes(1) 256 | }) 257 | -------------------------------------------------------------------------------- /src/__tests__/multi-base.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {render} from '../' 3 | 4 | // these are created once per test suite and reused for each case 5 | let treeA, treeB 6 | beforeAll(() => { 7 | treeA = document.createElement('div') 8 | treeB = document.createElement('div') 9 | document.body.appendChild(treeA) 10 | document.body.appendChild(treeB) 11 | }) 12 | 13 | afterAll(() => { 14 | treeA.parentNode.removeChild(treeA) 15 | treeB.parentNode.removeChild(treeB) 16 | }) 17 | 18 | test('baseElement isolates trees from one another', () => { 19 | const {getByText: getByTextInA} = render(
Jekyll
, { 20 | baseElement: treeA, 21 | }) 22 | const {getByText: getByTextInB} = render(
Hyde
, { 23 | baseElement: treeB, 24 | }) 25 | 26 | expect(() => getByTextInA('Jekyll')).not.toThrow( 27 | 'Unable to find an element with the text: Jekyll.', 28 | ) 29 | expect(() => getByTextInB('Jekyll')).toThrow( 30 | 'Unable to find an element with the text: Jekyll.', 31 | ) 32 | 33 | expect(() => getByTextInA('Hyde')).toThrow( 34 | 'Unable to find an element with the text: Hyde.', 35 | ) 36 | expect(() => getByTextInB('Hyde')).not.toThrow( 37 | 'Unable to find an element with the text: Hyde.', 38 | ) 39 | }) 40 | 41 | // https://github.com/testing-library/eslint-plugin-testing-library/issues/188 42 | /* 43 | eslint 44 | testing-library/prefer-screen-queries: "off", 45 | */ 46 | -------------------------------------------------------------------------------- /src/__tests__/new-act.js: -------------------------------------------------------------------------------- 1 | let asyncAct 2 | 3 | jest.mock('react', () => { 4 | return { 5 | ...jest.requireActual('react'), 6 | act: cb => { 7 | return cb() 8 | }, 9 | } 10 | }) 11 | 12 | beforeEach(() => { 13 | jest.resetModules() 14 | asyncAct = require('../act-compat').default 15 | jest.spyOn(console, 'error').mockImplementation(() => {}) 16 | }) 17 | 18 | afterEach(() => { 19 | jest.restoreAllMocks() 20 | }) 21 | 22 | test('async act works when it does not exist (older versions of react)', async () => { 23 | const callback = jest.fn() 24 | await asyncAct(async () => { 25 | await Promise.resolve() 26 | await callback() 27 | }) 28 | expect(console.error).toHaveBeenCalledTimes(0) 29 | expect(callback).toHaveBeenCalledTimes(1) 30 | 31 | callback.mockClear() 32 | console.error.mockClear() 33 | 34 | await asyncAct(async () => { 35 | await Promise.resolve() 36 | await callback() 37 | }) 38 | expect(console.error).toHaveBeenCalledTimes(0) 39 | expect(callback).toHaveBeenCalledTimes(1) 40 | }) 41 | 42 | test('async act recovers from errors', async () => { 43 | try { 44 | await asyncAct(async () => { 45 | await null 46 | throw new Error('test error') 47 | }) 48 | } catch (err) { 49 | console.error('call console.error') 50 | } 51 | expect(console.error).toHaveBeenCalledTimes(1) 52 | expect(console.error.mock.calls).toMatchInlineSnapshot(` 53 | [ 54 | [ 55 | call console.error, 56 | ], 57 | ] 58 | `) 59 | }) 60 | 61 | test('async act recovers from sync errors', async () => { 62 | try { 63 | await asyncAct(() => { 64 | throw new Error('test error') 65 | }) 66 | } catch (err) { 67 | console.error('call console.error') 68 | } 69 | expect(console.error).toHaveBeenCalledTimes(1) 70 | expect(console.error.mock.calls).toMatchInlineSnapshot(` 71 | [ 72 | [ 73 | call console.error, 74 | ], 75 | ] 76 | `) 77 | }) 78 | 79 | /* eslint no-console:0 */ 80 | -------------------------------------------------------------------------------- /src/__tests__/render.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import ReactDOMServer from 'react-dom/server' 4 | import {fireEvent, render, screen, configure} from '../' 5 | 6 | const isReact18 = React.version.startsWith('18.') 7 | const isReact19 = React.version.startsWith('19.') 8 | 9 | const testGateReact18 = isReact18 ? test : test.skip 10 | const testGateReact19 = isReact19 ? test : test.skip 11 | 12 | describe('render API', () => { 13 | let originalConfig 14 | beforeEach(() => { 15 | // Grab the existing configuration so we can restore 16 | // it at the end of the test 17 | configure(existingConfig => { 18 | originalConfig = existingConfig 19 | // Don't change the existing config 20 | return {} 21 | }) 22 | }) 23 | 24 | afterEach(() => { 25 | configure(originalConfig) 26 | }) 27 | 28 | test('renders div into document', () => { 29 | const ref = React.createRef() 30 | const {container} = render(
) 31 | expect(container.firstChild).toBe(ref.current) 32 | }) 33 | 34 | test('works great with react portals', () => { 35 | class MyPortal extends React.Component { 36 | constructor(...args) { 37 | super(...args) 38 | this.portalNode = document.createElement('div') 39 | this.portalNode.dataset.testid = 'my-portal' 40 | } 41 | componentDidMount() { 42 | document.body.appendChild(this.portalNode) 43 | } 44 | componentWillUnmount() { 45 | this.portalNode.parentNode.removeChild(this.portalNode) 46 | } 47 | render() { 48 | return ReactDOM.createPortal( 49 | , 50 | this.portalNode, 51 | ) 52 | } 53 | } 54 | 55 | function Greet({greeting, subject}) { 56 | return ( 57 |
58 | 59 | {greeting} {subject} 60 | 61 |
62 | ) 63 | } 64 | 65 | const {unmount} = render() 66 | expect(screen.getByText('Hello World')).toBeInTheDocument() 67 | const portalNode = screen.getByTestId('my-portal') 68 | expect(portalNode).toBeInTheDocument() 69 | unmount() 70 | expect(portalNode).not.toBeInTheDocument() 71 | }) 72 | 73 | test('returns baseElement which defaults to document.body', () => { 74 | const {baseElement} = render(
) 75 | expect(baseElement).toBe(document.body) 76 | }) 77 | 78 | test('supports fragments', () => { 79 | class Test extends React.Component { 80 | render() { 81 | return ( 82 |
83 | DocumentFragment is pretty cool! 84 |
85 | ) 86 | } 87 | } 88 | 89 | const {asFragment} = render() 90 | expect(asFragment()).toMatchSnapshot() 91 | }) 92 | 93 | test('renders options.wrapper around node', () => { 94 | const WrapperComponent = ({children}) => ( 95 |
{children}
96 | ) 97 | 98 | const {container} = render(
, { 99 | wrapper: WrapperComponent, 100 | }) 101 | 102 | expect(screen.getByTestId('wrapper')).toBeInTheDocument() 103 | expect(container.firstChild).toMatchInlineSnapshot(` 104 |
107 |
110 |
111 | `) 112 | }) 113 | 114 | test('renders options.wrapper around node when reactStrictMode is true', () => { 115 | configure({reactStrictMode: true}) 116 | 117 | const WrapperComponent = ({children}) => ( 118 |
{children}
119 | ) 120 | const {container} = render(
, { 121 | wrapper: WrapperComponent, 122 | }) 123 | 124 | expect(screen.getByTestId('wrapper')).toBeInTheDocument() 125 | expect(container.firstChild).toMatchInlineSnapshot(` 126 |
129 |
132 |
133 | `) 134 | }) 135 | 136 | test('renders twice when reactStrictMode is true', () => { 137 | configure({reactStrictMode: true}) 138 | 139 | const spy = jest.fn() 140 | function Component() { 141 | spy() 142 | return null 143 | } 144 | 145 | render() 146 | expect(spy).toHaveBeenCalledTimes(2) 147 | }) 148 | 149 | test('flushes useEffect cleanup functions sync on unmount()', () => { 150 | const spy = jest.fn() 151 | function Component() { 152 | React.useEffect(() => spy, []) 153 | return null 154 | } 155 | const {unmount} = render() 156 | expect(spy).toHaveBeenCalledTimes(0) 157 | 158 | unmount() 159 | 160 | expect(spy).toHaveBeenCalledTimes(1) 161 | }) 162 | 163 | test('can be called multiple times on the same container', () => { 164 | const container = document.createElement('div') 165 | 166 | const {unmount} = render(, {container}) 167 | 168 | expect(container).toContainHTML('') 169 | 170 | render(, {container}) 171 | 172 | expect(container).toContainHTML('') 173 | 174 | unmount() 175 | 176 | expect(container).toBeEmptyDOMElement() 177 | }) 178 | 179 | test('hydrate will make the UI interactive', () => { 180 | function App() { 181 | const [clicked, handleClick] = React.useReducer(n => n + 1, 0) 182 | 183 | return ( 184 | 187 | ) 188 | } 189 | const ui = 190 | const container = document.createElement('div') 191 | document.body.appendChild(container) 192 | container.innerHTML = ReactDOMServer.renderToString(ui) 193 | 194 | expect(container).toHaveTextContent('clicked:0') 195 | 196 | render(ui, {container, hydrate: true}) 197 | 198 | fireEvent.click(container.querySelector('button')) 199 | 200 | expect(container).toHaveTextContent('clicked:1') 201 | }) 202 | 203 | test('hydrate can have a wrapper', () => { 204 | const wrapperComponentMountEffect = jest.fn() 205 | function WrapperComponent({children}) { 206 | React.useEffect(() => { 207 | wrapperComponentMountEffect() 208 | }) 209 | 210 | return children 211 | } 212 | const ui =
213 | const container = document.createElement('div') 214 | document.body.appendChild(container) 215 | container.innerHTML = ReactDOMServer.renderToString(ui) 216 | 217 | render(ui, {container, hydrate: true, wrapper: WrapperComponent}) 218 | 219 | expect(wrapperComponentMountEffect).toHaveBeenCalledTimes(1) 220 | }) 221 | 222 | testGateReact18('legacyRoot uses legacy ReactDOM.render', () => { 223 | expect(() => { 224 | render(
, {legacyRoot: true}) 225 | }).toErrorDev( 226 | [ 227 | "Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot", 228 | ], 229 | {withoutStack: true}, 230 | ) 231 | }) 232 | 233 | testGateReact19('legacyRoot throws', () => { 234 | expect(() => { 235 | render(
, {legacyRoot: true}) 236 | }).toThrowErrorMatchingInlineSnapshot( 237 | `\`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.`, 238 | ) 239 | }) 240 | 241 | testGateReact18('legacyRoot uses legacy ReactDOM.hydrate', () => { 242 | const ui =
243 | const container = document.createElement('div') 244 | container.innerHTML = ReactDOMServer.renderToString(ui) 245 | expect(() => { 246 | render(ui, {container, hydrate: true, legacyRoot: true}) 247 | }).toErrorDev( 248 | [ 249 | "Warning: ReactDOM.hydrate is no longer supported in React 18. Use hydrateRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot", 250 | ], 251 | {withoutStack: true}, 252 | ) 253 | }) 254 | 255 | testGateReact19('legacyRoot throws even with hydrate', () => { 256 | const ui =
257 | const container = document.createElement('div') 258 | container.innerHTML = ReactDOMServer.renderToString(ui) 259 | expect(() => { 260 | render(ui, {container, hydrate: true, legacyRoot: true}) 261 | }).toThrowErrorMatchingInlineSnapshot( 262 | `\`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.`, 263 | ) 264 | }) 265 | 266 | test('reactStrictMode in renderOptions has precedence over config when rendering', () => { 267 | const wrapperComponentMountEffect = jest.fn() 268 | function WrapperComponent({children}) { 269 | React.useEffect(() => { 270 | wrapperComponentMountEffect() 271 | }) 272 | 273 | return children 274 | } 275 | const ui =
276 | configure({reactStrictMode: false}) 277 | 278 | render(ui, {wrapper: WrapperComponent, reactStrictMode: true}) 279 | 280 | expect(wrapperComponentMountEffect).toHaveBeenCalledTimes(2) 281 | }) 282 | 283 | test('reactStrictMode in config is used when renderOptions does not specify reactStrictMode', () => { 284 | const wrapperComponentMountEffect = jest.fn() 285 | function WrapperComponent({children}) { 286 | React.useEffect(() => { 287 | wrapperComponentMountEffect() 288 | }) 289 | 290 | return children 291 | } 292 | const ui =
293 | configure({reactStrictMode: true}) 294 | 295 | render(ui, {wrapper: WrapperComponent}) 296 | 297 | expect(wrapperComponentMountEffect).toHaveBeenCalledTimes(2) 298 | }) 299 | }) 300 | -------------------------------------------------------------------------------- /src/__tests__/renderHook.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from 'react' 2 | import {configure, renderHook} from '../pure' 3 | 4 | const isReact18 = React.version.startsWith('18.') 5 | const isReact19 = React.version.startsWith('19.') 6 | 7 | const testGateReact18 = isReact18 ? test : test.skip 8 | const testGateReact19 = isReact19 ? test : test.skip 9 | 10 | test('gives committed result', () => { 11 | const {result} = renderHook(() => { 12 | const [state, setState] = React.useState(1) 13 | 14 | React.useEffect(() => { 15 | setState(2) 16 | }, []) 17 | 18 | return [state, setState] 19 | }) 20 | 21 | expect(result.current).toEqual([2, expect.any(Function)]) 22 | }) 23 | 24 | test('allows rerendering', () => { 25 | const {result, rerender} = renderHook( 26 | ({branch}) => { 27 | const [left, setLeft] = React.useState('left') 28 | const [right, setRight] = React.useState('right') 29 | 30 | // eslint-disable-next-line jest/no-if, jest/no-conditional-in-test -- false-positive 31 | switch (branch) { 32 | case 'left': 33 | return [left, setLeft] 34 | case 'right': 35 | return [right, setRight] 36 | 37 | default: 38 | throw new Error( 39 | 'No Props passed. This is a bug in the implementation', 40 | ) 41 | } 42 | }, 43 | {initialProps: {branch: 'left'}}, 44 | ) 45 | 46 | expect(result.current).toEqual(['left', expect.any(Function)]) 47 | 48 | rerender({branch: 'right'}) 49 | 50 | expect(result.current).toEqual(['right', expect.any(Function)]) 51 | }) 52 | 53 | test('allows wrapper components', async () => { 54 | const Context = React.createContext('default') 55 | function Wrapper({children}) { 56 | return {children} 57 | } 58 | const {result} = renderHook( 59 | () => { 60 | return React.useContext(Context) 61 | }, 62 | { 63 | wrapper: Wrapper, 64 | }, 65 | ) 66 | 67 | expect(result.current).toEqual('provided') 68 | }) 69 | 70 | testGateReact18('legacyRoot uses legacy ReactDOM.render', () => { 71 | const Context = React.createContext('default') 72 | function Wrapper({children}) { 73 | return {children} 74 | } 75 | let result 76 | expect(() => { 77 | result = renderHook( 78 | () => { 79 | return React.useContext(Context) 80 | }, 81 | { 82 | wrapper: Wrapper, 83 | legacyRoot: true, 84 | }, 85 | ).result 86 | }).toErrorDev( 87 | [ 88 | "Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot", 89 | ], 90 | {withoutStack: true}, 91 | ) 92 | expect(result.current).toEqual('provided') 93 | }) 94 | 95 | testGateReact19('legacyRoot throws', () => { 96 | const Context = React.createContext('default') 97 | function Wrapper({children}) { 98 | return {children} 99 | } 100 | expect(() => { 101 | renderHook( 102 | () => { 103 | return React.useContext(Context) 104 | }, 105 | { 106 | wrapper: Wrapper, 107 | legacyRoot: true, 108 | }, 109 | ).result 110 | }).toThrowErrorMatchingInlineSnapshot( 111 | `\`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.`, 112 | ) 113 | }) 114 | 115 | describe('reactStrictMode', () => { 116 | let originalConfig 117 | beforeEach(() => { 118 | // Grab the existing configuration so we can restore 119 | // it at the end of the test 120 | configure(existingConfig => { 121 | originalConfig = existingConfig 122 | // Don't change the existing config 123 | return {} 124 | }) 125 | }) 126 | 127 | afterEach(() => { 128 | configure(originalConfig) 129 | }) 130 | 131 | test('reactStrictMode in renderOptions has precedence over config when rendering', () => { 132 | const hookMountEffect = jest.fn() 133 | configure({reactStrictMode: false}) 134 | 135 | renderHook(() => useEffect(() => hookMountEffect()), { 136 | reactStrictMode: true, 137 | }) 138 | 139 | expect(hookMountEffect).toHaveBeenCalledTimes(2) 140 | }) 141 | }) 142 | -------------------------------------------------------------------------------- /src/__tests__/rerender.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {render, configure} from '../' 3 | 4 | describe('rerender API', () => { 5 | let originalConfig 6 | beforeEach(() => { 7 | // Grab the existing configuration so we can restore 8 | // it at the end of the test 9 | configure(existingConfig => { 10 | originalConfig = existingConfig 11 | // Don't change the existing config 12 | return {} 13 | }) 14 | }) 15 | 16 | afterEach(() => { 17 | configure(originalConfig) 18 | }) 19 | 20 | test('rerender will re-render the element', () => { 21 | const Greeting = props =>
{props.message}
22 | const {container, rerender} = render() 23 | expect(container.firstChild).toHaveTextContent('hi') 24 | rerender() 25 | expect(container.firstChild).toHaveTextContent('hey') 26 | }) 27 | 28 | test('hydrate will not update props until next render', () => { 29 | const initialInputElement = document.createElement('input') 30 | const container = document.createElement('div') 31 | container.appendChild(initialInputElement) 32 | document.body.appendChild(container) 33 | 34 | const firstValue = 'hello' 35 | initialInputElement.value = firstValue 36 | 37 | const {rerender} = render( null} />, { 38 | container, 39 | hydrate: true, 40 | }) 41 | 42 | expect(initialInputElement).toHaveValue(firstValue) 43 | 44 | const secondValue = 'goodbye' 45 | rerender( null} />) 46 | expect(initialInputElement).toHaveValue(secondValue) 47 | }) 48 | 49 | test('re-renders options.wrapper around node when reactStrictMode is true', () => { 50 | configure({reactStrictMode: true}) 51 | 52 | const WrapperComponent = ({children}) => ( 53 |
{children}
54 | ) 55 | const Greeting = props =>
{props.message}
56 | const {container, rerender} = render(, { 57 | wrapper: WrapperComponent, 58 | }) 59 | 60 | expect(container.firstChild).toMatchInlineSnapshot(` 61 |
64 |
65 | hi 66 |
67 |
68 | `) 69 | 70 | rerender() 71 | expect(container.firstChild).toMatchInlineSnapshot(` 72 |
75 |
76 | hey 77 |
78 |
79 | `) 80 | }) 81 | 82 | test('re-renders twice when reactStrictMode is true', () => { 83 | configure({reactStrictMode: true}) 84 | 85 | const spy = jest.fn() 86 | function Component() { 87 | spy() 88 | return null 89 | } 90 | 91 | const {rerender} = render() 92 | expect(spy).toHaveBeenCalledTimes(2) 93 | 94 | spy.mockClear() 95 | rerender() 96 | expect(spy).toHaveBeenCalledTimes(2) 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /src/__tests__/stopwatch.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {render, fireEvent, screen} from '../' 3 | 4 | class StopWatch extends React.Component { 5 | state = {lapse: 0, running: false} 6 | handleRunClick = () => { 7 | this.setState(state => { 8 | if (state.running) { 9 | clearInterval(this.timer) 10 | } else { 11 | const startTime = Date.now() - this.state.lapse 12 | this.timer = setInterval(() => { 13 | this.setState({lapse: Date.now() - startTime}) 14 | }) 15 | } 16 | return {running: !state.running} 17 | }) 18 | } 19 | handleClearClick = () => { 20 | clearInterval(this.timer) 21 | this.setState({lapse: 0, running: false}) 22 | } 23 | componentWillUnmount() { 24 | clearInterval(this.timer) 25 | } 26 | render() { 27 | const {lapse, running} = this.state 28 | return ( 29 |
30 | {lapse}ms 31 | 34 | 35 |
36 | ) 37 | } 38 | } 39 | 40 | const sleep = t => new Promise(resolve => setTimeout(resolve, t)) 41 | 42 | test('unmounts a component', async () => { 43 | const {unmount, container} = render() 44 | fireEvent.click(screen.getByText('Start')) 45 | unmount() 46 | // hey there reader! You don't need to have an assertion like this one 47 | // this is just me making sure that the unmount function works. 48 | // You don't need to do this in your apps. Just rely on the fact that this works. 49 | expect(container).toBeEmptyDOMElement() 50 | // just wait to see if the interval is cleared or not 51 | // if it's not, then we'll call setState on an unmounted component 52 | // and get an error. 53 | await sleep(5) 54 | }) 55 | -------------------------------------------------------------------------------- /src/act-compat.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as DeprecatedReactTestUtils from 'react-dom/test-utils' 3 | 4 | const reactAct = 5 | typeof React.act === 'function' ? React.act : DeprecatedReactTestUtils.act 6 | 7 | function getGlobalThis() { 8 | /* istanbul ignore else */ 9 | if (typeof globalThis !== 'undefined') { 10 | return globalThis 11 | } 12 | /* istanbul ignore next */ 13 | if (typeof self !== 'undefined') { 14 | return self 15 | } 16 | /* istanbul ignore next */ 17 | if (typeof window !== 'undefined') { 18 | return window 19 | } 20 | /* istanbul ignore next */ 21 | if (typeof global !== 'undefined') { 22 | return global 23 | } 24 | /* istanbul ignore next */ 25 | throw new Error('unable to locate global object') 26 | } 27 | 28 | function setIsReactActEnvironment(isReactActEnvironment) { 29 | getGlobalThis().IS_REACT_ACT_ENVIRONMENT = isReactActEnvironment 30 | } 31 | 32 | function getIsReactActEnvironment() { 33 | return getGlobalThis().IS_REACT_ACT_ENVIRONMENT 34 | } 35 | 36 | function withGlobalActEnvironment(actImplementation) { 37 | return callback => { 38 | const previousActEnvironment = getIsReactActEnvironment() 39 | setIsReactActEnvironment(true) 40 | try { 41 | // The return value of `act` is always a thenable. 42 | let callbackNeedsToBeAwaited = false 43 | const actResult = actImplementation(() => { 44 | const result = callback() 45 | if ( 46 | result !== null && 47 | typeof result === 'object' && 48 | typeof result.then === 'function' 49 | ) { 50 | callbackNeedsToBeAwaited = true 51 | } 52 | return result 53 | }) 54 | if (callbackNeedsToBeAwaited) { 55 | const thenable = actResult 56 | return { 57 | then: (resolve, reject) => { 58 | thenable.then( 59 | returnValue => { 60 | setIsReactActEnvironment(previousActEnvironment) 61 | resolve(returnValue) 62 | }, 63 | error => { 64 | setIsReactActEnvironment(previousActEnvironment) 65 | reject(error) 66 | }, 67 | ) 68 | }, 69 | } 70 | } else { 71 | setIsReactActEnvironment(previousActEnvironment) 72 | return actResult 73 | } 74 | } catch (error) { 75 | // Can't be a `finally {}` block since we don't know if we have to immediately restore IS_REACT_ACT_ENVIRONMENT 76 | // or if we have to await the callback first. 77 | setIsReactActEnvironment(previousActEnvironment) 78 | throw error 79 | } 80 | } 81 | } 82 | 83 | const act = withGlobalActEnvironment(reactAct) 84 | 85 | export default act 86 | export { 87 | setIsReactActEnvironment as setReactActEnvironment, 88 | getIsReactActEnvironment, 89 | } 90 | 91 | /* eslint no-console:0 */ 92 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | import { 2 | getConfig as getConfigDTL, 3 | configure as configureDTL, 4 | } from '@testing-library/dom' 5 | 6 | let configForRTL = { 7 | reactStrictMode: false, 8 | } 9 | 10 | function getConfig() { 11 | return { 12 | ...getConfigDTL(), 13 | ...configForRTL, 14 | } 15 | } 16 | 17 | function configure(newConfig) { 18 | if (typeof newConfig === 'function') { 19 | // Pass the existing config out to the provided function 20 | // and accept a delta in return 21 | newConfig = newConfig(getConfig()) 22 | } 23 | 24 | const {reactStrictMode, ...configForDTL} = newConfig 25 | 26 | configureDTL(configForDTL) 27 | 28 | configForRTL = { 29 | ...configForRTL, 30 | reactStrictMode, 31 | } 32 | } 33 | 34 | export {getConfig, configure} 35 | -------------------------------------------------------------------------------- /src/fire-event.js: -------------------------------------------------------------------------------- 1 | import {fireEvent as dtlFireEvent} from '@testing-library/dom' 2 | 3 | // react-testing-library's version of fireEvent will call 4 | // dom-testing-library's version of fireEvent. The reason 5 | // we make this distinction however is because we have 6 | // a few extra events that work a bit differently 7 | const fireEvent = (...args) => dtlFireEvent(...args) 8 | 9 | Object.keys(dtlFireEvent).forEach(key => { 10 | fireEvent[key] = (...args) => dtlFireEvent[key](...args) 11 | }) 12 | 13 | // React event system tracks native mouseOver/mouseOut events for 14 | // running onMouseEnter/onMouseLeave handlers 15 | // @link https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/react-dom/src/events/EnterLeaveEventPlugin.js#L24-L31 16 | const mouseEnter = fireEvent.mouseEnter 17 | const mouseLeave = fireEvent.mouseLeave 18 | fireEvent.mouseEnter = (...args) => { 19 | mouseEnter(...args) 20 | return fireEvent.mouseOver(...args) 21 | } 22 | fireEvent.mouseLeave = (...args) => { 23 | mouseLeave(...args) 24 | return fireEvent.mouseOut(...args) 25 | } 26 | 27 | const pointerEnter = fireEvent.pointerEnter 28 | const pointerLeave = fireEvent.pointerLeave 29 | fireEvent.pointerEnter = (...args) => { 30 | pointerEnter(...args) 31 | return fireEvent.pointerOver(...args) 32 | } 33 | fireEvent.pointerLeave = (...args) => { 34 | pointerLeave(...args) 35 | return fireEvent.pointerOut(...args) 36 | } 37 | 38 | const select = fireEvent.select 39 | fireEvent.select = (node, init) => { 40 | select(node, init) 41 | // React tracks this event only on focused inputs 42 | node.focus() 43 | 44 | // React creates this event when one of the following native events happens 45 | // - contextMenu 46 | // - mouseUp 47 | // - dragEnd 48 | // - keyUp 49 | // - keyDown 50 | // so we can use any here 51 | // @link https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/react-dom/src/events/SelectEventPlugin.js#L203-L224 52 | fireEvent.keyUp(node, init) 53 | } 54 | 55 | // React event system tracks native focusout/focusin events for 56 | // running blur/focus handlers 57 | // @link https://github.com/facebook/react/pull/19186 58 | const blur = fireEvent.blur 59 | const focus = fireEvent.focus 60 | fireEvent.blur = (...args) => { 61 | fireEvent.focusOut(...args) 62 | return blur(...args) 63 | } 64 | fireEvent.focus = (...args) => { 65 | fireEvent.focusIn(...args) 66 | return focus(...args) 67 | } 68 | 69 | export {fireEvent} 70 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import {getIsReactActEnvironment, setReactActEnvironment} from './act-compat' 2 | import {cleanup} from './pure' 3 | 4 | // if we're running in a test runner that supports afterEach 5 | // or teardown then we'll automatically run cleanup afterEach test 6 | // this ensures that tests run in isolation from each other 7 | // if you don't like this then either import the `pure` module 8 | // or set the RTL_SKIP_AUTO_CLEANUP env variable to 'true'. 9 | if (typeof process === 'undefined' || !process.env?.RTL_SKIP_AUTO_CLEANUP) { 10 | // ignore teardown() in code coverage because Jest does not support it 11 | /* istanbul ignore else */ 12 | if (typeof afterEach === 'function') { 13 | afterEach(() => { 14 | cleanup() 15 | }) 16 | } else if (typeof teardown === 'function') { 17 | // Block is guarded by `typeof` check. 18 | // eslint does not support `typeof` guards. 19 | // eslint-disable-next-line no-undef 20 | teardown(() => { 21 | cleanup() 22 | }) 23 | } 24 | 25 | // No test setup with other test runners available 26 | /* istanbul ignore else */ 27 | if (typeof beforeAll === 'function' && typeof afterAll === 'function') { 28 | // This matches the behavior of React < 18. 29 | let previousIsReactActEnvironment = getIsReactActEnvironment() 30 | beforeAll(() => { 31 | previousIsReactActEnvironment = getIsReactActEnvironment() 32 | setReactActEnvironment(true) 33 | }) 34 | 35 | afterAll(() => { 36 | setReactActEnvironment(previousIsReactActEnvironment) 37 | }) 38 | } 39 | } 40 | 41 | export * from './pure' 42 | -------------------------------------------------------------------------------- /src/pure.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import * as ReactDOMClient from 'react-dom/client' 4 | import { 5 | getQueriesForElement, 6 | prettyDOM, 7 | configure as configureDTL, 8 | } from '@testing-library/dom' 9 | import act, { 10 | getIsReactActEnvironment, 11 | setReactActEnvironment, 12 | } from './act-compat' 13 | import {fireEvent} from './fire-event' 14 | import {getConfig, configure} from './config' 15 | 16 | function jestFakeTimersAreEnabled() { 17 | /* istanbul ignore else */ 18 | if (typeof jest !== 'undefined' && jest !== null) { 19 | return ( 20 | // legacy timers 21 | setTimeout._isMockFunction === true || // modern timers 22 | // eslint-disable-next-line prefer-object-has-own -- No Object.hasOwn in all target environments we support. 23 | Object.prototype.hasOwnProperty.call(setTimeout, 'clock') 24 | ) 25 | } // istanbul ignore next 26 | 27 | return false 28 | } 29 | 30 | configureDTL({ 31 | unstable_advanceTimersWrapper: cb => { 32 | return act(cb) 33 | }, 34 | // We just want to run `waitFor` without IS_REACT_ACT_ENVIRONMENT 35 | // But that's not necessarily how `asyncWrapper` is used since it's a public method. 36 | // Let's just hope nobody else is using it. 37 | asyncWrapper: async cb => { 38 | const previousActEnvironment = getIsReactActEnvironment() 39 | setReactActEnvironment(false) 40 | try { 41 | const result = await cb() 42 | // Drain microtask queue. 43 | // Otherwise we'll restore the previous act() environment, before we resolve the `waitFor` call. 44 | // The caller would have no chance to wrap the in-flight Promises in `act()` 45 | await new Promise(resolve => { 46 | setTimeout(() => { 47 | resolve() 48 | }, 0) 49 | 50 | if (jestFakeTimersAreEnabled()) { 51 | jest.advanceTimersByTime(0) 52 | } 53 | }) 54 | 55 | return result 56 | } finally { 57 | setReactActEnvironment(previousActEnvironment) 58 | } 59 | }, 60 | eventWrapper: cb => { 61 | let result 62 | act(() => { 63 | result = cb() 64 | }) 65 | return result 66 | }, 67 | }) 68 | 69 | // Ideally we'd just use a WeakMap where containers are keys and roots are values. 70 | // We use two variables so that we can bail out in constant time when we render with a new container (most common use case) 71 | /** 72 | * @type {Set} 73 | */ 74 | const mountedContainers = new Set() 75 | /** 76 | * @type Array<{container: import('react-dom').Container, root: ReturnType}> 77 | */ 78 | const mountedRootEntries = [] 79 | 80 | function strictModeIfNeeded(innerElement, reactStrictMode) { 81 | return reactStrictMode ?? getConfig().reactStrictMode 82 | ? React.createElement(React.StrictMode, null, innerElement) 83 | : innerElement 84 | } 85 | 86 | function wrapUiIfNeeded(innerElement, wrapperComponent) { 87 | return wrapperComponent 88 | ? React.createElement(wrapperComponent, null, innerElement) 89 | : innerElement 90 | } 91 | 92 | function createConcurrentRoot( 93 | container, 94 | { 95 | hydrate, 96 | onCaughtError, 97 | onRecoverableError, 98 | ui, 99 | wrapper: WrapperComponent, 100 | reactStrictMode, 101 | }, 102 | ) { 103 | let root 104 | if (hydrate) { 105 | act(() => { 106 | root = ReactDOMClient.hydrateRoot( 107 | container, 108 | strictModeIfNeeded( 109 | wrapUiIfNeeded(ui, WrapperComponent), 110 | reactStrictMode, 111 | ), 112 | {onCaughtError, onRecoverableError}, 113 | ) 114 | }) 115 | } else { 116 | root = ReactDOMClient.createRoot(container, { 117 | onCaughtError, 118 | onRecoverableError, 119 | }) 120 | } 121 | 122 | return { 123 | hydrate() { 124 | /* istanbul ignore if */ 125 | if (!hydrate) { 126 | throw new Error( 127 | 'Attempted to hydrate a non-hydrateable root. This is a bug in `@testing-library/react`.', 128 | ) 129 | } 130 | // Nothing to do since hydration happens when creating the root object. 131 | }, 132 | render(element) { 133 | root.render(element) 134 | }, 135 | unmount() { 136 | root.unmount() 137 | }, 138 | } 139 | } 140 | 141 | function createLegacyRoot(container) { 142 | return { 143 | hydrate(element) { 144 | ReactDOM.hydrate(element, container) 145 | }, 146 | render(element) { 147 | ReactDOM.render(element, container) 148 | }, 149 | unmount() { 150 | ReactDOM.unmountComponentAtNode(container) 151 | }, 152 | } 153 | } 154 | 155 | function renderRoot( 156 | ui, 157 | { 158 | baseElement, 159 | container, 160 | hydrate, 161 | queries, 162 | root, 163 | wrapper: WrapperComponent, 164 | reactStrictMode, 165 | }, 166 | ) { 167 | act(() => { 168 | if (hydrate) { 169 | root.hydrate( 170 | strictModeIfNeeded( 171 | wrapUiIfNeeded(ui, WrapperComponent), 172 | reactStrictMode, 173 | ), 174 | container, 175 | ) 176 | } else { 177 | root.render( 178 | strictModeIfNeeded( 179 | wrapUiIfNeeded(ui, WrapperComponent), 180 | reactStrictMode, 181 | ), 182 | container, 183 | ) 184 | } 185 | }) 186 | 187 | return { 188 | container, 189 | baseElement, 190 | debug: (el = baseElement, maxLength, options) => 191 | Array.isArray(el) 192 | ? // eslint-disable-next-line no-console 193 | el.forEach(e => console.log(prettyDOM(e, maxLength, options))) 194 | : // eslint-disable-next-line no-console, 195 | console.log(prettyDOM(el, maxLength, options)), 196 | unmount: () => { 197 | act(() => { 198 | root.unmount() 199 | }) 200 | }, 201 | rerender: rerenderUi => { 202 | renderRoot(rerenderUi, { 203 | container, 204 | baseElement, 205 | root, 206 | wrapper: WrapperComponent, 207 | reactStrictMode, 208 | }) 209 | // Intentionally do not return anything to avoid unnecessarily complicating the API. 210 | // folks can use all the same utilities we return in the first place that are bound to the container 211 | }, 212 | asFragment: () => { 213 | /* istanbul ignore else (old jsdom limitation) */ 214 | if (typeof document.createRange === 'function') { 215 | return document 216 | .createRange() 217 | .createContextualFragment(container.innerHTML) 218 | } else { 219 | const template = document.createElement('template') 220 | template.innerHTML = container.innerHTML 221 | return template.content 222 | } 223 | }, 224 | ...getQueriesForElement(baseElement, queries), 225 | } 226 | } 227 | 228 | function render( 229 | ui, 230 | { 231 | container, 232 | baseElement = container, 233 | legacyRoot = false, 234 | onCaughtError, 235 | onUncaughtError, 236 | onRecoverableError, 237 | queries, 238 | hydrate = false, 239 | wrapper, 240 | reactStrictMode, 241 | } = {}, 242 | ) { 243 | if (onUncaughtError !== undefined) { 244 | throw new Error( 245 | 'onUncaughtError is not supported. The `render` call will already throw on uncaught errors.', 246 | ) 247 | } 248 | if (legacyRoot && typeof ReactDOM.render !== 'function') { 249 | const error = new Error( 250 | '`legacyRoot: true` is not supported in this version of React. ' + 251 | 'If your app runs React 19 or later, you should remove this flag. ' + 252 | 'If your app runs React 18 or earlier, visit https://react.dev/blog/2022/03/08/react-18-upgrade-guide for upgrade instructions.', 253 | ) 254 | Error.captureStackTrace(error, render) 255 | throw error 256 | } 257 | 258 | if (!baseElement) { 259 | // default to document.body instead of documentElement to avoid output of potentially-large 260 | // head elements (such as JSS style blocks) in debug output 261 | baseElement = document.body 262 | } 263 | if (!container) { 264 | container = baseElement.appendChild(document.createElement('div')) 265 | } 266 | 267 | let root 268 | // 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. 269 | if (!mountedContainers.has(container)) { 270 | const createRootImpl = legacyRoot ? createLegacyRoot : createConcurrentRoot 271 | root = createRootImpl(container, { 272 | hydrate, 273 | onCaughtError, 274 | onRecoverableError, 275 | ui, 276 | wrapper, 277 | reactStrictMode, 278 | }) 279 | 280 | mountedRootEntries.push({container, root}) 281 | // we'll add it to the mounted containers regardless of whether it's actually 282 | // added to document.body so the cleanup method works regardless of whether 283 | // they're passing us a custom container or not. 284 | mountedContainers.add(container) 285 | } else { 286 | mountedRootEntries.forEach(rootEntry => { 287 | // Else is unreachable since `mountedContainers` has the `container`. 288 | // Only reachable if one would accidentally add the container to `mountedContainers` but not the root to `mountedRootEntries` 289 | /* istanbul ignore else */ 290 | if (rootEntry.container === container) { 291 | root = rootEntry.root 292 | } 293 | }) 294 | } 295 | 296 | return renderRoot(ui, { 297 | container, 298 | baseElement, 299 | queries, 300 | hydrate, 301 | wrapper, 302 | root, 303 | reactStrictMode, 304 | }) 305 | } 306 | 307 | function cleanup() { 308 | mountedRootEntries.forEach(({root, container}) => { 309 | act(() => { 310 | root.unmount() 311 | }) 312 | if (container.parentNode === document.body) { 313 | document.body.removeChild(container) 314 | } 315 | }) 316 | mountedRootEntries.length = 0 317 | mountedContainers.clear() 318 | } 319 | 320 | function renderHook(renderCallback, options = {}) { 321 | const {initialProps, ...renderOptions} = options 322 | 323 | if (renderOptions.legacyRoot && typeof ReactDOM.render !== 'function') { 324 | const error = new Error( 325 | '`legacyRoot: true` is not supported in this version of React. ' + 326 | 'If your app runs React 19 or later, you should remove this flag. ' + 327 | 'If your app runs React 18 or earlier, visit https://react.dev/blog/2022/03/08/react-18-upgrade-guide for upgrade instructions.', 328 | ) 329 | Error.captureStackTrace(error, renderHook) 330 | throw error 331 | } 332 | 333 | const result = React.createRef() 334 | 335 | function TestComponent({renderCallbackProps}) { 336 | const pendingResult = renderCallback(renderCallbackProps) 337 | 338 | React.useEffect(() => { 339 | result.current = pendingResult 340 | }) 341 | 342 | return null 343 | } 344 | 345 | const {rerender: baseRerender, unmount} = render( 346 | , 347 | renderOptions, 348 | ) 349 | 350 | function rerender(rerenderCallbackProps) { 351 | return baseRerender( 352 | , 353 | ) 354 | } 355 | 356 | return {result, rerender, unmount} 357 | } 358 | 359 | // just re-export everything from dom-testing-library 360 | export * from '@testing-library/dom' 361 | export {render, renderHook, cleanup, act, fireEvent, getConfig, configure} 362 | 363 | /* eslint func-name-matching:0 */ 364 | -------------------------------------------------------------------------------- /tests/failOnUnexpectedConsoleCalls.js: -------------------------------------------------------------------------------- 1 | // Fork of https://github.com/facebook/react/blob/513417d6951fa3ff5729302b7990b84604b11afa/scripts/jest/setupTests.js#L71-L161 2 | /** 3 | MIT License 4 | 5 | Copyright (c) Facebook, Inc. and its affiliates. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | */ 25 | /* eslint-disable prefer-template */ 26 | /* eslint-disable func-names */ 27 | const util = require('util') 28 | const chalk = require('chalk') 29 | const shouldIgnoreConsoleError = require('./shouldIgnoreConsoleError') 30 | 31 | const patchConsoleMethod = (methodName, unexpectedConsoleCallStacks) => { 32 | const newMethod = function (format, ...args) { 33 | // Ignore uncaught errors reported by jsdom 34 | // and React addendums because they're too noisy. 35 | if (methodName === 'error' && shouldIgnoreConsoleError(format, args)) { 36 | return 37 | } 38 | 39 | // Capture the call stack now so we can warn about it later. 40 | // The call stack has helpful information for the test author. 41 | // Don't throw yet though b'c it might be accidentally caught and suppressed. 42 | const stack = new Error().stack 43 | unexpectedConsoleCallStacks.push([ 44 | stack.substr(stack.indexOf('\n') + 1), 45 | util.format(format, ...args), 46 | ]) 47 | } 48 | 49 | console[methodName] = newMethod 50 | 51 | return newMethod 52 | } 53 | 54 | const isSpy = spy => 55 | (spy.calls && typeof spy.calls.count === 'function') || 56 | spy._isMockFunction === true 57 | 58 | const flushUnexpectedConsoleCalls = ( 59 | mockMethod, 60 | methodName, 61 | expectedMatcher, 62 | unexpectedConsoleCallStacks, 63 | ) => { 64 | if (console[methodName] !== mockMethod && !isSpy(console[methodName])) { 65 | throw new Error( 66 | `Test did not tear down console.${methodName} mock properly.`, 67 | ) 68 | } 69 | if (unexpectedConsoleCallStacks.length > 0) { 70 | const messages = unexpectedConsoleCallStacks.map( 71 | ([stack, message]) => 72 | `${chalk.red(message)}\n` + 73 | `${stack 74 | .split('\n') 75 | .map(line => chalk.gray(line)) 76 | .join('\n')}`, 77 | ) 78 | 79 | const message = 80 | `Expected test not to call ${chalk.bold( 81 | `console.${methodName}()`, 82 | )}.\n\n` + 83 | 'If the warning is expected, test for it explicitly by:\n' + 84 | `1. Using the ${chalk.bold('.' + expectedMatcher + '()')} ` + 85 | `matcher, or...\n` + 86 | `2. Mock it out using ${chalk.bold( 87 | 'spyOnDev', 88 | )}(console, '${methodName}') or ${chalk.bold( 89 | 'spyOnProd', 90 | )}(console, '${methodName}'), and test that the warning occurs.` 91 | 92 | throw new Error(`${message}\n\n${messages.join('\n\n')}`) 93 | } 94 | } 95 | 96 | const unexpectedErrorCallStacks = [] 97 | const unexpectedWarnCallStacks = [] 98 | 99 | const errorMethod = patchConsoleMethod('error', unexpectedErrorCallStacks) 100 | const warnMethod = patchConsoleMethod('warn', unexpectedWarnCallStacks) 101 | 102 | const flushAllUnexpectedConsoleCalls = () => { 103 | flushUnexpectedConsoleCalls( 104 | errorMethod, 105 | 'error', 106 | 'toErrorDev', 107 | unexpectedErrorCallStacks, 108 | ) 109 | flushUnexpectedConsoleCalls( 110 | warnMethod, 111 | 'warn', 112 | 'toWarnDev', 113 | unexpectedWarnCallStacks, 114 | ) 115 | unexpectedErrorCallStacks.length = 0 116 | unexpectedWarnCallStacks.length = 0 117 | } 118 | 119 | const resetAllUnexpectedConsoleCalls = () => { 120 | unexpectedErrorCallStacks.length = 0 121 | unexpectedWarnCallStacks.length = 0 122 | } 123 | 124 | expect.extend({ 125 | ...require('./toWarnDev'), 126 | }) 127 | 128 | beforeEach(resetAllUnexpectedConsoleCalls) 129 | afterEach(flushAllUnexpectedConsoleCalls) 130 | -------------------------------------------------------------------------------- /tests/setup-env.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect' 2 | import './failOnUnexpectedConsoleCalls' 3 | import {TextEncoder} from 'util' 4 | import {MessageChannel} from 'worker_threads' 5 | 6 | global.TextEncoder = TextEncoder 7 | // TODO: Revisit once https://github.com/jsdom/jsdom/issues/2448 is resolved 8 | // This isn't perfect but good enough. 9 | global.MessageChannel = MessageChannel 10 | -------------------------------------------------------------------------------- /tests/shouldIgnoreConsoleError.js: -------------------------------------------------------------------------------- 1 | // Fork of https://github.com/facebook/react/blob/513417d6951fa3ff5729302b7990b84604b11afa/scripts/jest/shouldIgnoreConsoleError.js 2 | /** 3 | MIT License 4 | 5 | Copyright (c) Facebook, Inc. and its affiliates. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | */ 25 | 26 | module.exports = function shouldIgnoreConsoleError(format) { 27 | if (process.env.NODE_ENV !== 'production') { 28 | if (typeof format === 'string') { 29 | if (format.indexOf('Error: Uncaught [') === 0) { 30 | // This looks like an uncaught error from invokeGuardedCallback() wrapper 31 | // in development that is reported by jsdom. Ignore because it's noisy. 32 | return true 33 | } 34 | if (format.indexOf('The above error occurred') === 0) { 35 | // This looks like an error addendum from ReactFiberErrorLogger. 36 | // Ignore it too. 37 | return true 38 | } 39 | if ( 40 | format.startsWith( 41 | 'Warning: `ReactDOMTestUtils.act` is deprecated in favor of `React.act`.', 42 | ) 43 | ) { 44 | // This is a React bug in 18.3.0. 45 | // Versions with `ReactDOMTestUtils.ac` being deprecated, should have `React.act` 46 | return true 47 | } 48 | } 49 | } 50 | // Looks legit 51 | return false 52 | } 53 | -------------------------------------------------------------------------------- /tests/toWarnDev.js: -------------------------------------------------------------------------------- 1 | // Fork of https://github.com/facebook/react/blob/513417d6951fa3ff5729302b7990b84604b11afa/scripts/jest/matchers/toWarnDev.js 2 | /** 3 | MIT License 4 | 5 | Copyright (c) Facebook, Inc. and its affiliates. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | */ 25 | /* eslint-disable no-unsafe-finally */ 26 | /* eslint-disable no-negated-condition */ 27 | /* eslint-disable no-invalid-this */ 28 | /* eslint-disable prefer-template */ 29 | /* eslint-disable func-names */ 30 | /* eslint-disable complexity */ 31 | const util = require('util') 32 | const jestDiff = require('jest-diff').diff 33 | const shouldIgnoreConsoleError = require('./shouldIgnoreConsoleError') 34 | 35 | function normalizeCodeLocInfo(str) { 36 | if (typeof str !== 'string') { 37 | return str 38 | } 39 | // This special case exists only for the special source location in 40 | // ReactElementValidator. That will go away if we remove source locations. 41 | str = str.replace(/Check your code at .+?:\d+/g, 'Check your code at **') 42 | // V8 format: 43 | // at Component (/path/filename.js:123:45) 44 | // React format: 45 | // in Component (at filename.js:123) 46 | // eslint-disable-next-line prefer-arrow-callback 47 | return str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) { 48 | return '\n in ' + name + ' (at **)' 49 | }) 50 | } 51 | 52 | const createMatcherFor = (consoleMethod, matcherName) => 53 | function matcher(callback, expectedMessages, options = {}) { 54 | if (process.env.NODE_ENV !== 'production') { 55 | // Warn about incorrect usage of matcher. 56 | if (typeof expectedMessages === 'string') { 57 | expectedMessages = [expectedMessages] 58 | } else if (!Array.isArray(expectedMessages)) { 59 | throw Error( 60 | `${matcherName}() requires a parameter of type string or an array of strings ` + 61 | `but was given ${typeof expectedMessages}.`, 62 | ) 63 | } 64 | if ( 65 | options != null && 66 | (typeof options !== 'object' || Array.isArray(options)) 67 | ) { 68 | throw new Error( 69 | `${matcherName}() second argument, when present, should be an object. ` + 70 | 'Did you forget to wrap the messages into an array?', 71 | ) 72 | } 73 | if (arguments.length > 3) { 74 | // `matcher` comes from Jest, so it's more than 2 in practice 75 | throw new Error( 76 | `${matcherName}() received more than two arguments. ` + 77 | 'Did you forget to wrap the messages into an array?', 78 | ) 79 | } 80 | 81 | const withoutStack = options.withoutStack 82 | const logAllErrors = options.logAllErrors 83 | const warningsWithoutComponentStack = [] 84 | const warningsWithComponentStack = [] 85 | const unexpectedWarnings = [] 86 | 87 | let lastWarningWithMismatchingFormat = null 88 | let lastWarningWithExtraComponentStack = null 89 | 90 | // Catch errors thrown by the callback, 91 | // But only rethrow them if all test expectations have been satisfied. 92 | // Otherwise an Error in the callback can mask a failed expectation, 93 | // and result in a test that passes when it shouldn't. 94 | let caughtError 95 | 96 | const isLikelyAComponentStack = message => 97 | typeof message === 'string' && 98 | (message.includes('\n in ') || message.includes('\n at ')) 99 | 100 | const consoleSpy = (format, ...args) => { 101 | // Ignore uncaught errors reported by jsdom 102 | // and React addendums because they're too noisy. 103 | if ( 104 | !logAllErrors && 105 | consoleMethod === 'error' && 106 | shouldIgnoreConsoleError(format, args) 107 | ) { 108 | return 109 | } 110 | 111 | const message = util.format(format, ...args) 112 | const normalizedMessage = normalizeCodeLocInfo(message) 113 | 114 | // Remember if the number of %s interpolations 115 | // doesn't match the number of arguments. 116 | // We'll fail the test if it happens. 117 | let argIndex = 0 118 | String(format).replace(/%s/g, () => argIndex++) 119 | if (argIndex !== args.length) { 120 | lastWarningWithMismatchingFormat = { 121 | format, 122 | args, 123 | expectedArgCount: argIndex, 124 | } 125 | } 126 | 127 | // Protect against accidentally passing a component stack 128 | // to warning() which already injects the component stack. 129 | if ( 130 | args.length >= 2 && 131 | isLikelyAComponentStack(args[args.length - 1]) && 132 | isLikelyAComponentStack(args[args.length - 2]) 133 | ) { 134 | lastWarningWithExtraComponentStack = { 135 | format, 136 | } 137 | } 138 | 139 | for (let index = 0; index < expectedMessages.length; index++) { 140 | const expectedMessage = expectedMessages[index] 141 | if ( 142 | normalizedMessage === expectedMessage || 143 | normalizedMessage.includes(expectedMessage) 144 | ) { 145 | if (isLikelyAComponentStack(normalizedMessage)) { 146 | warningsWithComponentStack.push(normalizedMessage) 147 | } else { 148 | warningsWithoutComponentStack.push(normalizedMessage) 149 | } 150 | expectedMessages.splice(index, 1) 151 | return 152 | } 153 | } 154 | 155 | let errorMessage 156 | if (expectedMessages.length === 0) { 157 | errorMessage = 158 | 'Unexpected warning recorded: ' + 159 | this.utils.printReceived(normalizedMessage) 160 | } else if (expectedMessages.length === 1) { 161 | errorMessage = 162 | 'Unexpected warning recorded: ' + 163 | jestDiff(expectedMessages[0], normalizedMessage) 164 | } else { 165 | errorMessage = 166 | 'Unexpected warning recorded: ' + 167 | jestDiff(expectedMessages, [normalizedMessage]) 168 | } 169 | 170 | // Record the call stack for unexpected warnings. 171 | // We don't throw an Error here though, 172 | // Because it might be suppressed by ReactFiberScheduler. 173 | unexpectedWarnings.push(new Error(errorMessage)) 174 | } 175 | 176 | // TODO Decide whether we need to support nested toWarn* expectations. 177 | // If we don't need it, add a check here to see if this is already our spy, 178 | // And throw an error. 179 | const originalMethod = console[consoleMethod] 180 | 181 | // Avoid using Jest's built-in spy since it can't be removed. 182 | console[consoleMethod] = consoleSpy 183 | 184 | try { 185 | callback() 186 | } catch (error) { 187 | caughtError = error 188 | } finally { 189 | // Restore the unspied method so that unexpected errors fail tests. 190 | console[consoleMethod] = originalMethod 191 | 192 | // Any unexpected Errors thrown by the callback should fail the test. 193 | // This should take precedence since unexpected errors could block warnings. 194 | if (caughtError) { 195 | throw caughtError 196 | } 197 | 198 | // Any unexpected warnings should be treated as a failure. 199 | if (unexpectedWarnings.length > 0) { 200 | return { 201 | message: () => unexpectedWarnings[0].stack, 202 | pass: false, 203 | } 204 | } 205 | 206 | // Any remaining messages indicate a failed expectations. 207 | if (expectedMessages.length > 0) { 208 | return { 209 | message: () => 210 | `Expected warning was not recorded:\n ${this.utils.printReceived( 211 | expectedMessages[0], 212 | )}`, 213 | pass: false, 214 | } 215 | } 216 | 217 | if (typeof withoutStack === 'number') { 218 | // We're expecting a particular number of warnings without stacks. 219 | if (withoutStack !== warningsWithoutComponentStack.length) { 220 | return { 221 | message: () => 222 | `Expected ${withoutStack} warnings without a component stack but received ${warningsWithoutComponentStack.length}:\n` + 223 | warningsWithoutComponentStack.map(warning => 224 | this.utils.printReceived(warning), 225 | ), 226 | pass: false, 227 | } 228 | } 229 | } else if (withoutStack === true) { 230 | // We're expecting that all warnings won't have the stack. 231 | // If some warnings have it, it's an error. 232 | if (warningsWithComponentStack.length > 0) { 233 | return { 234 | message: () => 235 | `Received warning unexpectedly includes a component stack:\n ${this.utils.printReceived( 236 | warningsWithComponentStack[0], 237 | )}\nIf this warning intentionally includes the component stack, remove ` + 238 | `{withoutStack: true} from the ${matcherName}() call. If you have a mix of ` + 239 | `warnings with and without stack in one ${matcherName}() call, pass ` + 240 | `{withoutStack: N} where N is the number of warnings without stacks.`, 241 | pass: false, 242 | } 243 | } 244 | } else if (withoutStack === false || withoutStack === undefined) { 245 | // We're expecting that all warnings *do* have the stack (default). 246 | // If some warnings don't have it, it's an error. 247 | if (warningsWithoutComponentStack.length > 0) { 248 | return { 249 | message: () => 250 | `Received warning unexpectedly does not include a component stack:\n ${this.utils.printReceived( 251 | warningsWithoutComponentStack[0], 252 | )}\nIf this warning intentionally omits the component stack, add ` + 253 | `{withoutStack: true} to the ${matcherName} call.`, 254 | pass: false, 255 | } 256 | } 257 | } else { 258 | throw Error( 259 | `The second argument for ${matcherName}(), when specified, must be an object. It may have a ` + 260 | `property called "withoutStack" whose value may be undefined, boolean, or a number. ` + 261 | `Instead received ${typeof withoutStack}.`, 262 | ) 263 | } 264 | 265 | if (lastWarningWithMismatchingFormat !== null) { 266 | return { 267 | message: () => 268 | `Received ${ 269 | lastWarningWithMismatchingFormat.args.length 270 | } arguments for a message with ${ 271 | lastWarningWithMismatchingFormat.expectedArgCount 272 | } placeholders:\n ${this.utils.printReceived( 273 | lastWarningWithMismatchingFormat.format, 274 | )}`, 275 | pass: false, 276 | } 277 | } 278 | 279 | if (lastWarningWithExtraComponentStack !== null) { 280 | return { 281 | message: () => 282 | `Received more than one component stack for a warning:\n ${this.utils.printReceived( 283 | lastWarningWithExtraComponentStack.format, 284 | )}\nDid you accidentally pass a stack to warning() as the last argument? ` + 285 | `Don't forget warning() already injects the component stack automatically.`, 286 | pass: false, 287 | } 288 | } 289 | 290 | return {pass: true} 291 | } 292 | } else { 293 | // Any uncaught errors or warnings should fail tests in production mode. 294 | callback() 295 | 296 | return {pass: true} 297 | } 298 | } 299 | 300 | module.exports = { 301 | toWarnDev: createMatcherFor('warn', 'toWarnDev'), 302 | toErrorDev: createMatcherFor('error', 'toErrorDev'), 303 | } 304 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | // TypeScript Version: 3.8 2 | import * as ReactDOMClient from 'react-dom/client' 3 | import { 4 | queries, 5 | Queries, 6 | BoundFunction, 7 | prettyFormat, 8 | Config as ConfigDTL, 9 | } from '@testing-library/dom' 10 | import {act as reactDeprecatedAct} from 'react-dom/test-utils' 11 | //@ts-ignore 12 | import {act as reactAct} from 'react' 13 | 14 | export * from '@testing-library/dom' 15 | 16 | export interface Config extends ConfigDTL { 17 | reactStrictMode: boolean 18 | } 19 | 20 | export interface ConfigFn { 21 | (existingConfig: Config): Partial 22 | } 23 | 24 | export function configure(configDelta: ConfigFn | Partial): void 25 | 26 | export function getConfig(): Config 27 | 28 | export type RenderResult< 29 | Q extends Queries = typeof queries, 30 | Container extends RendererableContainer | HydrateableContainer = HTMLElement, 31 | BaseElement extends RendererableContainer | HydrateableContainer = Container, 32 | > = { 33 | container: Container 34 | baseElement: BaseElement 35 | debug: ( 36 | baseElement?: 37 | | RendererableContainer 38 | | HydrateableContainer 39 | | Array 40 | | undefined, 41 | maxLength?: number | undefined, 42 | options?: prettyFormat.OptionsReceived | undefined, 43 | ) => void 44 | rerender: (ui: React.ReactNode) => void 45 | unmount: () => void 46 | asFragment: () => DocumentFragment 47 | } & {[P in keyof Q]: BoundFunction} 48 | 49 | /** @deprecated */ 50 | export type BaseRenderOptions< 51 | Q extends Queries, 52 | Container extends RendererableContainer | HydrateableContainer, 53 | BaseElement extends RendererableContainer | HydrateableContainer, 54 | > = RenderOptions 55 | 56 | type RendererableContainer = ReactDOMClient.Container 57 | type HydrateableContainer = Parameters[0] 58 | /** @deprecated */ 59 | export interface ClientRenderOptions< 60 | Q extends Queries, 61 | Container extends RendererableContainer, 62 | BaseElement extends RendererableContainer = Container, 63 | > extends BaseRenderOptions { 64 | /** 65 | * If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side 66 | * rendering and use ReactDOM.hydrate to mount your components. 67 | * 68 | * @see https://testing-library.com/docs/react-testing-library/api/#hydrate) 69 | */ 70 | hydrate?: false | undefined 71 | } 72 | /** @deprecated */ 73 | export interface HydrateOptions< 74 | Q extends Queries, 75 | Container extends HydrateableContainer, 76 | BaseElement extends HydrateableContainer = Container, 77 | > extends BaseRenderOptions { 78 | /** 79 | * If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side 80 | * rendering and use ReactDOM.hydrate to mount your components. 81 | * 82 | * @see https://testing-library.com/docs/react-testing-library/api/#hydrate) 83 | */ 84 | hydrate: true 85 | } 86 | 87 | export interface RenderOptions< 88 | Q extends Queries = typeof queries, 89 | Container extends RendererableContainer | HydrateableContainer = HTMLElement, 90 | BaseElement extends RendererableContainer | HydrateableContainer = Container, 91 | > { 92 | /** 93 | * By default, React Testing Library will create a div and append that div to the document.body. Your React component will be rendered in the created div. If you provide your own HTMLElement container via this option, 94 | * it will not be appended to the document.body automatically. 95 | * 96 | * For example: If you are unit testing a `` element, it cannot be a child of a div. In this case, you can 97 | * specify a table as the render container. 98 | * 99 | * @see https://testing-library.com/docs/react-testing-library/api/#container 100 | */ 101 | container?: Container | undefined 102 | /** 103 | * Defaults to the container if the container is specified. Otherwise `document.body` is used for the default. This is used as 104 | * the base element for the queries as well as what is printed when you use `debug()`. 105 | * 106 | * @see https://testing-library.com/docs/react-testing-library/api/#baseelement 107 | */ 108 | baseElement?: BaseElement | undefined 109 | /** 110 | * If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side 111 | * rendering and use ReactDOM.hydrate to mount your components. 112 | * 113 | * @see https://testing-library.com/docs/react-testing-library/api/#hydrate) 114 | */ 115 | hydrate?: boolean | undefined 116 | /** 117 | * Only works if used with React 18. 118 | * Set to `true` if you want to force synchronous `ReactDOM.render`. 119 | * Otherwise `render` will default to concurrent React if available. 120 | */ 121 | legacyRoot?: boolean | undefined 122 | /** 123 | * Only supported in React 19. 124 | * Callback called when React catches an error in an Error Boundary. 125 | * Called with the error caught by the Error Boundary, and an `errorInfo` object containing the `componentStack`. 126 | * 127 | * @see {@link https://react.dev/reference/react-dom/client/createRoot#parameters createRoot#options} 128 | */ 129 | onCaughtError?: ReactDOMClient.RootOptions extends { 130 | onCaughtError: infer OnCaughtError 131 | } 132 | ? OnCaughtError 133 | : never 134 | /** 135 | * Callback called when React automatically recovers from errors. 136 | * Called with an error React throws, and an `errorInfo` object containing the `componentStack`. 137 | * Some recoverable errors may include the original error cause as `error.cause`. 138 | * 139 | * @see {@link https://react.dev/reference/react-dom/client/createRoot#parameters createRoot#options} 140 | */ 141 | onRecoverableError?: ReactDOMClient.RootOptions['onRecoverableError'] 142 | /** 143 | * Not supported at the moment 144 | */ 145 | onUncaughtError?: never 146 | /** 147 | * Queries to bind. Overrides the default set from DOM Testing Library unless merged. 148 | * 149 | * @see https://testing-library.com/docs/react-testing-library/api/#queries 150 | */ 151 | queries?: Q | undefined 152 | /** 153 | * Pass a React Component as the wrapper option to have it rendered around the inner element. This is most useful for creating 154 | * reusable custom render functions for common data providers. See setup for examples. 155 | * 156 | * @see https://testing-library.com/docs/react-testing-library/api/#wrapper 157 | */ 158 | wrapper?: React.JSXElementConstructor<{children: React.ReactNode}> | undefined 159 | /** 160 | * When enabled, is rendered around the inner element. 161 | * If defined, overrides the value of `reactStrictMode` set in `configure`. 162 | */ 163 | reactStrictMode?: boolean 164 | } 165 | 166 | type Omit = Pick> 167 | 168 | /** 169 | * Render into a container which is appended to document.body. It should be used with cleanup. 170 | */ 171 | export function render< 172 | Q extends Queries = typeof queries, 173 | Container extends RendererableContainer | HydrateableContainer = HTMLElement, 174 | BaseElement extends RendererableContainer | HydrateableContainer = Container, 175 | >( 176 | ui: React.ReactNode, 177 | options: RenderOptions, 178 | ): RenderResult 179 | export function render( 180 | ui: React.ReactNode, 181 | options?: Omit | undefined, 182 | ): RenderResult 183 | 184 | export interface RenderHookResult { 185 | /** 186 | * Triggers a re-render. The props will be passed to your renderHook callback. 187 | */ 188 | rerender: (props?: Props) => void 189 | /** 190 | * This is a stable reference to the latest value returned by your renderHook 191 | * callback 192 | */ 193 | result: { 194 | /** 195 | * The value returned by your renderHook callback 196 | */ 197 | current: Result 198 | } 199 | /** 200 | * Unmounts the test component. This is useful for when you need to test 201 | * any cleanup your useEffects have. 202 | */ 203 | unmount: () => void 204 | } 205 | 206 | /** @deprecated */ 207 | export type BaseRenderHookOptions< 208 | Props, 209 | Q extends Queries, 210 | Container extends RendererableContainer | HydrateableContainer, 211 | BaseElement extends Element | DocumentFragment, 212 | > = RenderHookOptions 213 | 214 | /** @deprecated */ 215 | export interface ClientRenderHookOptions< 216 | Props, 217 | Q extends Queries, 218 | Container extends Element | DocumentFragment, 219 | BaseElement extends Element | DocumentFragment = Container, 220 | > extends BaseRenderHookOptions { 221 | /** 222 | * If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side 223 | * rendering and use ReactDOM.hydrate to mount your components. 224 | * 225 | * @see https://testing-library.com/docs/react-testing-library/api/#hydrate) 226 | */ 227 | hydrate?: false | undefined 228 | } 229 | 230 | /** @deprecated */ 231 | export interface HydrateHookOptions< 232 | Props, 233 | Q extends Queries, 234 | Container extends Element | DocumentFragment, 235 | BaseElement extends Element | DocumentFragment = Container, 236 | > extends BaseRenderHookOptions { 237 | /** 238 | * If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side 239 | * rendering and use ReactDOM.hydrate to mount your components. 240 | * 241 | * @see https://testing-library.com/docs/react-testing-library/api/#hydrate) 242 | */ 243 | hydrate: true 244 | } 245 | 246 | export interface RenderHookOptions< 247 | Props, 248 | Q extends Queries = typeof queries, 249 | Container extends RendererableContainer | HydrateableContainer = HTMLElement, 250 | BaseElement extends RendererableContainer | HydrateableContainer = Container, 251 | > extends BaseRenderOptions { 252 | /** 253 | * The argument passed to the renderHook callback. Can be useful if you plan 254 | * to use the rerender utility to change the values passed to your hook. 255 | */ 256 | initialProps?: Props | undefined 257 | } 258 | 259 | /** 260 | * Allows you to render a hook within a test React component without having to 261 | * create that component yourself. 262 | */ 263 | export function renderHook< 264 | Result, 265 | Props, 266 | Q extends Queries = typeof queries, 267 | Container extends RendererableContainer | HydrateableContainer = HTMLElement, 268 | BaseElement extends RendererableContainer | HydrateableContainer = Container, 269 | >( 270 | render: (initialProps: Props) => Result, 271 | options?: RenderHookOptions | undefined, 272 | ): RenderHookResult 273 | 274 | /** 275 | * Unmounts React trees that were mounted with render. 276 | */ 277 | export function cleanup(): void 278 | 279 | /** 280 | * Simply calls React.act(cb) 281 | * If that's not available (older version of react) then it 282 | * simply calls the deprecated version which is ReactTestUtils.act(cb) 283 | */ 284 | // IfAny from https://stackoverflow.com/a/61626123/3406963 285 | export const act: 0 extends 1 & typeof reactAct 286 | ? typeof reactDeprecatedAct 287 | : typeof reactAct 288 | -------------------------------------------------------------------------------- /types/pure.d.ts: -------------------------------------------------------------------------------- 1 | export * from './' 2 | -------------------------------------------------------------------------------- /types/test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {render, fireEvent, screen, waitFor, renderHook} from '.' 3 | import * as pure from './pure' 4 | 5 | export async function testRender() { 6 | const view = render(