diff --git a/examples/basic/.eslintignore b/examples/basic/.eslintignore deleted file mode 100644 index bda2aca85..000000000 --- a/examples/basic/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -jest-setup.ts diff --git a/examples/basic/.eslintrc b/examples/basic/.eslintrc deleted file mode 100644 index 59f14136a..000000000 --- a/examples/basic/.eslintrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "@callstack" -} diff --git a/examples/basic/eslint.config.mjs b/examples/basic/eslint.config.mjs new file mode 100644 index 000000000..8023fb27a --- /dev/null +++ b/examples/basic/eslint.config.mjs @@ -0,0 +1,13 @@ +import testingLibrary from 'eslint-plugin-testing-library'; +import tseslint from 'typescript-eslint'; + +export default [ + { + ignores: ['node_modules/**', 'jest-setup.ts'], + }, + ...tseslint.configs.recommended, + { + files: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'], + ...testingLibrary.configs['flat/react'], + }, +]; diff --git a/examples/basic/package.json b/examples/basic/package.json index 8ad7c2cd9..57c248bae 100644 --- a/examples/basic/package.json +++ b/examples/basic/package.json @@ -22,13 +22,14 @@ "devDependencies": { "@babel/core": "^7.24.0", "@testing-library/react-native": "^14.0.0-beta.0", - "@types/eslint": "^8.56.10", "@types/jest": "^29.5.12", "@types/react": "~19.1.10", - "eslint": "^8.57.0", + "eslint": "^9.0.0", + "eslint-plugin-testing-library": "^7.1.1", "jest": "^29.7.0", "test-renderer": "0.14.0", - "typescript": "~5.9.2" + "typescript": "~5.9.2", + "typescript-eslint": "^8.0.0" }, "private": true, "packageManager": "yarn@4.0.1" diff --git a/examples/basic/yarn.lock b/examples/basic/yarn.lock index 94459f86d..4c26f570d 100644 --- a/examples/basic/yarn.lock +++ b/examples/basic/yarn.lock @@ -1236,45 +1236,91 @@ __metadata: languageName: node linkType: hard -"@eslint-community/eslint-utils@npm:^4.2.0": - version: 4.7.0 - resolution: "@eslint-community/eslint-utils@npm:4.7.0" +"@eslint-community/eslint-utils@npm:^4.8.0, @eslint-community/eslint-utils@npm:^4.9.1": + version: 4.9.1 + resolution: "@eslint-community/eslint-utils@npm:4.9.1" dependencies: eslint-visitor-keys: "npm:^3.4.3" peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - checksum: 10c0/c0f4f2bd73b7b7a9de74b716a664873d08ab71ab439e51befe77d61915af41a81ecec93b408778b3a7856185244c34c2c8ee28912072ec14def84ba2dec70adf + checksum: 10c0/dc4ab5e3e364ef27e33666b11f4b86e1a6c1d7cbf16f0c6ff87b1619b3562335e9201a3d6ce806221887ff780ec9d828962a290bb910759fd40a674686503f02 languageName: node linkType: hard -"@eslint-community/regexpp@npm:^4.6.1": - version: 4.12.1 - resolution: "@eslint-community/regexpp@npm:4.12.1" - checksum: 10c0/a03d98c246bcb9109aec2c08e4d10c8d010256538dcb3f56610191607214523d4fb1b00aa81df830b6dffb74c5fa0be03642513a289c567949d3e550ca11cdf6 +"@eslint-community/regexpp@npm:^4.12.1, @eslint-community/regexpp@npm:^4.12.2": + version: 4.12.2 + resolution: "@eslint-community/regexpp@npm:4.12.2" + checksum: 10c0/fddcbc66851b308478d04e302a4d771d6917a0b3740dc351513c0da9ca2eab8a1adf99f5e0aa7ab8b13fa0df005c81adeee7e63a92f3effd7d367a163b721c2d languageName: node linkType: hard -"@eslint/eslintrc@npm:^2.1.4": - version: 2.1.4 - resolution: "@eslint/eslintrc@npm:2.1.4" +"@eslint/config-array@npm:^0.21.1": + version: 0.21.1 + resolution: "@eslint/config-array@npm:0.21.1" + dependencies: + "@eslint/object-schema": "npm:^2.1.7" + debug: "npm:^4.3.1" + minimatch: "npm:^3.1.2" + checksum: 10c0/2f657d4edd6ddcb920579b72e7a5b127865d4c3fb4dda24f11d5c4f445a93ca481aebdbd6bf3291c536f5d034458dbcbb298ee3b698bc6c9dd02900fe87eec3c + languageName: node + linkType: hard + +"@eslint/config-helpers@npm:^0.4.2": + version: 0.4.2 + resolution: "@eslint/config-helpers@npm:0.4.2" + dependencies: + "@eslint/core": "npm:^0.17.0" + checksum: 10c0/92efd7a527b2d17eb1a148409d71d80f9ac160b565ac73ee092252e8bf08ecd08670699f46b306b94f13d22e88ac88a612120e7847570dd7cdc72f234d50dcb4 + languageName: node + linkType: hard + +"@eslint/core@npm:^0.17.0": + version: 0.17.0 + resolution: "@eslint/core@npm:0.17.0" + dependencies: + "@types/json-schema": "npm:^7.0.15" + checksum: 10c0/9a580f2246633bc752298e7440dd942ec421860d1946d0801f0423830e67887e4aeba10ab9a23d281727a978eb93d053d1922a587d502942a713607f40ed704e + languageName: node + linkType: hard + +"@eslint/eslintrc@npm:^3.3.1": + version: 3.3.3 + resolution: "@eslint/eslintrc@npm:3.3.3" dependencies: ajv: "npm:^6.12.4" debug: "npm:^4.3.2" - espree: "npm:^9.6.0" - globals: "npm:^13.19.0" + espree: "npm:^10.0.1" + globals: "npm:^14.0.0" ignore: "npm:^5.2.0" import-fresh: "npm:^3.2.1" - js-yaml: "npm:^4.1.0" + js-yaml: "npm:^4.1.1" minimatch: "npm:^3.1.2" strip-json-comments: "npm:^3.1.1" - checksum: 10c0/32f67052b81768ae876c84569ffd562491ec5a5091b0c1e1ca1e0f3c24fb42f804952fdd0a137873bc64303ba368a71ba079a6f691cee25beee9722d94cc8573 + checksum: 10c0/532c7acc7ddd042724c28b1f020bd7bf148fcd4653bb44c8314168b5f772508c842ce4ee070299cac51c5c5757d2124bdcfcef5551c8c58ff9986e3e17f2260d + languageName: node + linkType: hard + +"@eslint/js@npm:9.39.2": + version: 9.39.2 + resolution: "@eslint/js@npm:9.39.2" + checksum: 10c0/00f51c52b04ac79faebfaa65a9652b2093b9c924e945479f1f3945473f78aee83cbc76c8d70bbffbf06f7024626575b16d97b66eab16182e1d0d39daff2f26f5 + languageName: node + linkType: hard + +"@eslint/object-schema@npm:^2.1.7": + version: 2.1.7 + resolution: "@eslint/object-schema@npm:2.1.7" + checksum: 10c0/936b6e499853d1335803f556d526c86f5fe2259ed241bc665000e1d6353828edd913feed43120d150adb75570cae162cf000b5b0dfc9596726761c36b82f4e87 languageName: node linkType: hard -"@eslint/js@npm:8.57.1": - version: 8.57.1 - resolution: "@eslint/js@npm:8.57.1" - checksum: 10c0/b489c474a3b5b54381c62e82b3f7f65f4b8a5eaaed126546520bf2fede5532a8ed53212919fed1e9048dcf7f37167c8561d58d0ba4492a4244004e7793805223 +"@eslint/plugin-kit@npm:^0.4.1": + version: 0.4.1 + resolution: "@eslint/plugin-kit@npm:0.4.1" + dependencies: + "@eslint/core": "npm:^0.17.0" + levn: "npm:^0.4.1" + checksum: 10c0/51600f78b798f172a9915dffb295e2ffb44840d583427bc732baf12ecb963eb841b253300e657da91d890f4b323d10a1bd12934bf293e3018d8bb66fdce5217b languageName: node linkType: hard @@ -1681,14 +1727,20 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/config-array@npm:^0.13.0": - version: 0.13.0 - resolution: "@humanwhocodes/config-array@npm:0.13.0" +"@humanfs/core@npm:^0.19.1": + version: 0.19.1 + resolution: "@humanfs/core@npm:0.19.1" + checksum: 10c0/aa4e0152171c07879b458d0e8a704b8c3a89a8c0541726c6b65b81e84fd8b7564b5d6c633feadc6598307d34564bd53294b533491424e8e313d7ab6c7bc5dc67 + languageName: node + linkType: hard + +"@humanfs/node@npm:^0.16.6": + version: 0.16.7 + resolution: "@humanfs/node@npm:0.16.7" dependencies: - "@humanwhocodes/object-schema": "npm:^2.0.3" - debug: "npm:^4.3.1" - minimatch: "npm:^3.0.5" - checksum: 10c0/205c99e756b759f92e1f44a3dc6292b37db199beacba8f26c2165d4051fe73a4ae52fdcfd08ffa93e7e5cb63da7c88648f0e84e197d154bbbbe137b2e0dd332e + "@humanfs/core": "npm:^0.19.1" + "@humanwhocodes/retry": "npm:^0.4.0" + checksum: 10c0/9f83d3cf2cfa37383e01e3cdaead11cd426208e04c44adcdd291aa983aaf72d7d3598844d2fe9ce54896bb1bf8bd4b56883376611c8905a19c44684642823f30 languageName: node linkType: hard @@ -1699,10 +1751,10 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/object-schema@npm:^2.0.3": - version: 2.0.3 - resolution: "@humanwhocodes/object-schema@npm:2.0.3" - checksum: 10c0/80520eabbfc2d32fe195a93557cef50dfe8c8905de447f022675aaf66abc33ae54098f5ea78548d925aa671cd4ab7c7daa5ad704fe42358c9b5e7db60f80696c +"@humanwhocodes/retry@npm:^0.4.0, @humanwhocodes/retry@npm:^0.4.2": + version: 0.4.3 + resolution: "@humanwhocodes/retry@npm:0.4.3" + checksum: 10c0/3775bb30087d4440b3f7406d5a057777d90e4b9f435af488a4923ef249e93615fb78565a85f173a186a076c7706a81d0d57d563a2624e4de2c5c9c66c486ce42 languageName: node linkType: hard @@ -2078,33 +2130,6 @@ __metadata: languageName: node linkType: hard -"@nodelib/fs.scandir@npm:2.1.5": - version: 2.1.5 - resolution: "@nodelib/fs.scandir@npm:2.1.5" - dependencies: - "@nodelib/fs.stat": "npm:2.0.5" - run-parallel: "npm:^1.1.9" - checksum: 10c0/732c3b6d1b1e967440e65f284bd06e5821fedf10a1bea9ed2bb75956ea1f30e08c44d3def9d6a230666574edbaf136f8cfd319c14fd1f87c66e6a44449afb2eb - languageName: node - linkType: hard - -"@nodelib/fs.stat@npm:2.0.5": - version: 2.0.5 - resolution: "@nodelib/fs.stat@npm:2.0.5" - checksum: 10c0/88dafe5e3e29a388b07264680dc996c17f4bda48d163a9d4f5c1112979f0ce8ec72aa7116122c350b4e7976bc5566dc3ddb579be1ceaacc727872eb4ed93926d - languageName: node - linkType: hard - -"@nodelib/fs.walk@npm:^1.2.8": - version: 1.2.8 - resolution: "@nodelib/fs.walk@npm:1.2.8" - dependencies: - "@nodelib/fs.scandir": "npm:2.1.5" - fastq: "npm:^1.6.0" - checksum: 10c0/db9de047c3bb9b51f9335a7bb46f4fcfb6829fb628318c12115fbaf7d369bfce71c15b103d1fc3b464812d936220ee9bc1c8f762d032c9f6be9acc99249095b1 - languageName: node - linkType: hard - "@npmcli/agent@npm:^3.0.0": version: 3.0.0 resolution: "@npmcli/agent@npm:3.0.0" @@ -2410,17 +2435,7 @@ __metadata: languageName: node linkType: hard -"@types/eslint@npm:^8.56.10": - version: 8.56.12 - resolution: "@types/eslint@npm:8.56.12" - dependencies: - "@types/estree": "npm:*" - "@types/json-schema": "npm:*" - checksum: 10c0/e4ca426abe9d55f82b69a3250bec78b6d340ad1e567f91c97ecc59d3b2d6a1d8494955ac62ad0ea14b97519db580611c02be8277cbea370bdfb0f96aa2910504 - languageName: node - linkType: hard - -"@types/estree@npm:*": +"@types/estree@npm:^1.0.6": version: 1.0.8 resolution: "@types/estree@npm:1.0.8" checksum: 10c0/39d34d1afaa338ab9763f37ad6066e3f349444f9052b9676a7cc0252ef9485a41c6d81c9c4e0d26e9077993354edf25efc853f3224dd4b447175ef62bdcc86a5 @@ -2471,7 +2486,7 @@ __metadata: languageName: node linkType: hard -"@types/json-schema@npm:*": +"@types/json-schema@npm:^7.0.15": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" checksum: 10c0/a996a745e6c5d60292f36731dd41341339d4eeed8180bb09226e5c8d23759067692b1d88e5d91d72ee83dfc00d3aca8e7bd43ea120516c17922cbcb7c3e252db @@ -2528,7 +2543,142 @@ __metadata: languageName: node linkType: hard -"@ungap/structured-clone@npm:^1.2.0, @ungap/structured-clone@npm:^1.3.0": +"@typescript-eslint/eslint-plugin@npm:8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/eslint-plugin@npm:8.54.0" + dependencies: + "@eslint-community/regexpp": "npm:^4.12.2" + "@typescript-eslint/scope-manager": "npm:8.54.0" + "@typescript-eslint/type-utils": "npm:8.54.0" + "@typescript-eslint/utils": "npm:8.54.0" + "@typescript-eslint/visitor-keys": "npm:8.54.0" + ignore: "npm:^7.0.5" + natural-compare: "npm:^1.4.0" + ts-api-utils: "npm:^2.4.0" + peerDependencies: + "@typescript-eslint/parser": ^8.54.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/e533c8285880b883e02a833f378597c2776e6b0c20a5935440e2a02c1c42f40069a8badcf6d581bb4ec35a6856a806c4b66674c1c15c33cd64cc6b9c0cdd1dad + languageName: node + linkType: hard + +"@typescript-eslint/parser@npm:8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/parser@npm:8.54.0" + dependencies: + "@typescript-eslint/scope-manager": "npm:8.54.0" + "@typescript-eslint/types": "npm:8.54.0" + "@typescript-eslint/typescript-estree": "npm:8.54.0" + "@typescript-eslint/visitor-keys": "npm:8.54.0" + debug: "npm:^4.4.3" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/60a1cfe94bc23086f03701640f4d83d7e37b8f4d729011e0f029e5accf2b3d099c50938c0a798a399e86046279432ff663f33102ba4338c4c82f7acead2bcbac + languageName: node + linkType: hard + +"@typescript-eslint/project-service@npm:8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/project-service@npm:8.54.0" + dependencies: + "@typescript-eslint/tsconfig-utils": "npm:^8.54.0" + "@typescript-eslint/types": "npm:^8.54.0" + debug: "npm:^4.4.3" + peerDependencies: + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/3392ae259199021a80616a44d9484d1c363f61bc5c631dff2d08c6a906c98716a20caa7b832b8970120a1eb1eb2de3ee890cd527d6edb04f532f4e48a690a792 + languageName: node + linkType: hard + +"@typescript-eslint/scope-manager@npm:8.54.0, @typescript-eslint/scope-manager@npm:^8.51.0": + version: 8.54.0 + resolution: "@typescript-eslint/scope-manager@npm:8.54.0" + dependencies: + "@typescript-eslint/types": "npm:8.54.0" + "@typescript-eslint/visitor-keys": "npm:8.54.0" + checksum: 10c0/794740a5c0c1afc38d71e6bc59cc62870286e40d99f15e9760e76fb3d4197e961ee151c286c428535c404f5137721242a14da21350b749d0feb1f589f167814f + languageName: node + linkType: hard + +"@typescript-eslint/tsconfig-utils@npm:8.54.0, @typescript-eslint/tsconfig-utils@npm:^8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.54.0" + peerDependencies: + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/e8598b0f051650c085d749002138d12249a3efd03e7de02e9e7913939dddd649d159b91f29ca3d28f5ee798b3f528a7195688e23c5e0b315d534e7af20a0c99a + languageName: node + linkType: hard + +"@typescript-eslint/type-utils@npm:8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/type-utils@npm:8.54.0" + dependencies: + "@typescript-eslint/types": "npm:8.54.0" + "@typescript-eslint/typescript-estree": "npm:8.54.0" + "@typescript-eslint/utils": "npm:8.54.0" + debug: "npm:^4.4.3" + ts-api-utils: "npm:^2.4.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/ad807800d8b2662f823505249a84a6f5b1246b192a7ff08c49f298e220e4d9bb3d76f1f0852510421e030161604a4b939bff87f11b9074f118a3bd1d26139c6f + languageName: node + linkType: hard + +"@typescript-eslint/types@npm:8.54.0, @typescript-eslint/types@npm:^8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/types@npm:8.54.0" + checksum: 10c0/2219594fe5e8931ff91fd1b7a2606d33cd4f093d43f9ca71bcaa37f106ef79ad51f830dea51392f7e3d8bca77f7077ef98733f87bc008fad2f0bbd9ea5fb8a40 + languageName: node + linkType: hard + +"@typescript-eslint/typescript-estree@npm:8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.54.0" + dependencies: + "@typescript-eslint/project-service": "npm:8.54.0" + "@typescript-eslint/tsconfig-utils": "npm:8.54.0" + "@typescript-eslint/types": "npm:8.54.0" + "@typescript-eslint/visitor-keys": "npm:8.54.0" + debug: "npm:^4.4.3" + minimatch: "npm:^9.0.5" + semver: "npm:^7.7.3" + tinyglobby: "npm:^0.2.15" + ts-api-utils: "npm:^2.4.0" + peerDependencies: + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/1a1a7c0a318e71f3547ab5573198d36165ea152c50447ef92e6326303f9a5c397606201ba80c7b86a725dcdd2913e924be94466a0c33b1b0c3ee852059e646b6 + languageName: node + linkType: hard + +"@typescript-eslint/utils@npm:8.54.0, @typescript-eslint/utils@npm:^8.51.0": + version: 8.54.0 + resolution: "@typescript-eslint/utils@npm:8.54.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.9.1" + "@typescript-eslint/scope-manager": "npm:8.54.0" + "@typescript-eslint/types": "npm:8.54.0" + "@typescript-eslint/typescript-estree": "npm:8.54.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/949a97dca8024d39666e04ecdf2d4e12722f5064c387901e72bdcc7adafb96cf650a070dc79f9dd46fa1aae6ac2b5eac5ae3fe5a6979385208c28809a1bd143f + languageName: node + linkType: hard + +"@typescript-eslint/visitor-keys@npm:8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.54.0" + dependencies: + "@typescript-eslint/types": "npm:8.54.0" + eslint-visitor-keys: "npm:^4.2.1" + checksum: 10c0/f83a9aa92f7f4d1fdb12cbca28c6f5704c36371264606b456388b2c869fc61e73c86d3736556e1bb6e253f3a607128b5b1bf6c68395800ca06f18705576faadd + languageName: node + linkType: hard + +"@ungap/structured-clone@npm:^1.3.0": version: 1.3.0 resolution: "@ungap/structured-clone@npm:1.3.0" checksum: 10c0/0fc3097c2540ada1fc340ee56d58d96b5b536a2a0dab6e3ec17d4bfc8c4c86db345f61a375a8185f9da96f01c69678f836a2b57eeaa9e4b8eeafd26428e57b0a @@ -2599,7 +2749,7 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.14.0, acorn@npm:^8.9.0": +"acorn@npm:^8.14.0, acorn@npm:^8.15.0": version: 8.15.0 resolution: "acorn@npm:8.15.0" bin: @@ -3426,7 +3576,7 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.6": +"cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.6": version: 7.0.6 resolution: "cross-spawn@npm:7.0.6" dependencies: @@ -3490,6 +3640,18 @@ __metadata: languageName: node linkType: hard +"debug@npm:^4.4.3": + version: 4.4.3 + resolution: "debug@npm:4.4.3" + dependencies: + ms: "npm:^2.1.3" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10c0/d79136ec6c83ecbefd0f6a5593da6a9c91ec4d7ddc4b54c883d6e71ec9accb5f67a1a5e96d00a328196b5b5c86d365e98d8a3a70856aaf16b4e7b1985e67f5a6 + languageName: node + linkType: hard + "dedent@npm:^1.0.0": version: 1.6.0 resolution: "dedent@npm:1.6.0" @@ -3574,15 +3736,6 @@ __metadata: languageName: node linkType: hard -"doctrine@npm:^3.0.0": - version: 3.0.0 - resolution: "doctrine@npm:3.0.0" - dependencies: - esutils: "npm:^2.0.2" - checksum: 10c0/c96bdccabe9d62ab6fea9399fdff04a66e6563c1d6fb3a3a063e8d53c3bb136ba63e84250bbf63d00086a769ad53aef92d2bd483f03f837fc97b71cbee6b2520 - languageName: node - linkType: hard - "dotenv-expand@npm:~11.0.6": version: 11.0.7 resolution: "dotenv-expand@npm:11.0.7" @@ -3752,79 +3905,99 @@ __metadata: languageName: node linkType: hard -"eslint-scope@npm:^7.2.2": - version: 7.2.2 - resolution: "eslint-scope@npm:7.2.2" +"eslint-plugin-testing-library@npm:^7.1.1": + version: 7.15.4 + resolution: "eslint-plugin-testing-library@npm:7.15.4" + dependencies: + "@typescript-eslint/scope-manager": "npm:^8.51.0" + "@typescript-eslint/utils": "npm:^8.51.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + checksum: 10c0/5c35dd1c0dbb6bbbae5f379c4508d0efd377fd99ba75ee63607650a7c695ae88240984d547329a18a5d0ba3126d51c1cdf4b904856242214dcfac3f13835f051 + languageName: node + linkType: hard + +"eslint-scope@npm:^8.4.0": + version: 8.4.0 + resolution: "eslint-scope@npm:8.4.0" dependencies: esrecurse: "npm:^4.3.0" estraverse: "npm:^5.2.0" - checksum: 10c0/613c267aea34b5a6d6c00514e8545ef1f1433108097e857225fed40d397dd6b1809dffd11c2fde23b37ca53d7bf935fe04d2a18e6fc932b31837b6ad67e1c116 + checksum: 10c0/407f6c600204d0f3705bd557f81bd0189e69cd7996f408f8971ab5779c0af733d1af2f1412066b40ee1588b085874fc37a2333986c6521669cdbdd36ca5058e0 languageName: node linkType: hard -"eslint-visitor-keys@npm:^3.4.1, eslint-visitor-keys@npm:^3.4.3": +"eslint-visitor-keys@npm:^3.4.3": version: 3.4.3 resolution: "eslint-visitor-keys@npm:3.4.3" checksum: 10c0/92708e882c0a5ffd88c23c0b404ac1628cf20104a108c745f240a13c332a11aac54f49a22d5762efbffc18ecbc9a580d1b7ad034bf5f3cc3307e5cbff2ec9820 languageName: node linkType: hard -"eslint@npm:^8.57.0": - version: 8.57.1 - resolution: "eslint@npm:8.57.1" +"eslint-visitor-keys@npm:^4.2.1": + version: 4.2.1 + resolution: "eslint-visitor-keys@npm:4.2.1" + checksum: 10c0/fcd43999199d6740db26c58dbe0c2594623e31ca307e616ac05153c9272f12f1364f5a0b1917a8e962268fdecc6f3622c1c2908b4fcc2e047a106fe6de69dc43 + languageName: node + linkType: hard + +"eslint@npm:^9.0.0": + version: 9.39.2 + resolution: "eslint@npm:9.39.2" dependencies: - "@eslint-community/eslint-utils": "npm:^4.2.0" - "@eslint-community/regexpp": "npm:^4.6.1" - "@eslint/eslintrc": "npm:^2.1.4" - "@eslint/js": "npm:8.57.1" - "@humanwhocodes/config-array": "npm:^0.13.0" + "@eslint-community/eslint-utils": "npm:^4.8.0" + "@eslint-community/regexpp": "npm:^4.12.1" + "@eslint/config-array": "npm:^0.21.1" + "@eslint/config-helpers": "npm:^0.4.2" + "@eslint/core": "npm:^0.17.0" + "@eslint/eslintrc": "npm:^3.3.1" + "@eslint/js": "npm:9.39.2" + "@eslint/plugin-kit": "npm:^0.4.1" + "@humanfs/node": "npm:^0.16.6" "@humanwhocodes/module-importer": "npm:^1.0.1" - "@nodelib/fs.walk": "npm:^1.2.8" - "@ungap/structured-clone": "npm:^1.2.0" + "@humanwhocodes/retry": "npm:^0.4.2" + "@types/estree": "npm:^1.0.6" ajv: "npm:^6.12.4" chalk: "npm:^4.0.0" - cross-spawn: "npm:^7.0.2" + cross-spawn: "npm:^7.0.6" debug: "npm:^4.3.2" - doctrine: "npm:^3.0.0" escape-string-regexp: "npm:^4.0.0" - eslint-scope: "npm:^7.2.2" - eslint-visitor-keys: "npm:^3.4.3" - espree: "npm:^9.6.1" - esquery: "npm:^1.4.2" + eslint-scope: "npm:^8.4.0" + eslint-visitor-keys: "npm:^4.2.1" + espree: "npm:^10.4.0" + esquery: "npm:^1.5.0" esutils: "npm:^2.0.2" fast-deep-equal: "npm:^3.1.3" - file-entry-cache: "npm:^6.0.1" + file-entry-cache: "npm:^8.0.0" find-up: "npm:^5.0.0" glob-parent: "npm:^6.0.2" - globals: "npm:^13.19.0" - graphemer: "npm:^1.4.0" ignore: "npm:^5.2.0" imurmurhash: "npm:^0.1.4" is-glob: "npm:^4.0.0" - is-path-inside: "npm:^3.0.3" - js-yaml: "npm:^4.1.0" json-stable-stringify-without-jsonify: "npm:^1.0.1" - levn: "npm:^0.4.1" lodash.merge: "npm:^4.6.2" minimatch: "npm:^3.1.2" natural-compare: "npm:^1.4.0" optionator: "npm:^0.9.3" - strip-ansi: "npm:^6.0.1" - text-table: "npm:^0.2.0" + peerDependencies: + jiti: "*" + peerDependenciesMeta: + jiti: + optional: true bin: eslint: bin/eslint.js - checksum: 10c0/1fd31533086c1b72f86770a4d9d7058ee8b4643fd1cfd10c7aac1ecb8725698e88352a87805cf4b2ce890aa35947df4b4da9655fb7fdfa60dbb448a43f6ebcf1 + checksum: 10c0/bb88ca8fd16bb7e1ac3e13804c54d41c583214460c0faa7b3e7c574e69c5600c7122295500fb4b0c06067831111db740931e98da1340329527658e1cf80073d3 languageName: node linkType: hard -"espree@npm:^9.6.0, espree@npm:^9.6.1": - version: 9.6.1 - resolution: "espree@npm:9.6.1" +"espree@npm:^10.0.1, espree@npm:^10.4.0": + version: 10.4.0 + resolution: "espree@npm:10.4.0" dependencies: - acorn: "npm:^8.9.0" + acorn: "npm:^8.15.0" acorn-jsx: "npm:^5.3.2" - eslint-visitor-keys: "npm:^3.4.1" - checksum: 10c0/1a2e9b4699b715347f62330bcc76aee224390c28bb02b31a3752e9d07549c473f5f986720483c6469cf3cfb3c9d05df612ffc69eb1ee94b54b739e67de9bb460 + eslint-visitor-keys: "npm:^4.2.1" + checksum: 10c0/c63fe06131c26c8157b4083313cb02a9a54720a08e21543300e55288c40e06c3fc284bdecf108d3a1372c5934a0a88644c98714f38b6ae8ed272b40d9ea08d6b languageName: node linkType: hard @@ -3838,12 +4011,12 @@ __metadata: languageName: node linkType: hard -"esquery@npm:^1.4.2": - version: 1.6.0 - resolution: "esquery@npm:1.6.0" +"esquery@npm:^1.5.0": + version: 1.7.0 + resolution: "esquery@npm:1.7.0" dependencies: estraverse: "npm:^5.1.0" - checksum: 10c0/cb9065ec605f9da7a76ca6dadb0619dfb611e37a81e318732977d90fab50a256b95fee2d925fba7c2f3f0523aa16f91587246693bc09bc34d5a59575fe6e93d2 + checksum: 10c0/77d5173db450b66f3bc685d11af4c90cffeedb340f34a39af96d43509a335ce39c894fd79233df32d38f5e4e219fa0f7076f6ec90bae8320170ba082c0db4793 languageName: node linkType: hard @@ -4108,15 +4281,6 @@ __metadata: languageName: node linkType: hard -"fastq@npm:^1.6.0": - version: 1.19.1 - resolution: "fastq@npm:1.19.1" - dependencies: - reusify: "npm:^1.0.4" - checksum: 10c0/ebc6e50ac7048daaeb8e64522a1ea7a26e92b3cee5cd1c7f2316cdca81ba543aa40a136b53891446ea5c3a67ec215fbaca87ad405f102dd97012f62916905630 - languageName: node - linkType: hard - "fb-watchman@npm:^2.0.0": version: 2.0.2 resolution: "fb-watchman@npm:2.0.2" @@ -4172,12 +4336,12 @@ __metadata: languageName: node linkType: hard -"file-entry-cache@npm:^6.0.1": - version: 6.0.1 - resolution: "file-entry-cache@npm:6.0.1" +"file-entry-cache@npm:^8.0.0": + version: 8.0.0 + resolution: "file-entry-cache@npm:8.0.0" dependencies: - flat-cache: "npm:^3.0.4" - checksum: 10c0/58473e8a82794d01b38e5e435f6feaf648e3f36fdb3a56e98f417f4efae71ad1c0d4ebd8a9a7c50c3ad085820a93fc7494ad721e0e4ebc1da3573f4e1c3c7cdd + flat-cache: "npm:^4.0.0" + checksum: 10c0/9e2b5938b1cd9b6d7e3612bdc533afd4ac17b2fc646569e9a8abbf2eb48e5eb8e316bc38815a3ef6a1b456f4107f0d0f055a614ca613e75db6bf9ff4d72c1638 languageName: node linkType: hard @@ -4225,14 +4389,13 @@ __metadata: languageName: node linkType: hard -"flat-cache@npm:^3.0.4": - version: 3.2.0 - resolution: "flat-cache@npm:3.2.0" +"flat-cache@npm:^4.0.0": + version: 4.0.1 + resolution: "flat-cache@npm:4.0.1" dependencies: flatted: "npm:^3.2.9" - keyv: "npm:^4.5.3" - rimraf: "npm:^3.0.2" - checksum: 10c0/b76f611bd5f5d68f7ae632e3ae503e678d205cf97a17c6ab5b12f6ca61188b5f1f7464503efae6dc18683ed8f0b41460beb48ac4b9ac63fe6201296a91ba2f75 + keyv: "npm:^4.5.4" + checksum: 10c0/2c59d93e9faa2523e4fda6b4ada749bed432cfa28c8e251f33b25795e426a1c6dbada777afb1f74fcfff33934fdbdea921ee738fcc33e71adc9d6eca984a1cfc languageName: node linkType: hard @@ -4417,12 +4580,10 @@ __metadata: languageName: node linkType: hard -"globals@npm:^13.19.0": - version: 13.24.0 - resolution: "globals@npm:13.24.0" - dependencies: - type-fest: "npm:^0.20.2" - checksum: 10c0/d3c11aeea898eb83d5ec7a99508600fbe8f83d2cf00cbb77f873dbf2bcb39428eff1b538e4915c993d8a3b3473fa71eeebfe22c9bb3a3003d1e26b1f2c8a42cd +"globals@npm:^14.0.0": + version: 14.0.0 + resolution: "globals@npm:14.0.0" + checksum: 10c0/b96ff42620c9231ad468d4c58ff42afee7777ee1c963013ff8aabe095a451d0ceeb8dcd8ef4cbd64d2538cef45f787a78ba3a9574f4a634438963e334471302d languageName: node linkType: hard @@ -4433,13 +4594,6 @@ __metadata: languageName: node linkType: hard -"graphemer@npm:^1.4.0": - version: 1.4.0 - resolution: "graphemer@npm:1.4.0" - checksum: 10c0/e951259d8cd2e0d196c72ec711add7115d42eb9a8146c8eeda5b8d3ac91e5dd816b9cd68920726d9fd4490368e7ed86e9c423f40db87e2d8dfafa00fa17c3a31 - languageName: node - linkType: hard - "has-flag@npm:^3.0.0": version: 3.0.0 resolution: "has-flag@npm:3.0.0" @@ -4588,6 +4742,13 @@ __metadata: languageName: node linkType: hard +"ignore@npm:^7.0.5": + version: 7.0.5 + resolution: "ignore@npm:7.0.5" + checksum: 10c0/ae00db89fe873064a093b8999fe4cc284b13ef2a178636211842cceb650b9c3e390d3339191acb145d81ed5379d2074840cf0c33a20bdbd6f32821f79eb4ad5d + languageName: node + linkType: hard + "image-size@npm:^1.0.2": version: 1.2.1 resolution: "image-size@npm:1.2.1" @@ -4749,13 +4910,6 @@ __metadata: languageName: node linkType: hard -"is-path-inside@npm:^3.0.3": - version: 3.0.3 - resolution: "is-path-inside@npm:3.0.3" - checksum: 10c0/cf7d4ac35fb96bab6a1d2c3598fe5ebb29aafb52c0aaa482b5a3ed9d8ba3edc11631e3ec2637660c44b3ce0e61a08d54946e8af30dec0b60a7c27296c68ffd05 - languageName: node - linkType: hard - "is-stream@npm:^2.0.0": version: 2.0.1 resolution: "is-stream@npm:2.0.1" @@ -5364,6 +5518,17 @@ __metadata: languageName: node linkType: hard +"js-yaml@npm:^4.1.1": + version: 4.1.1 + resolution: "js-yaml@npm:4.1.1" + dependencies: + argparse: "npm:^2.0.1" + bin: + js-yaml: bin/js-yaml.js + checksum: 10c0/561c7d7088c40a9bb53cc75becbfb1df6ae49b34b5e6e5a81744b14ae8667ec564ad2527709d1a6e7d5e5fa6d483aa0f373a50ad98d42fde368ec4a190d4fae7 + languageName: node + linkType: hard + "jsbn@npm:1.1.0": version: 1.1.0 resolution: "jsbn@npm:1.1.0" @@ -5433,7 +5598,7 @@ __metadata: languageName: node linkType: hard -"keyv@npm:^4.5.3": +"keyv@npm:^4.5.4": version: 4.5.4 resolution: "keyv@npm:4.5.4" dependencies: @@ -6056,7 +6221,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": +"minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" dependencies: @@ -6065,7 +6230,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^9.0.0, minimatch@npm:^9.0.4": +"minimatch@npm:^9.0.0, minimatch@npm:^9.0.4, minimatch@npm:^9.0.5": version: 9.0.5 resolution: "minimatch@npm:9.0.5" dependencies: @@ -6825,13 +6990,6 @@ __metadata: languageName: node linkType: hard -"queue-microtask@npm:^1.2.2": - version: 1.2.3 - resolution: "queue-microtask@npm:1.2.3" - checksum: 10c0/900a93d3cdae3acd7d16f642c29a642aea32c2026446151f0778c62ac089d4b8e6c986811076e1ae180a694cedf077d453a11b58ff0a865629a4f82ab558e102 - languageName: node - linkType: hard - "queue@npm:6.0.2": version: 6.0.2 resolution: "queue@npm:6.0.2" @@ -7201,13 +7359,6 @@ __metadata: languageName: node linkType: hard -"reusify@npm:^1.0.4": - version: 1.1.0 - resolution: "reusify@npm:1.1.0" - checksum: 10c0/4eff0d4a5f9383566c7d7ec437b671cc51b25963bd61bf127c3f3d3f68e44a026d99b8d2f1ad344afff8d278a8fe70a8ea092650a716d22287e8bef7126bb2fa - languageName: node - linkType: hard - "rimraf@npm:^3.0.2": version: 3.0.2 resolution: "rimraf@npm:3.0.2" @@ -7225,10 +7376,10 @@ __metadata: dependencies: "@babel/core": "npm:^7.24.0" "@testing-library/react-native": "npm:^14.0.0-beta.0" - "@types/eslint": "npm:^8.56.10" "@types/jest": "npm:^29.5.12" "@types/react": "npm:~19.1.10" - eslint: "npm:^8.57.0" + eslint: "npm:^9.0.0" + eslint-plugin-testing-library: "npm:^7.1.1" expo: "npm:^54.0.32" expo-status-bar: "npm:~3.0.9" jest: "npm:^29.7.0" @@ -7239,18 +7390,10 @@ __metadata: react-native-web: "npm:^0.21.0" test-renderer: "npm:0.14.0" typescript: "npm:~5.9.2" + typescript-eslint: "npm:^8.0.0" languageName: unknown linkType: soft -"run-parallel@npm:^1.1.9": - version: 1.2.0 - resolution: "run-parallel@npm:1.2.0" - dependencies: - queue-microtask: "npm:^1.2.2" - checksum: 10c0/200b5ab25b5b8b7113f9901bfe3afc347e19bb7475b267d55ad0eb86a62a46d77510cb0f232507c9e5d497ebda569a08a9867d0d14f57a82ad5564d991588b39 - languageName: node - linkType: hard - "safe-buffer@npm:5.2.1": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" @@ -7304,6 +7447,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.7.3": + version: 7.7.4 + resolution: "semver@npm:7.7.4" + bin: + semver: bin/semver.js + checksum: 10c0/5215ad0234e2845d4ea5bb9d836d42b03499546ddafb12075566899fc617f68794bb6f146076b6881d755de17d6c6cc73372555879ec7dce2c2feee947866ad2 + languageName: node + linkType: hard + "send@npm:0.19.0": version: 0.19.0 resolution: "send@npm:0.19.0" @@ -7839,13 +7991,6 @@ __metadata: languageName: node linkType: hard -"text-table@npm:^0.2.0": - version: 0.2.0 - resolution: "text-table@npm:0.2.0" - checksum: 10c0/02805740c12851ea5982686810702e2f14369a5f4c5c40a836821e3eefc65ffeec3131ba324692a37608294b0fd8c1e55a2dd571ffed4909822787668ddbee5c - languageName: node - linkType: hard - "thenify-all@npm:^1.0.0": version: 1.6.0 resolution: "thenify-all@npm:1.6.0" @@ -7871,7 +8016,7 @@ __metadata: languageName: node linkType: hard -"tinyglobby@npm:^0.2.11": +"tinyglobby@npm:^0.2.11, tinyglobby@npm:^0.2.15": version: 0.2.15 resolution: "tinyglobby@npm:0.2.15" dependencies: @@ -7921,6 +8066,15 @@ __metadata: languageName: node linkType: hard +"ts-api-utils@npm:^2.4.0": + version: 2.4.0 + resolution: "ts-api-utils@npm:2.4.0" + peerDependencies: + typescript: ">=4.8.4" + checksum: 10c0/ed185861aef4e7124366a3f6561113557a57504267d4d452a51e0ba516a9b6e713b56b4aeaab9fa13de9db9ab755c65c8c13a777dba9133c214632cb7b65c083 + languageName: node + linkType: hard + "ts-interface-checker@npm:^0.1.9": version: 0.1.13 resolution: "ts-interface-checker@npm:0.1.13" @@ -7944,13 +8098,6 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^0.20.2": - version: 0.20.2 - resolution: "type-fest@npm:0.20.2" - checksum: 10c0/dea9df45ea1f0aaa4e2d3bed3f9a0bfe9e5b2592bddb92eb1bf06e50bcf98dbb78189668cd8bc31a0511d3fc25539b4cd5c704497e53e93e2d40ca764b10bfc3 - languageName: node - linkType: hard - "type-fest@npm:^0.21.3": version: 0.21.3 resolution: "type-fest@npm:0.21.3" @@ -7965,6 +8112,21 @@ __metadata: languageName: node linkType: hard +"typescript-eslint@npm:^8.0.0": + version: 8.54.0 + resolution: "typescript-eslint@npm:8.54.0" + dependencies: + "@typescript-eslint/eslint-plugin": "npm:8.54.0" + "@typescript-eslint/parser": "npm:8.54.0" + "@typescript-eslint/typescript-estree": "npm:8.54.0" + "@typescript-eslint/utils": "npm:8.54.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/0ba92aa22c0aa10c88b0f4732950ed64245947f1c4ac17328dff94b43eaeddd3068595788725781fba07a87cc964304a075b3e37f9a86312173498fcc6ab4338 + languageName: node + linkType: hard + "typescript@npm:~5.9.2": version: 5.9.3 resolution: "typescript@npm:5.9.3" diff --git a/website/docs/13.x/docs/guides/common-mistakes.mdx b/website/docs/13.x/docs/guides/common-mistakes.mdx new file mode 100644 index 000000000..984ce0635 --- /dev/null +++ b/website/docs/13.x/docs/guides/common-mistakes.mdx @@ -0,0 +1,587 @@ +# Common Mistakes with React Native Testing Library + +> **Note:** This guide is adapted from Kent C. Dodds' article ["Common mistakes with React Testing Library"](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library) for React Native Testing Library v13. The original article focuses on web React, but the principles apply to React Native as well. This adaptation includes React Native-specific examples and ARIA-compatible accessibility attributes. + +React Native Testing Library guiding principle is: + +> "The more your tests resemble the way your software is used, the more confidence they can give you." + +This guide outlines some common mistakes people make when using React Native Testing Library and how to avoid them. + +## Using the wrong query {#using-the-wrong-query} + +Importance: high + +React Native Testing Library provides several query types. Here's the priority order: + +1. **Queries that reflect user experience:** + - `getByRole` - most accessible + - `getByLabelText` - accessible label + - `getByPlaceholderText` - `TextInput` placeholder text + - `getByText` - text content + - `getByDisplayValue` - `TextInput` input value + +2. **Semantic queries:** + - `getByTestId` - only if nothing else works + +Here's an example of using the right query: + +```tsx +import { TextInput, View } from 'react-native'; +import { render, screen } from '@testing-library/react-native'; + +test('finds input by label', () => { + render( + + + + ); + + // ✅ Good - uses accessible label + const input = screen.getByLabelText('Username'); + + // ✅ Also good - uses placeholder + const inputByPlaceholder = screen.getByPlaceholderText('Enter username'); + + // ❌ Bad - uses testID when accessible queries work + // const input = screen.getByTestId('username-input'); +}); +``` + +## Not using `*ByRole` query most of the time {#not-using-byrole-most-of-the-time} + +Importance: high + +`getByRole` is the most accessible query and should be your first choice. It queries elements by their semantic role: + +```tsx +import { Pressable, Text, TextInput, View } from 'react-native'; +import { render, screen } from '@testing-library/react-native'; + +test('uses role queries', () => { + render( + + + Submit + + + + ); + + // ✅ Good - uses role query + const button = screen.getByRole('button', { name: 'Submit' }); + const searchbox = screen.getByRole('searchbox', { name: 'Search' }); + + expect(button).toBeOnTheScreen(); + expect(searchbox).toBeOnTheScreen(); +}); +``` + +Common roles in React Native include: + +- `button` - pressable elements +- `text` - static text +- `header` / `heading` - headers +- `searchbox` - search inputs +- `switch` - toggle switches +- `checkbox` - checkboxes +- `radio` - radio buttons +- And more... + +Note: React Native supports both ARIA-compatible (`role`) and legacy (`accessibilityRole`) props. Prefer `role` for consistency with web standards. + +## Using the wrong assertion {#using-the-wrong-assertion} + +Importance: high + +React Native Testing Library provides built-in Jest matchers. Make sure you're using the right ones: + +```tsx +import { Pressable, Text } from 'react-native'; +import { render, screen } from '@testing-library/react-native'; + +test('button is disabled', () => { + render( + + Submit + + ); + + const button = screen.getByRole('button', { name: 'Submit' }); + + // ✅ Good - uses RNTL matcher + expect(button).toBeDisabled(); + + // ❌ Bad - doesn't use RNTL matcher + expect(button.props['aria-disabled']).toBe(true); +}); +``` + +Common matchers include: + +- `toBeOnTheScreen()` - checks if element is rendered (replaces `toBeInTheDocument()`) +- `toBeDisabled()` - checks if element is disabled +- `toHaveTextContent()` - checks text content +- `toHaveAccessibleName()` - checks accessible name +- And more... + +## Using `query*` variants for anything except checking for non-existence {#using-query-variants-for-anything-except-checking-for-non-existence} + +Importance: high + +Use `queryBy*` only when checking that an element doesn't exist: + +```tsx +import { View, Text } from 'react-native'; +import { render, screen } from '@testing-library/react-native'; + +test('checks non-existence', () => { + render( + + Hello + + ); + + // ✅ Good - uses queryBy for non-existence check + expect(screen.queryByText('Goodbye')).not.toBeOnTheScreen(); + + // ❌ Bad - uses queryBy when element should exist + // const element = screen.queryByText('Hello'); + // expect(element).toBeOnTheScreen(); + + // ✅ Good - uses getBy when element should exist + expect(screen.getByText('Hello')).toBeOnTheScreen(); +}); +``` + +## Using `waitFor` to wait for elements that can be queried with `find*` {#using-waitfor-to-wait-for-elements-that-can-be-queried-with-find} + +Importance: high + +Use `findBy*` queries instead of `waitFor` + `getBy*`: + +```tsx +import { View, Text } from 'react-native'; +import { render, screen, waitFor } from '@testing-library/react-native'; + +test('waits for element', async () => { + const Component = () => { + const [show, setShow] = React.useState(false); + + React.useEffect(() => { + setTimeout(() => setShow(true), 100); + }, []); + + return {show && Loaded}; + }; + + render(); + + // ✅ Good - uses findBy query + const element = await screen.findByText('Loaded'); + expect(element).toBeOnTheScreen(); + + // ❌ Bad - uses waitFor + getBy + // await waitFor(() => { + // expect(screen.getByText('Loaded')).toBeOnTheScreen(); + // }); +}); +``` + +## Performing side-effects in `waitFor` {#performing-side-effects-in-waitfor} + +Importance: high + +Don't perform side-effects in `waitFor` callbacks: + +```tsx +import { Pressable, Text, View } from 'react-native'; +import { render, screen, waitFor, fireEvent } from '@testing-library/react-native'; + +test('avoids side effects in waitFor', async () => { + const Component = () => { + const [count, setCount] = React.useState(0); + return ( + + setCount(count + 1)}> + Increment + + Count: {count} + + ); + }; + + render(); + + const button = screen.getByRole('button'); + + // ❌ Bad - side effect in waitFor + // await waitFor(() => { + // fireEvent.press(button); + // expect(screen.getByText('Count: 1')).toBeOnTheScreen(); + // }); + + // ✅ Good - side effect outside waitFor + fireEvent.press(button); + await waitFor(() => { + expect(screen.getByText('Count: 1')).toBeOnTheScreen(); + }); +}); +``` + +## Using `UNSAFE_root` to query for elements {#using-unsafe-root-to-query-for-elements} + +Importance: high + +React Native Testing Library provides an `UNSAFE_root` object that gives access to the root element, but you should avoid using it directly: + +```tsx +import { View, Text } from 'react-native'; +import { render } from '@testing-library/react-native'; + +test('finds element incorrectly', () => { + const { UNSAFE_root } = render( + + Hello + + ); + + // ❌ Bad - using UNSAFE_root directly + const element = UNSAFE_root.findAll((node) => node.props.testID === 'message')[0]; + + // ✅ Good - use proper queries + // const element = screen.getByTestId('message'); +}); +``` + +Instead, use the proper query methods from `screen` or the `render` result. The `UNSAFE_root` is a low-level API that you rarely need. + +## Passing an empty callback to `waitFor` {#passing-an-empty-callback-to-waitfor} + +Importance: high + +Don't pass an empty callback to `waitFor`: + +```tsx +import { View } from 'react-native'; +import { render, waitFor } from '@testing-library/react-native'; + +test('waits correctly', async () => { + render(); + + // ❌ Bad - empty callback + // await waitFor(() => {}); + + // ✅ Good - meaningful assertion + await waitFor(() => { + expect(screen.getByTestId('test')).toBeOnTheScreen(); + }); +}); +``` + +## Not using `screen` {#not-using-screen} + +Importance: medium + +You can get all the queries from the `render` result: + +```tsx +import { View, Text } from 'react-native'; +import { render } from '@testing-library/react-native'; + +test('renders component', () => { + const { getByText } = render( + + Hello + + ); + + expect(getByText('Hello')).toBeOnTheScreen(); +}); +``` + +But you can also get them from the `screen` object: + +```tsx +import { View, Text } from 'react-native'; +import { render, screen } from '@testing-library/react-native'; + +test('renders component', () => { + render( + + Hello + + ); + + expect(screen.getByText('Hello')).toBeOnTheScreen(); +}); +``` + +Using `screen` has several benefits: + +1. You don't need to destructure `getByText` from `render` +2. It's more consistent with the Testing Library ecosystem + +## Wrapping things in `act` unnecessarily {#wrapping-things-in-act-unnecessarily} + +Importance: medium + +React Native Testing Library's `userEvent` methods are already wrapped in `act`, so you don't need to wrap them yourself: + +```tsx +import { Pressable, Text, View } from 'react-native'; +import { render, fireEvent, screen } from '@testing-library/react-native'; + +test('updates on press', () => { + const Component = () => { + const [count, setCount] = React.useState(0); + return ( + + setCount(count + 1)}> + Count: {count} + + + ); + }; + + render(); + + const button = screen.getByRole('button'); + + // ✅ Good - fireEvent is already wrapped in act + fireEvent.press(button); + + expect(screen.getByText('Count: 1')).toBeOnTheScreen(); + + // ❌ Bad - unnecessary act wrapper + // act(() => { + // fireEvent.press(button); + // }); +}); +``` + +## Not using User Event API + +Importance: medium + +`userEvent` provides a more realistic way to simulate user interactions: + +```tsx +import { Pressable, Text, TextInput, View } from 'react-native'; +import { render, screen, userEvent } from '@testing-library/react-native'; + +test('uses userEvent', async () => { + const user = userEvent.setup(); + + const Component = () => { + const [value, setValue] = React.useState(''); + return ( + + + setValue('')}> + Clear + + + ); + }; + + render(); + + const input = screen.getByLabelText('Name'); + const button = screen.getByRole('button', { name: 'Clear' }); + + // ✅ Good - uses userEvent for realistic interactions + await user.type(input, 'John'); + expect(input).toHaveValue('John'); + + await user.press(button); + expect(input).toHaveValue(''); +}); +``` + +`userEvent` methods are async and must be awaited. Available methods include: + +- `press()` - simulates a press +- `longPress()` - simulates long press +- `type()` - simulates typing +- `clear()` - clears text input +- `paste()` - simulates pasting +- `scrollTo()` - simulates scrolling + +## Not querying by text {#not-querying-by-text} + +Importance: medium + +In React Native, text is rendered in `` components. You should query by the text content that users see: + +```tsx +import { Text, View } from 'react-native'; +import { render, screen } from '@testing-library/react-native'; + +test('finds text correctly', () => { + render( + + Hello World + + ); + + // ✅ Good - queries by visible text + expect(screen.getByText('Hello World')).toBeOnTheScreen(); + + // ❌ Bad - queries by testID when text is available + // expect(screen.getByTestId('greeting')).toBeOnTheScreen(); +}); +``` + +## Not using Testing Library ESLint plugins {#not-using-testing-library-eslint-plugins} + +Importance: medium + +There's an ESLint plugin for Testing Library: [`eslint-plugin-testing-library`](https://github.com/testing-library/eslint-plugin-testing-library). This plugin can help you avoid common mistakes and will automatically fix your code in many cases. + +You can install it with: + +```bash +yarn add --dev eslint-plugin-testing-library +``` + +And configure it in your `eslint.config.js` (flat config): + +```js +import testingLibrary from 'eslint-plugin-testing-library'; + +export default [testingLibrary.configs['flat/react']]; +``` + +Note: Unlike React Testing Library, React Native Testing Library has built-in Jest matchers, so you don't need `eslint-plugin-jest-dom`. + +## Using `cleanup` {#using-cleanup} + +Importance: medium + +React Native Testing Library automatically cleans up after each test. You don't need to call `cleanup()` manually unless you're using the `pure` export (which doesn't include automatic cleanup). + +If you want to disable automatic cleanup for a specific test, you can use: + +```tsx +import { render } from '@testing-library/react-native/pure'; + +test('does not cleanup', () => { + // This test won't cleanup automatically + render(); + // ... your test +}); +``` + +But in most cases, you don't need to worry about cleanup at all - it's handled automatically. + +## Using `get*` variants as assertions {#using-get-variants-as-assertions} + +Importance: low + +`getBy*` queries throw errors when elements aren't found, so they work as assertions. However, for better error messages, you might want to combine them with explicit matchers: + +```tsx +import { View, Text } from 'react-native'; +import { render, screen } from '@testing-library/react-native'; + +test('uses getBy as assertion', () => { + render( + + Hello + + ); + + // ✅ Good - getBy throws if not found, so it's an assertion + const element = screen.getByText('Hello'); + expect(element).toBeOnTheScreen(); + + // ✅ Also good - more explicit + expect(screen.getByText('Hello')).toBeOnTheScreen(); + + // ❌ Bad - redundant assertion + // const element = screen.getByText('Hello'); + // expect(element).not.toBeNull(); // getBy already throws if null +}); +``` + +## Having multiple assertions in a single `waitFor` callback {#having-multiple-assertions-in-a-single-waitfor-callback} + +Importance: low + +Keep `waitFor` callbacks focused on a single assertion: + +```tsx +import { View, Text } from 'react-native'; +import { render, screen, waitFor } from '@testing-library/react-native'; + +test('waits with single assertion', async () => { + const Component = () => { + const [count, setCount] = React.useState(0); + + React.useEffect(() => { + setTimeout(() => setCount(1), 100); + }, []); + + return ( + + Count: {count} + + ); + }; + + render(); + + // ✅ Good - single assertion per waitFor + await waitFor(() => { + expect(screen.getByText('Count: 1')).toBeOnTheScreen(); + }); + + // If you need multiple assertions, do them after waitFor + expect(screen.getByText('Count: 1')).toHaveTextContent('Count: 1'); + + // ❌ Bad - multiple assertions in waitFor + // await waitFor(() => { + // expect(screen.getByText('Count: 1')).toBeOnTheScreen(); + // expect(screen.getByText('Count: 1')).toHaveTextContent('Count: 1'); + // }); +}); +``` + +## Using `wrapper` as the variable name {#using-wrapper-as-the-variable-name} + +Importance: low + +This is not really a "mistake" per se, but it's a common pattern that can be improved. When you use the `wrapper` option in `render`, you might be tempted to name your wrapper component `Wrapper`: + +```tsx +import { View } from 'react-native'; +import { render, screen } from '@testing-library/react-native'; + +test('renders with wrapper', () => { + const Wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + + render(Content, { + wrapper: Wrapper, + }); + + expect(screen.getByTestId('content')).toBeOnTheScreen(); +}); +``` + +This works fine, but it's more conventional to name it something more descriptive like `ThemeProvider` or `AllTheProviders` (if you're wrapping with multiple providers). This makes it clearer what the wrapper is doing. + +## Summary + +The key principles to remember: + +1. **Use the right query** - Prefer `getByRole` as your first choice, use `findBy*` for async elements, and `queryBy*` only for checking non-existence +2. **Use proper assertions** - Use RNTL's built-in matchers (`toBeOnTheScreen()`, `toBeDisabled()`, etc.) instead of asserting on props directly +3. **Handle async operations correctly** - `userEvent` methods are async and must be awaited; `render()` and `fireEvent` are synchronous +4. **Use `waitFor` correctly** - Avoid side-effects in callbacks, use `findBy*` instead when possible, and keep callbacks focused +5. **Follow accessibility best practices** - Prefer ARIA attributes (`role`, `aria-label`) over `accessibility*` props +6. **Organize code well** - Use `screen` over destructuring, prefer `userEvent` over `fireEvent`, and don't use `cleanup()` + +By following these principles, your tests will be more maintainable, accessible, and reliable. diff --git a/website/docs/13.x/docs/guides/llm-guidelines.mdx b/website/docs/13.x/docs/guides/llm-guidelines.mdx new file mode 100644 index 000000000..f222c5263 --- /dev/null +++ b/website/docs/13.x/docs/guides/llm-guidelines.mdx @@ -0,0 +1,222 @@ +# LLM Guidelines for React Native Testing Library + +Actionable guidelines for writing tests with React Native Testing Library (RNTL) v13. + +## Core APIs + +### render + +```tsx +const result = render(, options?); // Sync in v13 +``` + +| Option | Description | +| ---------------- | ---------------------------------------------------------------- | +| `wrapper` | React component to wrap the rendered component (e.g., providers) | +| `createNodeMock` | Function to create mock refs | + +| Return | Description | +| --------------------- | -------------------------------------- | +| `rerender(component)` | Re-render with a new component | +| `unmount()` | Unmount the rendered component | +| `toJSON()` | Get JSON representation for snapshots | +| `debug(options?)` | Print the component tree to console | +| `root` | Root element of the rendered component | + +### screen + +**Prefer `screen`** over destructuring from `render()`. Provides all query methods after `render()` is called. + +```tsx +render(); +screen.getByRole('button'); // Access queries via screen +``` + +### renderHook + +```tsx +const { result, rerender, unmount } = renderHook(() => useMyHook(), options?); // Sync in v13 +``` + +| Option | Description | +| -------------- | -------------------------------------------------- | +| `initialProps` | Initial props passed to the hook | +| `wrapper` | React component to wrap the hook (e.g., providers) | + +| Return | Description | +| ------------------ | -------------------------------- | +| `result.current` | Current return value of the hook | +| `rerender(props?)` | Re-render hook with new props | +| `unmount()` | Unmount the hook | + +## Query Selection + +- **Prefer `getByRole`** as first choice for querying elements +- **Query priority**: `getByRole` → `getByLabelText` → `getByPlaceholderText` → `getByText` → `getByDisplayValue` → `getByTestId` (last resort) +- **Use `findBy*`** for elements that appear asynchronously (after API calls, timeouts, state updates) +- **Use `queryBy*` ONLY** for checking non-existence (with `.not.toBeOnTheScreen()`) +- **Never use `getBy*`** for non-existence checks +- **Avoid `container.queryAll()`** - use `screen` queries instead +- **Query by visible text**, not `testID` when text is available + +## Assertions + +- **Use RNTL matchers** - prefer semantic matchers over prop assertions +- **Combine queries with matchers**: `expect(screen.getByText('Hello')).toBeOnTheScreen()` +- **No redundant null checks** - `getBy*` already throws if not found + +## Jest Matchers Reference + +| Matcher | Description | +| --------------------------------- | ------------------------------------------------------------------------------------------- | +| `toBeOnTheScreen()` | Element is present in the element tree | +| `toBeVisible()` | Element is visible (checks style, `aria-hidden`, `accessibilityElementsHidden`, ancestors) | +| `toBeEmptyElement()` | Element has no children or text content | +| `toContainElement(element)` | Element contains another element | +| `toBeEnabled()` | Element is not disabled (checks `aria-disabled`, `accessibilityState`, ancestors) | +| `toBeDisabled()` | Element has `aria-disabled` or `accessibilityState={{ disabled: true }}` (checks ancestors) | +| `toBeBusy()` | Element has `aria-busy` or `accessibilityState={{ busy: true }}` | +| `toBeChecked()` | Element has `aria-checked` or `accessibilityState={{ checked: true }}` | +| `toBePartiallyChecked()` | Element has `aria-checked="mixed"` or `accessibilityState={{ checked: 'mixed' }}` | +| `toBeSelected()` | Element has `aria-selected` or `accessibilityState={{ selected: true }}` | +| `toBeExpanded()` | Element has `aria-expanded` or `accessibilityState={{ expanded: true }}` | +| `toBeCollapsed()` | Element has `aria-expanded={false}` or `accessibilityState={{ expanded: false }}` | +| `toHaveTextContent(text)` | Element has matching text content | +| `toHaveDisplayValue(value)` | TextInput has matching display value | +| `toHaveAccessibleName(name?)` | Element has matching `aria-label`, `accessibilityLabel`, or text content | +| `toHaveAccessibilityValue(value)` | Element has matching `aria-value*` or `accessibilityValue` | +| `toHaveStyle(style)` | Element has matching style | +| `toHaveProp(name, value?)` | Element has prop (use semantic matchers when possible) | + +## User Interactions + +**Prefer `userEvent`** over `fireEvent` for realistic user interaction simulation. `userEvent` triggers the complete event sequence that real users would produce. + +### userEvent (Preferred, Async) + +```tsx +const user = userEvent.setup(); +``` + +| Method | Description | +| ------------------------------------------ | ----------------------------------------------------------------------------------- | +| `await user.press(element)` | Press an element (triggers `pressIn`, `pressOut`, `press`) | +| `await user.longPress(element, options?)` | Long press with optional `{ duration }` | +| `await user.type(element, text, options?)` | Type into TextInput (triggers `focus`, `keyPress`, `change`, `changeText` per char) | +| `await user.clear(element)` | Clear TextInput (select all + backspace) | +| `await user.paste(element, text)` | Paste text into TextInput | +| `await user.scrollTo(element, options)` | Scroll a ScrollView with `{ y }` or `{ x }` offset | + +### fireEvent (Low-level, Sync in v13) + +Use only when `userEvent` doesn't support the event or when you need direct control. + +| Method | Description | +| ---------------------------------------- | --------------------------------------------- | +| `fireEvent(element, eventName, ...data)` | Fire any event by name | +| `fireEvent.press(element)` | Fire `onPress` only (no `pressIn`/`pressOut`) | +| `fireEvent.changeText(element, text)` | Fire `onChangeText` directly | +| `fireEvent.scroll(element, eventData)` | Fire `onScroll` with event data | + +## Sync vs Async APIs (v13) + +- **`render()`, `fireEvent.*`, `renderHook()` are synchronous** - no `await` needed +- **`userEvent.*` is asynchronous** - requires `await` +- **Don't wrap in `act()`** - `render` and `fireEvent` handle it internally +- **For React 19**: use async variants: `renderAsync()`, `fireEventAsync.*()` with `await`. + +## waitFor Usage + +- **Use `findBy*`** instead of `waitFor` + `getBy*` when waiting for elements +- **Never perform side-effects** (like `fireEvent.press()`) inside `waitFor` callbacks +- **One assertion per `waitFor`** callback +- **Never pass empty callbacks** - always include a meaningful assertion +- **Place side-effects before `waitFor`** - perform actions, then wait for result + +## Code Organization + +- **Use `screen`** instead of destructuring from `render()`: `screen.getByText()` not `const { getByText } = render()` +- **Prefer `userEvent`** over `fireEvent` for realistic interactions +- **Don't use `cleanup()`** - handled automatically +- **Name wrappers descriptively**: `ThemeProvider` not `Wrapper` +- **Install ESLint plugin**: `eslint-plugin-testing-library` + +## Quick Checklist + +- Using `getByRole` as first choice? +- Using `findBy*` for async elements (not `waitFor` + `getBy*`)? +- Using `queryBy*` only for non-existence? +- Using RNTL matchers (`toBeOnTheScreen()`, `toBeDisabled()`, etc.)? +- Using `screen` not destructuring from `render()`? +- Avoiding side-effects in `waitFor`? +- Using `userEvent` when appropriate? + +## Example: Good Pattern + +```tsx +import { render, screen } from '@testing-library/react-native'; +import userEvent from '@testing-library/react-native'; +import { Pressable, Text, TextInput, View } from 'react-native'; + +test('user can submit form', async () => { + const user = userEvent.setup(); + + const Component = () => { + const [name, setName] = React.useState(''); + const [submitted, setSubmitted] = React.useState(false); + + return ( + + + setSubmitted(true)}> + Submit + + {submitted && Form submitted!} + + ); + }; + + render(); + + // getByRole as first choice + const input = screen.getByRole('textbox', { name: 'Name' }); + const button = screen.getByRole('button', { name: 'Submit' }); + + // userEvent for realistic interactions (async) + await user.type(input, 'John Doe'); + await user.press(button); + + // findBy* for async elements + const successMessage = await screen.findByRole('alert'); + + // RNTL matchers + expect(successMessage).toBeOnTheScreen(); + expect(successMessage).toHaveTextContent('Form submitted!'); +}); +``` + +## Example: Anti-Patterns + +```tsx +// getBy* for non-existence +expect(screen.getByText('Error')).not.toBeOnTheScreen(); + +// waitFor + getBy* instead of findBy* +await waitFor(() => { + expect(screen.getByText('Loaded')).toBeOnTheScreen(); +}); + +// Side-effect in waitFor +await waitFor(async () => { + fireEvent.press(button); + expect(screen.getByText('Result')).toBeOnTheScreen(); +}); + +// accessibility* props instead of ARIA +; + +// Destructuring from render +const { getByText } = render(); +``` + +By following these guidelines, your tests will be more maintainable, accessible, and reliable. diff --git a/website/docs/14.x/docs/guides/_meta.json b/website/docs/14.x/docs/guides/_meta.json index 5ae12d7e2..ffb1036dc 100644 --- a/website/docs/14.x/docs/guides/_meta.json +++ b/website/docs/14.x/docs/guides/_meta.json @@ -1 +1 @@ -["how-to-query", "troubleshooting", "faq", "community-resources"] +["how-to-query", "common-mistakes", "troubleshooting", "faq", "community-resources"] diff --git a/website/docs/14.x/docs/guides/common-mistakes.mdx b/website/docs/14.x/docs/guides/common-mistakes.mdx new file mode 100644 index 000000000..7982a956e --- /dev/null +++ b/website/docs/14.x/docs/guides/common-mistakes.mdx @@ -0,0 +1,587 @@ +# Common Mistakes with React Native Testing Library + +> **Note:** This guide is adapted from Kent C. Dodds' article ["Common mistakes with React Testing Library"](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library) for React Native Testing Library v14. The original article focuses on web React, but the principles apply to React Native as well. This adaptation includes React Native-specific examples, async API usage (v14), and ARIA-compatible accessibility attributes. + +React Native Testing Library guiding principle is: + +> "The more your tests resemble the way your software is used, the more confidence they can give you." + +This guide outlines some common mistakes people make when using React Native Testing Library and how to avoid them. + +## Using the wrong query {#using-the-wrong-query} + +Importance: high + +React Native Testing Library provides several query types. Here's the priority order: + +1. **Queries that reflect user experience:** + - `getByRole` - most accessible + - `getByLabelText` - accessible label + - `getByPlaceholderText` - `TextInput` placeholder text + - `getByText` - text content + - `getByDisplayValue` - `TextInput` input value + +2. **Semantic queries:** + - `getByTestId` - only if nothing else works + +Here's an example of using the right query: + +```tsx +import { TextInput, View } from 'react-native'; +import { render, screen } from '@testing-library/react-native'; + +test('finds input by label', async () => { + await render( + + + + ); + + // ✅ Good - uses accessible label + const input = screen.getByLabelText('Username'); + + // ✅ Also good - uses placeholder + const inputByPlaceholder = screen.getByPlaceholderText('Enter username'); + + // ❌ Bad - uses testID when accessible queries work + // const input = screen.getByTestId('username-input'); +}); +``` + +## Not using `*ByRole` query most of the time {#not-using-byrole-most-of-the-time} + +Importance: high + +`getByRole` is the most accessible query and should be your first choice. It queries elements by their semantic role: + +```tsx +import { Pressable, Text, TextInput, View } from 'react-native'; +import { render, screen } from '@testing-library/react-native'; + +test('uses role queries', async () => { + await render( + + + Submit + + + + ); + + // ✅ Good - uses role query + const button = screen.getByRole('button', { name: 'Submit' }); + const searchbox = screen.getByRole('searchbox', { name: 'Search' }); + + expect(button).toBeOnTheScreen(); + expect(searchbox).toBeOnTheScreen(); +}); +``` + +Common roles in React Native include: + +- `button` - pressable elements +- `text` - static text +- `header` / `heading` - headers +- `searchbox` - search inputs +- `switch` - toggle switches +- `checkbox` - checkboxes +- `radio` - radio buttons +- And more... + +Note: React Native supports both ARIA-compatible (`role`) and legacy (`accessibilityRole`) props. Prefer `role` for consistency with web standards. + +## Using the wrong assertion {#using-the-wrong-assertion} + +Importance: high + +React Native Testing Library provides built-in Jest matchers. Make sure you're using the right ones: + +```tsx +import { Pressable, Text } from 'react-native'; +import { render, screen } from '@testing-library/react-native'; + +test('button is disabled', async () => { + await render( + + Submit + + ); + + const button = screen.getByRole('button', { name: 'Submit' }); + + // ✅ Good - uses RNTL matcher + expect(button).toBeDisabled(); + + // ❌ Bad - doesn't use RNTL matcher + expect(button.props['aria-disabled']).toBe(true); +}); +``` + +Common matchers include: + +- `toBeOnTheScreen()` - checks if element is rendered (replaces `toBeInTheDocument()`) +- `toBeDisabled()` - checks if element is disabled +- `toHaveTextContent()` - checks text content +- `toHaveAccessibleName()` - checks accessible name +- And more... + +## Using `query*` variants for anything except checking for non-existence {#using-query-variants-for-anything-except-checking-for-non-existence} + +Importance: high + +Use `queryBy*` only when checking that an element doesn't exist: + +```tsx +import { View, Text } from 'react-native'; +import { render, screen } from '@testing-library/react-native'; + +test('checks non-existence', async () => { + await render( + + Hello + + ); + + // ✅ Good - uses queryBy for non-existence check + expect(screen.queryByText('Goodbye')).not.toBeOnTheScreen(); + + // ❌ Bad - uses queryBy when element should exist + // const element = screen.queryByText('Hello'); + // expect(element).toBeOnTheScreen(); + + // ✅ Good - uses getBy when element should exist + expect(screen.getByText('Hello')).toBeOnTheScreen(); +}); +``` + +## Using `waitFor` to wait for elements that can be queried with `find*` {#using-waitfor-to-wait-for-elements-that-can-be-queried-with-find} + +Importance: high + +Use `findBy*` queries instead of `waitFor` + `getBy*`: + +```tsx +import { View, Text } from 'react-native'; +import { render, screen, waitFor } from '@testing-library/react-native'; + +test('waits for element', async () => { + const Component = () => { + const [show, setShow] = React.useState(false); + + React.useEffect(() => { + setTimeout(() => setShow(true), 100); + }, []); + + return {show && Loaded}; + }; + + await render(); + + // ✅ Good - uses findBy query + const element = await screen.findByText('Loaded'); + expect(element).toBeOnTheScreen(); + + // ❌ Bad - uses waitFor + getBy + // await waitFor(() => { + // expect(screen.getByText('Loaded')).toBeOnTheScreen(); + // }); +}); +``` + +## Performing side-effects in `waitFor` {#performing-side-effects-in-waitfor} + +Importance: high + +Don't perform side-effects in `waitFor` callbacks: + +```tsx +import { Pressable, Text, View } from 'react-native'; +import { render, screen, waitFor, fireEvent } from '@testing-library/react-native'; + +test('avoids side effects in waitFor', async () => { + const Component = () => { + const [count, setCount] = React.useState(0); + return ( + + setCount(count + 1)}> + Increment + + Count: {count} + + ); + }; + + await render(); + + const button = screen.getByRole('button'); + + // ❌ Bad - side effect in waitFor + // await waitFor(async () => { + // await fireEvent.press(button); + // expect(screen.getByText('Count: 1')).toBeOnTheScreen(); + // }); + + // ✅ Good - side effect outside waitFor + await fireEvent.press(button); + await waitFor(() => { + expect(screen.getByText('Count: 1')).toBeOnTheScreen(); + }); +}); +``` + +## Using `container` to query for elements {#using-container-to-query-for-elements} + +Importance: high + +React Native Testing Library provides a `container` object that has a `queryAll` method, but you should avoid using it directly: + +```tsx +import { View, Text } from 'react-native'; +import { render } from '@testing-library/react-native'; + +test('finds element incorrectly', async () => { + const { container } = await render( + + Hello + + ); + + // ❌ Bad - using container.queryAll directly + const element = container.queryAll((node) => node.props.testID === 'message')[0]; + + // ✅ Good - use proper queries + // const element = screen.getByTestId('message'); +}); +``` + +Instead, use the proper query methods from `screen` or the `render` result. The `container` is a low-level API that you rarely need. + +## Passing an empty callback to `waitFor` {#passing-an-empty-callback-to-waitfor} + +Importance: high + +Don't pass an empty callback to `waitFor`: + +```tsx +import { View } from 'react-native'; +import { render, waitFor } from '@testing-library/react-native'; + +test('waits correctly', async () => { + await render(); + + // ❌ Bad - empty callback + // await waitFor(() => {}); + + // ✅ Good - meaningful assertion + await waitFor(() => { + expect(screen.getByTestId('test')).toBeOnTheScreen(); + }); +}); +``` + +## Not using `screen` {#not-using-screen} + +Importance: medium + +You can get all the queries from the `render` result: + +```tsx +import { View, Text } from 'react-native'; +import { render } from '@testing-library/react-native'; + +test('renders component', async () => { + const { getByText } = await render( + + Hello + + ); + + expect(getByText('Hello')).toBeOnTheScreen(); +}); +``` + +But you can also get them from the `screen` object: + +```tsx +import { View, Text } from 'react-native'; +import { render, screen } from '@testing-library/react-native'; + +test('renders component', async () => { + await render( + + Hello + + ); + + expect(screen.getByText('Hello')).toBeOnTheScreen(); +}); +``` + +Using `screen` has several benefits: + +1. You don't need to destructure `getByText` from `render` +2. It's more consistent with the Testing Library ecosystem + +## Wrapping things in `act` unnecessarily {#wrapping-things-in-act-unnecessarily} + +Importance: medium + +React Native Testing Library's `render`, `renderHook`, `userEvent`, and `fireEvent` are already wrapped in `act`, so you don't need to wrap them yourself: + +```tsx +import { Pressable, Text, View } from 'react-native'; +import { render, fireEvent, screen } from '@testing-library/react-native'; + +test('updates on press', async () => { + const Component = () => { + const [count, setCount] = React.useState(0); + return ( + + setCount(count + 1)}> + Count: {count} + + + ); + }; + + await render(); + + const button = screen.getByRole('button'); + + // ✅ Good - fireEvent is already wrapped in act + await fireEvent.press(button); + + expect(screen.getByText('Count: 1')).toBeOnTheScreen(); + + // ❌ Bad - unnecessary act wrapper + // await act(async () => { + // await fireEvent.press(button); + // }); +}); +``` + +## Not using User Event API + +Importance: medium + +`userEvent` provides a more realistic way to simulate user interactions: + +```tsx +import { Pressable, Text, TextInput, View } from 'react-native'; +import { render, screen, userEvent } from '@testing-library/react-native'; + +test('uses userEvent', async () => { + const user = userEvent.setup(); + + const Component = () => { + const [value, setValue] = React.useState(''); + return ( + + + setValue('')}> + Clear + + + ); + }; + + await render(); + + const input = screen.getByLabelText('Name'); + const button = screen.getByRole('button', { name: 'Clear' }); + + // ✅ Good - uses userEvent for realistic interactions + await user.type(input, 'John'); + expect(input).toHaveValue('John'); + + await user.press(button); + expect(input).toHaveValue(''); +}); +``` + +`userEvent` methods are async and must be awaited. Available methods include: + +- `press()` - simulates a press +- `longPress()` - simulates long press +- `type()` - simulates typing +- `clear()` - clears text input +- `paste()` - simulates pasting +- `scrollTo()` - simulates scrolling + +## Not querying by text {#not-querying-by-text} + +Importance: medium + +In React Native, text is rendered in `` components. You should query by the text content that users see: + +```tsx +import { Text, View } from 'react-native'; +import { render, screen } from '@testing-library/react-native'; + +test('finds text correctly', async () => { + await render( + + Hello World + + ); + + // ✅ Good - queries by visible text + expect(screen.getByText('Hello World')).toBeOnTheScreen(); + + // ❌ Bad - queries by testID when text is available + // expect(screen.getByTestId('greeting')).toBeOnTheScreen(); +}); +``` + +## Not using Testing Library ESLint plugins {#not-using-testing-library-eslint-plugins} + +Importance: medium + +There's an ESLint plugin for Testing Library: [`eslint-plugin-testing-library`](https://github.com/testing-library/eslint-plugin-testing-library). This plugin can help you avoid common mistakes and will automatically fix your code in many cases. + +You can install it with: + +```bash +yarn add --dev eslint-plugin-testing-library +``` + +And configure it in your `eslint.config.js` (flat config): + +```js +import testingLibrary from 'eslint-plugin-testing-library'; + +export default [testingLibrary.configs['flat/react']]; +``` + +Note: Unlike React Testing Library, React Native Testing Library has built-in Jest matchers, so you don't need `eslint-plugin-jest-dom`. + +## Using `cleanup` {#using-cleanup} + +Importance: medium + +React Native Testing Library automatically cleans up after each test. You don't need to call `cleanup()` manually unless you're using the `pure` export (which doesn't include automatic cleanup). + +If you want to disable automatic cleanup for a specific test, you can use: + +```tsx +import { render } from '@testing-library/react-native/pure'; + +test('does not cleanup', async () => { + // This test won't cleanup automatically + await render(); + // ... your test +}); +``` + +But in most cases, you don't need to worry about cleanup at all - it's handled automatically. + +## Using `get*` variants as assertions {#using-get-variants-as-assertions} + +Importance: low + +`getBy*` queries throw errors when elements aren't found, so they work as assertions. However, for better error messages, you might want to combine them with explicit matchers: + +```tsx +import { View, Text } from 'react-native'; +import { render, screen } from '@testing-library/react-native'; + +test('uses getBy as assertion', async () => { + await render( + + Hello + + ); + + // ✅ Good - getBy throws if not found, so it's an assertion + const element = screen.getByText('Hello'); + expect(element).toBeOnTheScreen(); + + // ✅ Also good - more explicit + expect(screen.getByText('Hello')).toBeOnTheScreen(); + + // ❌ Bad - redundant assertion + // const element = screen.getByText('Hello'); + // expect(element).not.toBeNull(); // getBy already throws if null +}); +``` + +## Having multiple assertions in a single `waitFor` callback {#having-multiple-assertions-in-a-single-waitfor-callback} + +Importance: low + +Keep `waitFor` callbacks focused on a single assertion: + +```tsx +import { View, Text } from 'react-native'; +import { render, screen, waitFor } from '@testing-library/react-native'; + +test('waits with single assertion', async () => { + const Component = () => { + const [count, setCount] = React.useState(0); + + React.useEffect(() => { + setTimeout(() => setCount(1), 100); + }, []); + + return ( + + Count: {count} + + ); + }; + + await render(); + + // ✅ Good - single assertion per waitFor + await waitFor(() => { + expect(screen.getByText('Count: 1')).toBeOnTheScreen(); + }); + + // If you need multiple assertions, do them after waitFor + expect(screen.getByText('Count: 1')).toHaveTextContent('Count: 1'); + + // ❌ Bad - multiple assertions in waitFor + // await waitFor(() => { + // expect(screen.getByText('Count: 1')).toBeOnTheScreen(); + // expect(screen.getByText('Count: 1')).toHaveTextContent('Count: 1'); + // }); +}); +``` + +## Using `wrapper` as the variable name {#using-wrapper-as-the-variable-name} + +Importance: low + +This is not really a "mistake" per se, but it's a common pattern that can be improved. When you use the `wrapper` option in `render`, you might be tempted to name your wrapper component `Wrapper`: + +```tsx +import { View } from 'react-native'; +import { render, screen } from '@testing-library/react-native'; + +test('renders with wrapper', async () => { + const Wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + + await render(Content, { + wrapper: Wrapper, + }); + + expect(screen.getByTestId('content')).toBeOnTheScreen(); +}); +``` + +This works fine, but it's more conventional to name it something more descriptive like `ThemeProvider` or `AllTheProviders` (if you're wrapping with multiple providers). This makes it clearer what the wrapper is doing. + +## Summary + +The key principles to remember: + +1. **Use the right query** - Prefer `getByRole` as your first choice, use `findBy*` for async elements, and `queryBy*` only for checking non-existence +2. **Use proper assertions** - Use RNTL's built-in matchers (`toBeOnTheScreen()`, `toBeDisabled()`, etc.) instead of asserting on props directly +3. **Handle async operations correctly** - Always `await` `render()`, `renderHook`, `fireEvent`,and `userEvent` methods +4. **Use `waitFor` correctly** - Avoid side-effects in callbacks, use `findBy*` instead when possible, and keep callbacks focused +5. **Follow accessibility best practices** - Prefer ARIA attributes (`role`, `aria-label`) over `accessibility*` props +6. **Organize code well** - Use `screen` over destructuring, prefer `userEvent` over `fireEvent`, and don't use `cleanup()` + +By following these principles, your tests will be more maintainable, accessible, and reliable. diff --git a/website/docs/14.x/docs/guides/llm-guidelines.mdx b/website/docs/14.x/docs/guides/llm-guidelines.mdx new file mode 100644 index 000000000..35c9942a9 --- /dev/null +++ b/website/docs/14.x/docs/guides/llm-guidelines.mdx @@ -0,0 +1,229 @@ +# LLM Guidelines for React Native Testing Library + +Actionable guidelines for writing tests with React Native Testing Library (RNTL) v14. + +## Core APIs + +### render + +```tsx +const result = await render(, options?); +``` + +| Option | Description | +| ---------------- | ---------------------------------------------------------------- | +| `wrapper` | React component to wrap the rendered component (e.g., providers) | +| `createNodeMock` | Function to create mock refs | + +| Return | Description | +| --------------------- | ------------------------------------------------ | +| `rerender(component)` | Re-render with a new component (async) | +| `unmount()` | Unmount the rendered component (async) | +| `toJSON()` | Get JSON representation for snapshots | +| `debug(options?)` | Print the component tree to console | +| `container` | Root host element of the rendered tree | +| `root` | First child host element (your component's root) | + +### screen + +**Prefer `screen`** over destructuring from `render()`. Provides all query methods after `render()` is called. + +```tsx +await render(); +screen.getByRole('button'); // Access queries via screen +``` + +### renderHook + +```tsx +const { result, rerender, unmount } = await renderHook(() => useMyHook(), options?); +``` + +| Option | Description | +| -------------- | -------------------------------------------------- | +| `initialProps` | Initial props passed to the hook | +| `wrapper` | React component to wrap the hook (e.g., providers) | + +| Return | Description | +| ------------------ | ------------------------------------- | +| `result.current` | Current return value of the hook | +| `rerender(props?)` | Re-render hook with new props (async) | +| `unmount()` | Unmount the hook (async) | + +## Query Selection + +- **Prefer `getByRole`** as first choice for querying elements +- **Query priority**: `getByRole` → `getByLabelText` → `getByPlaceholderText` → `getByText` → `getByDisplayValue` → `getByTestId` (last resort) +- **Use `findBy*`** for elements that appear asynchronously (after API calls, timeouts, state updates) +- **Use `queryBy*` ONLY** for checking non-existence (with `.not.toBeOnTheScreen()`) +- **Never use `getBy*`** for non-existence checks +- **Avoid `container.queryAll()`** - use `screen` queries instead +- **Query by visible text**, not `testID` when text is available + +## Assertions + +- **Use RNTL matchers** - prefer semantic matchers over prop assertions +- **Combine queries with matchers**: `expect(screen.getByText('Hello')).toBeOnTheScreen()` +- **No redundant null checks** - `getBy*` already throws if not found + +## Jest Matchers Reference + +| Matcher | Description | +| --------------------------------- | ------------------------------------------------------------------------------------------- | +| `toBeOnTheScreen()` | Element is present in the element tree | +| `toBeVisible()` | Element is visible (checks style, `aria-hidden`, `accessibilityElementsHidden`, ancestors) | +| `toBeEmptyElement()` | Element has no children or text content | +| `toContainElement(element)` | Element contains another element | +| `toBeEnabled()` | Element is not disabled (checks `aria-disabled`, `accessibilityState`, ancestors) | +| `toBeDisabled()` | Element has `aria-disabled` or `accessibilityState={{ disabled: true }}` (checks ancestors) | +| `toBeBusy()` | Element has `aria-busy` or `accessibilityState={{ busy: true }}` | +| `toBeChecked()` | Element has `aria-checked` or `accessibilityState={{ checked: true }}` | +| `toBePartiallyChecked()` | Element has `aria-checked="mixed"` or `accessibilityState={{ checked: 'mixed' }}` | +| `toBeSelected()` | Element has `aria-selected` or `accessibilityState={{ selected: true }}` | +| `toBeExpanded()` | Element has `aria-expanded` or `accessibilityState={{ expanded: true }}` | +| `toBeCollapsed()` | Element has `aria-expanded={false}` or `accessibilityState={{ expanded: false }}` | +| `toHaveTextContent(text)` | Element has matching text content | +| `toHaveDisplayValue(value)` | TextInput has matching display value | +| `toHaveAccessibleName(name?)` | Element has matching `aria-label`, `accessibilityLabel`, or text content | +| `toHaveAccessibilityValue(value)` | Element has matching `aria-value*` or `accessibilityValue` | +| `toHaveStyle(style)` | Element has matching style | +| `toHaveProp(name, value?)` | Element has prop (use semantic matchers when possible) | + +## User Interactions + +**Prefer `userEvent`** over `fireEvent` for realistic user interaction simulation. `userEvent` triggers the complete event sequence that real users would produce. + +### userEvent (Preferred) + +```tsx +const user = userEvent.setup(); +``` + +| Method | Description | +| ------------------------------------ | ----------------------------------------------------------------------------------- | +| `user.press(element)` | Press an element (triggers `pressIn`, `pressOut`, `press`) | +| `user.longPress(element, options?)` | Long press with optional `{ duration }` | +| `user.type(element, text, options?)` | Type into TextInput (triggers `focus`, `keyPress`, `change`, `changeText` per char) | +| `user.clear(element)` | Clear TextInput (select all + backspace) | +| `user.paste(element, text)` | Paste text into TextInput | +| `user.scrollTo(element, options)` | Scroll a ScrollView with `{ y }` or `{ x }` offset | + +### fireEvent (Low-level) + +Use only when `userEvent` doesn't support the event or when you need direct control. + +| Method | Description | +| ---------------------------------------- | --------------------------------------------- | +| `fireEvent(element, eventName, ...data)` | Fire any event by name | +| `fireEvent.press(element)` | Fire `onPress` only (no `pressIn`/`pressOut`) | +| `fireEvent.changeText(element, text)` | Fire `onChangeText` directly | +| `fireEvent.scroll(element, eventData)` | Fire `onScroll` with event data | + +## Async/Await (v14) + +- **Always `await`**: `render()`, `fireEvent.*`, `renderHook()`, `userEvent.*` +- **Make test functions `async`**: `test('name', async () => { ... })` +- **Don't wrap in `act()`** - `render` and `fireEvent` handle it internally + +## waitFor Usage + +- **Use `findBy*`** instead of `waitFor` + `getBy*` when waiting for elements +- **Never perform side-effects** (like `fireEvent.press()`) inside `waitFor` callbacks +- **One assertion per `waitFor`** callback +- **Never pass empty callbacks** - always include a meaningful assertion +- **Place side-effects before `waitFor`** - perform actions, then wait for result + +## Code Organization + +- **Use `screen`** instead of destructuring from `render()`: `screen.getByText()` not `const { getByText } = render()` +- **Prefer `userEvent`** over `fireEvent` for realistic interactions +- **Don't use `cleanup()`** - handled automatically +- **Name wrappers descriptively**: `ThemeProvider` not `Wrapper` +- **Install ESLint plugin**: `eslint-plugin-testing-library` + +## Quick Checklist + +- ✅ Using `getByRole` as first choice? +- ✅ Using `await` for all async operations? +- ✅ Using `findBy*` for async elements (not `waitFor` + `getBy*`)? +- ✅ Using `queryBy*` only for non-existence? +- ✅ Using RNTL matchers (`toBeOnTheScreen()`, `toBeDisabled()`, etc.)? +- ✅ Using `screen` not destructuring from `render()`? +- ✅ Avoiding side-effects in `waitFor`? +- ✅ Using `userEvent` when appropriate? + +## Example: Good Pattern + +```tsx +import { render, screen } from '@testing-library/react-native'; +import userEvent from '@testing-library/react-native'; +import { Pressable, Text, TextInput, View } from 'react-native'; + +test('user can submit form', async () => { + const user = userEvent.setup(); + + const Component = () => { + const [name, setName] = React.useState(''); + const [submitted, setSubmitted] = React.useState(false); + + return ( + + + setSubmitted(true)}> + Submit + + {submitted && Form submitted!} + + ); + }; + + await render(); + + // ✅ getByRole as first choice + const input = screen.getByRole('textbox', { name: 'Name' }); + const button = screen.getByRole('button', { name: 'Submit' }); + + // ✅ userEvent for realistic interactions + await user.type(input, 'John Doe'); + await user.press(button); + + // ✅ findBy* for async elements + const successMessage = await screen.findByRole('alert'); + + // ✅ RNTL matchers + expect(successMessage).toBeOnTheScreen(); + expect(successMessage).toHaveTextContent('Form submitted!'); +}); +``` + +## Example: Anti-Patterns + +```tsx +// ❌ Missing await +test('bad', () => { + render(); + fireEvent.press(screen.getByText('Submit')); +}); + +// ❌ getBy* for non-existence +expect(screen.getByText('Error')).not.toBeOnTheScreen(); + +// ❌ waitFor + getBy* instead of findBy* +await waitFor(() => { + expect(screen.getByText('Loaded')).toBeOnTheScreen(); +}); + +// ❌ Side-effect in waitFor +await waitFor(async () => { + await fireEvent.press(button); + expect(screen.getByText('Result')).toBeOnTheScreen(); +}); + +// ❌ accessibility* props instead of ARIA +; + +// ❌ Destructuring from render +const { getByText } = await render(); +``` + +By following these guidelines, your tests will be more maintainable, accessible, and reliable. diff --git a/website/rspress.config.ts b/website/rspress.config.ts index 05bf15d68..295377938 100644 --- a/website/rspress.config.ts +++ b/website/rspress.config.ts @@ -96,99 +96,6 @@ const sidebar12x = { ], }; -const sidebar13x = { - '/13.x/docs/': [ - { - text: 'Getting started', - items: [ - { text: 'Introduction', link: '/13.x/docs/start/intro' }, - { text: 'Quick Start', link: '/13.x/docs/start/quick-start' }, - ], - }, - { - text: 'API reference', - items: [ - { text: 'Render function', link: '/13.x/docs/api/render' }, - { text: 'Screen object', link: '/13.x/docs/api/screen' }, - { text: 'Queries', link: '/13.x/docs/api/queries' }, - { text: 'Jest Matchers', link: '/13.x/docs/api/jest-matchers' }, - { - text: 'Triggering events', - items: [ - { text: 'Fire Event', link: '/13.x/docs/api/events/fire-event' }, - { text: 'User Event', link: '/13.x/docs/api/events/user-event' }, - ], - }, - { - text: 'Miscellaneous', - items: [ - { text: 'Accessibility', link: '/13.x/docs/api/misc/accessibility' }, - { text: 'Async utilities', link: '/13.x/docs/api/misc/async' }, - { text: 'Config', link: '/13.x/docs/api/misc/config' }, - { text: 'Other', link: '/13.x/docs/api/misc/other' }, - { text: 'Render Hook', link: '/13.x/docs/api/misc/render-hook' }, - ], - }, - ], - }, - { - text: 'Guides', - items: [ - { text: 'How to Query', link: '/13.x/docs/guides/how-to-query' }, - { text: 'React 19', link: '/13.x/docs/guides/react-19' }, - { text: 'Troubleshooting', link: '/13.x/docs/guides/troubleshooting' }, - { text: 'FAQ', link: '/13.x/docs/guides/faq' }, - { text: 'Community Resources', link: '/13.x/docs/guides/community-resources' }, - ], - }, - { - text: 'Advanced Guides', - items: [ - { text: 'Testing Environment', link: '/13.x/docs/advanced/testing-env' }, - { text: 'Third-party Integration', link: '/13.x/docs/advanced/third-party-integration' }, - { text: 'Understanding Act', link: '/13.x/docs/advanced/understanding-act' }, - ], - }, - { - text: 'Migration Guides', - collapsed: true, - items: [ - { text: 'v13 Migration', link: '/13.x/docs/migration/v13' }, - { text: 'Jest Matchers', link: '/13.x/docs/migration/jest-matchers' }, - { - text: 'Previous versions', - collapsed: true, - items: [ - { text: 'v12', link: '/13.x/docs/migration/previous/v12' }, - { text: 'v11', link: '/13.x/docs/migration/previous/v11' }, - { text: 'v9', link: '/13.x/docs/migration/previous/v9' }, - { text: 'v7', link: '/13.x/docs/migration/previous/v7' }, - { text: 'v2', link: '/13.x/docs/migration/previous/v2' }, - ], - }, - ], - }, - ], - '/13.x/cookbook/': [ - { text: 'Cookbook', link: '/13.x/cookbook/' }, - { - text: 'Basic Recipes', - items: [ - { text: 'Async Tests', link: '/13.x/cookbook/basics/async-tests' }, - { text: 'Custom Render', link: '/13.x/cookbook/basics/custom-render' }, - ], - }, - { - text: 'Advanced Recipes', - items: [{ text: 'Network Requests', link: '/13.x/cookbook/advanced/network-requests' }], - }, - { - text: 'State Management Recipes', - items: [{ text: 'Jotai', link: '/13.x/cookbook/state-management/jotai' }], - }, - ], -}; - const sidebar14x = { '/14.x/docs/': [ { @@ -229,6 +136,8 @@ const sidebar14x = { text: 'Guides', items: [ { text: 'How to Query', link: '/14.x/docs/guides/how-to-query' }, + { text: 'Common Mistakes', link: '/14.x/docs/guides/common-mistakes' }, + { text: 'LLM Guidelines', link: '/14.x/docs/guides/llm-guidelines' }, { text: 'Troubleshooting', link: '/14.x/docs/guides/troubleshooting' }, { text: 'FAQ', link: '/14.x/docs/guides/faq' }, { text: 'Community Resources', link: '/14.x/docs/guides/community-resources' }, @@ -303,6 +212,8 @@ const sidebarDefault = { text: 'Guides', items: [ { text: 'How to Query', link: '/docs/guides/how-to-query' }, + { text: 'Common Mistakes', link: '/docs/guides/common-mistakes' }, + { text: 'LLM Guidelines', link: '/docs/guides/llm-guidelines' }, { text: 'React 19', link: '/docs/guides/react-19' }, { text: 'Troubleshooting', link: '/docs/guides/troubleshooting' }, { text: 'FAQ', link: '/docs/guides/faq' }, @@ -409,7 +320,6 @@ export default defineConfig({ sidebar: { ...sidebarDefault, ...sidebar12x, - ...sidebar13x, ...sidebar14x, }, },