diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2920eee3..0f708b7f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -27,9 +27,6 @@ updates: - 'react-dom' - '@types/react' - '@types/react-dom' - testing: - patterns: - - '@testing-library/*' vite: patterns: - 'vite' @@ -37,6 +34,7 @@ updates: vitest: patterns: - 'vitest' + - 'vitest-browser-react' - '@vitest/*' workbox: patterns: diff --git a/config/test/setupTests.ts b/config/test/setupTests.ts deleted file mode 100644 index cce5708f..00000000 --- a/config/test/setupTests.ts +++ /dev/null @@ -1,9 +0,0 @@ -import '@testing-library/jest-dom/vitest'; -import { cleanup } from '@testing-library/react'; -import { afterEach } from 'vitest'; - -// Clear all mocks and cleanup DOM after every test -afterEach(() => { - vi.clearAllMocks(); - cleanup(); -}); diff --git a/package-lock.json b/package-lock.json index 34a21dc3..b3efccb3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,8 +40,6 @@ "devDependencies": { "@shlinkio/eslint-config-js-coding-standard": "~4.1.0", "@tailwindcss/vite": "^4.3.3", - "@testing-library/jest-dom": "^7.0.0", - "@testing-library/react": "^16.3.2", "@total-typescript/shoehorn": "^0.1.2", "@types/node": "^26.1.2", "@types/react": "^19.2.18", @@ -61,15 +59,10 @@ "typescript": "^7.0.2", "vite": "^8.2.0", "vite-plugin-pwa": "^1.3.0", - "vitest": "^4.0.3" + "vitest": "^4.0.3", + "vitest-browser-react": "^2.2.0" } }, - "node_modules/@adobe/css-tools": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz", - "integrity": "sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==", - "dev": true - }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -3653,98 +3646,6 @@ "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, - "node_modules/@testing-library/dom": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", - "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@testing-library/dom/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@testing-library/jest-dom": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.0.tgz", - "integrity": "sha512-HKAH9C6mBo5yBG6yRO5i43L2iisencAo5z+o5P/saHUoY+miC5ivXRxHBJcFyB5ypPNxHJdK3BoF/3O4DIptMg==", - "dev": true, - "dependencies": { - "@adobe/css-tools": "^4.4.0", - "aria-query": "^5.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.6.3", - "picocolors": "^1.1.1", - "redent": "^3.0.0" - }, - "engines": { - "node": ">=22", - "npm": ">=6", - "yarn": ">=1" - }, - "peerDependencies": { - "@testing-library/dom": ">=10 <11" - } - }, - "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", - "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", - "dev": true - }, - "node_modules/@testing-library/react": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", - "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.12.5" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@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 - } - } - }, "node_modules/@total-typescript/shoehorn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@total-typescript/shoehorn/-/shoehorn-0.1.2.tgz", @@ -3766,13 +3667,6 @@ "node": ">=12" } }, - "node_modules/@types/aria-query": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz", - "integrity": "sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==", - "dev": true, - "peer": true - }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -4446,40 +4340,6 @@ "node": ">=14.0" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "peer": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "dependencies": { - "dequal": "^2.0.3" - } - }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", @@ -4800,26 +4660,6 @@ "node": ">=6" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "peer": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "peer": true - }, "node_modules/common-tags": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", @@ -4896,11 +4736,6 @@ "node": ">=8" } }, - "node_modules/css.escape": { - "version": "1.5.1", - "dev": true, - "license": "MIT" - }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -5168,15 +5003,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -5185,12 +5011,6 @@ "node": ">=8" } }, - "node_modules/dom-accessibility-api": { - "version": "0.5.13", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -5871,14 +5691,6 @@ "url": "https://opencollective.com/immer" } }, - "node_modules/indent-string": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -6699,16 +6511,6 @@ "node": "20 || >=22" } }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, - "peer": true, - "bin": { - "lz-string": "bin/bin.js" - } - }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -6756,14 +6558,6 @@ "version": "2.1.1", "license": "MIT" }, - "node_modules/min-indent": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", @@ -7150,38 +6944,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/pretty-format": { - "version": "27.5.1", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/pretty-format/node_modules/react-is": { - "version": "17.0.2", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -7325,18 +7087,6 @@ "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/redent": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", @@ -7977,17 +7727,6 @@ "node": ">=10" } }, - "node_modules/strip-indent": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -8878,6 +8617,31 @@ } } }, + "node_modules/vitest-browser-react": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/vitest-browser-react/-/vitest-browser-react-2.2.0.tgz", + "integrity": "sha512-oY3KM6305kwJMa6nHo92vVtkOsih7mjEf12dLKuphaF+9ywWPEc+qanIBd394SZ6m5LadVEaG6dicvvizOzmjA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@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", + "vitest": "^4.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", @@ -9268,12 +9032,6 @@ } }, "dependencies": { - "@adobe/css-tools": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz", - "integrity": "sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==", - "dev": true - }, "@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -11407,65 +11165,6 @@ "tailwindcss": "4.3.3" } }, - "@testing-library/dom": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", - "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", - "dev": true, - "peer": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" - }, - "dependencies": { - "chalk": { - "version": "4.1.2", - "dev": true, - "peer": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - } - } - }, - "@testing-library/jest-dom": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.0.tgz", - "integrity": "sha512-HKAH9C6mBo5yBG6yRO5i43L2iisencAo5z+o5P/saHUoY+miC5ivXRxHBJcFyB5ypPNxHJdK3BoF/3O4DIptMg==", - "dev": true, - "requires": { - "@adobe/css-tools": "^4.4.0", - "aria-query": "^5.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.6.3", - "picocolors": "^1.1.1", - "redent": "^3.0.0" - }, - "dependencies": { - "dom-accessibility-api": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", - "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", - "dev": true - } - } - }, - "@testing-library/react": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", - "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", - "dev": true, - "requires": { - "@babel/runtime": "^7.12.5" - } - }, "@total-typescript/shoehorn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@total-typescript/shoehorn/-/shoehorn-0.1.2.tgz", @@ -11484,13 +11183,6 @@ "string.prototype.matchall": "^4.0.12" } }, - "@types/aria-query": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz", - "integrity": "sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==", - "dev": true, - "peer": true - }, "@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -11886,30 +11578,6 @@ "integrity": "sha512-XleryMhbuksdKtofnWZ9Sk+4CUTbms4Mb/EU32SZwToAyZ5RgVos/ki8n+yr0LWHOGKuakbXTuuYNHLQjhddgg==", "dev": true }, - "ansi-regex": { - "version": "5.0.1", - "dev": true, - "peer": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "peer": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "requires": { - "dequal": "^2.0.3" - } - }, "array-buffer-byte-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", @@ -12122,23 +11790,6 @@ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "peer": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "peer": true - }, "common-tags": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", @@ -12197,10 +11848,6 @@ "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", "dev": true }, - "css.escape": { - "version": "1.5.1", - "dev": true - }, "csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -12372,22 +12019,11 @@ "object-keys": "^1.1.1" } }, - "dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true - }, "detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==" }, - "dom-accessibility-api": { - "version": "0.5.13", - "dev": true, - "peer": true - }, "dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -12868,10 +12504,6 @@ "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.16.tgz", "integrity": "sha512-Xs7H9rBc+kti1J6RueUvbEBkmOz7jqj11XYgf+YMXAYzu8EeE7hwZ9poLXdVfVnGmJu7QAf41T7H2KuF6QoK6Q==" }, - "indent-string": { - "version": "4.0.0", - "dev": true - }, "internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -13336,13 +12968,6 @@ "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", "dev": true }, - "lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, - "peer": true - }, "magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -13380,10 +13005,6 @@ "merge": { "version": "2.1.1" }, - "min-indent": { - "version": "1.0.1", - "dev": true - }, "minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", @@ -13611,28 +13232,6 @@ "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", "dev": true }, - "pretty-format": { - "version": "27.5.1", - "dev": true, - "peer": true, - "requires": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "dev": true, - "peer": true - }, - "react-is": { - "version": "17.0.2", - "dev": true, - "peer": true - } - } - }, "punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -13718,14 +13317,6 @@ "victory-vendor": "^37.0.2" } }, - "redent": { - "version": "3.0.0", - "dev": true, - "requires": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - } - }, "redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", @@ -14188,13 +13779,6 @@ "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", "dev": true }, - "strip-indent": { - "version": "3.0.0", - "dev": true, - "requires": { - "min-indent": "^1.0.0" - } - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -14649,6 +14233,13 @@ "why-is-node-running": "^2.3.0" } }, + "vitest-browser-react": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/vitest-browser-react/-/vitest-browser-react-2.2.0.tgz", + "integrity": "sha512-oY3KM6305kwJMa6nHo92vVtkOsih7mjEf12dLKuphaF+9ywWPEc+qanIBd394SZ6m5LadVEaG6dicvvizOzmjA==", + "dev": true, + "requires": {} + }, "webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", diff --git a/package.json b/package.json index 84cc07c4..d0376356 100644 --- a/package.json +++ b/package.json @@ -57,8 +57,6 @@ "devDependencies": { "@shlinkio/eslint-config-js-coding-standard": "~4.1.0", "@tailwindcss/vite": "^4.3.3", - "@testing-library/jest-dom": "^7.0.0", - "@testing-library/react": "^16.3.2", "@total-typescript/shoehorn": "^0.1.2", "@types/node": "^26.1.2", "@types/react": "^19.2.18", @@ -78,7 +76,8 @@ "typescript": "^7.0.2", "vite": "^8.2.0", "vite-plugin-pwa": "^1.3.0", - "vitest": "^4.0.3" + "vitest": "^4.0.3", + "vitest-browser-react": "^2.2.0" }, "browserslist": [ ">0.2%", diff --git a/test/__helpers__/setUpTest.tsx b/test/__helpers__/setUpTest.tsx index d04e7936..ff4f2760 100644 --- a/test/__helpers__/setUpTest.tsx +++ b/test/__helpers__/setUpTest.tsx @@ -1,17 +1,19 @@ import type { ShlinkApiClient } from '@shlinkio/shlink-js-sdk'; -import type { RenderOptions } from '@testing-library/react'; -import { render } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import type { PropsWithChildren, ReactElement } from 'react'; import { Provider } from 'react-redux'; +import type { RenderOptions } from 'vitest-browser-react'; +import { render as vitestRender } from 'vitest-browser-react'; import { userEvent } from 'vitest/browser'; import { ContainerProvider } from '../../src/container/context'; import type { RootState } from '../../src/store'; import { setUpStore } from '../../src/store'; -export const renderWithEvents = (element: ReactElement, options?: RenderOptions) => ({ +export const render = vitestRender; + +export const renderWithEvents = async (element: ReactElement, options?: RenderOptions) => ({ user: userEvent.setup(), - ...render(element, options), + ...(await render(element, options)), }); export type RenderOptionsWithState = Omit & { @@ -32,7 +34,7 @@ export type RenderOptionsWithState = Omit & { * Render provided ReactElement wrapped in a redux `Provider` and a `ContainerProvider` with a single * `buildShlinkApiClient` dependency. */ -export const renderWithStore = ( +export const renderWithStore = async ( element: ReactElement, { initialState = {}, buildShlinkApiClient = vi.fn(), ...options }: RenderOptionsWithState = {}, ) => { @@ -45,6 +47,6 @@ export const renderWithStore = ( return { store, - ...renderWithEvents(element, { ...options, wrapper: Wrapper }), + ...(await renderWithEvents(element, { ...options, wrapper: Wrapper })), }; }; diff --git a/test/app/App.test.tsx b/test/app/App.test.tsx index b768994f..f8e1ac70 100644 --- a/test/app/App.test.tsx +++ b/test/app/App.test.tsx @@ -1,10 +1,8 @@ import type { HttpClient } from '@shlinkio/shlink-js-sdk'; -import { act, screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { MemoryRouter } from 'react-router'; import { App } from '../../src/app/App'; import { ContainerProvider } from '../../src/container/context'; -import type { ServerWithId } from '../../src/servers/data'; import { checkAccessibility } from '../__helpers__/accessibility'; import { renderWithStore } from '../__helpers__/setUpTest'; @@ -13,31 +11,29 @@ vi.mock(import('../../src/common/ShlinkWebComponentContainer'), () => ({ })); describe('', () => { - const setUp = async (activeRoute = '/') => - act(() => - renderWithStore( - - ({}), - buildShlinkApiClient: vi.fn(), - useTimeoutToggle: vi.fn().mockReturnValue([false, vi.fn()]), - })} - > - - - , - { - initialState: { - servers: { - abc123: fromPartial({ id: 'abc123', name: 'abc123 server' }), - def456: fromPartial({ id: 'def456', name: 'def456 server' }), - }, - settings: fromPartial({}), - appUpdated: false, + const setUp = (activeRoute = '/') => + renderWithStore( + + ({}), + buildShlinkApiClient: vi.fn(), + useTimeoutToggle: vi.fn().mockReturnValue([false, vi.fn()]), + })} + > + + + , + { + initialState: { + servers: { + abc123: fromPartial({ id: 'abc123', name: 'abc123 server' }), + def456: fromPartial({ id: 'def456', name: 'def456 server' }), }, + settings: fromPartial({}), + appUpdated: false, }, - ), + }, ); it('passes a11y checks', () => checkAccessibility(setUp())); @@ -53,8 +49,8 @@ describe('', () => { ['/server/def456/bar', 'ShlinkWebComponentContainer'], ['/other', 'Oops! We could not find requested route.'], ])('renders expected route', async (activeRoute, expectedComponent) => { - await setUp(activeRoute); - expect(screen.getByText(expectedComponent)).toBeInTheDocument(); + const screen = await setUp(activeRoute); + await expect.element(screen.getByText(expectedComponent)).toBeInTheDocument(); }); it.each([ @@ -62,13 +58,13 @@ describe('', () => { ['/bar', false], ['/', true], ])('renders expected classes on shlink-wrapper based on current pathname', async (pathname, isFlex) => { - await setUp(pathname); + const screen = await setUp(pathname); const shlinkWrapper = screen.getByTestId('shlink-wrapper'); if (isFlex) { - expect(shlinkWrapper).toHaveClass('flex'); + await expect.element(shlinkWrapper).toHaveClass('flex'); } else { - expect(shlinkWrapper).not.toHaveClass('flex'); + await expect.element(shlinkWrapper).not.toHaveClass('flex'); } }); }); diff --git a/test/common/AppUpdateBanner.test.tsx b/test/common/AppUpdateBanner.test.tsx index 24af528d..782d70e6 100644 --- a/test/common/AppUpdateBanner.test.tsx +++ b/test/common/AppUpdateBanner.test.tsx @@ -1,4 +1,3 @@ -import { act, screen, waitFor } from '@testing-library/react'; import { AppUpdateBanner } from '../../src/common/AppUpdateBanner'; import { checkAccessibility } from '../__helpers__/accessibility'; import { renderWithEvents } from '../__helpers__/setUpTest'; @@ -6,27 +5,20 @@ import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { const onClose = vi.fn(); const forceUpdate = vi.fn(); - const setUp = async () => { - const result = await act(() => - renderWithEvents(), - ); - await waitFor(() => screen.getByRole('alert')); - - return result; - }; + const setUp = () => renderWithEvents(); it('passes a11y checks', () => checkAccessibility(setUp())); it('renders initial state', async () => { - await setUp(); + const screen = await setUp(); - expect(screen.getByRole('heading')).toHaveTextContent('This app has just been updated!'); - expect(screen.queryByText('Restarting...')).not.toBeInTheDocument(); - expect(screen.getByText('Restart now')).not.toHaveAttribute('disabled'); + await expect.element(screen.getByRole('heading')).toHaveTextContent('This app has just been updated!'); + await expect.element(screen.getByText('Restarting...')).not.toBeInTheDocument(); + await expect.element(screen.getByText('Restart now')).not.toHaveAttribute('disabled'); }); it('invokes toggle when alert is closed', async () => { - const { user } = await setUp(); + const { user, ...screen } = await setUp(); expect(onClose).not.toHaveBeenCalled(); await user.click(screen.getByLabelText('Close')); @@ -34,12 +26,12 @@ describe('', () => { }); it('triggers the update when clicking the button', async () => { - const { user } = await setUp(); + const { user, ...screen } = await setUp(); expect(forceUpdate).not.toHaveBeenCalled(); await user.click(screen.getByText(/^Restart now/)); expect(forceUpdate).toHaveBeenCalled(); - expect(await screen.findByText('Restarting...')).toBeInTheDocument(); - expect(screen.queryByText(/^Restart now/)).not.toBeInTheDocument(); + await expect.element(screen.getByText('Restarting...')).toBeInTheDocument(); + await expect.element(screen.getByText(/^Restart now/)).not.toBeInTheDocument(); }); }); diff --git a/test/common/ErrorHandler.test.tsx b/test/common/ErrorHandler.test.tsx index 2bbd85e2..76337a6c 100644 --- a/test/common/ErrorHandler.test.tsx +++ b/test/common/ErrorHandler.test.tsx @@ -1,7 +1,6 @@ -import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; -import type { PropsWithChildren, ReactNode } from 'react'; -import { ErrorHandler as BaseErrorHandler } from '../../src/common/ErrorHandler'; +import type { ReactNode } from 'react'; +import { ErrorHandler } from '../../src/common/ErrorHandler'; import { checkAccessibility } from '../__helpers__/accessibility'; import { renderWithEvents } from '../__helpers__/setUpTest'; @@ -13,8 +12,12 @@ describe('', () => { const reload = vi.fn(); const location = fromPartial({ reload }); const cons = fromPartial({ error: vi.fn() }); - const ErrorHandler = (props: PropsWithChildren) => ; - const setUp = (children: ReactNode = 'Error') => renderWithEvents({children}); + const setUp = (children: ReactNode = 'Error') => + renderWithEvents( + + {children} + , + ); beforeEach(() => { vi.spyOn(console, 'error').mockImplementation(() => {}); // Silence react errors @@ -22,23 +25,23 @@ describe('', () => { it('passes a11y checks', () => checkAccessibility(setUp())); - it('renders children when no error has occurred', () => { - setUp(Foo); + it('renders children when no error has occurred', async () => { + const screen = await setUp(Foo); - expect(screen.getByText('Foo')).toBeInTheDocument(); - expect(screen.queryByText('Oops! This is awkward :S')).not.toBeInTheDocument(); - expect(screen.queryByRole('button')).not.toBeInTheDocument(); + await expect.element(screen.getByText('Foo')).toBeInTheDocument(); + await expect.element(screen.getByText('Oops! This is awkward :S')).not.toBeInTheDocument(); + await expect.element(screen.getByRole('button')).not.toBeInTheDocument(); }); - it('renders error page when error has occurred', () => { - setUp(); + it('renders error page when error has occurred', async () => { + const screen = await setUp(); - expect(screen.getByText('Oops! This is awkward :S')).toBeInTheDocument(); - expect(screen.getByRole('button')).toBeInTheDocument(); + await expect.element(screen.getByText('Oops! This is awkward :S')).toBeInTheDocument(); + await expect.element(screen.getByRole('button')).toBeInTheDocument(); }); it('reloads page on button click', async () => { - const { user } = setUp(); + const { user, ...screen } = await setUp(); expect(reload).not.toHaveBeenCalled(); await user.click(screen.getByRole('button')); diff --git a/test/common/Home.test.tsx b/test/common/Home.test.tsx index 5a2d6579..6b082465 100644 --- a/test/common/Home.test.tsx +++ b/test/common/Home.test.tsx @@ -1,4 +1,3 @@ -import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { MemoryRouter } from 'react-router'; import { Home } from '../../src/common/Home'; @@ -20,9 +19,9 @@ describe('', () => { it('passes a11y checks', () => checkAccessibility(setUp({ '1a': fromPartial({ name: 'foo', id: '1' }) }))); - it('renders title', () => { - setUp(); - expect(screen.getByRole('heading', { name: 'Welcome!' })).toBeInTheDocument(); + it('renders title', async () => { + const screen = await setUp(); + await expect.element(screen.getByRole('heading', { name: 'Welcome!' })).toBeInTheDocument(); }); it.each([ @@ -35,15 +34,17 @@ describe('', () => { 3, ], [{}, 2], - ])('shows link to create or set-up server only when no servers exist', (servers, expectedServers) => { - setUp(servers); - const links = screen.getAllByRole('link'); + ])('shows link to create or set-up server only when no servers exist', async (servers, expectedServers) => { + const screen = await setUp(servers); + const links = screen.getByRole('link').elements(); expect(links).toHaveLength(expectedServers); if (Object.keys(servers).length === 0) { - expect(screen.getByText('This application will help you manage your Shlink servers.')).toBeInTheDocument(); - expect(screen.getByText('Learn more about Shlink')).toBeInTheDocument(); + await expect + .element(screen.getByText('This application will help you manage your Shlink servers.')) + .toBeInTheDocument(); + await expect.element(screen.getByText('Learn more about Shlink')).toBeInTheDocument(); } }); }); diff --git a/test/common/MainHeader.test.tsx b/test/common/MainHeader.test.tsx index 5517d85f..1553f530 100644 --- a/test/common/MainHeader.test.tsx +++ b/test/common/MainHeader.test.tsx @@ -1,4 +1,3 @@ -import { screen } from '@testing-library/react'; import { createMemoryHistory } from 'history'; import { Router } from 'react-router'; import { MainHeader } from '../../src/common/MainHeader'; @@ -19,9 +18,9 @@ describe('', () => { it('passes a11y checks', () => checkAccessibility(setUp())); - it('renders ServersDropdown', () => { - setUp(); - expect(screen.getByRole('button', { name: 'Servers' })).toBeInTheDocument(); + it('renders ServersDropdown', async () => { + const screen = await setUp(); + await expect.element(screen.getByRole('button', { name: 'Servers' })).toBeInTheDocument(); }); it.each([ @@ -30,11 +29,10 @@ describe('', () => { ['/settings', true], ['/settings/foo', true], ['/settings/bar', true], - ])('sets link to settings as active only when current path is settings', (currentPath, isActive) => { - setUp(currentPath); - expect(screen.getByRole('menuitem', { name: /Settings$/ })).toHaveAttribute( - 'data-active', - isActive ? 'true' : 'false', - ); + ])('sets link to settings as active only when current path is settings', async (currentPath, isActive) => { + const screen = await setUp(currentPath); + await expect + .element(screen.getByRole('menuitem', { name: /Settings$/ })) + .toHaveAttribute('data-active', isActive ? 'true' : 'false'); }); }); diff --git a/test/common/NotFound.test.tsx b/test/common/NotFound.test.tsx index 32e1fe85..69e8428a 100644 --- a/test/common/NotFound.test.tsx +++ b/test/common/NotFound.test.tsx @@ -1,7 +1,7 @@ -import { render, screen } from '@testing-library/react'; import { MemoryRouter } from 'react-router'; import { NotFound } from '../../src/common/NotFound'; import { checkAccessibility } from '../__helpers__/accessibility'; +import { render } from '../__helpers__/setUpTest'; describe('', () => { const setUp = (props = {}) => @@ -13,29 +13,31 @@ describe('', () => { it('passes a11y checks', () => checkAccessibility(setUp())); - it('shows expected error title', () => { - setUp(); - expect(screen.getByText('Oops! We could not find requested route.')).toBeInTheDocument(); + it('shows expected error title', async () => { + const screen = await setUp(); + await expect.element(screen.getByText('Oops! We could not find requested route.')).toBeInTheDocument(); }); - it('shows expected error message', () => { - setUp(); - expect( - screen.getByText( - "Use your browser's back button to navigate to the page you have previously come from, or just press this button.", - ), - ).toBeInTheDocument(); + it('shows expected error message', async () => { + const screen = await setUp(); + await expect + .element( + screen.getByText( + "Use your browser's back button to navigate to the page you have previously come from, or just press this button.", + ), + ) + .toBeInTheDocument(); }); it.each([ [{}, '/', 'Home'], [{ to: '/foo/bar', children: 'Hello' }, '/foo/bar', 'Hello'], [{ to: '/baz-bar', children: <>Foo }, '/baz-bar', 'Foo'], - ])('shows expected link and text', (props, expectedLink, expectedText) => { - setUp(props); + ])('shows expected link and text', async (props, expectedLink, expectedText) => { + const screen = await setUp(props); const link = screen.getByRole('link'); - expect(link).toHaveAttribute('href', expectedLink); - expect(link).toHaveTextContent(expectedText); + await expect.element(link).toHaveAttribute('href', expectedLink); + await expect.element(link).toHaveTextContent(expectedText); }); }); diff --git a/test/common/ScrollToTop.test.tsx b/test/common/ScrollToTop.test.tsx index 5afe9416..ec49f5dd 100644 --- a/test/common/ScrollToTop.test.tsx +++ b/test/common/ScrollToTop.test.tsx @@ -1,7 +1,7 @@ -import { render, screen } from '@testing-library/react'; import { MemoryRouter } from 'react-router'; import { ScrollToTop } from '../../src/common/ScrollToTop'; import { checkAccessibility } from '../__helpers__/accessibility'; +import { render } from '../__helpers__/setUpTest.tsx'; describe('', () => { const setUp = (children = 'Foo') => @@ -13,8 +13,8 @@ describe('', () => { it('passes a11y checks', () => checkAccessibility(setUp())); - it.each([['Foobar'], ['Barfoo'], ['Something']])('just renders children', (children) => { - setUp(children); - expect(screen.getByText(children)).toBeInTheDocument(); + it.each([['Foobar'], ['Barfoo'], ['Something']])('just renders children', async (children) => { + const screen = await setUp(children); + await expect.element(screen.getByText(children)).toBeInTheDocument(); }); }); diff --git a/test/common/ShlinkVersions.test.tsx b/test/common/ShlinkVersions.test.tsx index 7cd203fb..598a4d85 100644 --- a/test/common/ShlinkVersions.test.tsx +++ b/test/common/ShlinkVersions.test.tsx @@ -1,9 +1,9 @@ -import { render, screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import type { ShlinkVersionsProps } from '../../src/common/ShlinkVersions'; import { ShlinkVersions } from '../../src/common/ShlinkVersions'; import type { NonReachableServer, NotFoundServer, ReachableServer } from '../../src/servers/data'; import { checkAccessibility } from '../__helpers__/accessibility'; +import { render } from '../__helpers__/setUpTest.tsx'; describe('', () => { const setUp = (props: ShlinkVersionsProps) => render(); @@ -19,20 +19,18 @@ describe('', () => { ['not-semver', fromPartial({ version: '1.0.0', printableVersion: 'some' }), 'latest', 'some'], ])( 'displays expected versions when selected server is reachable', - (clientVersion, selectedServer, expectedClientVersion, expectedServerVersion) => { - setUp({ clientVersion, selectedServer }); - const [serverLink, clientLink] = screen.getAllByRole('link'); + async (clientVersion, selectedServer, expectedClientVersion, expectedServerVersion) => { + const screen = await setUp({ clientVersion, selectedServer }); + const [serverLink, clientLink] = screen.getByRole('link').all(); - expect(serverLink).toHaveAttribute( - 'href', - `https://github.com/shlinkio/shlink/releases/${expectedServerVersion}`, - ); - expect(serverLink).toHaveTextContent(expectedServerVersion); - expect(clientLink).toHaveAttribute( - 'href', - `https://github.com/shlinkio/shlink-web-client/releases/${expectedClientVersion}`, - ); - expect(clientLink).toHaveTextContent(expectedClientVersion); + await expect + .element(serverLink) + .toHaveAttribute('href', `https://github.com/shlinkio/shlink/releases/${expectedServerVersion}`); + await expect.element(serverLink).toHaveTextContent(expectedServerVersion); + await expect + .element(clientLink) + .toHaveAttribute('href', `https://github.com/shlinkio/shlink-web-client/releases/${expectedClientVersion}`); + await expect.element(clientLink).toHaveTextContent(expectedClientVersion); }, ); @@ -40,11 +38,13 @@ describe('', () => { ['1.2.3', null], ['1.2.3', fromPartial({ serverNotFound: true })], ['1.2.3', fromPartial({ serverNotReachable: true })], - ])('displays only client version when selected server is not reachable', (clientVersion, selectedServer) => { - setUp({ clientVersion, selectedServer }); - const links = screen.getAllByRole('link'); + ])('displays only client version when selected server is not reachable', async (clientVersion, selectedServer) => { + const screen = await setUp({ clientVersion, selectedServer }); + const links = screen.getByRole('link').all(); expect(links).toHaveLength(1); - expect(links[0]).toHaveAttribute('href', 'https://github.com/shlinkio/shlink-web-client/releases/v1.2.3'); + await expect + .element(links[0]) + .toHaveAttribute('href', 'https://github.com/shlinkio/shlink-web-client/releases/v1.2.3'); }); }); diff --git a/test/common/ShlinkVersionsContainer.test.tsx b/test/common/ShlinkVersionsContainer.test.tsx index dabdeeb8..7809335e 100644 --- a/test/common/ShlinkVersionsContainer.test.tsx +++ b/test/common/ShlinkVersionsContainer.test.tsx @@ -20,13 +20,13 @@ describe('', () => { [null, false], [fromPartial({}), false], [fromPartial({ version: '1.0.0' }), true], - ])('renders proper col classes based on sidebar status', (selectedServer, shouldAddMargin) => { - const { container } = setUp(selectedServer); + ])('renders proper col classes based on sidebar status', async (selectedServer, shouldAddMargin) => { + const { container } = await setUp(selectedServer); if (shouldAddMargin) { - expect(container.firstChild).toHaveClass('md:ml-(--aside-menu-width)'); + await expect.element(container.firstChild as HTMLElement).toHaveClass('md:ml-(--aside-menu-width)'); } else { - expect(container.firstChild).not.toHaveClass('md:ml-(--aside-menu-width)'); + await expect.element(container.firstChild as HTMLElement).not.toHaveClass('md:ml-(--aside-menu-width)'); } }); }); diff --git a/test/common/ShlinkWebComponentContainer.test.tsx b/test/common/ShlinkWebComponentContainer.test.tsx index f892f128..6d5eb707 100644 --- a/test/common/ShlinkWebComponentContainer.test.tsx +++ b/test/common/ShlinkWebComponentContainer.test.tsx @@ -1,4 +1,3 @@ -import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { MemoryRouter } from 'react-router'; import { ShlinkWebComponentContainer } from '../../src/common/ShlinkWebComponentContainer'; @@ -25,11 +24,11 @@ describe('', () => { it('passes a11y checks', () => checkAccessibility(setUp(fromPartial({ version: '3.0.0' })))); - it('shows loading indicator while loading server', () => { - setUp(null); + it('shows loading indicator while loading server', async () => { + const screen = await setUp(null); - expect(screen.getByText('Loading...')).toBeInTheDocument(); - expect(screen.queryByText('ShlinkWebComponent')).not.toBeInTheDocument(); + await expect.element(screen.getByText('Loading...')).toBeInTheDocument(); + await expect.element(screen.getByText('ShlinkWebComponent')).not.toBeInTheDocument(); }); it.each([ @@ -38,18 +37,18 @@ describe('', () => { fromPartial({ id: 'foo', serverNotReachable: true }), /Could not connect to this Shlink server/, ], - ])('shows error for non reachable servers', (selectedServer, expectedError) => { - setUp(selectedServer); + ])('shows error for non reachable servers', async (selectedServer, expectedError) => { + const screen = await setUp(selectedServer); - expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); - expect(screen.getByText(expectedError)).toBeInTheDocument(); - expect(screen.queryByText('ShlinkWebComponent')).not.toBeInTheDocument(); + await expect.element(screen.getByText('Loading...')).not.toBeInTheDocument(); + await expect.element(screen.getByText(expectedError)).toBeInTheDocument(); + await expect.element(screen.getByText('ShlinkWebComponent')).not.toBeInTheDocument(); }); - it('renders ShlinkWebComponent for reachable servers', () => { - setUp(fromPartial({ version: '3.0.0' })); + it('renders ShlinkWebComponent for reachable servers', async () => { + const screen = await setUp(fromPartial({ version: '3.0.0' })); - expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); - expect(screen.getByText('ShlinkWebComponent')).toBeInTheDocument(); + await expect.element(screen.getByText('Loading...')).not.toBeInTheDocument(); + await expect.element(screen.getByText('ShlinkWebComponent')).toBeInTheDocument(); }); }); diff --git a/test/common/img/ShlinkLogo.test.tsx b/test/common/img/ShlinkLogo.test.tsx index 4b4fbd55..78c36442 100644 --- a/test/common/img/ShlinkLogo.test.tsx +++ b/test/common/img/ShlinkLogo.test.tsx @@ -1,8 +1,8 @@ import { brandColor } from '@shlinkio/shlink-frontend-kit'; -import { render } from '@testing-library/react'; import type { ShlinkLogoProps } from '../../../src/common/img/ShlinkLogo'; import { ShlinkLogo } from '../../../src/common/img/ShlinkLogo'; import { checkAccessibility } from '../../__helpers__/accessibility'; +import { render } from '../../__helpers__/setUpTest.tsx'; describe('', () => { const setUp = (props: ShlinkLogoProps = {}) => render(); @@ -13,22 +13,22 @@ describe('', () => { [undefined, brandColor()], ['red', 'red'], ['white', 'white'], - ])('renders expected color', (color, expectedColor) => { - const { container } = setUp({ color }); - expect(container.querySelector('g')).toHaveAttribute('fill', expectedColor); + ])('renders expected color', async (color, expectedColor) => { + const { container } = await setUp({ color }); + await expect.element(container.querySelector('g')).toHaveAttribute('fill', expectedColor); }); it.each([ [undefined, undefined], ['foo', 'foo'], ['bar', 'bar'], - ])('renders expected class', (className, expectedClassName) => { - const { container } = setUp({ className }); + ])('renders expected class', async (className, expectedClassName) => { + const { container } = await setUp({ className }); if (expectedClassName) { - expect(container.firstChild).toHaveAttribute('class', expectedClassName); + await expect.element(container.firstChild as HTMLElement).toHaveAttribute('class', expectedClassName); } else { - expect(container.firstChild).not.toHaveAttribute('class'); + await expect.element(container.firstChild as HTMLElement).not.toHaveAttribute('class'); } }); }); diff --git a/test/container/context.test.tsx b/test/container/context.test.tsx index bdcfb4ba..51c74964 100644 --- a/test/container/context.test.tsx +++ b/test/container/context.test.tsx @@ -1,6 +1,6 @@ -import { render } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { ContainerProvider, useDependencies } from '../../src/container/context'; +import { render } from '../__helpers__/setUpTest.tsx'; describe('context', () => { describe('useDependencies', () => { @@ -12,24 +12,24 @@ describe('context', () => { return null; } - it('throws when used outside of ContainerProvider', () => { - expect(() => render()).toThrowError( + it('throws when used outside of ContainerProvider', async () => { + await expect(() => render()).rejects.toThrow( 'You cannot use "useDependencies" outside of a ContainerProvider', ); }); - it('throws when requested dependency is not found in container', () => { - expect(() => + it('throws when requested dependency is not found in container', async () => { + await expect(() => render( , ), - ).toThrowError('Dependency with name "foo" not found in container'); + ).rejects.toThrow('Dependency with name "foo" not found in container'); }); - it('gets dependency from container', () => { - render( + it('gets dependency from container', async () => { + await render( , diff --git a/test/servers/CreateServer.test.tsx b/test/servers/CreateServer.test.tsx index a1d4cb5c..0a2d244b 100644 --- a/test/servers/CreateServer.test.tsx +++ b/test/servers/CreateServer.test.tsx @@ -1,4 +1,3 @@ -import { fireEvent, screen, waitFor } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { createMemoryHistory } from 'history'; import { Router } from 'react-router'; @@ -17,7 +16,11 @@ describe('', () => { const defaultServers: ServersMap = { foo: fromPartial({ url: 'https://existing_url.com', apiKey: 'existing_api_key', id: 'foo' }), }; - const setUp = ({ serversImported = false, importFailed = false, servers = defaultServers }: SetUpOptions = {}) => { + const setUp = async ({ + serversImported = false, + importFailed = false, + servers = defaultServers, + }: SetUpOptions = {}) => { let callCount = 0; const useTimeoutToggle = vi.fn().mockImplementation(() => { const result = [callCount % 2 === 0 ? serversImported : importFailed, () => null]; @@ -28,40 +31,44 @@ describe('', () => { return { history, - ...renderWithStore( + ...(await renderWithStore( , { initialState: { servers }, }, - ), + )), }; }; it('passes a11y checks', () => checkAccessibility(setUp())); - it('shows success message when imported is true', () => { - setUp({ serversImported: true }); + it('shows success message when imported is true', async () => { + const screen = await setUp({ serversImported: true }); - expect(screen.getByText('Servers properly imported. You can now select one from the list :)')).toBeInTheDocument(); - expect( - screen.queryByText('The servers could not be imported. Make sure the format is correct.'), - ).not.toBeInTheDocument(); - expect(screen.queryByText('ImportServersBtn')).not.toBeInTheDocument(); + await expect + .element(screen.getByText('Servers properly imported. You can now select one from the list :)')) + .toBeInTheDocument(); + await expect + .element(screen.getByText('The servers could not be imported. Make sure the format is correct.')) + .not.toBeInTheDocument(); + await expect.element(screen.getByText('ImportServersBtn')).not.toBeInTheDocument(); }); - it('shows error message when import failed', () => { - setUp({ importFailed: true }); + it('shows error message when import failed', async () => { + const screen = await setUp({ importFailed: true }); - expect( - screen.queryByText('Servers properly imported. You can now select one from the list :)'), - ).not.toBeInTheDocument(); - expect(screen.getByText('The servers could not be imported. Make sure the format is correct.')).toBeInTheDocument(); + await expect + .element(screen.getByText('Servers properly imported. You can now select one from the list :)')) + .not.toBeInTheDocument(); + await expect + .element(screen.getByText('The servers could not be imported. Make sure the format is correct.')) + .toBeInTheDocument(); }); it('creates server data when form is submitted', async () => { - const { user, history, store } = setUp(); + const { user, history, store, ...screen } = await setUp(); const expectedServerId = 'the_name-the_url.com'; await user.type(screen.getByLabelText(/^Name/), 'the_name'); @@ -69,7 +76,7 @@ describe('', () => { await user.type(screen.getByLabelText(/^API key/), 'the_api_key'); expect(store.getState().servers[expectedServerId]).not.toBeDefined(); - fireEvent.submit(screen.getByRole('form')); + await user.click(screen.getByRole('button', { name: 'Create server' })); expect(store.getState().servers[expectedServerId]).toEqual( expect.objectContaining({ id: expectedServerId, @@ -80,19 +87,19 @@ describe('', () => { ); expect(history.location.pathname).toEqual(`/server/${expectedServerId}`); - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + await expect.element(screen.getByRole('dialog')).not.toBeInTheDocument(); }); it('displays dialog when trying to create a duplicated server', async () => { - const { user, history } = setUp(); + const { user, history, ...screen } = await setUp(); await user.type(screen.getByLabelText(/^Name/), 'the_name'); await user.type(screen.getByLabelText(/^URL/), 'https://existing_url.com'); await user.type(screen.getByLabelText(/^API key/), 'existing_api_key'); - fireEvent.submit(screen.getByRole('form')); + await user.click(screen.getByRole('button', { name: 'Create server' })); - await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()); + await expect.element(screen.getByRole('dialog')).toBeInTheDocument(); await user.click(screen.getByRole('button', { name: 'Discard' })); expect(history.location.pathname).toEqual('/foo'); // Goes back to first route from history's initialEntries diff --git a/test/servers/DeleteServerButton.test.tsx b/test/servers/DeleteServerButton.test.tsx index 689b9fa6..94fbde6b 100644 --- a/test/servers/DeleteServerButton.test.tsx +++ b/test/servers/DeleteServerButton.test.tsx @@ -1,4 +1,3 @@ -import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { createMemoryHistory } from 'history'; import type { ReactNode } from 'react'; @@ -8,9 +7,9 @@ import { checkAccessibility } from '../__helpers__/accessibility'; import { renderWithStore } from '../__helpers__/setUpTest'; describe('', () => { - const setUp = (children: ReactNode = 'Remove this server') => { + const setUp = async (children: ReactNode = 'Remove this server') => { const history = createMemoryHistory({ initialEntries: ['/foo'] }); - const result = renderWithStore( + const result = await renderWithStore( {children} , @@ -21,22 +20,23 @@ describe('', () => { it('passes a11y checks', () => checkAccessibility(setUp('Delete me'))); - it.each([['Foo bar'], ['baz'], ['something']])('renders expected content', (children) => { - const { container } = setUp(children); + it.each([['Foo bar'], ['baz'], ['something']])('renders expected content', async (children) => { + const { container } = await setUp(children); + expect(container.firstChild).toBeTruthy(); expect(container.firstChild).toMatchSnapshot(); }); it('displays modal when button is clicked', async () => { - const { user } = setUp(); + const { user, ...screen } = await setUp(); - expect(screen.queryByText(/Are you sure you want to remove/)).not.toBeInTheDocument(); + await expect.element(screen.getByText(/Are you sure you want to remove/)).not.toBeInTheDocument(); await user.click(screen.getByText('Remove this server')); - expect(screen.getByText(/Are you sure you want to remove/)).toBeInTheDocument(); + await expect.element(screen.getByText(/Are you sure you want to remove/)).toBeInTheDocument(); }); it('navigates to home when deletion is confirmed', async () => { - const { user, history } = setUp(); + const { user, history, ...screen } = await setUp(); // Open modal await user.click(screen.getByText('Remove this server')); diff --git a/test/servers/DeleteServerModal.test.tsx b/test/servers/DeleteServerModal.test.tsx index b6e9560c..bf635bfb 100644 --- a/test/servers/DeleteServerModal.test.tsx +++ b/test/servers/DeleteServerModal.test.tsx @@ -1,5 +1,5 @@ -import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; +import type { RenderResult } from 'vitest-browser-react'; import type { ServerWithId } from '../../src/servers/data'; import { DeleteServerModal } from '../../src/servers/DeleteServerModal'; import { checkAccessibility } from '../__helpers__/accessibility'; @@ -18,36 +18,36 @@ describe('', () => { it('passes a11y checks', () => checkAccessibility(setUp())); - it('renders a modal window', () => { - setUp(); + it('renders a modal window', async () => { + const screen = await setUp(); - expect(screen.getByRole('dialog')).toBeInTheDocument(); - expect(screen.getByRole('heading')).toHaveTextContent('Remove server'); + await expect.element(screen.getByRole('dialog')).toBeInTheDocument(); + await expect.element(screen.getByRole('heading')).toHaveTextContent('Remove server'); }); - it('displays the name of the server as part of the content', () => { - setUp(); + it('displays the name of the server as part of the content', async () => { + const screen = await setUp(); - expect(screen.getByText(/^Are you sure you want to remove/)).toBeInTheDocument(); - expect(screen.getByText(serverName)).toBeInTheDocument(); + await expect.element(screen.getByText(/^Are you sure you want to remove/)).toBeInTheDocument(); + await expect.element(screen.getByText(serverName)).toBeInTheDocument(); }); - it.each([[() => screen.getByRole('button', { name: 'Cancel' })], [() => screen.getByLabelText('Close dialog')]])( - 'closes dialog when clicking cancel button', - async (getButton) => { - const { user, store } = setUp(); + it.each([ + [(screen: RenderResult) => screen.getByRole('button', { name: 'Cancel' })], + [(screen: RenderResult) => screen.getByLabelText('Close dialog')], + ])('closes dialog when clicking cancel button', async (getButton) => { + const { user, store, ...screen } = await setUp(); - expect(screen.getByRole('dialog')).toBeInTheDocument(); - await user.click(getButton()); - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + await expect.element(screen.getByRole('dialog')).toBeInTheDocument(); + await user.click(getButton(screen)); + await expect.element(screen.getByRole('dialog')).not.toBeInTheDocument(); - // No server has been deleted - expect(Object.keys(store.getState().servers)).toHaveLength(1); - }, - ); + // No server has been deleted + expect(Object.keys(store.getState().servers)).toHaveLength(1); + }); it('deletes server when clicking accept button', async () => { - const { user, store } = setUp(); + const { user, store, ...screen } = await setUp(); expect(Object.keys(store.getState().servers)).toHaveLength(1); await user.click(screen.getByRole('button', { name: 'Delete' })); diff --git a/test/servers/EditServer.test.tsx b/test/servers/EditServer.test.tsx index 86744176..a5cbfe62 100644 --- a/test/servers/EditServer.test.tsx +++ b/test/servers/EditServer.test.tsx @@ -1,4 +1,3 @@ -import { fireEvent, screen, waitFor } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { createMemoryHistory } from 'history'; import { Router } from 'react-router'; @@ -12,14 +11,14 @@ describe('', () => { const defaultSelectedServer = fromPartial({ id: 'abc123', name: 'the_name', - url: 'the_url', + url: 'https://example.com', apiKey: 'the_api_key', }); - const setUp = (selectedServer: SelectedServer = defaultSelectedServer) => { + const setUp = async (selectedServer: SelectedServer = defaultSelectedServer) => { const history = createMemoryHistory({ initialEntries: ['/foo', '/bar'] }); return { history, - ...renderWithStore( + ...(await renderWithStore( , @@ -29,46 +28,42 @@ describe('', () => { servers: isServerWithId(selectedServer) ? { [selectedServer.id]: selectedServer } : {}, }, }, - ), + )), }; }; it('passes a11y checks', () => checkAccessibility(setUp())); - it('renders nothing if selected server is not reachable', () => { - setUp(fromPartial({})); + it('renders nothing if selected server is not reachable', async () => { + const screen = await setUp(fromPartial({})); - expect(screen.queryByText('Edit')).not.toBeInTheDocument(); - expect(screen.queryByText('Cancel')).not.toBeInTheDocument(); - expect(screen.queryByText('Save')).not.toBeInTheDocument(); + await expect.element(screen.getByText('Edit')).not.toBeInTheDocument(); + await expect.element(screen.getByText('Cancel')).not.toBeInTheDocument(); + await expect.element(screen.getByText('Save')).not.toBeInTheDocument(); }); - it('renders server title', () => { - setUp(); + it('renders server title', async () => { + const screen = await setUp(); expect(screen.getByText(`Edit "${defaultSelectedServer.name}"`)).toBeInTheDocument(); }); - it('display the server info in the form components', () => { - setUp(); + it('display the server info in the form components', async () => { + const screen = await setUp(); - expect(screen.getByLabelText(/^Name/)).toBeInTheDocument(); - expect(screen.getByLabelText(/^URL/)).toBeInTheDocument(); - expect(screen.getByLabelText(/^API key/)).toBeInTheDocument(); + await expect.element(screen.getByLabelText(/^Name/)).toBeInTheDocument(); + await expect.element(screen.getByLabelText(/^URL/)).toBeInTheDocument(); + await expect.element(screen.getByLabelText(/^API key/)).toBeInTheDocument(); }); it('edits server and redirects to it when form is submitted', async () => { - const { user, history, store } = setUp(); + const { user, history, store, ...screen } = await setUp(); await user.type(screen.getByLabelText(/^Name/), ' edited'); - await user.type(screen.getByLabelText(/^URL/), ' edited'); - // TODO Using fire event because userEvent.click on the Submit button does not submit the form - // await user.click(screen.getByRole('button', { name: 'Save' })); - fireEvent.submit(screen.getByRole('form')); + await user.click(screen.getByRole('button', { name: 'Save' })); expect(store.getState().servers[defaultSelectedServer.id]).toEqual( expect.objectContaining({ name: 'the_name edited', - url: 'the_url edited', }), ); @@ -79,18 +74,16 @@ describe('', () => { it.each([{ forwardCredentials: true }, { forwardCredentials: false }])( 'edits advanced options - forward credentials', async ({ forwardCredentials }) => { - const { user, store } = setUp({ ...defaultSelectedServer, forwardCredentials }); + const { user, store, ...screen } = await setUp({ ...defaultSelectedServer, forwardCredentials }); await user.click(screen.getByText('Advanced options')); await user.click(screen.getByLabelText('Forward credentials to this server on every request.')); - fireEvent.submit(screen.getByRole('form')); + await user.click(screen.getByRole('button', { name: 'Save' })); - await waitFor(() => - expect(store.getState().servers[defaultSelectedServer.id]).toEqual( - expect.objectContaining({ - forwardCredentials: !forwardCredentials, - }), - ), + expect(store.getState().servers[defaultSelectedServer.id]).toEqual( + expect.objectContaining({ + forwardCredentials: !forwardCredentials, + }), ); }, ); diff --git a/test/servers/ManageServers.test.tsx b/test/servers/ManageServers.test.tsx index 7d63d57c..ddfffd9d 100644 --- a/test/servers/ManageServers.test.tsx +++ b/test/servers/ManageServers.test.tsx @@ -1,4 +1,3 @@ -import { screen, waitFor } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { MemoryRouter } from 'react-router'; import type { ServersMap, ServerWithId } from '../../src/servers/data'; @@ -33,77 +32,77 @@ describe('', () => { )); it('shows search field which allows searching servers, affecting te amount of rendered rows', async () => { - const { user } = setUp({ + const { user, ...screen } = await setUp({ foo: createServerMock('foo'), bar: createServerMock('bar'), baz: createServerMock('baz'), }); const search = async (searchTerm: string) => { - await user.clear(screen.getByPlaceholderText('Search...')); - await user.type(screen.getByPlaceholderText('Search...'), searchTerm); + await user.clear(screen.getByPlaceholder('Search...')); + await user.type(screen.getByPlaceholder('Search...'), searchTerm); }; // Add one for the header row - const expectRows = (amount: number) => expect(screen.getAllByRole('row')).toHaveLength(amount + 1); + const expectRows = (amount: number) => + expect.poll(() => screen.getByRole('row').elements()).toHaveLength(amount + 1); - expectRows(3); - expect(screen.queryByText('No servers found.')).not.toBeInTheDocument(); + await expectRows(3); + await expect.element(screen.getByText('No servers found.')).not.toBeInTheDocument(); await search('foo'); - await waitFor(() => expectRows(1)); - expect(screen.queryByText('No servers found.')).not.toBeInTheDocument(); + await expectRows(1); + await expect.element(screen.getByText('No servers found.')).not.toBeInTheDocument(); await search('Ba'); - await waitFor(() => expectRows(2)); - expect(screen.queryByText('No servers found.')).not.toBeInTheDocument(); + await expectRows(2); + await expect.element(screen.getByText('No servers found.')).not.toBeInTheDocument(); await search('invalid'); - await waitFor(() => expectRows(1)); - expect(screen.getByText('No servers found.')).toBeInTheDocument(); + await expectRows(1); + await expect.element(screen.getByText('No servers found.')).toBeInTheDocument(); }); - it.each([ - [createServerMock('foo'), 3], - [createServerMock('foo', true), 4], - ])('shows different amount of columns if there are at least one auto-connect server', (server, expectedCols) => { - setUp({ server }); + it.each([[createServerMock('foo')], [createServerMock('foo', true)]])( + 'shows different amount of columns if there are at least one auto-connect server', + async (server) => { + const screen = await setUp({ server }); - expect(screen.getAllByRole('columnheader')).toHaveLength(expectedCols); - if (server.autoConnect) { - expect(screen.getByTestId('auto-connect')).toBeInTheDocument(); - } else { - expect(screen.queryByTestId('auto-connect')).not.toBeInTheDocument(); - } - }); + if (server.autoConnect) { + await expect.element(screen.getByTestId('auto-connect')).toBeInTheDocument(); + } else { + await expect.element(screen.getByTestId('auto-connect')).not.toBeInTheDocument(); + } + }, + ); it.each([ [{}, 0], [{ foo: createServerMock('foo') }, 1], - ])('shows export button if the list of servers is not empty', (servers, expectedButtons) => { - setUp(servers); - expect(screen.queryAllByRole('button', { name: 'Export servers' })).toHaveLength(expectedButtons); + ])('shows export button if the list of servers is not empty', async (servers, expectedButtons) => { + const screen = await setUp(servers); + expect(screen.getByRole('button', { name: 'Export servers' }).elements()).toHaveLength(expectedButtons); }); it('allows exporting servers when clicking on button', async () => { - const { user } = setUp({ foo: createServerMock('foo') }); + const { user, ...screen } = await setUp({ foo: createServerMock('foo') }); expect(exportServers).not.toHaveBeenCalled(); await user.click(screen.getByRole('button', { name: 'Export servers' })); expect(exportServers).toHaveBeenCalled(); }); - it.each([[true], [false]])('shows an error message if an error occurs while importing servers', (hasError) => { + it.each([[true], [false]])('shows an error message if an error occurs while importing servers', async (hasError) => { useTimeoutToggle.mockReturnValue([hasError, vi.fn()]); - setUp({ foo: createServerMock('foo') }); + const screen = await setUp({ foo: createServerMock('foo') }); if (hasError) { - expect( - screen.getByText('The servers could not be imported. Make sure the format is correct.'), - ).toBeInTheDocument(); + await expect + .element(screen.getByText('The servers could not be imported. Make sure the format is correct.')) + .toBeInTheDocument(); } else { - expect( - screen.queryByText('The servers could not be imported. Make sure the format is correct.'), - ).not.toBeInTheDocument(); + await expect + .element(screen.getByText('The servers could not be imported. Make sure the format is correct.')) + .not.toBeInTheDocument(); } }); }); diff --git a/test/servers/ManageServersRow.test.tsx b/test/servers/ManageServersRow.test.tsx index 4bd81b80..e31d2052 100644 --- a/test/servers/ManageServersRow.test.tsx +++ b/test/servers/ManageServersRow.test.tsx @@ -1,5 +1,4 @@ import { Table } from '@shlinkio/shlink-frontend-kit'; -import { screen } from '@testing-library/react'; import { MemoryRouter } from 'react-router'; import type { ServerWithId } from '../../src/servers/data'; import { ManageServersRow } from '../../src/servers/ManageServersRow'; @@ -27,28 +26,27 @@ describe('', () => { it.each([ [true, 4], [false, 3], - ])('renders expected amount of columns', (hasAutoConnect, expectedCols) => { - setUp(hasAutoConnect); - expect(screen.getAllByRole('cell')).toHaveLength(expectedCols); + ])('renders expected amount of columns', async (hasAutoConnect, expectedCols) => { + const screen = await setUp(hasAutoConnect); + expect(screen.getByRole('cell').elements()).toHaveLength(expectedCols); }); - it('renders an options dropdown', () => { - setUp(); - expect(screen.getByRole('button', { name: 'Options' })).toBeInTheDocument(); + it('renders an options dropdown', async () => { + const screen = await setUp(); + await expect.element(screen.getByRole('button', { name: 'Options' })).toBeInTheDocument(); }); - it.each([[true], [false]])('renders auto-connect icon only if server is autoConnect', (autoConnect) => { - const { container } = setUp(true, autoConnect); + it.each([[true], [false]])('renders auto-connect icon only if server is autoConnect', async (autoConnect) => { + const { container } = await setUp(true, autoConnect); expect(container).toMatchSnapshot(); }); - it('renders server props where appropriate', () => { - setUp(); - + it('renders server props where appropriate', async () => { + const screen = await setUp(); const link = screen.getByRole('link'); - expect(link).toHaveAttribute('href', `/server/${server.id}`); - expect(link).toHaveTextContent(server.name); - expect(screen.getByText(server.url)).toBeInTheDocument(); + await expect.element(link).toHaveAttribute('href', `/server/${server.id}`); + await expect.element(link).toHaveTextContent(server.name); + await expect.element(screen.getByText(server.url)).toBeInTheDocument(); }); }); diff --git a/test/servers/ManageServersRowDropdown.test.tsx b/test/servers/ManageServersRowDropdown.test.tsx index d347df4f..921727e4 100644 --- a/test/servers/ManageServersRowDropdown.test.tsx +++ b/test/servers/ManageServersRowDropdown.test.tsx @@ -1,6 +1,6 @@ -import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { MemoryRouter } from 'react-router'; +import type { RenderResult } from 'vitest-browser-react'; import type { UserEvent } from 'vitest/browser'; import type { ServerWithId } from '../../src/servers/data'; import { ManageServersRowDropdown } from '../../src/servers/ManageServersRowDropdown'; @@ -21,50 +21,54 @@ describe('', () => { }, ); }; - const toggleDropdown = (user: UserEvent) => user.click(screen.getByRole('button')); + const toggleDropdown = (user: UserEvent, screen: RenderResult) => user.click(screen.getByRole('button')); it('passes a11y checks', async () => { - const { user, container } = setUp(); + const { user, ...screen } = await setUp(); // Open menu - await toggleDropdown(user); + await toggleDropdown(user, screen); - return checkAccessibility({ container }); + return checkAccessibility(screen); }); it('renders expected amount of dropdown items', async () => { - const { user } = setUp(); + const { user, ...screen } = await setUp(); - expect(screen.queryByRole('menu')).not.toBeInTheDocument(); - await toggleDropdown(user); - expect(screen.getByRole('menu')).toBeInTheDocument(); + await expect.element(screen.getByRole('menu')).not.toBeInTheDocument(); + await toggleDropdown(user, screen); + await expect.element(screen.getByRole('menu')).toBeInTheDocument(); - expect(screen.getAllByRole('menuitem')).toHaveLength(4); - expect(screen.getByRole('menuitem', { name: 'Connect' })).toHaveAttribute('href', '/server/abc123'); - expect(screen.getByRole('menuitem', { name: 'Edit server' })).toHaveAttribute('href', '/server/abc123/edit'); + expect(screen.getByRole('menuitem').elements()).toHaveLength(4); + await expect + .element(screen.getByRole('menuitem', { name: 'Connect', exact: true })) + .toHaveAttribute('href', '/server/abc123'); + await expect + .element(screen.getByRole('menuitem', { name: 'Edit server' })) + .toHaveAttribute('href', '/server/abc123/edit'); }); it.each([true, false])('allows toggling auto-connect', async (autoConnect) => { - const { user, store } = setUp(autoConnect); + const { user, store, ...screen } = await setUp(autoConnect); - await toggleDropdown(user); + await toggleDropdown(user, screen); await user.click(screen.getByRole('menuitem', { name: autoConnect ? 'Do not auto-connect' : 'Auto-connect' })); expect(Object.values(store.getState().servers)[0].autoConnect).toEqual(!autoConnect); }); it('renders deletion modal', async () => { - const { user } = setUp(); + const { user, ...screen } = await setUp(); - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + expect(screen.getByRole('dialog')).not.toBeInTheDocument(); - await toggleDropdown(user); + await toggleDropdown(user, screen); await user.click(screen.getByRole('menuitem', { name: 'Remove server' })); - expect(screen.getByRole('dialog')).toBeInTheDocument(); + await expect.element(screen.getByRole('dialog')).toBeInTheDocument(); }); - it.each([[true], [false]])('renders expected size and icon', (autoConnect) => { - const { container } = setUp(autoConnect); + it.each([[true], [false]])('renders expected size and icon', async (autoConnect) => { + const { container } = await setUp(autoConnect); expect(container).toMatchSnapshot(); }); }); diff --git a/test/servers/ServersDropdown.test.tsx b/test/servers/ServersDropdown.test.tsx index e9962c46..b2bc8ef4 100644 --- a/test/servers/ServersDropdown.test.tsx +++ b/test/servers/ServersDropdown.test.tsx @@ -1,4 +1,3 @@ -import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { MemoryRouter } from 'react-router'; import type { ServersMap } from '../../src/servers/data'; @@ -25,43 +24,45 @@ describe('', () => { ); it('passes a11y checks', async () => { - const { user, ...rest } = setUp(); + const { user, container, ...screen } = await setUp(); // Open menu await user.click(screen.getByText('Servers')); - return checkAccessibility(rest); + return checkAccessibility({ container }); }); it('contains the list of servers and the "mange servers" button', async () => { - const { user } = setUp(); + const { user, ...screen } = await setUp(); await user.click(screen.getByText('Servers')); - const items = screen.getAllByRole('menuitem'); + const items = screen.getByRole('menuitem').elements(); // We have to add two for the "Manage servers" and the "Settings" menu items expect(items).toHaveLength(Object.values(fallbackServers).length + 2); - expect(items[1]).toHaveTextContent('foo'); - expect(items[2]).toHaveTextContent('bar'); - expect(items[3]).toHaveTextContent('baz'); - expect(items[4]).toHaveTextContent('Manage servers'); + await expect.element(items[1]).toHaveTextContent('foo'); + await expect.element(items[2]).toHaveTextContent('bar'); + await expect.element(items[3]).toHaveTextContent('baz'); + await expect.element(items[4]).toHaveTextContent('Manage servers'); }); - it('contains a toggle with proper text', () => { - setUp(); - expect(screen.getByRole('button')).toHaveTextContent('Servers'); + it('contains a toggle with proper text', async () => { + const screen = await setUp(); + await expect.element(screen.getByRole('button')).toHaveTextContent('Servers'); }); it('contains a button to manage servers', async () => { - const { user } = setUp(); + const { user, ...screen } = await setUp(); await user.click(screen.getByText('Servers')); - expect(screen.getByRole('menuitem', { name: 'Manage servers' })).toHaveAttribute('href', '/manage-servers'); + await expect + .element(screen.getByRole('menuitem', { name: 'Manage servers' })) + .toHaveAttribute('href', '/manage-servers'); }); it('shows only create link when no servers exist yet', async () => { - const { user } = setUp({}); + const { user, ...screen } = await setUp({}); await user.click(screen.getByText('Servers')); - expect(screen.getByRole('menuitem', { name: 'Add a server' })).toBeInTheDocument(); + await expect.element(screen.getByRole('menuitem', { name: 'Add a server' })).toBeInTheDocument(); }); }); diff --git a/test/servers/ServersListGroup.test.tsx b/test/servers/ServersListGroup.test.tsx index 3e2dc26c..5e993877 100644 --- a/test/servers/ServersListGroup.test.tsx +++ b/test/servers/ServersListGroup.test.tsx @@ -1,9 +1,9 @@ -import { render, screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { MemoryRouter } from 'react-router'; import type { ServerWithId } from '../../src/servers/data'; import { ServersListGroup } from '../../src/servers/ServersListGroup'; import { checkAccessibility } from '../__helpers__/accessibility'; +import { render } from '../__helpers__/setUpTest.tsx'; describe('', () => { const servers: ServerWithId[] = [fromPartial({ name: 'foo', id: '123' }), fromPartial({ name: 'bar', id: '456' })]; @@ -19,21 +19,21 @@ describe('', () => { it('passes a11y checks', () => checkAccessibility(setUp())); - it.each([[servers], [[]]])('shows servers list', (servers) => { - setUp({ servers }); + it.each([[servers], [[]]])('shows servers list', async (servers) => { + const screen = await setUp({ servers }); - expect(screen.queryAllByTestId('list')).toHaveLength(servers.length ? 1 : 0); - expect(screen.queryAllByRole('link')).toHaveLength(servers.length); + expect(screen.getByTestId('list').elements()).toHaveLength(servers.length ? 1 : 0); + expect(screen.getByRole('link').elements()).toHaveLength(servers.length); }); - it.each([[true], [false], [undefined]])('renders proper classes for embedded', (borderless) => { - setUp({ servers, borderless }); + it.each([[true], [false], [undefined]])('renders proper classes for embedded', async (borderless) => { + const screen = await setUp({ servers, borderless }); const list = screen.getByTestId('list'); if (!borderless) { - expect(list).toHaveClass('border-y'); + await expect.element(list).toHaveClass('border-y'); } else { - expect(list).not.toHaveClass('border-y'); + await expect.element(list).not.toHaveClass('border-y'); } }); }); diff --git a/test/servers/helpers/DuplicatedServersModal.test.tsx b/test/servers/helpers/DuplicatedServersModal.test.tsx index 34e5d286..5fa968e8 100644 --- a/test/servers/helpers/DuplicatedServersModal.test.tsx +++ b/test/servers/helpers/DuplicatedServersModal.test.tsx @@ -1,4 +1,3 @@ -import { act, screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import type { ServerData } from '../../../src/servers/data'; import { DuplicatedServersModal } from '../../../src/servers/helpers/DuplicatedServersModal'; @@ -9,10 +8,8 @@ describe('', () => { const onClose = vi.fn(); const onConfirm = vi.fn(); const setUp = (duplicatedServers: ServerData[] = []) => - act(() => - renderWithEvents( - , - ), + renderWithEvents( + , ); const mockServer = (data: Partial = {}) => fromPartial(data); @@ -25,8 +22,8 @@ describe('', () => { [[mockServer(), mockServer(), mockServer()], 3], [[mockServer(), mockServer(), mockServer(), mockServer()], 4], ])('renders expected amount of items', async (duplicatedServers, expectedItems) => { - await setUp(duplicatedServers); - expect(screen.queryAllByRole('listitem')).toHaveLength(expectedItems); + const screen = await setUp(duplicatedServers); + expect(screen.getByRole('listitem')).toHaveLength(expectedItems); }); it.each([ @@ -51,13 +48,13 @@ describe('', () => { }, ], ])('renders expected texts based on amount of servers', async (duplicatedServers, assertions) => { - await setUp(duplicatedServers); + const screen = await setUp(duplicatedServers); - expect(screen.getByRole('heading')).toHaveTextContent(assertions.header); - expect(screen.getByText(assertions.firstParagraph)).toBeInTheDocument(); - expect(screen.getByText(assertions.lastParagraph)).toBeInTheDocument(); - expect(screen.getByRole('button', { name: assertions.discardBtn })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: assertions.confirmButton })).toBeInTheDocument(); + await expect.element(screen.getByRole('heading')).toHaveTextContent(assertions.header); + await expect.element(screen.getByText(assertions.firstParagraph)).toBeInTheDocument(); + await expect.element(screen.getByText(assertions.lastParagraph)).toBeInTheDocument(); + await expect.element(screen.getByRole('button', { name: assertions.discardBtn })).toBeInTheDocument(); + await expect.element(screen.getByRole('button', { name: assertions.confirmButton })).toBeInTheDocument(); }); it.each([ @@ -65,26 +62,31 @@ describe('', () => { [[mockServer({ url: 'url', apiKey: 'apiKey' })]], [[mockServer({ url: 'url_1', apiKey: 'apiKey_1' }), mockServer({ url: 'url_2', apiKey: 'apiKey_2' })]], ])('displays provided server data', async (duplicatedServers) => { - await setUp(duplicatedServers); + const screen = await setUp(duplicatedServers); if (duplicatedServers.length === 0) { - expect(screen.queryByRole('listitem')).not.toBeInTheDocument(); + await expect.element(screen.getByRole('listitem')).not.toBeInTheDocument(); } else if (duplicatedServers.length === 1) { - const [firstItem, secondItem] = screen.getAllByRole('listitem'); + const [firstItem, secondItem] = screen.getByRole('listitem').elements(); - expect(firstItem).toHaveTextContent(`URL: ${duplicatedServers[0].url}`); - expect(secondItem).toHaveTextContent(`API key: ${duplicatedServers[0].apiKey}`); + await expect.element(firstItem).toHaveTextContent(`URL: ${duplicatedServers[0].url}`); + await expect.element(secondItem).toHaveTextContent(`API key: ${duplicatedServers[0].apiKey}`); } else { expect.assertions(duplicatedServers.length); - screen.getAllByRole('listitem').forEach((item, index) => { - const server = duplicatedServers[index]; - expect(item).toHaveTextContent(`${server.url} - ${server.apiKey}`); - }); + await Promise.all( + screen + .getByRole('listitem') + .elements() + .map((item, index) => { + const server = duplicatedServers[index]; + return expect.element(item).toHaveTextContent(`${server.url} - ${server.apiKey}`); + }), + ); } }); it('invokes onClose when appropriate button is clicked', async () => { - const { user } = await setUp(); + const { user, ...screen } = await setUp(); expect(onClose).not.toHaveBeenCalled(); await user.click(screen.getByRole('button', { name: 'Discard' })); @@ -92,7 +94,7 @@ describe('', () => { }); it('invokes onConfirm when appropriate button is clicked', async () => { - const { user } = await setUp(); + const { user, ...screen } = await setUp(); expect(onConfirm).not.toHaveBeenCalled(); await user.click(screen.getByRole('button', { name: 'Save duplicate' })); diff --git a/test/servers/helpers/ImportServersBtn.test.tsx b/test/servers/helpers/ImportServersBtn.test.tsx index e9303fd6..2d7d7194 100644 --- a/test/servers/helpers/ImportServersBtn.test.tsx +++ b/test/servers/helpers/ImportServersBtn.test.tsx @@ -1,4 +1,3 @@ -import { screen, waitFor } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import type { ServerData, ServersMap, ServerWithId } from '../../../src/servers/data'; import type { ImportServersBtnProps } from '../../../src/servers/helpers/ImportServersBtn'; @@ -20,35 +19,35 @@ describe('', () => { it('passes a11y checks', () => checkAccessibility(setUp())); it('shows tooltip on button hover', async () => { - const { user } = setUp(); + const { user, ...screen } = await setUp(); - expect(screen.queryByText(/^You can create servers by importing a CSV file/)).not.toBeInTheDocument(); + await expect.element(screen.getByText('You can create servers by importing a CSV file')).not.toBeInTheDocument(); await user.hover(screen.getByRole('button')); - await waitFor(() => - expect(screen.getByText(/^You can create servers by importing a CSV file/)).toBeInTheDocument(), - ); + await expect.element(screen.getByText('You can create servers by importing a CSV file')).toBeInTheDocument(); }); it.each([ [undefined, ''], ['foo', 'foo'], ['bar', 'bar'], - ])('allows a class name to be provided', (providedClassName, expectedClassName) => { - setUp({ className: providedClassName }); - expect(screen.getByRole('button')).toHaveAttribute('class', expect.stringContaining(expectedClassName)); + ])('allows a class name to be provided', async (providedClassName, expectedClassName) => { + const screen = await setUp({ className: providedClassName }); + await expect + .element(screen.getByRole('button')) + .toHaveAttribute('class', expect.stringContaining(expectedClassName)); }); it.each([ [undefined, 'Import from file'], ['foo', 'foo'], ['bar', 'bar'], - ])('has expected text', (children, expectedText) => { - setUp({ children }); - expect(screen.getByRole('button')).toHaveTextContent(expectedText); + ])('has expected text', async (children, expectedText) => { + const screen = await setUp({ children }); + await expect.element(screen.getByRole('button')).toHaveTextContent(expectedText); }); it('imports servers when file input changes', async () => { - const { user } = setUp(); + const { user, ...screen } = await setUp(); await user.upload(screen.getByTestId('csv-file-input'), csvFile); expect(importServersFromFile).toHaveBeenCalledTimes(1); @@ -70,18 +69,18 @@ describe('', () => { id: 'existingserver-s.test', }; const newServer: ServerData = { name: 'newServer', url: 'http://s.test/newUrl', apiKey: 'newApiKey' }; - const { user, store } = setUp({}, { [existingServer.id]: existingServer }); + const { user, store, ...screen } = await setUp({}, { [existingServer.id]: existingServer }); importServersFromFile.mockResolvedValue([existingServerData, newServer]); - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + await expect.element(screen.getByRole('dialog')).not.toBeInTheDocument(); await user.upload(screen.getByTestId('csv-file-input'), csvFile); // Once the file is uploaded, non-duplicated servers are immediately created const { servers } = store.getState(); expect(Object.keys(servers)).toHaveLength(2); - expect(screen.getByRole('dialog')).toBeInTheDocument(); + await expect.element(screen.getByRole('dialog')).toBeInTheDocument(); await user.click(screen.getByRole('button', { name: btnName })); // If duplicated servers are saved, there's one extra server creation diff --git a/test/servers/helpers/ServerError.test.tsx b/test/servers/helpers/ServerError.test.tsx index 3c4ebfaf..706b1d85 100644 --- a/test/servers/helpers/ServerError.test.tsx +++ b/test/servers/helpers/ServerError.test.tsx @@ -1,4 +1,3 @@ -import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { MemoryRouter } from 'react-router'; import type { NonReachableServer, NotFoundServer, SelectedServer } from '../../../src/servers/data'; @@ -45,10 +44,10 @@ describe('', () => { notFound: ['Could not find this Shlink server.'], }, ], - ])('renders expected information based on provided server type', (selectedServer, { found, notFound }) => { - setUp(selectedServer); + ])('renders expected information based on provided server type', async (selectedServer, { found, notFound }) => { + const screen = await setUp(selectedServer); - found.forEach((text) => expect(screen.getByText(text)).toBeInTheDocument()); - notFound.forEach((text) => expect(screen.queryByText(text)).not.toBeInTheDocument()); + await Promise.all(found.map((text) => expect.element(screen.getByText(text)).toBeInTheDocument())); + await Promise.all(notFound.map((text) => expect.element(screen.getByText(text)).not.toBeInTheDocument())); }); }); diff --git a/test/servers/helpers/ServerForm.test.tsx b/test/servers/helpers/ServerForm.test.tsx index 1305c51d..05da61da 100644 --- a/test/servers/helpers/ServerForm.test.tsx +++ b/test/servers/helpers/ServerForm.test.tsx @@ -1,38 +1,50 @@ -import { fireEvent, screen } from '@testing-library/react'; import { ServerForm } from '../../../src/servers/helpers/ServerForm'; import { checkAccessibility } from '../../__helpers__/accessibility'; import { renderWithEvents } from '../../__helpers__/setUpTest'; describe('', () => { const onSubmit = vi.fn(); - const setUp = () => renderWithEvents(Something); + const setUp = () => + renderWithEvents( + + Something + + , + ); it('passes a11y checks', () => checkAccessibility(setUp())); - it('renders inputs', () => { - setUp(); + it('renders inputs', async () => { + const screen = await setUp(); - expect(screen.getByLabelText(/^Name/)).toBeInTheDocument(); - expect(screen.getByLabelText(/^URL/)).toBeInTheDocument(); - expect(screen.getByLabelText(/^API key/)).toBeInTheDocument(); - expect(screen.getByText('Something')).toBeInTheDocument(); - expect(screen.getByText('Advanced options')).toBeInTheDocument(); + await expect.element(screen.getByLabelText(/^Name/)).toBeInTheDocument(); + await expect.element(screen.getByLabelText(/^URL/)).toBeInTheDocument(); + await expect.element(screen.getByLabelText(/^API key/)).toBeInTheDocument(); + await expect.element(screen.getByText('Something')).toBeInTheDocument(); + await expect.element(screen.getByText('Advanced options')).toBeInTheDocument(); }); it('invokes submit callback when submit event is triggered', async () => { - setUp(); + const { user, ...screen } = await setUp(); expect(onSubmit).not.toHaveBeenCalled(); - fireEvent.submit(screen.getByRole('form'), { preventDefault: vi.fn() }); + + // Fill required elements so the form can be submitted + await user.type(screen.getByLabelText(/^Name/), 'The server'); + await user.type(screen.getByLabelText(/^URL/), 'https://example.com'); + await user.type(screen.getByLabelText(/^API key/), '123456'); + + await user.click(screen.getByRole('button', { name: 'Submit' })); + expect(onSubmit).toHaveBeenCalled(); }); it('shows advanced options', async () => { - const { user } = setUp(); + const { user, ...screen } = await setUp(); const forwardCredentialsLabel = 'Forward credentials to this server on every request.'; - expect(screen.queryByLabelText(forwardCredentialsLabel)).not.toBeInTheDocument(); + await expect.element(screen.getByLabelText(forwardCredentialsLabel)).not.toBeInTheDocument(); await user.click(screen.getByText('Advanced options')); - expect(screen.getByLabelText(forwardCredentialsLabel)).toBeInTheDocument(); + await expect.element(screen.getByLabelText(forwardCredentialsLabel)).toBeInTheDocument(); }); }); diff --git a/vite.config.ts b/vite.config.ts index fc3e19e8..1b404501 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -39,6 +39,9 @@ export default defineConfig({ // Vitest config test: { + globals: true, + clearMocks: true, + // Run tests in an actual browser browser: { provider: playwright({ @@ -50,8 +53,6 @@ export default defineConfig({ screenshotFailures: false, instances: [{ browser: 'chromium' }], }, - globals: true, - setupFiles: './config/test/setupTests.ts', coverage: { provider: 'v8', reporter: ['text', 'text-summary', 'html', 'clover'],