diff --git a/.all-contributorsrc b/.all-contributorsrc
index b0027a6f..b22c9414 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -1,12 +1,13 @@
{
"projectName": "react-testing-library",
- "projectOwner": "kentcdodds",
+ "projectOwner": "testing-library",
"repoType": "github",
"files": [
"README.md"
],
"imageSize": 100,
"commit": false,
+ "skipCi": false,
"contributors": [
{
"login": "kentcdodds",
@@ -313,6 +314,1077 @@
"contributions": [
"doc"
]
+ },
+ {
+ "login": "thesalah",
+ "name": "Salah",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/6624197?v=4",
+ "profile": "https://github.com/thesalah",
+ "contributions": [
+ "code",
+ "test"
+ ]
+ },
+ {
+ "login": "icfantv",
+ "name": "Adam Gordon",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/370054?v=4",
+ "profile": "http://gordonizer.com",
+ "contributions": [
+ "bug",
+ "code"
+ ]
+ },
+ {
+ "login": "silvenon",
+ "name": "Matija Marohnić",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/471278?v=4",
+ "profile": "https://silvenon.com",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "Dajust",
+ "name": "Justice Mba",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/8015514?v=4",
+ "profile": "https://github.com/Dajust",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "MarkPollmann",
+ "name": "Mark Pollmann",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/5286559?v=4",
+ "profile": "https://markpollmann.com/",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "ehteshamkafeel",
+ "name": "Ehtesham Kafeel",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/1213123?v=4",
+ "profile": "https://github.com/ehteshamkafeel",
+ "contributions": [
+ "code",
+ "doc"
+ ]
+ },
+ {
+ "login": "jpavon",
+ "name": "Julio Pavón",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/1493505?v=4",
+ "profile": "http://jpavon.com",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "duncanleung",
+ "name": "Duncan L",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/1765048?v=4",
+ "profile": "http://www.duncanleung.com/",
+ "contributions": [
+ "doc",
+ "example"
+ ]
+ },
+ {
+ "login": "tyagow",
+ "name": "Tiago Almeida",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/700778?v=4",
+ "profile": "https://www.linkedin.com/in/tyagow/?locale=en_US",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "rbrtsmith",
+ "name": "Robert Smith",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/4982001?v=4",
+ "profile": "http://rbrtsmith.com/",
+ "contributions": [
+ "bug"
+ ]
+ },
+ {
+ "login": "zgreen",
+ "name": "Zach Green",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/1700355?v=4",
+ "profile": "https://offbyone.tech",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "dadamssg",
+ "name": "dadamssg",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/881986?v=4",
+ "profile": "https://github.com/dadamssg",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "YazanAabeed",
+ "name": "Yazan Aabed",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/8734097?v=4",
+ "profile": "https://www.yaabed.com/",
+ "contributions": [
+ "blog"
+ ]
+ },
+ {
+ "login": "timbonicus",
+ "name": "Tim",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/556258?v=4",
+ "profile": "https://github.com/timbonicus",
+ "contributions": [
+ "bug",
+ "code",
+ "doc",
+ "test"
+ ]
+ },
+ {
+ "login": "divyanshu013",
+ "name": "Divyanshu Maithani",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/6682655?v=4",
+ "profile": "http://divyanshu.xyz",
+ "contributions": [
+ "tutorial",
+ "video"
+ ]
+ },
+ {
+ "login": "metagrover",
+ "name": "Deepak Grover",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/9116042?v=4",
+ "profile": "https://www.linkedin.com/in/metagrover",
+ "contributions": [
+ "tutorial",
+ "video"
+ ]
+ },
+ {
+ "login": "eyalcohen4",
+ "name": "Eyal Cohen",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/16276358?v=4",
+ "profile": "https://github.com/eyalcohen4",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "petermakowski",
+ "name": "Peter Makowski",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/7452681?v=4",
+ "profile": "https://github.com/petermakowski",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "Michielnuyts",
+ "name": "Michiel Nuyts",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/20361668?v=4",
+ "profile": "https://github.com/Michielnuyts",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "joeynimu",
+ "name": "Joe Ng'ethe",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/1195863?v=4",
+ "profile": "https://github.com/joeynimu",
+ "contributions": [
+ "code",
+ "doc"
+ ]
+ },
+ {
+ "login": "Enikol",
+ "name": "Kate",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/19998290?v=4",
+ "profile": "https://github.com/Enikol",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "SeanRParker",
+ "name": "Sean",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/11980217?v=4",
+ "profile": "http://www.seanrparker.com",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "jlongster",
+ "name": "James Long",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/17031?v=4",
+ "profile": "http://jlongster.com",
+ "contributions": [
+ "ideas",
+ "platform"
+ ]
+ },
+ {
+ "login": "hhagely",
+ "name": "Herb Hagely",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/10118777?v=4",
+ "profile": "https://github.com/hhagely",
+ "contributions": [
+ "example"
+ ]
+ },
+ {
+ "login": "themostcolm",
+ "name": "Alex Wendte",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/5779538?v=4",
+ "profile": "http://www.wendtedesigns.com/",
+ "contributions": [
+ "example"
+ ]
+ },
+ {
+ "login": "M0nica",
+ "name": "Monica Powell",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/6998954?v=4",
+ "profile": "http://www.aboutmonica.com",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "sivkoff",
+ "name": "Vitaly Sivkov",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/2699953?v=4",
+ "profile": "http://sivkoff.com",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "weyert",
+ "name": "Weyert de Boer",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/7049?v=4",
+ "profile": "https://github.com/weyert",
+ "contributions": [
+ "ideas",
+ "review",
+ "design"
+ ]
+ },
+ {
+ "login": "EstebanMarin",
+ "name": "EstebanMarin",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/13613037?v=4",
+ "profile": "https://github.com/EstebanMarin",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "vctormb",
+ "name": "Victor Martins",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/13953703?v=4",
+ "profile": "https://github.com/vctormb",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "RoystonS",
+ "name": "Royston Shufflebotham",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/19773?v=4",
+ "profile": "https://github.com/RoystonS",
+ "contributions": [
+ "bug",
+ "doc",
+ "example"
+ ]
+ },
+ {
+ "login": "chrbala",
+ "name": "chrbala",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/6834804?v=4",
+ "profile": "https://github.com/chrbala",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "donavon",
+ "name": "Donavon West",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/887639?v=4",
+ "profile": "http://donavon.com",
+ "contributions": [
+ "code",
+ "doc",
+ "ideas",
+ "test"
+ ]
+ },
+ {
+ "login": "maisano",
+ "name": "Richard Maisano",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/689081?v=4",
+ "profile": "https://github.com/maisano",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "marcobiedermann",
+ "name": "Marco Biedermann",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/5244986?v=4",
+ "profile": "https://www.marcobiedermann.com",
+ "contributions": [
+ "code",
+ "maintenance",
+ "test"
+ ]
+ },
+ {
+ "login": "alexzherdev",
+ "name": "Alex Zherdev",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/93752?v=4",
+ "profile": "https://github.com/alexzherdev",
+ "contributions": [
+ "bug",
+ "code"
+ ]
+ },
+ {
+ "login": "Andrewmat",
+ "name": "André Matulionis dos Santos",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/5133846?v=4",
+ "profile": "https://twitter.com/Andrewmat",
+ "contributions": [
+ "code",
+ "example",
+ "test"
+ ]
+ },
+ {
+ "login": "FredyC",
+ "name": "Daniel K.",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/1096340?v=4",
+ "profile": "https://github.com/FredyC",
+ "contributions": [
+ "bug",
+ "code",
+ "ideas",
+ "test",
+ "review"
+ ]
+ },
+ {
+ "login": "mohamedmagdy17593",
+ "name": "mohamedmagdy17593",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/40938625?v=4",
+ "profile": "https://github.com/mohamedmagdy17593",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "lorensr",
+ "name": "Loren ☺️",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/251288?v=4",
+ "profile": "http://lorensr.me",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "MarkFalconbridge",
+ "name": "MarkFalconbridge",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/20678943?v=4",
+ "profile": "https://github.com/MarkFalconbridge",
+ "contributions": [
+ "bug",
+ "code"
+ ]
+ },
+ {
+ "login": "viniciusavieira",
+ "name": "Vinicius",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/2073019?v=4",
+ "profile": "https://github.com/viniciusavieira",
+ "contributions": [
+ "doc",
+ "example"
+ ]
+ },
+ {
+ "login": "pschyma",
+ "name": "Peter Schyma",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/2489928?v=4",
+ "profile": "https://github.com/pschyma",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "ianschmitz",
+ "name": "Ian Schmitz",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/6355370?v=4",
+ "profile": "https://github.com/ianschmitz",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "joual",
+ "name": "Joel Marcotte",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/157877?v=4",
+ "profile": "https://github.com/joual",
+ "contributions": [
+ "bug",
+ "test",
+ "code"
+ ]
+ },
+ {
+ "login": "aledustet",
+ "name": "Alejandro Dustet",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/2413802?v=4",
+ "profile": "http://aledustet.com",
+ "contributions": [
+ "bug"
+ ]
+ },
+ {
+ "login": "bcarroll22",
+ "name": "Brandon Carroll",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/11020406?v=4",
+ "profile": "https://github.com/bcarroll22",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "lucas0707",
+ "name": "Lucas Machado",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/26284338?v=4",
+ "profile": "https://github.com/lucas0707",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "pascalduez",
+ "name": "Pascal Duez",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/335467?v=4",
+ "profile": "http://pascalduez.me",
+ "contributions": [
+ "platform"
+ ]
+ },
+ {
+ "login": "NMinhNguyen",
+ "name": "Minh Nguyen",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/2852660?v=4",
+ "profile": "https://twitter.com/minh_ngvyen",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "LiaoJimmy",
+ "name": "LiaoJimmy",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/11155585?v=4",
+ "profile": "http://iababy46.blogspot.tw/",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "threepointone",
+ "name": "Sunil Pai",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/18808?v=4",
+ "profile": "https://github.com/threepointone",
+ "contributions": [
+ "code",
+ "test"
+ ]
+ },
+ {
+ "login": "gaearon",
+ "name": "Dan Abramov",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/810438?v=4",
+ "profile": "http://twitter.com/dan_abramov",
+ "contributions": [
+ "review"
+ ]
+ },
+ {
+ "login": "ChristianMurphy",
+ "name": "Christian Murphy",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/3107513?v=4",
+ "profile": "https://github.com/ChristianMurphy",
+ "contributions": [
+ "infra"
+ ]
+ },
+ {
+ "login": "jeetiss",
+ "name": "Ivakhnenko Dmitry",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/6726016?v=4",
+ "profile": "https://jeetiss.github.io/",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "jamesgeorge007",
+ "name": "James George",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/25279263?v=4",
+ "profile": "https://ghuser.io/jamesgeorge007",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "JSFernandes",
+ "name": "João Fernandes",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/1075053?v=4",
+ "profile": "https://joaofernandes.me/",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "alejandroperea",
+ "name": "Alejandro Perea",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/6084749?v=4",
+ "profile": "https://github.com/alejandroperea",
+ "contributions": [
+ "review"
+ ]
+ },
+ {
+ "login": "nickmccurdy",
+ "name": "Nick McCurdy",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/927220?v=4",
+ "profile": "https://nickmccurdy.com/",
+ "contributions": [
+ "review",
+ "question",
+ "infra"
+ ]
+ },
+ {
+ "login": "eps1lon",
+ "name": "Sebastian Silbermann",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/12292047?v=4",
+ "profile": "https://twitter.com/sebsilbermann",
+ "contributions": [
+ "review"
+ ]
+ },
+ {
+ "login": "afontcu",
+ "name": "Adrià Fontcuberta",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/9197791?v=4",
+ "profile": "https://afontcu.dev",
+ "contributions": [
+ "review",
+ "doc"
+ ]
+ },
+ {
+ "login": "johnnyreilly",
+ "name": "John Reilly",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/1010525?v=4",
+ "profile": "https://blog.johnnyreilly.com/",
+ "contributions": [
+ "review"
+ ]
+ },
+ {
+ "login": "MichaelDeBoey",
+ "name": "Michaël De Boey",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/6643991?v=4",
+ "profile": "https://michaeldeboey.be",
+ "contributions": [
+ "review",
+ "code"
+ ]
+ },
+ {
+ "login": "cimbul",
+ "name": "Tim Yates",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/927923?v=4",
+ "profile": "https://cimbul.com",
+ "contributions": [
+ "review"
+ ]
+ },
+ {
+ "login": "eventualbuddha",
+ "name": "Brian Donovan",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/1938?v=4",
+ "profile": "https://github.com/eventualbuddha",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "JaysQubeXon",
+ "name": "Noam Gabriel Jacobson",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/18309230?v=4",
+ "profile": "https://github.com/JaysQubeXon",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "rvdkooy",
+ "name": "Ronald van der Kooij",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/4119960?v=4",
+ "profile": "https://github.com/rvdkooy",
+ "contributions": [
+ "test",
+ "code"
+ ]
+ },
+ {
+ "login": "aayushrajvanshi",
+ "name": "Aayush Rajvanshi",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/14968551?v=4",
+ "profile": "https://github.com/aayushrajvanshi",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "ely-alamillo",
+ "name": "Ely Alamillo",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/24350492?v=4",
+ "profile": "https://elyalamillo.com",
+ "contributions": [
+ "code",
+ "test"
+ ]
+ },
+ {
+ "login": "danieljcafonso",
+ "name": "Daniel Afonso",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/35337607?v=4",
+ "profile": "https://github.com/danieljcafonso",
+ "contributions": [
+ "code",
+ "test"
+ ]
+ },
+ {
+ "login": "LaurensBosscher",
+ "name": "Laurens Bosscher",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/13363196?v=4",
+ "profile": "http://www.laurensbosscher.nl",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "sakito21",
+ "name": "Sakito Mukai",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/15010907?v=4",
+ "profile": "https://twitter.com/__sakito__",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "tteke",
+ "name": "Türker Teke",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/12457162?v=4",
+ "profile": "http://turkerteke.com",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "zbrogz",
+ "name": "Zach Brogan",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/319162?v=4",
+ "profile": "http://linkedin.com/in/zachbrogan",
+ "contributions": [
+ "code",
+ "test"
+ ]
+ },
+ {
+ "login": "ryota-murakami",
+ "name": "Ryota Murakami",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/5501268?v=4",
+ "profile": "https://ryota-murakami.github.io/",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "hottmanmichael",
+ "name": "Michael Hottman",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/10534502?v=4",
+ "profile": "https://github.com/hottmanmichael",
+ "contributions": [
+ "ideas"
+ ]
+ },
+ {
+ "login": "stevenfitzpatrick",
+ "name": "Steven Fitzpatrick",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/23268855?v=4",
+ "profile": "https://github.com/stevenfitzpatrick",
+ "contributions": [
+ "bug"
+ ]
+ },
+ {
+ "login": "juangl",
+ "name": "Juan Je García",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/1887029?v=4",
+ "profile": "https://github.com/juangl",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "Ishaan28malik",
+ "name": "Championrunner",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/27343592?v=4",
+ "profile": "https://ghuser.io/Ishaan28malik",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "samtsai",
+ "name": "Sam Tsai",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/225526?v=4",
+ "profile": "https://github.com/samtsai",
+ "contributions": [
+ "code",
+ "test",
+ "doc"
+ ]
+ },
+ {
+ "login": "screendriver",
+ "name": "Christian Rackerseder",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/149248?v=4",
+ "profile": "https://www.echooff.dev",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "NiGhTTraX",
+ "name": "Andrei Picus",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/485061?v=4",
+ "profile": "https://github.com/NiGhTTraX",
+ "contributions": [
+ "bug",
+ "review"
+ ]
+ },
+ {
+ "login": "kettanaito",
+ "name": "Artem Zakharchenko",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/14984911?v=4",
+ "profile": "https://redd.one",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "michael-siek",
+ "name": "Michael",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/45568605?v=4",
+ "profile": "http://michaelsiek.com",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "2dubbing",
+ "name": "Braden Lee",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/15885679?v=4",
+ "profile": "http://2dubbing.tistory.com",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "kamranayub",
+ "name": "Kamran Ayub",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/563819?v=4",
+ "profile": "http://kamranicus.com/",
+ "contributions": [
+ "code",
+ "test"
+ ]
+ },
+ {
+ "login": "MatanBobi",
+ "name": "Matan Borenkraout",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/12711091?v=4",
+ "profile": "https://twitter.com/matanbobi",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "radar",
+ "name": "Ryan Bigg",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/2687?v=4",
+ "profile": "http://ryanbigg.com",
+ "contributions": [
+ "maintenance"
+ ]
+ },
+ {
+ "login": "antonhalim",
+ "name": "Anton Halim",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/10498035?v=4",
+ "profile": "https://antonhalim.com",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "artem-malko",
+ "name": "Artem Malko",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/1823689?v=4",
+ "profile": "http://artmalko.ru",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "ljosberinn",
+ "name": "Gerrit Alex",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/29307652?v=4",
+ "profile": "http://gerritalex.de",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "karthick3018",
+ "name": "Karthick Raja",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/47154512?v=4",
+ "profile": "https://github.com/karthick3018",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "theashraf",
+ "name": "Abdelrahman Ashraf",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/39750790?v=4",
+ "profile": "https://github.com/theashraf",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "lidoravitan",
+ "name": "Lidor Avitan",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/35113398?v=4",
+ "profile": "https://github.com/lidoravitan",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "ljharb",
+ "name": "Jordan Harband",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/45469?v=4",
+ "profile": "https://github.com/ljharb",
+ "contributions": [
+ "review",
+ "ideas"
+ ]
+ },
+ {
+ "login": "marcosvega91",
+ "name": "Marco Moretti",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/5365582?v=4",
+ "profile": "https://github.com/marcosvega91",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "sanchit121",
+ "name": "sanchit121",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/30828115?v=4",
+ "profile": "https://github.com/sanchit121",
+ "contributions": [
+ "bug",
+ "code"
+ ]
+ },
+ {
+ "login": "solufa",
+ "name": "Solufa",
+ "avatar_url": "https://avatars.githubusercontent.com/u/9402912?v=4",
+ "profile": "https://github.com/solufa",
+ "contributions": [
+ "bug",
+ "code"
+ ]
+ },
+ {
+ "login": "AriPerkkio",
+ "name": "Ari Perkkiö",
+ "avatar_url": "https://avatars.githubusercontent.com/u/14806298?v=4",
+ "profile": "https://codepen.io/ariperkkio/",
+ "contributions": [
+ "test"
+ ]
+ },
+ {
+ "login": "jhnns",
+ "name": "Johannes Ewald",
+ "avatar_url": "https://avatars.githubusercontent.com/u/781746?v=4",
+ "profile": "https://github.com/jhnns",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "anpaopao",
+ "name": "Angus J. Pope",
+ "avatar_url": "https://avatars.githubusercontent.com/u/44686792?v=4",
+ "profile": "https://github.com/anpaopao",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "leschdom",
+ "name": "Dominik Lesch",
+ "avatar_url": "https://avatars.githubusercontent.com/u/62334278?v=4",
+ "profile": "https://github.com/leschdom",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "ImADrafter",
+ "name": "Marcos Gómez",
+ "avatar_url": "https://avatars.githubusercontent.com/u/44379989?v=4",
+ "profile": "https://github.com/ImADrafter",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "akashshyamdev",
+ "name": "Akash Shyam",
+ "avatar_url": "https://avatars.githubusercontent.com/u/56759828?v=4",
+ "profile": "https://www.akashshyam.online/",
+ "contributions": [
+ "bug"
+ ]
+ },
+ {
+ "login": "fmeum",
+ "name": "Fabian Meumertzheim",
+ "avatar_url": "https://avatars.githubusercontent.com/u/4312191?v=4",
+ "profile": "https://hen.ne.ke",
+ "contributions": [
+ "code",
+ "bug"
+ ]
+ },
+ {
+ "login": "Nokel81",
+ "name": "Sebastian Malton",
+ "avatar_url": "https://avatars.githubusercontent.com/u/8225332?v=4",
+ "profile": "https://github.com/Nokel81",
+ "contributions": [
+ "bug",
+ "code"
+ ]
+ },
+ {
+ "login": "mboettcher",
+ "name": "Martin Böttcher",
+ "avatar_url": "https://avatars.githubusercontent.com/u/2325337?v=4",
+ "profile": "https://github.com/mboettcher",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "TkDodo",
+ "name": "Dominik Dorfmeister",
+ "avatar_url": "https://avatars.githubusercontent.com/u/1021430?v=4",
+ "profile": "http://tkdodo.eu",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "stephensauceda",
+ "name": "Stephen Sauceda",
+ "avatar_url": "https://avatars.githubusercontent.com/u/1017723?v=4",
+ "profile": "https://stephensauceda.com",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "cmdcolin",
+ "name": "Colin Diesh",
+ "avatar_url": "https://avatars.githubusercontent.com/u/6511937?v=4",
+ "profile": "http://cmdcolin.github.io",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "yinm",
+ "name": "Yusuke Iinuma",
+ "avatar_url": "https://avatars.githubusercontent.com/u/13295106?v=4",
+ "profile": "http://yinm.info",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "trappar",
+ "name": "Jeff Way",
+ "avatar_url": "https://avatars.githubusercontent.com/u/525726?v=4",
+ "profile": "https://github.com/trappar",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "bernardobelchior",
+ "name": "Bernardo Belchior",
+ "avatar_url": "https://avatars.githubusercontent.com/u/12778398?v=4",
+ "profile": "http://belchior.me",
+ "contributions": [
+ "code",
+ "doc"
+ ]
}
- ]
+ ],
+ "contributorsPerLine": 7,
+ "repoHost": "https://github.com",
+ "commitType": "docs",
+ "commitConvention": "angular"
}
diff --git a/.bundle.main.env b/.bundle.main.env
new file mode 100644
index 00000000..669fe7e4
--- /dev/null
+++ b/.bundle.main.env
@@ -0,0 +1,2 @@
+BUILD_GLOBALS={"react-dom/test-utils":"ReactTestUtils","react":"React","react-dom":"ReactDOM"}
+
diff --git a/.bundle.pure.env b/.bundle.pure.env
new file mode 100644
index 00000000..fed4df2e
--- /dev/null
+++ b/.bundle.pure.env
@@ -0,0 +1,3 @@
+BUILD_FILENAME_SUFFIX=.pure
+BUILD_INPUT=src/pure.js
+
diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json
new file mode 100644
index 00000000..002bafb4
--- /dev/null
+++ b/.codesandbox/ci.json
@@ -0,0 +1,5 @@
+{
+ "installCommand": "install:csb",
+ "sandboxes": ["new", "github/kentcdodds/react-testing-library-examples"],
+ "node": "18"
+}
diff --git a/.gitattributes b/.gitattributes
index 391f0a4e..6313b56c 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,2 +1 @@
-* text=auto
-*.js text eol=lf
+* text=auto eol=lf
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
index f6a0575d..496c8563 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
@@ -20,11 +20,10 @@ Thanks for your interest in the project. We appreciate bugs filed and PRs submit
❓ Questions:
For questions related to using the library, please visit a support community
- instead of filing an issue on GitHub.
- * React Spectrum
- https://spectrum.chat/react-testing-library
- * Reactiflux on Discord
- https://www.reactiflux.com
+ instead of filing an issue on GitHub. You can follow the instructions in this
+ codesandbox to make a reproduction of your issue: https://kcd.im/rtl-help
+ * Discord
+ https://discord.gg/testing-library
* Stack Overflow
https://stackoverflow.com/questions/tagged/react-testing-library
@@ -43,14 +42,26 @@ tutorial to learn how: http://kcd.im/pull-request
-->
-* `react-testing-library` version:
-* `react` version:
-* `node` version:
-* `npm` (or `yarn`) version:
+- `@testing-library/react` version:
+- Testing Framework and version:
+
+- DOM Environment:
+
+
+
Relevant code or config
```javascript
+
```
What you did:
@@ -63,7 +74,8 @@ Reproduction repository:
-* `react-testing-library` version:
-* `react` version:
-* `node` version:
-* `npm` (or `yarn`) version:
+- `@testing-library/react` version:
+- Testing Framework and version:
+
+- DOM Environment:
+
+
+
### Relevant code or config:
@@ -33,6 +44,11 @@ tutorial to learn how: http://kcd.im/pull-request
var your => (code) => here;
```
+
+
### What you did:
@@ -44,11 +60,8 @@ var your => (code) => here;
### Reproduction:
### Problem description:
diff --git a/.github/ISSUE_TEMPLATE/Question.md b/.github/ISSUE_TEMPLATE/Question.md
index 595d5278..e625486b 100644
--- a/.github/ISSUE_TEMPLATE/Question.md
+++ b/.github/ISSUE_TEMPLATE/Question.md
@@ -12,11 +12,13 @@ and feature requests so we recommend not using this medium to ask them here 😁
## ❓ Support Forums
-* React Spectrum
- https://spectrum.chat/react-testing-library
-* Reactiflux on Discord
- https://www.reactiflux.com
-* Stack Overflow
+For questions related to using the library, please visit a support community
+instead of filing an issue on GitHub. You can follow the instructions in this
+codesandbox to make a reproduction of your issue: https://kcd.im/rtl-help
+
+- Discord https://discord.gg/testing-library
+- Stack Overflow
https://stackoverflow.com/questions/tagged/react-testing-library
+- Documentation: https://github.com/testing-library/testing-library-docs
**ISSUES WHICH ARE QUESTIONS WILL BE CLOSED**
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index aa0dc2b8..f5000e21 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -34,9 +34,11 @@ merge of your pull request!
-* [ ] Documentation
-* [ ] Tests
-* [ ] Ready to be merged
-* [ ] Added myself to contributors table
+- [ ] Documentation added to the
+ [docs site](https://github.com/testing-library/testing-library-docs)
+- [ ] Tests
+- [ ] TypeScript definitions updated
+- [ ] Ready to be merged
+
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
new file mode 100644
index 00000000..f239c717
--- /dev/null
+++ b/.github/workflows/validate.yml
@@ -0,0 +1,117 @@
+name: validate
+on:
+ push:
+ branches:
+ # Match SemVer major release branches
+ # e.g. "12.x" or "8.x"
+ - '[0-9]+.x'
+ - 'main'
+ - 'next'
+ - 'next-major'
+ - 'beta'
+ - 'alpha'
+ - '!all-contributors/**'
+ pull_request:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ actions: write # to cancel/stop running workflows (styfle/cancel-workflow-action)
+ contents: read # to fetch code (actions/checkout)
+
+jobs:
+ main:
+ continue-on-error: ${{ matrix.react != 'latest' }}
+ # ignore all-contributors PRs
+ if: ${{ !contains(github.head_ref, 'all-contributors') }}
+ strategy:
+ fail-fast: false
+ matrix:
+ node: [18, 20]
+ react: ['18.x', latest, canary, experimental]
+ runs-on: ubuntu-latest
+ steps:
+ - name: ⬇️ Checkout repo
+ uses: actions/checkout@v4
+
+ - name: ⎔ Setup node
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ matrix.node }}
+
+ - name: 📥 Download deps
+ uses: bahmutov/npm-install@v1
+ with:
+ useLockFile: false
+
+ # TODO: Can be removed if https://github.com/kentcdodds/kcd-scripts/pull/146 is released
+ - name: Verify format (`npm run format` committed?)
+ run: npm run format -- --check --no-write
+
+ # as requested by the React team :)
+ # https://reactjs.org/blog/2019/10/22/react-release-channels.html#using-the-next-channel-for-integration-testing
+ - name: ⚛️ Setup react
+ run: npm install react@${{ matrix.react }} react-dom@${{ matrix.react }}
+
+ - name: ⚛️ Setup react types
+ if: ${{ matrix.react != 'canary' && matrix.react != 'experimental' }}
+ run:
+ npm install @types/react@${{ matrix.react }} @types/react-dom@${{
+ matrix.react }}
+
+ - name: ▶️ Run validate script
+ run: npm run validate
+
+ - name: ⬆️ Upload coverage report
+ uses: codecov/codecov-action@v5
+ with:
+ fail_ci_if_error: true
+ flags: ${{ matrix.react }}
+ token: ${{ secrets.CODECOV_TOKEN }}
+
+ release:
+ permissions:
+ actions: write # to cancel/stop running workflows (styfle/cancel-workflow-action)
+ contents: write # to create release tags (cycjimmy/semantic-release-action)
+ issues: write # to post release that resolves an issue (cycjimmy/semantic-release-action)
+
+ needs: main
+ runs-on: ubuntu-latest
+ if:
+ ${{ github.repository == 'testing-library/react-testing-library' &&
+ github.event_name == 'push' }}
+ steps:
+ - name: ⬇️ Checkout repo
+ uses: actions/checkout@v4
+
+ - name: ⎔ Setup node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 14
+
+ - name: 📥 Download deps
+ uses: bahmutov/npm-install@v1
+ with:
+ useLockFile: false
+
+ - name: 🏗 Run build script
+ run: npm run build
+
+ - name: 🚀 Release
+ uses: cycjimmy/semantic-release-action@v2
+ with:
+ semantic_version: 17
+ branches: |
+ [
+ '+([0-9])?(.{+([0-9]),x}).x',
+ 'main',
+ 'next',
+ 'next-major',
+ {name: 'beta', prerelease: true},
+ {name: 'alpha', prerelease: true}
+ ]
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 09048d22..8e0c70cb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,7 @@
node_modules
coverage
dist
-.opt-in
-.opt-out
.DS_Store
-.eslintcache
# these cause more harm than good
# when working with contributors
diff --git a/.huskyrc.js b/.huskyrc.js
new file mode 100644
index 00000000..5e45c45d
--- /dev/null
+++ b/.huskyrc.js
@@ -0,0 +1 @@
+module.exports = require('kcd-scripts/husky')
diff --git a/.npmrc b/.npmrc
index d2722898..1df2a6d8 100644
--- a/.npmrc
+++ b/.npmrc
@@ -1,2 +1,2 @@
-registry=http://registry.npmjs.org/
+registry=https://registry.npmjs.org/
package-lock=false
diff --git a/.prettierignore b/.prettierignore
index 30117ea2..9c628283 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,4 +1,3 @@
-package.json
node_modules
-dist
coverage
+dist
diff --git a/.prettierrc b/.prettierrc
deleted file mode 100644
index fb31ee19..00000000
--- a/.prettierrc
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "printWidth": 80,
- "tabWidth": 2,
- "useTabs": false,
- "semi": false,
- "singleQuote": true,
- "trailingComma": "all",
- "bracketSpacing": false,
- "jsxBracketSameLine": false
-}
diff --git a/.prettierrc.js b/.prettierrc.js
new file mode 100644
index 00000000..4679d9bf
--- /dev/null
+++ b/.prettierrc.js
@@ -0,0 +1 @@
+module.exports = require('kcd-scripts/prettier')
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 08be7ec0..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,13 +0,0 @@
-sudo: false
-language: node_js
-cache:
- directories:
- - ~/.npm
-notifications:
- email: false
-node_js: '8'
-install: npm install
-script: npm run validate
-after_success: kcd-scripts travis-after-success
-branches:
- only: master
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 06d221aa..2a675299 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,5 @@
# CHANGELOG
-The changelog is automatically updated using [semantic-release](https://github.com/semantic-release/semantic-release).
-You can see it on the [releases page](../../releases).
+The changelog is automatically updated using
+[semantic-release](https://github.com/semantic-release/semantic-release). You
+can see it on the [releases page](../../releases).
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index afe24327..47681ae0 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -2,73 +2,127 @@
## Our Pledge
-In the interest of fostering an open and welcoming environment, we as
-contributors and maintainers pledge to making participation in our project and
-our community a harassment-free experience for everyone, regardless of age, body
-size, disability, ethnicity, gender identity and expression, level of experience,
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
-## Our Standards
-
-Examples of behavior that contributes to creating a positive environment
-include:
-
-* Using welcoming and inclusive language
-* Being respectful of differing viewpoints and experiences
-* Gracefully accepting constructive criticism
-* Focusing on what is best for the community
-* Showing empathy towards other community members
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
-Examples of unacceptable behavior by participants include:
+## Our Standards
-* The use of sexualized language or imagery and unwelcome sexual attention or
- advances
-* Trolling, insulting/derogatory comments, and personal or political attacks
-* Public or private harassment
-* Publishing others' private information, such as a physical or electronic
- address, without explicit permission
-* Other conduct which could reasonably be considered inappropriate in a
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+- Demonstrating empathy and kindness toward other people
+- Being respectful of differing opinions, viewpoints, and experiences
+- Giving and gracefully accepting constructive feedback
+- Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+- Focusing on what is best not just for us as individuals, but for the overall
+ community
+
+Examples of unacceptable behavior include:
+
+- The use of sexualized language or imagery, and sexual attention or advances of
+ any kind
+- Trolling, insulting or derogatory comments, and personal or political attacks
+- Public or private harassment
+- Publishing others' private information, such as a physical or email address,
+ without their explicit permission
+- Other conduct which could reasonably be considered inappropriate in a
professional setting
-## Our Responsibilities
+## Enforcement Responsibilities
-Project maintainers are responsible for clarifying the standards of acceptable
-behavior and are expected to take appropriate and fair corrective action in
-response to any instances of unacceptable behavior.
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
-Project maintainers have the right and responsibility to remove, edit, or
-reject comments, commits, code, wiki edits, issues, and other contributions
-that are not aligned to this Code of Conduct, or to ban temporarily or
-permanently any contributor for other behaviors that they deem inappropriate,
-threatening, offensive, or harmful.
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
## Scope
-This Code of Conduct applies both within project spaces and in public spaces
-when an individual is representing the project or its community. Examples of
-representing a project or community include using an official project e-mail
-address, posting via an official social media account, or acting as an appointed
-representative at an online or offline event. Representation of a project may be
-further defined and clarified by project maintainers.
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
-reported by contacting the project team at kent+coc@doddsfamily.us. All
-complaints will be reviewed and investigated and will result in a response that
-is deemed necessary and appropriate to the circumstances. The project team is
-obligated to maintain confidentiality with regard to the reporter of an incident.
-Further details of specific enforcement policies may be posted separately.
+reported to the community leaders responsible for enforcement at
+me+coc@kentcdodds.com. All complaints will be reviewed and investigated promptly
+and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
-Project maintainers who do not follow or enforce the Code of Conduct in good
-faith may face temporary or permanent repercussions as determined by other
-members of the project's leadership.
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series of
+actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or permanent
+ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within the
+community.
## Attribution
-This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
-available at [http://contributor-covenant.org/version/1/4][version]
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.0, available at
+https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by
+[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
+
+[homepage]: https://www.contributor-covenant.org
-[homepage]: http://contributor-covenant.org
-[version]: http://contributor-covenant.org/version/1/4/
+For answers to common questions about this code of conduct, see the FAQ at
+https://www.contributor-covenant.org/faq. Translations are available at
+https://www.contributor-covenant.org/translations.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2cb6bdbf..e16e9d61 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,8 +2,8 @@
Thanks for being willing to contribute!
-**Working on your first Pull Request?** You can learn how from this _free_ series
-[How to Contribute to an Open Source Project on GitHub][egghead]
+**Working on your first Pull Request?** You can learn how from this _free_
+series [How to Contribute to an Open Source Project on GitHub][egghead]
## Project setup
@@ -11,54 +11,36 @@ Thanks for being willing to contribute!
2. Run `npm run setup -s` to install dependencies and run validation
3. Create a branch for your PR with `git checkout -b pr/your-branch-name`
-> Tip: Keep your `master` branch pointing at the original repository and make
-> pull requests from branches on your fork. To do this, run:
+> Tip: Keep your `main` branch pointing at the original repository and make pull
+> requests from branches on your fork. To do this, run:
>
> ```
-> git remote add upstream https://github.com/kentcdodds/react-testing-library.git
+> git remote add upstream https://github.com/testing-library/react-testing-library.git
> git fetch upstream
-> git branch --set-upstream-to=upstream/master master
+> git branch --set-upstream-to=upstream/main main
> ```
>
-> This will add the original repository as a "remote" called "upstream,"
-> Then fetch the git information from that remote, then set your local `master`
-> branch to use the upstream master branch whenever you run `git pull`.
-> Then you can make all of your pull request branches based on this `master`
-> branch. Whenever you want to update your version of `master`, do a regular
-> `git pull`.
-
-## Add yourself as a contributor
-
-This project follows the [all contributors][all-contributors] specification.
-To add yourself to the table of contributors on the `README.md`, please use the
-automated script as part of your PR:
-
-```console
-npm run add-contributor
-```
-
-Follow the prompt and commit `.all-contributorsrc` and `README.md` in the PR.
-If you've already added yourself to the list and are making
-a new type of contribution, you can run it again and select the added
-contribution type.
+> This will add the original repository as a "remote" called "upstream," Then
+> fetch the git information from that remote, then set your local `main` branch
+> to use the upstream main branch whenever you run `git pull`. Then you can make
+> all of your pull request branches based on this `main` branch. Whenever you
+> want to update your version of `main`, do a regular `git pull`.
## Committing and Pushing changes
Please make sure to run the tests before you commit your changes. You can run
-`npm run test:update` which will update any snapshots that need updating.
-Make sure to include those changes (if they exist) in your commit.
-
-### opt into git hooks
+`npm run test:update` which will update any snapshots that need updating. Make
+sure to include those changes (if they exist) in your commit.
-There are git hooks set up with this project that are automatically installed
-when you install dependencies. They're really handy, but are turned off by
-default (so as to not hinder new contributors). You can opt into these by
-creating a file called `.opt-in` at the root of the project and putting this
-inside:
+### Update Typings
-```
-pre-commit
-```
+If your PR introduced some changes in the API, you are more than welcome to
+modify the TypeScript type definition to reflect those changes. Just modify the
+`/types/index.d.ts` file accordingly. If you have never seen TypeScript
+definitions before, you can read more about it in its
+[documentation pages](https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html).
+Though this library itself is not written in TypeScript we use
+[dtslint](https://github.com/microsoft/dtslint) to lint our typings.
## Help needed
@@ -67,6 +49,6 @@ Please checkout the [the open issues][issues]
Also, please watch the repo and respond to questions/bug reports/feature
requests! Thanks!
-[egghead]: https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github
-[all-contributors]: https://github.com/kentcdodds/all-contributors
-[issues]: https://github.com/kentcdodds/react-testing-library/issues
+[egghead]:
+ https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github
+[issues]: https://github.com/testing-library/react-testing-library/issues
diff --git a/LICENSE b/LICENSE
index 4c43675b..ca399d57 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,5 +1,5 @@
The MIT License (MIT)
-Copyright (c) 2017 Kent C. Dodds
+Copyright (c) 2017-Present Kent C. Dodds
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 5120ef3b..7e18d5dd 100644
--- a/README.md
+++ b/README.md
@@ -1,893 +1,401 @@
-
react-testing-library
+
React Testing Library
-
+
-
Simple and complete React DOM testing utilities that encourage good testing practices.
+
Simple and complete React DOM testing utilities that encourage good testing
+practices.
+
+
+
+[**Read The Docs**](https://testing-library.com/react) |
+[Edit the docs](https://github.com/testing-library/testing-library-docs)
+
+
+
[![Build Status][build-badge]][build]
[![Code Coverage][coverage-badge]][coverage]
[![version][version-badge]][package]
[![downloads][downloads-badge]][npmtrends]
[![MIT License][license-badge]][license]
-
-[](#contributors)
+[![All Contributors][all-contributors-badge]](#contributors)
[![PRs Welcome][prs-badge]][prs]
[![Code of Conduct][coc-badge]][coc]
-[![Join the community on Spectrum][spectrum-badge]][spectrum]
+[![Discord][discord-badge]][discord]
[![Watch on GitHub][github-watch-badge]][github-watch]
[![Star on GitHub][github-star-badge]][github-star]
[![Tweet][twitter-badge]][twitter]
+
-## The problem
-
-You want to write maintainable tests for your React components. As a part of
-this goal, you want your tests to avoid including implementation details of
-your components and rather focus on making your tests give you the confidence
-for which they are intended. As part of this, you want your testbase to be
-maintainable in the long run so refactors of your components (changes to
-implementation but not functionality) don't break your tests and slow you and
-your team down.
-
-## This solution
-
-The `react-testing-library` is a very light-weight solution for testing React
-components. It provides light utility functions on top of `react-dom` and
-`react-dom/test-utils`, in a way that encourages better testing practices.
-It's primary guiding principle is:
-
-> [The more your tests resemble the way your software is used, the more confidence they can give you.][guiding-principle]
-
-So rather than dealing with instances of rendered react components, your tests
-will work with actual DOM nodes. The utilities this library provides facilitate
-querying the DOM in the same way the user would. Finding for elements by their
-label text (just like a user would), finding links and buttons from their text
-(like a user would). It also exposes a recommended way to find elements by a
-`data-testid` as an "escape hatch" for elements where the text content and label
-do not make sense or is not practical.
-
-This library encourages your applications to be more accessible and allows you
-to get your tests closer to using your components the way a user will, which
-allows your tests to give you more confidence that your application will work
-when a real user uses it.
-
-This library is a replacement for [enzyme](http://airbnb.io/enzyme/). While you
-_can_ follow these guidelines using enzyme itself, enforcing this is harder
-because of all the extra utilities that enzyme provides (utilities which
-facilitate testing implementation details). Read more about this in
-[the FAQ](#faq) below.
-
-**What this library is not**:
-
-1. A test runner or framework
-2. Specific to a testing framework (though we recommend Jest as our
- preference, the library works with any framework)
-
-> NOTE: This library is built on top of
-> [`dom-testing-library`](https://github.com/kentcdodds/dom-testing-library)
-> which is where most of the logic behind the queries is.
+
## Table of Contents
-
-* [Installation](#installation)
-* [Usage](#usage)
- * [`render`](#render)
- * [`renderIntoDocument`](#renderintodocument)
- * [`cleanup`](#cleanup)
- * [`Simulate`](#simulate)
- * [`wait`](#wait)
- * [`waitForElement`](#waitforelement)
- * [`fireEvent(node: HTMLElement, event: Event)`](#fireeventnode-htmlelement-event-event)
- * [`prettyDOM`](#prettydom)
-* [`TextMatch`](#textmatch)
-* [`query` APIs](#query-apis)
-* [`queryAll` and `getAll` APIs](#queryall-and-getall-apis)
-* [Examples](#examples)
-* [Learning Material](#learning-material)
-* [FAQ](#faq)
-* [Other Solutions](#other-solutions)
-* [Guiding Principles](#guiding-principles)
-* [Contributors](#contributors)
-* [Issues](#issues)
- * [🐛 Bugs](#-bugs)
- * [💡 Feature Requests](#-feature-requests)
- * [❓ Questions](#-questions)
-* [LICENSE](#license)
+- [The problem](#the-problem)
+- [The solution](#the-solution)
+- [Installation](#installation)
+ - [Suppressing unnecessary warnings on React DOM 16.8](#suppressing-unnecessary-warnings-on-react-dom-168)
+- [Examples](#examples)
+ - [Basic Example](#basic-example)
+ - [Complex Example](#complex-example)
+ - [More Examples](#more-examples)
+- [Hooks](#hooks)
+- [Guiding Principles](#guiding-principles)
+- [Docs](#docs)
+- [Issues](#issues)
+ - [🐛 Bugs](#-bugs)
+ - [💡 Feature Requests](#-feature-requests)
+ - [❓ Questions](#-questions)
+- [Contributors](#contributors)
+- [LICENSE](#license)
-## Installation
-
-This module is distributed via [npm][npm] which is bundled with [node][node] and
-should be installed as one of your project's `devDependencies`:
-
-```
-npm install --save-dev react-testing-library
-```
-
-This library has a `peerDependencies` listing for `react-dom`.
-
-You may also be interested in installing `jest-dom` so you can use
-[the custom jest matchers](https://github.com/gnapse/jest-dom#readme)
-
-## Usage
-
-```javascript
-// __tests__/fetch.js
-import React from 'react'
-import {render, Simulate, wait} from 'react-testing-library'
-// this add custom jest matchers from jest-dom
-import 'jest-dom/extend-expect'
-import axiosMock from 'axios' // the mock lives in a __mocks__ directory
-import Fetch from '../fetch' // see the tests for a full implementation
-
-test('Fetch makes an API call and displays the greeting when load-greeting is clicked', async () => {
- // Arrange
- axiosMock.get.mockResolvedValueOnce({data: {greeting: 'hello there'}})
- const url = '/greeting'
- const {getByText, getByTestId, container} = render( )
-
- // Act
- Simulate.click(getByText('Load Greeting'))
-
- // let's wait for our mocked `get` request promise to resolve
- // wait will wait until the callback doesn't throw an error
- await wait(() => getByTestId('greeting-text'))
-
- // Assert
- expect(axiosMock.get).toHaveBeenCalledTimes(1)
- expect(axiosMock.get).toHaveBeenCalledWith(url)
- expect(getByTestId('greeting-text')).toHaveTextContent('hello there')
- expect(getByTestId('ok-button')).toHaveAttribute('disabled')
- // snapshots work great with regular DOM nodes!
- expect(container.firstChild).toMatchSnapshot()
-})
-```
-
-### `render`
-
-In the example above, the `render` method returns an object that has a few
-properties:
+## The problem
-#### `container`
+You want to write maintainable tests for your React components. As a part of
+this goal, you want your tests to avoid including implementation details of your
+components and rather focus on making your tests give you the confidence for
+which they are intended. As part of this, you want your testbase to be
+maintainable in the long run so refactors of your components (changes to
+implementation but not functionality) don't break your tests and slow you and
+your team down.
-The containing DOM node of your rendered React Element (rendered using
-`ReactDOM.render`). It's a `div`. This is a regular DOM node, so you can call
-`container.querySelector` etc. to inspect the children.
+## The solution
-> Tip: To get the root element of your rendered element, use `container.firstChild`.
->
-> NOTE: When that root element is a
-> [React Fragment](https://reactjs.org/docs/fragments.html), `container.firstChild`
-> will only get the first child of that Fragment, not the Fragment itself.
+The `React Testing Library` is a very lightweight solution for testing React
+components. It provides light utility functions on top of `react-dom` and
+`react-dom/test-utils`, in a way that encourages better testing practices. Its
+primary guiding principle is:
-#### `debug`
+> [The more your tests resemble the way your software is used, the more
+> confidence they can give you.][guiding-principle]
-This method is a shortcut for `console.log(prettyDOM(container))`.
+## Installation
-```javascript
-import {render} from 'react-testing-library'
+This module is distributed via [npm][npm] which is bundled with [node][node] and
+should be installed as one of your project's `devDependencies`.
+Starting from RTL version 16, you'll also need to install
+`@testing-library/dom`:
-const HelloWorld = () => Hello World
-const {debug} = render( )
-debug()
-//
-//
Hello World
-//
```
-
-Learn more about [`prettyDOM`](#prettydom) below.
-
-#### `rerender`
-
-It'd probably be better if you test the component that's doing the prop updating
-to ensure that the props are being updated correctly (see
-[the Guiding Principles section](#guiding-principles)). That said, if you'd
-prefer to update the props of a rendered component in your test, this function
-can be used to update props of the rendered component.
-
-```javascript
-import {render} from 'react-testing-library'
-
-const {rerender} = render( )
-
-// re-render the same component with different props
-rerender( )
+npm install --save-dev @testing-library/react @testing-library/dom
```
-[Open the tests](https://github.com/kentcdodds/react-testing-library/blob/master/examples/__tests__/update-props.js)
-for a full example of this.
-
-#### `unmount`
+or
-This will cause the rendered component to be unmounted. This is useful for
-testing what happens when your component is removed from the page (like testing
-that you don't leave event handlers hanging around causing memory leaks).
+for installation via [yarn][yarn]
-> This method is a pretty small abstraction over
-> `ReactDOM.unmountComponentAtNode`
-
-```javascript
-import {render} from 'react-testing-library'
-
-const {container, unmount} = render( )
-unmount()
-// your component has been unmounted and now: container.innerHTML === ''
```
-
-#### `getByLabelText(text: TextMatch, options: {selector: string = '*'}): HTMLElement`
-
-This will search for the label that matches the given [`TextMatch`](#textmatch),
-then find the element associated with that label.
-
-```javascript
-import {render} from 'react-testing-library'
-
-const {getByLabelText} = render( )
-const inputNode = getByLabelText('Username')
-
-// this would find the input node for the following DOM structures:
-// The "for" attribute (NOTE: in JSX with React you'll write "htmlFor" rather than "for")
-// Username
-//
-//
-// The aria-labelledby attribute
-// Username
-//
-//
-// Wrapper labels
-// Username
-//
-// It will NOT find the input node for this:
-// Username
-//
-// For this case, you can provide a `selector` in the options:
-const inputNode = getByLabelText('username', {selector: 'input'})
-// and that would work
-// Note that will also work, but take
-// care because this is not a label that users can see on the page. So
-// the purpose of your input should be obvious for those users.
+yarn add --dev @testing-library/react @testing-library/dom
```
-> Note: This method will throw an error if it cannot find the node. If you don't
-> want this behavior (for example you wish to assert that it doesn't exist),
-> then use `queryByLabelText` instead.
+This library has `peerDependencies` listings for `react`, `react-dom` and
+starting from RTL version 16 also `@testing-library/dom`.
-#### `getByPlaceholderText(text: TextMatch): HTMLElement`
+_React Testing Library versions 13+ require React v18. If your project uses an
+older version of React, be sure to install version 12:_
-This will search for all elements with a placeholder attribute and find one
-that matches the given [`TextMatch`](#textmatch).
-
-```javascript
-import {render} from 'react-testing-library'
-
-const {getByPlaceholderText} = render( )
-const inputNode = getByPlaceholderText('Username')
```
+npm install --save-dev @testing-library/react@12
-> NOTE: a placeholder is not a good substitute for a label so you should
-> generally use `getByLabelText` instead.
-
-#### `getByText(text: TextMatch): HTMLElement`
-
-This will search for all elements that have a text node with `textContent`
-matching the given [`TextMatch`](#textmatch).
-```javascript
-import {render} from 'react-testing-library'
-
-const {getByText} = render(About ℹ️ )
-const aboutAnchorNode = getByText('about')
+yarn add --dev @testing-library/react@12
```
-#### `getByAltText(text: TextMatch): HTMLElement`
+You may also be interested in installing `@testing-library/jest-dom` so you can
+use [the custom jest matchers](https://github.com/testing-library/jest-dom).
-This will return the element (normally an ` `) that has the given `alt`
-text. Note that it only supports elements which accept an `alt` attribute:
-[` `](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img),
-[` `](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input),
-and [` `](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/area)
-(intentionally excluding [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/applet) as it's deprecated).
+> [**Docs**](https://testing-library.com/react)
-```javascript
-import {render} from 'react-testing-library'
+### Suppressing unnecessary warnings on React DOM 16.8
-const {getByAltText} = render(
- ,
-)
-const incrediblesPosterImg = getByAltText(/incredibles.*poster$/i)
-```
+There is a known compatibility issue with React DOM 16.8 where you will see the
+following warning:
-#### `getByTestId(text: TextMatch): HTMLElement`
-
-A shortcut to `` container.querySelector(`[data-testid="${yourId}"]`) `` (and it
-also accepts a [`TextMatch`](#textmatch)).
-
-```javascript
-import {render} from 'react-testing-library'
-
-const {getByTestId} = render( )
-const usernameInputElement = getByTestId('username-input')
```
-
-> In the spirit of [the guiding principles](#guiding-principles), it is
-> recommended to use this only after `getByLabel`, `getByPlaceholderText` or
-> `getByText` don't work for your use case. Using data-testid attributes do
-> not resemble how your software is used and should be avoided if possible.
-> That said, they are _way_ better than querying based on DOM structure.
-> Learn more about `data-testid`s from the blog post
-> ["Making your UI tests resilient to change"][data-testid-blog-post]
-
-### `renderIntoDocument`
-
-Render into `document.body`. Should be used with [cleanup](#cleanup).
-`renderIntoDocument` will return the same object as [render](#render)
-
-```javascript
-import {renderIntoDocument} from 'react-testing-library'
-
-renderIntoDocument(
)
+Warning: An update to ComponentName inside a test was not wrapped in act(...).
```
-### `cleanup`
+If you cannot upgrade to React DOM 16.9, you may suppress the warnings by adding
+the following snippet to your test configuration
+([learn more](https://github.com/testing-library/react-testing-library/issues/281)):
-Unmounts React trees that were mounted with [renderIntoDocument](#renderintodocument).
-
-```javascript
-import {cleanup, renderIntoDocument} from 'react-testing-library'
-
-afterEach(cleanup)
+```js
+// this is just a little hack to silence a warning that we'll get until we
+// upgrade to 16.9. See also: https://github.com/facebook/react/pull/14853
+const originalError = console.error
+beforeAll(() => {
+ console.error = (...args) => {
+ if (/Warning.*not wrapped in act/.test(args[0])) {
+ return
+ }
+ originalError.call(console, ...args)
+ }
+})
-test('renders into document', () => {
- renderIntoDocument(
)
- // ...
+afterAll(() => {
+ console.error = originalError
})
```
-Failing to call `cleanup` when you've called `renderIntoDocument` could
-result in a memory leak and tests which are not `idempotent` (which can
-lead to difficult to debug errors in your tests).
-
-### `Simulate`
-
-This is simply a re-export from the `Simulate` utility from
-`react-dom/test-utils`. See [the docs](https://reactjs.org/docs/test-utils.html#simulate).
+## Examples
-Note: `Simulate` does not simulate _browser_ events, meaning if you have an element like
+### Basic Example
+
+```jsx
+// hidden-message.js
+import * as React from 'react'
+
+// NOTE: React Testing Library works well with React Hooks and classes.
+// Your tests will be the same regardless of how you write your components.
+function HiddenMessage({children}) {
+ const [showMessage, setShowMessage] = React.useState(false)
+ return (
+
+ Show Message
+ setShowMessage(e.target.checked)}
+ checked={showMessage}
+ />
+ {showMessage ? children : null}
+
+ )
+}
-```javascript
-Submit
+export default HiddenMessage
```
-calling `Simulate.click` will not cause the submit event to be invoked. In order to get around this and for more info, see [`fireEvent`](#fireeventnode-htmlelement-event-event).
-
-In general, it is better to use `fireEvent` whenever possible because it mimics more closely what happens in the browser when an event happens.
+```jsx
+// __tests__/hidden-message.js
+// these imports are something you'd normally configure Jest to import for you
+// automatically. Learn more in the setup docs: https://testing-library.com/docs/react-testing-library/setup#cleanup
+import '@testing-library/jest-dom'
+// NOTE: jest-dom adds handy assertions to Jest and is recommended, but not required
-### `wait`
+import * as React from 'react'
+import {render, fireEvent, screen} from '@testing-library/react'
+import HiddenMessage from '../hidden-message'
-Defined as:
-
-```typescript
-function wait(
- callback?: () => void,
- options?: {
- timeout?: number
- interval?: number
- },
-): Promise
-```
+test('shows the children when the checkbox is checked', () => {
+ const testMessage = 'Test Message'
+ render({testMessage} )
-When in need to wait for non-deterministic periods of time you can use `wait`,
-to wait for your expectations to pass. The `wait` function is a small wrapper
-around the
-[`wait-for-expect`](https://github.com/TheBrainFamily/wait-for-expect) module.
-Here's a simple example:
+ // query* functions will return the element or null if it cannot be found
+ // get* functions will return the element or throw an error if it cannot be found
+ expect(screen.queryByText(testMessage)).toBeNull()
-```javascript
-import {render, wait} from 'react-testing-library'
+ // the queries can accept a regex to make your selectors more resilient to content tweaks and changes.
+ fireEvent.click(screen.getByLabelText(/show/i))
-test('waiting for an expectation to pass before proceeding', async () => {
- const {getByLabelText} = render( )
-
- // wait until the callback does not throw an error. In this case, that means
- // it'll wait until we can get a form control with a label that matches "username"
- await wait(() => getByLabelText('username'))
- getByLabelText('username').value = 'chucknorris'
+ // .toBeInTheDocument() is an assertion that comes from jest-dom
+ // otherwise you could use .toBeDefined()
+ expect(screen.getByText(testMessage)).toBeInTheDocument()
})
```
-This can be useful if you have a unit test that mocks API calls and you need
-to wait for your mock promises to all resolve. This can also be useful when
-(for example) you integration test your apollo-connected react components that
-go a couple level deep, with queries fired up in consequent components.
-
-The default `callback` is a no-op function (used like `await wait()`). This can
-be helpful if you only need to wait for one tick of the event loop.
-
-The default `timeout` is `4500ms` which will keep you under
-[Jest's default timeout of `5000ms`](https://facebook.github.io/jest/docs/en/jest-object.html#jestsettimeouttimeout).
-
-The default `interval` is `50ms`. However it will run your callback immediately
-on the next tick of the event loop (in a `setTimeout`) before starting the
-intervals.
-
-> NOTE: `wait`'s callback can be called many times and because of this you should use as few `wait` calls as possible in each test and put minimum amount of code that absolutely needs to be waited on inside each `wait`'s `callback`. This will help your tests run faster and can avoid unnecessary runtime complexity.
-
-### `waitForElement`
-
-See [dom-testing-library#waitForElement](https://github.com/kentcdodds/dom-testing-library#waitforelement)
-
-```js
-import {render, waitForElement} from 'react-testing-library'
-
-test('waiting for an element', async () => {
- const {getByText} = render( )
+### Complex Example
+
+```jsx
+// login.js
+import * as React from 'react'
+
+function Login() {
+ const [state, setState] = React.useReducer((s, a) => ({...s, ...a}), {
+ resolved: false,
+ loading: false,
+ error: null,
+ })
+
+ function handleSubmit(event) {
+ event.preventDefault()
+ const {usernameInput, passwordInput} = event.target.elements
+
+ setState({loading: true, resolved: false, error: null})
+
+ window
+ .fetch('/api/login', {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({
+ username: usernameInput.value,
+ password: passwordInput.value,
+ }),
+ })
+ .then(r => r.json().then(data => (r.ok ? data : Promise.reject(data))))
+ .then(
+ user => {
+ setState({loading: false, resolved: true, error: null})
+ window.localStorage.setItem('token', user.token)
+ },
+ error => {
+ setState({loading: false, resolved: false, error: error.message})
+ },
+ )
+ }
+
+ return (
+
+
+ {state.error ?
{state.error}
: null}
+ {state.resolved ? (
+
Congrats! You're signed in!
+ ) : null}
+
+ )
+}
+
+export default Login
+```
+
+```jsx
+// __tests__/login.js
+// again, these first two imports are something you'd normally handle in
+// your testing framework configuration rather than importing them in every file.
+import '@testing-library/jest-dom'
+import * as React from 'react'
+// import API mocking utilities from Mock Service Worker.
+import {rest} from 'msw'
+import {setupServer} from 'msw/node'
+// import testing utilities
+import {render, fireEvent, screen} from '@testing-library/react'
+import Login from '../login'
+
+const fakeUserResponse = {token: 'fake_user_token'}
+const server = setupServer(
+ rest.post('/api/login', (req, res, ctx) => {
+ return res(ctx.json(fakeUserResponse))
+ }),
+)
- await waitForElement(() => getByText('Search'))
+beforeAll(() => server.listen())
+afterEach(() => {
+ server.resetHandlers()
+ window.localStorage.removeItem('token')
})
-```
-
-
-
- Example
-
-
-```diff
-import {render, Simulate, waitForElement} from 'react-testing-library'
-
-test('should submit form when valid', async () => {
- const mockSubmit = jest.fn()
- const {
- container,
- getByLabelText,
- getByText
- } = render()
- const nameInput = getByLabelText('Name')
- nameInput.value = 'Chewbacca'
- Simulate.change(nameInput)
-+ // wait for button to appear and click it
-+ const submitButton = await waitForElement(() => getByText('Search'))
-+ Simulate.click(submitButton)
-+ expect(mockSubmit).toBeCalled()
+afterAll(() => server.close())
+
+test('allows the user to login successfully', async () => {
+ render( )
+
+ // fill out the form
+ fireEvent.change(screen.getByLabelText(/username/i), {
+ target: {value: 'chuck'},
+ })
+ fireEvent.change(screen.getByLabelText(/password/i), {
+ target: {value: 'norris'},
+ })
+
+ fireEvent.click(screen.getByText(/submit/i))
+
+ // just like a manual tester, we'll instruct our test to wait for the alert
+ // to show up before continuing with our assertions.
+ const alert = await screen.findByRole('alert')
+
+ // .toHaveTextContent() comes from jest-dom's assertions
+ // otherwise you could use expect(alert.textContent).toMatch(/congrats/i)
+ // but jest-dom will give you better error messages which is why it's recommended
+ expect(alert).toHaveTextContent(/congrats/i)
+ expect(window.localStorage.getItem('token')).toEqual(fakeUserResponse.token)
})
-```
-
-
-
-### `fireEvent(node: HTMLElement, event: Event)`
-
-Fire DOM events.
-React attaches an event handler on the `document` and handles some DOM events
-via event delegation (events bubbling up from a `target` to an ancestor). Because
-of this, your `node` must be in the `document.body` for `fireEvent` to work with
-React. You can render into the document using the
-[renderIntoDocument](#renderintodocument) utility. This is an alternative to
-simulating Synthetic React Events via [Simulate](#simulate). The benefit of
-using `fireEvent` over `Simulate` is that you are testing real DOM events
-instead of Synthetic Events. This aligns better with
-[the Guiding Principles](#guiding-principles).
-
-> NOTE: If you don't like having to render into the document to get `fireEvent`
-> working, then feel free to try to chip into making it possible for React
-> to attach event handlers to the rendered node rather than the `document`.
-> Learn more here:
-> [facebook/react#2043](https://github.com/facebook/react/issues/2043)
-
-```javascript
-import {renderIntoDocument, cleanup, fireEvent} from 'react-testing-library'
-
-// don't forget to clean up the document.body
-afterEach(cleanup)
-
-test('clicks submit button', () => {
- const spy = jest.fn()
- const {getByText} = renderIntoDocument(Submit )
-
- fireEvent(
- getByText('Submit'),
- new MouseEvent('click', {
- bubbles: true, // click events must bubble for React to see it
- cancelable: true,
+test('handles server exceptions', async () => {
+ // mock the server error response for this test suite only.
+ server.use(
+ rest.post('/api/login', (req, res, ctx) => {
+ return res(ctx.status(500), ctx.json({message: 'Internal server error'}))
}),
)
- expect(spy).toHaveBeenCalledTimes(1)
-})
-```
-
-#### `fireEvent[eventName](node: HTMLElement, eventProperties: Object)`
-
-Convenience methods for firing DOM events. Check out
-[dom-testing-library/src/events.js](https://github.com/kentcdodds/dom-testing-library/blob/master/src/events.js)
-for a full list as well as default `eventProperties`.
-
-```javascript
-import {renderIntoDocument, fireEvent} from 'react-testing-library'
-
-const {getElementByText} = renderIntoDocument()
-
-// similar to the above example
-// click will bubble for React to see it
-const rightClick = {button: 2}
-fireEvent.click(getElementByText('Submit'), rightClick)
-// default `button` property for click events is set to `0` which is a left click.
-```
-
-### `prettyDOM`
-
-This helper function can be used to print out readable representation of the DOM
-tree of a node. This can be helpful for instance when debugging tests.
-
-It is defined as:
-
-```typescript
-function prettyDOM(node: HTMLElement, maxLength?: number): string
-```
-
-It receives the root node to print out, and an optional extra argument to limit
-the size of the resulting string, for cases when it becomes too large.
-
-This function is usually used alongside `console.log` to temporarily print out
-DOM trees during tests for debugging purposes:
-
-```javascript
-import {render, prettyDOM} from 'react-testing-library'
+ render( )
-const HelloWorld = () => Hello World
-const {container} = render( )
-console.log(prettyDOM(container))
-//
-//
Hello World
-//
-```
-
-## `TextMatch`
-
-Several APIs accept a `TextMatch` which can be a `string`, `regex` or a
-`function` which returns `true` for a match and `false` for a mismatch.
-
-See [dom-testing-library#textmatch][dom-testing-lib-textmatch] for options.
-
-Examples:
-
-```javascript
-import {render, getByText} from 'react-testing-library'
-
-const {container} = render(Hello World
)
-
-// WILL find the div:
+ // fill out the form
+ fireEvent.change(screen.getByLabelText(/username/i), {
+ target: {value: 'chuck'},
+ })
+ fireEvent.change(screen.getByLabelText(/password/i), {
+ target: {value: 'norris'},
+ })
-// Matching a string:
-getByText(container, 'Hello World') // full string match
-getByText(container, 'llo Worl', {exact: false}) // substring match
-getByText(container, 'hello world', {exact: false}) // ignore case
+ fireEvent.click(screen.getByText(/submit/i))
-// Matching a regex:
-getByText(container, /World/) // substring match
-getByText(container, /world/i) // substring match, ignore case
-getByText(container, /^hello world$/i) // full string match, ignore case
-getByText(container, /Hello W?oRlD/i) // advanced regex
+ // wait for the error message
+ const alert = await screen.findByRole('alert')
-// Matching with a custom function:
-getByText(container, (content, element) => content.startsWith('Hello'))
-
-// WILL NOT find the div:
-
-getByText(container, 'Goodbye World') // full string does not match
-getByText(container, /hello world/) // case-sensitive regex with different case
-// function looking for a span when it's actually a div:
-getByText(container, (content, element) => {
- return element.tagName.toLowerCase() === 'span' && content.startsWith('Hello')
+ expect(alert).toHaveTextContent(/internal server error/i)
+ expect(window.localStorage.getItem('token')).toBeNull()
})
```
-## `query` APIs
-
-Each of the `get` APIs listed in [the `render`](#render) section above have a
-complimentary `query` API. The `get` APIs will throw errors if a proper node
-cannot be found. This is normally the desired effect. However, if you want to
-make an assertion that an element is _not_ present in the DOM, then you can use
-the `query` API instead:
-
-```javascript
-import {render} from 'react-testing-library'
-
-const {queryByText} = render()
-const submitButton = queryByText('submit')
-expect(submitButton).toBeNull() // it doesn't exist
-```
-
-## `queryAll` and `getAll` APIs
+> We recommend using [Mock Service Worker](https://github.com/mswjs/msw) library
+> to declaratively mock API communication in your tests instead of stubbing
+> `window.fetch`, or relying on third-party adapters.
-Each of the `query` APIs have a corresponsing `queryAll` version that always returns an Array of matching nodes. `getAll` is the same but throws when the array has a length of 0.
+### More Examples
-```javascript
-import {render} from 'react-testing-library'
+> We're in the process of moving examples to the
+> [docs site](https://testing-library.com/docs/example-codesandbox)
-const {queryByText} = render( )
-const submitButtons = queryAllByText('submit')
-expect(submitButtons).toHaveLength(3) // expect 3 elements
-expect(submitButtons[0]).toBeInTheDOM()
-```
-
-## Examples
-
-You'll find examples of testing with different libraries in
-[the `examples` directory](https://github.com/kentcdodds/react-testing-library/blob/master/examples).
+You'll find runnable examples of testing with different libraries in
+[the `react-testing-library-examples` codesandbox](https://codesandbox.io/s/github/kentcdodds/react-testing-library-examples).
Some included are:
-* [`react-redux`](https://github.com/kentcdodds/react-testing-library/blob/master/examples/__tests__/react-redux.js)
-* [`react-router`](https://github.com/kentcdodds/react-testing-library/blob/master/examples/__tests__/react-router.js)
-* [`react-context`](https://github.com/kentcdodds/react-testing-library/blob/master/examples/__tests__/react-context.js)
-
-## Learning Material
-
-* [Confident React](https://www.youtube.com/watch?v=qXRPHRgcXJ0&list=PLV5CVI1eNcJgNqzNwcs4UKrlJdhfDjshf)
-* [Test Driven Development with react-testing-library](https://www.youtube.com/watch?v=kCR3JAR7CHE&list=PLV5CVI1eNcJgCrPH_e6d57KRUTiDZgs0u)
-* [Testing React and Web Applications](https://kentcdodds.com/workshops/#testing-react-and-web-applications)
-* [Build a joke app with TDD](https://medium.com/@mbaranovski/quick-guide-to-tdd-in-react-81888be67c64) by [@mbaranovski](https://github.com/mbaranovski)
-* [Build a comment feed with TDD](https://medium.freecodecamp.org/how-to-build-sturdy-react-apps-with-tdd-and-the-react-testing-library-47ad3c5c8e47) by [@iwilsonq](https://github.com/iwilsonq)
-* [A clear way to unit testing React JS components using Jest and react-testing-library](https://www.richardkotze.com/coding/react-testing-library-jest) by [Richard Kotze](https://github.com/rkotze)
-
-* [Intro to react-testing-library](https://chrisnoring.gitbooks.io/react/content/testing/react-testing-library.html) by [Chris Noring](https://github.com/softchris)
-* [Integration testing in React](https://medium.com/@jeffreyrussom/integration-testing-in-react-21f92a55a894) by [Jeffrey Russom](https://github.com/qswitcher)
-
-Feel free to contribute more!
-
-## FAQ
-
-
-
-Which get method should I use?
-
-Based on [the Guiding Principles](#guiding-principles), your test should
-resemble how your code (component, page, etc.) as much as possible. With this
-in mind, we recommend this order of priority:
-
-1. `getByLabelText`: Only really good for form fields, but this is the number 1
- method a user finds those elements, so it should be your top preference.
-2. `getByPlaceholderText`: [A placeholder is not a substitute for a label](https://www.nngroup.com/articles/form-design-placeholders/).
- But if that's all you have, then it's better than alternatives.
-3. `getByText`: Not useful for forms, but this is the number 1 method a user
- finds other elements (like buttons to click), so it should be your top
- preference for non-form elements.
-4. `getByAltText`: If your element is one which supports `alt` text
- (`img`, `area`, and `input`), then you can use this to find that element.
-5. `getByTestId`: The user cannot see (or hear) these, so this is only
- recommended for cases where you can't match by text or it doesn't make sense
- (the text is dynamic).
-
-Other than that, you can also use the `container` to query the rendered
-component as well (using the regular
-[`querySelector` API](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)).
-
-
+- [`react-redux`](https://codesandbox.io/s/github/kentcdodds/react-testing-library-examples/tree/main/?fontsize=14&module=%2Fsrc%2F__tests__%2Freact-redux.js&previewwindow=tests)
+- [`react-router`](https://codesandbox.io/s/github/kentcdodds/react-testing-library-examples/tree/main/?fontsize=14&module=%2Fsrc%2F__tests__%2Freact-router.js&previewwindow=tests)
+- [`react-context`](https://codesandbox.io/s/github/kentcdodds/react-testing-library-examples/tree/main/?fontsize=14&module=%2Fsrc%2F__tests__%2Freact-context.js&previewwindow=tests)
-
+## Hooks
-Can I write unit tests with this library?
-
-Definitely yes! You can write unit and integration tests with this library.
-See below for more on how to mock dependencies (because this library
-intentionally does NOT support shallow rendering) if you want to unit test a
-high level component. The tests in this project show several examples of
-unit testing with this library.
-
-As you write your tests, keep in mind:
-
-> The more your tests resemble the way your software is used, the more confidence they can give you. - [17 Feb 2018][guiding-principle]
-
-
-
-
-
-What if my app is localized and I don't have access to the text in test?
-
-This is fairly common. Our first bit of advice is to try to get the default
-text used in your tests. That will make everything much easier (more than just
-using this utility). If that's not possible, then you're probably best
-to just stick with `data-testid`s (which is not bad anyway).
-
-
-
-
-
-If I can't use shallow rendering, how do I mock out components in tests?
-
-In general, you should avoid mocking out components (see
-[the Guiding Principles section](#guiding-principles)). However if you need to,
-then it's pretty trivial using
-[Jest's mocking feature](https://facebook.github.io/jest/docs/en/manual-mocks.html).
-One case that I've found mocking to be especially useful is for animation
-libraries. I don't want my tests to wait for animations to end.
-
-```javascript
-jest.mock('react-transition-group', () => {
- const FakeTransition = jest.fn(({children}) => children)
- const FakeCSSTransition = jest.fn(
- props =>
- props.in ? {props.children} : null,
- )
- return {CSSTransition: FakeCSSTransition, Transition: FakeTransition}
-})
-
-test('you can mock things with jest.mock', () => {
- const {getByTestId, queryByTestId} = render(
- ,
- )
- expect(queryByTestId('hidden-message')).toBeTruthy() // we just care it exists
- // hide the message
- Simulate.click(getByTestId('toggle-message'))
- // in the real world, the CSSTransition component would take some time
- // before finishing the animation which would actually hide the message.
- // So we've mocked it out for our tests to make it happen instantly
- expect(queryByTestId('hidden-message')).toBeNull() // we just care it doesn't exist
-})
-```
-
-Note that because they're Jest mock functions (`jest.fn()`), you could also make
-assertions on those as well if you wanted.
-
-[Open full test](https://github.com/kentcdodds/react-testing-library/blob/master/examples/__tests__/mock.react-transition-group.js)
-for the full example.
-
-This looks like more work that shallow rendering (and it is), but it gives you
-more confidence so long as your mock resembles the thing you're mocking closly
-enough.
-
-If you want to make things more like shallow rendering, then you could do
-something more
-[like this](https://github.com/kentcdodds/react-testing-library/blob/master/examples/__tests__/shallow.react-transition-group.js).
-
-Learn more about how Jest mocks work from my blog post:
-["But really, what is a JavaScript mock?"](https://blog.kentcdodds.com/but-really-what-is-a-javascript-mock-10d060966f7d)
-
-
-
-
-
-What if I want to verify that an element does NOT exist?
-
-You typically will get access to rendered elements using the `getByTestId` utility. However, that function will throw an error if the element isn't found. If you want to specifically test for the absence of an element, then you should use the `queryByTestId` utility which will return the element if found or `null` if not.
-
-```javascript
-expect(queryByTestId('thing-that-does-not-exist')).toBeNull()
-```
+If you are interested in testing a custom hook, check out [React Hooks Testing
+Library][react-hooks-testing-library].
-
-
-
-
-I really don't like data-testids, but none of the other queries make sense. Do I have to use a data-testid?
-
-Definitely not. That said, a common reason people don't like the `data-testid`
-attribute is they're concerned about shipping that to production. I'd suggest
-that you probably want some simple E2E tests that run in production on occasion
-to make certain that things are working smoothly. In that case the `data-testid`
-attributes will be very useful. Even if you don't run these in production, you
-may want to run some E2E tests that run on the same code you're about to ship to
-production. In that case, the `data-testid` attributes will be valuable there as
-well.
-
-All that said, if you really don't want to ship `data-testid` attributes, then you
-can use
-[this simple babel plugin](https://www.npmjs.com/package/babel-plugin-react-remove-properties)
-to remove them.
-
-If you don't want to use them at all, then you can simply use regular DOM
-methods and properties to query elements off your container.
-
-```javascript
-const firstLiInDiv = container.querySelector('div li')
-const allLisInDiv = container.querySelectorAll('div li')
-const rootElement = container.firstChild
-```
-
-
-
-
-
-What if I’m iterating over a list of items that I want to put the data-testid="item" attribute on. How do I distinguish them from each other?
-
-You can make your selector just choose the one you want by including :nth-child in the selector.
-
-```javascript
-const thirdLiInUl = container.querySelector('ul > li:nth-child(3)')
-```
-
-Or you could include the index or an ID in your attribute:
-
-```javascript
-{item.text}
-```
-
-And then you could use the `getByTestId` utility:
-
-```javascript
-const items = [
- /* your items */
-]
-const {getByTestId} = render(/* your component with the items */)
-const thirdItem = getByTestId(`item-${items[2].id}`)
-```
-
-
-
-
-
-What about enzyme is "bloated with complexity and features" and "encourage
-poor testing practices"?
-
-Most of the damaging features have to do with encouraging testing implementation
-details. Primarily, these are
-[shallow rendering](http://airbnb.io/enzyme/docs/api/shallow.html), APIs which
-allow selecting rendered elements by component constructors, and APIs which
-allow you to get and interact with component instances (and their
-state/properties) (most of enzyme's wrapper APIs allow this).
-
-The guiding principle for this library is:
-
-> The more your tests resemble the way your software is used, the more confidence they can give you. - [17 Feb 2018][guiding-principle]
-
-Because users can't directly interact with your app's component instances,
-assert on their internal state or what components they render, or call their
-internal methods, doing those things in your tests reduce the confidence they're
-able to give you.
-
-That's not to say that there's never a use case for doing those things, so they
-should be possible to accomplish, just not the default and natural way to test
-react components.
-
-
-
-
-
-Why isn't snapshot diffing working?
-
-If you use the
-[snapshot-diff](https://github.com/jest-community/snapshot-diff)
-library to save snapshot diffs, it won't work out of the box because
-this library uses the DOM which is mutable. Changes don't return new
-objects so snapshot-diff will think it's the same object and avoid diffing it.
-
-Luckily there's an easy way to make it work: clone the DOM when
-passing it into snapshot-diff. It looks like this:
-
-```js
-const firstVersion = container.cloneNode(true)
-// Do some changes
-snapshotDiff(firstVersion, container.cloneNode(true))
-```
-
-
-
-## Other Solutions
-
-In preparing this project,
-[I tweeted about it](https://twitter.com/kentcdodds/status/974278185540964352)
-and
-[Sune Simonsen](https://github.com/sunesimonsen)
-[took up the challenge](https://twitter.com/sunesimonsen/status/974784783908818944).
-We had different ideas of what to include in the library, so I decided to create
-this one instead.
+> NOTE: it is not recommended to test single-use custom hooks in isolation from
+> the components where it's being used. It's better to test the component that's
+> using the hook rather than the hook itself. The `React Hooks Testing Library`
+> is intended to be used for reusable hooks/libraries.
## Guiding Principles
-> [The more your tests resemble the way your software is used, the more confidence they can give you.][guiding-principle]
+> [The more your tests resemble the way your software is used, the more
+> confidence they can give you.][guiding-principle]
We try to only expose methods and utilities that encourage you to write tests
-that closely resemble how your react components are used.
+that closely resemble how your React components are used.
Utilities are included in this project based on the following guiding
principles:
@@ -901,29 +409,17 @@ principles:
`react-dom`.
3. Utility implementations and APIs should be simple and flexible.
-At the end of the day, what we want is for this library to be pretty
-light-weight, simple, and understandable.
+Most importantly, we want React Testing Library to be pretty light-weight,
+simple, and easy to understand.
-## Contributors
+## Docs
-Thanks goes to these people ([emoji key][emojis]):
-
-
-
-| [Kent C. Dodds ](https://kentcdodds.com) [💻](https://github.com/kentcdodds/react-testing-library/commits?author=kentcdodds "Code") [📖](https://github.com/kentcdodds/react-testing-library/commits?author=kentcdodds "Documentation") [🚇](#infra-kentcdodds "Infrastructure (Hosting, Build-Tools, etc)") [⚠️](https://github.com/kentcdodds/react-testing-library/commits?author=kentcdodds "Tests") | [Ryan Castner ](http://audiolion.github.io) [📖](https://github.com/kentcdodds/react-testing-library/commits?author=audiolion "Documentation") | [Daniel Sandiego ](https://www.dnlsandiego.com) [💻](https://github.com/kentcdodds/react-testing-library/commits?author=dnlsandiego "Code") | [Paweł Mikołajczyk ](https://github.com/Miklet) [💻](https://github.com/kentcdodds/react-testing-library/commits?author=Miklet "Code") | [Alejandro Ñáñez Ortiz ](http://co.linkedin.com/in/alejandronanez/) [📖](https://github.com/kentcdodds/react-testing-library/commits?author=alejandronanez "Documentation") | [Matt Parrish ](https://github.com/pbomb) [🐛](https://github.com/kentcdodds/react-testing-library/issues?q=author%3Apbomb "Bug reports") [💻](https://github.com/kentcdodds/react-testing-library/commits?author=pbomb "Code") [📖](https://github.com/kentcdodds/react-testing-library/commits?author=pbomb "Documentation") [⚠️](https://github.com/kentcdodds/react-testing-library/commits?author=pbomb "Tests") | [Justin Hall ](https://github.com/wKovacs64) [📦](#platform-wKovacs64 "Packaging/porting to new platform") |
-| :---: | :---: | :---: | :---: | :---: | :---: | :---: |
-| [Anto Aravinth ](https://github.com/antoaravinth) [💻](https://github.com/kentcdodds/react-testing-library/commits?author=antoaravinth "Code") [⚠️](https://github.com/kentcdodds/react-testing-library/commits?author=antoaravinth "Tests") [📖](https://github.com/kentcdodds/react-testing-library/commits?author=antoaravinth "Documentation") | [Jonah Moses ](https://github.com/JonahMoses) [📖](https://github.com/kentcdodds/react-testing-library/commits?author=JonahMoses "Documentation") | [Łukasz Gandecki ](http://team.thebrain.pro) [💻](https://github.com/kentcdodds/react-testing-library/commits?author=lgandecki "Code") [⚠️](https://github.com/kentcdodds/react-testing-library/commits?author=lgandecki "Tests") [📖](https://github.com/kentcdodds/react-testing-library/commits?author=lgandecki "Documentation") | [Ivan Babak ](https://sompylasar.github.io) [🐛](https://github.com/kentcdodds/react-testing-library/issues?q=author%3Asompylasar "Bug reports") [🤔](#ideas-sompylasar "Ideas, Planning, & Feedback") | [Jesse Day ](https://github.com/jday3) [💻](https://github.com/kentcdodds/react-testing-library/commits?author=jday3 "Code") | [Ernesto García ](http://gnapse.github.io) [💬](#question-gnapse "Answering Questions") [💻](https://github.com/kentcdodds/react-testing-library/commits?author=gnapse "Code") [📖](https://github.com/kentcdodds/react-testing-library/commits?author=gnapse "Documentation") | [Josef Maxx Blake ](http://jomaxx.com) [💻](https://github.com/kentcdodds/react-testing-library/commits?author=jomaxx "Code") [📖](https://github.com/kentcdodds/react-testing-library/commits?author=jomaxx "Documentation") [⚠️](https://github.com/kentcdodds/react-testing-library/commits?author=jomaxx "Tests") |
-| [Michal Baranowski ](https://twitter.com/baranovskim) [📝](#blog-mbaranovski "Blogposts") [✅](#tutorial-mbaranovski "Tutorials") | [Arthur Puthin ](https://github.com/aputhin) [📖](https://github.com/kentcdodds/react-testing-library/commits?author=aputhin "Documentation") | [Thomas Chia ](https://github.com/thchia) [💻](https://github.com/kentcdodds/react-testing-library/commits?author=thchia "Code") [📖](https://github.com/kentcdodds/react-testing-library/commits?author=thchia "Documentation") | [Thiago Galvani ](http://ilegra.com/) [📖](https://github.com/kentcdodds/react-testing-library/commits?author=thiagopaiva99 "Documentation") | [Christian ](http://Chriswcs.github.io) [⚠️](https://github.com/kentcdodds/react-testing-library/commits?author=ChrisWcs "Tests") | [Alex Krolick ](https://alexkrolick.com) [💬](#question-alexkrolick "Answering Questions") [📖](https://github.com/kentcdodds/react-testing-library/commits?author=alexkrolick "Documentation") [💡](#example-alexkrolick "Examples") [🤔](#ideas-alexkrolick "Ideas, Planning, & Feedback") | [Johann Hubert Sonntagbauer ](https://github.com/johann-sonntagbauer) [💻](https://github.com/kentcdodds/react-testing-library/commits?author=johann-sonntagbauer "Code") [📖](https://github.com/kentcdodds/react-testing-library/commits?author=johann-sonntagbauer "Documentation") [⚠️](https://github.com/kentcdodds/react-testing-library/commits?author=johann-sonntagbauer "Tests") |
-| [Maddi Joyce ](http://www.maddijoyce.com) [💻](https://github.com/kentcdodds/react-testing-library/commits?author=maddijoyce "Code") | [Ryan Vice ](http://www.vicesoftware.com) [📖](https://github.com/kentcdodds/react-testing-library/commits?author=RyanAtViceSoftware "Documentation") | [Ian Wilson ](https://ianwilson.io) [📝](#blog-iwilsonq "Blogposts") [✅](#tutorial-iwilsonq "Tutorials") | [Daniel ](https://github.com/InExtremaRes) [🐛](https://github.com/kentcdodds/react-testing-library/issues?q=author%3AInExtremaRes "Bug reports") [💻](https://github.com/kentcdodds/react-testing-library/commits?author=InExtremaRes "Code") | [Giorgio Polvara ](https://twitter.com/Gpx) [🐛](https://github.com/kentcdodds/react-testing-library/issues?q=author%3AGpx "Bug reports") [🤔](#ideas-Gpx "Ideas, Planning, & Feedback") | [John Gozde ](https://github.com/jgoz) [💻](https://github.com/kentcdodds/react-testing-library/commits?author=jgoz "Code") | [Sam Horton ](https://twitter.com/SavePointSam) [📖](https://github.com/kentcdodds/react-testing-library/commits?author=SavePointSam "Documentation") [💡](#example-SavePointSam "Examples") [🤔](#ideas-SavePointSam "Ideas, Planning, & Feedback") |
-| [Richard Kotze (mobile) ](http://www.richardkotze.com) [📖](https://github.com/kentcdodds/react-testing-library/commits?author=rkotze "Documentation") | [Brahian E. Soto Mercedes ](https://github.com/sotobuild) [📖](https://github.com/kentcdodds/react-testing-library/commits?author=sotobuild "Documentation") | [Benoit de La Forest ](https://github.com/bdelaforest) [📖](https://github.com/kentcdodds/react-testing-library/commits?author=bdelaforest "Documentation") |
-
-
-This project follows the [all-contributors][all-contributors] specification.
-Contributions of any kind welcome!
+[**Read The Docs**](https://testing-library.com/react) |
+[Edit the docs](https://github.com/testing-library/testing-library-docs)
## Issues
-_Looking to contribute? Look for the [Good First Issue][good-first-issue] label._
+Looking to contribute? Look for the [Good First Issue][good-first-issue] label.
### 🐛 Bugs
@@ -943,51 +439,254 @@ a 👍. This helps maintainers prioritize what to work on.
For questions related to using the library, please visit a support community
instead of filing an issue on GitHub.
-* [Spectrum][spectrum]
-* [Reactiflux on Discord][reactiflux]
-* [Stack Overflow][stackoverflow]
+- [Discord][discord]
+- [Stack Overflow][stackoverflow]
+
+## Contributors
+
+Thanks goes to these people ([emoji key][emojis]):
+
+
+
+
+
+
+
+
+
+
+
+This project follows the [all-contributors][all-contributors] specification.
+Contributions of any kind welcome!
## LICENSE
-MIT
+[MIT](LICENSE)
-
+
[npm]: https://www.npmjs.com/
+[yarn]: https://classic.yarnpkg.com
[node]: https://nodejs.org
-[build-badge]: https://img.shields.io/travis/kentcdodds/react-testing-library.svg?style=flat-square
-[build]: https://travis-ci.org/kentcdodds/react-testing-library
-[coverage-badge]: https://img.shields.io/codecov/c/github/kentcdodds/react-testing-library.svg?style=flat-square
-[coverage]: https://codecov.io/github/kentcdodds/react-testing-library
-[version-badge]: https://img.shields.io/npm/v/react-testing-library.svg?style=flat-square
-[package]: https://www.npmjs.com/package/react-testing-library
-[downloads-badge]: https://img.shields.io/npm/dm/react-testing-library.svg?style=flat-square
-[npmtrends]: http://www.npmtrends.com/react-testing-library
-[spectrum-badge]: https://withspectrum.github.io/badge/badge.svg
-[spectrum]: https://spectrum.chat/react-testing-library
-[license-badge]: https://img.shields.io/npm/l/react-testing-library.svg?style=flat-square
-[license]: https://github.com/kentcdodds/react-testing-library/blob/master/LICENSE
+[build-badge]: https://img.shields.io/github/actions/workflow/status/testing-library/react-testing-library/validate.yml?branch=main&logo=github
+[build]: https://github.com/testing-library/react-testing-library/actions?query=workflow%3Avalidate
+[coverage-badge]: https://img.shields.io/codecov/c/github/testing-library/react-testing-library.svg?style=flat-square
+[coverage]: https://codecov.io/github/testing-library/react-testing-library
+[version-badge]: https://img.shields.io/npm/v/@testing-library/react.svg?style=flat-square
+[package]: https://www.npmjs.com/package/@testing-library/react
+[downloads-badge]: https://img.shields.io/npm/dm/@testing-library/react.svg?style=flat-square
+[npmtrends]: http://www.npmtrends.com/@testing-library/react
+[license-badge]: https://img.shields.io/npm/l/@testing-library/react.svg?style=flat-square
+[license]: https://github.com/testing-library/react-testing-library/blob/main/LICENSE
[prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square
[prs]: http://makeapullrequest.com
-[donate-badge]: https://img.shields.io/badge/$-support-green.svg?style=flat-square
[coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square
-[coc]: https://github.com/kentcdodds/react-testing-library/blob/master/CODE_OF_CONDUCT.md
-[github-watch-badge]: https://img.shields.io/github/watchers/kentcdodds/react-testing-library.svg?style=social
-[github-watch]: https://github.com/kentcdodds/react-testing-library/watchers
-[github-star-badge]: https://img.shields.io/github/stars/kentcdodds/react-testing-library.svg?style=social
-[github-star]: https://github.com/kentcdodds/react-testing-library/stargazers
-[twitter]: https://twitter.com/intent/tweet?text=Check%20out%20react-testing-library%20by%20%40kentcdodds%20https%3A%2F%2Fgithub.com%2Fkentcdodds%2Freact-testing-library%20%F0%9F%91%8D
-[twitter-badge]: https://img.shields.io/twitter/url/https/github.com/kentcdodds/react-testing-library.svg?style=social
-[emojis]: https://github.com/kentcdodds/all-contributors#emoji-key
-[all-contributors]: https://github.com/kentcdodds/all-contributors
-[set-immediate]: https://developer.mozilla.org/en-US/docs/Web/API/Window/setImmediate
+[coc]: https://github.com/testing-library/react-testing-library/blob/main/CODE_OF_CONDUCT.md
+[github-watch-badge]: https://img.shields.io/github/watchers/testing-library/react-testing-library.svg?style=social
+[github-watch]: https://github.com/testing-library/react-testing-library/watchers
+[github-star-badge]: https://img.shields.io/github/stars/testing-library/react-testing-library.svg?style=social
+[github-star]: https://github.com/testing-library/react-testing-library/stargazers
+[twitter]: https://twitter.com/intent/tweet?text=Check%20out%20react-testing-library%20by%20%40@TestingLib%20https%3A%2F%2Fgithub.com%2Ftesting-library%2Freact-testing-library%20%F0%9F%91%8D
+[twitter-badge]: https://img.shields.io/twitter/url/https/github.com/testing-library/react-testing-library.svg?style=social
+[emojis]: https://github.com/all-contributors/all-contributors#emoji-key
+[all-contributors]: https://github.com/all-contributors/all-contributors
+[all-contributors-badge]: https://img.shields.io/github/all-contributors/testing-library/react-testing-library?color=orange&style=flat-square
[guiding-principle]: https://twitter.com/kentcdodds/status/977018512689455106
-[data-testid-blog-post]: https://blog.kentcdodds.com/making-your-ui-tests-resilient-to-change-d37a6ee37269
-[dom-testing-lib-textmatch]: https://github.com/kentcdodds/dom-testing-library#textmatch
-[bugs]: https://github.com/kentcdodds/react-testing-library/issues?q=is%3Aissue+is%3Aopen+label%3Abug+sort%3Acreated-desc
-[requests]: https://github.com/kentcdodds/react-testing-library/issues?q=is%3Aissue+sort%3Areactions-%2B1-desc+label%3Aenhancement+is%3Aopen
-[good-first-issue]: https://github.com/kentcdodds/react-testing-library/issues?utf8=✓&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3A"good+first+issue"+
-[reactiflux]: https://www.reactiflux.com/
+[bugs]: https://github.com/testing-library/react-testing-library/issues?q=is%3Aissue+is%3Aopen+label%3Abug+sort%3Acreated-desc
+[requests]: https://github.com/testing-library/react-testing-library/issues?q=is%3Aissue+sort%3Areactions-%2B1-desc+label%3Aenhancement+is%3Aopen
+[good-first-issue]: https://github.com/testing-library/react-testing-library/issues?utf8=✓&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3A"good+first+issue"+
+[discord-badge]: https://img.shields.io/discord/723559267868737556.svg?color=7389D8&labelColor=6A7EC2&logo=discord&logoColor=ffffff&style=flat-square
+[discord]: https://discord.gg/testing-library
[stackoverflow]: https://stackoverflow.com/questions/tagged/react-testing-library
+[react-hooks-testing-library]: https://github.com/testing-library/react-hooks-testing-library
+
+
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 00000000..472fcd83
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,20 @@
+coverage:
+ status:
+ project:
+ default:
+ # basic
+ target: 100%
+ threshold: 0%
+ flags:
+ - canary
+ - experimental
+ - latest
+ branches:
+ - main
+ - 12.x
+ if_ci_failed: success
+ if_not_found: failure
+ informational: false
+ only_pulls: false
+github_checks:
+ annotations: true
diff --git a/dont-cleanup-after-each.js b/dont-cleanup-after-each.js
new file mode 100644
index 00000000..083a8188
--- /dev/null
+++ b/dont-cleanup-after-each.js
@@ -0,0 +1 @@
+process.env.RTL_SKIP_AUTO_CLEANUP = true
diff --git a/examples/.eslintrc.js b/examples/.eslintrc.js
deleted file mode 100644
index 67ff778c..00000000
--- a/examples/.eslintrc.js
+++ /dev/null
@@ -1,15 +0,0 @@
-module.exports = {
- // we have to do this so our tests can reference 'react-testing-library'
- overrides: [
- {
- files: ['**/__tests__/**'],
- settings: {
- 'import/resolver': {
- jest: {
- jestConfigFile: require.resolve('./jest.config.js'),
- },
- },
- },
- },
- ],
-}
diff --git a/examples/README.md b/examples/README.md
deleted file mode 100644
index 6eda8302..00000000
--- a/examples/README.md
+++ /dev/null
@@ -1,30 +0,0 @@
-# Examples
-
-Here we have some examples for you to know how to not only use
-`react-testing-library` but also in general how to test common scenarios that
-pop up with React. Check out the `__tests__` directory for the different
-examples.
-
-## Setup
-
-The examples have a unique jest/eslint set up so the test files will resemble
-how they might appear in your project. (You'll see in the tests that we can
-`import {render} from 'react-testing-library'`).
-
-## Contribute
-
-We're always happy to accept contributions to the examples. Can't have too many
-of these as there are TONs of different ways to test React. Examples of testing
-components that use different and common libraries is always welcome. Try to
-keep examples simple enough for people to understand the main thing we're trying
-to demonstrate from the example.
-
-Please follow the guidelines found in [CONTRIBUTING.md][contributing] to set up
-the project.
-
-To run the tests, you can run `npm test examples`, or if you're working on a
-specific example, you can run `npm test name-of-your-file`. This will put you
-into Jest's interactive watch mode with a filter based on the name you provided.
-
-[contributing]: https://github.com/kentcdodds/react-testing-library/blob/master/CONTRIBUTING.md
-[jest-dom]: https://github.com/gnapse/jest-dom
diff --git a/examples/__tests__/mock.react-transition-group.js b/examples/__tests__/mock.react-transition-group.js
deleted file mode 100644
index 2f161a4c..00000000
--- a/examples/__tests__/mock.react-transition-group.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import React from 'react'
-import {CSSTransition} from 'react-transition-group'
-import {render, Simulate} from 'react-testing-library'
-
-function Fade({children, ...props}) {
- return (
-
- {children}
-
- )
-}
-
-class HiddenMessage extends React.Component {
- state = {show: this.props.initialShow || false}
- toggle = () => {
- this.setState(({show}) => ({show: !show}))
- }
- render() {
- return (
-
-
Toggle
-
- Hello world
-
-
- )
- }
-}
-
-jest.mock('react-transition-group', () => {
- const FakeTransition = jest.fn(({children}) => children)
- const FakeCSSTransition = jest.fn(
- props =>
- props.in ? {props.children} : null,
- )
- return {CSSTransition: FakeCSSTransition, Transition: FakeTransition}
-})
-
-test('you can mock things with jest.mock', () => {
- const {getByText, queryByText} = render( )
- expect(getByText('Hello world')).toBeTruthy() // we just care it exists
- // hide the message
- Simulate.click(getByText('Toggle'))
- // in the real world, the CSSTransition component would take some time
- // before finishing the animation which would actually hide the message.
- // So we've mocked it out for our tests to make it happen instantly
- expect(queryByText('Hello World')).toBeNull() // we just care it doesn't exist
-})
diff --git a/examples/__tests__/react-context.js b/examples/__tests__/react-context.js
deleted file mode 100644
index 9cc8ef8f..00000000
--- a/examples/__tests__/react-context.js
+++ /dev/null
@@ -1,56 +0,0 @@
-import React from 'react'
-import {render} from 'react-testing-library'
-import 'jest-dom/extend-expect'
-import {NameContext, NameProvider, NameConsumer} from '../react-context'
-
-/**
- * Test default values by rendering a context consumer without a
- * matching provider
- */
-test('NameConsumer shows default value', () => {
- const {getByText} = render( )
- expect(getByText(/^My Name Is:/)).toHaveTextContent('My Name Is: Unknown')
-})
-
-/**
- * To test a component tree that uses a context consumer but not the provider,
- * wrap the tree with a matching provider
- */
-test('NameConsumer shows value from provider', () => {
- const tree = (
-
-
-
- )
- const {getByText} = render(tree)
- expect(getByText(/^My Name Is:/)).toHaveTextContent('My Name Is: C3P0')
-})
-
-/**
- * To test a component that provides a context value, render a matching
- * consumer as the child
- */
-test('NameProvider composes full name from first, last', () => {
- const tree = (
-
-
- {value => Received: {value} }
-
-
- )
- const {getByText} = render(tree)
- expect(getByText(/^Received:/).textContent).toBe('Received: Boba Fett')
-})
-
-/**
- * A tree containing both a providers and consumer can be rendered normally
- */
-test('NameProvider/Consumer shows name of character', () => {
- const tree = (
-
-
-
- )
- const {getByText} = render(tree)
- expect(getByText(/^My Name Is:/).textContent).toBe('My Name Is: Leia Organa')
-})
diff --git a/examples/__tests__/react-redux.js b/examples/__tests__/react-redux.js
deleted file mode 100644
index f5ee1764..00000000
--- a/examples/__tests__/react-redux.js
+++ /dev/null
@@ -1,104 +0,0 @@
-import React from 'react'
-import {createStore} from 'redux'
-import {Provider, connect} from 'react-redux'
-import {render, Simulate} from 'react-testing-library'
-
-// counter.js
-class Counter extends React.Component {
- increment = () => {
- this.props.dispatch({type: 'INCREMENT'})
- }
-
- decrement = () => {
- this.props.dispatch({type: 'DECREMENT'})
- }
-
- render() {
- return (
-
-
Counter
-
- -
- {this.props.count}
- +
-
-
- )
- }
-}
-
-// normally this would be:
-// export default connect(state => ({count: state.count}))(Counter)
-// but for this test we'll give it a variable name
-// because we're doing this all in one file
-const ConnectedCounter = connect(state => ({count: state.count}))(Counter)
-
-// app.js
-function reducer(state = {count: 0}, action) {
- switch (action.type) {
- case 'INCREMENT':
- return {
- count: state.count + 1,
- }
- case 'DECREMENT':
- return {
- count: state.count - 1,
- }
- default:
- return state
- }
-}
-
-// normally here you'd do:
-// const store = createStore(reducer)
-// ReactDOM.render(
-//
-//
-// ,
-// document.getElementById('root'),
-// )
-// but for this test we'll umm... not do that :)
-
-// Now here's what your test will look like:
-
-// this is a handy function that I normally make available for all my tests
-// that deal with connected components.
-// you can provide initialState or the entire store that the ui is rendered with
-function renderWithRedux(
- ui,
- {initialState, store = createStore(reducer, initialState)} = {},
-) {
- return {
- ...render({ui} ),
- // adding `store` to the returned utilities to allow us
- // to reference it in our tests (just try to avoid using
- // this to test implementation details).
- store,
- }
-}
-
-test('can render with redux with defaults', () => {
- const {getByTestId, getByText} = renderWithRedux( )
- Simulate.click(getByText('+'))
- expect(getByTestId('count-value').textContent).toBe('1')
-})
-
-test('can render with redux with custom initial state', () => {
- const {getByTestId, getByText} = renderWithRedux( , {
- initialState: {count: 3},
- })
- Simulate.click(getByText('-'))
- expect(getByTestId('count-value').textContent).toBe('2')
-})
-
-test('can render with redux with custom store', () => {
- // this is a silly store that can never be changed
- const store = createStore(() => ({count: 1000}))
- const {getByTestId, getByText} = renderWithRedux( , {
- store,
- })
- Simulate.click(getByText('+'))
- expect(getByTestId('count-value').textContent).toBe('1000')
- Simulate.click(getByText('-'))
- expect(getByTestId('count-value').textContent).toBe('1000')
-})
diff --git a/examples/__tests__/react-router.js b/examples/__tests__/react-router.js
deleted file mode 100644
index f21bb967..00000000
--- a/examples/__tests__/react-router.js
+++ /dev/null
@@ -1,69 +0,0 @@
-import React from 'react'
-import {withRouter} from 'react-router'
-import {Link, Route, Router, Switch} from 'react-router-dom'
-import {createMemoryHistory} from 'history'
-import {render, Simulate} from 'react-testing-library'
-
-const About = () => You are on the about page
-const Home = () => You are home
-const NoMatch = () => No match
-
-const LocationDisplay = withRouter(({location}) => (
- {location.pathname}
-))
-
-function App() {
- return (
-
- Home
- About
-
-
-
-
-
-
-
- )
-}
-
-// Ok, so here's what your tests might look like
-
-// this is a handy function that I would utilize for any component
-// that relies on the router being in context
-function renderWithRouter(
- ui,
- {route = '/', history = createMemoryHistory({initialEntries: [route]})} = {},
-) {
- return {
- ...render({ui} ),
- // adding `history` to the returned utilities to allow us
- // to reference it in our tests (just try to avoid using
- // this to test implementation details).
- history,
- }
-}
-
-test('full app rendering/navigating', () => {
- const {container, getByText} = renderWithRouter( )
- // normally I'd use a data-testid, but just wanted to show this is also possible
- expect(container.innerHTML).toMatch('You are home')
- const leftClick = {button: 0}
- Simulate.click(getByText(/about/i), leftClick)
- // normally I'd use a data-testid, but just wanted to show this is also possible
- expect(container.innerHTML).toMatch('You are on the about page')
-})
-
-test('landing on a bad page', () => {
- const {container} = renderWithRouter( , {
- route: '/something-that-does-not-match',
- })
- // normally I'd use a data-testid, but just wanted to show this is also possible
- expect(container.innerHTML).toMatch('No match')
-})
-
-test('rendering a component that uses withRouter', () => {
- const route = '/some-route'
- const {getByTestId} = renderWithRouter( , {route})
- expect(getByTestId('location-display').textContent).toBe(route)
-})
diff --git a/examples/__tests__/shallow.react-transition-group.js b/examples/__tests__/shallow.react-transition-group.js
deleted file mode 100644
index 3124ef37..00000000
--- a/examples/__tests__/shallow.react-transition-group.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import React from 'react'
-import {CSSTransition} from 'react-transition-group'
-import {render, Simulate} from 'react-testing-library'
-
-function Fade({children, ...props}) {
- return (
-
- {children}
-
- )
-}
-
-class HiddenMessage extends React.Component {
- state = {show: this.props.initialShow || false}
- toggle = () => {
- this.setState(({show}) => ({show: !show}))
- }
- render() {
- return (
-
-
Toggle
-
- Hello world
-
-
- )
- }
-}
-
-jest.mock('react-transition-group', () => {
- const FakeCSSTransition = jest.fn(() => null)
- return {CSSTransition: FakeCSSTransition}
-})
-
-test('you can mock things with jest.mock', () => {
- const {getByText} = render( )
- const context = expect.any(Object)
- const children = expect.any(Object)
- const defaultProps = {children, timeout: 1000, className: 'fade'}
- expect(CSSTransition).toHaveBeenCalledWith(
- {in: true, ...defaultProps},
- context,
- )
- Simulate.click(getByText(/toggle/i))
- expect(CSSTransition).toHaveBeenCalledWith(
- {in: true, ...defaultProps},
- expect.any(Object),
- )
-})
diff --git a/examples/__tests__/update-props.js b/examples/__tests__/update-props.js
deleted file mode 100644
index 58731fec..00000000
--- a/examples/__tests__/update-props.js
+++ /dev/null
@@ -1,31 +0,0 @@
-// This is an example of how to update the props of a rendered component.
-// the basic idea is to simply call `render` again and provide the same container
-// that your first call created for you.
-
-import React from 'react'
-import {render} from 'react-testing-library'
-
-let idCounter = 1
-
-class NumberDisplay extends React.Component {
- id = idCounter++ // to ensure we don't remount a different instance
- render() {
- return (
-
- {this.props.number}
- {this.id}
-
- )
- }
-}
-
-test('calling render with the same component on the same container does not remount', () => {
- const {getByTestId, rerender} = render( )
- expect(getByTestId('number-display').textContent).toBe('1')
-
- // re-render the same component with different props
- rerender( )
- expect(getByTestId('number-display').textContent).toBe('2')
-
- expect(getByTestId('instance-id').textContent).toBe('1')
-})
diff --git a/examples/jest.config.js b/examples/jest.config.js
deleted file mode 100644
index 8dc4330d..00000000
--- a/examples/jest.config.js
+++ /dev/null
@@ -1,11 +0,0 @@
-const jestConfig = require('kcd-scripts/jest')
-
-module.exports = Object.assign(jestConfig, {
- rootDir: __dirname,
- roots: [__dirname],
- displayName: 'example',
- moduleNameMapper: {
- // this is just here so our examples look like they would in a real project
- 'react-testing-library': require.resolve('../src'),
- },
-})
diff --git a/examples/react-context.js b/examples/react-context.js
deleted file mode 100644
index 4ec24f6c..00000000
--- a/examples/react-context.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import React from 'react'
-
-const NameContext = React.createContext('Unknown')
-
-const NameProvider = ({children, first, last}) => {
- const fullName = `${first} ${last}`
- return (
- {children}
- )
-}
-
-const NameConsumer = () => (
-
- {value => My Name Is: {value}
}
-
-)
-
-export {NameContext, NameConsumer, NameProvider}
diff --git a/jest.config.js b/jest.config.js
index 23133e67..860358cd 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -1,5 +1,19 @@
-const jestConfig = require('kcd-scripts/jest')
+const {jest: jestConfig} = require('kcd-scripts/config')
module.exports = Object.assign(jestConfig, {
- displayName: 'library',
+ coverageThreshold: {
+ ...jestConfig.coverageThreshold,
+ // Full coverage across the build matrix (React 18, 19) but not in a single job
+ // Ful coverage is checked via codecov
+ './src/act-compat': {
+ branches: 90,
+ },
+ './src/pure': {
+ // minimum coverage of jobs using React 18 and 19
+ branches: 95,
+ functions: 88,
+ lines: 92,
+ statements: 92,
+ },
+ },
})
diff --git a/other/MAINTAINING.md b/other/MAINTAINING.md
index 025b6732..da09ba7c 100644
--- a/other/MAINTAINING.md
+++ b/other/MAINTAINING.md
@@ -4,60 +4,67 @@ This is documentation for maintainers of this project.
## Code of Conduct
-Please review, understand, and be an example of it. Violations of the code of conduct are
-taken seriously, even (especially) for maintainers.
+Please review, understand, and be an example of it. Violations of the code of
+conduct are taken seriously, even (especially) for maintainers.
## Issues
-We want to support and build the community. We do that best by helping people learn to solve
-their own problems. We have an issue template and hopefully most folks follow it. If it's
-not clear what the issue is, invite them to create a minimal reproduction of what they're trying
-to accomplish or the bug they think they've found.
+We want to support and build the community. We do that best by helping people
+learn to solve their own problems. We have an issue template and hopefully most
+folks follow it. If it's not clear what the issue is, invite them to create a
+minimal reproduction of what they're trying to accomplish or the bug they think
+they've found.
Once it's determined that a code change is necessary, point people to
-[makeapullrequest.com](http://makeapullrequest.com) and invite them to make a pull request.
-If they're the one who needs the feature, they're the one who can build it. If they need
-some hand holding and you have time to lend a hand, please do so. It's an investment into
-another human being, and an investment into a potential maintainer.
+[makeapullrequest.com](http://makeapullrequest.com) and invite them to make a
+pull request. If they're the one who needs the feature, they're the one who can
+build it. If they need some hand holding and you have time to lend a hand,
+please do so. It's an investment into another human being, and an investment
+into a potential maintainer.
-Remember that this is open source, so the code is not yours, it's ours. If someone needs a change
-in the codebase, you don't have to make it happen yourself. Commit as much time to the project
-as you want/need to. Nobody can ask any more of you than that.
+Remember that this is open source, so the code is not yours, it's ours. If
+someone needs a change in the codebase, you don't have to make it happen
+yourself. Commit as much time to the project as you want/need to. Nobody can ask
+any more of you than that.
## Pull Requests
-As a maintainer, you're fine to make your branches on the main repo or on your own fork. Either
-way is fine.
+As a maintainer, you're fine to make your branches on the main repo or on your
+own fork. Either way is fine.
-When we receive a pull request, a travis build is kicked off automatically (see the `.travis.yml`
-for what runs in the travis build). We avoid merging anything that breaks the travis build.
+When we receive a pull request, a github action is kicked off automatically (see
+the `.github/workflows/validate.yml` for what runs in the action). We avoid
+merging anything that breaks the validate action.
-Please review PRs and focus on the code rather than the individual. You never know when this is
-someone's first ever PR and we want their experience to be as positive as possible, so be
-uplifting and constructive.
+Please review PRs and focus on the code rather than the individual. You never
+know when this is someone's first ever PR and we want their experience to be as
+positive as possible, so be uplifting and constructive.
When you merge the pull request, 99% of the time you should use the
-[Squash and merge](https://help.github.com/articles/merging-a-pull-request/) feature. This keeps
-our git history clean, but more importantly, this allows us to make any necessary changes to the
-commit message so we release what we want to release. See the next section on Releases for more
-about that.
+[Squash and merge](https://help.github.com/articles/merging-a-pull-request/)
+feature. This keeps our git history clean, but more importantly, this allows us
+to make any necessary changes to the commit message so we release what we want
+to release. See the next section on Releases for more about that.
## Release
-Our releases are automatic. They happen whenever code lands into `master`. A travis build gets
-kicked off and if it's successful, a tool called
-[`semantic-release`](https://github.com/semantic-release/semantic-release) is used to
-automatically publish a new release to npm as well as a changelog to GitHub. It is only able to
-determine the version and whether a release is necessary by the git commit messages. With this
-in mind, **please brush up on [the commit message convention][commit] which drives our releases.**
+Our releases are automatic. They happen whenever code lands into `main`. A
+github action gets kicked off and if it's successful, a tool called
+[`semantic-release`](https://github.com/semantic-release/semantic-release) is
+used to automatically publish a new release to npm as well as a changelog to
+GitHub. It is only able to determine the version and whether a release is
+necessary by the git commit messages. With this in mind, **please brush up on
+[the commit message convention][commit] which drives our releases.**
-> One important note about this: Please make sure that commit messages do NOT contain the words
-> "BREAKING CHANGE" in them unless we want to push a major version. I've been burned by this
-> more than once where someone will include "BREAKING CHANGE: None" and it will end up releasing
-> a new major version. Not a huge deal honestly, but kind of annoying...
+> One important note about this: Please make sure that commit messages do NOT
+> contain the words "BREAKING CHANGE" in them unless we want to push a major
+> version. I've been burned by this more than once where someone will include
+> "BREAKING CHANGE: None" and it will end up releasing a new major version. Not
+> a huge deal honestly, but kind of annoying...
## Thanks!
Thank you so much for helping to maintain this project!
-[commit]: https://github.com/conventional-changelog-archived-repos/conventional-changelog-angular/blob/ed32559941719a130bb0327f886d6a32a8cbc2ba/convention.md
+[commit]:
+ https://github.com/conventional-changelog-archived-repos/conventional-changelog-angular/blob/ed32559941719a130bb0327f886d6a32a8cbc2ba/convention.md
diff --git a/other/cheat-sheet.pdf b/other/cheat-sheet.pdf
new file mode 100644
index 00000000..d8288e03
Binary files /dev/null and b/other/cheat-sheet.pdf differ
diff --git a/other/design files/README.txt b/other/design files/README.txt
new file mode 100644
index 00000000..ac8b1325
--- /dev/null
+++ b/other/design files/README.txt
@@ -0,0 +1,13 @@
+# Cheat sheet design
+
+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
+
+## Fonts used
+
+- Menlo
+- Arial
+
+## Standard distances
+
+15pt between boxes
+8pt margin from box edge to inner text
\ No newline at end of file
diff --git a/other/design files/cheat-sheet.afpub b/other/design files/cheat-sheet.afpub
new file mode 100644
index 00000000..bd0b0329
Binary files /dev/null and b/other/design files/cheat-sheet.afpub differ
diff --git a/other/jest.config.js b/other/jest.config.js
deleted file mode 100644
index 3efec572..00000000
--- a/other/jest.config.js
+++ /dev/null
@@ -1,9 +0,0 @@
-module.exports = {
- coverageDirectory: '../coverage',
- collectCoverageFrom: [
- '**/src/**/*.js',
- '!**/__tests__/**',
- '!**/node_modules/**',
- ],
- projects: ['./', './examples'],
-}
diff --git a/other/manual-releases.md b/other/manual-releases.md
index 2834084e..3bceb32e 100644
--- a/other/manual-releases.md
+++ b/other/manual-releases.md
@@ -1,9 +1,10 @@
# manual-releases
-This project has an automated release set up. So things are only released when there are
-useful changes in the code that justify a release. But sometimes things get messed up one way or another
-and we need to trigger the release ourselves. When this happens, simply bump the number below and commit
-that with the following commit message based on your needs:
+This project has an automated release set up. So things are only released when
+there are useful changes in the code that justify a release. But sometimes
+things get messed up one way or another and we need to trigger the release
+ourselves. When this happens, simply bump the number below and commit that with
+the following commit message based on your needs:
**Major**
@@ -40,4 +41,4 @@ change is to release a new patch version.
Reference: #
```
-The number of times we've had to do a manual release is: 2
+The number of times we've had to do a manual release is: 5
diff --git a/other/testingjavascript.jpg b/other/testingjavascript.jpg
new file mode 100644
index 00000000..e59e5ed9
Binary files /dev/null and b/other/testingjavascript.jpg differ
diff --git a/package.json b/package.json
index 02c7f563..b1bff976 100644
--- a/package.json
+++ b/package.json
@@ -1,25 +1,34 @@
{
- "name": "react-testing-library",
+ "name": "@testing-library/react",
"version": "0.0.0-semantically-released",
"description": "Simple and complete React DOM testing utilities that encourage good testing practices.",
"main": "dist/index.js",
- "typings": "typings/index.d.ts",
+ "types": "types/index.d.ts",
+ "module": "dist/@testing-library/react.esm.js",
"engines": {
- "node": ">=6"
+ "node": ">=18"
},
"scripts": {
- "add-contributor": "kcd-scripts contributors add",
- "build": "kcd-scripts build",
+ "prebuild": "rimraf dist",
+ "build": "npm-run-all --parallel build:main build:bundle:main build:bundle:pure",
+ "build:bundle:main": "dotenv -e .bundle.main.env kcd-scripts build -- --bundle --no-clean",
+ "build:bundle:pure": "dotenv -e .bundle.main.env -e .bundle.pure.env kcd-scripts build -- --bundle --no-clean",
+ "build:main": "kcd-scripts build --no-clean",
+ "format": "kcd-scripts format",
+ "install:csb": "npm install",
"lint": "kcd-scripts lint",
- "test": "kcd-scripts test --config=other/jest.config.js",
- "test:update": "npm test -- --updateSnapshot --coverage",
- "validate": "kcd-scripts validate",
"setup": "npm install && npm run validate -s",
- "precommit": "kcd-scripts precommit"
+ "test": "kcd-scripts test",
+ "test:update": "npm test -- --updateSnapshot --coverage",
+ "typecheck": "kcd-scripts typecheck --build types",
+ "validate": "kcd-scripts validate"
},
"files": [
"dist",
- "typings"
+ "dont-cleanup-after-each.js",
+ "pure.js",
+ "pure.d.ts",
+ "types/*.d.ts"
],
"keywords": [
"testing",
@@ -33,50 +42,75 @@
"end-to-end",
"e2e"
],
- "author": "Kent C. Dodds (http://kentcdodds.com/)",
+ "author": "Kent C. Dodds (https://kentcdodds.com)",
"license": "MIT",
"dependencies": {
- "dom-testing-library": "^2.3.1",
- "wait-for-expect": "^0.5.0"
+ "@babel/runtime": "^7.12.5"
},
"devDependencies": {
- "@types/react-dom": "^16.0.5",
- "axios": "^0.18.0",
- "eslint-import-resolver-jest": "^2.1.1",
- "history": "^4.7.2",
- "jest-dom": "^1.0.0",
- "jest-in-case": "^1.0.2",
- "kcd-scripts": "^0.37.0",
- "react": "^16.3.2",
- "react-dom": "^16.3.2",
- "react-redux": "^5.0.7",
- "react-router": "^4.2.0",
- "react-router-dom": "^4.2.2",
- "react-transition-group": "^2.3.1",
- "redux": "^4.0.0"
+ "@testing-library/dom": "^10.0.0",
+ "@testing-library/jest-dom": "^5.11.6",
+ "@types/react": "^19.0.0",
+ "@types/react-dom": "^19.0.0",
+ "chalk": "^4.1.2",
+ "dotenv-cli": "^4.0.0",
+ "jest-diff": "^29.7.0",
+ "kcd-scripts": "^13.0.0",
+ "npm-run-all2": "^6.2.6",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "rimraf": "^3.0.2",
+ "typescript": "^4.1.2"
},
"peerDependencies": {
- "react-dom": "*"
+ "@testing-library/dom": "^10.0.0",
+ "@types/react": "^18.0.0 || ^19.0.0",
+ "@types/react-dom": "^18.0.0 || ^19.0.0",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
},
"eslintConfig": {
"extends": "./node_modules/kcd-scripts/eslint.js",
+ "parserOptions": {
+ "ecmaVersion": 2022
+ },
+ "globals": {
+ "globalThis": "readonly"
+ },
"rules": {
"react/prop-types": "off",
+ "react/no-adjacent-inline-elements": "off",
"import/no-unassigned-import": "off",
- "import/named": "off"
+ "import/named": "off",
+ "testing-library/no-container": "off",
+ "testing-library/no-debugging-utils": "off",
+ "testing-library/no-dom-import": "off",
+ "testing-library/no-unnecessary-act": "off",
+ "testing-library/prefer-explicit-assert": "off",
+ "testing-library/prefer-find-by": "off",
+ "testing-library/prefer-user-event": "off"
}
},
"eslintIgnore": [
"node_modules",
"coverage",
- "dist"
+ "dist",
+ "*.d.ts"
],
"repository": {
"type": "git",
- "url": "git+https://github.com/kentcdodds/react-testing-library.git"
+ "url": "https://github.com/testing-library/react-testing-library"
},
"bugs": {
- "url": "https://github.com/kentcdodds/react-testing-library/issues"
+ "url": "https://github.com/testing-library/react-testing-library/issues"
},
- "homepage": "https://github.com/kentcdodds/react-testing-library#readme"
+ "homepage": "https://github.com/testing-library/react-testing-library#readme"
}
diff --git a/pure.d.ts b/pure.d.ts
new file mode 100644
index 00000000..c3cc4e69
--- /dev/null
+++ b/pure.d.ts
@@ -0,0 +1 @@
+export * from './types/pure'
diff --git a/pure.js b/pure.js
new file mode 100644
index 00000000..75dc0452
--- /dev/null
+++ b/pure.js
@@ -0,0 +1,2 @@
+// makes it so people can import from '@testing-library/react/pure'
+module.exports = require('./dist/pure')
diff --git a/src/__mocks__/axios.js b/src/__mocks__/axios.js
index 9d70d624..30970385 100644
--- a/src/__mocks__/axios.js
+++ b/src/__mocks__/axios.js
@@ -5,4 +5,4 @@ module.exports = {
// Note:
// For now we don't need any other method (POST/PUT/PATCH), what we have already works fine.
// We will add more methods only if we need to.
-// For reference please read: https://github.com/kentcdodds/react-testing-library/issues/2
+// For reference please read: https://github.com/testing-library/react-testing-library/issues/2
diff --git a/src/__tests__/__snapshots__/fetch.js.snap b/src/__tests__/__snapshots__/fetch.js.snap
deleted file mode 100644
index 69e0e57b..00000000
--- a/src/__tests__/__snapshots__/fetch.js.snap
+++ /dev/null
@@ -1,12 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Fetch makes an API call and displays the greeting when load-greeting is clicked 1`] = `
-
-
- Fetch
-
-
- hello there
-
-
-`;
diff --git a/src/__tests__/__snapshots__/render.js.snap b/src/__tests__/__snapshots__/render.js.snap
new file mode 100644
index 00000000..345cd937
--- /dev/null
+++ b/src/__tests__/__snapshots__/render.js.snap
@@ -0,0 +1,12 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`render API supports fragments 1`] = `
+
+
+
+ DocumentFragment
+
+ is pretty cool!
+
+
+`;
diff --git a/src/__tests__/act.js b/src/__tests__/act.js
new file mode 100644
index 00000000..5430f28b
--- /dev/null
+++ b/src/__tests__/act.js
@@ -0,0 +1,69 @@
+import * as React from 'react'
+import {act, render, fireEvent, screen} from '../'
+
+test('render calls useEffect immediately', () => {
+ const effectCb = jest.fn()
+ function MyUselessComponent() {
+ React.useEffect(effectCb)
+ return null
+ }
+ render( )
+ expect(effectCb).toHaveBeenCalledTimes(1)
+})
+
+test('findByTestId returns the element', async () => {
+ const ref = React.createRef()
+ render(
)
+ expect(await screen.findByTestId('foo')).toBe(ref.current)
+})
+
+test('fireEvent triggers useEffect calls', () => {
+ const effectCb = jest.fn()
+ function Counter() {
+ React.useEffect(effectCb)
+ const [count, setCount] = React.useState(0)
+ return setCount(count + 1)}>{count}
+ }
+ const {
+ container: {firstChild: buttonNode},
+ } = render( )
+
+ effectCb.mockClear()
+ fireEvent.click(buttonNode)
+ expect(buttonNode).toHaveTextContent('1')
+ expect(effectCb).toHaveBeenCalledTimes(1)
+})
+
+test('calls to hydrate will run useEffects', () => {
+ const effectCb = jest.fn()
+ function MyUselessComponent() {
+ React.useEffect(effectCb)
+ return null
+ }
+ render( , {hydrate: true})
+ expect(effectCb).toHaveBeenCalledTimes(1)
+})
+
+test('cleans up IS_REACT_ACT_ENVIRONMENT if its callback throws', () => {
+ global.IS_REACT_ACT_ENVIRONMENT = false
+
+ expect(() =>
+ act(() => {
+ throw new Error('threw')
+ }),
+ ).toThrow('threw')
+
+ expect(global.IS_REACT_ACT_ENVIRONMENT).toEqual(false)
+})
+
+test('cleans up IS_REACT_ACT_ENVIRONMENT if its async callback throws', async () => {
+ global.IS_REACT_ACT_ENVIRONMENT = false
+
+ await expect(() =>
+ act(async () => {
+ throw new Error('thenable threw')
+ }),
+ ).rejects.toThrow('thenable threw')
+
+ expect(global.IS_REACT_ACT_ENVIRONMENT).toEqual(false)
+})
diff --git a/src/__tests__/auto-cleanup-skip.js b/src/__tests__/auto-cleanup-skip.js
new file mode 100644
index 00000000..5696d4e3
--- /dev/null
+++ b/src/__tests__/auto-cleanup-skip.js
@@ -0,0 +1,18 @@
+import * as React from 'react'
+
+let render
+beforeAll(() => {
+ process.env.RTL_SKIP_AUTO_CLEANUP = 'true'
+ const rtl = require('../')
+ render = rtl.render
+})
+
+// This one verifies that if RTL_SKIP_AUTO_CLEANUP is set
+// then we DON'T auto-wire up the afterEach for folks
+test('first', () => {
+ render(hi
)
+})
+
+test('second', () => {
+ expect(document.body.innerHTML).toEqual('')
+})
diff --git a/src/__tests__/auto-cleanup.js b/src/__tests__/auto-cleanup.js
new file mode 100644
index 00000000..450a6136
--- /dev/null
+++ b/src/__tests__/auto-cleanup.js
@@ -0,0 +1,13 @@
+import * as React from 'react'
+import {render} from '../'
+
+// This just verifies that by importing RTL in an
+// environment which supports afterEach (like jest)
+// we'll get automatic cleanup between tests.
+test('first', () => {
+ render(hi
)
+})
+
+test('second', () => {
+ expect(document.body).toBeEmptyDOMElement()
+})
diff --git a/src/__tests__/cleanup.js b/src/__tests__/cleanup.js
new file mode 100644
index 00000000..9f17c722
--- /dev/null
+++ b/src/__tests__/cleanup.js
@@ -0,0 +1,126 @@
+import * as React from 'react'
+import {render, cleanup} from '../'
+
+test('cleans up the document', () => {
+ const spy = jest.fn()
+ const divId = 'my-div'
+
+ class Test extends React.Component {
+ componentWillUnmount() {
+ expect(document.getElementById(divId)).toBeInTheDocument()
+ spy()
+ }
+
+ render() {
+ return
+ }
+ }
+
+ render( )
+ cleanup()
+ expect(document.body).toBeEmptyDOMElement()
+ expect(spy).toHaveBeenCalledTimes(1)
+})
+
+test('cleanup does not error when an element is not a child', () => {
+ render(
, {container: document.createElement('div')})
+ cleanup()
+})
+
+test('cleanup runs effect cleanup functions', () => {
+ const spy = jest.fn()
+
+ const Test = () => {
+ React.useEffect(() => spy)
+
+ return null
+ }
+
+ render( )
+ cleanup()
+ expect(spy).toHaveBeenCalledTimes(1)
+})
+
+describe('fake timers and missing act warnings', () => {
+ beforeEach(() => {
+ jest.resetAllMocks()
+ jest.spyOn(console, 'error').mockImplementation(() => {
+ // assert messages explicitly
+ })
+ jest.useFakeTimers()
+ })
+
+ afterEach(() => {
+ jest.restoreAllMocks()
+ jest.useRealTimers()
+ })
+
+ test('cleanup does not flush microtasks', () => {
+ const microTaskSpy = jest.fn()
+ function Test() {
+ const counter = 1
+ const [, setDeferredCounter] = React.useState(null)
+ React.useEffect(() => {
+ let cancelled = false
+ Promise.resolve().then(() => {
+ microTaskSpy()
+ // eslint-disable-next-line jest/no-if, jest/no-conditional-in-test -- false positive
+ if (!cancelled) {
+ setDeferredCounter(counter)
+ }
+ })
+
+ return () => {
+ cancelled = true
+ }
+ }, [counter])
+
+ return null
+ }
+ render( )
+
+ cleanup()
+
+ expect(microTaskSpy).toHaveBeenCalledTimes(0)
+ // console.error is mocked
+ // eslint-disable-next-line no-console
+ expect(console.error).toHaveBeenCalledTimes(0)
+ })
+
+ test('cleanup does not swallow missing act warnings', () => {
+ const deferredStateUpdateSpy = jest.fn()
+ function Test() {
+ const counter = 1
+ const [, setDeferredCounter] = React.useState(null)
+ React.useEffect(() => {
+ let cancelled = false
+ setTimeout(() => {
+ deferredStateUpdateSpy()
+ // eslint-disable-next-line jest/no-conditional-in-test -- false-positive
+ if (!cancelled) {
+ setDeferredCounter(counter)
+ }
+ }, 0)
+
+ return () => {
+ cancelled = true
+ }
+ }, [counter])
+
+ return null
+ }
+ render( )
+
+ jest.runAllTimers()
+ cleanup()
+
+ expect(deferredStateUpdateSpy).toHaveBeenCalledTimes(1)
+ // console.error is mocked
+ // eslint-disable-next-line no-console
+ expect(console.error).toHaveBeenCalledTimes(1)
+ // eslint-disable-next-line no-console
+ expect(console.error.mock.calls[0][0]).toMatch(
+ 'a test was not wrapped in act(...)',
+ )
+ })
+})
diff --git a/src/__tests__/config.js b/src/__tests__/config.js
new file mode 100644
index 00000000..7fdb1e00
--- /dev/null
+++ b/src/__tests__/config.js
@@ -0,0 +1,66 @@
+import {configure, getConfig} from '../'
+
+describe('configuration API', () => {
+ let originalConfig
+ beforeEach(() => {
+ // Grab the existing configuration so we can restore
+ // it at the end of the test
+ configure(existingConfig => {
+ originalConfig = existingConfig
+ // Don't change the existing config
+ return {}
+ })
+ })
+
+ afterEach(() => {
+ configure(originalConfig)
+ })
+
+ describe('DTL options', () => {
+ test('configure can set by a plain JS object', () => {
+ const testIdAttribute = 'not-data-testid'
+ configure({testIdAttribute})
+
+ expect(getConfig().testIdAttribute).toBe(testIdAttribute)
+ })
+
+ test('configure can set by a function', () => {
+ // setup base option
+ const baseTestIdAttribute = 'data-testid'
+ configure({testIdAttribute: baseTestIdAttribute})
+
+ const modifiedPrefix = 'modified-'
+ configure(existingConfig => ({
+ testIdAttribute: `${modifiedPrefix}${existingConfig.testIdAttribute}`,
+ }))
+
+ expect(getConfig().testIdAttribute).toBe(
+ `${modifiedPrefix}${baseTestIdAttribute}`,
+ )
+ })
+ })
+
+ describe('RTL options', () => {
+ test('configure can set by a plain JS object', () => {
+ configure({reactStrictMode: true})
+
+ expect(getConfig().reactStrictMode).toBe(true)
+ })
+
+ test('configure can set by a function', () => {
+ configure(existingConfig => ({
+ reactStrictMode: !existingConfig.reactStrictMode,
+ }))
+
+ expect(getConfig().reactStrictMode).toBe(true)
+ })
+ })
+
+ test('configure can set DTL and RTL options at once', () => {
+ const testIdAttribute = 'not-data-testid'
+ configure({testIdAttribute, reactStrictMode: true})
+
+ expect(getConfig().testIdAttribute).toBe(testIdAttribute)
+ expect(getConfig().reactStrictMode).toBe(true)
+ })
+})
diff --git a/src/__tests__/debug.js b/src/__tests__/debug.js
index 4cab1ea9..c6a1d1fe 100644
--- a/src/__tests__/debug.js
+++ b/src/__tests__/debug.js
@@ -1,5 +1,5 @@
-import React from 'react'
-import {render} from '../'
+import * as React from 'react'
+import {render, screen} from '../'
beforeEach(() => {
jest.spyOn(console, 'log').mockImplementation(() => {})
@@ -19,4 +19,37 @@ test('debug pretty prints the container', () => {
)
})
-/* eslint no-console:0 */
+test('debug pretty prints multiple containers', () => {
+ const HelloWorld = () => (
+ <>
+ Hello World
+ Hello World
+ >
+ )
+ const {debug} = render( )
+ const multipleElements = screen.getAllByTestId('testId')
+ debug(multipleElements)
+
+ expect(console.log).toHaveBeenCalledTimes(2)
+ expect(console.log).toHaveBeenCalledWith(
+ expect.stringContaining('Hello World'),
+ )
+})
+
+test('allows same arguments as prettyDOM', () => {
+ const HelloWorld = () => Hello World
+ const {debug, container} = render( )
+ debug(container, 6, {highlight: false})
+ expect(console.log).toHaveBeenCalledTimes(1)
+ expect(console.log.mock.calls[0]).toMatchInlineSnapshot(`
+ [
+
+ ...,
+ ]
+ `)
+})
+
+/*
+eslint
+ no-console: "off",
+*/
diff --git a/src/__tests__/end-to-end.js b/src/__tests__/end-to-end.js
index 6768dbe7..f93c23be 100644
--- a/src/__tests__/end-to-end.js
+++ b/src/__tests__/end-to-end.js
@@ -1,40 +1,234 @@
-import React from 'react'
-import {render, wait} from '../'
-
-const fetchAMessage = () =>
- new Promise(resolve => {
- // we are using random timeout here to simulate a real-time example
- // of an async operation calling a callback at a non-deterministic time
- const randomTimeout = Math.floor(Math.random() * 100)
- setTimeout(() => {
- resolve({returnedMessage: 'Hello World'})
- }, randomTimeout)
- })
-
-class ComponentWithLoader extends React.Component {
- state = {loading: true}
- async componentDidMount() {
- const data = await fetchAMessage()
- this.setState({data, loading: false}) // eslint-disable-line
- }
- render() {
- if (this.state.loading) {
- return
Loading...
- } else {
+import * as React from 'react'
+import {render, waitForElementToBeRemoved, screen, waitFor} from '../'
+
+describe.each([
+ ['real timers', () => jest.useRealTimers()],
+ ['fake legacy timers', () => jest.useFakeTimers('legacy')],
+ ['fake modern timers', () => jest.useFakeTimers('modern')],
+])(
+ 'it waits for the data to be loaded in a macrotask using %s',
+ (label, useTimers) => {
+ beforeEach(() => {
+ useTimers()
+ })
+
+ afterEach(() => {
+ jest.useRealTimers()
+ })
+
+ const fetchAMessageInAMacrotask = () =>
+ new Promise(resolve => {
+ // we are using random timeout here to simulate a real-time example
+ // of an async operation calling a callback at a non-deterministic time
+ const randomTimeout = Math.floor(Math.random() * 100)
+ setTimeout(() => {
+ resolve({returnedMessage: 'Hello World'})
+ }, randomTimeout)
+ })
+
+ function ComponentWithMacrotaskLoader() {
+ const [state, setState] = React.useState({data: undefined, loading: true})
+ React.useEffect(() => {
+ let cancelled = false
+ fetchAMessageInAMacrotask().then(data => {
+ if (!cancelled) {
+ setState({data, loading: false})
+ }
+ })
+
+ return () => {
+ cancelled = true
+ }
+ }, [])
+
+ if (state.loading) {
+ return
Loading...
+ }
+
return (
- Loaded this message: {this.state.data.returnedMessage}!
+ Loaded this message: {state.data.returnedMessage}!
)
}
- }
-}
-test('it waits for the data to be loaded', async () => {
- const {queryByText, queryByTestId} = render(
)
+ test('waitForElementToBeRemoved', async () => {
+ render(
)
+ const loading = () => screen.getByText('Loading...')
+ await waitForElementToBeRemoved(loading)
+ expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/)
+ })
+
+ test('waitFor', async () => {
+ render(
)
+ await waitFor(() => screen.getByText(/Loading../))
+ await waitFor(() => screen.getByText(/Loaded this message:/))
+ expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/)
+ })
+
+ test('findBy', async () => {
+ render(
)
+ await expect(screen.findByTestId('message')).resolves.toHaveTextContent(
+ /Hello World/,
+ )
+ })
+ },
+)
+
+describe.each([
+ ['real timers', () => jest.useRealTimers()],
+ ['fake legacy timers', () => jest.useFakeTimers('legacy')],
+ ['fake modern timers', () => jest.useFakeTimers('modern')],
+])(
+ 'it waits for the data to be loaded in many microtask using %s',
+ (label, useTimers) => {
+ beforeEach(() => {
+ useTimers()
+ })
+
+ afterEach(() => {
+ jest.useRealTimers()
+ })
+
+ const fetchAMessageInAMicrotask = () =>
+ Promise.resolve({
+ status: 200,
+ json: () => Promise.resolve({title: 'Hello World'}),
+ })
+
+ function ComponentWithMicrotaskLoader() {
+ const [fetchState, setFetchState] = React.useState({fetching: true})
+
+ React.useEffect(() => {
+ if (fetchState.fetching) {
+ fetchAMessageInAMicrotask().then(res => {
+ return (
+ res
+ .json()
+ // By spec, the runtime can only yield back to the event loop once
+ // the microtask queue is empty.
+ // So we ensure that we actually wait for that as well before yielding back from `waitFor`.
+ .then(data => data)
+ .then(data => data)
+ .then(data => data)
+ .then(data => data)
+ .then(data => data)
+ .then(data => data)
+ .then(data => data)
+ .then(data => data)
+ .then(data => data)
+ .then(data => data)
+ .then(data => data)
+ .then(data => {
+ setFetchState({todo: data.title, fetching: false})
+ })
+ )
+ })
+ }
+ }, [fetchState])
+
+ if (fetchState.fetching) {
+ return
Loading..
+ }
+
+ return (
+
Loaded this message: {fetchState.todo}
+ )
+ }
+
+ test('waitForElementToBeRemoved', async () => {
+ render(
)
+ const loading = () => screen.getByText('Loading..')
+ await waitForElementToBeRemoved(loading)
+ expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/)
+ })
+
+ test('waitFor', async () => {
+ render(
)
+ await waitFor(() => {
+ screen.getByText('Loading..')
+ })
+ await waitFor(() => {
+ screen.getByText(/Loaded this message:/)
+ })
+ expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/)
+ })
+
+ test('findBy', async () => {
+ render(
)
+ await expect(screen.findByTestId('message')).resolves.toHaveTextContent(
+ /Hello World/,
+ )
+ })
+ },
+)
+
+describe.each([
+ ['real timers', () => jest.useRealTimers()],
+ ['fake legacy timers', () => jest.useFakeTimers('legacy')],
+ ['fake modern timers', () => jest.useFakeTimers('modern')],
+])(
+ 'it waits for the data to be loaded in a microtask using %s',
+ (label, useTimers) => {
+ beforeEach(() => {
+ useTimers()
+ })
+
+ afterEach(() => {
+ jest.useRealTimers()
+ })
+
+ const fetchAMessageInAMicrotask = () =>
+ Promise.resolve({
+ status: 200,
+ json: () => Promise.resolve({title: 'Hello World'}),
+ })
+
+ function ComponentWithMicrotaskLoader() {
+ const [fetchState, setFetchState] = React.useState({fetching: true})
+
+ React.useEffect(() => {
+ if (fetchState.fetching) {
+ fetchAMessageInAMicrotask().then(res => {
+ return res.json().then(data => {
+ setFetchState({todo: data.title, fetching: false})
+ })
+ })
+ }
+ }, [fetchState])
+
+ if (fetchState.fetching) {
+ return
Loading..
+ }
+
+ return (
+
Loaded this message: {fetchState.todo}
+ )
+ }
+
+ test('waitForElementToBeRemoved', async () => {
+ render(
)
+ const loading = () => screen.getByText('Loading..')
+ await waitForElementToBeRemoved(loading)
+ expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/)
+ })
- expect(queryByText('Loading...')).toBeTruthy()
+ test('waitFor', async () => {
+ render(
)
+ await waitFor(() => {
+ screen.getByText('Loading..')
+ })
+ await waitFor(() => {
+ screen.getByText(/Loaded this message:/)
+ })
+ expect(screen.getByTestId('message')).toHaveTextContent(/Hello World/)
+ })
- await wait(() => expect(queryByText('Loading...')).toBeNull())
- expect(queryByTestId('message').textContent).toMatch(/Hello World/)
-})
+ test('findBy', async () => {
+ render(
)
+ await expect(screen.findByTestId('message')).resolves.toHaveTextContent(
+ /Hello World/,
+ )
+ })
+ },
+)
diff --git a/src/__tests__/error-handlers.js b/src/__tests__/error-handlers.js
new file mode 100644
index 00000000..60db1410
--- /dev/null
+++ b/src/__tests__/error-handlers.js
@@ -0,0 +1,183 @@
+/* eslint-disable jest/no-if */
+/* eslint-disable jest/no-conditional-in-test */
+/* eslint-disable jest/no-conditional-expect */
+import * as React from 'react'
+import {render, renderHook} from '../'
+
+const isReact19 = React.version.startsWith('19.')
+
+const testGateReact19 = isReact19 ? test : test.skip
+
+test('render errors', () => {
+ function Thrower() {
+ throw new Error('Boom!')
+ }
+
+ if (isReact19) {
+ expect(() => {
+ render(
)
+ }).toThrow('Boom!')
+ } else {
+ expect(() => {
+ expect(() => {
+ render(
)
+ }).toThrow('Boom!')
+ }).toErrorDev([
+ 'Error: Uncaught [Error: Boom!]',
+ // React retries on error
+ 'Error: Uncaught [Error: Boom!]',
+ ])
+ }
+})
+
+test('onUncaughtError is not supported in render', () => {
+ function Thrower() {
+ throw new Error('Boom!')
+ }
+ const onUncaughtError = jest.fn(() => {})
+
+ expect(() => {
+ render(
, {
+ onUncaughtError(error, errorInfo) {
+ console.log({error, errorInfo})
+ },
+ })
+ }).toThrow(
+ 'onUncaughtError is not supported. The `render` call will already throw on uncaught errors.',
+ )
+
+ expect(onUncaughtError).toHaveBeenCalledTimes(0)
+})
+
+testGateReact19('onCaughtError is supported in render', () => {
+ const thrownError = new Error('Boom!')
+ const handleComponentDidCatch = jest.fn()
+ const onCaughtError = jest.fn()
+ class ErrorBoundary extends React.Component {
+ state = {error: null}
+ static getDerivedStateFromError(error) {
+ return {error}
+ }
+ componentDidCatch(error, errorInfo) {
+ handleComponentDidCatch(error, errorInfo)
+ }
+ render() {
+ if (this.state.error) {
+ return null
+ }
+ return this.props.children
+ }
+ }
+ function Thrower() {
+ throw thrownError
+ }
+
+ render(
+
+
+ ,
+ {
+ onCaughtError,
+ },
+ )
+
+ expect(onCaughtError).toHaveBeenCalledWith(thrownError, {
+ componentStack: expect.any(String),
+ errorBoundary: expect.any(Object),
+ })
+})
+
+test('onRecoverableError is supported in render', () => {
+ const onRecoverableError = jest.fn()
+
+ const container = document.createElement('div')
+ container.innerHTML = '
server
'
+ // We just hope we forwarded the callback correctly (which is guaranteed since we just pass it along)
+ // Frankly, I'm too lazy to assert on React 18 hydration errors since they're a mess.
+ // eslint-disable-next-line jest/no-conditional-in-test
+ if (isReact19) {
+ render(
client
, {
+ container,
+ hydrate: true,
+ onRecoverableError,
+ })
+ expect(onRecoverableError).toHaveBeenCalledTimes(1)
+ } else {
+ expect(() => {
+ render(
client
, {
+ container,
+ hydrate: true,
+ onRecoverableError,
+ })
+ }).toErrorDev(['', ''], {withoutStack: 1})
+ expect(onRecoverableError).toHaveBeenCalledTimes(2)
+ }
+})
+
+test('onUncaughtError is not supported in renderHook', () => {
+ function useThrower() {
+ throw new Error('Boom!')
+ }
+ const onUncaughtError = jest.fn(() => {})
+
+ expect(() => {
+ renderHook(useThrower, {
+ onUncaughtError(error, errorInfo) {
+ console.log({error, errorInfo})
+ },
+ })
+ }).toThrow(
+ 'onUncaughtError is not supported. The `render` call will already throw on uncaught errors.',
+ )
+
+ expect(onUncaughtError).toHaveBeenCalledTimes(0)
+})
+
+testGateReact19('onCaughtError is supported in renderHook', () => {
+ const thrownError = new Error('Boom!')
+ const handleComponentDidCatch = jest.fn()
+ const onCaughtError = jest.fn()
+ class ErrorBoundary extends React.Component {
+ state = {error: null}
+ static getDerivedStateFromError(error) {
+ return {error}
+ }
+ componentDidCatch(error, errorInfo) {
+ handleComponentDidCatch(error, errorInfo)
+ }
+ render() {
+ if (this.state.error) {
+ return null
+ }
+ return this.props.children
+ }
+ }
+ function useThrower() {
+ throw thrownError
+ }
+
+ renderHook(useThrower, {
+ onCaughtError,
+ wrapper: ErrorBoundary,
+ })
+
+ expect(onCaughtError).toHaveBeenCalledWith(thrownError, {
+ componentStack: expect.any(String),
+ errorBoundary: expect.any(Object),
+ })
+})
+
+// Currently, there's no recoverable error without hydration.
+// The option is still supported though.
+test('onRecoverableError is supported in renderHook', () => {
+ const onRecoverableError = jest.fn()
+
+ renderHook(
+ () => {
+ // TODO: trigger recoverable error
+ },
+ {
+ onRecoverableError,
+ },
+ )
+})
diff --git a/src/__tests__/events.js b/src/__tests__/events.js
index 73fd1175..587bfdae 100644
--- a/src/__tests__/events.js
+++ b/src/__tests__/events.js
@@ -1,5 +1,5 @@
-import React from 'react'
-import {renderIntoDocument, cleanup, fireEvent} from '../'
+import * as React from 'react'
+import {render, fireEvent} from '../'
const eventTypes = [
{
@@ -30,7 +30,7 @@ const eventTypes = [
},
{
type: 'Focus',
- events: ['change', 'input', 'invalid'],
+ events: ['input', 'invalid'],
elementType: 'input',
},
{
@@ -62,6 +62,22 @@ const eventTypes = [
],
elementType: 'button',
},
+ {
+ type: 'Pointer',
+ events: [
+ 'pointerOver',
+ 'pointerEnter',
+ 'pointerDown',
+ 'pointerMove',
+ 'pointerUp',
+ 'pointerCancel',
+ 'pointerOut',
+ 'pointerLeave',
+ 'gotPointerCapture',
+ 'lostPointerCapture',
+ ],
+ elementType: 'button',
+ },
{
type: 'Selection',
events: ['select'],
@@ -128,8 +144,6 @@ const eventTypes = [
},
]
-afterEach(cleanup)
-
eventTypes.forEach(({type, events, elementType, init}) => {
describe(`${type} Events`, () => {
events.forEach(eventName => {
@@ -141,7 +155,7 @@ eventTypes.forEach(({type, events, elementType, init}) => {
const ref = React.createRef()
const spy = jest.fn()
- renderIntoDocument(
+ render(
React.createElement(elementType, {
[propName]: spy,
ref,
@@ -154,3 +168,89 @@ eventTypes.forEach(({type, events, elementType, init}) => {
})
})
})
+
+eventTypes.forEach(({type, events, elementType, init}) => {
+ describe(`Native ${type} Events`, () => {
+ events.forEach(eventName => {
+ let nativeEventName = eventName.toLowerCase()
+
+ // The doubleClick synthetic event maps to the dblclick native event
+ if (nativeEventName === 'doubleclick') {
+ nativeEventName = 'dblclick'
+ }
+
+ it(`triggers native ${nativeEventName}`, () => {
+ const ref = React.createRef()
+ const spy = jest.fn()
+ const Element = elementType
+
+ const NativeEventElement = () => {
+ React.useEffect(() => {
+ const element = ref.current
+ element.addEventListener(nativeEventName, spy)
+ return () => {
+ element.removeEventListener(nativeEventName, spy)
+ }
+ })
+ return
+ }
+
+ render(
)
+
+ fireEvent[eventName](ref.current, init)
+ expect(spy).toHaveBeenCalledTimes(1)
+ })
+ })
+ })
+})
+
+test('onChange works', () => {
+ const handleChange = jest.fn()
+ const {
+ container: {firstChild: input},
+ } = render(
)
+ fireEvent.change(input, {target: {value: 'a'}})
+ expect(handleChange).toHaveBeenCalledTimes(1)
+})
+
+test('calling `fireEvent` directly works too', () => {
+ const handleEvent = jest.fn()
+ const {
+ container: {firstChild: button},
+ } = render(
)
+ fireEvent(
+ button,
+ new Event('MouseEvent', {
+ bubbles: true,
+ cancelable: true,
+ button: 0,
+ }),
+ )
+})
+
+test('blur/focus bubbles in react', () => {
+ const handleBlur = jest.fn()
+ const handleBubbledBlur = jest.fn()
+ const handleFocus = jest.fn()
+ const handleBubbledFocus = jest.fn()
+ const {container} = render(
+
+
+
,
+ )
+ const button = container.firstChild.firstChild
+
+ fireEvent.focus(button)
+
+ expect(handleBlur).toHaveBeenCalledTimes(0)
+ expect(handleBubbledBlur).toHaveBeenCalledTimes(0)
+ expect(handleFocus).toHaveBeenCalledTimes(1)
+ expect(handleBubbledFocus).toHaveBeenCalledTimes(1)
+
+ fireEvent.blur(button)
+
+ expect(handleBlur).toHaveBeenCalledTimes(1)
+ expect(handleBubbledBlur).toHaveBeenCalledTimes(1)
+ expect(handleFocus).toHaveBeenCalledTimes(1)
+ expect(handleBubbledFocus).toHaveBeenCalledTimes(1)
+})
diff --git a/src/__tests__/fetch.js b/src/__tests__/fetch.js
deleted file mode 100644
index 58a23fe4..00000000
--- a/src/__tests__/fetch.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import React from 'react'
-import axiosMock from 'axios'
-import {render, Simulate, wait} from '../'
-
-// instead of importing it, we'll define it inline here
-// import Fetch from '../fetch'
-
-class Fetch extends React.Component {
- state = {}
- componentDidUpdate(prevProps) {
- if (this.props.url !== prevProps.url) {
- this.fetch()
- }
- }
- fetch = async () => {
- const response = await axiosMock.get(this.props.url)
- this.setState({data: response.data})
- }
- render() {
- const {data} = this.state
- return (
-
- Fetch
- {data ? {data.greeting} : null}
-
- )
- }
-}
-
-test('Fetch makes an API call and displays the greeting when load-greeting is clicked', async () => {
- // Arrange
- axiosMock.get.mockResolvedValueOnce({data: {greeting: 'hello there'}})
- const url = '/greeting'
- const {container, getByText} = render(
)
-
- // Act
- Simulate.click(getByText('Fetch'))
-
- await wait()
-
- // Assert
- expect(axiosMock.get).toHaveBeenCalledTimes(1)
- expect(axiosMock.get).toHaveBeenCalledWith(url)
- // this assertion is funny because if the textContent were not "hello there"
- // then the `getByText` would throw anyway... 🤔
- expect(getByText('hello there').textContent).toBe('hello there')
- expect(container.firstChild).toMatchSnapshot()
-})
diff --git a/src/__tests__/forms.js b/src/__tests__/forms.js
deleted file mode 100644
index 4371f005..00000000
--- a/src/__tests__/forms.js
+++ /dev/null
@@ -1,60 +0,0 @@
-import React from 'react'
-import {render, Simulate} from '../'
-
-function Login({onSubmit}) {
- return (
-
-
-
- )
-}
-
-test('login form submits', () => {
- const fakeUser = {username: 'jackiechan', password: 'hiya! 🥋'}
- const handleSubmit = jest.fn()
- const {container, getByLabelText, getByText} = render(
-
,
- )
-
- const usernameNode = getByLabelText(/username/i)
- const passwordNode = getByLabelText(/password/i)
- const formNode = container.querySelector('form')
- const submitButtonNode = getByText(/submit/i)
-
- // Act
- usernameNode.value = fakeUser.username
- passwordNode.value = fakeUser.password
- // NOTE: in jsdom, it's not possible to trigger a form submission
- // by clicking on the submit button. This is really unfortunate.
- // So the next best thing is to simulate a submit on the form itself
- // then ensure that there's a submit button.
- Simulate.submit(formNode)
-
- // Assert
- expect(handleSubmit).toHaveBeenCalledTimes(1)
- expect(handleSubmit).toHaveBeenCalledWith(fakeUser)
- // make sure the form is submittable
- expect(submitButtonNode.type).toBe('submit')
-})
-
-/* eslint jsx-a11y/label-has-for:0 */
diff --git a/src/__tests__/multi-base.js b/src/__tests__/multi-base.js
new file mode 100644
index 00000000..ef5a7e11
--- /dev/null
+++ b/src/__tests__/multi-base.js
@@ -0,0 +1,45 @@
+import * as React from 'react'
+import {render} from '../'
+
+// these are created once per test suite and reused for each case
+let treeA, treeB
+beforeAll(() => {
+ treeA = document.createElement('div')
+ treeB = document.createElement('div')
+ document.body.appendChild(treeA)
+ document.body.appendChild(treeB)
+})
+
+afterAll(() => {
+ treeA.parentNode.removeChild(treeA)
+ treeB.parentNode.removeChild(treeB)
+})
+
+test('baseElement isolates trees from one another', () => {
+ const {getByText: getByTextInA} = render(
Jekyll
, {
+ baseElement: treeA,
+ })
+ const {getByText: getByTextInB} = render(
Hyde
, {
+ baseElement: treeB,
+ })
+
+ expect(() => getByTextInA('Jekyll')).not.toThrow(
+ 'Unable to find an element with the text: Jekyll.',
+ )
+ expect(() => getByTextInB('Jekyll')).toThrow(
+ 'Unable to find an element with the text: Jekyll.',
+ )
+
+ expect(() => getByTextInA('Hyde')).toThrow(
+ 'Unable to find an element with the text: Hyde.',
+ )
+ expect(() => getByTextInB('Hyde')).not.toThrow(
+ 'Unable to find an element with the text: Hyde.',
+ )
+})
+
+// https://github.com/testing-library/eslint-plugin-testing-library/issues/188
+/*
+eslint
+ testing-library/prefer-screen-queries: "off",
+*/
diff --git a/src/__tests__/new-act.js b/src/__tests__/new-act.js
new file mode 100644
index 00000000..0464ad24
--- /dev/null
+++ b/src/__tests__/new-act.js
@@ -0,0 +1,79 @@
+let asyncAct
+
+jest.mock('react', () => {
+ return {
+ ...jest.requireActual('react'),
+ act: cb => {
+ return cb()
+ },
+ }
+})
+
+beforeEach(() => {
+ jest.resetModules()
+ asyncAct = require('../act-compat').default
+ jest.spyOn(console, 'error').mockImplementation(() => {})
+})
+
+afterEach(() => {
+ jest.restoreAllMocks()
+})
+
+test('async act works when it does not exist (older versions of react)', async () => {
+ const callback = jest.fn()
+ await asyncAct(async () => {
+ await Promise.resolve()
+ await callback()
+ })
+ expect(console.error).toHaveBeenCalledTimes(0)
+ expect(callback).toHaveBeenCalledTimes(1)
+
+ callback.mockClear()
+ console.error.mockClear()
+
+ await asyncAct(async () => {
+ await Promise.resolve()
+ await callback()
+ })
+ expect(console.error).toHaveBeenCalledTimes(0)
+ expect(callback).toHaveBeenCalledTimes(1)
+})
+
+test('async act recovers from errors', async () => {
+ try {
+ await asyncAct(async () => {
+ await null
+ throw new Error('test error')
+ })
+ } catch (err) {
+ console.error('call console.error')
+ }
+ expect(console.error).toHaveBeenCalledTimes(1)
+ expect(console.error.mock.calls).toMatchInlineSnapshot(`
+ [
+ [
+ call console.error,
+ ],
+ ]
+ `)
+})
+
+test('async act recovers from sync errors', async () => {
+ try {
+ await asyncAct(() => {
+ throw new Error('test error')
+ })
+ } catch (err) {
+ console.error('call console.error')
+ }
+ expect(console.error).toHaveBeenCalledTimes(1)
+ expect(console.error.mock.calls).toMatchInlineSnapshot(`
+ [
+ [
+ call console.error,
+ ],
+ ]
+ `)
+})
+
+/* eslint no-console:0 */
diff --git a/src/__tests__/render-into-document.js b/src/__tests__/render-into-document.js
deleted file mode 100644
index 886e239a..00000000
--- a/src/__tests__/render-into-document.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import React from 'react'
-import {renderIntoDocument, cleanup} from '../'
-
-afterEach(cleanup)
-
-it('renders button into document', () => {
- const ref = React.createRef()
- const {container} = renderIntoDocument(
)
- expect(container.firstChild).toBe(ref.current)
-})
-
-it('cleansup document', () => {
- const spy = jest.fn()
-
- class Test extends React.Component {
- componentWillUnmount() {
- spy()
- }
-
- render() {
- return
- }
- }
-
- renderIntoDocument(
)
- cleanup()
- expect(document.body.innerHTML).toBe('')
- expect(spy).toHaveBeenCalledTimes(1)
-})
diff --git a/src/__tests__/render.js b/src/__tests__/render.js
new file mode 100644
index 00000000..6f5b5b39
--- /dev/null
+++ b/src/__tests__/render.js
@@ -0,0 +1,299 @@
+import * as React from 'react'
+import ReactDOM from 'react-dom'
+import ReactDOMServer from 'react-dom/server'
+import {fireEvent, render, screen, configure} from '../'
+
+const isReact18 = React.version.startsWith('18.')
+const isReact19 = React.version.startsWith('19.')
+
+const testGateReact18 = isReact18 ? test : test.skip
+const testGateReact19 = isReact19 ? test : test.skip
+
+describe('render API', () => {
+ let originalConfig
+ beforeEach(() => {
+ // Grab the existing configuration so we can restore
+ // it at the end of the test
+ configure(existingConfig => {
+ originalConfig = existingConfig
+ // Don't change the existing config
+ return {}
+ })
+ })
+
+ afterEach(() => {
+ configure(originalConfig)
+ })
+
+ test('renders div into document', () => {
+ const ref = React.createRef()
+ const {container} = render(
)
+ expect(container.firstChild).toBe(ref.current)
+ })
+
+ test('works great with react portals', () => {
+ class MyPortal extends React.Component {
+ constructor(...args) {
+ super(...args)
+ this.portalNode = document.createElement('div')
+ this.portalNode.dataset.testid = 'my-portal'
+ }
+ componentDidMount() {
+ document.body.appendChild(this.portalNode)
+ }
+ componentWillUnmount() {
+ this.portalNode.parentNode.removeChild(this.portalNode)
+ }
+ render() {
+ return ReactDOM.createPortal(
+
,
+ this.portalNode,
+ )
+ }
+ }
+
+ function Greet({greeting, subject}) {
+ return (
+
+
+ {greeting} {subject}
+
+
+ )
+ }
+
+ const {unmount} = render(
)
+ expect(screen.getByText('Hello World')).toBeInTheDocument()
+ const portalNode = screen.getByTestId('my-portal')
+ expect(portalNode).toBeInTheDocument()
+ unmount()
+ expect(portalNode).not.toBeInTheDocument()
+ })
+
+ test('returns baseElement which defaults to document.body', () => {
+ const {baseElement} = render(
)
+ expect(baseElement).toBe(document.body)
+ })
+
+ test('supports fragments', () => {
+ class Test extends React.Component {
+ render() {
+ return (
+
+ DocumentFragment
is pretty cool!
+
+ )
+ }
+ }
+
+ const {asFragment} = render(
)
+ expect(asFragment()).toMatchSnapshot()
+ })
+
+ test('renders options.wrapper around node', () => {
+ const WrapperComponent = ({children}) => (
+
{children}
+ )
+
+ const {container} = render(
, {
+ wrapper: WrapperComponent,
+ })
+
+ expect(screen.getByTestId('wrapper')).toBeInTheDocument()
+ expect(container.firstChild).toMatchInlineSnapshot(`
+
+ `)
+ })
+
+ test('renders options.wrapper around node when reactStrictMode is true', () => {
+ configure({reactStrictMode: true})
+
+ const WrapperComponent = ({children}) => (
+
{children}
+ )
+ const {container} = render(
, {
+ wrapper: WrapperComponent,
+ })
+
+ expect(screen.getByTestId('wrapper')).toBeInTheDocument()
+ expect(container.firstChild).toMatchInlineSnapshot(`
+
+ `)
+ })
+
+ test('renders twice when reactStrictMode is true', () => {
+ configure({reactStrictMode: true})
+
+ const spy = jest.fn()
+ function Component() {
+ spy()
+ return null
+ }
+
+ render(
)
+ expect(spy).toHaveBeenCalledTimes(2)
+ })
+
+ test('flushes useEffect cleanup functions sync on unmount()', () => {
+ const spy = jest.fn()
+ function Component() {
+ React.useEffect(() => spy, [])
+ return null
+ }
+ const {unmount} = render(
)
+ expect(spy).toHaveBeenCalledTimes(0)
+
+ unmount()
+
+ expect(spy).toHaveBeenCalledTimes(1)
+ })
+
+ test('can be called multiple times on the same container', () => {
+ const container = document.createElement('div')
+
+ const {unmount} = render(
, {container})
+
+ expect(container).toContainHTML('
')
+
+ render(
, {container})
+
+ expect(container).toContainHTML('
')
+
+ unmount()
+
+ expect(container).toBeEmptyDOMElement()
+ })
+
+ test('hydrate will make the UI interactive', () => {
+ function App() {
+ const [clicked, handleClick] = React.useReducer(n => n + 1, 0)
+
+ return (
+
+ clicked:{clicked}
+
+ )
+ }
+ const ui =
+ const container = document.createElement('div')
+ document.body.appendChild(container)
+ container.innerHTML = ReactDOMServer.renderToString(ui)
+
+ expect(container).toHaveTextContent('clicked:0')
+
+ render(ui, {container, hydrate: true})
+
+ fireEvent.click(container.querySelector('button'))
+
+ expect(container).toHaveTextContent('clicked:1')
+ })
+
+ test('hydrate can have a wrapper', () => {
+ const wrapperComponentMountEffect = jest.fn()
+ function WrapperComponent({children}) {
+ React.useEffect(() => {
+ wrapperComponentMountEffect()
+ })
+
+ return children
+ }
+ const ui =
+ const container = document.createElement('div')
+ document.body.appendChild(container)
+ container.innerHTML = ReactDOMServer.renderToString(ui)
+
+ render(ui, {container, hydrate: true, wrapper: WrapperComponent})
+
+ expect(wrapperComponentMountEffect).toHaveBeenCalledTimes(1)
+ })
+
+ testGateReact18('legacyRoot uses legacy ReactDOM.render', () => {
+ expect(() => {
+ render(
, {legacyRoot: true})
+ }).toErrorDev(
+ [
+ "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",
+ ],
+ {withoutStack: true},
+ )
+ })
+
+ testGateReact19('legacyRoot throws', () => {
+ expect(() => {
+ render(
, {legacyRoot: true})
+ }).toThrowErrorMatchingInlineSnapshot(
+ `\`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.`,
+ )
+ })
+
+ testGateReact18('legacyRoot uses legacy ReactDOM.hydrate', () => {
+ const ui =
+ const container = document.createElement('div')
+ container.innerHTML = ReactDOMServer.renderToString(ui)
+ expect(() => {
+ render(ui, {container, hydrate: true, legacyRoot: true})
+ }).toErrorDev(
+ [
+ "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",
+ ],
+ {withoutStack: true},
+ )
+ })
+
+ testGateReact19('legacyRoot throws even with hydrate', () => {
+ const ui =
+ const container = document.createElement('div')
+ container.innerHTML = ReactDOMServer.renderToString(ui)
+ expect(() => {
+ render(ui, {container, hydrate: true, legacyRoot: true})
+ }).toThrowErrorMatchingInlineSnapshot(
+ `\`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.`,
+ )
+ })
+
+ test('reactStrictMode in renderOptions has precedence over config when rendering', () => {
+ const wrapperComponentMountEffect = jest.fn()
+ function WrapperComponent({children}) {
+ React.useEffect(() => {
+ wrapperComponentMountEffect()
+ })
+
+ return children
+ }
+ const ui =
+ configure({reactStrictMode: false})
+
+ render(ui, {wrapper: WrapperComponent, reactStrictMode: true})
+
+ expect(wrapperComponentMountEffect).toHaveBeenCalledTimes(2)
+ })
+
+ test('reactStrictMode in config is used when renderOptions does not specify reactStrictMode', () => {
+ const wrapperComponentMountEffect = jest.fn()
+ function WrapperComponent({children}) {
+ React.useEffect(() => {
+ wrapperComponentMountEffect()
+ })
+
+ return children
+ }
+ const ui =
+ configure({reactStrictMode: true})
+
+ render(ui, {wrapper: WrapperComponent})
+
+ expect(wrapperComponentMountEffect).toHaveBeenCalledTimes(2)
+ })
+})
diff --git a/src/__tests__/renderHook.js b/src/__tests__/renderHook.js
new file mode 100644
index 00000000..f331e90e
--- /dev/null
+++ b/src/__tests__/renderHook.js
@@ -0,0 +1,141 @@
+import React, {useEffect} from 'react'
+import {configure, renderHook} from '../pure'
+
+const isReact18 = React.version.startsWith('18.')
+const isReact19 = React.version.startsWith('19.')
+
+const testGateReact18 = isReact18 ? test : test.skip
+const testGateReact19 = isReact19 ? test : test.skip
+
+test('gives committed result', () => {
+ const {result} = renderHook(() => {
+ const [state, setState] = React.useState(1)
+
+ React.useEffect(() => {
+ setState(2)
+ }, [])
+
+ return [state, setState]
+ })
+
+ expect(result.current).toEqual([2, expect.any(Function)])
+})
+
+test('allows rerendering', () => {
+ const {result, rerender} = renderHook(
+ ({branch}) => {
+ const [left, setLeft] = React.useState('left')
+ const [right, setRight] = React.useState('right')
+
+ // eslint-disable-next-line jest/no-if, jest/no-conditional-in-test -- false-positive
+ switch (branch) {
+ case 'left':
+ return [left, setLeft]
+ case 'right':
+ return [right, setRight]
+
+ default:
+ throw new Error(
+ 'No Props passed. This is a bug in the implementation',
+ )
+ }
+ },
+ {initialProps: {branch: 'left'}},
+ )
+
+ expect(result.current).toEqual(['left', expect.any(Function)])
+
+ rerender({branch: 'right'})
+
+ expect(result.current).toEqual(['right', expect.any(Function)])
+})
+
+test('allows wrapper components', async () => {
+ const Context = React.createContext('default')
+ function Wrapper({children}) {
+ return
{children}
+ }
+ const {result} = renderHook(
+ () => {
+ return React.useContext(Context)
+ },
+ {
+ wrapper: Wrapper,
+ },
+ )
+
+ expect(result.current).toEqual('provided')
+})
+
+testGateReact18('legacyRoot uses legacy ReactDOM.render', () => {
+ const Context = React.createContext('default')
+ function Wrapper({children}) {
+ return
{children}
+ }
+ let result
+ expect(() => {
+ result = renderHook(
+ () => {
+ return React.useContext(Context)
+ },
+ {
+ wrapper: Wrapper,
+ legacyRoot: true,
+ },
+ ).result
+ }).toErrorDev(
+ [
+ "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",
+ ],
+ {withoutStack: true},
+ )
+ expect(result.current).toEqual('provided')
+})
+
+testGateReact19('legacyRoot throws', () => {
+ const Context = React.createContext('default')
+ function Wrapper({children}) {
+ return
{children}
+ }
+ expect(() => {
+ renderHook(
+ () => {
+ return React.useContext(Context)
+ },
+ {
+ wrapper: Wrapper,
+ legacyRoot: true,
+ },
+ ).result
+ }).toThrowErrorMatchingInlineSnapshot(
+ `\`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.`,
+ )
+})
+
+describe('reactStrictMode', () => {
+ let originalConfig
+ beforeEach(() => {
+ // Grab the existing configuration so we can restore
+ // it at the end of the test
+ configure(existingConfig => {
+ originalConfig = existingConfig
+ // Don't change the existing config
+ return {}
+ })
+ })
+
+ afterEach(() => {
+ configure(originalConfig)
+ })
+
+ test('reactStrictMode in renderOptions has precedence over config when rendering', () => {
+ const hookMountEffect = jest.fn()
+ configure({reactStrictMode: false})
+
+ renderHook(() => useEffect(() => hookMountEffect()), {
+ reactStrictMode: true,
+ })
+
+ expect(hookMountEffect).toHaveBeenCalledTimes(2)
+ })
+})
diff --git a/src/__tests__/rerender.js b/src/__tests__/rerender.js
index 2a1c332d..6c48c4dd 100644
--- a/src/__tests__/rerender.js
+++ b/src/__tests__/rerender.js
@@ -1,11 +1,98 @@
-import React from 'react'
-import {render} from '../'
-import 'jest-dom/extend-expect'
-
-test('rerender will re-render the element', () => {
- const Greeting = props =>
{props.message}
- const {container, rerender} = render(
)
- expect(container.firstChild).toHaveTextContent('hi')
- rerender(
)
- expect(container.firstChild).toHaveTextContent('hey')
+import * as React from 'react'
+import {render, configure} from '../'
+
+describe('rerender API', () => {
+ let originalConfig
+ beforeEach(() => {
+ // Grab the existing configuration so we can restore
+ // it at the end of the test
+ configure(existingConfig => {
+ originalConfig = existingConfig
+ // Don't change the existing config
+ return {}
+ })
+ })
+
+ afterEach(() => {
+ configure(originalConfig)
+ })
+
+ test('rerender will re-render the element', () => {
+ const Greeting = props =>
{props.message}
+ const {container, rerender} = render(
)
+ expect(container.firstChild).toHaveTextContent('hi')
+ rerender(
)
+ expect(container.firstChild).toHaveTextContent('hey')
+ })
+
+ test('hydrate will not update props until next render', () => {
+ const initialInputElement = document.createElement('input')
+ const container = document.createElement('div')
+ container.appendChild(initialInputElement)
+ document.body.appendChild(container)
+
+ const firstValue = 'hello'
+ initialInputElement.value = firstValue
+
+ const {rerender} = render(
null} />, {
+ container,
+ hydrate: true,
+ })
+
+ expect(initialInputElement).toHaveValue(firstValue)
+
+ const secondValue = 'goodbye'
+ rerender(
null} />)
+ expect(initialInputElement).toHaveValue(secondValue)
+ })
+
+ test('re-renders options.wrapper around node when reactStrictMode is true', () => {
+ configure({reactStrictMode: true})
+
+ const WrapperComponent = ({children}) => (
+
{children}
+ )
+ const Greeting = props =>
{props.message}
+ const {container, rerender} = render(
, {
+ wrapper: WrapperComponent,
+ })
+
+ expect(container.firstChild).toMatchInlineSnapshot(`
+
+ `)
+
+ rerender(
)
+ expect(container.firstChild).toMatchInlineSnapshot(`
+
+ `)
+ })
+
+ test('re-renders twice when reactStrictMode is true', () => {
+ configure({reactStrictMode: true})
+
+ const spy = jest.fn()
+ function Component() {
+ spy()
+ return null
+ }
+
+ const {rerender} = render(
)
+ expect(spy).toHaveBeenCalledTimes(2)
+
+ spy.mockClear()
+ rerender(
)
+ expect(spy).toHaveBeenCalledTimes(2)
+ })
})
diff --git a/src/__tests__/stopwatch.js b/src/__tests__/stopwatch.js
index 1dab173a..e3eaebbe 100644
--- a/src/__tests__/stopwatch.js
+++ b/src/__tests__/stopwatch.js
@@ -1,5 +1,5 @@
-import React from 'react'
-import {render, Simulate} from '../'
+import * as React from 'react'
+import {render, fireEvent, screen} from '../'
class StopWatch extends React.Component {
state = {lapse: 0, running: false}
@@ -37,21 +37,18 @@ class StopWatch extends React.Component {
}
}
-const wait = time => new Promise(resolve => setTimeout(resolve, time))
+const sleep = t => new Promise(resolve => setTimeout(resolve, t))
test('unmounts a component', async () => {
- jest.spyOn(console, 'error').mockImplementation(() => {})
- const {unmount, getByText, container} = render(
)
- Simulate.click(getByText('Start'))
+ const {unmount, container} = render(
)
+ fireEvent.click(screen.getByText('Start'))
unmount()
// hey there reader! You don't need to have an assertion like this one
// this is just me making sure that the unmount function works.
// You don't need to do this in your apps. Just rely on the fact that this works.
- expect(container.innerHTML).toBe('')
+ expect(container).toBeEmptyDOMElement()
// just wait to see if the interval is cleared or not
// if it's not, then we'll call setState on an unmounted component
// and get an error.
- await wait()
- // eslint-disable-next-line no-console
- expect(console.error).not.toHaveBeenCalled()
+ await sleep(5)
})
diff --git a/src/act-compat.js b/src/act-compat.js
new file mode 100644
index 00000000..6eaec0fb
--- /dev/null
+++ b/src/act-compat.js
@@ -0,0 +1,91 @@
+import * as React from 'react'
+import * as DeprecatedReactTestUtils from 'react-dom/test-utils'
+
+const reactAct =
+ typeof React.act === 'function' ? React.act : DeprecatedReactTestUtils.act
+
+function getGlobalThis() {
+ /* istanbul ignore else */
+ if (typeof globalThis !== 'undefined') {
+ return globalThis
+ }
+ /* istanbul ignore next */
+ if (typeof self !== 'undefined') {
+ return self
+ }
+ /* istanbul ignore next */
+ if (typeof window !== 'undefined') {
+ return window
+ }
+ /* istanbul ignore next */
+ if (typeof global !== 'undefined') {
+ return global
+ }
+ /* istanbul ignore next */
+ throw new Error('unable to locate global object')
+}
+
+function setIsReactActEnvironment(isReactActEnvironment) {
+ getGlobalThis().IS_REACT_ACT_ENVIRONMENT = isReactActEnvironment
+}
+
+function getIsReactActEnvironment() {
+ return getGlobalThis().IS_REACT_ACT_ENVIRONMENT
+}
+
+function withGlobalActEnvironment(actImplementation) {
+ return callback => {
+ const previousActEnvironment = getIsReactActEnvironment()
+ setIsReactActEnvironment(true)
+ try {
+ // The return value of `act` is always a thenable.
+ let callbackNeedsToBeAwaited = false
+ const actResult = actImplementation(() => {
+ const result = callback()
+ if (
+ result !== null &&
+ typeof result === 'object' &&
+ typeof result.then === 'function'
+ ) {
+ callbackNeedsToBeAwaited = true
+ }
+ return result
+ })
+ if (callbackNeedsToBeAwaited) {
+ const thenable = actResult
+ return {
+ then: (resolve, reject) => {
+ thenable.then(
+ returnValue => {
+ setIsReactActEnvironment(previousActEnvironment)
+ resolve(returnValue)
+ },
+ error => {
+ setIsReactActEnvironment(previousActEnvironment)
+ reject(error)
+ },
+ )
+ },
+ }
+ } else {
+ setIsReactActEnvironment(previousActEnvironment)
+ return actResult
+ }
+ } catch (error) {
+ // Can't be a `finally {}` block since we don't know if we have to immediately restore IS_REACT_ACT_ENVIRONMENT
+ // or if we have to await the callback first.
+ setIsReactActEnvironment(previousActEnvironment)
+ throw error
+ }
+ }
+}
+
+const act = withGlobalActEnvironment(reactAct)
+
+export default act
+export {
+ setIsReactActEnvironment as setReactActEnvironment,
+ getIsReactActEnvironment,
+}
+
+/* eslint no-console:0 */
diff --git a/src/config.js b/src/config.js
new file mode 100644
index 00000000..dc8a5035
--- /dev/null
+++ b/src/config.js
@@ -0,0 +1,34 @@
+import {
+ getConfig as getConfigDTL,
+ configure as configureDTL,
+} from '@testing-library/dom'
+
+let configForRTL = {
+ reactStrictMode: false,
+}
+
+function getConfig() {
+ return {
+ ...getConfigDTL(),
+ ...configForRTL,
+ }
+}
+
+function configure(newConfig) {
+ if (typeof newConfig === 'function') {
+ // Pass the existing config out to the provided function
+ // and accept a delta in return
+ newConfig = newConfig(getConfig())
+ }
+
+ const {reactStrictMode, ...configForDTL} = newConfig
+
+ configureDTL(configForDTL)
+
+ configForRTL = {
+ ...configForRTL,
+ reactStrictMode,
+ }
+}
+
+export {getConfig, configure}
diff --git a/src/fire-event.js b/src/fire-event.js
new file mode 100644
index 00000000..cb790c7f
--- /dev/null
+++ b/src/fire-event.js
@@ -0,0 +1,69 @@
+import {fireEvent as dtlFireEvent} from '@testing-library/dom'
+
+// react-testing-library's version of fireEvent will call
+// dom-testing-library's version of fireEvent. The reason
+// we make this distinction however is because we have
+// a few extra events that work a bit differently
+const fireEvent = (...args) => dtlFireEvent(...args)
+
+Object.keys(dtlFireEvent).forEach(key => {
+ fireEvent[key] = (...args) => dtlFireEvent[key](...args)
+})
+
+// React event system tracks native mouseOver/mouseOut events for
+// running onMouseEnter/onMouseLeave handlers
+// @link https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/react-dom/src/events/EnterLeaveEventPlugin.js#L24-L31
+const mouseEnter = fireEvent.mouseEnter
+const mouseLeave = fireEvent.mouseLeave
+fireEvent.mouseEnter = (...args) => {
+ mouseEnter(...args)
+ return fireEvent.mouseOver(...args)
+}
+fireEvent.mouseLeave = (...args) => {
+ mouseLeave(...args)
+ return fireEvent.mouseOut(...args)
+}
+
+const pointerEnter = fireEvent.pointerEnter
+const pointerLeave = fireEvent.pointerLeave
+fireEvent.pointerEnter = (...args) => {
+ pointerEnter(...args)
+ return fireEvent.pointerOver(...args)
+}
+fireEvent.pointerLeave = (...args) => {
+ pointerLeave(...args)
+ return fireEvent.pointerOut(...args)
+}
+
+const select = fireEvent.select
+fireEvent.select = (node, init) => {
+ select(node, init)
+ // React tracks this event only on focused inputs
+ node.focus()
+
+ // React creates this event when one of the following native events happens
+ // - contextMenu
+ // - mouseUp
+ // - dragEnd
+ // - keyUp
+ // - keyDown
+ // so we can use any here
+ // @link https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/react-dom/src/events/SelectEventPlugin.js#L203-L224
+ fireEvent.keyUp(node, init)
+}
+
+// React event system tracks native focusout/focusin events for
+// running blur/focus handlers
+// @link https://github.com/facebook/react/pull/19186
+const blur = fireEvent.blur
+const focus = fireEvent.focus
+fireEvent.blur = (...args) => {
+ fireEvent.focusOut(...args)
+ return blur(...args)
+}
+fireEvent.focus = (...args) => {
+ fireEvent.focusIn(...args)
+ return focus(...args)
+}
+
+export {fireEvent}
diff --git a/src/index.js b/src/index.js
index c23b88bb..bb0d0270 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,47 +1,41 @@
-import ReactDOM from 'react-dom'
-import {Simulate} from 'react-dom/test-utils'
-import {getQueriesForElement, prettyDOM} from 'dom-testing-library'
+import {getIsReactActEnvironment, setReactActEnvironment} from './act-compat'
+import {cleanup} from './pure'
-function render(ui, {container = document.createElement('div')} = {}) {
- ReactDOM.render(ui, container)
- return {
- container,
- // eslint-disable-next-line no-console
- debug: () => console.log(prettyDOM(container)),
- unmount: () => ReactDOM.unmountComponentAtNode(container),
- rerender: rerenderUi => {
- render(rerenderUi, {container})
- // Intentionally do not return anything to avoid unnecessarily complicating the API.
- // folks can use all the same utilities we return in the first place that are bound to the container
- },
- ...getQueriesForElement(container),
+// if we're running in a test runner that supports afterEach
+// or teardown then we'll automatically run cleanup afterEach test
+// this ensures that tests run in isolation from each other
+// if you don't like this then either import the `pure` module
+// or set the RTL_SKIP_AUTO_CLEANUP env variable to 'true'.
+if (typeof process === 'undefined' || !process.env?.RTL_SKIP_AUTO_CLEANUP) {
+ // ignore teardown() in code coverage because Jest does not support it
+ /* istanbul ignore else */
+ if (typeof afterEach === 'function') {
+ afterEach(() => {
+ cleanup()
+ })
+ } else if (typeof teardown === 'function') {
+ // Block is guarded by `typeof` check.
+ // eslint does not support `typeof` guards.
+ // eslint-disable-next-line no-undef
+ teardown(() => {
+ cleanup()
+ })
}
-}
-
-const mountedContainers = new Set()
-function renderIntoDocument(ui) {
- const container = document.body.appendChild(document.createElement('div'))
- mountedContainers.add(container)
- return render(ui, {container})
-}
+ // No test setup with other test runners available
+ /* istanbul ignore else */
+ if (typeof beforeAll === 'function' && typeof afterAll === 'function') {
+ // This matches the behavior of React < 18.
+ let previousIsReactActEnvironment = getIsReactActEnvironment()
+ beforeAll(() => {
+ previousIsReactActEnvironment = getIsReactActEnvironment()
+ setReactActEnvironment(true)
+ })
-function cleanup() {
- mountedContainers.forEach(container => {
- document.body.removeChild(container)
- ReactDOM.unmountComponentAtNode(container)
- mountedContainers.delete(container)
- })
+ afterAll(() => {
+ setReactActEnvironment(previousIsReactActEnvironment)
+ })
+ }
}
-// fallback to synthetic events for React events that the DOM doesn't support
-const syntheticEvents = ['change', 'select', 'mouseEnter', 'mouseLeave']
-syntheticEvents.forEach(eventName => {
- document.addEventListener(eventName.toLowerCase(), e => {
- Simulate[eventName](e.target, e)
- })
-})
-
-// just re-export everything from dom-testing-library
-export * from 'dom-testing-library'
-export {render, Simulate, renderIntoDocument, cleanup}
+export * from './pure'
diff --git a/src/pure.js b/src/pure.js
new file mode 100644
index 00000000..0f9c487d
--- /dev/null
+++ b/src/pure.js
@@ -0,0 +1,363 @@
+import * as React from 'react'
+import ReactDOM from 'react-dom'
+import * as ReactDOMClient from 'react-dom/client'
+import {
+ getQueriesForElement,
+ prettyDOM,
+ configure as configureDTL,
+} from '@testing-library/dom'
+import act, {
+ getIsReactActEnvironment,
+ setReactActEnvironment,
+} from './act-compat'
+import {fireEvent} from './fire-event'
+import {getConfig, configure} from './config'
+
+function jestFakeTimersAreEnabled() {
+ /* istanbul ignore else */
+ if (typeof jest !== 'undefined' && jest !== null) {
+ return (
+ // legacy timers
+ setTimeout._isMockFunction === true || // modern timers
+ // eslint-disable-next-line prefer-object-has-own -- No Object.hasOwn in all target environments we support.
+ Object.prototype.hasOwnProperty.call(setTimeout, 'clock')
+ )
+ } // istanbul ignore next
+
+ return false
+}
+
+configureDTL({
+ unstable_advanceTimersWrapper: cb => {
+ return act(cb)
+ },
+ // We just want to run `waitFor` without IS_REACT_ACT_ENVIRONMENT
+ // But that's not necessarily how `asyncWrapper` is used since it's a public method.
+ // Let's just hope nobody else is using it.
+ asyncWrapper: async cb => {
+ const previousActEnvironment = getIsReactActEnvironment()
+ setReactActEnvironment(false)
+ try {
+ const result = await cb()
+ // Drain microtask queue.
+ // Otherwise we'll restore the previous act() environment, before we resolve the `waitFor` call.
+ // The caller would have no chance to wrap the in-flight Promises in `act()`
+ await new Promise(resolve => {
+ setTimeout(() => {
+ resolve()
+ }, 0)
+
+ if (jestFakeTimersAreEnabled()) {
+ jest.advanceTimersByTime(0)
+ }
+ })
+
+ return result
+ } finally {
+ setReactActEnvironment(previousActEnvironment)
+ }
+ },
+ eventWrapper: cb => {
+ let result
+ act(() => {
+ result = cb()
+ })
+ return result
+ },
+})
+
+// Ideally we'd just use a WeakMap where containers are keys and roots are values.
+// We use two variables so that we can bail out in constant time when we render with a new container (most common use case)
+/**
+ * @type {Set
}
+ */
+const mountedContainers = new Set()
+/**
+ * @type Array<{container: import('react-dom').Container, root: ReturnType}>
+ */
+const mountedRootEntries = []
+
+function strictModeIfNeeded(innerElement, reactStrictMode) {
+ return reactStrictMode ?? getConfig().reactStrictMode
+ ? React.createElement(React.StrictMode, null, innerElement)
+ : innerElement
+}
+
+function wrapUiIfNeeded(innerElement, wrapperComponent) {
+ return wrapperComponent
+ ? React.createElement(wrapperComponent, null, innerElement)
+ : innerElement
+}
+
+function createConcurrentRoot(
+ container,
+ {
+ hydrate,
+ onCaughtError,
+ onRecoverableError,
+ ui,
+ wrapper: WrapperComponent,
+ reactStrictMode,
+ },
+) {
+ let root
+ if (hydrate) {
+ act(() => {
+ root = ReactDOMClient.hydrateRoot(
+ container,
+ strictModeIfNeeded(
+ wrapUiIfNeeded(ui, WrapperComponent),
+ reactStrictMode,
+ ),
+ {onCaughtError, onRecoverableError},
+ )
+ })
+ } else {
+ root = ReactDOMClient.createRoot(container, {
+ onCaughtError,
+ onRecoverableError,
+ })
+ }
+
+ return {
+ hydrate() {
+ /* istanbul ignore if */
+ if (!hydrate) {
+ throw new Error(
+ 'Attempted to hydrate a non-hydrateable root. This is a bug in `@testing-library/react`.',
+ )
+ }
+ // Nothing to do since hydration happens when creating the root object.
+ },
+ render(element) {
+ root.render(element)
+ },
+ unmount() {
+ root.unmount()
+ },
+ }
+}
+
+function createLegacyRoot(container) {
+ return {
+ hydrate(element) {
+ ReactDOM.hydrate(element, container)
+ },
+ render(element) {
+ ReactDOM.render(element, container)
+ },
+ unmount() {
+ ReactDOM.unmountComponentAtNode(container)
+ },
+ }
+}
+
+function renderRoot(
+ ui,
+ {
+ baseElement,
+ container,
+ hydrate,
+ queries,
+ root,
+ wrapper: WrapperComponent,
+ reactStrictMode,
+ },
+) {
+ act(() => {
+ if (hydrate) {
+ root.hydrate(
+ strictModeIfNeeded(
+ wrapUiIfNeeded(ui, WrapperComponent),
+ reactStrictMode,
+ ),
+ container,
+ )
+ } else {
+ root.render(
+ strictModeIfNeeded(
+ wrapUiIfNeeded(ui, WrapperComponent),
+ reactStrictMode,
+ ),
+ container,
+ )
+ }
+ })
+
+ return {
+ container,
+ baseElement,
+ debug: (el = baseElement, maxLength, options) =>
+ Array.isArray(el)
+ ? // eslint-disable-next-line no-console
+ el.forEach(e => console.log(prettyDOM(e, maxLength, options)))
+ : // eslint-disable-next-line no-console,
+ console.log(prettyDOM(el, maxLength, options)),
+ unmount: () => {
+ act(() => {
+ root.unmount()
+ })
+ },
+ rerender: rerenderUi => {
+ renderRoot(rerenderUi, {
+ container,
+ baseElement,
+ root,
+ wrapper: WrapperComponent,
+ reactStrictMode,
+ })
+ // Intentionally do not return anything to avoid unnecessarily complicating the API.
+ // folks can use all the same utilities we return in the first place that are bound to the container
+ },
+ asFragment: () => {
+ /* istanbul ignore else (old jsdom limitation) */
+ if (typeof document.createRange === 'function') {
+ return document
+ .createRange()
+ .createContextualFragment(container.innerHTML)
+ } else {
+ const template = document.createElement('template')
+ template.innerHTML = container.innerHTML
+ return template.content
+ }
+ },
+ ...getQueriesForElement(baseElement, queries),
+ }
+}
+
+function render(
+ ui,
+ {
+ container,
+ baseElement = container,
+ legacyRoot = false,
+ onCaughtError,
+ onUncaughtError,
+ onRecoverableError,
+ queries,
+ hydrate = false,
+ wrapper,
+ reactStrictMode,
+ } = {},
+) {
+ if (onUncaughtError !== undefined) {
+ throw new Error(
+ 'onUncaughtError is not supported. The `render` call will already throw on uncaught errors.',
+ )
+ }
+ if (legacyRoot && typeof ReactDOM.render !== 'function') {
+ const error = new Error(
+ '`legacyRoot: true` is not supported in this version of React. ' +
+ 'If your app runs React 19 or later, you should remove this flag. ' +
+ 'If your app runs React 18 or earlier, visit https://react.dev/blog/2022/03/08/react-18-upgrade-guide for upgrade instructions.',
+ )
+ Error.captureStackTrace(error, render)
+ throw error
+ }
+
+ if (!baseElement) {
+ // default to document.body instead of documentElement to avoid output of potentially-large
+ // head elements (such as JSS style blocks) in debug output
+ baseElement = document.body
+ }
+ if (!container) {
+ container = baseElement.appendChild(document.createElement('div'))
+ }
+
+ let root
+ // eslint-disable-next-line no-negated-condition -- we want to map the evolution of this over time. The root is created first. Only later is it re-used so we don't want to read the case that happens later first.
+ if (!mountedContainers.has(container)) {
+ const createRootImpl = legacyRoot ? createLegacyRoot : createConcurrentRoot
+ root = createRootImpl(container, {
+ hydrate,
+ onCaughtError,
+ onRecoverableError,
+ ui,
+ wrapper,
+ reactStrictMode,
+ })
+
+ mountedRootEntries.push({container, root})
+ // we'll add it to the mounted containers regardless of whether it's actually
+ // added to document.body so the cleanup method works regardless of whether
+ // they're passing us a custom container or not.
+ mountedContainers.add(container)
+ } else {
+ mountedRootEntries.forEach(rootEntry => {
+ // Else is unreachable since `mountedContainers` has the `container`.
+ // Only reachable if one would accidentally add the container to `mountedContainers` but not the root to `mountedRootEntries`
+ /* istanbul ignore else */
+ if (rootEntry.container === container) {
+ root = rootEntry.root
+ }
+ })
+ }
+
+ return renderRoot(ui, {
+ container,
+ baseElement,
+ queries,
+ hydrate,
+ wrapper,
+ root,
+ reactStrictMode,
+ })
+}
+
+function cleanup() {
+ mountedRootEntries.forEach(({root, container}) => {
+ act(() => {
+ root.unmount()
+ })
+ if (container.parentNode === document.body) {
+ document.body.removeChild(container)
+ }
+ })
+ mountedRootEntries.length = 0
+ mountedContainers.clear()
+}
+
+function renderHook(renderCallback, options = {}) {
+ const {initialProps, ...renderOptions} = options
+
+ if (renderOptions.legacyRoot && typeof ReactDOM.render !== 'function') {
+ const error = new Error(
+ '`legacyRoot: true` is not supported in this version of React. ' +
+ 'If your app runs React 19 or later, you should remove this flag. ' +
+ 'If your app runs React 18 or earlier, visit https://react.dev/blog/2022/03/08/react-18-upgrade-guide for upgrade instructions.',
+ )
+ Error.captureStackTrace(error, renderHook)
+ throw error
+ }
+
+ const result = React.createRef()
+
+ function TestComponent({renderCallbackProps}) {
+ const pendingResult = renderCallback(renderCallbackProps)
+
+ React.useEffect(() => {
+ result.current = pendingResult
+ })
+
+ return null
+ }
+
+ const {rerender: baseRerender, unmount} = render(
+ ,
+ renderOptions,
+ )
+
+ function rerender(rerenderCallbackProps) {
+ return baseRerender(
+ ,
+ )
+ }
+
+ return {result, rerender, unmount}
+}
+
+// just re-export everything from dom-testing-library
+export * from '@testing-library/dom'
+export {render, renderHook, cleanup, act, fireEvent, getConfig, configure}
+
+/* eslint func-name-matching:0 */
diff --git a/tests/failOnUnexpectedConsoleCalls.js b/tests/failOnUnexpectedConsoleCalls.js
new file mode 100644
index 00000000..83e0c641
--- /dev/null
+++ b/tests/failOnUnexpectedConsoleCalls.js
@@ -0,0 +1,129 @@
+// Fork of https://github.com/facebook/react/blob/513417d6951fa3ff5729302b7990b84604b11afa/scripts/jest/setupTests.js#L71-L161
+/**
+MIT License
+
+Copyright (c) Facebook, Inc. and its affiliates.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+/* eslint-disable prefer-template */
+/* eslint-disable func-names */
+const util = require('util')
+const chalk = require('chalk')
+const shouldIgnoreConsoleError = require('./shouldIgnoreConsoleError')
+
+const patchConsoleMethod = (methodName, unexpectedConsoleCallStacks) => {
+ const newMethod = function (format, ...args) {
+ // Ignore uncaught errors reported by jsdom
+ // and React addendums because they're too noisy.
+ if (methodName === 'error' && shouldIgnoreConsoleError(format, args)) {
+ return
+ }
+
+ // Capture the call stack now so we can warn about it later.
+ // The call stack has helpful information for the test author.
+ // Don't throw yet though b'c it might be accidentally caught and suppressed.
+ const stack = new Error().stack
+ unexpectedConsoleCallStacks.push([
+ stack.substr(stack.indexOf('\n') + 1),
+ util.format(format, ...args),
+ ])
+ }
+
+ console[methodName] = newMethod
+
+ return newMethod
+}
+
+const isSpy = spy =>
+ (spy.calls && typeof spy.calls.count === 'function') ||
+ spy._isMockFunction === true
+
+const flushUnexpectedConsoleCalls = (
+ mockMethod,
+ methodName,
+ expectedMatcher,
+ unexpectedConsoleCallStacks,
+) => {
+ if (console[methodName] !== mockMethod && !isSpy(console[methodName])) {
+ throw new Error(
+ `Test did not tear down console.${methodName} mock properly.`,
+ )
+ }
+ if (unexpectedConsoleCallStacks.length > 0) {
+ const messages = unexpectedConsoleCallStacks.map(
+ ([stack, message]) =>
+ `${chalk.red(message)}\n` +
+ `${stack
+ .split('\n')
+ .map(line => chalk.gray(line))
+ .join('\n')}`,
+ )
+
+ const message =
+ `Expected test not to call ${chalk.bold(
+ `console.${methodName}()`,
+ )}.\n\n` +
+ 'If the warning is expected, test for it explicitly by:\n' +
+ `1. Using the ${chalk.bold('.' + expectedMatcher + '()')} ` +
+ `matcher, or...\n` +
+ `2. Mock it out using ${chalk.bold(
+ 'spyOnDev',
+ )}(console, '${methodName}') or ${chalk.bold(
+ 'spyOnProd',
+ )}(console, '${methodName}'), and test that the warning occurs.`
+
+ throw new Error(`${message}\n\n${messages.join('\n\n')}`)
+ }
+}
+
+const unexpectedErrorCallStacks = []
+const unexpectedWarnCallStacks = []
+
+const errorMethod = patchConsoleMethod('error', unexpectedErrorCallStacks)
+const warnMethod = patchConsoleMethod('warn', unexpectedWarnCallStacks)
+
+const flushAllUnexpectedConsoleCalls = () => {
+ flushUnexpectedConsoleCalls(
+ errorMethod,
+ 'error',
+ 'toErrorDev',
+ unexpectedErrorCallStacks,
+ )
+ flushUnexpectedConsoleCalls(
+ warnMethod,
+ 'warn',
+ 'toWarnDev',
+ unexpectedWarnCallStacks,
+ )
+ unexpectedErrorCallStacks.length = 0
+ unexpectedWarnCallStacks.length = 0
+}
+
+const resetAllUnexpectedConsoleCalls = () => {
+ unexpectedErrorCallStacks.length = 0
+ unexpectedWarnCallStacks.length = 0
+}
+
+expect.extend({
+ ...require('./toWarnDev'),
+})
+
+beforeEach(resetAllUnexpectedConsoleCalls)
+afterEach(flushAllUnexpectedConsoleCalls)
diff --git a/tests/setup-env.js b/tests/setup-env.js
new file mode 100644
index 00000000..1a4401de
--- /dev/null
+++ b/tests/setup-env.js
@@ -0,0 +1,9 @@
+import '@testing-library/jest-dom/extend-expect'
+import './failOnUnexpectedConsoleCalls'
+import {TextEncoder} from 'util'
+import {MessageChannel} from 'worker_threads'
+
+global.TextEncoder = TextEncoder
+// TODO: Revisit once https://github.com/jsdom/jsdom/issues/2448 is resolved
+// This isn't perfect but good enough.
+global.MessageChannel = MessageChannel
diff --git a/tests/shouldIgnoreConsoleError.js b/tests/shouldIgnoreConsoleError.js
new file mode 100644
index 00000000..1c722ba1
--- /dev/null
+++ b/tests/shouldIgnoreConsoleError.js
@@ -0,0 +1,52 @@
+// Fork of https://github.com/facebook/react/blob/513417d6951fa3ff5729302b7990b84604b11afa/scripts/jest/shouldIgnoreConsoleError.js
+/**
+MIT License
+
+Copyright (c) Facebook, Inc. and its affiliates.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+
+module.exports = function shouldIgnoreConsoleError(format) {
+ if (process.env.NODE_ENV !== 'production') {
+ if (typeof format === 'string') {
+ if (format.indexOf('Error: Uncaught [') === 0) {
+ // This looks like an uncaught error from invokeGuardedCallback() wrapper
+ // in development that is reported by jsdom. Ignore because it's noisy.
+ return true
+ }
+ if (format.indexOf('The above error occurred') === 0) {
+ // This looks like an error addendum from ReactFiberErrorLogger.
+ // Ignore it too.
+ return true
+ }
+ if (
+ format.startsWith(
+ 'Warning: `ReactDOMTestUtils.act` is deprecated in favor of `React.act`.',
+ )
+ ) {
+ // This is a React bug in 18.3.0.
+ // Versions with `ReactDOMTestUtils.ac` being deprecated, should have `React.act`
+ return true
+ }
+ }
+ }
+ // Looks legit
+ return false
+}
diff --git a/tests/toWarnDev.js b/tests/toWarnDev.js
new file mode 100644
index 00000000..3005125e
--- /dev/null
+++ b/tests/toWarnDev.js
@@ -0,0 +1,303 @@
+// Fork of https://github.com/facebook/react/blob/513417d6951fa3ff5729302b7990b84604b11afa/scripts/jest/matchers/toWarnDev.js
+/**
+MIT License
+
+Copyright (c) Facebook, Inc. and its affiliates.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ */
+/* eslint-disable no-unsafe-finally */
+/* eslint-disable no-negated-condition */
+/* eslint-disable no-invalid-this */
+/* eslint-disable prefer-template */
+/* eslint-disable func-names */
+/* eslint-disable complexity */
+const util = require('util')
+const jestDiff = require('jest-diff').diff
+const shouldIgnoreConsoleError = require('./shouldIgnoreConsoleError')
+
+function normalizeCodeLocInfo(str) {
+ if (typeof str !== 'string') {
+ return str
+ }
+ // This special case exists only for the special source location in
+ // ReactElementValidator. That will go away if we remove source locations.
+ str = str.replace(/Check your code at .+?:\d+/g, 'Check your code at **')
+ // V8 format:
+ // at Component (/path/filename.js:123:45)
+ // React format:
+ // in Component (at filename.js:123)
+ // eslint-disable-next-line prefer-arrow-callback
+ return str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) {
+ return '\n in ' + name + ' (at **)'
+ })
+}
+
+const createMatcherFor = (consoleMethod, matcherName) =>
+ function matcher(callback, expectedMessages, options = {}) {
+ if (process.env.NODE_ENV !== 'production') {
+ // Warn about incorrect usage of matcher.
+ if (typeof expectedMessages === 'string') {
+ expectedMessages = [expectedMessages]
+ } else if (!Array.isArray(expectedMessages)) {
+ throw Error(
+ `${matcherName}() requires a parameter of type string or an array of strings ` +
+ `but was given ${typeof expectedMessages}.`,
+ )
+ }
+ if (
+ options != null &&
+ (typeof options !== 'object' || Array.isArray(options))
+ ) {
+ throw new Error(
+ `${matcherName}() second argument, when present, should be an object. ` +
+ 'Did you forget to wrap the messages into an array?',
+ )
+ }
+ if (arguments.length > 3) {
+ // `matcher` comes from Jest, so it's more than 2 in practice
+ throw new Error(
+ `${matcherName}() received more than two arguments. ` +
+ 'Did you forget to wrap the messages into an array?',
+ )
+ }
+
+ const withoutStack = options.withoutStack
+ const logAllErrors = options.logAllErrors
+ const warningsWithoutComponentStack = []
+ const warningsWithComponentStack = []
+ const unexpectedWarnings = []
+
+ let lastWarningWithMismatchingFormat = null
+ let lastWarningWithExtraComponentStack = null
+
+ // Catch errors thrown by the callback,
+ // But only rethrow them if all test expectations have been satisfied.
+ // Otherwise an Error in the callback can mask a failed expectation,
+ // and result in a test that passes when it shouldn't.
+ let caughtError
+
+ const isLikelyAComponentStack = message =>
+ typeof message === 'string' &&
+ (message.includes('\n in ') || message.includes('\n at '))
+
+ const consoleSpy = (format, ...args) => {
+ // Ignore uncaught errors reported by jsdom
+ // and React addendums because they're too noisy.
+ if (
+ !logAllErrors &&
+ consoleMethod === 'error' &&
+ shouldIgnoreConsoleError(format, args)
+ ) {
+ return
+ }
+
+ const message = util.format(format, ...args)
+ const normalizedMessage = normalizeCodeLocInfo(message)
+
+ // Remember if the number of %s interpolations
+ // doesn't match the number of arguments.
+ // We'll fail the test if it happens.
+ let argIndex = 0
+ String(format).replace(/%s/g, () => argIndex++)
+ if (argIndex !== args.length) {
+ lastWarningWithMismatchingFormat = {
+ format,
+ args,
+ expectedArgCount: argIndex,
+ }
+ }
+
+ // Protect against accidentally passing a component stack
+ // to warning() which already injects the component stack.
+ if (
+ args.length >= 2 &&
+ isLikelyAComponentStack(args[args.length - 1]) &&
+ isLikelyAComponentStack(args[args.length - 2])
+ ) {
+ lastWarningWithExtraComponentStack = {
+ format,
+ }
+ }
+
+ for (let index = 0; index < expectedMessages.length; index++) {
+ const expectedMessage = expectedMessages[index]
+ if (
+ normalizedMessage === expectedMessage ||
+ normalizedMessage.includes(expectedMessage)
+ ) {
+ if (isLikelyAComponentStack(normalizedMessage)) {
+ warningsWithComponentStack.push(normalizedMessage)
+ } else {
+ warningsWithoutComponentStack.push(normalizedMessage)
+ }
+ expectedMessages.splice(index, 1)
+ return
+ }
+ }
+
+ let errorMessage
+ if (expectedMessages.length === 0) {
+ errorMessage =
+ 'Unexpected warning recorded: ' +
+ this.utils.printReceived(normalizedMessage)
+ } else if (expectedMessages.length === 1) {
+ errorMessage =
+ 'Unexpected warning recorded: ' +
+ jestDiff(expectedMessages[0], normalizedMessage)
+ } else {
+ errorMessage =
+ 'Unexpected warning recorded: ' +
+ jestDiff(expectedMessages, [normalizedMessage])
+ }
+
+ // Record the call stack for unexpected warnings.
+ // We don't throw an Error here though,
+ // Because it might be suppressed by ReactFiberScheduler.
+ unexpectedWarnings.push(new Error(errorMessage))
+ }
+
+ // TODO Decide whether we need to support nested toWarn* expectations.
+ // If we don't need it, add a check here to see if this is already our spy,
+ // And throw an error.
+ const originalMethod = console[consoleMethod]
+
+ // Avoid using Jest's built-in spy since it can't be removed.
+ console[consoleMethod] = consoleSpy
+
+ try {
+ callback()
+ } catch (error) {
+ caughtError = error
+ } finally {
+ // Restore the unspied method so that unexpected errors fail tests.
+ console[consoleMethod] = originalMethod
+
+ // Any unexpected Errors thrown by the callback should fail the test.
+ // This should take precedence since unexpected errors could block warnings.
+ if (caughtError) {
+ throw caughtError
+ }
+
+ // Any unexpected warnings should be treated as a failure.
+ if (unexpectedWarnings.length > 0) {
+ return {
+ message: () => unexpectedWarnings[0].stack,
+ pass: false,
+ }
+ }
+
+ // Any remaining messages indicate a failed expectations.
+ if (expectedMessages.length > 0) {
+ return {
+ message: () =>
+ `Expected warning was not recorded:\n ${this.utils.printReceived(
+ expectedMessages[0],
+ )}`,
+ pass: false,
+ }
+ }
+
+ if (typeof withoutStack === 'number') {
+ // We're expecting a particular number of warnings without stacks.
+ if (withoutStack !== warningsWithoutComponentStack.length) {
+ return {
+ message: () =>
+ `Expected ${withoutStack} warnings without a component stack but received ${warningsWithoutComponentStack.length}:\n` +
+ warningsWithoutComponentStack.map(warning =>
+ this.utils.printReceived(warning),
+ ),
+ pass: false,
+ }
+ }
+ } else if (withoutStack === true) {
+ // We're expecting that all warnings won't have the stack.
+ // If some warnings have it, it's an error.
+ if (warningsWithComponentStack.length > 0) {
+ return {
+ message: () =>
+ `Received warning unexpectedly includes a component stack:\n ${this.utils.printReceived(
+ warningsWithComponentStack[0],
+ )}\nIf this warning intentionally includes the component stack, remove ` +
+ `{withoutStack: true} from the ${matcherName}() call. If you have a mix of ` +
+ `warnings with and without stack in one ${matcherName}() call, pass ` +
+ `{withoutStack: N} where N is the number of warnings without stacks.`,
+ pass: false,
+ }
+ }
+ } else if (withoutStack === false || withoutStack === undefined) {
+ // We're expecting that all warnings *do* have the stack (default).
+ // If some warnings don't have it, it's an error.
+ if (warningsWithoutComponentStack.length > 0) {
+ return {
+ message: () =>
+ `Received warning unexpectedly does not include a component stack:\n ${this.utils.printReceived(
+ warningsWithoutComponentStack[0],
+ )}\nIf this warning intentionally omits the component stack, add ` +
+ `{withoutStack: true} to the ${matcherName} call.`,
+ pass: false,
+ }
+ }
+ } else {
+ throw Error(
+ `The second argument for ${matcherName}(), when specified, must be an object. It may have a ` +
+ `property called "withoutStack" whose value may be undefined, boolean, or a number. ` +
+ `Instead received ${typeof withoutStack}.`,
+ )
+ }
+
+ if (lastWarningWithMismatchingFormat !== null) {
+ return {
+ message: () =>
+ `Received ${
+ lastWarningWithMismatchingFormat.args.length
+ } arguments for a message with ${
+ lastWarningWithMismatchingFormat.expectedArgCount
+ } placeholders:\n ${this.utils.printReceived(
+ lastWarningWithMismatchingFormat.format,
+ )}`,
+ pass: false,
+ }
+ }
+
+ if (lastWarningWithExtraComponentStack !== null) {
+ return {
+ message: () =>
+ `Received more than one component stack for a warning:\n ${this.utils.printReceived(
+ lastWarningWithExtraComponentStack.format,
+ )}\nDid you accidentally pass a stack to warning() as the last argument? ` +
+ `Don't forget warning() already injects the component stack automatically.`,
+ pass: false,
+ }
+ }
+
+ return {pass: true}
+ }
+ } else {
+ // Any uncaught errors or warnings should fail tests in production mode.
+ callback()
+
+ return {pass: true}
+ }
+ }
+
+module.exports = {
+ toWarnDev: createMatcherFor('warn', 'toWarnDev'),
+ toErrorDev: createMatcherFor('error', 'toErrorDev'),
+}
diff --git a/types/index.d.ts b/types/index.d.ts
new file mode 100644
index 00000000..bdd60567
--- /dev/null
+++ b/types/index.d.ts
@@ -0,0 +1,287 @@
+// TypeScript Version: 3.8
+import * as ReactDOMClient from 'react-dom/client'
+import {
+ queries,
+ Queries,
+ BoundFunction,
+ prettyFormat,
+ Config as ConfigDTL,
+} from '@testing-library/dom'
+import {act as reactDeprecatedAct} from 'react-dom/test-utils'
+//@ts-ignore
+import {act as reactAct} from 'react'
+
+export * from '@testing-library/dom'
+
+export interface Config extends ConfigDTL {
+ reactStrictMode: boolean
+}
+
+export interface ConfigFn {
+ (existingConfig: Config): Partial
+}
+
+export function configure(configDelta: ConfigFn | Partial): void
+
+export function getConfig(): Config
+
+export type RenderResult<
+ Q extends Queries = typeof queries,
+ Container extends RendererableContainer | HydrateableContainer = HTMLElement,
+ BaseElement extends RendererableContainer | HydrateableContainer = Container,
+> = {
+ container: Container
+ baseElement: BaseElement
+ debug: (
+ baseElement?:
+ | RendererableContainer
+ | HydrateableContainer
+ | Array
+ | undefined,
+ maxLength?: number | undefined,
+ options?: prettyFormat.OptionsReceived | undefined,
+ ) => void
+ rerender: (ui: React.ReactNode) => void
+ unmount: () => void
+ asFragment: () => DocumentFragment
+} & {[P in keyof Q]: BoundFunction}
+
+/** @deprecated */
+export type BaseRenderOptions<
+ Q extends Queries,
+ Container extends RendererableContainer | HydrateableContainer,
+ BaseElement extends RendererableContainer | HydrateableContainer,
+> = RenderOptions
+
+type RendererableContainer = ReactDOMClient.Container
+type HydrateableContainer = Parameters[0]
+/** @deprecated */
+export interface ClientRenderOptions<
+ Q extends Queries,
+ Container extends RendererableContainer,
+ BaseElement extends RendererableContainer = Container,
+> extends BaseRenderOptions {
+ /**
+ * If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side
+ * rendering and use ReactDOM.hydrate to mount your components.
+ *
+ * @see https://testing-library.com/docs/react-testing-library/api/#hydrate)
+ */
+ hydrate?: false | undefined
+}
+/** @deprecated */
+export interface HydrateOptions<
+ Q extends Queries,
+ Container extends HydrateableContainer,
+ BaseElement extends HydrateableContainer = Container,
+> extends BaseRenderOptions {
+ /**
+ * If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side
+ * rendering and use ReactDOM.hydrate to mount your components.
+ *
+ * @see https://testing-library.com/docs/react-testing-library/api/#hydrate)
+ */
+ hydrate: true
+}
+
+export interface RenderOptions<
+ Q extends Queries = typeof queries,
+ Container extends RendererableContainer | HydrateableContainer = HTMLElement,
+ BaseElement extends RendererableContainer | HydrateableContainer = Container,
+> {
+ /**
+ * 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,
+ * it will not be appended to the document.body automatically.
+ *
+ * For example: If you are unit testing a `` element, it cannot be a child of a div. In this case, you can
+ * specify a table as the render container.
+ *
+ * @see https://testing-library.com/docs/react-testing-library/api/#container
+ */
+ container?: Container | undefined
+ /**
+ * Defaults to the container if the container is specified. Otherwise `document.body` is used for the default. This is used as
+ * the base element for the queries as well as what is printed when you use `debug()`.
+ *
+ * @see https://testing-library.com/docs/react-testing-library/api/#baseelement
+ */
+ baseElement?: BaseElement | undefined
+ /**
+ * If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side
+ * rendering and use ReactDOM.hydrate to mount your components.
+ *
+ * @see https://testing-library.com/docs/react-testing-library/api/#hydrate)
+ */
+ hydrate?: boolean | undefined
+ /**
+ * Only works if used with React 18.
+ * Set to `true` if you want to force synchronous `ReactDOM.render`.
+ * Otherwise `render` will default to concurrent React if available.
+ */
+ legacyRoot?: boolean | undefined
+ /**
+ * Only supported in React 19.
+ * Callback called when React catches an error in an Error Boundary.
+ * Called with the error caught by the Error Boundary, and an `errorInfo` object containing the `componentStack`.
+ *
+ * @see {@link https://react.dev/reference/react-dom/client/createRoot#parameters createRoot#options}
+ */
+ onCaughtError?: ReactDOMClient.RootOptions extends {
+ onCaughtError: infer OnCaughtError
+ }
+ ? OnCaughtError
+ : never
+ /**
+ * Callback called when React automatically recovers from errors.
+ * Called with an error React throws, and an `errorInfo` object containing the `componentStack`.
+ * Some recoverable errors may include the original error cause as `error.cause`.
+ *
+ * @see {@link https://react.dev/reference/react-dom/client/createRoot#parameters createRoot#options}
+ */
+ onRecoverableError?: ReactDOMClient.RootOptions['onRecoverableError']
+ /**
+ * Not supported at the moment
+ */
+ onUncaughtError?: never
+ /**
+ * Queries to bind. Overrides the default set from DOM Testing Library unless merged.
+ *
+ * @see https://testing-library.com/docs/react-testing-library/api/#queries
+ */
+ queries?: Q | undefined
+ /**
+ * Pass a React Component as the wrapper option to have it rendered around the inner element. This is most useful for creating
+ * reusable custom render functions for common data providers. See setup for examples.
+ *
+ * @see https://testing-library.com/docs/react-testing-library/api/#wrapper
+ */
+ wrapper?: React.JSXElementConstructor<{children: React.ReactNode}> | undefined
+ /**
+ * When enabled, is rendered around the inner element.
+ * If defined, overrides the value of `reactStrictMode` set in `configure`.
+ */
+ reactStrictMode?: boolean
+}
+
+type Omit = Pick>
+
+/**
+ * Render into a container which is appended to document.body. It should be used with cleanup.
+ */
+export function render<
+ Q extends Queries = typeof queries,
+ Container extends RendererableContainer | HydrateableContainer = HTMLElement,
+ BaseElement extends RendererableContainer | HydrateableContainer = Container,
+>(
+ ui: React.ReactNode,
+ options: RenderOptions,
+): RenderResult
+export function render(
+ ui: React.ReactNode,
+ options?: Omit | undefined,
+): RenderResult
+
+export interface RenderHookResult {
+ /**
+ * Triggers a re-render. The props will be passed to your renderHook callback.
+ */
+ rerender: (props?: Props) => void
+ /**
+ * This is a stable reference to the latest value returned by your renderHook
+ * callback
+ */
+ result: {
+ /**
+ * The value returned by your renderHook callback
+ */
+ current: Result
+ }
+ /**
+ * Unmounts the test component. This is useful for when you need to test
+ * any cleanup your useEffects have.
+ */
+ unmount: () => void
+}
+
+/** @deprecated */
+export type BaseRenderHookOptions<
+ Props,
+ Q extends Queries,
+ Container extends RendererableContainer | HydrateableContainer,
+ BaseElement extends Element | DocumentFragment,
+> = RenderHookOptions
+
+/** @deprecated */
+export interface ClientRenderHookOptions<
+ Props,
+ Q extends Queries,
+ Container extends Element | DocumentFragment,
+ BaseElement extends Element | DocumentFragment = Container,
+> extends BaseRenderHookOptions {
+ /**
+ * If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side
+ * rendering and use ReactDOM.hydrate to mount your components.
+ *
+ * @see https://testing-library.com/docs/react-testing-library/api/#hydrate)
+ */
+ hydrate?: false | undefined
+}
+
+/** @deprecated */
+export interface HydrateHookOptions<
+ Props,
+ Q extends Queries,
+ Container extends Element | DocumentFragment,
+ BaseElement extends Element | DocumentFragment = Container,
+> extends BaseRenderHookOptions {
+ /**
+ * If `hydrate` is set to `true`, then it will render with `ReactDOM.hydrate`. This may be useful if you are using server-side
+ * rendering and use ReactDOM.hydrate to mount your components.
+ *
+ * @see https://testing-library.com/docs/react-testing-library/api/#hydrate)
+ */
+ hydrate: true
+}
+
+export interface RenderHookOptions<
+ Props,
+ Q extends Queries = typeof queries,
+ Container extends RendererableContainer | HydrateableContainer = HTMLElement,
+ BaseElement extends RendererableContainer | HydrateableContainer = Container,
+> extends BaseRenderOptions {
+ /**
+ * The argument passed to the renderHook callback. Can be useful if you plan
+ * to use the rerender utility to change the values passed to your hook.
+ */
+ initialProps?: Props | undefined
+}
+
+/**
+ * Allows you to render a hook within a test React component without having to
+ * create that component yourself.
+ */
+export function renderHook<
+ Result,
+ Props,
+ Q extends Queries = typeof queries,
+ Container extends RendererableContainer | HydrateableContainer = HTMLElement,
+ BaseElement extends RendererableContainer | HydrateableContainer = Container,
+>(
+ render: (initialProps: Props) => Result,
+ options?: RenderHookOptions | undefined,
+): RenderHookResult
+
+/**
+ * Unmounts React trees that were mounted with render.
+ */
+export function cleanup(): void
+
+/**
+ * Simply calls React.act(cb)
+ * If that's not available (older version of react) then it
+ * simply calls the deprecated version which is ReactTestUtils.act(cb)
+ */
+// IfAny from https://stackoverflow.com/a/61626123/3406963
+export const act: 0 extends 1 & typeof reactAct
+ ? typeof reactDeprecatedAct
+ : typeof reactAct
diff --git a/types/pure.d.ts b/types/pure.d.ts
new file mode 100644
index 00000000..7b527195
--- /dev/null
+++ b/types/pure.d.ts
@@ -0,0 +1 @@
+export * from './'
diff --git a/types/test.tsx b/types/test.tsx
new file mode 100644
index 00000000..825d5699
--- /dev/null
+++ b/types/test.tsx
@@ -0,0 +1,317 @@
+import * as React from 'react'
+import {render, fireEvent, screen, waitFor, renderHook} from '.'
+import * as pure from './pure'
+
+export async function testRender() {
+ const view = render( )
+
+ // single queries
+ view.getByText('foo')
+ view.queryByText('foo')
+ await view.findByText('foo')
+
+ // multiple queries
+ view.getAllByText('bar')
+ view.queryAllByText('bar')
+ await view.findAllByText('bar')
+
+ // helpers
+ const {container, rerender, debug} = view
+ expectType(container)
+ return {container, rerender, debug}
+}
+
+export async function testPureRender() {
+ const view = pure.render( )
+
+ // single queries
+ view.getByText('foo')
+ view.queryByText('foo')
+ await view.findByText('foo')
+
+ // multiple queries
+ view.getAllByText('bar')
+ view.queryAllByText('bar')
+ await view.findAllByText('bar')
+
+ // helpers
+ const {container, rerender, debug} = view
+ expectType(container)
+ return {container, rerender, debug}
+}
+
+export function testRenderOptions() {
+ const container = document.createElement('div')
+ const options = {container}
+ const {container: returnedContainer} = render( , options)
+ expectType(returnedContainer)
+
+ render(
, {wrapper: () => null})
+}
+
+export function testSVGRenderOptions() {
+ const container = document.createElementNS(
+ 'http://www.w3.org/2000/svg',
+ 'svg',
+ )
+ const options = {container}
+ const {container: returnedContainer} = render( , options)
+ expectType(returnedContainer)
+}
+
+export function testFireEvent() {
+ const {container} = render( )
+ fireEvent.click(container)
+}
+
+export function testConfigure() {
+ // test for DTL's config
+ pure.configure({testIdAttribute: 'foobar'})
+ pure.configure(existingConfig => ({
+ testIdAttribute: `modified-${existingConfig.testIdAttribute}`,
+ }))
+
+ // test for RTL's config
+ pure.configure({reactStrictMode: true})
+ pure.configure(existingConfig => ({
+ reactStrictMode: !existingConfig.reactStrictMode,
+ }))
+}
+
+export function testGetConfig() {
+ // test for DTL's config
+ pure.getConfig().testIdAttribute
+
+ // test for RTL's config
+ pure.getConfig().reactStrictMode
+}
+
+export function testDebug() {
+ const {debug, getAllByTestId} = render(
+ <>
+ Hello World
+ Hello World
+ >,
+ )
+ debug(getAllByTestId('testid'))
+}
+
+export async function testScreen() {
+ render( )
+
+ await screen.findByRole('button')
+}
+
+export async function testWaitFor() {
+ const {container} = render( )
+ fireEvent.click(container)
+ await waitFor(() => {})
+}
+
+export function testQueries() {
+ const {getByLabelText} = render(
+ Username ,
+ )
+ expectType>(
+ getByLabelText('Username'),
+ )
+
+ const container = document.createElement('div')
+ const options = {container}
+ const {getByText} = render(Hello World
, options)
+ expectType>(
+ getByText('Hello World'),
+ )
+}
+
+export function wrappedRender(
+ ui: React.ReactNode,
+ options?: pure.RenderOptions,
+) {
+ const Wrapper = ({
+ children,
+ }: {
+ children: React.ReactNode
+ }): React.JSX.Element => {
+ return {children}
+ }
+
+ return pure.render(ui, {
+ wrapper: Wrapper,
+ // testing exactOptionalPropertyTypes comaptibility
+ hydrate: options?.hydrate,
+ ...options,
+ })
+}
+
+export function wrappedRenderB(
+ ui: React.ReactNode,
+ options?: pure.RenderOptions,
+) {
+ const Wrapper: React.FunctionComponent<{children?: React.ReactNode}> = ({
+ children,
+ }) => {
+ return {children}
+ }
+
+ return pure.render(ui, {wrapper: Wrapper, ...options})
+}
+
+export function wrappedRenderC(
+ ui: React.ReactNode,
+ options?: pure.RenderOptions,
+) {
+ interface AppWrapperProps {
+ children?: React.ReactNode
+ userProviderProps?: {user: string}
+ }
+ const AppWrapperProps: React.FunctionComponent = ({
+ children,
+ userProviderProps = {user: 'TypeScript'},
+ }) => {
+ return {children}
+ }
+
+ return pure.render(ui, {wrapper: AppWrapperProps, ...options})
+}
+
+export function wrappedRenderHook(
+ hook: () => unknown,
+ options?: pure.RenderHookOptions,
+) {
+ interface AppWrapperProps {
+ children?: React.ReactNode
+ userProviderProps?: {user: string}
+ }
+ const AppWrapperProps: React.FunctionComponent = ({
+ children,
+ userProviderProps = {user: 'TypeScript'},
+ }) => {
+ return {children}
+ }
+
+ return pure.renderHook(hook, {...options})
+}
+
+export function testBaseElement() {
+ const {baseElement: baseDefaultElement} = render(
)
+ expectType(baseDefaultElement)
+
+ const container = document.createElement('input')
+ const {baseElement: baseElementFromContainer} = render(
, {container})
+ expectType(
+ baseElementFromContainer,
+ )
+
+ const baseElementOption = document.createElement('input')
+ const {baseElement: baseElementFromOption} = render(
, {
+ baseElement: baseElementOption,
+ })
+ expectType(
+ baseElementFromOption,
+ )
+}
+
+export function testRenderHook() {
+ const {result, rerender, unmount} = renderHook(() => React.useState(2)[0])
+
+ expectType(result.current)
+
+ rerender()
+
+ unmount()
+
+ renderHook(() => null, {wrapper: () => null})
+}
+
+export function testRenderHookProps() {
+ const {result, rerender, unmount} = renderHook(
+ ({defaultValue}) => React.useState(defaultValue)[0],
+ {initialProps: {defaultValue: 2}},
+ )
+
+ expectType(result.current)
+
+ rerender()
+
+ unmount()
+}
+
+export function testContainer() {
+ render('a', {container: document.createElement('div')})
+ render('a', {container: document.createDocumentFragment()})
+ // Only allowed in React 19
+ render('a', {container: document})
+ render('a', {container: document.createElement('div'), hydrate: true})
+ // Only allowed for createRoot but typing `render` appropriately makes it harder to compose.
+ render('a', {container: document.createDocumentFragment(), hydrate: true})
+ render('a', {container: document, hydrate: true})
+
+ renderHook(() => null, {container: document.createElement('div')})
+ renderHook(() => null, {container: document.createDocumentFragment()})
+ // Only allowed in React 19
+ renderHook(() => null, {container: document})
+ renderHook(() => null, {
+ container: document.createElement('div'),
+ hydrate: true,
+ })
+ // Only allowed for createRoot but typing `render` appropriately makes it harder to compose.
+ renderHook(() => null, {
+ container: document.createDocumentFragment(),
+ hydrate: true,
+ })
+ renderHook(() => null, {container: document, hydrate: true})
+}
+
+export function testErrorHandlers() {
+ // React 19 types are not used in tests. Verify manually if this works with `"@types/react": "npm:types-react@rc"`
+ render(null, {
+ // Should work with React 19 types
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error
+ onCaughtError: () => {},
+ })
+ render(null, {
+ // Should never work as it's not supported yet.
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error
+ onUncaughtError: () => {},
+ })
+ render(null, {
+ onRecoverableError: (error, errorInfo) => {
+ console.error(error)
+ console.log(errorInfo.componentStack)
+ },
+ })
+}
+
+/*
+eslint
+ testing-library/prefer-explicit-assert: "off",
+ testing-library/no-wait-for-empty-callback: "off",
+ testing-library/prefer-screen-queries: "off"
+*/
+
+// https://stackoverflow.com/questions/53807517/how-to-test-if-two-types-are-exactly-the-same
+type IfEquals = (() => G extends T
+ ? 1
+ : 2) extends () => G extends U ? 1 : 2
+ ? Yes
+ : No
+
+/**
+ * Issues a type error if `Expected` is not identical to `Actual`.
+ *
+ * `Expected` should be declared when invoking `expectType`.
+ * `Actual` should almost always we be a `typeof value` statement.
+ *
+ * Source: https://github.com/mui-org/material-ui/blob/6221876a4b468a3330ffaafa8472de7613933b87/packages/material-ui-types/index.d.ts#L73-L84
+ *
+ * @example `expectType(value)`
+ * TypeScript issues a type error since `value is not assignable to never`.
+ * This means `typeof value` is not identical to `number | string`
+ * @param actual
+ */
+declare function expectType(
+ actual: IfEquals,
+): void
diff --git a/types/tsconfig.json b/types/tsconfig.json
new file mode 100644
index 00000000..bad26af7
--- /dev/null
+++ b/types/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../node_modules/kcd-scripts/shared-tsconfig.json",
+ "compilerOptions": {
+ "exactOptionalPropertyTypes": true,
+ "skipLibCheck": false
+ },
+ "include": ["."]
+}
diff --git a/typings/index.d.ts b/typings/index.d.ts
deleted file mode 100644
index 61039bb7..00000000
--- a/typings/index.d.ts
+++ /dev/null
@@ -1,156 +0,0 @@
-import {Simulate as ReactSimulate} from 'react-dom/test-utils'
-import {
- AllByAttribute,
- AllByText,
- BoundFunction,
- GetByAttribute,
- GetByText,
- QueryByAttribute,
- QueryByText,
-} from 'dom-testing-library'
-
-type TextMatchFunction = (content: string, element: HTMLElement) => boolean
-type TextMatch = string | RegExp | TextMatchFunction
-type TextMatchOptions = {
- exact?: boolean
- trim?: boolean
- collapseWhitespace?: boolean
-}
-
-interface GetsAndQueries {
- queryByTestId: BoundFunction
- queryAllByTestId: BoundFunction
- getByTestId: BoundFunction
- getAllByTestId: BoundFunction
- queryByText: BoundFunction
- queryAllByText: BoundFunction
- getByText: BoundFunction
- getAllByText: BoundFunction
- queryByPlaceholderText: BoundFunction
- queryAllByPlaceholderText: BoundFunction
- getByPlaceholderText: BoundFunction
- getAllByPlaceholderText: BoundFunction
- queryByLabelText: BoundFunction
- queryAllByLabelText: BoundFunction
- getByLabelText: BoundFunction
- getAllByLabelText: BoundFunction
- queryByAltText: BoundFunction
- queryAllByAltText: BoundFunction
- getByAltText: BoundFunction
- getAllByAltText: BoundFunction
-}
-
-interface RenderResult extends GetsAndQueries {
- container: HTMLDivElement
- debug: () => void
- rerender: (ui: React.ReactElement) => void
- unmount: VoidFunction
-}
-
-export function render(
- ui: React.ReactElement,
- options?: {container: HTMLElement},
-): RenderResult
-
-export const Simulate: typeof ReactSimulate
-
-export function wait(
- callback?: () => void,
- options?: {
- timeout?: number
- interval?: number
- },
-): Promise
-
-export function waitForElement(
- callback?: () => T,
- options?: {
- container?: HTMLElement
- timeout?: number
- mutationObserverOptions?: MutationObserverInit
- },
-): Promise
-
-type EventType =
- | 'copy'
- | 'cut'
- | 'paste'
- | 'compositionEnd'
- | 'compositionStart'
- | 'compositionUpdate'
- | 'keyDown'
- | 'keyPress'
- | 'keyUp'
- | 'focus'
- | 'blur'
- | 'change'
- | 'input'
- | 'invalid'
- | 'submit'
- | 'click'
- | 'contextMenu'
- | 'dblClick'
- | 'drag'
- | 'dragEnd'
- | 'dragEnter'
- | 'dragExit'
- | 'dragLeave'
- | 'dragOver'
- | 'dragStart'
- | 'drop'
- | 'mouseDown'
- | 'mouseEnter'
- | 'mouseLeave'
- | 'mouseMove'
- | 'mouseOut'
- | 'mouseOver'
- | 'mouseUp'
- | 'select'
- | 'touchCancel'
- | 'touchEnd'
- | 'touchMove'
- | 'touchStart'
- | 'scroll'
- | 'wheel'
- | 'abort'
- | 'canPlay'
- | 'canPlayThrough'
- | 'durationChange'
- | 'emptied'
- | 'encrypted'
- | 'ended'
- | 'loadedData'
- | 'loadedMetadata'
- | 'loadStart'
- | 'pause'
- | 'play'
- | 'playing'
- | 'progress'
- | 'rateChange'
- | 'seeked'
- | 'seeking'
- | 'stalled'
- | 'suspend'
- | 'timeUpdate'
- | 'volumeChange'
- | 'waiting'
- | 'load'
- | 'error'
- | 'animationStart'
- | 'animationEnd'
- | 'animationIteration'
- | 'transitionEnd'
- | 'doubleClick'
-
-type FireFunction = (element: HTMLElement, event: Event) => boolean
-type FireObject = {
- [K in EventType]: (element: HTMLElement, options?: {}) => boolean
-}
-
-export const fireEvent: FireFunction & FireObject
-
-export function renderIntoDocument(ui: React.ReactElement): RenderResult
-
-export function cleanup(): void
-
-export function getQueriesForElement(element: HTMLElement): GetsAndQueries