From 024293ebef8d8e378dd1ba50e5d4c5dfd1e3bd3b Mon Sep 17 00:00:00 2001 From: Awais Jibran Date: Thu, 29 Oct 2020 15:23:44 +0500 Subject: [PATCH] test(support-tools-page): add testing for support tools page --- jest.config.js | 7 +- package-lock.json | 519 ++++++++++++++++++ package.json | 9 +- src/{ => components/common}/PageLoading.jsx | 6 +- src/components/common/PageLoading.test.jsx | 33 ++ .../__snapshots__/PageLoading.test.jsx.snap | 25 + src/dates/formatDate.js | 2 +- src/dates/formatDate.test.js | 13 + src/index.jsx | 16 +- src/setupTest.js | 16 + src/support-home/SupportHomePage.jsx | 17 + src/support-home/SupportHomePage.test.jsx | 38 ++ .../SupportHomePage.test.jsx.snap | 22 + src/user-messages/messages.js | 4 + src/users/CourseSummary.jsx | 55 +- src/users/EnrollmentForm.jsx | 2 +- src/users/EntitlementForm.jsx | 2 +- src/users/Entitlements.jsx | 2 +- src/users/UserPage.jsx | 60 +- src/users/UserPage.test.jsx | 121 ++++ src/users/UserSearch.test.jsx | 71 +++ src/users/UserSummary.jsx | 2 +- .../__snapshots__/UserPage.test.jsx.snap | 51 ++ .../__snapshots__/UserSearch.test.jsx.snap | 32 ++ src/users/{ => data}/api.js | 105 ++-- src/utils/index.js | 10 + src/utils/index.test.js | 32 ++ 27 files changed, 1143 insertions(+), 129 deletions(-) rename src/{ => components/common}/PageLoading.jsx (87%) create mode 100644 src/components/common/PageLoading.test.jsx create mode 100644 src/components/common/__snapshots__/PageLoading.test.jsx.snap create mode 100644 src/dates/formatDate.test.js create mode 100644 src/support-home/SupportHomePage.jsx create mode 100644 src/support-home/SupportHomePage.test.jsx create mode 100644 src/support-home/__snapshots__/SupportHomePage.test.jsx.snap create mode 100644 src/user-messages/messages.js create mode 100644 src/users/UserPage.test.jsx create mode 100644 src/users/UserSearch.test.jsx create mode 100644 src/users/__snapshots__/UserPage.test.jsx.snap create mode 100644 src/users/__snapshots__/UserSearch.test.jsx.snap rename src/users/{ => data}/api.js (79%) create mode 100644 src/utils/index.js create mode 100644 src/utils/index.test.js diff --git a/jest.config.js b/jest.config.js index 7c936b4c0..e9c25bca6 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,10 +1,11 @@ const { createConfig } = require('@edx/frontend-build'); module.exports = createConfig('jest', { - setupFiles: [ - '/src/setupTest.js', - ], + setupFiles: ['/src/setupTest.js'], + collectCoverage: true, + collectCoverageFrom: ['src/**/*.{js,jsx}'], coveragePathIgnorePatterns: [ + '/node_modules/', 'src/setupTest.js', 'src/i18n', ], diff --git a/package-lock.json b/package-lock.json index 427001225..974dbea83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7516,6 +7516,12 @@ "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", "dev": true }, + "array-filter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-1.0.0.tgz", + "integrity": "sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=", + "dev": true + }, "array-find-index": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", @@ -9499,6 +9505,12 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "check-prop-types": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/check-prop-types/-/check-prop-types-1.1.2.tgz", + "integrity": "sha512-hGDrZ1yhRgKuP1yzZ5sUX/PPmlKBLOF1GyF0Z008Sienko3BFZmlCXnmq+npRTIL/WlFCUzThyd+F5PQnnT1ug==", + "dev": true + }, "check-types": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/check-types/-/check-types-8.0.3.tgz", @@ -11057,6 +11069,12 @@ "path-type": "^3.0.0" } }, + "discontinuous-range": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha1-44Mx8IRLukm5qctxx3FYWqsbxlo=", + "dev": true + }, "dns-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", @@ -11487,6 +11505,269 @@ "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", "dev": true }, + "enzyme": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/enzyme/-/enzyme-3.11.0.tgz", + "integrity": "sha512-Dw8/Gs4vRjxY6/6i9wU0V+utmQO9kvh9XLnz3LIudviOnVYDEe2ec+0k+NQoMamn1VrjKgCUOWj5jG/5M5M0Qw==", + "dev": true, + "requires": { + "array.prototype.flat": "^1.2.3", + "cheerio": "^1.0.0-rc.3", + "enzyme-shallow-equal": "^1.0.1", + "function.prototype.name": "^1.1.2", + "has": "^1.0.3", + "html-element-map": "^1.2.0", + "is-boolean-object": "^1.0.1", + "is-callable": "^1.1.5", + "is-number-object": "^1.0.4", + "is-regex": "^1.0.5", + "is-string": "^1.0.5", + "is-subset": "^0.1.1", + "lodash.escape": "^4.0.1", + "lodash.isequal": "^4.5.0", + "object-inspect": "^1.7.0", + "object-is": "^1.0.2", + "object.assign": "^4.1.0", + "object.entries": "^1.1.1", + "object.values": "^1.1.1", + "raf": "^3.4.1", + "rst-selector-parser": "^2.2.3", + "string.prototype.trim": "^1.2.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + }, + "dependencies": { + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true + }, + "object.assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz", + "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.0", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + } + } + }, + "is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==", + "dev": true + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object-is": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.3.tgz", + "integrity": "sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + } + }, + "string.prototype.trimend": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.2.tgz", + "integrity": "sha512-8oAG/hi14Z4nOVP0z6mdiVZ/wqjDtWSLygMigTzAb+7aPEDTleeFf+WrF+alzecxIRkckkJVn+dTlwzJXORATw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + } + }, + "string.prototype.trimstart": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.2.tgz", + "integrity": "sha512-7F6CdBTl5zyu30BJFdzSTlSlLPwODC23Od+iLoVH8X6+3fvDPPuBVVj9iaB1GOsSTSIgVfsfm27R2FGrAPznWg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + } + } + } + }, + "enzyme-adapter-react-16": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.5.tgz", + "integrity": "sha512-33yUJGT1nHFQlbVI5qdo5Pfqvu/h4qPwi1o0a6ZZsjpiqq92a3HjynDhwd1IeED+Su60HDWV8mxJqkTnLYdGkw==", + "dev": true, + "requires": { + "enzyme-adapter-utils": "^1.13.1", + "enzyme-shallow-equal": "^1.0.4", + "has": "^1.0.3", + "object.assign": "^4.1.0", + "object.values": "^1.1.1", + "prop-types": "^15.7.2", + "react-is": "^16.13.1", + "react-test-renderer": "^16.0.0-0", + "semver": "^5.7.0" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "react-test-renderer": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.14.0.tgz", + "integrity": "sha512-L8yPjqPE5CZO6rKsKXRO/rVPiaCOy0tQQJbC+UjPNlobl5mad59lvPjwFsQHTvL03caVDIVr9x9/OSgDe6I5Eg==", + "dev": true, + "requires": { + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "react-is": "^16.8.6", + "scheduler": "^0.19.1" + } + } + } + }, + "enzyme-adapter-utils": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.13.1.tgz", + "integrity": "sha512-5A9MXXgmh/Tkvee3bL/9RCAAgleHqFnsurTYCbymecO4ohvtNO5zqIhHxV370t7nJAwaCfkgtffarKpC0GPt0g==", + "dev": true, + "requires": { + "airbnb-prop-types": "^2.16.0", + "function.prototype.name": "^1.1.2", + "object.assign": "^4.1.0", + "object.fromentries": "^2.0.2", + "prop-types": "^15.7.2", + "semver": "^5.7.1" + } + }, + "enzyme-shallow-equal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.4.tgz", + "integrity": "sha512-MttIwB8kKxypwHvRynuC3ahyNc+cFbR8mjVIltnmzQ0uKGqmsfO4bfBuLxb0beLNPhjblUEYvEbsg+VSygvF1Q==", + "dev": true, + "requires": { + "has": "^1.0.3", + "object-is": "^1.1.2" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==", + "dev": true + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true + }, + "object-is": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.3.tgz", + "integrity": "sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + } + }, + "object.assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz", + "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.0", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "string.prototype.trimend": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.2.tgz", + "integrity": "sha512-8oAG/hi14Z4nOVP0z6mdiVZ/wqjDtWSLygMigTzAb+7aPEDTleeFf+WrF+alzecxIRkckkJVn+dTlwzJXORATw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + } + }, + "string.prototype.trimstart": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.2.tgz", + "integrity": "sha512-7F6CdBTl5zyu30BJFdzSTlSlLPwODC23Od+iLoVH8X6+3fvDPPuBVVj9iaB1GOsSTSIgVfsfm27R2FGrAPznWg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + } + } + } + }, "errno": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", @@ -14340,6 +14621,15 @@ "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==", "dev": true }, + "html-element-map": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/html-element-map/-/html-element-map-1.2.0.tgz", + "integrity": "sha512-0uXq8HsuG1v2TmQ8QkIhzbrqeskE4kn52Q18QJ9iAA/SnHoEKXWiUxHQtclRsCFWEUD2So34X+0+pZZu862nnw==", + "dev": true, + "requires": { + "array-filter": "^1.0.0" + } + }, "html-encoding-sniffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", @@ -15289,6 +15579,12 @@ "binary-extensions": "^1.0.0" } }, + "is-boolean-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.0.1.tgz", + "integrity": "sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ==", + "dev": true + }, "is-buffer": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", @@ -15497,6 +15793,12 @@ "integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=", "dev": true }, + "is-negative-zero": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz", + "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=", + "dev": true + }, "is-number": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", @@ -15523,6 +15825,12 @@ } } }, + "is-number-object": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz", + "integrity": "sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==", + "dev": true + }, "is-obj": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", @@ -15631,6 +15939,12 @@ "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", "dev": true }, + "is-subset": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", + "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=", + "dev": true + }, "is-svg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-3.0.0.tgz", @@ -17932,6 +18246,24 @@ "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=", "dev": true }, + "lodash.escape": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz", + "integrity": "sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg=", + "dev": true + }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", + "dev": true + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", + "dev": true + }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -18667,6 +18999,12 @@ "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" }, + "moo": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz", + "integrity": "sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w==", + "dev": true + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -18750,6 +19088,27 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "nearley": { + "version": "2.19.7", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.19.7.tgz", + "integrity": "sha512-Y+KNwhBPcSJKeyQCFjn8B/MIe+DDlhaaDgjVldhy5xtFewIbiQgcbZV8k2gCVwkI1ZsKCnjIYZbR+0Fim5QYgg==", + "dev": true, + "requires": { + "commander": "^2.19.0", + "moo": "^0.5.0", + "railroad-diagrams": "^1.0.0", + "randexp": "0.4.6", + "semver": "^5.4.1" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + } + } + }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", @@ -21083,6 +21442,31 @@ "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==", "dev": true }, + "raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "dev": true, + "requires": { + "performance-now": "^2.1.0" + } + }, + "railroad-diagrams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha1-635iZ1SN3t+4mcG5Dlc3RVnN234=", + "dev": true + }, + "randexp": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "dev": true, + "requires": { + "discontinuous-range": "1.0.0", + "ret": "~0.1.10" + } + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -21430,6 +21814,46 @@ "tiny-warning": "^1.0.0" } }, + "react-shallow-renderer": { + "version": "16.14.1", + "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.14.1.tgz", + "integrity": "sha512-rkIMcQi01/+kxiTE9D3fdS959U1g7gs+/rborw++42m1O9FAQiNI/UNRZExVUoAOprn4umcXf+pFRou8i4zuBg==", + "dev": true, + "requires": { + "object-assign": "^4.1.1", + "react-is": "^16.12.0 || ^17.0.0" + } + }, + "react-test-renderer": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-17.0.1.tgz", + "integrity": "sha512-/dRae3mj6aObwkjCcxZPlxDFh73XZLgvwhhyON2haZGUEhiaY5EjfAdw+d/rQmlcFwdTpMXCSGVk374QbCTlrA==", + "dev": true, + "requires": { + "object-assign": "^4.1.1", + "react-is": "^17.0.1", + "react-shallow-renderer": "^16.13.1", + "scheduler": "^0.20.1" + }, + "dependencies": { + "react-is": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz", + "integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==", + "dev": true + }, + "scheduler": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.1.tgz", + "integrity": "sha512-LKTe+2xNJBNxu/QhHvDR14wUXHRQbVY5ZOYpOGWRzhydZUqrLb2JBvLPY7cAqFmqrWuDED0Mjk7013SZiOz6Bw==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + } + } + }, "react-transition-group": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz", @@ -22203,6 +22627,16 @@ "inherits": "^2.0.1" } }, + "rst-selector-parser": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz", + "integrity": "sha1-gbIw6i/MYGbInjRy3nlChdmwPZE=", + "dev": true, + "requires": { + "lodash.flattendeep": "^4.4.0", + "nearley": "^2.7.10" + } + }, "rsvp": { "version": "4.8.5", "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", @@ -23997,6 +24431,91 @@ } } }, + "string.prototype.trim": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.2.tgz", + "integrity": "sha512-b5yrbl3BXIjHau9Prk7U0RRYcUYdN4wGSVaqoBQS50CCE3KBuYU0TYRNPFCP7aVoNMX87HKThdMRVIP3giclKg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.0" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==", + "dev": true + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true + }, + "object.assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz", + "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.0", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "string.prototype.trimend": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.2.tgz", + "integrity": "sha512-8oAG/hi14Z4nOVP0z6mdiVZ/wqjDtWSLygMigTzAb+7aPEDTleeFf+WrF+alzecxIRkckkJVn+dTlwzJXORATw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + } + }, + "string.prototype.trimstart": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.2.tgz", + "integrity": "sha512-7F6CdBTl5zyu30BJFdzSTlSlLPwODC23Od+iLoVH8X6+3fvDPPuBVVj9iaB1GOsSTSIgVfsfm27R2FGrAPznWg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + } + } + } + }, "string.prototype.trimend": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.0.tgz", diff --git a/package.json b/package.json index 2a0cc5076..553d5f333 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,10 @@ "i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null", "is-es5": "es-check es5 ./dist/*.js", "lint": "fedx-scripts eslint --ext .js --ext .jsx .", - "snapshot": "fedx-scripts jest --updateSnapshot", "start": "fedx-scripts webpack-dev-server --progress", - "test": "fedx-scripts jest --coverage --passWithNoTests" + "debug-test": "node --inspect-brk node_modules/.bin/jest --coverage --runInBand", + "test": "TZ=UTC fedx-scripts jest --coverage --runInBand", + "snapshot": "fedx-scripts jest --updateSnapshot" }, "author": "edX", "license": "AGPL-3.0", @@ -51,10 +52,14 @@ }, "devDependencies": { "@edx/frontend-build": "3.1.20", + "check-prop-types": "^1.1.2", "codecov": "3.8.0", + "enzyme": "^3.3.0", + "enzyme-adapter-react-16": "^1.1.1", "es-check": "5.1.1", "glob": "7.1.6", "jest": "26.6.1", + "react-test-renderer": "^17.0.1", "reactifex": "1.1.1" } } diff --git a/src/PageLoading.jsx b/src/components/common/PageLoading.jsx similarity index 87% rename from src/PageLoading.jsx rename to src/components/common/PageLoading.jsx index 1b1135dcf..b20223c62 100644 --- a/src/PageLoading.jsx +++ b/src/components/common/PageLoading.jsx @@ -7,11 +7,7 @@ export default class PageLoading extends Component { return null; } - return ( - - {this.props.srMessage} - - ); + return {this.props.srMessage}; } render() { diff --git a/src/components/common/PageLoading.test.jsx b/src/components/common/PageLoading.test.jsx new file mode 100644 index 000000000..c27e0723e --- /dev/null +++ b/src/components/common/PageLoading.test.jsx @@ -0,0 +1,33 @@ +import { shallow } from 'enzyme'; +import React from 'react'; +import renderer from 'react-test-renderer'; +import { checkProps } from '../../setupTest'; +import PageLoading from './PageLoading'; + +describe('', () => { + it('does not render without message', () => { + const wrapper = shallow(); + + expect(wrapper.find('.sr-only')).toHaveLength(0); + }); + it('renders expected message', () => { + const message = 'Loading...'; + + const wrapper = shallow(); + const srElement = wrapper.find('.sr-only'); + + expect(srElement).toHaveLength(1); + expect(srElement.text()).toEqual(message); + }); + it('does not throw a warning with correct props', () => { + const expectedProps = { srMessage: 'Loading...' }; + const propsError = checkProps(PageLoading, expectedProps); + + expect(propsError).toBeUndefined(); + }); + it('snapshot matches correctly', () => { + const tree = renderer.create().toJSON(); + + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/src/components/common/__snapshots__/PageLoading.test.jsx.snap b/src/components/common/__snapshots__/PageLoading.test.jsx.snap new file mode 100644 index 000000000..09022dcef --- /dev/null +++ b/src/components/common/__snapshots__/PageLoading.test.jsx.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` snapshot matches correctly 1`] = ` +
+
+
+ + Loading + +
+
+
+`; diff --git a/src/dates/formatDate.js b/src/dates/formatDate.js index b40124b1b..4b27ad2e6 100644 --- a/src/dates/formatDate.js +++ b/src/dates/formatDate.js @@ -1,6 +1,6 @@ import moment from 'moment'; -export default function formatDate(date) { +export default function formatDate(date) { // todo: move this utils/index.js if (date) { return moment(date).format('lll'); } diff --git a/src/dates/formatDate.test.js b/src/dates/formatDate.test.js new file mode 100644 index 000000000..5c73e6619 --- /dev/null +++ b/src/dates/formatDate.test.js @@ -0,0 +1,13 @@ +import formatDate from './formatDate'; + +describe('Format Date', () => { + it('correctly formats date', () => { + const formatedDate = formatDate('2015-09-19T11:00:00'); + + expect(formatedDate).toEqual('Sep 19, 2015 11:00 AM'); + }); + it('returns N/A if data is not provided', () => { + expect(formatDate('')).toEqual('N/A'); + expect(formatDate()).toEqual('N/A'); + }); +}); diff --git a/src/index.jsx b/src/index.jsx index 8a0c0ca5b..90f2d31fd 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -7,8 +7,9 @@ import { AppProvider, ErrorPage } from '@edx/frontend-platform/react'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import React from 'react'; import ReactDOM from 'react-dom'; -import { Switch, Route, Link } from 'react-router-dom'; +import { Switch, Route } from 'react-router-dom'; +import SupportHomePage from './support-home/SupportHomePage'; import Header from './support-header'; import appMessages from './i18n'; import UserPage from './users/UserPage'; @@ -17,17 +18,6 @@ import UserMessagesProvider from './user-messages/UserMessagesProvider'; import './index.scss'; import './assets/favicon.ico'; -function supportLinks() { - return ( -
-

Support Tools

-
    -
  • Search Users
  • -
-
- ); -} - subscribe(APP_READY, () => { const { administrator } = getAuthenticatedUser(); if (!administrator) { @@ -39,7 +29,7 @@ subscribe(APP_READY, () => {
- + diff --git a/src/setupTest.js b/src/setupTest.js index b012711b9..f2aad641d 100755 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -1 +1,17 @@ import 'babel-polyfill'; +import Enzyme from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import checkPropTypes from 'check-prop-types'; + +Enzyme.configure({ adapter: new Adapter() }); + +// eslint-disable-next-line import/prefer-default-export +export function checkProps(component, expectedProps) { + return checkPropTypes( + // eslint-disable-next-line react/forbid-foreign-prop-types + component.propTypes, + expectedProps, + 'props', + component.name, + ); +} diff --git a/src/support-home/SupportHomePage.jsx b/src/support-home/SupportHomePage.jsx new file mode 100644 index 000000000..4e79a530a --- /dev/null +++ b/src/support-home/SupportHomePage.jsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +const SupportHomePage = () => ( +
+

Support Tools

+
    +
  • + + Search Users + +
  • +
+
+); + +export default SupportHomePage; diff --git a/src/support-home/SupportHomePage.test.jsx b/src/support-home/SupportHomePage.test.jsx new file mode 100644 index 000000000..f120bfff1 --- /dev/null +++ b/src/support-home/SupportHomePage.test.jsx @@ -0,0 +1,38 @@ +import { shallow } from 'enzyme'; +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import renderer from 'react-test-renderer'; +import SupportHomePage from './SupportHomePage'; + +const SupportHomePageWrapper = () => ( + + + +); + +describe('SupportLinksPage', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallow(); + }); + + it('correctly renders support links', () => { + expect(wrapper.find('li')).toHaveLength(1); + }); + it('has search users link', () => { + const searchUsersText = wrapper.find('#search-users').text(); + + expect(searchUsersText).toEqual('Search Users'); + }); + it('has correct page heading', () => { + const supportToolsText = wrapper.find('h3').text(); + + expect(supportToolsText).toEqual('Support Tools'); + }); + it('matches snapshot', () => { + const tree = renderer.create().toJSON(); + + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/src/support-home/__snapshots__/SupportHomePage.test.jsx.snap b/src/support-home/__snapshots__/SupportHomePage.test.jsx.snap new file mode 100644 index 000000000..3ddee4e45 --- /dev/null +++ b/src/support-home/__snapshots__/SupportHomePage.test.jsx.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SupportLinksPage matches snapshot 1`] = ` +
+

+ Support Tools +

+ +
+`; diff --git a/src/user-messages/messages.js b/src/user-messages/messages.js new file mode 100644 index 000000000..5d0c9df7f --- /dev/null +++ b/src/user-messages/messages.js @@ -0,0 +1,4 @@ +export const USER_IDENTIFIER_INVALID_ERROR = 'The searched username or email is invalid. Please correct the username or email and try again.'; +export const USERNAME_IDENTIFIER_NOT_FOUND_ERROR = 'We couldn\'t find a user with the username "{identifier}".'; +export const USER_EMAIL_IDENTIFIER_NOT_FOUND_ERROR = 'We couldn\'t find a user with the email "{identifier}".'; +export const UNKNOWN_API_ERROR = "There was an error loading this user's data. Check the JavaScript console for detailed errors."; diff --git a/src/users/CourseSummary.jsx b/src/users/CourseSummary.jsx index 69e6c659d..c203a432b 100644 --- a/src/users/CourseSummary.jsx +++ b/src/users/CourseSummary.jsx @@ -1,9 +1,8 @@ import React from 'react'; import PropTypes from 'prop-types'; - -import PageLoading from '../PageLoading'; -import AlertList from '../user-messages/AlertList'; +import PageLoading from '../components/common/PageLoading'; import formatDate from '../dates/formatDate'; +import AlertList from '../user-messages/AlertList'; export default function CourseSummary({ courseData, @@ -17,22 +16,17 @@ export default function CourseSummary({ return (
    - { - courseRuns.map((item) => ( -
  • - {item.key} Start: {formatDate(item.start)} End: {formatDate(item.end)} -
  • - )) - } + {courseRuns.map((item) => ( +
  • + {item.key} Start: {formatDate(item.start)} End: + {formatDate(item.end)} +
  • + ))}
); } - return ( -
- No Course Runs available -
- ); + return
No Course Runs available
; } function renderHideButton() { @@ -51,11 +45,7 @@ export default function CourseSummary({ } return (
- {!courseData && !errors && ( - - )} + {!courseData && !errors && } {errors && ( <> @@ -64,9 +54,7 @@ export default function CourseSummary({ )} {courseData && !errors && (
-

- Course Summary: {courseData.title} -

+

Course Summary: {courseData.title}

@@ -89,15 +77,22 @@ export default function CourseSummary({
Marketing {courseData ? ( - Marketing URL - ) : ''} + + Marketing URL + + ) : ( + '' + )}
{renderHideButton()}
- )}
); @@ -110,9 +105,11 @@ CourseSummary.propTypes = { key: PropTypes.string, levelType: PropTypes.string, marketingUrl: PropTypes.string, - courseRuns: PropTypes.arrayOf(PropTypes.shape({ - key: PropTypes.string, - })), + courseRuns: PropTypes.arrayOf( + PropTypes.shape({ + key: PropTypes.string, + }), + ), }), errors: PropTypes.bool, clearHandler: PropTypes.func, diff --git a/src/users/EnrollmentForm.jsx b/src/users/EnrollmentForm.jsx index 5194debb5..6124b7eb6 100644 --- a/src/users/EnrollmentForm.jsx +++ b/src/users/EnrollmentForm.jsx @@ -6,7 +6,7 @@ import { import classNames from 'classnames'; import AlertList from '../user-messages/AlertList'; -import { postEnrollmentChange } from './api'; +import { postEnrollmentChange } from './data/api'; import UserMessagesContext from '../user-messages/UserMessagesContext'; const getModes = function getModes(enrollment) { diff --git a/src/users/EntitlementForm.jsx b/src/users/EntitlementForm.jsx index 727e157dd..069ecb18e 100644 --- a/src/users/EntitlementForm.jsx +++ b/src/users/EntitlementForm.jsx @@ -7,7 +7,7 @@ import classNames from 'classnames'; import UserMessagesContext from '../user-messages/UserMessagesContext'; import AlertList from '../user-messages/AlertList'; -import { postEntitlement, patchEntitlement } from './api'; +import { postEntitlement, patchEntitlement } from './data/api'; export const REISSUE = 'reissue'; export const CREATE = 'create'; diff --git a/src/users/Entitlements.jsx b/src/users/Entitlements.jsx index 864e2373a..cac6ecc5b 100644 --- a/src/users/Entitlements.jsx +++ b/src/users/Entitlements.jsx @@ -11,7 +11,7 @@ import EntitlementForm, { CREATE, REISSUE } from './EntitlementForm'; import sort from './sort'; import Table from '../Table'; import CourseSummary from './CourseSummary'; -import { getCourseData } from './api'; +import { getCourseData } from './data/api'; import UserMessagesContext from '../user-messages/UserMessagesContext'; import formatDate from '../dates/formatDate'; diff --git a/src/users/UserPage.jsx b/src/users/UserPage.jsx index 54f4d1abe..82e71cf60 100644 --- a/src/users/UserPage.jsx +++ b/src/users/UserPage.jsx @@ -1,41 +1,33 @@ +import { camelCaseObject, getConfig, history } from '@edx/frontend-platform'; +import PropTypes from 'prop-types'; import React, { - useCallback, useState, useEffect, useContext, + useCallback, useContext, useEffect, useState, } from 'react'; -import PropTypes from 'prop-types'; - -import { camelCaseObject, getConfig, history } from '@edx/frontend-platform'; import { Link } from 'react-router-dom'; -import PageLoading from '../PageLoading'; - -import UserSummary from './UserSummary'; +import PageLoading from '../components/common/PageLoading'; +import AlertList from '../user-messages/AlertList'; +import { USER_IDENTIFIER_INVALID_ERROR } from '../user-messages/messages'; +import UserMessagesContext from '../user-messages/UserMessagesContext'; +import { isEmail, isValidUsername } from '../utils/index'; +import { getAllUserData } from './data/api'; import Enrollments from './Enrollments'; import Entitlements from './Entitlements'; import UserSearch from './UserSearch'; -import { getAllUserData } from './api'; -import UserMessagesContext from '../user-messages/UserMessagesContext'; -import AlertList from '../user-messages/AlertList'; +import UserSummary from './UserSummary'; // Supports urls such as /users/?username={username} and /users/?email={email} export default function UserPage({ location }) { const url = getConfig().BASE_URL + location.pathname + location.search; - const params = (new URL(url)).searchParams; - const [userIdentifier, setUserIdentifier] = useState(params.get('username') || params.get('email') || undefined); + const params = new URL(url).searchParams; + const [userIdentifier, setUserIdentifier] = useState( + params.get('username') || params.get('email') || undefined, + ); const [searching, setSearching] = useState(false); const [data, setData] = useState({ enrollments: null, entitlements: null }); const [loading, setLoading] = useState(false); const [showEnrollments, setShowEnrollments] = useState(true); const [showEntitlements, setShowEntitlements] = useState(false); const { add, clear } = useContext(UserMessagesContext); - const EMAIL_REGEX = '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'; - const USERNAME_REGEX = '^[\\w.@_+-]+$'; - - function isEmail(searchValue) { - return !!(searchValue && searchValue.match(EMAIL_REGEX)); - } - - function isValidUsername(searchValue) { - return !!(searchValue && searchValue.match(USERNAME_REGEX)); - } function pushHistoryIfChanged(nextUrl) { if (nextUrl !== location.pathname + location.search) { @@ -45,7 +37,7 @@ export default function UserPage({ location }) { function processSearchResult(searchValue, result) { if (result.errors.length > 0) { - result.errors.forEach(error => add(error)); + result.errors.forEach((error) => add(error)); history.replace('/users'); document.title = 'Support Tools | edX'; } else if (isEmail(searchValue)) { @@ -66,7 +58,7 @@ export default function UserPage({ location }) { add({ code: null, dismissible: true, - text: 'The searched username or email is invalid. Please correct the username or email and try again.', + text: USER_IDENTIFIER_INVALID_ERROR, type: 'error', topic: 'general', }); @@ -88,7 +80,7 @@ export default function UserPage({ location }) { setData(camelCaseObject(result)); processSearchResult(searchValue, result); }); - // This is the case of an empty search (maybe a user wanted to clear out what they were seeing) + // This is the case of an empty search (maybe a user wanted to clear out what they were seeing) } else if (searchValue === '') { clear('general'); history.replace('/users'); @@ -143,12 +135,12 @@ export default function UserPage({ location }) { {/* NOTE: the "key" here causes the UserSearch component to re-render completely when the user identifier changes. Doing so clears out the search box. */} - - {loading && ( - - )} + + {loading && } {!loading && data.user && data.user.username && ( <> )} {!loading && !userIdentifier && ( -
-

Please search for a username or email.

-
+
+

Please search for a username or email.

+
)} ); diff --git a/src/users/UserPage.test.jsx b/src/users/UserPage.test.jsx new file mode 100644 index 000000000..96df0fca2 --- /dev/null +++ b/src/users/UserPage.test.jsx @@ -0,0 +1,121 @@ +import * as Auth from '@edx/frontend-platform/auth'; +import { mount } from 'enzyme'; +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { MemoryRouter } from 'react-router-dom'; +import renderer from 'react-test-renderer'; +import { checkProps } from '../setupTest'; + +import * as messages from '../user-messages/messages'; +import UserMessagesProvider from '../user-messages/UserMessagesProvider'; +import UserPage from './UserPage'; + +jest.mock('@edx/frontend-platform/auth'); + +const UserPageWrapper = (props) => ( + + + + + +); + +// Function to wait until the entire component is fully painted. +const waitForComponentToPaint = async (wrapper) => { + await act(async () => { + await new Promise((resolve) => setTimeout(resolve)); + wrapper.update(); + }); +}; + +describe('User Page', () => { + let location; + + beforeEach(() => { + location = { pathname: '/users', search: '' }; + }); + + describe('Checking PropTypes', () => { + it('does not throw a warning', () => { + const expectedProps = { location }; + const propsError = checkProps(UserPage, expectedProps); + + expect(propsError).toBeUndefined(); + }); + }); + describe('shows expected error alert', () => { + const mockAuthResponseError = (code) => { + Auth.getAuthenticatedHttpClient = jest.fn(() => { + const error = new Error(); + error.customAttributes = { + httpErrorStatus: code, + httpErrorResponseData: JSON.stringify(''), + }; + throw error; + }); + }; + it('when user identifier is invalid', () => { + const invalidUsername = 'invalid username'; + location.search = `?username=${invalidUsername}`; + const wrapper = mount(); + + const searchInput = wrapper.find('input[name="userIdentifier"]'); + const alert = wrapper.find('.alert'); + + expect(searchInput).toHaveLength(1); + expect(searchInput.prop('defaultValue')).toEqual(invalidUsername); + expect(alert).toHaveLength(1); + expect(alert.text()).toEqual(messages.USER_IDENTIFIER_INVALID_ERROR); + }); + it('when user identifier is not found', async () => { + const validUsername = 'valid-non-existing-username'; + location.search = `?username=${validUsername}`; + mockAuthResponseError(404); + const expectedAlert = messages.USERNAME_IDENTIFIER_NOT_FOUND_ERROR.replace( + '{identifier}', + validUsername, + ); + + const wrapper = mount(); + await waitForComponentToPaint(wrapper); + + const alert = wrapper.find('.alert'); + expect(alert).toHaveLength(1); + expect(alert.text()).toEqual(expectedAlert); + }); + it('when user email is not found', async () => { + const validEmail = 'valid-non-existing@email.com'; + location.search = `?email=${validEmail}`; + mockAuthResponseError(404); + const expectedAlert = messages.USER_EMAIL_IDENTIFIER_NOT_FOUND_ERROR.replace( + '{identifier}', + validEmail, + ); + + const wrapper = mount(); + await waitForComponentToPaint(wrapper); + + const alert = wrapper.find('.alert'); + expect(alert).toHaveLength(1); + expect(alert.text()).toEqual(expectedAlert); + }); + it('when user email is not found', async () => { + const validEmail = 'valid@email.com'; + location.search = `?email=${validEmail}`; + mockAuthResponseError(500); + + const wrapper = mount(); + await waitForComponentToPaint(wrapper); + + const alert = wrapper.find('.alert'); + expect(alert).toHaveLength(1); + expect(alert.text()).toEqual(messages.UNKNOWN_API_ERROR); + }); + }); + it('snapshot matches correctly', () => { + const tree = renderer + .create() + .toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/src/users/UserSearch.test.jsx b/src/users/UserSearch.test.jsx new file mode 100644 index 000000000..0f284fa0a --- /dev/null +++ b/src/users/UserSearch.test.jsx @@ -0,0 +1,71 @@ +import { mount } from 'enzyme'; +import React from 'react'; +import renderer from 'react-test-renderer'; +import { checkProps } from '../setupTest'; +import UserSearch from './UserSearch'; + +describe('User Search Page', () => { + let props; + let wrapper; + + beforeEach(() => { + props = { userIdentifier: '', searchHandler: jest.fn() }; + wrapper = mount(); + }); + + describe('Checking PropTypes', () => { + it('does not throw a warning', () => { + const propsError = checkProps(UserSearch, props); + + expect(propsError).toBeUndefined(); + }); + it('does not throw error with undefined userIdentifier', () => { + delete props.userIdentifier; + const propsError = checkProps(UserSearch, props); + + expect(propsError).toBeUndefined(); + }); + it('throw error with undefined search handler', () => { + const propsError = checkProps(UserSearch, {}); + + expect(propsError).not.toBeUndefined(); + expect(propsError).toContain('Failed props type'); + expect(propsError).toContain('searchHandler'); + }); + }); + + describe('renders correctly', () => { + it('with correct user identifier', () => { + expect(wrapper.find('input').prop('defaultValue')).toEqual( + props.userIdentifier, + ); + }); + it('with correct default user identifier', () => { + delete props.userIdentifier; + const userSearchwrapper = mount(); + + expect(userSearchwrapper.find('input').prop('defaultValue')).toEqual(''); + }); + it('with submit button', () => { + expect(wrapper.find('button')).toHaveLength(1); + expect(wrapper.find('button').text()).toEqual('Search'); + }); + + it('when submit button is clicked', () => { + const searchProps = { userIdentifier: 'staff', searchHandler: jest.fn() }; + const userSearchwrapper = mount(); + + userSearchwrapper.find('button').simulate('click'); + + expect(searchProps.searchHandler).toHaveBeenCalledWith( + searchProps.userIdentifier, + ); + }); + + it('matches snapshot', () => { + const tree = renderer.create(wrapper).toJSON(); + + expect(tree).toMatchSnapshot(); + }); + }); +}); diff --git a/src/users/UserSummary.jsx b/src/users/UserSummary.jsx index da6aa0f96..2da9555a0 100644 --- a/src/users/UserSummary.jsx +++ b/src/users/UserSummary.jsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { Modal, Button, Input } from '@edx/paragon'; -import { postTogglePasswordStatus } from './api'; +import { postTogglePasswordStatus } from './data/api'; import Table from '../Table'; import formatDate from '../dates/formatDate'; diff --git a/src/users/__snapshots__/UserPage.test.jsx.snap b/src/users/__snapshots__/UserPage.test.jsx.snap new file mode 100644 index 000000000..f18d3ad36 --- /dev/null +++ b/src/users/__snapshots__/UserPage.test.jsx.snap @@ -0,0 +1,51 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`User Page snapshot matches correctly 1`] = ` +
+
+ + < Back to Tools + +
+
+
+ + + +
+
+
+

+ Please search for a username or email. +

+
+
+`; diff --git a/src/users/__snapshots__/UserSearch.test.jsx.snap b/src/users/__snapshots__/UserSearch.test.jsx.snap new file mode 100644 index 000000000..e6a876973 --- /dev/null +++ b/src/users/__snapshots__/UserSearch.test.jsx.snap @@ -0,0 +1,32 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`User Search Page renders correctly matches snapshot 1`] = ` +
+
+ + + +
+
+`; diff --git a/src/users/api.js b/src/users/data/api.js similarity index 79% rename from src/users/api.js rename to src/users/data/api.js index 37afee840..436915fc2 100644 --- a/src/users/api.js +++ b/src/users/data/api.js @@ -1,13 +1,16 @@ import { getConfig } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; - -const EMAIL_REGEX = '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'; -const USERNAME_REGEX = '^[\\w.@_+-]+$'; +import * as messages from '../../user-messages/messages'; +import { isEmail, isValidUsername } from '../../utils/index'; export async function getEntitlements(username, page = 1) { - const baseURL = `${getConfig().LMS_BASE_URL}/api/entitlements/v1/entitlements/`; + const baseURL = `${ + getConfig().LMS_BASE_URL + }/api/entitlements/v1/entitlements/`; const queryString = `user=${username}&page=${page}`; - const { data } = await getAuthenticatedHttpClient().get(`${baseURL}?${queryString}`); + const { data } = await getAuthenticatedHttpClient().get( + `${baseURL}?${queryString}`, + ); if (data.next !== null) { const nextPageData = await getEntitlements(username, data.current_page + 1); data.results = data.results.concat(nextPageData.results); @@ -29,7 +32,7 @@ export async function getSsoRecords(username) { ); let parsedData = []; if (data.length > 0) { - parsedData = data.map(entry => ({ + parsedData = data.map((entry) => ({ ...entry, extraData: JSON.parse(entry.extraData), })); @@ -39,18 +42,18 @@ export async function getSsoRecords(username) { export async function getUser(userIdentifier) { let url = `${getConfig().LMS_BASE_URL}/api/user/v1/accounts`; - let notFoundErrorText = "We couldn't find a user with the "; - // I am avoiding an `else` case here because we have already validated the input - // to fall into one of these cases. - if (userIdentifier.match(EMAIL_REGEX)) { - url += `?email=${userIdentifier}`; - notFoundErrorText += `email "${userIdentifier}".`; - } else if (userIdentifier.match(USERNAME_REGEX)) { - url += `/${userIdentifier}`; - notFoundErrorText += `username "${userIdentifier}".`; - } else { + const identifierIsEmail = isEmail(userIdentifier); + const identifierIsUsername = isValidUsername(userIdentifier); + + // todo: we have already validated the input to fall into one of these cases. + // The following `if` is not required. + if (!(identifierIsEmail || identifierIsUsername)) { throw new Error('Invalid Argument!'); } + url = identifierIsEmail + ? (url += `?email=${userIdentifier}`) + : (url += `/${userIdentifier}`); + try { const { data } = await getAuthenticatedHttpClient().get(url); return Array.isArray(data) && data.length > 0 ? data[0] : data; @@ -60,6 +63,11 @@ export async function getUser(userIdentifier) { // never do this in a customer-facing app. // eslint-disable-next-line no-console console.log(JSON.parse(error.customAttributes.httpErrorResponseData)); + const notFoundErrorText = (identifierIsEmail + ? messages.USER_EMAIL_IDENTIFIER_NOT_FOUND_ERROR + : messages.USERNAME_IDENTIFIER_NOT_FOUND_ERROR + ).replace('{identifier}', userIdentifier); + if (error.customAttributes.httpErrorStatus === 404) { error.userError = { code: null, @@ -74,7 +82,7 @@ export async function getUser(userIdentifier) { error.userError = { code: null, dismissible: true, - text: 'There was an error loading this user\'s data. Check the JavaScript console for detailed errors.', + text: messages.UNKNOWN_API_ERROR, type: 'danger', topic: 'general', }; @@ -90,7 +98,9 @@ export async function getUserVerificationDetail(username) { }; try { const { data } = await getAuthenticatedHttpClient().get( - `${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}/verifications/`, + `${ + getConfig().LMS_BASE_URL + }/api/user/v1/accounts/${username}/verifications/`, ); return data; } catch (error) { @@ -109,7 +119,9 @@ export async function getUserVerificationDetail(username) { export async function getUserVerificationStatus(username) { try { const { data } = await getAuthenticatedHttpClient().get( - `${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}/verification_status/`, + `${ + getConfig().LMS_BASE_URL + }/api/user/v1/accounts/${username}/verification_status/`, ); const extraData = await getUserVerificationDetail(username); data.extraData = extraData; @@ -180,10 +192,9 @@ export async function getAllUserData(userIdentifier) { export async function getCourseData(courseUUID) { try { - const { data } = await getAuthenticatedHttpClient() - .get( - `${getConfig().DISCOVERY_API_BASE_URL}/api/v1/courses/${courseUUID}/`, - ); + const { data } = await getAuthenticatedHttpClient().get( + `${getConfig().DISCOVERY_API_BASE_URL}/api/v1/courses/${courseUUID}/`, + ); return data; } catch (error) { // eslint-disable-next-line no-console @@ -210,18 +221,23 @@ export async function getCourseData(courseUUID) { } export async function patchEntitlement({ - uuid, action, unenrolledRun = null, comments = null, + uuid, + action, + unenrolledRun = null, + comments = null, }) { try { const { data } = await getAuthenticatedHttpClient().patch( `${getConfig().LMS_BASE_URL}/api/entitlements/v1/entitlements/${uuid}/`, { expired_at: null, - support_details: [{ - unenrolled_run: unenrolledRun, - action, - comments, - }], + support_details: [ + { + unenrolled_run: unenrolledRun, + action, + comments, + }, + ], }, ); return data; @@ -238,7 +254,8 @@ export async function patchEntitlement({ { code: null, dismissible: true, - text: 'There was an error submitting this entitlement. Check the JavaScript console for detailed errors.', + text: + 'There was an error submitting this entitlement. Check the JavaScript console for detailed errors.', type: 'danger', topic: 'entitlements', }, @@ -248,7 +265,11 @@ export async function patchEntitlement({ } export async function postEntitlement({ - user, courseUuid, mode, action, comments = null, + user, + courseUuid, + mode, + action, + comments = null, }) { try { const { data } = await getAuthenticatedHttpClient().post( @@ -258,10 +279,12 @@ export async function postEntitlement({ user, mode, refund_locked: true, - support_details: [{ - action, - comments, - }], + support_details: [ + { + action, + comments, + }, + ], }, ); return data; @@ -278,7 +301,8 @@ export async function postEntitlement({ { code: null, dismissible: true, - text: 'There was an error submitting this entitlement. Check the JavaScript console for detailed errors.', + text: + 'There was an error submitting this entitlement. Check the JavaScript console for detailed errors.', type: 'danger', topic: 'entitlements', }, @@ -288,7 +312,11 @@ export async function postEntitlement({ } export async function postEnrollmentChange({ - user, courseID, newMode, oldMode, reason, + user, + courseID, + newMode, + oldMode, + reason, }) { try { const { data } = await getAuthenticatedHttpClient().post( @@ -314,7 +342,8 @@ export async function postEnrollmentChange({ { code: null, dismissible: true, - text: 'There was an error submitting this entitlement. Check the JavaScript console for detailed errors.', + text: + 'There was an error submitting this entitlement. Check the JavaScript console for detailed errors.', type: 'danger', topic: 'enrollments', }, diff --git a/src/utils/index.js b/src/utils/index.js new file mode 100644 index 000000000..d007c3ff7 --- /dev/null +++ b/src/utils/index.js @@ -0,0 +1,10 @@ +const EMAIL_REGEX = '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'; +const USERNAME_REGEX = '^[\\w.@_+-]+$'; + +// todo: No need for !!. This operation is not affecting the value. +export const isEmail = (value) => !!(value && value.match(EMAIL_REGEX)); + +/* eslint-disable arrow-body-style */ +export const isValidUsername = (searchValue) => { + return !!(searchValue && searchValue.match(USERNAME_REGEX)); +}; diff --git a/src/utils/index.test.js b/src/utils/index.test.js new file mode 100644 index 000000000..e2863ac5a --- /dev/null +++ b/src/utils/index.test.js @@ -0,0 +1,32 @@ +import { isEmail, isValidUsername } from './index'; + +describe('Test Utils', () => { + const validEmails = ['staff@email.com', 'admin@email.co.og']; + const invalidEmails = [ + '', + ' ', + undefined, + 'invalid email@mail.com', + 'invalid-email', + '2020email.com', + 'www.email.com', + ]; + const validUsername = ['staff', 'admin123']; + const invalidUsername = ['', ' ', undefined, 'invalid username', '%invalid']; + test.each(validEmails)('isEmail return true for %s', (email) => { + expect(isEmail(email)).toBe(true); + }); + test.each(invalidEmails)('isEmail return false for %s', (email) => { + expect(isEmail(email)).toBe(false); + }); + + test.each(validUsername)('isValidUsername return true for %s', (username) => { + expect(isValidUsername(username)).toBe(true); + }); + test.each(invalidUsername)( + 'isValidUsername return false for %s', + (username) => { + expect(isValidUsername(username)).toBe(false); + }, + ); +});