Merge pull request #1985 from acelaya-forks/vitest-improvements

Vitest improvements
This commit is contained in:
Alejandro Celaya
2026-08-16 21:11:22 +02:00
committed by GitHub
31 changed files with 445 additions and 858 deletions

View File

@@ -27,9 +27,6 @@ updates:
- 'react-dom' - 'react-dom'
- '@types/react' - '@types/react'
- '@types/react-dom' - '@types/react-dom'
testing:
patterns:
- '@testing-library/*'
vite: vite:
patterns: patterns:
- 'vite' - 'vite'
@@ -37,6 +34,7 @@ updates:
vitest: vitest:
patterns: patterns:
- 'vitest' - 'vitest'
- 'vitest-browser-react'
- '@vitest/*' - '@vitest/*'
workbox: workbox:
patterns: patterns:

View File

@@ -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();
});

477
package-lock.json generated
View File

@@ -40,8 +40,6 @@
"devDependencies": { "devDependencies": {
"@shlinkio/eslint-config-js-coding-standard": "~4.1.0", "@shlinkio/eslint-config-js-coding-standard": "~4.1.0",
"@tailwindcss/vite": "^4.3.3", "@tailwindcss/vite": "^4.3.3",
"@testing-library/jest-dom": "^7.0.0",
"@testing-library/react": "^16.3.2",
"@total-typescript/shoehorn": "^0.1.2", "@total-typescript/shoehorn": "^0.1.2",
"@types/node": "^26.1.2", "@types/node": "^26.1.2",
"@types/react": "^19.2.18", "@types/react": "^19.2.18",
@@ -61,15 +59,10 @@
"typescript": "^7.0.2", "typescript": "^7.0.2",
"vite": "^8.2.0", "vite": "^8.2.0",
"vite-plugin-pwa": "^1.3.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": { "node_modules/@babel/code-frame": {
"version": "7.29.0", "version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", "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" "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": { "node_modules/@total-typescript/shoehorn": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/@total-typescript/shoehorn/-/shoehorn-0.1.2.tgz", "resolved": "https://registry.npmjs.org/@total-typescript/shoehorn/-/shoehorn-0.1.2.tgz",
@@ -3766,13 +3667,6 @@
"node": ">=12" "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": { "node_modules/@types/chai": {
"version": "5.2.3", "version": "5.2.3",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
@@ -4446,40 +4340,6 @@
"node": ">=14.0" "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": { "node_modules/array-buffer-byte-length": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
@@ -4800,26 +4660,6 @@
"node": ">=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": { "node_modules/common-tags": {
"version": "1.8.2", "version": "1.8.2",
"resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz",
@@ -4896,11 +4736,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/css.escape": {
"version": "1.5.1",
"dev": true,
"license": "MIT"
},
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -5168,15 +5003,6 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/detect-libc": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -5185,12 +5011,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/dom-accessibility-api": {
"version": "0.5.13",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/dunder-proto": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -5871,14 +5691,6 @@
"url": "https://opencollective.com/immer" "url": "https://opencollective.com/immer"
} }
}, },
"node_modules/indent-string": {
"version": "4.0.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/internal-slot": { "node_modules/internal-slot": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
@@ -6699,16 +6511,6 @@
"node": "20 || >=22" "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": { "node_modules/magic-string": {
"version": "0.30.21", "version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -6756,14 +6558,6 @@
"version": "2.1.1", "version": "2.1.1",
"license": "MIT" "license": "MIT"
}, },
"node_modules/min-indent": {
"version": "1.0.1",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/minipass": { "node_modules/minipass": {
"version": "7.1.3", "version": "7.1.3",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
@@ -7150,38 +6944,6 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "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" "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": { "node_modules/redux": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
@@ -7977,17 +7727,6 @@
"node": ">=10" "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": { "node_modules/supports-color": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "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": { "node_modules/webidl-conversions": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
@@ -9268,12 +9032,6 @@
} }
}, },
"dependencies": { "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": { "@babel/code-frame": {
"version": "7.29.0", "version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
@@ -11407,65 +11165,6 @@
"tailwindcss": "4.3.3" "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": { "@total-typescript/shoehorn": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/@total-typescript/shoehorn/-/shoehorn-0.1.2.tgz", "resolved": "https://registry.npmjs.org/@total-typescript/shoehorn/-/shoehorn-0.1.2.tgz",
@@ -11484,13 +11183,6 @@
"string.prototype.matchall": "^4.0.12" "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": { "@types/chai": {
"version": "5.2.3", "version": "5.2.3",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
@@ -11886,30 +11578,6 @@
"integrity": "sha512-XleryMhbuksdKtofnWZ9Sk+4CUTbms4Mb/EU32SZwToAyZ5RgVos/ki8n+yr0LWHOGKuakbXTuuYNHLQjhddgg==", "integrity": "sha512-XleryMhbuksdKtofnWZ9Sk+4CUTbms4Mb/EU32SZwToAyZ5RgVos/ki8n+yr0LWHOGKuakbXTuuYNHLQjhddgg==",
"dev": true "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": { "array-buffer-byte-length": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", "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", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" "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": { "common-tags": {
"version": "1.8.2", "version": "1.8.2",
"resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz",
@@ -12197,10 +11848,6 @@
"integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==",
"dev": true "dev": true
}, },
"css.escape": {
"version": "1.5.1",
"dev": true
},
"csstype": { "csstype": {
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -12372,22 +12019,11 @@
"object-keys": "^1.1.1" "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": { "detect-libc": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==" "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="
}, },
"dom-accessibility-api": {
"version": "0.5.13",
"dev": true,
"peer": true
},
"dunder-proto": { "dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "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", "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.16.tgz",
"integrity": "sha512-Xs7H9rBc+kti1J6RueUvbEBkmOz7jqj11XYgf+YMXAYzu8EeE7hwZ9poLXdVfVnGmJu7QAf41T7H2KuF6QoK6Q==" "integrity": "sha512-Xs7H9rBc+kti1J6RueUvbEBkmOz7jqj11XYgf+YMXAYzu8EeE7hwZ9poLXdVfVnGmJu7QAf41T7H2KuF6QoK6Q=="
}, },
"indent-string": {
"version": "4.0.0",
"dev": true
},
"internal-slot": { "internal-slot": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", "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==", "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==",
"dev": true "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": { "magic-string": {
"version": "0.30.21", "version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -13380,10 +13005,6 @@
"merge": { "merge": {
"version": "2.1.1" "version": "2.1.1"
}, },
"min-indent": {
"version": "1.0.1",
"dev": true
},
"minipass": { "minipass": {
"version": "7.1.3", "version": "7.1.3",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
@@ -13611,28 +13232,6 @@
"integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==",
"dev": true "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": { "punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -13718,14 +13317,6 @@
"victory-vendor": "^37.0.2" "victory-vendor": "^37.0.2"
} }
}, },
"redent": {
"version": "3.0.0",
"dev": true,
"requires": {
"indent-string": "^4.0.0",
"strip-indent": "^3.0.0"
}
},
"redux": { "redux": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
@@ -14188,13 +13779,6 @@
"integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==",
"dev": true "dev": true
}, },
"strip-indent": {
"version": "3.0.0",
"dev": true,
"requires": {
"min-indent": "^1.0.0"
}
},
"supports-color": { "supports-color": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -14649,6 +14233,13 @@
"why-is-node-running": "^2.3.0" "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": { "webidl-conversions": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",

View File

@@ -57,8 +57,6 @@
"devDependencies": { "devDependencies": {
"@shlinkio/eslint-config-js-coding-standard": "~4.1.0", "@shlinkio/eslint-config-js-coding-standard": "~4.1.0",
"@tailwindcss/vite": "^4.3.3", "@tailwindcss/vite": "^4.3.3",
"@testing-library/jest-dom": "^7.0.0",
"@testing-library/react": "^16.3.2",
"@total-typescript/shoehorn": "^0.1.2", "@total-typescript/shoehorn": "^0.1.2",
"@types/node": "^26.1.2", "@types/node": "^26.1.2",
"@types/react": "^19.2.18", "@types/react": "^19.2.18",
@@ -78,7 +76,8 @@
"typescript": "^7.0.2", "typescript": "^7.0.2",
"vite": "^8.2.0", "vite": "^8.2.0",
"vite-plugin-pwa": "^1.3.0", "vite-plugin-pwa": "^1.3.0",
"vitest": "^4.0.3" "vitest": "^4.0.3",
"vitest-browser-react": "^2.2.0"
}, },
"browserslist": [ "browserslist": [
">0.2%", ">0.2%",

View File

@@ -1,17 +1,19 @@
import type { ShlinkApiClient } from '@shlinkio/shlink-js-sdk'; 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 { fromPartial } from '@total-typescript/shoehorn';
import type { PropsWithChildren, ReactElement } from 'react'; import type { PropsWithChildren, ReactElement } from 'react';
import { Provider } from 'react-redux'; 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 { userEvent } from 'vitest/browser';
import { ContainerProvider } from '../../src/container/context'; import { ContainerProvider } from '../../src/container/context';
import type { RootState } from '../../src/store'; import type { RootState } from '../../src/store';
import { setUpStore } 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(), user: userEvent.setup(),
...render(element, options), ...(await render(element, options)),
}); });
export type RenderOptionsWithState = Omit<RenderOptions, 'wrapper'> & { export type RenderOptionsWithState = Omit<RenderOptions, 'wrapper'> & {
@@ -32,7 +34,7 @@ export type RenderOptionsWithState = Omit<RenderOptions, 'wrapper'> & {
* Render provided ReactElement wrapped in a redux `Provider` and a `ContainerProvider` with a single * Render provided ReactElement wrapped in a redux `Provider` and a `ContainerProvider` with a single
* `buildShlinkApiClient` dependency. * `buildShlinkApiClient` dependency.
*/ */
export const renderWithStore = ( export const renderWithStore = async (
element: ReactElement, element: ReactElement,
{ initialState = {}, buildShlinkApiClient = vi.fn(), ...options }: RenderOptionsWithState = {}, { initialState = {}, buildShlinkApiClient = vi.fn(), ...options }: RenderOptionsWithState = {},
) => { ) => {
@@ -45,6 +47,6 @@ export const renderWithStore = (
return { return {
store, store,
...renderWithEvents(element, { ...options, wrapper: Wrapper }), ...(await renderWithEvents(element, { ...options, wrapper: Wrapper })),
}; };
}; };

View File

@@ -1,10 +1,8 @@
import type { HttpClient } from '@shlinkio/shlink-js-sdk'; import type { HttpClient } from '@shlinkio/shlink-js-sdk';
import { act, screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router'; import { MemoryRouter } from 'react-router';
import { App } from '../../src/app/App'; import { App } from '../../src/app/App';
import { ContainerProvider } from '../../src/container/context'; import { ContainerProvider } from '../../src/container/context';
import type { ServerWithId } from '../../src/servers/data';
import { checkAccessibility } from '../__helpers__/accessibility'; import { checkAccessibility } from '../__helpers__/accessibility';
import { renderWithStore } from '../__helpers__/setUpTest'; import { renderWithStore } from '../__helpers__/setUpTest';
@@ -13,31 +11,29 @@ vi.mock(import('../../src/common/ShlinkWebComponentContainer'), () => ({
})); }));
describe('<App />', () => { describe('<App />', () => {
const setUp = async (activeRoute = '/') => const setUp = (activeRoute = '/') =>
act(() => renderWithStore(
renderWithStore( <MemoryRouter initialEntries={[{ pathname: activeRoute }]}>
<MemoryRouter initialEntries={[{ pathname: activeRoute }]}> <ContainerProvider
<ContainerProvider value={fromPartial({
value={fromPartial({ HttpClient: fromPartial<HttpClient>({}),
HttpClient: fromPartial<HttpClient>({}), buildShlinkApiClient: vi.fn(),
buildShlinkApiClient: vi.fn(), useTimeoutToggle: vi.fn().mockReturnValue([false, vi.fn()]),
useTimeoutToggle: vi.fn().mockReturnValue([false, vi.fn()]), })}
})} >
> <App />
<App /> </ContainerProvider>
</ContainerProvider> </MemoryRouter>,
</MemoryRouter>, {
{ initialState: {
initialState: { servers: {
servers: { abc123: fromPartial({ id: 'abc123', name: 'abc123 server' }),
abc123: fromPartial<ServerWithId>({ id: 'abc123', name: 'abc123 server' }), def456: fromPartial({ id: 'def456', name: 'def456 server' }),
def456: fromPartial<ServerWithId>({ id: 'def456', name: 'def456 server' }),
},
settings: fromPartial({}),
appUpdated: false,
}, },
settings: fromPartial({}),
appUpdated: false,
}, },
), },
); );
it('passes a11y checks', () => checkAccessibility(setUp())); it('passes a11y checks', () => checkAccessibility(setUp()));
@@ -53,8 +49,8 @@ describe('<App />', () => {
['/server/def456/bar', 'ShlinkWebComponentContainer'], ['/server/def456/bar', 'ShlinkWebComponentContainer'],
['/other', 'Oops! We could not find requested route.'], ['/other', 'Oops! We could not find requested route.'],
])('renders expected route', async (activeRoute, expectedComponent) => { ])('renders expected route', async (activeRoute, expectedComponent) => {
await setUp(activeRoute); const screen = await setUp(activeRoute);
expect(screen.getByText(expectedComponent)).toBeInTheDocument(); await expect.element(screen.getByText(expectedComponent)).toBeInTheDocument();
}); });
it.each([ it.each([
@@ -62,13 +58,13 @@ describe('<App />', () => {
['/bar', false], ['/bar', false],
['/', true], ['/', true],
])('renders expected classes on shlink-wrapper based on current pathname', async (pathname, isFlex) => { ])('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'); const shlinkWrapper = screen.getByTestId('shlink-wrapper');
if (isFlex) { if (isFlex) {
expect(shlinkWrapper).toHaveClass('flex'); await expect.element(shlinkWrapper).toHaveClass('flex');
} else { } else {
expect(shlinkWrapper).not.toHaveClass('flex'); await expect.element(shlinkWrapper).not.toHaveClass('flex');
} }
}); });
}); });

View File

@@ -1,4 +1,3 @@
import { act, screen, waitFor } from '@testing-library/react';
import { AppUpdateBanner } from '../../src/common/AppUpdateBanner'; import { AppUpdateBanner } from '../../src/common/AppUpdateBanner';
import { checkAccessibility } from '../__helpers__/accessibility'; import { checkAccessibility } from '../__helpers__/accessibility';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
@@ -6,27 +5,20 @@ import { renderWithEvents } from '../__helpers__/setUpTest';
describe('<AppUpdateBanner />', () => { describe('<AppUpdateBanner />', () => {
const onClose = vi.fn(); const onClose = vi.fn();
const forceUpdate = vi.fn(); const forceUpdate = vi.fn();
const setUp = async () => { const setUp = () => renderWithEvents(<AppUpdateBanner isOpen onClose={onClose} forceUpdate={forceUpdate} />);
const result = await act(() =>
renderWithEvents(<AppUpdateBanner isOpen onClose={onClose} forceUpdate={forceUpdate} />),
);
await waitFor(() => screen.getByRole('alert'));
return result;
};
it('passes a11y checks', () => checkAccessibility(setUp())); it('passes a11y checks', () => checkAccessibility(setUp()));
it('renders initial state', async () => { it('renders initial state', async () => {
await setUp(); const screen = await setUp();
expect(screen.getByRole('heading')).toHaveTextContent('This app has just been updated!'); await expect.element(screen.getByRole('heading')).toHaveTextContent('This app has just been updated!');
expect(screen.queryByText('Restarting...')).not.toBeInTheDocument(); await expect.element(screen.getByText('Restarting...')).not.toBeInTheDocument();
expect(screen.getByText('Restart now')).not.toHaveAttribute('disabled'); await expect.element(screen.getByText('Restart now')).not.toHaveAttribute('disabled');
}); });
it('invokes toggle when alert is closed', async () => { it('invokes toggle when alert is closed', async () => {
const { user } = await setUp(); const { user, ...screen } = await setUp();
expect(onClose).not.toHaveBeenCalled(); expect(onClose).not.toHaveBeenCalled();
await user.click(screen.getByLabelText('Close')); await user.click(screen.getByLabelText('Close'));
@@ -34,12 +26,12 @@ describe('<AppUpdateBanner />', () => {
}); });
it('triggers the update when clicking the button', async () => { it('triggers the update when clicking the button', async () => {
const { user } = await setUp(); const { user, ...screen } = await setUp();
expect(forceUpdate).not.toHaveBeenCalled(); expect(forceUpdate).not.toHaveBeenCalled();
await user.click(screen.getByText(/^Restart now/)); await user.click(screen.getByText(/^Restart now/));
expect(forceUpdate).toHaveBeenCalled(); expect(forceUpdate).toHaveBeenCalled();
expect(await screen.findByText('Restarting...')).toBeInTheDocument(); await expect.element(screen.getByText('Restarting...')).toBeInTheDocument();
expect(screen.queryByText(/^Restart now/)).not.toBeInTheDocument(); await expect.element(screen.getByText(/^Restart now/)).not.toBeInTheDocument();
}); });
}); });

View File

@@ -1,7 +1,6 @@
import { screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import type { PropsWithChildren, ReactNode } from 'react'; import type { ReactNode } from 'react';
import { ErrorHandler as BaseErrorHandler } from '../../src/common/ErrorHandler'; import { ErrorHandler } from '../../src/common/ErrorHandler';
import { checkAccessibility } from '../__helpers__/accessibility'; import { checkAccessibility } from '../__helpers__/accessibility';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
@@ -13,8 +12,12 @@ describe('<ErrorHandler />', () => {
const reload = vi.fn(); const reload = vi.fn();
const location = fromPartial<Window['location']>({ reload }); const location = fromPartial<Window['location']>({ reload });
const cons = fromPartial<Console>({ error: vi.fn() }); const cons = fromPartial<Console>({ error: vi.fn() });
const ErrorHandler = (props: PropsWithChildren) => <BaseErrorHandler console={cons} location={location} {...props} />; const setUp = (children: ReactNode = 'Error') =>
const setUp = (children: ReactNode = 'Error') => renderWithEvents(<ErrorHandler>{children}</ErrorHandler>); renderWithEvents(
<ErrorHandler console={cons} location={location}>
{children}
</ErrorHandler>,
);
beforeEach(() => { beforeEach(() => {
vi.spyOn(console, 'error').mockImplementation(() => {}); // Silence react errors vi.spyOn(console, 'error').mockImplementation(() => {}); // Silence react errors
@@ -22,23 +25,23 @@ describe('<ErrorHandler />', () => {
it('passes a11y checks', () => checkAccessibility(setUp())); it('passes a11y checks', () => checkAccessibility(setUp()));
it('renders children when no error has occurred', () => { it('renders children when no error has occurred', async () => {
setUp(<span>Foo</span>); const screen = await setUp(<span>Foo</span>);
expect(screen.getByText('Foo')).toBeInTheDocument(); await expect.element(screen.getByText('Foo')).toBeInTheDocument();
expect(screen.queryByText('Oops! This is awkward :S')).not.toBeInTheDocument(); await expect.element(screen.getByText('Oops! This is awkward :S')).not.toBeInTheDocument();
expect(screen.queryByRole('button')).not.toBeInTheDocument(); await expect.element(screen.getByRole('button')).not.toBeInTheDocument();
}); });
it('renders error page when error has occurred', () => { it('renders error page when error has occurred', async () => {
setUp(<ComponentWithError />); const screen = await setUp(<ComponentWithError />);
expect(screen.getByText('Oops! This is awkward :S')).toBeInTheDocument(); await expect.element(screen.getByText('Oops! This is awkward :S')).toBeInTheDocument();
expect(screen.getByRole('button')).toBeInTheDocument(); await expect.element(screen.getByRole('button')).toBeInTheDocument();
}); });
it('reloads page on button click', async () => { it('reloads page on button click', async () => {
const { user } = setUp(<ComponentWithError />); const { user, ...screen } = await setUp(<ComponentWithError />);
expect(reload).not.toHaveBeenCalled(); expect(reload).not.toHaveBeenCalled();
await user.click(screen.getByRole('button')); await user.click(screen.getByRole('button'));

View File

@@ -1,4 +1,3 @@
import { screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router'; import { MemoryRouter } from 'react-router';
import { Home } from '../../src/common/Home'; import { Home } from '../../src/common/Home';
@@ -20,9 +19,9 @@ describe('<Home />', () => {
it('passes a11y checks', () => it('passes a11y checks', () =>
checkAccessibility(setUp({ '1a': fromPartial<ServerWithId>({ name: 'foo', id: '1' }) }))); checkAccessibility(setUp({ '1a': fromPartial<ServerWithId>({ name: 'foo', id: '1' }) })));
it('renders title', () => { it('renders title', async () => {
setUp(); const screen = await setUp();
expect(screen.getByRole('heading', { name: 'Welcome!' })).toBeInTheDocument(); await expect.element(screen.getByRole('heading', { name: 'Welcome!' })).toBeInTheDocument();
}); });
it.each([ it.each([
@@ -35,15 +34,17 @@ describe('<Home />', () => {
3, 3,
], ],
[{}, 2], [{}, 2],
])('shows link to create or set-up server only when no servers exist', (servers, expectedServers) => { ])('shows link to create or set-up server only when no servers exist', async (servers, expectedServers) => {
setUp(servers); const screen = await setUp(servers);
const links = screen.getAllByRole('link'); const links = screen.getByRole('link').elements();
expect(links).toHaveLength(expectedServers); expect(links).toHaveLength(expectedServers);
if (Object.keys(servers).length === 0) { if (Object.keys(servers).length === 0) {
expect(screen.getByText('This application will help you manage your Shlink servers.')).toBeInTheDocument(); await expect
expect(screen.getByText('Learn more about Shlink')).toBeInTheDocument(); .element(screen.getByText('This application will help you manage your Shlink servers.'))
.toBeInTheDocument();
await expect.element(screen.getByText('Learn more about Shlink')).toBeInTheDocument();
} }
}); });
}); });

View File

@@ -1,4 +1,3 @@
import { screen } from '@testing-library/react';
import { createMemoryHistory } from 'history'; import { createMemoryHistory } from 'history';
import { Router } from 'react-router'; import { Router } from 'react-router';
import { MainHeader } from '../../src/common/MainHeader'; import { MainHeader } from '../../src/common/MainHeader';
@@ -19,9 +18,9 @@ describe('<MainHeader />', () => {
it('passes a11y checks', () => checkAccessibility(setUp())); it('passes a11y checks', () => checkAccessibility(setUp()));
it('renders ServersDropdown', () => { it('renders ServersDropdown', async () => {
setUp(); const screen = await setUp();
expect(screen.getByRole('button', { name: 'Servers' })).toBeInTheDocument(); await expect.element(screen.getByRole('button', { name: 'Servers' })).toBeInTheDocument();
}); });
it.each([ it.each([
@@ -30,11 +29,10 @@ describe('<MainHeader />', () => {
['/settings', true], ['/settings', true],
['/settings/foo', true], ['/settings/foo', true],
['/settings/bar', true], ['/settings/bar', true],
])('sets link to settings as active only when current path is settings', (currentPath, isActive) => { ])('sets link to settings as active only when current path is settings', async (currentPath, isActive) => {
setUp(currentPath); const screen = await setUp(currentPath);
expect(screen.getByRole('menuitem', { name: /Settings$/ })).toHaveAttribute( await expect
'data-active', .element(screen.getByRole('menuitem', { name: /Settings$/ }))
isActive ? 'true' : 'false', .toHaveAttribute('data-active', isActive ? 'true' : 'false');
);
}); });
}); });

View File

@@ -1,7 +1,7 @@
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router'; import { MemoryRouter } from 'react-router';
import { NotFound } from '../../src/common/NotFound'; import { NotFound } from '../../src/common/NotFound';
import { checkAccessibility } from '../__helpers__/accessibility'; import { checkAccessibility } from '../__helpers__/accessibility';
import { render } from '../__helpers__/setUpTest';
describe('<NotFound />', () => { describe('<NotFound />', () => {
const setUp = (props = {}) => const setUp = (props = {}) =>
@@ -13,29 +13,31 @@ describe('<NotFound />', () => {
it('passes a11y checks', () => checkAccessibility(setUp())); it('passes a11y checks', () => checkAccessibility(setUp()));
it('shows expected error title', () => { it('shows expected error title', async () => {
setUp(); const screen = await setUp();
expect(screen.getByText('Oops! We could not find requested route.')).toBeInTheDocument(); await expect.element(screen.getByText('Oops! We could not find requested route.')).toBeInTheDocument();
}); });
it('shows expected error message', () => { it('shows expected error message', async () => {
setUp(); const screen = await setUp();
expect( await expect
screen.getByText( .element(
"Use your browser's back button to navigate to the page you have previously come from, or just press this button.", screen.getByText(
), "Use your browser's back button to navigate to the page you have previously come from, or just press this button.",
).toBeInTheDocument(); ),
)
.toBeInTheDocument();
}); });
it.each([ it.each([
[{}, '/', 'Home'], [{}, '/', 'Home'],
[{ to: '/foo/bar', children: 'Hello' }, '/foo/bar', 'Hello'], [{ to: '/foo/bar', children: 'Hello' }, '/foo/bar', 'Hello'],
[{ to: '/baz-bar', children: <>Foo</> }, '/baz-bar', 'Foo'], [{ to: '/baz-bar', children: <>Foo</> }, '/baz-bar', 'Foo'],
])('shows expected link and text', (props, expectedLink, expectedText) => { ])('shows expected link and text', async (props, expectedLink, expectedText) => {
setUp(props); const screen = await setUp(props);
const link = screen.getByRole('link'); const link = screen.getByRole('link');
expect(link).toHaveAttribute('href', expectedLink); await expect.element(link).toHaveAttribute('href', expectedLink);
expect(link).toHaveTextContent(expectedText); await expect.element(link).toHaveTextContent(expectedText);
}); });
}); });

View File

@@ -1,7 +1,7 @@
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router'; import { MemoryRouter } from 'react-router';
import { ScrollToTop } from '../../src/common/ScrollToTop'; import { ScrollToTop } from '../../src/common/ScrollToTop';
import { checkAccessibility } from '../__helpers__/accessibility'; import { checkAccessibility } from '../__helpers__/accessibility';
import { render } from '../__helpers__/setUpTest.tsx';
describe('<ScrollToTop />', () => { describe('<ScrollToTop />', () => {
const setUp = (children = 'Foo') => const setUp = (children = 'Foo') =>
@@ -13,8 +13,8 @@ describe('<ScrollToTop />', () => {
it('passes a11y checks', () => checkAccessibility(setUp())); it('passes a11y checks', () => checkAccessibility(setUp()));
it.each([['Foobar'], ['Barfoo'], ['Something']])('just renders children', (children) => { it.each([['Foobar'], ['Barfoo'], ['Something']])('just renders children', async (children) => {
setUp(children); const screen = await setUp(children);
expect(screen.getByText(children)).toBeInTheDocument(); await expect.element(screen.getByText(children)).toBeInTheDocument();
}); });
}); });

View File

@@ -1,9 +1,9 @@
import { render, screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ShlinkVersionsProps } from '../../src/common/ShlinkVersions'; import type { ShlinkVersionsProps } from '../../src/common/ShlinkVersions';
import { ShlinkVersions } from '../../src/common/ShlinkVersions'; import { ShlinkVersions } from '../../src/common/ShlinkVersions';
import type { NonReachableServer, NotFoundServer, ReachableServer } from '../../src/servers/data'; import type { NonReachableServer, NotFoundServer, ReachableServer } from '../../src/servers/data';
import { checkAccessibility } from '../__helpers__/accessibility'; import { checkAccessibility } from '../__helpers__/accessibility';
import { render } from '../__helpers__/setUpTest.tsx';
describe('<ShlinkVersions />', () => { describe('<ShlinkVersions />', () => {
const setUp = (props: ShlinkVersionsProps) => render(<ShlinkVersions {...props} />); const setUp = (props: ShlinkVersionsProps) => render(<ShlinkVersions {...props} />);
@@ -19,20 +19,18 @@ describe('<ShlinkVersions />', () => {
['not-semver', fromPartial<ReachableServer>({ version: '1.0.0', printableVersion: 'some' }), 'latest', 'some'], ['not-semver', fromPartial<ReachableServer>({ version: '1.0.0', printableVersion: 'some' }), 'latest', 'some'],
])( ])(
'displays expected versions when selected server is reachable', 'displays expected versions when selected server is reachable',
(clientVersion, selectedServer, expectedClientVersion, expectedServerVersion) => { async (clientVersion, selectedServer, expectedClientVersion, expectedServerVersion) => {
setUp({ clientVersion, selectedServer }); const screen = await setUp({ clientVersion, selectedServer });
const [serverLink, clientLink] = screen.getAllByRole('link'); const [serverLink, clientLink] = screen.getByRole('link').all();
expect(serverLink).toHaveAttribute( await expect
'href', .element(serverLink)
`https://github.com/shlinkio/shlink/releases/${expectedServerVersion}`, .toHaveAttribute('href', `https://github.com/shlinkio/shlink/releases/${expectedServerVersion}`);
); await expect.element(serverLink).toHaveTextContent(expectedServerVersion);
expect(serverLink).toHaveTextContent(expectedServerVersion); await expect
expect(clientLink).toHaveAttribute( .element(clientLink)
'href', .toHaveAttribute('href', `https://github.com/shlinkio/shlink-web-client/releases/${expectedClientVersion}`);
`https://github.com/shlinkio/shlink-web-client/releases/${expectedClientVersion}`, await expect.element(clientLink).toHaveTextContent(expectedClientVersion);
);
expect(clientLink).toHaveTextContent(expectedClientVersion);
}, },
); );
@@ -40,11 +38,13 @@ describe('<ShlinkVersions />', () => {
['1.2.3', null], ['1.2.3', null],
['1.2.3', fromPartial<NotFoundServer>({ serverNotFound: true })], ['1.2.3', fromPartial<NotFoundServer>({ serverNotFound: true })],
['1.2.3', fromPartial<NonReachableServer>({ serverNotReachable: true })], ['1.2.3', fromPartial<NonReachableServer>({ serverNotReachable: true })],
])('displays only client version when selected server is not reachable', (clientVersion, selectedServer) => { ])('displays only client version when selected server is not reachable', async (clientVersion, selectedServer) => {
setUp({ clientVersion, selectedServer }); const screen = await setUp({ clientVersion, selectedServer });
const links = screen.getAllByRole('link'); const links = screen.getByRole('link').all();
expect(links).toHaveLength(1); 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');
}); });
}); });

View File

@@ -20,13 +20,13 @@ describe('<ShlinkVersionsContainer />', () => {
[null, false], [null, false],
[fromPartial<SelectedServer>({}), false], [fromPartial<SelectedServer>({}), false],
[fromPartial<ReachableServer>({ version: '1.0.0' }), true], [fromPartial<ReachableServer>({ version: '1.0.0' }), true],
])('renders proper col classes based on sidebar status', (selectedServer, shouldAddMargin) => { ])('renders proper col classes based on sidebar status', async (selectedServer, shouldAddMargin) => {
const { container } = setUp(selectedServer); const { container } = await setUp(selectedServer);
if (shouldAddMargin) { 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 { } 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)');
} }
}); });
}); });

View File

@@ -1,4 +1,3 @@
import { screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router'; import { MemoryRouter } from 'react-router';
import { ShlinkWebComponentContainer } from '../../src/common/ShlinkWebComponentContainer'; import { ShlinkWebComponentContainer } from '../../src/common/ShlinkWebComponentContainer';
@@ -25,11 +24,11 @@ describe('<ShlinkWebComponentContainer />', () => {
it('passes a11y checks', () => checkAccessibility(setUp(fromPartial({ version: '3.0.0' })))); it('passes a11y checks', () => checkAccessibility(setUp(fromPartial({ version: '3.0.0' }))));
it('shows loading indicator while loading server', () => { it('shows loading indicator while loading server', async () => {
setUp(null); const screen = await setUp(null);
expect(screen.getByText('Loading...')).toBeInTheDocument(); await expect.element(screen.getByText('Loading...')).toBeInTheDocument();
expect(screen.queryByText('ShlinkWebComponent')).not.toBeInTheDocument(); await expect.element(screen.getByText('ShlinkWebComponent')).not.toBeInTheDocument();
}); });
it.each([ it.each([
@@ -38,18 +37,18 @@ describe('<ShlinkWebComponentContainer />', () => {
fromPartial<NonReachableServer>({ id: 'foo', serverNotReachable: true }), fromPartial<NonReachableServer>({ id: 'foo', serverNotReachable: true }),
/Could not connect to this Shlink server/, /Could not connect to this Shlink server/,
], ],
])('shows error for non reachable servers', (selectedServer, expectedError) => { ])('shows error for non reachable servers', async (selectedServer, expectedError) => {
setUp(selectedServer); const screen = await setUp(selectedServer);
expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); await expect.element(screen.getByText('Loading...')).not.toBeInTheDocument();
expect(screen.getByText(expectedError)).toBeInTheDocument(); await expect.element(screen.getByText(expectedError)).toBeInTheDocument();
expect(screen.queryByText('ShlinkWebComponent')).not.toBeInTheDocument(); await expect.element(screen.getByText('ShlinkWebComponent')).not.toBeInTheDocument();
}); });
it('renders ShlinkWebComponent for reachable servers', () => { it('renders ShlinkWebComponent for reachable servers', async () => {
setUp(fromPartial({ version: '3.0.0' })); const screen = await setUp(fromPartial({ version: '3.0.0' }));
expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); await expect.element(screen.getByText('Loading...')).not.toBeInTheDocument();
expect(screen.getByText('ShlinkWebComponent')).toBeInTheDocument(); await expect.element(screen.getByText('ShlinkWebComponent')).toBeInTheDocument();
}); });
}); });

View File

@@ -1,8 +1,8 @@
import { brandColor } from '@shlinkio/shlink-frontend-kit'; import { brandColor } from '@shlinkio/shlink-frontend-kit';
import { render } from '@testing-library/react';
import type { ShlinkLogoProps } from '../../../src/common/img/ShlinkLogo'; import type { ShlinkLogoProps } from '../../../src/common/img/ShlinkLogo';
import { ShlinkLogo } from '../../../src/common/img/ShlinkLogo'; import { ShlinkLogo } from '../../../src/common/img/ShlinkLogo';
import { checkAccessibility } from '../../__helpers__/accessibility'; import { checkAccessibility } from '../../__helpers__/accessibility';
import { render } from '../../__helpers__/setUpTest.tsx';
describe('<ShlinkLogo />', () => { describe('<ShlinkLogo />', () => {
const setUp = (props: ShlinkLogoProps = {}) => render(<ShlinkLogo {...props} />); const setUp = (props: ShlinkLogoProps = {}) => render(<ShlinkLogo {...props} />);
@@ -13,22 +13,22 @@ describe('<ShlinkLogo />', () => {
[undefined, brandColor()], [undefined, brandColor()],
['red', 'red'], ['red', 'red'],
['white', 'white'], ['white', 'white'],
])('renders expected color', (color, expectedColor) => { ])('renders expected color', async (color, expectedColor) => {
const { container } = setUp({ color }); const { container } = await setUp({ color });
expect(container.querySelector('g')).toHaveAttribute('fill', expectedColor); await expect.element(container.querySelector('g')).toHaveAttribute('fill', expectedColor);
}); });
it.each([ it.each([
[undefined, undefined], [undefined, undefined],
['foo', 'foo'], ['foo', 'foo'],
['bar', 'bar'], ['bar', 'bar'],
])('renders expected class', (className, expectedClassName) => { ])('renders expected class', async (className, expectedClassName) => {
const { container } = setUp({ className }); const { container } = await setUp({ className });
if (expectedClassName) { if (expectedClassName) {
expect(container.firstChild).toHaveAttribute('class', expectedClassName); await expect.element(container.firstChild as HTMLElement).toHaveAttribute('class', expectedClassName);
} else { } else {
expect(container.firstChild).not.toHaveAttribute('class'); await expect.element(container.firstChild as HTMLElement).not.toHaveAttribute('class');
} }
}); });
}); });

View File

@@ -1,6 +1,6 @@
import { render } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { ContainerProvider, useDependencies } from '../../src/container/context'; import { ContainerProvider, useDependencies } from '../../src/container/context';
import { render } from '../__helpers__/setUpTest.tsx';
describe('context', () => { describe('context', () => {
describe('useDependencies', () => { describe('useDependencies', () => {
@@ -12,24 +12,24 @@ describe('context', () => {
return null; return null;
} }
it('throws when used outside of ContainerProvider', () => { it('throws when used outside of ContainerProvider', async () => {
expect(() => render(<TestComponent name="foo" />)).toThrowError( await expect(() => render(<TestComponent name="foo" />)).rejects.toThrow(
'You cannot use "useDependencies" outside of a ContainerProvider', 'You cannot use "useDependencies" outside of a ContainerProvider',
); );
}); });
it('throws when requested dependency is not found in container', () => { it('throws when requested dependency is not found in container', async () => {
expect(() => await expect(() =>
render( render(
<ContainerProvider value={fromPartial({})}> <ContainerProvider value={fromPartial({})}>
<TestComponent name="foo" /> <TestComponent name="foo" />
</ContainerProvider>, </ContainerProvider>,
), ),
).toThrowError('Dependency with name "foo" not found in container'); ).rejects.toThrow('Dependency with name "foo" not found in container');
}); });
it('gets dependency from container', () => { it('gets dependency from container', async () => {
render( await render(
<ContainerProvider value={fromPartial({ foo: 'the dependency' })}> <ContainerProvider value={fromPartial({ foo: 'the dependency' })}>
<TestComponent name="foo" /> <TestComponent name="foo" />
</ContainerProvider>, </ContainerProvider>,

View File

@@ -1,4 +1,3 @@
import { fireEvent, screen, waitFor } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { createMemoryHistory } from 'history'; import { createMemoryHistory } from 'history';
import { Router } from 'react-router'; import { Router } from 'react-router';
@@ -17,7 +16,11 @@ describe('<CreateServer />', () => {
const defaultServers: ServersMap = { const defaultServers: ServersMap = {
foo: fromPartial({ url: 'https://existing_url.com', apiKey: 'existing_api_key', id: 'foo' }), 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; let callCount = 0;
const useTimeoutToggle = vi.fn().mockImplementation(() => { const useTimeoutToggle = vi.fn().mockImplementation(() => {
const result = [callCount % 2 === 0 ? serversImported : importFailed, () => null]; const result = [callCount % 2 === 0 ? serversImported : importFailed, () => null];
@@ -28,40 +31,44 @@ describe('<CreateServer />', () => {
return { return {
history, history,
...renderWithStore( ...(await renderWithStore(
<Router location={history.location} navigator={history}> <Router location={history.location} navigator={history}>
<CreateServer useTimeoutToggle={useTimeoutToggle} /> <CreateServer useTimeoutToggle={useTimeoutToggle} />
</Router>, </Router>,
{ {
initialState: { servers }, initialState: { servers },
}, },
), )),
}; };
}; };
it('passes a11y checks', () => checkAccessibility(setUp())); it('passes a11y checks', () => checkAccessibility(setUp()));
it('shows success message when imported is true', () => { it('shows success message when imported is true', async () => {
setUp({ serversImported: true }); const screen = await setUp({ serversImported: true });
expect(screen.getByText('Servers properly imported. You can now select one from the list :)')).toBeInTheDocument(); await expect
expect( .element(screen.getByText('Servers properly imported. You can now select one from the list :)'))
screen.queryByText('The servers could not be imported. Make sure the format is correct.'), .toBeInTheDocument();
).not.toBeInTheDocument(); await expect
expect(screen.queryByText('ImportServersBtn')).not.toBeInTheDocument(); .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', () => { it('shows error message when import failed', async () => {
setUp({ importFailed: true }); const screen = await setUp({ importFailed: true });
expect( await expect
screen.queryByText('Servers properly imported. You can now select one from the list :)'), .element(screen.getByText('Servers properly imported. You can now select one from the list :)'))
).not.toBeInTheDocument(); .not.toBeInTheDocument();
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();
}); });
it('creates server data when form is submitted', async () => { 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'; const expectedServerId = 'the_name-the_url.com';
await user.type(screen.getByLabelText(/^Name/), 'the_name'); await user.type(screen.getByLabelText(/^Name/), 'the_name');
@@ -69,7 +76,7 @@ describe('<CreateServer />', () => {
await user.type(screen.getByLabelText(/^API key/), 'the_api_key'); await user.type(screen.getByLabelText(/^API key/), 'the_api_key');
expect(store.getState().servers[expectedServerId]).not.toBeDefined(); 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(store.getState().servers[expectedServerId]).toEqual(
expect.objectContaining({ expect.objectContaining({
id: expectedServerId, id: expectedServerId,
@@ -80,19 +87,19 @@ describe('<CreateServer />', () => {
); );
expect(history.location.pathname).toEqual(`/server/${expectedServerId}`); 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 () => { 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(/^Name/), 'the_name');
await user.type(screen.getByLabelText(/^URL/), 'https://existing_url.com'); await user.type(screen.getByLabelText(/^URL/), 'https://existing_url.com');
await user.type(screen.getByLabelText(/^API key/), 'existing_api_key'); 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' })); await user.click(screen.getByRole('button', { name: 'Discard' }));
expect(history.location.pathname).toEqual('/foo'); // Goes back to first route from history's initialEntries expect(history.location.pathname).toEqual('/foo'); // Goes back to first route from history's initialEntries

View File

@@ -1,4 +1,3 @@
import { screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { createMemoryHistory } from 'history'; import { createMemoryHistory } from 'history';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
@@ -8,9 +7,9 @@ import { checkAccessibility } from '../__helpers__/accessibility';
import { renderWithStore } from '../__helpers__/setUpTest'; import { renderWithStore } from '../__helpers__/setUpTest';
describe('<DeleteServerButton />', () => { describe('<DeleteServerButton />', () => {
const setUp = (children: ReactNode = 'Remove this server') => { const setUp = async (children: ReactNode = 'Remove this server') => {
const history = createMemoryHistory({ initialEntries: ['/foo'] }); const history = createMemoryHistory({ initialEntries: ['/foo'] });
const result = renderWithStore( const result = await renderWithStore(
<Router location={history.location} navigator={history}> <Router location={history.location} navigator={history}>
<DeleteServerButton server={fromPartial({})}>{children}</DeleteServerButton> <DeleteServerButton server={fromPartial({})}>{children}</DeleteServerButton>
</Router>, </Router>,
@@ -21,22 +20,23 @@ describe('<DeleteServerButton />', () => {
it('passes a11y checks', () => checkAccessibility(setUp('Delete me'))); it('passes a11y checks', () => checkAccessibility(setUp('Delete me')));
it.each([['Foo bar'], ['baz'], ['something']])('renders expected content', (children) => { it.each([['Foo bar'], ['baz'], ['something']])('renders expected content', async (children) => {
const { container } = setUp(children); const { container } = await setUp(children);
expect(container.firstChild).toBeTruthy(); expect(container.firstChild).toBeTruthy();
expect(container.firstChild).toMatchSnapshot(); expect(container.firstChild).toMatchSnapshot();
}); });
it('displays modal when button is clicked', async () => { 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')); 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 () => { it('navigates to home when deletion is confirmed', async () => {
const { user, history } = setUp(); const { user, history, ...screen } = await setUp();
// Open modal // Open modal
await user.click(screen.getByText('Remove this server')); await user.click(screen.getByText('Remove this server'));

View File

@@ -1,5 +1,5 @@
import { screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import type { RenderResult } from 'vitest-browser-react';
import type { ServerWithId } from '../../src/servers/data'; import type { ServerWithId } from '../../src/servers/data';
import { DeleteServerModal } from '../../src/servers/DeleteServerModal'; import { DeleteServerModal } from '../../src/servers/DeleteServerModal';
import { checkAccessibility } from '../__helpers__/accessibility'; import { checkAccessibility } from '../__helpers__/accessibility';
@@ -18,36 +18,36 @@ describe('<DeleteServerModal />', () => {
it('passes a11y checks', () => checkAccessibility(setUp())); it('passes a11y checks', () => checkAccessibility(setUp()));
it('renders a modal window', () => { it('renders a modal window', async () => {
setUp(); const screen = await setUp();
expect(screen.getByRole('dialog')).toBeInTheDocument(); await expect.element(screen.getByRole('dialog')).toBeInTheDocument();
expect(screen.getByRole('heading')).toHaveTextContent('Remove server'); await expect.element(screen.getByRole('heading')).toHaveTextContent('Remove server');
}); });
it('displays the name of the server as part of the content', () => { it('displays the name of the server as part of the content', async () => {
setUp(); const screen = await setUp();
expect(screen.getByText(/^Are you sure you want to remove/)).toBeInTheDocument(); await expect.element(screen.getByText(/^Are you sure you want to remove/)).toBeInTheDocument();
expect(screen.getByText(serverName)).toBeInTheDocument(); await expect.element(screen.getByText(serverName)).toBeInTheDocument();
}); });
it.each([[() => screen.getByRole('button', { name: 'Cancel' })], [() => screen.getByLabelText('Close dialog')]])( it.each([
'closes dialog when clicking cancel button', [(screen: RenderResult) => screen.getByRole('button', { name: 'Cancel' })],
async (getButton) => { [(screen: RenderResult) => screen.getByLabelText('Close dialog')],
const { user, store } = setUp(); ])('closes dialog when clicking cancel button', async (getButton) => {
const { user, store, ...screen } = await setUp();
expect(screen.getByRole('dialog')).toBeInTheDocument(); await expect.element(screen.getByRole('dialog')).toBeInTheDocument();
await user.click(getButton()); await user.click(getButton(screen));
expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); await expect.element(screen.getByRole('dialog')).not.toBeInTheDocument();
// No server has been deleted // No server has been deleted
expect(Object.keys(store.getState().servers)).toHaveLength(1); expect(Object.keys(store.getState().servers)).toHaveLength(1);
}, });
);
it('deletes server when clicking accept button', async () => { 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); expect(Object.keys(store.getState().servers)).toHaveLength(1);
await user.click(screen.getByRole('button', { name: 'Delete' })); await user.click(screen.getByRole('button', { name: 'Delete' }));

View File

@@ -1,4 +1,3 @@
import { fireEvent, screen, waitFor } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { createMemoryHistory } from 'history'; import { createMemoryHistory } from 'history';
import { Router } from 'react-router'; import { Router } from 'react-router';
@@ -12,14 +11,14 @@ describe('<EditServer />', () => {
const defaultSelectedServer = fromPartial<ReachableServer>({ const defaultSelectedServer = fromPartial<ReachableServer>({
id: 'abc123', id: 'abc123',
name: 'the_name', name: 'the_name',
url: 'the_url', url: 'https://example.com',
apiKey: 'the_api_key', apiKey: 'the_api_key',
}); });
const setUp = (selectedServer: SelectedServer = defaultSelectedServer) => { const setUp = async (selectedServer: SelectedServer = defaultSelectedServer) => {
const history = createMemoryHistory({ initialEntries: ['/foo', '/bar'] }); const history = createMemoryHistory({ initialEntries: ['/foo', '/bar'] });
return { return {
history, history,
...renderWithStore( ...(await renderWithStore(
<Router location={history.location} navigator={history}> <Router location={history.location} navigator={history}>
<EditServer /> <EditServer />
</Router>, </Router>,
@@ -29,46 +28,42 @@ describe('<EditServer />', () => {
servers: isServerWithId(selectedServer) ? { [selectedServer.id]: selectedServer } : {}, servers: isServerWithId(selectedServer) ? { [selectedServer.id]: selectedServer } : {},
}, },
}, },
), )),
}; };
}; };
it('passes a11y checks', () => checkAccessibility(setUp())); it('passes a11y checks', () => checkAccessibility(setUp()));
it('renders nothing if selected server is not reachable', () => { it('renders nothing if selected server is not reachable', async () => {
setUp(fromPartial<SelectedServer>({})); const screen = await setUp(fromPartial<SelectedServer>({}));
expect(screen.queryByText('Edit')).not.toBeInTheDocument(); await expect.element(screen.getByText('Edit')).not.toBeInTheDocument();
expect(screen.queryByText('Cancel')).not.toBeInTheDocument(); await expect.element(screen.getByText('Cancel')).not.toBeInTheDocument();
expect(screen.queryByText('Save')).not.toBeInTheDocument(); await expect.element(screen.getByText('Save')).not.toBeInTheDocument();
}); });
it('renders server title', () => { it('renders server title', async () => {
setUp(); const screen = await setUp();
expect(screen.getByText(`Edit "${defaultSelectedServer.name}"`)).toBeInTheDocument(); expect(screen.getByText(`Edit "${defaultSelectedServer.name}"`)).toBeInTheDocument();
}); });
it('display the server info in the form components', () => { it('display the server info in the form components', async () => {
setUp(); const screen = await setUp();
expect(screen.getByLabelText(/^Name/)).toBeInTheDocument(); await expect.element(screen.getByLabelText(/^Name/)).toBeInTheDocument();
expect(screen.getByLabelText(/^URL/)).toBeInTheDocument(); await expect.element(screen.getByLabelText(/^URL/)).toBeInTheDocument();
expect(screen.getByLabelText(/^API key/)).toBeInTheDocument(); await expect.element(screen.getByLabelText(/^API key/)).toBeInTheDocument();
}); });
it('edits server and redirects to it when form is submitted', async () => { 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(/^Name/), ' edited');
await user.type(screen.getByLabelText(/^URL/), ' edited'); await user.click(screen.getByRole('button', { name: 'Save' }));
// 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'));
expect(store.getState().servers[defaultSelectedServer.id]).toEqual( expect(store.getState().servers[defaultSelectedServer.id]).toEqual(
expect.objectContaining({ expect.objectContaining({
name: 'the_name edited', name: 'the_name edited',
url: 'the_url edited',
}), }),
); );
@@ -79,18 +74,16 @@ describe('<EditServer />', () => {
it.each([{ forwardCredentials: true }, { forwardCredentials: false }])( it.each([{ forwardCredentials: true }, { forwardCredentials: false }])(
'edits advanced options - forward credentials', 'edits advanced options - forward credentials',
async ({ forwardCredentials }) => { 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.getByText('Advanced options'));
await user.click(screen.getByLabelText('Forward credentials to this server on every request.')); 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(store.getState().servers[defaultSelectedServer.id]).toEqual( expect.objectContaining({
expect.objectContaining({ forwardCredentials: !forwardCredentials,
forwardCredentials: !forwardCredentials, }),
}),
),
); );
}, },
); );

View File

@@ -1,4 +1,3 @@
import { screen, waitFor } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router'; import { MemoryRouter } from 'react-router';
import type { ServersMap, ServerWithId } from '../../src/servers/data'; import type { ServersMap, ServerWithId } from '../../src/servers/data';
@@ -33,77 +32,77 @@ describe('<ManageServers />', () => {
)); ));
it('shows search field which allows searching servers, affecting te amount of rendered rows', async () => { 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'), foo: createServerMock('foo'),
bar: createServerMock('bar'), bar: createServerMock('bar'),
baz: createServerMock('baz'), baz: createServerMock('baz'),
}); });
const search = async (searchTerm: string) => { const search = async (searchTerm: string) => {
await user.clear(screen.getByPlaceholderText('Search...')); await user.clear(screen.getByPlaceholder('Search...'));
await user.type(screen.getByPlaceholderText('Search...'), searchTerm); await user.type(screen.getByPlaceholder('Search...'), searchTerm);
}; };
// Add one for the header row // 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); await expectRows(3);
expect(screen.queryByText('No servers found.')).not.toBeInTheDocument(); await expect.element(screen.getByText('No servers found.')).not.toBeInTheDocument();
await search('foo'); await search('foo');
await waitFor(() => expectRows(1)); await expectRows(1);
expect(screen.queryByText('No servers found.')).not.toBeInTheDocument(); await expect.element(screen.getByText('No servers found.')).not.toBeInTheDocument();
await search('Ba'); await search('Ba');
await waitFor(() => expectRows(2)); await expectRows(2);
expect(screen.queryByText('No servers found.')).not.toBeInTheDocument(); await expect.element(screen.getByText('No servers found.')).not.toBeInTheDocument();
await search('invalid'); await search('invalid');
await waitFor(() => expectRows(1)); await expectRows(1);
expect(screen.getByText('No servers found.')).toBeInTheDocument(); await expect.element(screen.getByText('No servers found.')).toBeInTheDocument();
}); });
it.each([ it.each([[createServerMock('foo')], [createServerMock('foo', true)]])(
[createServerMock('foo'), 3], 'shows different amount of columns if there are at least one auto-connect server',
[createServerMock('foo', true), 4], async (server) => {
])('shows different amount of columns if there are at least one auto-connect server', (server, expectedCols) => { const screen = await setUp({ server });
setUp({ server });
expect(screen.getAllByRole('columnheader')).toHaveLength(expectedCols); if (server.autoConnect) {
if (server.autoConnect) { await expect.element(screen.getByTestId('auto-connect')).toBeInTheDocument();
expect(screen.getByTestId('auto-connect')).toBeInTheDocument(); } else {
} else { await expect.element(screen.getByTestId('auto-connect')).not.toBeInTheDocument();
expect(screen.queryByTestId('auto-connect')).not.toBeInTheDocument(); }
} },
}); );
it.each([ it.each([
[{}, 0], [{}, 0],
[{ foo: createServerMock('foo') }, 1], [{ foo: createServerMock('foo') }, 1],
])('shows export button if the list of servers is not empty', (servers, expectedButtons) => { ])('shows export button if the list of servers is not empty', async (servers, expectedButtons) => {
setUp(servers); const screen = await setUp(servers);
expect(screen.queryAllByRole('button', { name: 'Export servers' })).toHaveLength(expectedButtons); expect(screen.getByRole('button', { name: 'Export servers' }).elements()).toHaveLength(expectedButtons);
}); });
it('allows exporting servers when clicking on button', async () => { 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(); expect(exportServers).not.toHaveBeenCalled();
await user.click(screen.getByRole('button', { name: 'Export servers' })); await user.click(screen.getByRole('button', { name: 'Export servers' }));
expect(exportServers).toHaveBeenCalled(); 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()]); useTimeoutToggle.mockReturnValue([hasError, vi.fn()]);
setUp({ foo: createServerMock('foo') }); const screen = await setUp({ foo: createServerMock('foo') });
if (hasError) { if (hasError) {
expect( await expect
screen.getByText('The servers could not be imported. Make sure the format is correct.'), .element(screen.getByText('The servers could not be imported. Make sure the format is correct.'))
).toBeInTheDocument(); .toBeInTheDocument();
} else { } else {
expect( await expect
screen.queryByText('The servers could not be imported. Make sure the format is correct.'), .element(screen.getByText('The servers could not be imported. Make sure the format is correct.'))
).not.toBeInTheDocument(); .not.toBeInTheDocument();
} }
}); });
}); });

View File

@@ -1,5 +1,4 @@
import { Table } from '@shlinkio/shlink-frontend-kit'; import { Table } from '@shlinkio/shlink-frontend-kit';
import { screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router'; import { MemoryRouter } from 'react-router';
import type { ServerWithId } from '../../src/servers/data'; import type { ServerWithId } from '../../src/servers/data';
import { ManageServersRow } from '../../src/servers/ManageServersRow'; import { ManageServersRow } from '../../src/servers/ManageServersRow';
@@ -27,28 +26,27 @@ describe('<ManageServersRow />', () => {
it.each([ it.each([
[true, 4], [true, 4],
[false, 3], [false, 3],
])('renders expected amount of columns', (hasAutoConnect, expectedCols) => { ])('renders expected amount of columns', async (hasAutoConnect, expectedCols) => {
setUp(hasAutoConnect); const screen = await setUp(hasAutoConnect);
expect(screen.getAllByRole('cell')).toHaveLength(expectedCols); expect(screen.getByRole('cell').elements()).toHaveLength(expectedCols);
}); });
it('renders an options dropdown', () => { it('renders an options dropdown', async () => {
setUp(); const screen = await setUp();
expect(screen.getByRole('button', { name: 'Options' })).toBeInTheDocument(); await expect.element(screen.getByRole('button', { name: 'Options' })).toBeInTheDocument();
}); });
it.each([[true], [false]])('renders auto-connect icon only if server is autoConnect', (autoConnect) => { it.each([[true], [false]])('renders auto-connect icon only if server is autoConnect', async (autoConnect) => {
const { container } = setUp(true, autoConnect); const { container } = await setUp(true, autoConnect);
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();
}); });
it('renders server props where appropriate', () => { it('renders server props where appropriate', async () => {
setUp(); const screen = await setUp();
const link = screen.getByRole('link'); const link = screen.getByRole('link');
expect(link).toHaveAttribute('href', `/server/${server.id}`); await expect.element(link).toHaveAttribute('href', `/server/${server.id}`);
expect(link).toHaveTextContent(server.name); await expect.element(link).toHaveTextContent(server.name);
expect(screen.getByText(server.url)).toBeInTheDocument(); await expect.element(screen.getByText(server.url)).toBeInTheDocument();
}); });
}); });

View File

@@ -1,6 +1,6 @@
import { screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router'; import { MemoryRouter } from 'react-router';
import type { RenderResult } from 'vitest-browser-react';
import type { UserEvent } from 'vitest/browser'; import type { UserEvent } from 'vitest/browser';
import type { ServerWithId } from '../../src/servers/data'; import type { ServerWithId } from '../../src/servers/data';
import { ManageServersRowDropdown } from '../../src/servers/ManageServersRowDropdown'; import { ManageServersRowDropdown } from '../../src/servers/ManageServersRowDropdown';
@@ -21,50 +21,54 @@ describe('<ManageServersRowDropdown />', () => {
}, },
); );
}; };
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 () => { it('passes a11y checks', async () => {
const { user, container } = setUp(); const { user, ...screen } = await setUp();
// Open menu // Open menu
await toggleDropdown(user); await toggleDropdown(user, screen);
return checkAccessibility({ container }); return checkAccessibility(screen);
}); });
it('renders expected amount of dropdown items', async () => { it('renders expected amount of dropdown items', async () => {
const { user } = setUp(); const { user, ...screen } = await setUp();
expect(screen.queryByRole('menu')).not.toBeInTheDocument(); await expect.element(screen.getByRole('menu')).not.toBeInTheDocument();
await toggleDropdown(user); await toggleDropdown(user, screen);
expect(screen.getByRole('menu')).toBeInTheDocument(); await expect.element(screen.getByRole('menu')).toBeInTheDocument();
expect(screen.getAllByRole('menuitem')).toHaveLength(4); expect(screen.getByRole('menuitem').elements()).toHaveLength(4);
expect(screen.getByRole('menuitem', { name: 'Connect' })).toHaveAttribute('href', '/server/abc123'); await expect
expect(screen.getByRole('menuitem', { name: 'Edit server' })).toHaveAttribute('href', '/server/abc123/edit'); .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) => { 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' })); await user.click(screen.getByRole('menuitem', { name: autoConnect ? 'Do not auto-connect' : 'Auto-connect' }));
expect(Object.values(store.getState().servers)[0].autoConnect).toEqual(!autoConnect); expect(Object.values(store.getState().servers)[0].autoConnect).toEqual(!autoConnect);
}); });
it('renders deletion modal', async () => { 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' })); 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) => { it.each([[true], [false]])('renders expected size and icon', async (autoConnect) => {
const { container } = setUp(autoConnect); const { container } = await setUp(autoConnect);
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();
}); });
}); });

View File

@@ -1,4 +1,3 @@
import { screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router'; import { MemoryRouter } from 'react-router';
import type { ServersMap } from '../../src/servers/data'; import type { ServersMap } from '../../src/servers/data';
@@ -25,43 +24,45 @@ describe('<ServersDropdown />', () => {
); );
it('passes a11y checks', async () => { it('passes a11y checks', async () => {
const { user, ...rest } = setUp(); const { user, container, ...screen } = await setUp();
// Open menu // Open menu
await user.click(screen.getByText('Servers')); await user.click(screen.getByText('Servers'));
return checkAccessibility(rest); return checkAccessibility({ container });
}); });
it('contains the list of servers and the "mange servers" button', async () => { 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')); 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 // We have to add two for the "Manage servers" and the "Settings" menu items
expect(items).toHaveLength(Object.values(fallbackServers).length + 2); expect(items).toHaveLength(Object.values(fallbackServers).length + 2);
expect(items[1]).toHaveTextContent('foo'); await expect.element(items[1]).toHaveTextContent('foo');
expect(items[2]).toHaveTextContent('bar'); await expect.element(items[2]).toHaveTextContent('bar');
expect(items[3]).toHaveTextContent('baz'); await expect.element(items[3]).toHaveTextContent('baz');
expect(items[4]).toHaveTextContent('Manage servers'); await expect.element(items[4]).toHaveTextContent('Manage servers');
}); });
it('contains a toggle with proper text', () => { it('contains a toggle with proper text', async () => {
setUp(); const screen = await setUp();
expect(screen.getByRole('button')).toHaveTextContent('Servers'); await expect.element(screen.getByRole('button')).toHaveTextContent('Servers');
}); });
it('contains a button to manage servers', async () => { it('contains a button to manage servers', async () => {
const { user } = setUp(); const { user, ...screen } = await setUp();
await user.click(screen.getByText('Servers')); 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 () => { 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')); 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();
}); });
}); });

View File

@@ -1,9 +1,9 @@
import { render, screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router'; import { MemoryRouter } from 'react-router';
import type { ServerWithId } from '../../src/servers/data'; import type { ServerWithId } from '../../src/servers/data';
import { ServersListGroup } from '../../src/servers/ServersListGroup'; import { ServersListGroup } from '../../src/servers/ServersListGroup';
import { checkAccessibility } from '../__helpers__/accessibility'; import { checkAccessibility } from '../__helpers__/accessibility';
import { render } from '../__helpers__/setUpTest.tsx';
describe('<ServersListGroup />', () => { describe('<ServersListGroup />', () => {
const servers: ServerWithId[] = [fromPartial({ name: 'foo', id: '123' }), fromPartial({ name: 'bar', id: '456' })]; const servers: ServerWithId[] = [fromPartial({ name: 'foo', id: '123' }), fromPartial({ name: 'bar', id: '456' })];
@@ -19,21 +19,21 @@ describe('<ServersListGroup />', () => {
it('passes a11y checks', () => checkAccessibility(setUp())); it('passes a11y checks', () => checkAccessibility(setUp()));
it.each([[servers], [[]]])('shows servers list', (servers) => { it.each([[servers], [[]]])('shows servers list', async (servers) => {
setUp({ servers }); const screen = await setUp({ servers });
expect(screen.queryAllByTestId('list')).toHaveLength(servers.length ? 1 : 0); expect(screen.getByTestId('list').elements()).toHaveLength(servers.length ? 1 : 0);
expect(screen.queryAllByRole('link')).toHaveLength(servers.length); expect(screen.getByRole('link').elements()).toHaveLength(servers.length);
}); });
it.each([[true], [false], [undefined]])('renders proper classes for embedded', (borderless) => { it.each([[true], [false], [undefined]])('renders proper classes for embedded', async (borderless) => {
setUp({ servers, borderless }); const screen = await setUp({ servers, borderless });
const list = screen.getByTestId('list'); const list = screen.getByTestId('list');
if (!borderless) { if (!borderless) {
expect(list).toHaveClass('border-y'); await expect.element(list).toHaveClass('border-y');
} else { } else {
expect(list).not.toHaveClass('border-y'); await expect.element(list).not.toHaveClass('border-y');
} }
}); });
}); });

View File

@@ -1,4 +1,3 @@
import { act, screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ServerData } from '../../../src/servers/data'; import type { ServerData } from '../../../src/servers/data';
import { DuplicatedServersModal } from '../../../src/servers/helpers/DuplicatedServersModal'; import { DuplicatedServersModal } from '../../../src/servers/helpers/DuplicatedServersModal';
@@ -9,10 +8,8 @@ describe('<DuplicatedServersModal />', () => {
const onClose = vi.fn(); const onClose = vi.fn();
const onConfirm = vi.fn(); const onConfirm = vi.fn();
const setUp = (duplicatedServers: ServerData[] = []) => const setUp = (duplicatedServers: ServerData[] = []) =>
act(() => renderWithEvents(
renderWithEvents( <DuplicatedServersModal open duplicatedServers={duplicatedServers} onClose={onClose} onConfirm={onConfirm} />,
<DuplicatedServersModal open duplicatedServers={duplicatedServers} onClose={onClose} onConfirm={onConfirm} />,
),
); );
const mockServer = (data: Partial<ServerData> = {}) => fromPartial<ServerData>(data); const mockServer = (data: Partial<ServerData> = {}) => fromPartial<ServerData>(data);
@@ -25,8 +22,8 @@ describe('<DuplicatedServersModal />', () => {
[[mockServer(), mockServer(), mockServer()], 3], [[mockServer(), mockServer(), mockServer()], 3],
[[mockServer(), mockServer(), mockServer(), mockServer()], 4], [[mockServer(), mockServer(), mockServer(), mockServer()], 4],
])('renders expected amount of items', async (duplicatedServers, expectedItems) => { ])('renders expected amount of items', async (duplicatedServers, expectedItems) => {
await setUp(duplicatedServers); const screen = await setUp(duplicatedServers);
expect(screen.queryAllByRole('listitem')).toHaveLength(expectedItems); expect(screen.getByRole('listitem')).toHaveLength(expectedItems);
}); });
it.each([ it.each([
@@ -51,13 +48,13 @@ describe('<DuplicatedServersModal />', () => {
}, },
], ],
])('renders expected texts based on amount of servers', async (duplicatedServers, assertions) => { ])('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); await expect.element(screen.getByRole('heading')).toHaveTextContent(assertions.header);
expect(screen.getByText(assertions.firstParagraph)).toBeInTheDocument(); await expect.element(screen.getByText(assertions.firstParagraph)).toBeInTheDocument();
expect(screen.getByText(assertions.lastParagraph)).toBeInTheDocument(); await expect.element(screen.getByText(assertions.lastParagraph)).toBeInTheDocument();
expect(screen.getByRole('button', { name: assertions.discardBtn })).toBeInTheDocument(); await expect.element(screen.getByRole('button', { name: assertions.discardBtn })).toBeInTheDocument();
expect(screen.getByRole('button', { name: assertions.confirmButton })).toBeInTheDocument(); await expect.element(screen.getByRole('button', { name: assertions.confirmButton })).toBeInTheDocument();
}); });
it.each([ it.each([
@@ -65,26 +62,31 @@ describe('<DuplicatedServersModal />', () => {
[[mockServer({ url: 'url', apiKey: 'apiKey' })]], [[mockServer({ url: 'url', apiKey: 'apiKey' })]],
[[mockServer({ url: 'url_1', apiKey: 'apiKey_1' }), mockServer({ url: 'url_2', apiKey: 'apiKey_2' })]], [[mockServer({ url: 'url_1', apiKey: 'apiKey_1' }), mockServer({ url: 'url_2', apiKey: 'apiKey_2' })]],
])('displays provided server data', async (duplicatedServers) => { ])('displays provided server data', async (duplicatedServers) => {
await setUp(duplicatedServers); const screen = await setUp(duplicatedServers);
if (duplicatedServers.length === 0) { if (duplicatedServers.length === 0) {
expect(screen.queryByRole('listitem')).not.toBeInTheDocument(); await expect.element(screen.getByRole('listitem')).not.toBeInTheDocument();
} else if (duplicatedServers.length === 1) { } 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}`); await expect.element(firstItem).toHaveTextContent(`URL: ${duplicatedServers[0].url}`);
expect(secondItem).toHaveTextContent(`API key: ${duplicatedServers[0].apiKey}`); await expect.element(secondItem).toHaveTextContent(`API key: ${duplicatedServers[0].apiKey}`);
} else { } else {
expect.assertions(duplicatedServers.length); expect.assertions(duplicatedServers.length);
screen.getAllByRole('listitem').forEach((item, index) => { await Promise.all(
const server = duplicatedServers[index]; screen
expect(item).toHaveTextContent(`${server.url} - ${server.apiKey}`); .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 () => { it('invokes onClose when appropriate button is clicked', async () => {
const { user } = await setUp(); const { user, ...screen } = await setUp();
expect(onClose).not.toHaveBeenCalled(); expect(onClose).not.toHaveBeenCalled();
await user.click(screen.getByRole('button', { name: 'Discard' })); await user.click(screen.getByRole('button', { name: 'Discard' }));
@@ -92,7 +94,7 @@ describe('<DuplicatedServersModal />', () => {
}); });
it('invokes onConfirm when appropriate button is clicked', async () => { it('invokes onConfirm when appropriate button is clicked', async () => {
const { user } = await setUp(); const { user, ...screen } = await setUp();
expect(onConfirm).not.toHaveBeenCalled(); expect(onConfirm).not.toHaveBeenCalled();
await user.click(screen.getByRole('button', { name: 'Save duplicate' })); await user.click(screen.getByRole('button', { name: 'Save duplicate' }));

View File

@@ -1,4 +1,3 @@
import { screen, waitFor } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ServerData, ServersMap, ServerWithId } from '../../../src/servers/data'; import type { ServerData, ServersMap, ServerWithId } from '../../../src/servers/data';
import type { ImportServersBtnProps } from '../../../src/servers/helpers/ImportServersBtn'; import type { ImportServersBtnProps } from '../../../src/servers/helpers/ImportServersBtn';
@@ -20,35 +19,35 @@ describe('<ImportServersBtn />', () => {
it('passes a11y checks', () => checkAccessibility(setUp())); it('passes a11y checks', () => checkAccessibility(setUp()));
it('shows tooltip on button hover', async () => { 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 user.hover(screen.getByRole('button'));
await waitFor(() => await expect.element(screen.getByText('You can create servers by importing a CSV file')).toBeInTheDocument();
expect(screen.getByText(/^You can create servers by importing a CSV file/)).toBeInTheDocument(),
);
}); });
it.each([ it.each([
[undefined, ''], [undefined, ''],
['foo', 'foo'], ['foo', 'foo'],
['bar', 'bar'], ['bar', 'bar'],
])('allows a class name to be provided', (providedClassName, expectedClassName) => { ])('allows a class name to be provided', async (providedClassName, expectedClassName) => {
setUp({ className: providedClassName }); const screen = await setUp({ className: providedClassName });
expect(screen.getByRole('button')).toHaveAttribute('class', expect.stringContaining(expectedClassName)); await expect
.element(screen.getByRole('button'))
.toHaveAttribute('class', expect.stringContaining(expectedClassName));
}); });
it.each([ it.each([
[undefined, 'Import from file'], [undefined, 'Import from file'],
['foo', 'foo'], ['foo', 'foo'],
['bar', 'bar'], ['bar', 'bar'],
])('has expected text', (children, expectedText) => { ])('has expected text', async (children, expectedText) => {
setUp({ children }); const screen = await setUp({ children });
expect(screen.getByRole('button')).toHaveTextContent(expectedText); await expect.element(screen.getByRole('button')).toHaveTextContent(expectedText);
}); });
it('imports servers when file input changes', async () => { 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); await user.upload(screen.getByTestId('csv-file-input'), csvFile);
expect(importServersFromFile).toHaveBeenCalledTimes(1); expect(importServersFromFile).toHaveBeenCalledTimes(1);
@@ -70,18 +69,18 @@ describe('<ImportServersBtn />', () => {
id: 'existingserver-s.test', id: 'existingserver-s.test',
}; };
const newServer: ServerData = { name: 'newServer', url: 'http://s.test/newUrl', apiKey: 'newApiKey' }; 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]); 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); await user.upload(screen.getByTestId('csv-file-input'), csvFile);
// Once the file is uploaded, non-duplicated servers are immediately created // Once the file is uploaded, non-duplicated servers are immediately created
const { servers } = store.getState(); const { servers } = store.getState();
expect(Object.keys(servers)).toHaveLength(2); 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 })); await user.click(screen.getByRole('button', { name: btnName }));
// If duplicated servers are saved, there's one extra server creation // If duplicated servers are saved, there's one extra server creation

View File

@@ -1,4 +1,3 @@
import { screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router'; import { MemoryRouter } from 'react-router';
import type { NonReachableServer, NotFoundServer, SelectedServer } from '../../../src/servers/data'; import type { NonReachableServer, NotFoundServer, SelectedServer } from '../../../src/servers/data';
@@ -45,10 +44,10 @@ describe('<ServerError />', () => {
notFound: ['Could not find this Shlink server.'], notFound: ['Could not find this Shlink server.'],
}, },
], ],
])('renders expected information based on provided server type', (selectedServer, { found, notFound }) => { ])('renders expected information based on provided server type', async (selectedServer, { found, notFound }) => {
setUp(selectedServer); const screen = await setUp(selectedServer);
found.forEach((text) => expect(screen.getByText(text)).toBeInTheDocument()); await Promise.all(found.map((text) => expect.element(screen.getByText(text)).toBeInTheDocument()));
notFound.forEach((text) => expect(screen.queryByText(text)).not.toBeInTheDocument()); await Promise.all(notFound.map((text) => expect.element(screen.getByText(text)).not.toBeInTheDocument()));
}); });
}); });

View File

@@ -1,38 +1,50 @@
import { fireEvent, screen } from '@testing-library/react';
import { ServerForm } from '../../../src/servers/helpers/ServerForm'; import { ServerForm } from '../../../src/servers/helpers/ServerForm';
import { checkAccessibility } from '../../__helpers__/accessibility'; import { checkAccessibility } from '../../__helpers__/accessibility';
import { renderWithEvents } from '../../__helpers__/setUpTest'; import { renderWithEvents } from '../../__helpers__/setUpTest';
describe('<ServerForm />', () => { describe('<ServerForm />', () => {
const onSubmit = vi.fn(); const onSubmit = vi.fn();
const setUp = () => renderWithEvents(<ServerForm onSubmit={onSubmit}>Something</ServerForm>); const setUp = () =>
renderWithEvents(
<ServerForm onSubmit={onSubmit}>
<span>Something</span>
<button type="submit">Submit</button>
</ServerForm>,
);
it('passes a11y checks', () => checkAccessibility(setUp())); it('passes a11y checks', () => checkAccessibility(setUp()));
it('renders inputs', () => { it('renders inputs', async () => {
setUp(); const screen = await setUp();
expect(screen.getByLabelText(/^Name/)).toBeInTheDocument(); await expect.element(screen.getByLabelText(/^Name/)).toBeInTheDocument();
expect(screen.getByLabelText(/^URL/)).toBeInTheDocument(); await expect.element(screen.getByLabelText(/^URL/)).toBeInTheDocument();
expect(screen.getByLabelText(/^API key/)).toBeInTheDocument(); await expect.element(screen.getByLabelText(/^API key/)).toBeInTheDocument();
expect(screen.getByText('Something')).toBeInTheDocument(); await expect.element(screen.getByText('Something')).toBeInTheDocument();
expect(screen.getByText('Advanced options')).toBeInTheDocument(); await expect.element(screen.getByText('Advanced options')).toBeInTheDocument();
}); });
it('invokes submit callback when submit event is triggered', async () => { it('invokes submit callback when submit event is triggered', async () => {
setUp(); const { user, ...screen } = await setUp();
expect(onSubmit).not.toHaveBeenCalled(); 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(); expect(onSubmit).toHaveBeenCalled();
}); });
it('shows advanced options', async () => { it('shows advanced options', async () => {
const { user } = setUp(); const { user, ...screen } = await setUp();
const forwardCredentialsLabel = 'Forward credentials to this server on every request.'; 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')); await user.click(screen.getByText('Advanced options'));
expect(screen.getByLabelText(forwardCredentialsLabel)).toBeInTheDocument(); await expect.element(screen.getByLabelText(forwardCredentialsLabel)).toBeInTheDocument();
}); });
}); });

View File

@@ -39,6 +39,9 @@ export default defineConfig({
// Vitest config // Vitest config
test: { test: {
globals: true,
clearMocks: true,
// Run tests in an actual browser // Run tests in an actual browser
browser: { browser: {
provider: playwright({ provider: playwright({
@@ -50,8 +53,6 @@ export default defineConfig({
screenshotFailures: false, screenshotFailures: false,
instances: [{ browser: 'chromium' }], instances: [{ browser: 'chromium' }],
}, },
globals: true,
setupFiles: './config/test/setupTests.ts',
coverage: { coverage: {
provider: 'v8', provider: 'v8',
reporter: ['text', 'text-summary', 'html', 'clover'], reporter: ['text', 'text-summary', 'html', 'clover'],