diff --git a/CHANGELOG.md b/CHANGELOG.md index e644166e..d2844e1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] ### Added * [#807](https://github.com/shlinkio/shlink-web-client/issues/807) Add support for device-specific long-URLs when creating or editing short URLs. +* [#808](https://github.com/shlinkio/shlink-web-client/issues/808) Respect settings on excluding bots in the overview section, for visits cards. ### Changed * Update to Vite 4.1 diff --git a/package-lock.json b/package-lock.json index 83440753..c7d394ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "dependencies": { "@babel/preset-env": "^7.20.2", "@babel/preset-react": "^7.18.6", - "@babel/preset-typescript": "^7.18.6", + "@babel/preset-typescript": "^7.21.0", "@fortawesome/fontawesome-free": "^6.3.0", "@fortawesome/fontawesome-svg-core": "^6.3.0", "@fortawesome/free-brands-svg-icons": "^6.3.0", @@ -74,7 +74,7 @@ "@types/uuid": "^8.3.4", "@vitejs/plugin-react": "^3.1.0", "adm-zip": "^0.5.10", - "babel-jest": "^29.3.1", + "babel-jest": "^29.5.0", "chalk": "^5.2.0", "eslint": "^8.30.0", "identity-obj-proxy": "^3.0.0", @@ -257,15 +257,17 @@ "license": "ISC" }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.20.7", - "license": "MIT", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.0.tgz", + "integrity": "sha512-Q8wNiMIdwsv5la5SPxNYzzkPnjgC0Sy0i7jLkVOCdllu/xcVNkr3TeZzbHBJrj+XXRqzX5uCyCoV9eu6xUG7KQ==", "dependencies": { "@babel/helper-annotate-as-pure": "^7.18.6", "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-member-expression-to-functions": "^7.20.7", + "@babel/helper-function-name": "^7.21.0", + "@babel/helper-member-expression-to-functions": "^7.21.0", "@babel/helper-optimise-call-expression": "^7.18.6", "@babel/helper-replace-supers": "^7.20.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", "@babel/helper-split-export-declaration": "^7.18.6" }, "engines": { @@ -369,10 +371,11 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.20.7", - "license": "MIT", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.0.tgz", + "integrity": "sha512-Muu8cdZwNN6mRRNG6lAYErJ5X3bRevgYR2O8wN0yn7jJSnGDu6eG59RfT29JHxGUovyfrh6Pj0XzmR7drNVL3Q==", "dependencies": { - "@babel/types": "^7.20.7" + "@babel/types": "^7.21.0" }, "engines": { "node": ">=6.9.0" @@ -498,8 +501,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.18.6", - "license": "MIT", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz", + "integrity": "sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==", "engines": { "node": ">=6.9.0" } @@ -1008,10 +1012,11 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.18.6", - "license": "MIT", + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz", + "integrity": "sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.19.0" }, "engines": { "node": ">=6.9.0" @@ -1522,12 +1527,14 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.19.3", - "license": "MIT", + "version": "7.21.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.21.3.tgz", + "integrity": "sha512-RQxPz6Iqt8T0uw/WsJNReuBpWpBqs/n7mNo18sKLoTbMp+UrEekhH+pKSVC7gWz+DNjo9gryfV8YzCiT45RgMw==", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.19.0", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/plugin-syntax-typescript": "^7.18.6" + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-typescript": "^7.20.0" }, "engines": { "node": ">=6.9.0" @@ -1690,12 +1697,13 @@ } }, "node_modules/@babel/preset-typescript": { - "version": "7.18.6", - "license": "MIT", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.21.0.tgz", + "integrity": "sha512-myc9mpoVA5m1rF8K8DgLEatOYFDpwC+RkMkjZ0Du6uI62YvDe8uxIEYVs/VCdSJ097nlALiU/yBC7//3nI+hNg==", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-validator-option": "^7.18.6", - "@babel/plugin-transform-typescript": "^7.18.6" + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-validator-option": "^7.21.0", + "@babel/plugin-transform-typescript": "^7.21.0" }, "engines": { "node": ">=6.9.0" @@ -2861,11 +2869,12 @@ } }, "node_modules/@jest/schemas": { - "version": "29.0.0", + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", + "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", "dev": true, - "license": "MIT", "dependencies": { - "@sinclair/typebox": "^0.24.1" + "@sinclair/typebox": "^0.25.16" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -2913,25 +2922,26 @@ } }, "node_modules/@jest/transform": { - "version": "29.3.1", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.5.0.tgz", + "integrity": "sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw==", "dev": true, - "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", - "@jest/types": "^29.3.1", + "@jest/types": "^29.5.0", "@jridgewell/trace-mapping": "^0.3.15", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.3.1", - "jest-regex-util": "^29.2.0", - "jest-util": "^29.3.1", + "jest-haste-map": "^29.5.0", + "jest-regex-util": "^29.4.3", + "jest-util": "^29.5.0", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", - "write-file-atomic": "^4.0.1" + "write-file-atomic": "^4.0.2" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -3014,11 +3024,12 @@ } }, "node_modules/@jest/types": { - "version": "29.3.1", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz", + "integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==", "dev": true, - "license": "MIT", "dependencies": { - "@jest/schemas": "^29.0.0", + "@jest/schemas": "^29.4.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", @@ -3301,9 +3312,10 @@ } }, "node_modules/@sinclair/typebox": { - "version": "0.24.44", - "dev": true, - "license": "MIT" + "version": "0.25.24", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", + "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", + "dev": true }, "node_modules/@sinonjs/commons": { "version": "1.8.6", @@ -4585,14 +4597,15 @@ "peer": true }, "node_modules/babel-jest": { - "version": "29.3.1", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.5.0.tgz", + "integrity": "sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==", "dev": true, - "license": "MIT", "dependencies": { - "@jest/transform": "^29.3.1", + "@jest/transform": "^29.5.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.2.0", + "babel-preset-jest": "^29.5.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" @@ -4679,9 +4692,10 @@ } }, "node_modules/babel-plugin-jest-hoist": { - "version": "29.2.0", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.5.0.tgz", + "integrity": "sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==", "dev": true, - "license": "MIT", "dependencies": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", @@ -4755,11 +4769,12 @@ } }, "node_modules/babel-preset-jest": { - "version": "29.2.0", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.5.0.tgz", + "integrity": "sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==", "dev": true, - "license": "MIT", "dependencies": { - "babel-plugin-jest-hoist": "^29.2.0", + "babel-plugin-jest-hoist": "^29.5.0", "babel-preset-current-node-syntax": "^1.0.0" }, "engines": { @@ -8324,19 +8339,20 @@ } }, "node_modules/jest-haste-map": { - "version": "29.3.1", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.5.0.tgz", + "integrity": "sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA==", "dev": true, - "license": "MIT", "dependencies": { - "@jest/types": "^29.3.1", + "@jest/types": "^29.5.0", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.2.0", - "jest-util": "^29.3.1", - "jest-worker": "^29.3.1", + "jest-regex-util": "^29.4.3", + "jest-util": "^29.5.0", + "jest-worker": "^29.5.0", "micromatch": "^4.0.4", "walker": "^1.0.8" }, @@ -8627,9 +8643,10 @@ } }, "node_modules/jest-regex-util": { - "version": "29.2.0", + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.4.3.tgz", + "integrity": "sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==", "dev": true, - "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -9073,11 +9090,12 @@ } }, "node_modules/jest-util": { - "version": "29.3.1", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz", + "integrity": "sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==", "dev": true, - "license": "MIT", "dependencies": { - "@jest/types": "^29.3.1", + "@jest/types": "^29.5.0", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", @@ -9329,12 +9347,13 @@ } }, "node_modules/jest-worker": { - "version": "29.3.1", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz", + "integrity": "sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*", - "jest-util": "^29.3.1", + "jest-util": "^29.5.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" }, @@ -13033,14 +13052,17 @@ } }, "@babel/helper-create-class-features-plugin": { - "version": "7.20.7", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.0.tgz", + "integrity": "sha512-Q8wNiMIdwsv5la5SPxNYzzkPnjgC0Sy0i7jLkVOCdllu/xcVNkr3TeZzbHBJrj+XXRqzX5uCyCoV9eu6xUG7KQ==", "requires": { "@babel/helper-annotate-as-pure": "^7.18.6", "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-member-expression-to-functions": "^7.20.7", + "@babel/helper-function-name": "^7.21.0", + "@babel/helper-member-expression-to-functions": "^7.21.0", "@babel/helper-optimise-call-expression": "^7.18.6", "@babel/helper-replace-supers": "^7.20.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", "@babel/helper-split-export-declaration": "^7.18.6" } }, @@ -13099,9 +13121,11 @@ } }, "@babel/helper-member-expression-to-functions": { - "version": "7.20.7", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.0.tgz", + "integrity": "sha512-Muu8cdZwNN6mRRNG6lAYErJ5X3bRevgYR2O8wN0yn7jJSnGDu6eG59RfT29JHxGUovyfrh6Pj0XzmR7drNVL3Q==", "requires": { - "@babel/types": "^7.20.7" + "@babel/types": "^7.21.0" } }, "@babel/helper-module-imports": { @@ -13177,7 +13201,9 @@ "version": "7.19.1" }, "@babel/helper-validator-option": { - "version": "7.18.6" + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz", + "integrity": "sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==" }, "@babel/helper-wrap-function": { "version": "7.20.5", @@ -13456,9 +13482,11 @@ } }, "@babel/plugin-syntax-typescript": { - "version": "7.18.6", + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz", + "integrity": "sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==", "requires": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.19.0" } }, "@babel/plugin-transform-arrow-functions": { @@ -13711,11 +13739,14 @@ } }, "@babel/plugin-transform-typescript": { - "version": "7.19.3", + "version": "7.21.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.21.3.tgz", + "integrity": "sha512-RQxPz6Iqt8T0uw/WsJNReuBpWpBqs/n7mNo18sKLoTbMp+UrEekhH+pKSVC7gWz+DNjo9gryfV8YzCiT45RgMw==", "requires": { - "@babel/helper-create-class-features-plugin": "^7.19.0", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/plugin-syntax-typescript": "^7.18.6" + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-typescript": "^7.20.0" } }, "@babel/plugin-transform-unicode-escapes": { @@ -13838,11 +13869,13 @@ } }, "@babel/preset-typescript": { - "version": "7.18.6", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.21.0.tgz", + "integrity": "sha512-myc9mpoVA5m1rF8K8DgLEatOYFDpwC+RkMkjZ0Du6uI62YvDe8uxIEYVs/VCdSJ097nlALiU/yBC7//3nI+hNg==", "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-validator-option": "^7.18.6", - "@babel/plugin-transform-typescript": "^7.18.6" + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-validator-option": "^7.21.0", + "@babel/plugin-transform-typescript": "^7.21.0" } }, "@babel/runtime": { @@ -14508,10 +14541,12 @@ } }, "@jest/schemas": { - "version": "29.0.0", + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", + "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", "dev": true, "requires": { - "@sinclair/typebox": "^0.24.1" + "@sinclair/typebox": "^0.25.16" } }, "@jest/source-map": { @@ -14544,24 +14579,26 @@ } }, "@jest/transform": { - "version": "29.3.1", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.5.0.tgz", + "integrity": "sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw==", "dev": true, "requires": { "@babel/core": "^7.11.6", - "@jest/types": "^29.3.1", + "@jest/types": "^29.5.0", "@jridgewell/trace-mapping": "^0.3.15", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.3.1", - "jest-regex-util": "^29.2.0", - "jest-util": "^29.3.1", + "jest-haste-map": "^29.5.0", + "jest-regex-util": "^29.4.3", + "jest-util": "^29.5.0", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", - "write-file-atomic": "^4.0.1" + "write-file-atomic": "^4.0.2" }, "dependencies": { "ansi-styles": { @@ -14612,10 +14649,12 @@ } }, "@jest/types": { - "version": "29.3.1", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz", + "integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==", "dev": true, "requires": { - "@jest/schemas": "^29.0.0", + "@jest/schemas": "^29.4.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", @@ -14782,7 +14821,9 @@ } }, "@sinclair/typebox": { - "version": "0.24.44", + "version": "0.25.24", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", + "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", "dev": true }, "@sinonjs/commons": { @@ -15627,13 +15668,15 @@ "peer": true }, "babel-jest": { - "version": "29.3.1", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.5.0.tgz", + "integrity": "sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==", "dev": true, "requires": { - "@jest/transform": "^29.3.1", + "@jest/transform": "^29.5.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.2.0", + "babel-preset-jest": "^29.5.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" @@ -15686,7 +15729,9 @@ } }, "babel-plugin-jest-hoist": { - "version": "29.2.0", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.5.0.tgz", + "integrity": "sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==", "dev": true, "requires": { "@babel/template": "^7.3.3", @@ -15740,10 +15785,12 @@ } }, "babel-preset-jest": { - "version": "29.2.0", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.5.0.tgz", + "integrity": "sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==", "dev": true, "requires": { - "babel-plugin-jest-hoist": "^29.2.0", + "babel-plugin-jest-hoist": "^29.5.0", "babel-preset-current-node-syntax": "^1.0.0" } }, @@ -17968,19 +18015,21 @@ "dev": true }, "jest-haste-map": { - "version": "29.3.1", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.5.0.tgz", + "integrity": "sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA==", "dev": true, "requires": { - "@jest/types": "^29.3.1", + "@jest/types": "^29.5.0", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "fsevents": "^2.3.2", "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.2.0", - "jest-util": "^29.3.1", - "jest-worker": "^29.3.1", + "jest-regex-util": "^29.4.3", + "jest-util": "^29.5.0", + "jest-worker": "^29.5.0", "micromatch": "^4.0.4", "walker": "^1.0.8" } @@ -18160,7 +18209,9 @@ "requires": {} }, "jest-regex-util": { - "version": "29.2.0", + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.4.3.tgz", + "integrity": "sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==", "dev": true }, "jest-resolve": { @@ -18457,10 +18508,12 @@ } }, "jest-util": { - "version": "29.3.1", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz", + "integrity": "sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==", "dev": true, "requires": { - "@jest/types": "^29.3.1", + "@jest/types": "^29.5.0", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", @@ -18619,11 +18672,13 @@ } }, "jest-worker": { - "version": "29.3.1", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz", + "integrity": "sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==", "dev": true, "requires": { "@types/node": "*", - "jest-util": "^29.3.1", + "jest-util": "^29.5.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" }, diff --git a/package.json b/package.json index 0f3d44f2..e9d4c8bd 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "dependencies": { "@babel/preset-env": "^7.20.2", "@babel/preset-react": "^7.18.6", - "@babel/preset-typescript": "^7.18.6", + "@babel/preset-typescript": "^7.21.0", "@fortawesome/fontawesome-free": "^6.3.0", "@fortawesome/fontawesome-svg-core": "^6.3.0", "@fortawesome/free-brands-svg-icons": "^6.3.0", @@ -90,7 +90,7 @@ "@types/uuid": "^8.3.4", "@vitejs/plugin-react": "^3.1.0", "adm-zip": "^0.5.10", - "babel-jest": "^29.3.1", + "babel-jest": "^29.5.0", "chalk": "^5.2.0", "eslint": "^8.30.0", "identity-obj-proxy": "^3.0.0", diff --git a/src/api/types/index.ts b/src/api/types/index.ts index ddd3cd97..a06515f4 100644 --- a/src/api/types/index.ts +++ b/src/api/types/index.ts @@ -40,13 +40,24 @@ export interface ShlinkPaginator { totalItems: number; } +export interface ShlinkVisitsSummary { + total: number; + nonBots: number; + bots: number; +} + export interface ShlinkVisits { data: Visit[]; pagination: ShlinkPaginator; } export interface ShlinkVisitsOverview { + nonOrphanVisits?: ShlinkVisitsSummary; // Optional only before Shlink 3.5.0 + orphanVisits?: ShlinkVisitsSummary; // Optional only before Shlink 3.5.0 + + /** @deprecated */ visitsCount: number; + /** @deprecated */ orphanVisitsCount: number; } diff --git a/src/servers/Overview.tsx b/src/servers/Overview.tsx index 58f55668..83b7d7c3 100644 --- a/src/servers/Overview.tsx +++ b/src/servers/Overview.tsx @@ -5,6 +5,7 @@ import { Card, CardBody, CardHeader, Row } from 'reactstrap'; import type { ShlinkShortUrlsListParams } from '../api/types'; import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; import { Topics } from '../mercure/helpers/Topics'; +import type { Settings } from '../settings/reducers/settings'; import type { CreateShortUrlProps } from '../short-urls/CreateShortUrl'; import type { ShortUrlsList as ShortUrlsListState } from '../short-urls/reducers/shortUrlsList'; import { ITEMS_IN_OVERVIEW_PAGE } from '../short-urls/reducers/shortUrlsList'; @@ -16,6 +17,7 @@ import type { VisitsOverview } from '../visits/reducers/visitsOverview'; import type { SelectedServer } from './data'; import { getServerId } from './data'; import { HighlightCard } from './helpers/HighlightCard'; +import { VisitsHighlightCard } from './helpers/VisitsHighlightCard'; interface OverviewConnectProps { shortUrlsList: ShortUrlsListState; @@ -25,6 +27,7 @@ interface OverviewConnectProps { selectedServer: SelectedServer; visitsOverview: VisitsOverview; loadVisitsOverview: Function; + settings: Settings; } export const Overview = ( @@ -38,10 +41,11 @@ export const Overview = ( selectedServer, loadVisitsOverview, visitsOverview, + settings: { visits }, }: OverviewConnectProps) => { const { loading, shortUrls } = shortUrlsList; const { loading: loadingTags } = tagsList; - const { loading: loadingVisits, visitsCount, orphanVisitsCount } = visitsOverview; + const { loading: loadingVisits, nonOrphanVisits, orphanVisits } = visitsOverview; const serverId = getServerId(selectedServer); const linkToNonOrphanVisits = useFeature('nonOrphanVisits', selectedServer); const navigate = useNavigate(); @@ -56,14 +60,22 @@ export const Overview = ( <>
- - {loadingVisits ? 'Loading...' : prettify(visitsCount)} - +
- - {loadingVisits ? 'Loading...' : prettify(orphanVisitsCount)} - +
diff --git a/src/servers/helpers/HighlightCard.tsx b/src/servers/helpers/HighlightCard.tsx index a272be9a..99e35ecc 100644 --- a/src/servers/helpers/HighlightCard.tsx +++ b/src/servers/helpers/HighlightCard.tsx @@ -1,21 +1,30 @@ import { faArrowAltCircleRight as linkIcon } from '@fortawesome/free-regular-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import type { FC, PropsWithChildren } from 'react'; +import type { FC, PropsWithChildren, ReactNode } from 'react'; import { Link } from 'react-router-dom'; -import { Card, CardText, CardTitle } from 'reactstrap'; +import { Card, CardText, CardTitle, UncontrolledTooltip } from 'reactstrap'; +import { useElementRef } from '../../utils/helpers/hooks'; import './HighlightCard.scss'; export type HighlightCardProps = PropsWithChildren<{ title: string; - link?: string | false; + link?: string; + tooltip?: ReactNode; }>; -const buildExtraProps = (link?: string | false) => (!link ? {} : { tag: Link, to: link }); +const buildExtraProps = (link?: string) => (!link ? {} : { tag: Link, to: link }); -export const HighlightCard: FC = ({ children, title, link }) => ( - - {link && } - {title} - {children} - -); +export const HighlightCard: FC = ({ children, title, link, tooltip }) => { + const ref = useElementRef(); + + return ( + <> + + {link && } + {title} + {children} + + {tooltip && {tooltip}} + + ); +}; diff --git a/src/servers/helpers/VisitsHighlightCard.tsx b/src/servers/helpers/VisitsHighlightCard.tsx new file mode 100644 index 00000000..1617eb2e --- /dev/null +++ b/src/servers/helpers/VisitsHighlightCard.tsx @@ -0,0 +1,26 @@ +import type { FC } from 'react'; +import { prettify } from '../../utils/helpers/numbers'; +import type { PartialVisitsSummary } from '../../visits/reducers/visitsOverview'; +import type { HighlightCardProps } from './HighlightCard'; +import { HighlightCard } from './HighlightCard'; + +export type VisitsHighlightCardProps = Omit & { + loading: boolean; + excludeBots: boolean; + visitsSummary: PartialVisitsSummary; +}; + +export const VisitsHighlightCard: FC = ({ loading, excludeBots, visitsSummary, ...rest }) => ( + {excludeBots ? 'Plus' : 'Including'} {prettify(visitsSummary.bots)} potential bot visits + : undefined + } + {...rest} + > + {loading ? 'Loading...' : prettify( + excludeBots && visitsSummary.nonBots ? visitsSummary.nonBots : visitsSummary.total, + )} + +); diff --git a/src/servers/services/provideServices.ts b/src/servers/services/provideServices.ts index a7831580..fb8d4298 100644 --- a/src/servers/services/provideServices.ts +++ b/src/servers/services/provideServices.ts @@ -65,7 +65,7 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.serviceFactory('Overview', Overview, 'ShortUrlsTable', 'CreateShortUrl'); bottle.decorator('Overview', connect( - ['shortUrlsList', 'tagsList', 'selectedServer', 'mercureInfo', 'visitsOverview'], + ['shortUrlsList', 'tagsList', 'selectedServer', 'mercureInfo', 'visitsOverview', 'settings'], ['listShortUrls', 'listTags', 'createNewVisits', 'loadMercureInfo', 'loadVisitsOverview'], )); diff --git a/src/short-urls/data/index.ts b/src/short-urls/data/index.ts index f1a5f9f8..7d37e935 100644 --- a/src/short-urls/data/index.ts +++ b/src/short-urls/data/index.ts @@ -1,3 +1,4 @@ +import type { ShlinkVisitsSummary } from '../../api/types'; import type { Order } from '../../utils/helpers/ordering'; import type { Nullable, OptionalString } from '../../utils/utils'; @@ -41,7 +42,7 @@ export interface ShortUrl { dateCreated: string; /** @deprecated */ visitsCount: number; // Deprecated since Shlink 3.4.0 - visitsSummary?: ShortUrlVisitsSummary; // Optional only before Shlink 3.4.0 + visitsSummary?: ShlinkVisitsSummary; // Optional only before Shlink 3.4.0 meta: Required>; tags: string[]; domain: string | null; @@ -56,12 +57,6 @@ export interface ShortUrlMeta { maxVisits?: number; } -export interface ShortUrlVisitsSummary { - total: number; - nonBots: number; - bots: number; -} - export interface ShortUrlModalProps { shortUrl: ShortUrl; isOpen: boolean; diff --git a/src/visits/reducers/visitsOverview.ts b/src/visits/reducers/visitsOverview.ts index bab2b58b..073fa3d5 100644 --- a/src/visits/reducers/visitsOverview.ts +++ b/src/visits/reducers/visitsOverview.ts @@ -3,14 +3,24 @@ import { createSlice } from '@reduxjs/toolkit'; import type { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; import type { ShlinkVisitsOverview } from '../../api/types'; import { createAsyncThunk } from '../../utils/helpers/redux'; +import type { CreateVisit } from '../types'; import { groupNewVisitsByType } from '../types/helpers'; import { createNewVisits } from './visitCreation'; const REDUCER_PREFIX = 'shlink/visitsOverview'; -export interface VisitsOverview { - visitsCount: number; - orphanVisitsCount: number; +export type PartialVisitsSummary = { + total: number; + nonBots?: number; + bots?: number; +}; + +export type ParsedVisitsOverview = { + nonOrphanVisits: PartialVisitsSummary; + orphanVisits: PartialVisitsSummary; +}; + +export interface VisitsOverview extends ParsedVisitsOverview { loading: boolean; error: boolean; } @@ -18,15 +28,34 @@ export interface VisitsOverview { export type GetVisitsOverviewAction = PayloadAction; const initialState: VisitsOverview = { - visitsCount: 0, - orphanVisitsCount: 0, + nonOrphanVisits: { + total: 0, + }, + orphanVisits: { + total: 0, + }, loading: false, error: false, }; +const countBots = (visits: CreateVisit[]) => visits.filter(({ visit }) => visit.potentialBot).length; + export const loadVisitsOverview = (buildShlinkApiClient: ShlinkApiClientBuilder) => createAsyncThunk( `${REDUCER_PREFIX}/loadVisitsOverview`, - (_: void, { getState }): Promise => buildShlinkApiClient(getState).getVisitsOverview(), + (_: void, { getState }): Promise => buildShlinkApiClient(getState).getVisitsOverview().then( + (resp) => ({ + nonOrphanVisits: { + total: resp.nonOrphanVisits?.total ?? resp.visitsCount, + nonBots: resp.nonOrphanVisits?.nonBots, + bots: resp.nonOrphanVisits?.bots, + }, + orphanVisits: { + total: resp.orphanVisits?.total ?? resp.orphanVisitsCount, + nonBots: resp.orphanVisits?.nonBots, + bots: resp.orphanVisits?.bots, + }, + }), + ), ); export const visitsOverviewReducerCreator = ( @@ -40,13 +69,31 @@ export const visitsOverviewReducerCreator = ( builder.addCase(loadVisitsOverviewThunk.rejected, () => ({ ...initialState, error: true })); builder.addCase(loadVisitsOverviewThunk.fulfilled, (_, { payload }) => ({ ...initialState, ...payload })); - builder.addCase(createNewVisits, ({ visitsCount, orphanVisitsCount = 0, ...rest }, { payload }) => { - const { createdVisits } = payload; - const { regularVisits, orphanVisits } = groupNewVisitsByType(createdVisits); + builder.addCase(createNewVisits, ({ nonOrphanVisits, orphanVisits, ...rest }, { payload }) => { + const { nonOrphanVisits: newNonOrphanVisits, orphanVisits: newOrphanVisits } = groupNewVisitsByType( + payload.createdVisits, + ); + + const newNonOrphanTotalVisits = newNonOrphanVisits.length; + const newNonOrphanBotVisits = countBots(newNonOrphanVisits); + const newNonOrphanNonBotVisits = newNonOrphanTotalVisits - newNonOrphanBotVisits; + + const newOrphanTotalVisits = newOrphanVisits.length; + const newOrphanBotVisits = countBots(newOrphanVisits); + const newOrphanNonBotVisits = newOrphanTotalVisits - newOrphanBotVisits; + return { ...rest, - visitsCount: visitsCount + regularVisits.length, - orphanVisitsCount: orphanVisitsCount + orphanVisits.length, + nonOrphanVisits: { + total: nonOrphanVisits.total + newNonOrphanTotalVisits, + bots: nonOrphanVisits.bots && nonOrphanVisits.bots + newNonOrphanBotVisits, + nonBots: nonOrphanVisits.nonBots && nonOrphanVisits.nonBots + newNonOrphanNonBotVisits, + }, + orphanVisits: { + total: orphanVisits.total + newOrphanTotalVisits, + bots: orphanVisits.bots && orphanVisits.bots + newOrphanBotVisits, + nonBots: orphanVisits.nonBots && orphanVisits.nonBots + newOrphanNonBotVisits, + }, }; }); }, diff --git a/src/visits/types/helpers.ts b/src/visits/types/helpers.ts index 77714262..4cda7222 100644 --- a/src/visits/types/helpers.ts +++ b/src/visits/types/helpers.ts @@ -10,13 +10,13 @@ export const isNormalizedOrphanVisit = (visit: NormalizedVisit): visit is Normal export interface GroupedNewVisits { orphanVisits: CreateVisit[]; - regularVisits: CreateVisit[]; + nonOrphanVisits: CreateVisit[]; } export const groupNewVisitsByType = pipe( - groupBy((newVisit: CreateVisit) => (isOrphanVisit(newVisit.visit) ? 'orphanVisits' : 'regularVisits')), + groupBy((newVisit: CreateVisit) => (isOrphanVisit(newVisit.visit) ? 'orphanVisits' : 'nonOrphanVisits')), // @ts-expect-error Type declaration on groupBy is not correct. It can return undefined props - (result): GroupedNewVisits => ({ orphanVisits: [], regularVisits: [], ...result }), + (result): GroupedNewVisits => ({ orphanVisits: [], nonOrphanVisits: [], ...result }), ); export type HighlightableProps = T extends NormalizedOrphanVisit diff --git a/test/servers/Overview.test.tsx b/test/servers/Overview.test.tsx index 4b20e3a1..0b4c6b6d 100644 --- a/test/servers/Overview.test.tsx +++ b/test/servers/Overview.test.tsx @@ -1,13 +1,15 @@ -import { render, screen } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import { Mock } from 'ts-mockery'; import type { MercureInfo } from '../../src/mercure/reducers/mercureInfo'; import type { ReachableServer } from '../../src/servers/data'; import { Overview as overviewCreator } from '../../src/servers/Overview'; +import type { Settings } from '../../src/settings/reducers/settings'; import type { ShortUrlsList as ShortUrlsListState } from '../../src/short-urls/reducers/shortUrlsList'; import type { TagsList } from '../../src/tags/reducers/tagsList'; import { prettify } from '../../src/utils/helpers/numbers'; import type { VisitsOverview } from '../../src/visits/reducers/visitsOverview'; +import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { const ShortUrlsTable = () => <>ShortUrlsTable; @@ -20,7 +22,7 @@ describe('', () => { pagination: { totalItems: 83710 }, }; const serverId = '123'; - const setUp = (loading = false) => render( + const setUp = (loading = false, excludeBots = false) => renderWithEvents( ', () => { loadVisitsOverview={loadVisitsOverview} shortUrlsList={Mock.of({ loading, shortUrls })} tagsList={Mock.of({ loading, tags: ['foo', 'bar', 'baz'] })} - visitsOverview={Mock.of({ loading, visitsCount: 3456, orphanVisitsCount: 28 })} + visitsOverview={Mock.of({ + loading, + nonOrphanVisits: { total: 3456, bots: 1000, nonBots: 2456 }, + orphanVisits: { total: 28, bots: 15, nonBots: 13 }, + })} selectedServer={Mock.of({ id: serverId })} createNewVisits={jest.fn()} loadMercureInfo={jest.fn()} mercureInfo={Mock.all()} + settings={Mock.of({ visits: { excludeBots } })} /> , ); @@ -42,16 +49,19 @@ describe('', () => { expect(screen.getAllByText('Loading...')).toHaveLength(4); }); - it('displays amounts in cards after finishing loading', () => { - setUp(); + it.each([ + [false, 3456, 28], + [true, 2456, 13], + ])('displays amounts in cards after finishing loading', (excludeBots, expectedVisits, expectedOrphanVisits) => { + setUp(false, excludeBots); const headingElements = screen.getAllByRole('heading'); expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); expect(headingElements[0]).toHaveTextContent('Visits'); - expect(headingElements[1]).toHaveTextContent(prettify(3456)); + expect(headingElements[1]).toHaveTextContent(prettify(expectedVisits)); expect(headingElements[2]).toHaveTextContent('Orphan visits'); - expect(headingElements[3]).toHaveTextContent(prettify(28)); + expect(headingElements[3]).toHaveTextContent(prettify(expectedOrphanVisits)); expect(headingElements[4]).toHaveTextContent('Short URLs'); expect(headingElements[5]).toHaveTextContent(prettify(83710)); expect(headingElements[6]).toHaveTextContent('Tags'); @@ -77,4 +87,20 @@ describe('', () => { expect(links[3]).toHaveAttribute('href', `/server/${serverId}/create-short-url`); expect(links[4]).toHaveAttribute('href', `/server/${serverId}/list-short-urls/1`); }); + + it.each([ + [true], + [false], + ])('displays amounts of bots when hovering visits cards', async (excludeBots) => { + const { user } = setUp(false, excludeBots); + const expectTooltipToBeInTheDocument = async (tooltip: string) => waitFor( + () => expect(screen.getByText(/potential bot visits$/)).toHaveTextContent(tooltip), + ); + + await user.hover(screen.getByText(/^Visits/)); + await expectTooltipToBeInTheDocument(`${excludeBots ? 'Plus' : 'Including'} 1,000 potential bot visits`); + + await user.hover(screen.getByText(/^Orphan visits/)); + await expectTooltipToBeInTheDocument(`${excludeBots ? 'Plus' : 'Including'} 15 potential bot visits`); + }); }); diff --git a/test/servers/helpers/HighlightCard.test.tsx b/test/servers/helpers/HighlightCard.test.tsx index afc8f070..ec478de1 100644 --- a/test/servers/helpers/HighlightCard.test.tsx +++ b/test/servers/helpers/HighlightCard.test.tsx @@ -1,11 +1,12 @@ -import { render, screen } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; import type { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; import type { HighlightCardProps } from '../../../src/servers/helpers/HighlightCard'; import { HighlightCard } from '../../../src/servers/helpers/HighlightCard'; +import { renderWithEvents } from '../../__helpers__/setUpTest'; describe('', () => { - const setUp = (props: HighlightCardProps & { children?: ReactNode }) => render( + const setUp = (props: HighlightCardProps & { children?: ReactNode }) => renderWithEvents( , @@ -13,9 +14,9 @@ describe('', () => { it.each([ [undefined], - [false], + [''], ])('does not render icon when there is no link', (link) => { - setUp({ title: 'foo', link: link as undefined | false }); + setUp({ title: 'foo', link }); expect(screen.queryByRole('img', { hidden: true })).not.toBeInTheDocument(); expect(screen.queryByRole('link')).not.toBeInTheDocument(); @@ -27,7 +28,7 @@ describe('', () => { ['baz'], ])('renders provided title', (title) => { setUp({ title }); - expect(screen.getByText(title)).toHaveAttribute('class', expect.stringContaining('highlight-card__title')); + expect(screen.getByText(title)).toHaveClass('highlight-card__title'); }); it.each([ @@ -36,7 +37,7 @@ describe('', () => { ['baz'], ])('renders provided children', (children) => { setUp({ title: 'title', children }); - expect(screen.getByText(children)).toHaveAttribute('class', expect.stringContaining('card-text')); + expect(screen.getByText(children)).toHaveClass('card-text'); }); it.each([ @@ -49,4 +50,11 @@ describe('', () => { expect(screen.getByRole('img', { hidden: true })).toBeInTheDocument(); expect(screen.getByRole('link')).toHaveAttribute('href', `/${link}`); }); + + it('renders tooltip when provided', async () => { + const { user } = setUp({ title: 'title', children: 'Foo', tooltip: 'This is the tooltip' }); + + await user.hover(screen.getByText('Foo')); + await waitFor(() => expect(screen.getByText('This is the tooltip')).toBeInTheDocument()); + }); }); diff --git a/test/servers/helpers/VisitsHighlightCard.test.tsx b/test/servers/helpers/VisitsHighlightCard.test.tsx new file mode 100644 index 00000000..ede67601 --- /dev/null +++ b/test/servers/helpers/VisitsHighlightCard.test.tsx @@ -0,0 +1,60 @@ +import { screen, waitFor } from '@testing-library/react'; +import type { VisitsHighlightCardProps } from '../../../src/servers/helpers/VisitsHighlightCard'; +import { VisitsHighlightCard } from '../../../src/servers/helpers/VisitsHighlightCard'; +import { renderWithEvents } from '../../__helpers__/setUpTest'; + +describe('', () => { + const setUp = (props: Partial = {}) => renderWithEvents( + , + ); + + it.each([ + [true, () => expect(screen.getByText('Loading...')).toBeInTheDocument()], + [false, () => expect(screen.queryByText('Loading...')).not.toBeInTheDocument()], + ])('displays loading message on loading', (loading, assert) => { + setUp({ loading }); + assert(); + }); + + it('does not render tooltip when summary has no bots', async () => { + const { user } = setUp({ title: 'Foo' }); + + await user.hover(screen.getByText('Foo')); + await waitFor(() => expect(screen.queryByText(/potential bot visits$/)).not.toBeInTheDocument()); + }); + + it('renders tooltip when summary has bots', async () => { + const { user } = setUp({ + title: 'Foo', + visitsSummary: { total: 50, bots: 30 }, + }); + + await user.hover(screen.getByText('Foo')); + await waitFor(() => expect(screen.getByText(/potential bot visits$/)).toBeInTheDocument()); + }); + + it.each([ + [true, 20, () => { + expect(screen.getByText('20')).toBeInTheDocument(); + expect(screen.queryByText('50')).not.toBeInTheDocument(); + }], + [true, undefined, () => { + expect(screen.getByText('50')).toBeInTheDocument(); + expect(screen.queryByText('20')).not.toBeInTheDocument(); + }], + [false, 20, () => { + expect(screen.getByText('50')).toBeInTheDocument(); + expect(screen.queryByText('20')).not.toBeInTheDocument(); + }], + [false, undefined, () => { + expect(screen.getByText('50')).toBeInTheDocument(); + expect(screen.queryByText('20')).not.toBeInTheDocument(); + }], + ])('displays non-bots when present and bots are excluded', (excludeBots, nonBots, assert) => { + setUp({ + excludeBots, + visitsSummary: { total: 50, nonBots }, + }); + assert(); + }); +}); diff --git a/test/short-urls/helpers/ShortUrlStatus.test.tsx b/test/short-urls/helpers/ShortUrlStatus.test.tsx index 4f373503..1c5ea483 100644 --- a/test/short-urls/helpers/ShortUrlStatus.test.tsx +++ b/test/short-urls/helpers/ShortUrlStatus.test.tsx @@ -1,7 +1,8 @@ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Mock } from 'ts-mockery'; -import type { ShortUrl, ShortUrlMeta, ShortUrlVisitsSummary } from '../../../src/short-urls/data'; +import type { ShlinkVisitsSummary } from '../../../src/api/types'; +import type { ShortUrl, ShortUrlMeta } from '../../../src/short-urls/data'; import { ShortUrlStatus } from '../../../src/short-urls/helpers/ShortUrlStatus'; describe('', () => { @@ -23,12 +24,12 @@ describe('', () => { ], [ Mock.of({ maxVisits: 10 }), - Mock.of({ total: 10 }), + Mock.of({ total: 10 }), 'This short URL cannot be currently visited because it has reached the maximum amount of 10 visits.', ], [ Mock.of({ maxVisits: 1 }), - Mock.of({ total: 1 }), + Mock.of({ total: 1 }), 'This short URL cannot be currently visited because it has reached the maximum amount of 1 visit.', ], [{}, {}, 'This short URL can be visited normally.'], @@ -36,7 +37,7 @@ describe('', () => { [Mock.of({ validSince: '2020-01-01T10:30:15' }), {}, 'This short URL can be visited normally.'], [ Mock.of({ maxVisits: 10 }), - Mock.of({ total: 1 }), + Mock.of({ total: 1 }), 'This short URL can be visited normally.', ], ])('shows expected tooltip', async (meta, visitsSummary, expectedTooltip) => { diff --git a/test/visits/reducers/visitsOverview.test.ts b/test/visits/reducers/visitsOverview.test.ts index 4bf3568f..a5fbf757 100644 --- a/test/visits/reducers/visitsOverview.test.ts +++ b/test/visits/reducers/visitsOverview.test.ts @@ -5,8 +5,8 @@ import type { ShlinkState } from '../../../src/container/types'; import type { CreateVisitsAction } from '../../../src/visits/reducers/visitCreation'; import { createNewVisits } from '../../../src/visits/reducers/visitCreation'; import type { - GetVisitsOverviewAction, - VisitsOverview } from '../../../src/visits/reducers/visitsOverview'; + GetVisitsOverviewAction, ParsedVisitsOverview, + PartialVisitsSummary, VisitsOverview } from '../../../src/visits/reducers/visitsOverview'; import { loadVisitsOverview as loadVisitsOverviewCreator, visitsOverviewReducerCreator, @@ -46,45 +46,98 @@ describe('visitsOverviewReducer', () => { }); it('return visits overview on GET_OVERVIEW', () => { - const { loading, error, visitsCount } = reducer(state({ loading: true, error: false }), { - type: loadVisitsOverview.fulfilled.toString(), - payload: { visitsCount: 100 }, - }); + const action = loadVisitsOverview.fulfilled(Mock.of({ + nonOrphanVisits: { total: 100 }, + }), 'requestId'); + const { loading, error, nonOrphanVisits } = reducer(state({ loading: true, error: false }), action); expect(loading).toEqual(false); expect(error).toEqual(false); - expect(visitsCount).toEqual(100); + expect(nonOrphanVisits.total).toEqual(100); }); it.each([ [50, 53], [0, 3], - [undefined, 3], ])('returns updated amounts on CREATE_VISITS', (providedOrphanVisitsCount, expectedOrphanVisitsCount) => { - const { visitsCount, orphanVisitsCount } = reducer( - state({ visitsCount: 100, orphanVisitsCount: providedOrphanVisitsCount }), - { - type: createNewVisits.toString(), - payload: { - createdVisits: [ - Mock.of({ visit: Mock.all() }), - Mock.of({ visit: Mock.all() }), - Mock.of({ - visit: Mock.of({ visitedUrl: '' }), - }), - Mock.of({ - visit: Mock.of({ visitedUrl: '' }), - }), - Mock.of({ - visit: Mock.of({ visitedUrl: '' }), - }), - ], - }, - } as unknown as GetVisitsOverviewAction & CreateVisitsAction, + const { nonOrphanVisits, orphanVisits } = reducer( + state({ + nonOrphanVisits: { total: 100 }, + orphanVisits: { total: providedOrphanVisitsCount }, + }), + createNewVisits([ + Mock.of({ visit: Mock.all() }), + Mock.of({ visit: Mock.all() }), + Mock.of({ + visit: Mock.of({ visitedUrl: '' }), + }), + Mock.of({ + visit: Mock.of({ visitedUrl: '' }), + }), + Mock.of({ + visit: Mock.of({ visitedUrl: '' }), + }), + ]), ); - expect(visitsCount).toEqual(102); - expect(orphanVisitsCount).toEqual(expectedOrphanVisitsCount); + expect(nonOrphanVisits.total).toEqual(102); + expect(orphanVisits.total).toEqual(expectedOrphanVisitsCount); + }); + + it.each([ + [ + {} satisfies Omit, + {} satisfies Omit, + { total: 103 } satisfies PartialVisitsSummary, + { total: 203 } satisfies PartialVisitsSummary, + ], + [ + { bots: 35 } satisfies Omit, + { bots: 35 } satisfies Omit, + { total: 103, bots: 37 } satisfies PartialVisitsSummary, + { total: 203, bots: 36 } satisfies PartialVisitsSummary, + ], + [ + { nonBots: 41, bots: 85 } satisfies Omit, + { nonBots: 63, bots: 27 } satisfies Omit, + { total: 103, nonBots: 42, bots: 87 } satisfies PartialVisitsSummary, + { total: 203, nonBots: 65, bots: 28 } satisfies PartialVisitsSummary, + ], + [ + { nonBots: 56 } satisfies Omit, + { nonBots: 99 } satisfies Omit, + { total: 103, nonBots: 57 } satisfies PartialVisitsSummary, + { total: 203, nonBots: 101 } satisfies PartialVisitsSummary, + ], + ])('takes bots and non-bots into consideration when creating visits', ( + initialNonOrphanVisits, + initialOrphanVisits, + expectedNonOrphanVisits, + expectedOrphanVisits, + ) => { + const { nonOrphanVisits, orphanVisits } = reducer( + state({ + nonOrphanVisits: { total: 100, ...initialNonOrphanVisits }, + orphanVisits: { total: 200, ...initialOrphanVisits }, + }), + createNewVisits([ + Mock.of({ visit: Mock.all() }), + Mock.of({ visit: Mock.of({ potentialBot: true }) }), + Mock.of({ visit: Mock.of({ potentialBot: true }) }), + Mock.of({ + visit: Mock.of({ visitedUrl: '' }), + }), + Mock.of({ + visit: Mock.of({ visitedUrl: '' }), + }), + Mock.of({ + visit: Mock.of({ visitedUrl: '', potentialBot: true }), + }), + ]), + ); + + expect(nonOrphanVisits).toEqual(expectedNonOrphanVisits); + expect(orphanVisits).toEqual(expectedOrphanVisits); }); }); @@ -109,8 +162,30 @@ describe('visitsOverviewReducer', () => { expect(getVisitsOverview).toHaveBeenCalledTimes(1); }); - it('dispatches start and success when promise is resolved', async () => { - const resolvedOverview = Mock.of({ visitsCount: 50 }); + it.each([ + [ + // Shlink <3.5.0 + { visitsCount: 50, orphanVisitsCount: 20 } satisfies ShlinkVisitsOverview, + { + nonOrphanVisits: { total: 50, nonBots: undefined, bots: undefined }, + orphanVisits: { total: 20, nonBots: undefined, bots: undefined }, + }, + ], + [ + // Shlink >=3.5.0 + { + nonOrphanVisits: { total: 50, nonBots: 20, bots: 30 }, + orphanVisits: { total: 50, nonBots: 20, bots: 30 }, + visitsCount: 3, + orphanVisitsCount: 3, + } satisfies ShlinkVisitsOverview, + { + nonOrphanVisits: { total: 50, nonBots: 20, bots: 30 }, + orphanVisits: { total: 50, nonBots: 20, bots: 30 }, + }, + ], + ])('dispatches start and success when promise is resolved', async (serverResult, dispatchedPayload) => { + const resolvedOverview = Mock.of(serverResult); getVisitsOverview.mockResolvedValue(resolvedOverview); await loadVisitsOverview()(dispatchMock, getState, {}); @@ -121,7 +196,7 @@ describe('visitsOverviewReducer', () => { })); expect(dispatchMock).toHaveBeenNthCalledWith(2, expect.objectContaining({ type: loadVisitsOverview.fulfilled.toString(), - payload: { visitsCount: 50 }, + payload: dispatchedPayload, })); expect(getVisitsOverview).toHaveBeenCalledTimes(1); }); diff --git a/test/visits/types/helpers.test.ts b/test/visits/types/helpers.test.ts index de00bddd..75f915c3 100644 --- a/test/visits/types/helpers.test.ts +++ b/test/visits/types/helpers.test.ts @@ -8,7 +8,7 @@ import { groupNewVisitsByType, toApiParams } from '../../../src/visits/types/hel describe('visitsTypeHelpers', () => { describe('groupNewVisitsByType', () => { it.each([ - [[], { orphanVisits: [], regularVisits: [] }], + [[], { orphanVisits: [], nonOrphanVisits: [] }], ((): [CreateVisit[], GroupedNewVisits] => { const orphanVisits: CreateVisit[] = [ Mock.of({ @@ -18,7 +18,7 @@ describe('visitsTypeHelpers', () => { visit: Mock.of({ visitedUrl: '' }), }), ]; - const regularVisits: CreateVisit[] = [ + const nonOrphanVisits: CreateVisit[] = [ Mock.of({ visit: Mock.all() }), Mock.of({ visit: Mock.all() }), Mock.of({ visit: Mock.all() }), @@ -27,8 +27,8 @@ describe('visitsTypeHelpers', () => { ]; return [ - [...orphanVisits, ...regularVisits], - { orphanVisits, regularVisits }, + [...orphanVisits, ...nonOrphanVisits], + { orphanVisits, nonOrphanVisits }, ]; })(), ((): [CreateVisit[], GroupedNewVisits] => { @@ -44,16 +44,16 @@ describe('visitsTypeHelpers', () => { }), ]; - return [orphanVisits, { orphanVisits, regularVisits: [] }]; + return [orphanVisits, { orphanVisits, nonOrphanVisits: [] }]; })(), ((): [CreateVisit[], GroupedNewVisits] => { - const regularVisits: CreateVisit[] = [ + const nonOrphanVisits: CreateVisit[] = [ Mock.of({ visit: Mock.all() }), Mock.of({ visit: Mock.all() }), Mock.of({ visit: Mock.all() }), ]; - return [regularVisits, { orphanVisits: [], regularVisits }]; + return [nonOrphanVisits, { orphanVisits: [], nonOrphanVisits }]; })(), ])('groups new visits as expected', (createdVisits, expectedResult) => { expect(groupNewVisitsByType(createdVisits)).toEqual(expectedResult);