From a8e6da9bdfed06e26610b087b4e95a2f22a93fd1 Mon Sep 17 00:00:00 2001 From: Tim DiLauro Date: Fri, 23 Aug 2024 11:42:01 -0400 Subject: [PATCH] Upgrade Recharts. (PP-1531) (#127) --- package-lock.json | 361 +++++++---- package.json | 3 +- src/components/ContextProvider.tsx | 10 +- src/components/LibraryStats.tsx | 2 +- src/components/StatsCollectionsBarChart.tsx | 47 +- src/components/StatsUsageReportsGroup.tsx | 2 - .../__tests__/LibraryStats-test.tsx | 2 + src/context/appContext.ts | 9 +- src/interfaces.ts | 11 + src/stylesheets/stats.scss | 2 + tests/jest/components/Stats.test.tsx | 590 ++++++++++-------- tests/jest/testUtils/withProviders.tsx | 4 +- 12 files changed, 633 insertions(+), 410 deletions(-) diff --git a/package-lock.json b/package-lock.json index d4486cf44..66bf4bb8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,7 @@ "react-dom": "^16.8.6", "react-redux": "^7.2.9", "react-router": "^3.2.0", - "recharts": "^1.8.6", + "recharts": "^2.12.7", "redux": "^4.2.1", "redux-thunk": "^2.4.2", "request": "^2.85.0", @@ -91,6 +91,7 @@ "react-axe": "^3.3.0", "react-test-renderer": "^16.14.0", "redux-mock-store": "^1.5.4", + "resize-observer-polyfill": "^1.5.1", "sass": "^1.64.2", "sass-lint": "^1.13.1", "sass-loader": "^13.2.0", @@ -2233,11 +2234,41 @@ "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", "dev": true }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dependencies": { + "@types/d3-color": "*" + } + }, "node_modules/@types/d3-path": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.11.tgz", - "integrity": "sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==", - "dev": true + "integrity": "sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", + "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "dependencies": { + "@types/d3-time": "*" + } }, "node_modules/@types/d3-shape": { "version": "1.3.12", @@ -2248,6 +2279,16 @@ "@types/d3-path": "^1" } }, + "node_modules/@types/d3-time": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", + "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" + }, "node_modules/@types/eslint": { "version": "8.56.10", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", @@ -4802,6 +4843,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -5376,70 +5425,113 @@ } }, "node_modules/d3-array": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", - "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==" - }, - "node_modules/d3-collection": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", - "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==" + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } }, "node_modules/d3-color": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz", - "integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } }, "node_modules/d3-format": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz", - "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } }, "node_modules/d3-interpolate": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", - "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", "dependencies": { - "d3-color": "1" + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" } }, "node_modules/d3-path": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", - "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } }, "node_modules/d3-scale": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz", - "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", "dependencies": { - "d3-array": "^1.2.0", - "d3-collection": "1", - "d3-format": "1", - "d3-interpolate": "1", - "d3-time": "1", - "d3-time-format": "2" + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" } }, "node_modules/d3-shape": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", - "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", "dependencies": { - "d3-path": "1" + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" } }, "node_modules/d3-time": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", - "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } }, "node_modules/d3-time-format": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", - "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", "dependencies": { - "d3-time": "1" + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" } }, "node_modules/damerau-levenshtein": { @@ -7104,8 +7196,7 @@ "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" }, "node_modules/events": { "version": "3.3.0", @@ -7293,6 +7384,14 @@ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true }, + "node_modules/fast-equals": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz", + "integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -8902,6 +9001,14 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, "node_modules/interpret": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", @@ -11456,11 +11563,6 @@ "integrity": "sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==", "dev": true }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" - }, "node_modules/lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", @@ -11533,11 +11635,6 @@ "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", "dev": true }, - "node_modules/lodash.throttle": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", - "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==" - }, "node_modules/lodash.truncate": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", @@ -11749,11 +11846,6 @@ "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==" }, - "node_modules/math-expression-evaluator": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.4.0.tgz", - "integrity": "sha512-4vRUvPyxdO8cWULGTh9dZWL2tZK6LDBvj+OGHBER7poH9Qdt7kXEoj20wiz4lQUbUXQZFjPbe5mVDo9nutizCw==" - }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", @@ -14511,20 +14603,6 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, - "node_modules/react-resize-detector": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-2.3.0.tgz", - "integrity": "sha512-oCAddEWWeFWYH5FAcHdBYcZjAw9fMzRUK9sWSx6WvSSOPVRxcHd5zTIGy/mOus+AhN/u6T4TMiWxvq79PywnJQ==", - "dependencies": { - "lodash.debounce": "^4.0.8", - "lodash.throttle": "^4.1.1", - "prop-types": "^15.6.0", - "resize-observer-polyfill": "^1.5.0" - }, - "peerDependencies": { - "react": "^0.14.7 || ^15.0.0 || ^16.0.0" - } - }, "node_modules/react-router": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/react-router/-/react-router-3.2.6.tgz", @@ -14544,18 +14622,41 @@ } }, "node_modules/react-smooth": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-1.0.6.tgz", - "integrity": "sha512-B2vL4trGpNSMSOzFiAul9kFAsxTukL9Wyy9EXtkQy3GJr6sZqW9e1nShdVOJ3hRYamPZ94O17r3Q0bjSw3UYtg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.1.tgz", + "integrity": "sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w==", "dependencies": { - "lodash": "~4.17.4", - "prop-types": "^15.6.0", - "raf": "^3.4.0", - "react-transition-group": "^2.5.0" + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" }, "peerDependencies": { - "react": "^15.0.0 || ^16.0.0", - "react-dom": "^15.0.0 || ^16.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-smooth/node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/react-smooth/node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" } }, "node_modules/react-test-renderer": { @@ -14681,25 +14782,25 @@ "dev": true }, "node_modules/recharts": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-1.8.6.tgz", - "integrity": "sha512-UlfSEOnZRAxxaH33Fc86yHEcqN+IRauPP31NfVvlGudtwVZEIb2RFI5b1J3npQo7XyoSnkUodg3Ha6EupkV+SQ==", + "version": "2.12.7", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.7.tgz", + "integrity": "sha512-hlLJMhPQfv4/3NBSAyq3gzGg4h2v69RJh6KU7b3pXYNNAELs9kEoXOjbkxdXpALqKBoVmVptGfLpxdaVYqjmXQ==", "dependencies": { - "classnames": "^2.2.5", - "core-js": "^3.4.2", - "d3-interpolate": "^1.3.0", - "d3-scale": "^2.1.0", - "d3-shape": "^1.2.0", - "lodash": "^4.17.5", - "prop-types": "^15.6.0", - "react-resize-detector": "^2.3.0", - "react-smooth": "^1.0.5", - "recharts-scale": "^0.4.2", - "reduce-css-calc": "^1.3.0" + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^16.10.2", + "react-smooth": "^4.0.0", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" }, "peerDependencies": { - "react": "^15.0.0 || ^16.0.0", - "react-dom": "^15.0.0 || ^16.0.0" + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" } }, "node_modules/recharts-scale": { @@ -14752,29 +14853,6 @@ "node": ">=8" } }, - "node_modules/reduce-css-calc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz", - "integrity": "sha512-0dVfwYVOlf/LBA2ec4OwQ6p3X9mYxn/wOl2xTcLwjnPYrkgEfPx3VI4eGCH3rQLlPISG5v9I9bkZosKsNRTRKA==", - "dependencies": { - "balanced-match": "^0.4.2", - "math-expression-evaluator": "^1.2.14", - "reduce-function-call": "^1.0.1" - } - }, - "node_modules/reduce-css-calc/node_modules/balanced-match": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", - "integrity": "sha512-STw03mQKnGUYtoNjmowo4F2cRmIIxYEGiMsjjwla/u5P1lxadj/05WkNaFjNiKTgJkj8KiXbgAiRTmcQRwQNtg==" - }, - "node_modules/reduce-function-call": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.3.tgz", - "integrity": "sha512-Hl/tuV2VDgWgCSEeWMLwxLZqX7OK59eU1guxXsRKTAyeYimivsKdtcV4fu3r710tpG5GmDKDhQ0HSZLExnNmyQ==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/redux": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", @@ -15026,7 +15104,8 @@ "node_modules/resize-observer-polyfill": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", - "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "dev": true }, "node_modules/resolve": { "version": "2.0.0-next.5", @@ -17178,6 +17257,11 @@ "node": ">=0.6.0" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + }, "node_modules/tinycolor2": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", @@ -18135,6 +18219,35 @@ "extsprintf": "^1.2.0" } }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/victory-vendor/node_modules/@types/d3-shape": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", + "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "dependencies": { + "@types/d3-path": "*" + } + }, "node_modules/vscode-oniguruma": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", diff --git a/package.json b/package.json index 2f92cba91..35b87ca67 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "react-dom": "^16.8.6", "react-redux": "^7.2.9", "react-router": "^3.2.0", - "recharts": "^1.8.6", + "recharts": "^2.12.7", "redux": "^4.2.1", "redux-thunk": "^2.4.2", "request": "^2.85.0", @@ -119,6 +119,7 @@ "react-axe": "^3.3.0", "react-test-renderer": "^16.14.0", "redux-mock-store": "^1.5.4", + "resize-observer-polyfill": "^1.5.1", "sass": "^1.64.2", "sass-lint": "^1.13.1", "sass-loader": "^13.2.0", diff --git a/src/components/ContextProvider.tsx b/src/components/ContextProvider.tsx index faa6d3cac..8a4786f67 100644 --- a/src/components/ContextProvider.tsx +++ b/src/components/ContextProvider.tsx @@ -2,7 +2,11 @@ import * as React from "react"; import { Store } from "@reduxjs/toolkit"; import * as PropTypes from "prop-types"; import buildStore, { RootState } from "../store"; -import { FeatureFlags, PathFor } from "../interfaces"; +import { + DashboardCollectionsBarChart, + FeatureFlags, + PathFor, +} from "../interfaces"; import Admin from "../models/Admin"; import PathForProvider from "@thepalaceproject/web-opds-client/lib/components/context/PathForContext"; import ActionCreator from "../actions"; @@ -10,7 +14,7 @@ import AppContextProvider, { AppContextType } from "../context/appContext"; // Note: Not all elements of these props make it into the `ContextProvider`. // Some are exposed only through the `AppContextProvider` component (which -// this component wraps. +// this component wraps). // TODO: We should get this interface to the point where we can just extend // the `ConfigurationSettings` interface. export interface ContextProviderProps extends React.Props { @@ -25,6 +29,7 @@ export interface ContextProviderProps extends React.Props { }[]; featureFlags: FeatureFlags; quicksightPagePath?: string; + dashboardCollectionsBarChart?: DashboardCollectionsBarChart; } /** Provides a redux store, configuration options, and a function to create URLs @@ -109,6 +114,7 @@ export default class ContextProvider extends React.Component< admin: this.admin, featureFlags: this.props.featureFlags, quicksightPagePath: this.props.quicksightPagePath, + dashboardCollectionsBarChart: this.props.dashboardCollectionsBarChart, }; return ( diff --git a/src/components/LibraryStats.tsx b/src/components/LibraryStats.tsx index 245e10eb9..04823a8b9 100644 --- a/src/components/LibraryStats.tsx +++ b/src/components/LibraryStats.tsx @@ -31,7 +31,7 @@ export const inventoryKeyToLabelMap = { export const ALL_LIBRARIES_HEADING = "Dashboard for All Authorized Libraries"; /** Displays statistics about patrons, licenses, and collections from the server, - for a single library or all libraries the admin has access to. */ + for a single library or all libraries to which the admin has access. */ const LibraryStats = ({ stats, library }: LibraryStatsProps) => { const { name: libraryName, diff --git a/src/components/StatsCollectionsBarChart.tsx b/src/components/StatsCollectionsBarChart.tsx index 6bb0f48d7..52a1d65bc 100644 --- a/src/components/StatsCollectionsBarChart.tsx +++ b/src/components/StatsCollectionsBarChart.tsx @@ -1,5 +1,7 @@ import * as React from "react"; +import { useDashboardCollectionsBarChartSettings } from "../context/appContext"; import { CollectionInventory } from "../interfaces"; +import { ValueType } from "recharts/types/component/DefaultTooltipContent"; import { Bar, BarChart, @@ -12,6 +14,17 @@ import { import { inventoryKeyToLabelMap } from "./LibraryStats"; import { formatNumber } from "../utils/sharedFunctions"; +const stackId = "collections"; +const barSize = 50; +const meteredColor = "#606060"; +const unlimitedColor = "#404040"; +const openAccessColor = "#202020"; + +type Props = { + collections: CollectionInventory[]; + ResponsiveContainer?: any; +}; + const StatsCollectionsBarChart = ({ collections }: Props) => { const chartItems = collections ?.map(({ name, inventory, inventoryByMedium }) => ({ @@ -20,20 +33,23 @@ const StatsCollectionsBarChart = ({ collections }: Props) => { _by_medium: inventoryByMedium || {}, })) .sort((a, b) => (a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1)); + const chartWidth = useDashboardCollectionsBarChartSettings()?.width || "100%"; return ( - + { } /> @@ -76,15 +92,12 @@ type chartTooltipData = { perMedium?: OneLevelStatistics; }; -type Props = { - collections: CollectionInventory[]; -}; /* Customize the Rechart tooltip to provide additional information */ export const CustomTooltip = ({ active, payload, label: collectionName, -}: TooltipProps) => { +}: TooltipProps) => { if (!active) { return null; } diff --git a/src/components/StatsUsageReportsGroup.tsx b/src/components/StatsUsageReportsGroup.tsx index 390443eac..c8697f5e0 100644 --- a/src/components/StatsUsageReportsGroup.tsx +++ b/src/components/StatsUsageReportsGroup.tsx @@ -1,10 +1,8 @@ import React = require("react"); import { Button } from "library-simplified-reusable-components"; import StatsGroup from "./StatsGroup"; -import { InventoryStatistics } from "../interfaces"; import InventoryReportRequestModal from "./InventoryReportRequestModal"; import { useState } from "react"; -import { useAppContext } from "../context/appContext"; type Props = { heading?: string; diff --git a/src/components/__tests__/LibraryStats-test.tsx b/src/components/__tests__/LibraryStats-test.tsx index 2b058d1b3..5f58e2fcf 100644 --- a/src/components/__tests__/LibraryStats-test.tsx +++ b/src/components/__tests__/LibraryStats-test.tsx @@ -25,6 +25,8 @@ const getAllProviders = ({ isSysAdmin = false } = {}) => { return componentWithProviders({ contextProviderProps }); }; +global.ResizeObserver = require("resize-observer-polyfill"); + describe("LibraryStats", () => { // Convert from the API format to our in-app format. const statisticsData = normalizeStatistics(statisticsApiResponseData); diff --git a/src/context/appContext.ts b/src/context/appContext.ts index fb636a855..aa2208941 100644 --- a/src/context/appContext.ts +++ b/src/context/appContext.ts @@ -1,5 +1,5 @@ import { createContext, useContext } from "react"; -import { FeatureFlags } from "../interfaces"; +import { DashboardCollectionsBarChart, FeatureFlags } from "../interfaces"; import Admin from "../models/Admin"; export type AppContextType = { @@ -8,6 +8,7 @@ export type AppContextType = { admin: Admin; featureFlags: FeatureFlags; quicksightPagePath: string; + dashboardCollectionsBarChart?: DashboardCollectionsBarChart; }; // Don't export this, since we always want the error handling behavior of our hook. @@ -16,7 +17,9 @@ const AppContext = createContext(undefined); export const useAppContext = (): AppContextType => { const context = useContext(AppContext); if (context === undefined) { - throw new Error("useAppContext must be used within an AppContext povider."); + throw new Error( + "useAppContext must be used within an AppContext provider." + ); } return context; }; @@ -25,5 +28,7 @@ export const useCsrfToken = () => useAppContext().csrfToken; export const useAppAdmin = () => useAppContext().admin; export const useAppEmail = () => useAppAdmin().email; export const useAppFeatureFlags = () => useAppContext().featureFlags; +export const useDashboardCollectionsBarChartSettings = () => + useAppContext().dashboardCollectionsBarChart; export default AppContext.Provider; diff --git a/src/interfaces.ts b/src/interfaces.ts index 638601bad..b12a6942b 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -32,6 +32,17 @@ export interface ConfigurationSettings { /** `quickSightPagePath` contains the URL to the QuickSight dashboard page. Currently, this value does not change, so we can share it via fixed config. */ quicksightPagePath: string; + + /** Configuration for dashboard collections barchart. */ + dashboardCollectionsBarChart?: DashboardCollectionsBarChart; +} + +export interface DashboardCollectionsBarChart { + width?: number; +} + +export interface TestingFlags { + [key: string]: boolean; } export interface FeatureFlags { diff --git a/src/stylesheets/stats.scss b/src/stylesheets/stats.scss index de0007aad..c5bf99f79 100644 --- a/src/stylesheets/stats.scss +++ b/src/stylesheets/stats.scss @@ -76,6 +76,7 @@ text-align: left; font-style: italic; font-size: small; + text-wrap: balance; } .stat-usage-reports { @@ -102,6 +103,7 @@ h3 { font-weight: bolder; text-transform: uppercase; + text-wrap: balance; margin: 10px; margin-bottom: 2px; } diff --git a/tests/jest/components/Stats.test.tsx b/tests/jest/components/Stats.test.tsx index 2095f58af..731bf79f9 100644 --- a/tests/jest/components/Stats.test.tsx +++ b/tests/jest/components/Stats.test.tsx @@ -1,8 +1,6 @@ import * as React from "react"; import { render } from "@testing-library/react"; -import LibraryStats, { - ALL_LIBRARIES_HEADING, -} from "../../../src/components/LibraryStats"; +import { ALL_LIBRARIES_HEADING } from "../../../src/components/LibraryStats"; import { CustomTooltip } from "../../../src/components/StatsCollectionsBarChart"; import { componentWithProviders, @@ -28,6 +26,8 @@ import { api } from "../../../src/features/api/apiSlice"; const normalizedData = normalizeStatistics(statisticsApiResponseData); +global.ResizeObserver = require("resize-observer-polyfill"); + describe("Dashboard Statistics", () => { // NB: This adds test to the already existing tests in: // - `src/components/__tests__/LibraryStats-test.tsx`. @@ -274,7 +274,7 @@ describe("Dashboard Statistics", () => { }); }); - it("shows the right groups without a library", () => { + it("shows the right groups with/out a library", () => { const { getAllByRole } = renderWithProviders(); const groupHeadings = getAllByRole("heading", { level: 3 }); @@ -290,282 +290,354 @@ describe("Dashboard Statistics", () => { }); }); }); - }); - describe("requesting inventory reports", () => { - // Convert from the API format to our in-app format. - const statisticsData = normalizeStatistics(statisticsApiResponseData); - const librariesStatsTestDataByKey = statisticsData.libraries.reduce( - (map, library) => ({ ...map, [library.key]: library }), - {} - ); - const sampleStatsData = librariesStatsTestDataByKey[sampleLibraryKey]; - - const systemAdmin = [{ role: "system" }]; - const managerAll = [{ role: "manager-all" }]; - const librarianAll = [{ role: "librarian-all" }]; - - const fakeQuickSightHref = "https://example.com/fakeQS"; - const baseContextProviderProps = { - csrfToken: "", - featureFlags: { reportsOnlyForSysadmins: false }, - quicksightPagePath: fakeQuickSightHref, - }; - - const renderFor = ( - onlySysadmins: boolean, - roles: { role: string; library?: string }[] - ) => { - const contextProviderProps: ContextProviderProps = { - ...baseContextProviderProps, - featureFlags: { reportsOnlyForSysadmins: onlySysadmins }, - roles, - }; + describe("shows the correct UI with/out sysadmin role", () => { + const systemAdmin = [{ role: "system" }]; + const managerAll = [{ role: "manager-all" }]; + const librarianAll = [{ role: "librarian-all" }]; + + const collectionNames = [ + "New BiblioBoard Test", + "New Bibliotheca Test Collection", + "Palace Bookshelf", + "TEST Baker & Taylor", + "TEST Palace Marketplace", + ]; - const { - container, - getByRole, - queryByRole, - } = renderWithProviders( - , - { contextProviderProps } - ); + it("tests BarChart component", () => { + const contextProviderProps: Partial = { + roles: systemAdmin, + dashboardCollectionsBarChart: { width: 800 }, + }; + const { container, getByRole } = renderWithProviders( + , + { contextProviderProps } + ); - // We should always render a Usage reports group when a library is specified. - getByRole("heading", { level: 3, name: statGroupToHeading.usageReports }); - const usageReportLink = getByRole("link", { name: /View Usage/i }); - expect(usageReportLink).toHaveAttribute("href", fakeQuickSightHref); - - const result = queryByRole("button", { name: /Request Report/i }); - // Clean up the container after each render. - document.body.removeChild(container); - return result; - }; - - it("shows inventory reports only for sysadmins, if feature flag set", async () => { - // If the feature flag is set, the button should be visible only to sysadmins. - expect(renderFor(true, systemAdmin)).not.toBeNull(); - expect(renderFor(true, managerAll)).toBeNull(); - expect(renderFor(true, librarianAll)).toBeNull(); - // If the feature flag is false, the button should be visible to all users. - expect(renderFor(false, systemAdmin)).not.toBeNull(); - expect(renderFor(false, managerAll)).not.toBeNull(); - expect(renderFor(false, librarianAll)).not.toBeNull(); - }); - }); + const collectionsHeading = getByRole("heading", { + level: 3, + name: statGroupToHeading.collections, + }); + const collectionsGroup = collectionsHeading.closest(".stat-group"); + const barChartAxisTick = collectionsGroup.querySelectorAll( + ".recharts-cartesian-axis-tick" + ); - describe("charting - custom tooltip", () => { - const defaultLabel = "Collection X"; - const summaryInventory = { - availableTitles: 7953, - licensedTitles: 7974, - meteredLicenseTitles: 7974, - meteredLicensesAvailable: 75446, - meteredLicensesOwned: 301541, - openAccessTitles: 0, - titles: 7974, - unlimitedLicenseTitles: 0, - }; - const perMediumInventory = { - Audio: { - availableTitles: 148, - licensedTitles: 165, - meteredLicenseTitles: 165, - meteredLicensesAvailable: 221, - meteredLicensesOwned: 392, - openAccessTitles: 0, - titles: 165, - unlimitedLicenseTitles: 0, - }, - Book: { - availableTitles: 7805, - licensedTitles: 7809, - meteredLicenseTitles: 7809, - meteredLicensesAvailable: 75225, - meteredLicensesOwned: 301149, - openAccessTitles: 0, - titles: 7809, - unlimitedLicenseTitles: 0, - }, - }; - const defaultChartItemWithoutPerMediumInventory = { - name: defaultLabel, - ...summaryInventory, - }; - const defaultChartItemWithPerMediumInventory = { - ...defaultChartItemWithoutPerMediumInventory, - _by_medium: perMediumInventory, - }; - const defaultPayload = [ - { - fill: "#606060", - dataKey: "meteredLicenseTitles", - name: "Metered License Titles", - color: "#606060", - value: 7974, - }, - { - fill: "#404040", - dataKey: "unlimitedLicenseTitles", - name: "Unlimited License Titles", - color: "#404040", - value: 0, - }, - { - fill: "#202020", - dataKey: "openAccessTitles", - name: "Open Access Titles", - color: "#202020", - value: 0, - }, - ]; - - const populateTooltipProps = ({ - active = true, - label = defaultLabel, - payload = [], - chartItem = undefined, - }) => { - const constructedChartItem = !chartItem - ? chartItem - : { - ...chartItem, - name: label, - }; - const constructedPayload = payload.map((entry) => ({ - ...entry, - payload: constructedChartItem, - })); - return { - active, - label, - payload: constructedPayload, - }; - }; - - /** - * Helper function to test passing tests for a tooltip - * - * @param tooltipProps - passed to the component - * @param expectedInventoryItemText - the expected inventory item text content - */ - const expectPassingTestsForActiveTooltip = ({ - tooltipProps, - expectedInventoryItemText, - }) => { - const { container, getByRole } = render( - - ); - const tooltipContent = container.querySelector(".customTooltip"); - - const detail = tooltipContent.querySelector(".customTooltipDetail"); - const detailChildren = detail.children; - const heading = getByRole("heading", { level: 1, name: "Collection X" }); - const items = tooltipContent.querySelectorAll("p.customTooltipItem"); - const divider = detail.querySelector("hr"); - - expect(heading).toHaveTextContent("Collection X"); - - // Eight (8) metrics in the following order. - expect(items).toHaveLength(8); - // The expected inventory item labels array should be the same length. - expect(expectedInventoryItemText).toHaveLength(items.length); - // And the items should contain at least the expected text. - Array.from(items).forEach((item, index) => { - expect(item).toHaveTextContent(expectedInventoryItemText[index]); - }); + // We expect the first ticks to be along the y-axis, which + // should have our collection names. + collectionNames.forEach((name, index) => { + expect(barChartAxisTick[index]).toHaveTextContent(name); + }); - // The heading should be at the top and the divider (`hr`) - // should be between the third and fourth statistics. - expect(detailChildren).toHaveLength(10); - expect(heading).toEqual(detailChildren[0]); - expect(items[0]).toEqual(detailChildren[1]); - expect(items[2]).toEqual(detailChildren[3]); - expect(divider).toEqual(detailChildren[4]); - expect(items[3]).toEqual(detailChildren[5]); - expect(items[7]).toEqual(detailChildren[9]); - }; - - it("should not render when active is false", () => { - // Recharts sticks some extra props - const tooltipProps = populateTooltipProps({ - active: false, - chartItem: defaultChartItemWithPerMediumInventory, - payload: defaultPayload, + // Clean up the container after each render. + document.body.removeChild(container); }); - const { container } = render(); - const tooltipContent = container.querySelectorAll(".customTooltip"); + it("shows collection bar chart for sysadmins, but list for others", () => { + // We'll use this function to test multiple scenarios. + const testFor = ( + expectBarChart: boolean, + roles: { role: string; library?: string }[] + ) => { + const contextProviderProps: Partial = { roles }; + const { container, getByRole } = renderWithProviders( + , + { contextProviderProps } + ); + + const collectionsHeading = getByRole("heading", { + level: 3, + name: statGroupToHeading.collections, + }); + const collectionsGroup = collectionsHeading.closest(".stat-group"); + + if (expectBarChart) { + collectionsGroup.querySelector(".recharts-responsive-container"); + } else { + const list = collectionsGroup.querySelector("ul"); + const items = list.querySelectorAll("li"); + expect(items.length).toBe(collectionNames.length); + + collectionNames.forEach((name: string) => { + expect(list).toHaveTextContent(name); + }); + items.forEach((item, index) => { + expect(item).toHaveTextContent(collectionNames[index]); + }); + } + + // Clean up the container after each render. + document.body.removeChild(container); + }; + + // If the feature flag is set, the button should be visible only to sysadmins. + testFor(true, systemAdmin); + testFor(false, managerAll); + testFor(false, librarianAll); + }); - expect(tooltipContent).toHaveLength(0); - }); - it("should render when active is true", () => { - const tooltipProps = populateTooltipProps({ - active: true, - chartItem: defaultChartItemWithoutPerMediumInventory, - payload: defaultPayload, + it("shows inventory reports only for sysadmins, if sysadmin-only flag set", () => { + const fakeQuickSightHref = "https://example.com/fakeQS"; + + // We'll use this function to test multiple scenarios. + const renderFor = ( + onlySysadmins: boolean, + roles: { role: string; library?: string }[] + ) => { + const contextProviderProps: Partial = { + featureFlags: { reportsOnlyForSysadmins: onlySysadmins }, + roles, + quicksightPagePath: fakeQuickSightHref, + }; + const { container, getByRole, queryByRole } = renderWithProviders( + , + { + contextProviderProps, + } + ); + + // We should always render a Usage reports group when a library is specified. + getByRole("heading", { + level: 3, + name: statGroupToHeading.usageReports, + }); + const usageReportLink = getByRole("link", { name: /View Usage/i }); + expect(usageReportLink).toHaveAttribute("href", fakeQuickSightHref); + + const result = queryByRole("button", { name: /Request Report/i }); + + // Clean up the container after each render. + document.body.removeChild(container); + return result; + }; + + // If the feature flag is set, the button should be visible only to sysadmins. + expect(renderFor(true, systemAdmin)).not.toBeNull(); + expect(renderFor(true, managerAll)).toBeNull(); + expect(renderFor(true, librarianAll)).toBeNull(); + // If the feature flag is false, the button should be visible to all users. + expect(renderFor(false, systemAdmin)).not.toBeNull(); + expect(renderFor(false, managerAll)).not.toBeNull(); + expect(renderFor(false, librarianAll)).not.toBeNull(); }); + }); - const expectedInventoryItemText = [ - "Titles:", - "Available Titles:", - "Metered License Titles:", - "Licensed Titles:", - "Metered Licenses Available:", - "Metered Licenses Owned:", - "Open Access Titles:", - "Unlimited License Titles:", + describe("charting - custom tooltip", () => { + const defaultLabel = "Collection X"; + const summaryInventory = { + availableTitles: 7953, + licensedTitles: 7974, + meteredLicenseTitles: 7974, + meteredLicensesAvailable: 75446, + meteredLicensesOwned: 301541, + openAccessTitles: 0, + titles: 7974, + unlimitedLicenseTitles: 0, + }; + const perMediumInventory = { + Audio: { + availableTitles: 148, + licensedTitles: 165, + meteredLicenseTitles: 165, + meteredLicensesAvailable: 221, + meteredLicensesOwned: 392, + openAccessTitles: 0, + titles: 165, + unlimitedLicenseTitles: 0, + }, + Book: { + availableTitles: 7805, + licensedTitles: 7809, + meteredLicenseTitles: 7809, + meteredLicensesAvailable: 75225, + meteredLicensesOwned: 301149, + openAccessTitles: 0, + titles: 7809, + unlimitedLicenseTitles: 0, + }, + }; + const defaultChartItemWithoutPerMediumInventory = { + name: defaultLabel, + ...summaryInventory, + }; + const defaultChartItemWithPerMediumInventory = { + ...defaultChartItemWithoutPerMediumInventory, + _by_medium: perMediumInventory, + }; + const defaultPayload = [ + { + fill: "#606060", + dataKey: "meteredLicenseTitles", + name: "Metered License Titles", + color: "#606060", + value: 7974, + }, + { + fill: "#404040", + dataKey: "unlimitedLicenseTitles", + name: "Unlimited License Titles", + color: "#404040", + value: 0, + }, + { + fill: "#202020", + dataKey: "openAccessTitles", + name: "Open Access Titles", + color: "#202020", + value: 0, + }, ]; - expectPassingTestsForActiveTooltip({ + const populateTooltipProps = ({ + active = true, + label = defaultLabel, + payload = [], + chartItem = undefined, + }) => { + const constructedChartItem = !chartItem + ? chartItem + : { + ...chartItem, + name: label, + }; + const constructedPayload = payload.map((entry) => ({ + ...entry, + payload: constructedChartItem, + })); + return { + active, + label, + payload: constructedPayload, + }; + }; + + /** + * Helper function to test passing tests for a tooltip + * + * @param tooltipProps - passed to the component + * @param expectedInventoryItemText - the expected inventory item text content + */ + const expectPassingTestsForActiveTooltip = ({ tooltipProps, expectedInventoryItemText, + }) => { + const { container, getByRole } = render( + + ); + const tooltipContent = container.querySelector(".customTooltip"); + + const detail = tooltipContent.querySelector(".customTooltipDetail"); + const detailChildren = detail.children; + const heading = getByRole("heading", { + level: 1, + name: "Collection X", + }); + const items = tooltipContent.querySelectorAll("p.customTooltipItem"); + const divider = detail.querySelector("hr"); + + expect(heading).toHaveTextContent("Collection X"); + + // Eight (8) metrics in the following order. + expect(items).toHaveLength(8); + // The expected inventory item labels array should be the same length. + expect(expectedInventoryItemText).toHaveLength(items.length); + // And the items should contain at least the expected text. + Array.from(items).forEach((item, index) => { + expect(item).toHaveTextContent(expectedInventoryItemText[index]); + }); + + // The heading should be at the top and the divider (`hr`) + // should be between the third and fourth statistics. + expect(detailChildren).toHaveLength(10); + expect(heading).toEqual(detailChildren[0]); + expect(items[0]).toEqual(detailChildren[1]); + expect(items[2]).toEqual(detailChildren[3]); + expect(divider).toEqual(detailChildren[4]); + expect(items[3]).toEqual(detailChildren[5]); + expect(items[7]).toEqual(detailChildren[9]); + }; + + it("should not render when active is false", () => { + // Recharts sticks some extra props + const tooltipProps = populateTooltipProps({ + active: false, + chartItem: defaultChartItemWithPerMediumInventory, + payload: defaultPayload, + }); + + const { container } = render(); + const tooltipContent = container.querySelectorAll(".customTooltip"); + + expect(tooltipContent).toHaveLength(0); }); - }); - it("should render without per-medium inventory", () => { - const tooltipProps = populateTooltipProps({ - active: true, - chartItem: defaultChartItemWithoutPerMediumInventory, - payload: defaultPayload, - }); + it("should render when active is true", () => { + const tooltipProps = populateTooltipProps({ + active: true, + chartItem: defaultChartItemWithoutPerMediumInventory, + payload: defaultPayload, + }); - const expectedInventoryItemText = [ - "Titles: 7,974", - "Available Titles: 7,953", - "Metered License Titles: 7,974", - "Licensed Titles: 7,974", - "Metered Licenses Available: 75,446", - "Metered Licenses Owned: 301,541", - "Open Access Titles: 0", - "Unlimited License Titles: 0", - ]; + const expectedInventoryItemText = [ + "Titles:", + "Available Titles:", + "Metered License Titles:", + "Licensed Titles:", + "Metered Licenses Available:", + "Metered Licenses Owned:", + "Open Access Titles:", + "Unlimited License Titles:", + ]; - expectPassingTestsForActiveTooltip({ - tooltipProps, - expectedInventoryItemText, + expectPassingTestsForActiveTooltip({ + tooltipProps, + expectedInventoryItemText, + }); }); - }); - it("should render additional detail with per-medium inventory", () => { - const tooltipProps = populateTooltipProps({ - active: true, - chartItem: defaultChartItemWithPerMediumInventory, - payload: defaultPayload, + it("should render without per-medium inventory", () => { + const tooltipProps = populateTooltipProps({ + active: true, + chartItem: defaultChartItemWithoutPerMediumInventory, + payload: defaultPayload, + }); + + const expectedInventoryItemText = [ + "Titles: 7,974", + "Available Titles: 7,953", + "Metered License Titles: 7,974", + "Licensed Titles: 7,974", + "Metered Licenses Available: 75,446", + "Metered Licenses Owned: 301,541", + "Open Access Titles: 0", + "Unlimited License Titles: 0", + ]; + + expectPassingTestsForActiveTooltip({ + tooltipProps, + expectedInventoryItemText, + }); }); + it("should render additional detail with per-medium inventory", () => { + const tooltipProps = populateTooltipProps({ + active: true, + chartItem: defaultChartItemWithPerMediumInventory, + payload: defaultPayload, + }); - const expectedInventoryItemText = [ - "Titles: 7,974 (Audio: 165, Book: 7,809)", - "Available Titles: 7,953 (Audio: 148, Book: 7,805)", - "Metered License Titles: 7,974 (Audio: 165, Book: 7,809)", - "Licensed Titles: 7,974 (Audio: 165, Book: 7,809)", - "Metered Licenses Available: 75,446 (Audio: 221, Book: 75,225)", - "Metered Licenses Owned: 301,541 (Audio: 392, Book: 301,149)", - "Open Access Titles: 0", - "Unlimited License Titles: 0", - ]; + const expectedInventoryItemText = [ + "Titles: 7,974 (Audio: 165, Book: 7,809)", + "Available Titles: 7,953 (Audio: 148, Book: 7,805)", + "Metered License Titles: 7,974 (Audio: 165, Book: 7,809)", + "Licensed Titles: 7,974 (Audio: 165, Book: 7,809)", + "Metered Licenses Available: 75,446 (Audio: 221, Book: 75,225)", + "Metered Licenses Owned: 301,541 (Audio: 392, Book: 301,149)", + "Open Access Titles: 0", + "Unlimited License Titles: 0", + ]; - expectPassingTestsForActiveTooltip({ - tooltipProps, - expectedInventoryItemText, + expectPassingTestsForActiveTooltip({ + tooltipProps, + expectedInventoryItemText, + }); }); }); }); diff --git a/tests/jest/testUtils/withProviders.tsx b/tests/jest/testUtils/withProviders.tsx index a3183431e..3e7a153f5 100644 --- a/tests/jest/testUtils/withProviders.tsx +++ b/tests/jest/testUtils/withProviders.tsx @@ -23,7 +23,7 @@ const defaultReduxStore = store; // The `csrfToken` context provider prop is required, so we provide // a default value here, so it can be easily merged with other props. -const defaultContextProviderProps: ContextProviderProps = { +const requiredContextProviderProps: ContextProviderProps = { csrfToken: "", featureFlags: defaultFeatureFlags, }; @@ -49,7 +49,7 @@ export const componentWithProviders = ({ queryClient = new QueryClient(), }: TestProviderWrapperOptions = {}): React.FunctionComponent => { const effectiveContextProviderProps = { - ...defaultContextProviderProps, + ...requiredContextProviderProps, ...contextProviderProps, ...reduxProviderProps.store, // Context and Redux Provider stores must match. };