diff --git a/.all-contributorsrc b/.all-contributorsrc index 9b34a8ad..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", @@ -76,6 +77,1314 @@ "contributions": [ "platform" ] + }, + { + "login": "antoaravinth", + "name": "Anto Aravinth", + "avatar_url": "https://avatars1.githubusercontent.com/u/1241511?s=460&v=4", + "profile": "https://github.com/antoaravinth", + "contributions": [ + "code", + "test", + "doc" + ] + }, + { + "login": "JonahMoses", + "name": "Jonah Moses", + "avatar_url": "https://avatars2.githubusercontent.com/u/3462296?v=4", + "profile": "https://github.com/JonahMoses", + "contributions": [ + "doc" + ] + }, + { + "login": "lgandecki", + "name": "Łukasz Gandecki", + "avatar_url": "https://avatars1.githubusercontent.com/u/4002543?v=4", + "profile": "http://team.thebrain.pro", + "contributions": [ + "code", + "test", + "doc" + ] + }, + { + "login": "sompylasar", + "name": "Ivan Babak", + "avatar_url": "https://avatars2.githubusercontent.com/u/498274?v=4", + "profile": "https://sompylasar.github.io", + "contributions": [ + "bug", + "ideas" + ] + }, + { + "login": "jday3", + "name": "Jesse Day", + "avatar_url": "https://avatars3.githubusercontent.com/u/4439618?v=4", + "profile": "https://github.com/jday3", + "contributions": [ + "code" + ] + }, + { + "login": "gnapse", + "name": "Ernesto García", + "avatar_url": "https://avatars0.githubusercontent.com/u/15199?v=4", + "profile": "http://gnapse.github.io", + "contributions": [ + "question", + "code", + "doc" + ] + }, + { + "login": "jomaxx", + "name": "Josef Maxx Blake", + "avatar_url": "https://avatars2.githubusercontent.com/u/2747424?v=4", + "profile": "http://jomaxx.com", + "contributions": [ + "code", + "doc", + "test" + ] + }, + { + "login": "mbaranovski", + "name": "Michal Baranowski", + "avatar_url": "https://avatars1.githubusercontent.com/u/29602306?v=4", + "profile": "https://twitter.com/baranovskim", + "contributions": [ + "blog", + "tutorial" + ] + }, + { + "login": "aputhin", + "name": "Arthur Puthin", + "avatar_url": "https://avatars3.githubusercontent.com/u/13985684?v=4", + "profile": "https://github.com/aputhin", + "contributions": [ + "doc" + ] + }, + { + "login": "thchia", + "name": "Thomas Chia", + "avatar_url": "https://avatars2.githubusercontent.com/u/21194045?v=4", + "profile": "https://github.com/thchia", + "contributions": [ + "code", + "doc" + ] + }, + { + "login": "thiagopaiva99", + "name": "Thiago Galvani", + "avatar_url": "https://avatars3.githubusercontent.com/u/20430611?v=4", + "profile": "http://ilegra.com/", + "contributions": [ + "doc" + ] + }, + { + "login": "ChrisWcs", + "name": "Christian", + "avatar_url": "https://avatars1.githubusercontent.com/u/19828824?v=4", + "profile": "http://Chriswcs.github.io", + "contributions": [ + "test" + ] + }, + { + "login": "alexkrolick", + "name": "Alex Krolick", + "avatar_url": "https://avatars3.githubusercontent.com/u/1571667?v=4", + "profile": "https://alexkrolick.com", + "contributions": [ + "question", + "doc", + "example", + "ideas" + ] + }, + { + "login": "johann-sonntagbauer", + "name": "Johann Hubert Sonntagbauer", + "avatar_url": "https://avatars3.githubusercontent.com/u/1239401?v=4", + "profile": "https://github.com/johann-sonntagbauer", + "contributions": [ + "code", + "doc", + "test" + ] + }, + { + "login": "maddijoyce", + "name": "Maddi Joyce", + "avatar_url": "https://avatars2.githubusercontent.com/u/2224291?v=4", + "profile": "http://www.maddijoyce.com", + "contributions": [ + "code" + ] + }, + { + "login": "RyanAtViceSoftware", + "name": "Ryan Vice", + "avatar_url": "https://avatars2.githubusercontent.com/u/10080111?v=4", + "profile": "http://www.vicesoftware.com", + "contributions": [ + "doc" + ] + }, + { + "login": "iwilsonq", + "name": "Ian Wilson", + "avatar_url": "https://avatars1.githubusercontent.com/u/7942604?v=4", + "profile": "https://ianwilson.io", + "contributions": [ + "blog", + "tutorial" + ] + }, + { + "login": "InExtremaRes", + "name": "Daniel", + "avatar_url": "https://avatars2.githubusercontent.com/u/1635491?v=4", + "profile": "https://github.com/InExtremaRes", + "contributions": [ + "bug", + "code" + ] + }, + { + "login": "Gpx", + "name": "Giorgio Polvara", + "avatar_url": "https://avatars0.githubusercontent.com/u/767959?v=4", + "profile": "https://twitter.com/Gpx", + "contributions": [ + "bug", + "ideas" + ] + }, + { + "login": "jgoz", + "name": "John Gozde", + "avatar_url": "https://avatars2.githubusercontent.com/u/132233?v=4", + "profile": "https://github.com/jgoz", + "contributions": [ + "code" + ] + }, + { + "login": "SavePointSam", + "name": "Sam Horton", + "avatar_url": "https://avatars0.githubusercontent.com/u/8203211?v=4", + "profile": "https://twitter.com/SavePointSam", + "contributions": [ + "doc", + "example", + "ideas" + ] + }, + { + "login": "rkotze", + "name": "Richard Kotze (mobile)", + "avatar_url": "https://avatars2.githubusercontent.com/u/10452163?v=4", + "profile": "http://www.richardkotze.com", + "contributions": [ + "doc" + ] + }, + { + "login": "sotobuild", + "name": "Brahian E. Soto Mercedes", + "avatar_url": "https://avatars2.githubusercontent.com/u/10819833?v=4", + "profile": "https://github.com/sotobuild", + "contributions": [ + "doc" + ] + }, + { + "login": "bdelaforest", + "name": "Benoit de La Forest", + "avatar_url": "https://avatars2.githubusercontent.com/u/7151559?v=4", + "profile": "https://github.com/bdelaforest", + "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 6a7b4bff..496c8563 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,23 +1,67 @@ -* `react-testing-library` 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: @@ -30,9 +74,20 @@ Reproduction repository: Problem description: + + Suggested solution: + + diff --git a/.github/ISSUE_TEMPLATE/Bug_Report.md b/.github/ISSUE_TEMPLATE/Bug_Report.md new file mode 100644 index 00000000..c04bef38 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Bug_Report.md @@ -0,0 +1,76 @@ +--- +name: 🐛 Bug Report +about: Bugs, missing documentation, or unexpected behavior 🤔. +--- + + + +- `@testing-library/react` version: +- Testing Framework and version: + +- DOM Environment: + + + + +### Relevant code or config: + +```js +var your => (code) => here; +``` + + + +### What you did: + + + +### What happened: + + + +### Reproduction: + + + +### Problem description: + + + +### Suggested solution: + + diff --git a/.github/ISSUE_TEMPLATE/Feature_Request.md b/.github/ISSUE_TEMPLATE/Feature_Request.md new file mode 100644 index 00000000..357d1df7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Feature_Request.md @@ -0,0 +1,51 @@ +--- +name: 💡 Feature Request +about: I have a suggestion (and might want to implement myself 🙂)! +--- + + + +### Describe the feature you'd like: + + + +### Suggested implementation: + + + +### Describe alternatives you've considered: + + + +### Teachability, Documentation, Adoption, Migration Strategy: + + diff --git a/.github/ISSUE_TEMPLATE/Question.md b/.github/ISSUE_TEMPLATE/Question.md new file mode 100644 index 00000000..e625486b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Question.md @@ -0,0 +1,24 @@ +--- +name: ❓ Support Question +about: 🛑 If you have a question 💬, please check out our support channels! +--- + +-------------- 👆 Click "Preview"! + +Issues on GitHub are intended to be related to problems with the library itself +and feature requests so we recommend not using this medium to ask them here 😁. + +--- + +## ❓ Support Forums + +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 41f85a7d..7e18d5dd 100644 --- a/README.md +++ b/README.md @@ -1,569 +1,647 @@
-

react-testing-library

+

React Testing Library

-goat + goat -

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] - -[![All Contributors](https://img.shields.io/badge/all_contributors-7-orange.svg?style=flat-square)](#contributors) +[![All Contributors][all-contributors-badge]](#contributors) [![PRs Welcome][prs-badge]][prs] [![Code of Conduct][coc-badge]][coc] +[![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. However, the -de facto standard for testing ([enzyme](https://github.com/airbnb/enzyme)) is -bloated with complexity and features, most of which encourage poor testing -practices (mostly relating to testing implementation details). - -## 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. +
+ + TestingJavaScript.com Learn the smart, efficient way to test any JavaScript application. + +
## Table of Contents - -* [Installation](#installation) -* [Usage](#usage) - * [`Simulate`](#simulate) - * [`flushPromises`](#flushpromises) - * [`render`](#render) -* [`TextMatch`](#textmatch) -* [`query` APIs](#query-apis) -* [Examples](#examples) -* [FAQ](#faq) -* [Other Solutions](#other-solutions) -* [Guiding Principles](#guiding-principles) -* [Contributors](#contributors) -* [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 -``` +## The problem -This library has a `peerDependencies` listing for `react-dom`. +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. -## Usage +## The solution -```javascript -// __tests__/fetch.js -import React from 'react' -import {render, Simulate, flushPromises} from 'react-testing-library' -import axiosMock from 'axios' -import Fetch from '../fetch' // see the tests for a full implementation +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: -test('Fetch makes an API call and displays the greeting when load-greeting is clicked', async () => { - // Arrange - axiosMock.get.mockImplementationOnce(() => - Promise.resolve({ - data: {greeting: 'hello there'}, - }), - ) - const url = '/greeting' - const {getByText, getByTestId, container} = render() +> [The more your tests resemble the way your software is used, the more +> confidence they can give you.][guiding-principle] - // Act - Simulate.click(getByText('Load Greeting')) +## Installation - // let's wait for our mocked `get` request promise to resolve - await flushPromises() +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`: - // Assert - expect(axiosMock.get).toHaveBeenCalledTimes(1) - expect(axiosMock.get).toHaveBeenCalledWith(url) - expect(getByTestId('greeting-text').textContent).toBe('hello there') - expect(container.firstChild).toMatchSnapshot() -}) +``` +npm install --save-dev @testing-library/react @testing-library/dom ``` -### `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). - -### `flushPromises` - -This is a simple utility that's useful for when your component is doing some -async work that you've mocked out, but you still need to wait until the next -tick of the event loop before you can continue your assertions. It simply -returns a promise that resolves in a `setImmediate`. Especially useful when -you make your test function an `async` function and use -`await flushPromises()`. - -See an example in the section about `render` below. - -### `render` - -In the example above, the `render` method returns an object that has a few -properties: - -#### `container` - -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. - -> Tip: To get the root element of your rendered element, use `container.firstChild`. - -#### `unmount` - -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). +or -> This method is a pretty small abstraction over -> `ReactDOM.unmountComponentAtNode` +for installation via [yarn][yarn] -```javascript -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 -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") -// -// -// -// The aria-labelledby attribute -// -// -// -// Wrapper labels -// -// -// It will NOT find the input node for this: -// -// -// For this case, you can provide a `selector` in the options: -const inputNode = getByLabelText('username-input', {selector: 'input'}) -// and that would work +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. - -#### `getByPlaceholderText(text: TextMatch): HTMLElement` +This library has `peerDependencies` listings for `react`, `react-dom` and +starting from RTL version 16 also `@testing-library/dom`. -This will search for all elements with a placeholder attribute and find one -that matches the given [`TextMatch`](#textmatch). +_React Testing Library versions 13+ require React v18. If your project uses an +older version of React, be sure to install version 12:_ -```javascript -// -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` +yarn add --dev @testing-library/react@12 +``` -This will search for all elements that have a text node with `textContent` -matching the given [`TextMatch`](#textmatch). +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). -```javascript -// About ℹ️ -const aboutAnchorNode = getByText('about') -``` +> [**Docs**](https://testing-library.com/react) -#### `getByTestId` +### Suppressing unnecessary warnings on React DOM 16.8 -A shortcut to `` container.querySelector(`[data-testid="${yourId}"]`) ``. +There is a known compatibility issue with React DOM 16.8 where you will see the +following warning: -```javascript -// -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] - -## `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. - -Here's an example - -```javascript -//
Hello World
-// all of the following will find the div -getByText('Hello World') // full match -getByText('llo worl') // substring match -getByText('hello world') // strings ignore case -getByText(/Hello W?oRlD/i) // regex -getByText((content, element) => content.startsWith('Hello')) // function - -// all of the following will NOT find the div -getByText('Goodbye World') // non-string match -getByText(/hello world/) // case-sensitive regex with different case -// function looking for a span when it's actually a div -getByText((content, element) => { - return element.tagName.toLowerCase() === 'span' && content.startsWith('Hello') -}) +Warning: An update to ComponentName inside a test was not wrapped in act(...). ``` -## `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: +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)): + +```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) + } +}) -```javascript -const submitButton = queryByText('submit') -expect(submitButton).toBeNull() // it doesn't exist +afterAll(() => { + console.error = originalError +}) ``` ## Examples -You'll find examples of testing with different libraries in -[the test directory](https://github.com/kentcdodds/react-testing-library/blob/master/src/__tests__). -Some included are: - -* [`react-redux`](https://github.com/kentcdodds/react-testing-library/blob/master/src/__tests__/react-redux.js) -* [`react-router`](https://github.com/kentcdodds/react-testing-library/blob/master/src/__tests__/react-router.js) - -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. `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)). - -
- -
- -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). - -
- -
- -How do I update the props of a rendered component? - -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, the easiest -way to do that is: - -```javascript -const {container, getByTestId} = render() -expect(getByTestId('number-display').textContent).toBe('1') +### 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 ( +
+ + setShowMessage(e.target.checked)} + checked={showMessage} + /> + {showMessage ? children : null} +
+ ) +} -// re-render the same component with different props -// but pass the same container in the options argument. -// which will cause a re-render of the same instance (normal React behavior). -render(, {container}) -expect(getByTestId('number-display').textContent).toBe('2') +export default HiddenMessage ``` -[Open the tests](https://github.com/kentcdodds/react-testing-library/blob/master/src/__tests__/number-display.js) -for a full example of this. - -
+```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 -
+import * as React from 'react' +import {render, fireEvent, screen} from '@testing-library/react' +import HiddenMessage from '../hidden-message' -If I can't use shallow rendering, how do I mock out components in tests? +test('shows the children when the checkbox is checked', () => { + const testMessage = 'Test Message' + render({testMessage}) -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. + // 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 -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} -}) + // the queries can accept a regex to make your selectors more resilient to content tweaks and changes. + fireEvent.click(screen.getByLabelText(/show/i)) -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 + // .toBeInTheDocument() is an assertion that comes from jest-dom + // otherwise you could use .toBeDefined() + expect(screen.getByText(testMessage)).toBeInTheDocument() }) ``` -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/src/__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/src/__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. +### 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} +
+ ) +} -```javascript -expect(queryByTestId('thing-that-does-not-exist')).toBeNull() +export default Login ``` -
- -
- -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. +```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)) + }), +) + +beforeAll(() => server.listen()) +afterEach(() => { + server.resetHandlers() + window.localStorage.removeItem('token') +}) +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) +}) -```javascript -const firstLiInDiv = container.querySelector('div li') -const allLisInDiv = container.querySelectorAll('div li') -const rootElement = container.firstChild -``` +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'})) + }), + ) -
+ render() -
+ // fill out the form + fireEvent.change(screen.getByLabelText(/username/i), { + target: {value: 'chuck'}, + }) + fireEvent.change(screen.getByLabelText(/password/i), { + target: {value: 'norris'}, + }) -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? + fireEvent.click(screen.getByText(/submit/i)) -You can make your selector just choose the one you want by including :nth-child in the selector. + // wait for the error message + const alert = await screen.findByRole('alert') -```javascript -const thirdLiInUl = container.querySelector('ul > li:nth-child(3)') + expect(alert).toHaveTextContent(/internal server error/i) + expect(window.localStorage.getItem('token')).toBeNull() +}) ``` -Or you could include the index or an ID in your attribute: +> 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. -```javascript -
  • {item.text}
  • -``` +### More Examples -And then you could use the `getByTestId` utility: +> We're in the process of moving examples to the +> [docs site](https://testing-library.com/docs/example-codesandbox) -```javascript -const items = [ - /* your items */ -] -const {getByTestId} = render(/* your component with the items */) -const thirdItem = getByTestId(`item-${items[2].id}`) -``` +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://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 -What about enzyme is "bloated with complexity and features" and "encourage -poor testing practices"? +If you are interested in testing a custom hook, check out [React Hooks Testing +Library][react-hooks-testing-library]. -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). +> 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. -The guiding principle for this library is: +## Guiding Principles -> The more your tests resemble the way your software is used, the more confidence they can give you. - [17 Feb 2018][guiding-principle] +> [The more your tests resemble the way your software is used, the more +> confidence they can give you.][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. +We try to only expose methods and utilities that encourage you to write tests +that closely resemble how your React components are used. -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. +Utilities are included in this project based on the following guiding +principles: -
    +1. If it relates to rendering components, it deals with DOM nodes rather than + component instances, nor should it encourage dealing with component + instances. +2. It should be generally useful for testing individual React components or + full React applications. While this library is focused on `react-dom`, + utilities could be included even if they don't directly relate to + `react-dom`. +3. Utility implementations and APIs should be simple and flexible. -
    +Most importantly, we want React Testing Library to be pretty light-weight, +simple, and easy to understand. -How does flushPromises work and why would I need it? +## Docs -As mentioned [before](#flushpromises), `flushPromises` uses -[`setImmediate`][set-immediate] to schedule resolving a promise after any pending -tasks in -[the message queue](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop) -are processed. This includes any promises fired before in your test. +[**Read The Docs**](https://testing-library.com/react) | +[Edit the docs](https://github.com/testing-library/testing-library-docs) -If there are promise callbacks already in JavaScript's message queue pending to be -processed at the time `flushPromises` is called, then these will be processed before -the promise returned by `flushPromises` is resolved. So when you -`await flushPromises()` the code immediately after it is guaranteed to occur after -all the side effects of your async requests have ocurred. This includes any data -your test components might have requested. +## Issues -This is useful for instance, if your components perform any data requests and update -their state with the results when the request is resolved. It's important to note -that this is only effective if you've mocked out your async requests to resolve -immediately (like the `axios` mock we have in the examples). It will not `await` -for promises that are not already resolved by the time you attempt to flush them. +Looking to contribute? Look for the [Good First Issue][good-first-issue] label. -
    +### 🐛 Bugs -## Other Solutions +Please file an issue for bugs, missing documentation, or unexpected behavior. -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. +[**See Bugs**][bugs] -## Guiding Principles +### 💡 Feature Requests -> [The more your tests resemble the way your software is used, the more confidence they can give you.][guiding-principle] +Please file an issue to suggest new features. Vote on feature requests by adding +a 👍. This helps maintainers prioritize what to work on. -We try to only expose methods and utilities that encourage you to write tests -that closely resemble how your react components are used. +[**See Feature Requests**][requests] -Utilities are included in this project based on the following guiding -principles: +### ❓ Questions -1. If it relates to rendering components, it deals with DOM nodes rather than - component instances, nor should it encourage dealing with component - instances. -2. It should be generally useful for testing individual React components or - full React applications. While this library is focused on `react-dom`, - utilities could be included even if they don't directly relate to - `react-dom`. -3. Utility implementations and APIs should be simple and flexible. +For questions related to using the library, please visit a support community +instead of filing an issue on GitHub. -At the end of the day, what we want is for this library to be pretty -light-weight, simple, and understandable. +- [Discord][discord] +- [Stack Overflow][stackoverflow] ## Contributors 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") | -| :---: | :---: | :---: | :---: | :---: | :---: | :---: | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Kent C. Dodds
    Kent C. Dodds

    💻 📖 🚇 ⚠️
    Ryan Castner
    Ryan Castner

    📖
    Daniel Sandiego
    Daniel Sandiego

    💻
    Paweł Mikołajczyk
    Paweł Mikołajczyk

    💻
    Alejandro Ñáñez Ortiz
    Alejandro Ñáñez Ortiz

    📖
    Matt Parrish
    Matt Parrish

    🐛 💻 📖 ⚠️
    Justin Hall
    Justin Hall

    📦
    Anto Aravinth
    Anto Aravinth

    💻 ⚠️ 📖
    Jonah Moses
    Jonah Moses

    📖
    Łukasz Gandecki
    Łukasz Gandecki

    💻 ⚠️ 📖
    Ivan Babak
    Ivan Babak

    🐛 🤔
    Jesse Day
    Jesse Day

    💻
    Ernesto García
    Ernesto García

    💬 💻 📖
    Josef Maxx Blake
    Josef Maxx Blake

    💻 📖 ⚠️
    Michal Baranowski
    Michal Baranowski

    📝
    Arthur Puthin
    Arthur Puthin

    📖
    Thomas Chia
    Thomas Chia

    💻 📖
    Thiago Galvani
    Thiago Galvani

    📖
    Christian
    Christian

    ⚠️
    Alex Krolick
    Alex Krolick

    💬 📖 💡 🤔
    Johann Hubert Sonntagbauer
    Johann Hubert Sonntagbauer

    💻 📖 ⚠️
    Maddi Joyce
    Maddi Joyce

    💻
    Ryan Vice
    Ryan Vice

    📖
    Ian Wilson
    Ian Wilson

    📝
    Daniel
    Daniel

    🐛 💻
    Giorgio Polvara
    Giorgio Polvara

    🐛 🤔
    John Gozde
    John Gozde

    💻
    Sam Horton
    Sam Horton

    📖 💡 🤔
    Richard Kotze (mobile)
    Richard Kotze (mobile)

    📖
    Brahian E. Soto Mercedes
    Brahian E. Soto Mercedes

    📖
    Benoit de La Forest
    Benoit de La Forest

    📖
    Salah
    Salah

    💻 ⚠️
    Adam Gordon
    Adam Gordon

    🐛 💻
    Matija Marohnić
    Matija Marohnić

    📖
    Justice Mba
    Justice Mba

    📖
    Mark Pollmann
    Mark Pollmann

    📖
    Ehtesham Kafeel
    Ehtesham Kafeel

    💻 📖
    Julio Pavón
    Julio Pavón

    💻
    Duncan L
    Duncan L

    📖 💡
    Tiago Almeida
    Tiago Almeida

    📖
    Robert Smith
    Robert Smith

    🐛
    Zach Green
    Zach Green

    📖
    dadamssg
    dadamssg

    📖
    Yazan Aabed
    Yazan Aabed

    📝
    Tim
    Tim

    🐛 💻 📖 ⚠️
    Divyanshu Maithani
    Divyanshu Maithani

    📹
    Deepak Grover
    Deepak Grover

    📹
    Eyal Cohen
    Eyal Cohen

    📖
    Peter Makowski
    Peter Makowski

    📖
    Michiel Nuyts
    Michiel Nuyts

    📖
    Joe Ng'ethe
    Joe Ng'ethe

    💻 📖
    Kate
    Kate

    📖
    Sean
    Sean

    📖
    James Long
    James Long

    🤔 📦
    Herb Hagely
    Herb Hagely

    💡
    Alex Wendte
    Alex Wendte

    💡
    Monica Powell
    Monica Powell

    📖
    Vitaly Sivkov
    Vitaly Sivkov

    💻
    Weyert de Boer
    Weyert de Boer

    🤔 👀 🎨
    EstebanMarin
    EstebanMarin

    📖
    Victor Martins
    Victor Martins

    📖
    Royston Shufflebotham
    Royston Shufflebotham

    🐛 📖 💡
    chrbala
    chrbala

    💻
    Donavon West
    Donavon West

    💻 📖 🤔 ⚠️
    Richard Maisano
    Richard Maisano

    💻
    Marco Biedermann
    Marco Biedermann

    💻 🚧 ⚠️
    Alex Zherdev
    Alex Zherdev

    🐛 💻
    André Matulionis dos Santos
    André Matulionis dos Santos

    💻 💡 ⚠️
    Daniel K.
    Daniel K.

    🐛 💻 🤔 ⚠️ 👀
    mohamedmagdy17593
    mohamedmagdy17593

    💻
    Loren ☺️
    Loren ☺️

    📖
    MarkFalconbridge
    MarkFalconbridge

    🐛 💻
    Vinicius
    Vinicius

    📖 💡
    Peter Schyma
    Peter Schyma

    💻
    Ian Schmitz
    Ian Schmitz

    📖
    Joel Marcotte
    Joel Marcotte

    🐛 ⚠️ 💻
    Alejandro Dustet
    Alejandro Dustet

    🐛
    Brandon Carroll
    Brandon Carroll

    📖
    Lucas Machado
    Lucas Machado

    📖
    Pascal Duez
    Pascal Duez

    📦
    Minh Nguyen
    Minh Nguyen

    💻
    LiaoJimmy
    LiaoJimmy

    📖
    Sunil Pai
    Sunil Pai

    💻 ⚠️
    Dan Abramov
    Dan Abramov

    👀
    Christian Murphy
    Christian Murphy

    🚇
    Ivakhnenko Dmitry
    Ivakhnenko Dmitry

    💻
    James George
    James George

    📖
    João Fernandes
    João Fernandes

    📖
    Alejandro Perea
    Alejandro Perea

    👀
    Nick McCurdy
    Nick McCurdy

    👀 💬 🚇
    Sebastian Silbermann
    Sebastian Silbermann

    👀
    Adrià Fontcuberta
    Adrià Fontcuberta

    👀 📖
    John Reilly
    John Reilly

    👀
    Michaël De Boey
    Michaël De Boey

    👀 💻
    Tim Yates
    Tim Yates

    👀
    Brian Donovan
    Brian Donovan

    💻
    Noam Gabriel Jacobson
    Noam Gabriel Jacobson

    📖
    Ronald van der Kooij
    Ronald van der Kooij

    ⚠️ 💻
    Aayush Rajvanshi
    Aayush Rajvanshi

    📖
    Ely Alamillo
    Ely Alamillo

    💻 ⚠️
    Daniel Afonso
    Daniel Afonso

    💻 ⚠️
    Laurens Bosscher
    Laurens Bosscher

    💻
    Sakito Mukai
    Sakito Mukai

    📖
    Türker Teke
    Türker Teke

    📖
    Zach Brogan
    Zach Brogan

    💻 ⚠️
    Ryota Murakami
    Ryota Murakami

    📖
    Michael Hottman
    Michael Hottman

    🤔
    Steven Fitzpatrick
    Steven Fitzpatrick

    🐛
    Juan Je García
    Juan Je García

    📖
    Championrunner
    Championrunner

    📖
    Sam Tsai
    Sam Tsai

    💻 ⚠️ 📖
    Christian Rackerseder
    Christian Rackerseder

    💻
    Andrei Picus
    Andrei Picus

    🐛 👀
    Artem Zakharchenko
    Artem Zakharchenko

    📖
    Michael
    Michael

    📖
    Braden Lee
    Braden Lee

    📖
    Kamran Ayub
    Kamran Ayub

    💻 ⚠️
    Matan Borenkraout
    Matan Borenkraout

    💻
    Ryan Bigg
    Ryan Bigg

    🚧
    Anton Halim
    Anton Halim

    📖
    Artem Malko
    Artem Malko

    💻
    Gerrit Alex
    Gerrit Alex

    💻
    Karthick Raja
    Karthick Raja

    💻
    Abdelrahman Ashraf
    Abdelrahman Ashraf

    💻
    Lidor Avitan
    Lidor Avitan

    📖
    Jordan Harband
    Jordan Harband

    👀 🤔
    Marco Moretti
    Marco Moretti

    💻
    sanchit121
    sanchit121

    🐛 💻
    Solufa
    Solufa

    🐛 💻
    Ari Perkkiö
    Ari Perkkiö

    ⚠️
    Johannes Ewald
    Johannes Ewald

    💻
    Angus J. Pope
    Angus J. Pope

    📖
    Dominik Lesch
    Dominik Lesch

    📖
    Marcos Gómez
    Marcos Gómez

    📖
    Akash Shyam
    Akash Shyam

    🐛
    Fabian Meumertzheim
    Fabian Meumertzheim

    💻 🐛
    Sebastian Malton
    Sebastian Malton

    🐛 💻
    Martin Böttcher
    Martin Böttcher

    💻
    Dominik Dorfmeister
    Dominik Dorfmeister

    💻
    Stephen Sauceda
    Stephen Sauceda

    📖
    Colin Diesh
    Colin Diesh

    📖
    Yusuke Iinuma
    Yusuke Iinuma

    💻
    Jeff Way
    Jeff Way

    💻
    Bernardo Belchior
    Bernardo Belchior

    💻 📖
    + + + @@ -572,33 +650,43 @@ 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 -[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 +[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/jest.config.js b/jest.config.js new file mode 100644 index 00000000..860358cd --- /dev/null +++ b/jest.config.js @@ -0,0 +1,19 @@ +const {jest: jestConfig} = require('kcd-scripts/config') + +module.exports = Object.assign(jestConfig, { + coverageThreshold: { + ...jestConfig.coverageThreshold, + // Full coverage across the build matrix (React 18, 19) but not in a single job + // Ful coverage is checked via codecov + './src/act-compat': { + branches: 90, + }, + './src/pure': { + // minimum coverage of jobs using React 18 and 19 + branches: 95, + functions: 88, + lines: 92, + statements: 92, + }, + }, +}) 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/manual-releases.md b/other/manual-releases.md index f4c5b113..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: 1 +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 06c161a7..b1bff976 100644 --- a/package.json +++ b/package.json @@ -1,64 +1,116 @@ { - "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", + "setup": "npm install && npm run validate -s", "test": "kcd-scripts test", "test:update": "npm test -- --updateSnapshot --coverage", - "validate": "kcd-scripts validate", - "setup": "npm install && npm run validate -s", - "precommit": "kcd-scripts precommit" + "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": [], - "author": "Kent C. Dodds (http://kentcdodds.com/)", + "keywords": [ + "testing", + "react", + "ui", + "dom", + "jsdom", + "unit", + "integration", + "functional", + "end-to-end", + "e2e" + ], + "author": "Kent C. Dodds (https://kentcdodds.com)", "license": "MIT", - "dependencies": {}, + "dependencies": { + "@babel/runtime": "^7.12.5" + }, "devDependencies": { - "@types/react-dom": "^16.0.4", - "axios": "^0.18.0", - "history": "^4.7.2", - "jest-in-case": "^1.0.2", - "kcd-scripts": "^0.36.1", - "react": "^16.2.0", - "react-dom": "^16.2.0", - "react-redux": "^5.0.7", - "react-router": "^4.2.0", - "react-router-dom": "^4.2.2", - "react-transition-group": "^2.2.1", - "redux": "^3.7.2" + "@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/prop-types": "off", + "react/no-adjacent-inline-elements": "off", + "import/no-unassigned-import": "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__/element-queries.js.snap b/src/__tests__/__snapshots__/element-queries.js.snap deleted file mode 100644 index ccbe9bc6..00000000 --- a/src/__tests__/__snapshots__/element-queries.js.snap +++ /dev/null @@ -1,13 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`get throws a useful error message 1`] = `"Unable to find a label with the text of: LucyRicardo"`; - -exports[`get throws a useful error message 2`] = `"Unable to find an element with the placeholder text of: LucyRicardo"`; - -exports[`get throws a useful error message 3`] = `"Unable to find an element with the text: LucyRicardo. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible."`; - -exports[`get throws a useful error message 4`] = `"Unable to find an element by: [data-testid=\\"LucyRicardo\\"]"`; - -exports[`label with no form control 1`] = `"Found a label with the text of: alone, however no form control was found associated to that label. Make sure you're using the \\"for\\" attribute or \\"aria-labelledby\\" attribute correctly."`; - -exports[`totally empty label 1`] = `"Found a label with the text of: , however no form control was found associated to that label. Make sure you're using the \\"for\\" attribute or \\"aria-labelledby\\" attribute correctly."`; 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`] = ` -
    - - - 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 + } + 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('
    hi
    ') +}) 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 new file mode 100644 index 00000000..c6a1d1fe --- /dev/null +++ b/src/__tests__/debug.js @@ -0,0 +1,55 @@ +import * as React from 'react' +import {render, screen} from '../' + +beforeEach(() => { + jest.spyOn(console, 'log').mockImplementation(() => {}) +}) + +afterEach(() => { + console.log.mockRestore() +}) + +test('debug pretty prints the container', () => { + const HelloWorld = () =>

    Hello World

    + const {debug} = render() + debug() + expect(console.log).toHaveBeenCalledTimes(1) + expect(console.log).toHaveBeenCalledWith( + expect.stringContaining('Hello World'), + ) +}) + +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__/element-queries.js b/src/__tests__/element-queries.js deleted file mode 100644 index cec3f1ab..00000000 --- a/src/__tests__/element-queries.js +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react' -import {render} from '../' - -test('query can return null', () => { - const { - queryByLabelText, - queryByPlaceholderText, - queryByText, - queryByTestId, - } = render(
    ) - expect(queryByTestId('LucyRicardo')).toBeNull() - expect(queryByLabelText('LucyRicardo')).toBeNull() - expect(queryByPlaceholderText('LucyRicardo')).toBeNull() - expect(queryByText('LucyRicardo')).toBeNull() -}) - -test('get throws a useful error message', () => { - const {getByLabelText, getByPlaceholderText, getByText, getByTestId} = render( -
    , - ) - expect(() => getByLabelText('LucyRicardo')).toThrowErrorMatchingSnapshot() - expect(() => - getByPlaceholderText('LucyRicardo'), - ).toThrowErrorMatchingSnapshot() - expect(() => getByText('LucyRicardo')).toThrowErrorMatchingSnapshot() - expect(() => getByTestId('LucyRicardo')).toThrowErrorMatchingSnapshot() -}) - -test('get can get form controls by label text', () => { - const {getByLabelText} = render( -
    - -
    - - -
    -
    - - -
    -
    , - ) - expect(getByLabelText('1st').id).toBe('first-id') - expect(getByLabelText('2nd').id).toBe('second-id') - expect(getByLabelText('3rd').id).toBe('third-id') -}) - -test('get can get form controls by placeholder', () => { - const {getByPlaceholderText} = render( - , - ) - expect(getByPlaceholderText('username').id).toBe('username-id') -}) - -test('label with no form control', () => { - const {getByLabelText, queryByLabelText} = render() - expect(queryByLabelText('alone')).toBeNull() - expect(() => getByLabelText('alone')).toThrowErrorMatchingSnapshot() -}) - -test('totally empty label', () => { - const {getByLabelText, queryByLabelText} = 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 c20c7fbc..00000000 --- a/src/__tests__/fetch.js +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react' -import axiosMock from 'axios' -import {render, Simulate, flushPromises} 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 ( -
    - - {data ? {data.greeting} : null} -
    - ) - } -} - -test('Fetch makes an API call and displays the greeting when load-greeting is clicked', async () => { - // Arrange - axiosMock.get.mockImplementationOnce(() => - Promise.resolve({ - data: {greeting: 'hello there'}, - }), - ) - const url = '/greeting' - const {getByText, container} = render() - - // Act - Simulate.click(getByText('Fetch')) - - await flushPromises() - - // 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 a0a3ed06..00000000 --- a/src/__tests__/forms.js +++ /dev/null @@ -1,60 +0,0 @@ -import React from 'react' -import {render, Simulate} from '../' - -function Login({onSubmit}) { - return ( -
    -
    { - e.preventDefault() - const {username, password} = e.target.elements - onSubmit({ - username: username.value, - password: password.value, - }) - }} - > - - - - - -
    -
    - ) -} - -test('login form submits', () => { - const fakeUser = {username: 'jackiechan', password: 'hiya! 🥋'} - const handleSubmit = jest.fn() - const {container, getByLabelText, getByText} = render( - , - ) - - const usernameNode = getByLabelText('username') - const passwordNode = getByLabelText('password') - const formNode = container.querySelector('form') - const submitButtonNode = getByText('submit') - - // 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__/mock.react-transition-group.js b/src/__tests__/mock.react-transition-group.js deleted file mode 100644 index 67a8b790..00000000 --- a/src/__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 '../' - -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 ( -
    - - -
    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/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__/number-display.js b/src/__tests__/number-display.js deleted file mode 100644 index 60a9ea32..00000000 --- a/src/__tests__/number-display.js +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react' -import {render} from '../' - -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 {container, getByTestId} = render() - expect(getByTestId('number-display').textContent).toBe('1') - - // re-render the same component with different props - // but pass the same container in the options argument. - // which will cause a re-render of the same instance (normal React behavior). - render(, {container}) - expect(getByTestId('number-display').textContent).toBe('2') - - expect(getByTestId('instance-id').textContent).toBe('1') -}) diff --git a/src/__tests__/react-redux.js b/src/__tests__/react-redux.js deleted file mode 100644 index 6162753e..00000000 --- a/src/__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 '../' - -// 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/src/__tests__/react-router.js b/src/__tests__/react-router.js deleted file mode 100644 index d0d6c934..00000000 --- a/src/__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 '../' - -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'), 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/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 ( + + ) + } + 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 new file mode 100644 index 00000000..6c48c4dd --- /dev/null +++ b/src/__tests__/rerender.js @@ -0,0 +1,98 @@ +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(` +
    +
    + hi +
    +
    + `) + + rerender() + expect(container.firstChild).toMatchInlineSnapshot(` +
    +
    + hey +
    +
    + `) + }) + + 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__/shallow.react-transition-group.js b/src/__tests__/shallow.react-transition-group.js deleted file mode 100644 index cb3e329e..00000000 --- a/src/__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 '../' - -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 ( -
    - - -
    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')) - expect(CSSTransition).toHaveBeenCalledWith( - {in: true, ...defaultProps}, - expect.any(Object), - ) -}) diff --git a/src/__tests__/stopwatch.js b/src/__tests__/stopwatch.js index 2e6dbf57..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/__tests__/text-matchers.js b/src/__tests__/text-matchers.js deleted file mode 100644 index c932572a..00000000 --- a/src/__tests__/text-matchers.js +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react' -import cases from 'jest-in-case' -import {render} from '../' - -cases( - 'text matchers', - opts => { - const {getByText} = render( - - About - , - ) - expect(getByText(opts.textMatch).id).toBe('anchor') - }, - [ - {name: 'string match', textMatch: 'About'}, - {name: 'case insensitive', textMatch: 'about'}, - {name: 'regex', textMatch: /^about$/i}, - { - name: 'function', - textMatch: (text, element) => - element.tagName === 'A' && text.includes('out'), - }, - ], -) 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 fd46ac9e..bb0d0270 100644 --- a/src/index.js +++ b/src/index.js @@ -1,27 +1,41 @@ -import ReactDOM from 'react-dom' -import {Simulate} from 'react-dom/test-utils' -import * as queries from './queries' +import {getIsReactActEnvironment, setReactActEnvironment} from './act-compat' +import {cleanup} from './pure' -function render(ui, {container = document.createElement('div')} = {}) { - ReactDOM.render(ui, container) - const containerHelpers = Object.entries(queries).reduce( - (helpers, [key, fn]) => { - helpers[key] = fn.bind(null, container) - return helpers - }, - {}, - ) - return { - container, - unmount: () => ReactDOM.unmountComponentAtNode(container), - ...containerHelpers, +// 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() + }) } -} -// this returns a new promise and is just a simple way to -// wait until the next tick so resolved promises chains will continue -function flushPromises() { - return new Promise(resolve => setImmediate(resolve)) + // No test setup with other test runners available + /* istanbul ignore else */ + if (typeof beforeAll === 'function' && typeof afterAll === 'function') { + // This matches the behavior of React < 18. + let previousIsReactActEnvironment = getIsReactActEnvironment() + beforeAll(() => { + previousIsReactActEnvironment = getIsReactActEnvironment() + setReactActEnvironment(true) + }) + + afterAll(() => { + setReactActEnvironment(previousIsReactActEnvironment) + }) + } } -export {render, flushPromises, Simulate} +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/src/queries.js b/src/queries.js deleted file mode 100644 index 94382dab..00000000 --- a/src/queries.js +++ /dev/null @@ -1,143 +0,0 @@ -// Here are the queries for the library. -// The queries here should only be things that are accessible to both users who are using a screen reader -// and those who are not using a screen reader (with the exception of the data-testid attribute query). - -function queryLabelByText(container, text) { - return ( - Array.from(container.querySelectorAll('label')).find(label => - matches(label.textContent, label, text), - ) || null - ) -} - -function queryByLabelText(container, text, {selector = '*'} = {}) { - const label = queryLabelByText(container, text) - if (!label) { - return null - } - /* istanbul ignore if */ - if (label.control) { - // appears to be unsupported in jsdom: https://github.com/jsdom/jsdom/issues/2175 - // but this would be the proper way to do things - return label.control - } else if (label.getAttribute('for')) { - // - return container.querySelector(`#${label.getAttribute('for')}`) - } else if (label.getAttribute('id')) { - // - return container.querySelector( - `[aria-labelledby="${label.getAttribute('id')}"]`, - ) - } else if (label.childNodes.length) { - // - return label.querySelector(selector) - } else { - return null - } -} - -function queryByText(container, text, {selector = '*'} = {}) { - return ( - Array.from(container.querySelectorAll(selector)).find(node => - matches(getText(node), node, text), - ) || null - ) -} - -function queryByPlaceholderText(container, text) { - return ( - Array.from(container.querySelectorAll('[placeholder]')).find(node => - matches(node.getAttribute('placeholder'), node, text), - ) || null - ) -} - -function queryByTestId(container, id) { - return container.querySelector(getDataTestIdSelector(id)) -} - -function getDataTestIdSelector(id) { - return `[data-testid="${id}"]` -} - -function getText(node) { - return Array.from(node.childNodes) - .filter( - child => child.nodeType === Node.TEXT_NODE && Boolean(child.textContent), - ) - .map(c => c.textContent) - .join(' ') -} - -function matches(textToMatch, node, matcher) { - if (typeof matcher === 'string') { - return textToMatch.toLowerCase().includes(matcher.toLowerCase()) - } else if (typeof matcher === 'function') { - return matcher(textToMatch, node) - } else { - return matcher.test(textToMatch) - } -} - -// getters -// the reason we're not dynamically generating these functions that look so similar: -// 1. The error messages are specific to each one and depend on arguments -// 2. The stack trace will look better because it'll have a helpful method name. - -function getByTestId(container, id, ...rest) { - const el = queryByTestId(container, id, ...rest) - if (!el) { - throw new Error( - `Unable to find an element by: ${getDataTestIdSelector(id)}`, - ) - } - return el -} - -function getByPlaceholderText(container, text, ...rest) { - const el = queryByPlaceholderText(container, text, ...rest) - if (!el) { - throw new Error( - `Unable to find an element with the placeholder text of: ${text}`, - ) - } - return el -} - -function getByLabelText(container, text, ...rest) { - const el = queryByLabelText(container, text, ...rest) - if (!el) { - const label = queryLabelByText(container, text) - if (label) { - throw new Error( - `Found a label with the text of: ${text}, however no form control was found associated to that label. Make sure you're using the "for" attribute or "aria-labelledby" attribute correctly.`, - ) - } else { - throw new Error(`Unable to find a label with the text of: ${text}`) - } - } - return el -} - -function getByText(container, text, ...rest) { - const el = queryByText(container, text, ...rest) - if (!el) { - throw new Error( - `Unable to find an element with the text: ${text}. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.`, - ) - } - return el -} - -export { - queryByPlaceholderText, - getByPlaceholderText, - queryByText, - getByText, - queryByLabelText, - getByLabelText, - queryByTestId, - getByTestId, -} - -/* eslint complexity:["error", 14] */ 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(