diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d129ff33..2d112c6f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,3 +14,4 @@ jobs: node-version: 16.15 with-mutation-tests: true publish-coverage: true + force-install: true diff --git a/.github/workflows/deploy-preview.yml b/.github/workflows/deploy-preview.yml index b958cc37..76b02a48 100644 --- a/.github/workflows/deploy-preview.yml +++ b/.github/workflows/deploy-preview.yml @@ -19,7 +19,7 @@ jobs: node-version: 16.15 - name: Build run: | - npm ci && \ + npm ci --force && \ node ./scripts/set-homepage.js /shlink-web-client/${GITHUB_HEAD_REF#refs/heads/} && \ rm src/service-worker.ts && \ npm run build diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 1ab257a4..52d82e65 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -16,7 +16,7 @@ jobs: with: node-version: 16.15 - name: Generate release assets - run: npm ci && VERSION=${GITHUB_REF#refs/tags/v} npm run build:dist + run: npm ci --force && VERSION=${GITHUB_REF#refs/tags/v} npm run build:dist - name: Publish release with assets uses: docker://antonyurchenko/git-release:latest env: diff --git a/CHANGELOG.md b/CHANGELOG.md index 25c52727..f2dd09fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org). -## [3.7.1] - 2022-05-25 +## [3.7.2] - 2022-08-07 ### Added -* *Nothing* +* [#671](https://github.com/shlinkio/shlink-web-client/issues/671) Added proper color-scheme in root element based on selected theme. ### Changed -* [#648](https://github.com/shlinkio/shlink-web-client/pull/648) Migrated some scripts to ESM and updated to chalk 5. +* [#688](https://github.com/shlinkio/shlink-web-client/issues/688) Finalized migration from enzyme to react-testing-library. ### Deprecated * *Nothing* @@ -18,30 +18,49 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * *Nothing* ### Fixed -* [#653](https://github.com/shlinkio/shlink-web-client/pull/653) Fixed rendering values greater than 1000 in charts, when the browser has certain locales configured. +* [#695](https://github.com/shlinkio/shlink-web-client/issues/695) Fixed some warnings in tests. +* [#693](https://github.com/shlinkio/shlink-web-client/issues/693) Fixed tags, servers and domains search to make it case-insensitive. +* [#694](https://github.com/shlinkio/shlink-web-client/issues/694) Fixed editing and loading visits on short URLs with multi-segment slugs. -## [3.7.0] - 2022-05-14 +## [3.7.1] - 2022-05-25 ### Added -* [#622](https://github.com/shlinkio/shlink-web-client/pull/622) Added support to load domain visits when consuming Shlink 3.1.0 or newer. -* [#582](https://github.com/shlinkio/shlink-web-client/pull/582) Improved filtering short URLs by tag. - - Now, a new full tags selector component is available, which allows selecting any of the existing tags and also composes a toggle to filter by "any" tag or "all" tags. +* *Nothing* ### Changed -* [#616](https://github.com/shlinkio/shlink-web-client/pull/616) Updated to React 18. -* [#595](https://github.com/shlinkio/shlink-web-client/pull/595) Updated to react-chartjs-2 v4.1.0. -* [#594](https://github.com/shlinkio/shlink-web-client/pull/594) Updated to a new coding standard. -* [#627](https://github.com/shlinkio/shlink-web-client/pull/627) Updated to Jest 28. -* [#603](https://github.com/shlinkio/shlink-web-client/pull/603) Migrated to new and maintained dependencies to parse CSV<->JSON. -* [#610](https://github.com/shlinkio/shlink-web-client/pull/610) Migrated to a maintained coding style for CSS. -* [#619](https://github.com/shlinkio/shlink-web-client/pull/619) Introduced react testing library, to progressively replace enzyme. +* [#648](https://github.com/shlinkio/shlink-web-client/issues/648) Migrated some scripts to ESM and updated to chalk 5. ### Deprecated * *Nothing* ### Removed -* [#623](https://github.com/shlinkio/shlink-web-client/pull/623) Dropped support for Shlink older than 2.6.0. +* *Nothing* + +### Fixed +* [#653](https://github.com/shlinkio/shlink-web-client/issues/653) Fixed rendering values greater than 1000 in charts, when the browser has certain locales configured. + + +## [3.7.0] - 2022-05-14 +### Added +* [#622](https://github.com/shlinkio/shlink-web-client/issues/622) Added support to load domain visits when consuming Shlink 3.1.0 or newer. +* [#582](https://github.com/shlinkio/shlink-web-client/issues/582) Improved filtering short URLs by tag. + + Now, a new full tags selector component is available, which allows selecting any of the existing tags and also composes a toggle to filter by "any" tag or "all" tags. + +### Changed +* [#616](https://github.com/shlinkio/shlink-web-client/issues/616) Updated to React 18. +* [#595](https://github.com/shlinkio/shlink-web-client/issues/595) Updated to react-chartjs-2 v4.1.0. +* [#594](https://github.com/shlinkio/shlink-web-client/issues/594) Updated to a new coding standard. +* [#627](https://github.com/shlinkio/shlink-web-client/issues/627) Updated to Jest 28. +* [#603](https://github.com/shlinkio/shlink-web-client/issues/603) Migrated to new and maintained dependencies to parse CSV<->JSON. +* [#610](https://github.com/shlinkio/shlink-web-client/issues/610) Migrated to a maintained coding style for CSS. +* [#619](https://github.com/shlinkio/shlink-web-client/issues/619) Introduced react testing library, to progressively replace enzyme. + +### Deprecated +* *Nothing* + +### Removed +* [#623](https://github.com/shlinkio/shlink-web-client/issues/623) Dropped support for Shlink older than 2.6.0. ### Fixed * *Nothing* @@ -49,19 +68,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [3.6.0] - 2022-03-17 ### Added -* [#558](https://github.com/shlinkio/shlink-web-client/pull/558) Added dark text for tags where the generated background is too light, improving its legibility. -* [#570](https://github.com/shlinkio/shlink-web-client/pull/570) Added new section to load non-orphan visits all together when consuming Shlink 3.0.0. -* [#556](https://github.com/shlinkio/shlink-web-client/pull/556) Added support to filter short URLs list by "all" tags when consuming Shlink 3.0.0. -* [#549](https://github.com/shlinkio/shlink-web-client/pull/549) Allowed to export the list of short URLs as CSV. +* [#558](https://github.com/shlinkio/shlink-web-client/issues/558) Added dark text for tags where the generated background is too light, improving its legibility. +* [#570](https://github.com/shlinkio/shlink-web-client/issues/570) Added new section to load non-orphan visits all together when consuming Shlink 3.0.0. +* [#556](https://github.com/shlinkio/shlink-web-client/issues/556) Added support to filter short URLs list by "all" tags when consuming Shlink 3.0.0. +* [#549](https://github.com/shlinkio/shlink-web-client/issues/549) Allowed to export the list of short URLs as CSV. ### Changed -* [#543](https://github.com/shlinkio/shlink-web-client/pull/543) Redesigned settings section. -* [#567](https://github.com/shlinkio/shlink-web-client/pull/567) Improved Shlink 3.0.0 compatibility by checking the `INVALID_SHORT_URL_DELETION` error code when deleting short URLs. -* [#448](https://github.com/shlinkio/shlink-web-client/pull/448) Updated to bootstrap v5. -* [#524](https://github.com/shlinkio/shlink-web-client/pull/524) Updated to react-router v6. -* [#576](https://github.com/shlinkio/shlink-web-client/pull/576) Updated to fontawesome v6. -* [#579](https://github.com/shlinkio/shlink-web-client/pull/579) Replaced react-color with react-colorful. -* [#564](https://github.com/shlinkio/shlink-web-client/pull/564) Updated most of the dependencies. +* [#543](https://github.com/shlinkio/shlink-web-client/issues/543) Redesigned settings section. +* [#567](https://github.com/shlinkio/shlink-web-client/issues/567) Improved Shlink 3.0.0 compatibility by checking the `INVALID_SHORT_URL_DELETION` error code when deleting short URLs. +* [#448](https://github.com/shlinkio/shlink-web-client/issues/448) Updated to bootstrap v5. +* [#524](https://github.com/shlinkio/shlink-web-client/issues/524) Updated to react-router v6. +* [#576](https://github.com/shlinkio/shlink-web-client/issues/576) Updated to fontawesome v6. +* [#579](https://github.com/shlinkio/shlink-web-client/issues/579) Replaced react-color with react-colorful. +* [#564](https://github.com/shlinkio/shlink-web-client/issues/564) Updated most of the dependencies. ### Deprecated * *Nothing* @@ -70,7 +89,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * *Nothing* ### Fixed -* [#589](https://github.com/shlinkio/shlink-web-client/pull/589) Fixed alignment of shlink versions footer, by basing the logic on the presence of the sidebar instead of selected server. +* [#589](https://github.com/shlinkio/shlink-web-client/issues/589) Fixed alignment of shlink versions footer, by basing the logic on the presence of the sidebar instead of selected server. ## [3.5.1] - 2022-01-08 @@ -94,27 +113,27 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [3.5.0] - 2022-01-01 ### Added -* [#407](https://github.com/shlinkio/shlink-web-client/pull/407) Improved how visits (short URLs, tags and orphan) are loaded, to avoid ending up in a page with "There are no visits matching current filter". +* [#407](https://github.com/shlinkio/shlink-web-client/issues/407) Improved how visits (short URLs, tags and orphan) are loaded, to avoid ending up in a page with "There are no visits matching current filter". Now, the app will try to load visits for the configured default interval, and in parallel, it will load the latest visit. If the resulting list for that interval is empty, it will try to infer the closest interval with visits, based on the latest visit's date, and reload visits for that interval. -* [#547](https://github.com/shlinkio/shlink-web-client/pull/547) Improved domains page, to tell which of the domains are not properly configured. +* [#547](https://github.com/shlinkio/shlink-web-client/issues/547) Improved domains page, to tell which of the domains are not properly configured. Now, when this section is loaded, it tries to call the `GET /rest/health` endpoint for each one of the domains, and displays a warning icon on each one that failed. The warning includes a link to the documentation, explaining what are the steps to get it fixed. -* [#506](https://github.com/shlinkio/shlink-web-client/pull/506) Improved how servers are handled, displaying a warning when creating or importing servers that already exist. -* [#535](https://github.com/shlinkio/shlink-web-client/pull/535) Allowed editing default domain redirects when consuming Shlink 2.10 or newer. -* [#531](https://github.com/shlinkio/shlink-web-client/pull/531) Added custom slug field to the basic creation form in the Overview page. -* [#537](https://github.com/shlinkio/shlink-web-client/pull/537) Allowed to customize the ordering for every list in the app that supports it, being currently tags and short URLs. -* [#542](https://github.com/shlinkio/shlink-web-client/pull/542) Added ordering for short URLs to the query, so that it is consistent with the rest of the filtering params. +* [#506](https://github.com/shlinkio/shlink-web-client/issues/506) Improved how servers are handled, displaying a warning when creating or importing servers that already exist. +* [#535](https://github.com/shlinkio/shlink-web-client/issues/535) Allowed editing default domain redirects when consuming Shlink 2.10 or newer. +* [#531](https://github.com/shlinkio/shlink-web-client/issues/531) Added custom slug field to the basic creation form in the Overview page. +* [#537](https://github.com/shlinkio/shlink-web-client/issues/537) Allowed to customize the ordering for every list in the app that supports it, being currently tags and short URLs. +* [#542](https://github.com/shlinkio/shlink-web-client/issues/542) Added ordering for short URLs to the query, so that it is consistent with the rest of the filtering params. ### Changed -* [#534](https://github.com/shlinkio/shlink-web-client/pull/534) Updated axios. -* [#538](https://github.com/shlinkio/shlink-web-client/pull/538) Switched to the `-` notation in `orderBy` param for short URLs list, in preparation for Shlink v3.0.0 +* [#534](https://github.com/shlinkio/shlink-web-client/issues/534) Updated axios. +* [#538](https://github.com/shlinkio/shlink-web-client/issues/538) Switched to the `-` notation in `orderBy` param for short URLs list, in preparation for Shlink v3.0.0 ### Deprecated * *Nothing* diff --git a/Dockerfile b/Dockerfile index d907daa8..c6ce8170 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM node:16.15-alpine as node COPY . /shlink-web-client ARG VERSION="latest" ENV VERSION ${VERSION} -RUN cd /shlink-web-client && npm ci && NODE_ENV=production npm run build +RUN cd /shlink-web-client && npm ci --force && NODE_ENV=production npm run build FROM nginx:1.21-alpine LABEL maintainer="Alejandro Celaya " diff --git a/config/jest/setupBeforeEnzyme.js b/config/jest/setupBeforeEnzyme.js deleted file mode 100644 index 71c20acd..00000000 --- a/config/jest/setupBeforeEnzyme.js +++ /dev/null @@ -1,4 +0,0 @@ -import * as util from 'util'; - -global.TextEncoder = util.TextEncoder; -global.TextDecoder = util.TextDecoder; diff --git a/config/jest/setupEnzyme.js b/config/jest/setupEnzyme.js deleted file mode 100644 index 1d262ca5..00000000 --- a/config/jest/setupEnzyme.js +++ /dev/null @@ -1,4 +0,0 @@ -import Enzyme from 'enzyme'; -import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; - -Enzyme.configure({ adapter: new Adapter() }); diff --git a/config/jest/setupTests.ts b/config/jest/setupTests.ts index 5893b1ca..cec71a7a 100644 --- a/config/jest/setupTests.ts +++ b/config/jest/setupTests.ts @@ -4,4 +4,5 @@ import ResizeObserver from 'resize-observer-polyfill'; (global as any).ResizeObserver = ResizeObserver; (global as any).scrollTo = () => {}; +(global as any).prompt = () => {}; (global as any).matchMedia = (media: string) => ({ matches: false, media }); diff --git a/jest.config.js b/jest.config.js index 7a2f7fe3..1e37de5d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -15,7 +15,6 @@ module.exports = { lines: 90, }, }, - setupFiles: ['/config/jest/setupBeforeEnzyme.js', '/config/jest/setupEnzyme.js'], setupFilesAfterEnv: ['/config/jest/setupTests.ts'], testMatch: ['/test/**/*.test.{ts,tsx}'], testEnvironment: 'jsdom', diff --git a/package-lock.json b/package-lock.json index 959a29dd..1e4afac2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,14 +29,14 @@ "react": "^18.1.0", "react-chartjs-2": "^4.1.0", "react-colorful": "^5.5.1", - "react-copy-to-clipboard": "^5.0.4", - "react-datepicker": "^4.7.0", + "react-copy-to-clipboard": "^5.1.0", + "react-datepicker": "^4.8.0", "react-dom": "^18.1.0", "react-external-link": "^2.0.0", "react-leaflet": "^4.0.0", "react-redux": "^8.0.0", "react-router-dom": "^6.3.0", - "react-swipeable": "^6.2.0", + "react-swipeable": "^7.0.0", "react-tag-autocomplete": "^6.3.0", "reactstrap": "^9.0.1", "redux": "^4.2.0", @@ -59,7 +59,6 @@ "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^13.1.1", "@testing-library/user-event": "^14.1.1", - "@types/enzyme": "^3.10.11", "@types/jest": "^27.4.1", "@types/json2csv": "^5.0.3", "@types/leaflet": "^1.7.9", @@ -72,11 +71,9 @@ "@types/react-dom": "^18.0.3", "@types/react-tag-autocomplete": "^6.1.1", "@types/uuid": "^8.3.4", - "@wojtekmaj/enzyme-adapter-react-17": "0.6.5", "adm-zip": "^0.5.9", "babel-jest": "^28.0.3", "chalk": "^5.0.1", - "enzyme": "^3.11.0", "eslint": "^8.12.0", "identity-obj-proxy": "^3.0.0", "jest": "^28.0.3", @@ -4128,6 +4125,30 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.11", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz", @@ -5513,15 +5534,6 @@ "@types/node": "*" } }, - "node_modules/@types/cheerio": { - "version": "0.22.22", - "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.22.tgz", - "integrity": "sha512-05DYX4zU96IBfZFY+t3Mh88nlwSMtmmzSYaQkKN48T495VV1dkHSah6qYyDTN5ngaS0i0VonH37m+RuzSM0YiA==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -5541,16 +5553,6 @@ "@types/node": "*" } }, - "node_modules/@types/enzyme": { - "version": "3.10.11", - "resolved": "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.10.11.tgz", - "integrity": "sha512-LEtC7zXsQlbGXWGcnnmOI7rTyP+i1QzQv4Va91RKXDEukLDaNyxu0rXlfMiGEhJwfgTPCTb0R+Pnlj//oM9e/w==", - "dev": true, - "dependencies": { - "@types/cheerio": "*", - "@types/react": "*" - } - }, "node_modules/@types/eslint": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.29.0.tgz", @@ -6662,89 +6664,6 @@ "@xtuc/long": "4.2.2" } }, - "node_modules/@wojtekmaj/enzyme-adapter-react-17": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@wojtekmaj/enzyme-adapter-react-17/-/enzyme-adapter-react-17-0.6.5.tgz", - "integrity": "sha512-ChIObUiXXYUiqzXPqOai+p6KF5dlbItpDDYsftUOQiAiygbMDlLeJIjynC6ZrJIa2U2MpRp4YJmtR2GQyIHjgA==", - "dev": true, - "dependencies": { - "@wojtekmaj/enzyme-adapter-utils": "^0.1.1", - "enzyme-shallow-equal": "^1.0.0", - "has": "^1.0.0", - "object.assign": "^4.1.0", - "object.values": "^1.1.0", - "prop-types": "^15.7.0", - "react-is": "^17.0.2", - "react-test-renderer": "^17.0.0" - }, - "peerDependencies": { - "enzyme": "^3.0.0", - "react": "^17.0.0-0", - "react-dom": "^17.0.0-0" - } - }, - "node_modules/@wojtekmaj/enzyme-adapter-react-17/node_modules/@wojtekmaj/enzyme-adapter-utils": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@wojtekmaj/enzyme-adapter-utils/-/enzyme-adapter-utils-0.1.4.tgz", - "integrity": "sha512-ARGIQSIIv3oBia1m5Ihn1VU0FGmft6KPe39SBKTb8p7LSXO23YI4kNtc4M/cKoIY7P+IYdrZcgMObvedyjoSQA==", - "dev": true, - "dependencies": { - "function.prototype.name": "^1.1.0", - "has": "^1.0.0", - "object.fromentries": "^2.0.0", - "prop-types": "^15.7.0" - }, - "peerDependencies": { - "react": "^17.0.0-0" - } - }, - "node_modules/@wojtekmaj/enzyme-adapter-react-17/node_modules/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, - "dependencies": { - "has": "^1.0.3", - "object-is": "^1.1.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@wojtekmaj/enzyme-adapter-react-17/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true - }, - "node_modules/@wojtekmaj/enzyme-adapter-react-17/node_modules/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, - "dependencies": { - "object-assign": "^4.1.1", - "react-is": "^17.0.1", - "react-shallow-renderer": "^16.13.1", - "scheduler": "^0.20.1" - }, - "peerDependencies": { - "react": "17.0.1" - } - }, - "node_modules/@wojtekmaj/enzyme-adapter-react-17/node_modules/react-test-renderer/node_modules/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, - "dependencies": { - "object-assign": "^4.1.1", - "react-is": "^16.12.0 || ^17.0.0" - }, - "peerDependencies": { - "react": "^16.0.0 || ^17.0.0" - } - }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -7164,12 +7083,6 @@ "node": ">=6.0" } }, - "node_modules/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 - }, "node_modules/array-flatten": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", @@ -7260,9 +7173,9 @@ } }, "node_modules/async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", "dev": true, "dependencies": { "lodash": "^4.17.14" @@ -8289,61 +8202,6 @@ "integrity": "sha512-tzWzvgePgLORb9/3a0YenggReLKAIb2owL03H2Xdoe5pKcUyWRSEQ8xfCar8t2SIAuEDwtmx2da1YB52YuHQMQ==", "dev": true }, - "node_modules/cheerio": { - "version": "1.0.0-rc.3", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.3.tgz", - "integrity": "sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA==", - "dev": true, - "dependencies": { - "css-select": "~1.2.0", - "dom-serializer": "~0.1.1", - "entities": "~1.1.1", - "htmlparser2": "^3.9.1", - "lodash": "^4.15.0", - "parse5": "^3.0.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cheerio/node_modules/css-select": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", - "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", - "dev": true, - "dependencies": { - "boolbase": "~1.0.0", - "css-what": "2.1", - "domutils": "1.5.1", - "nth-check": "~1.0.1" - } - }, - "node_modules/cheerio/node_modules/dom-serializer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", - "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", - "dev": true, - "dependencies": { - "domelementtype": "^1.3.0", - "entities": "^1.1.1" - } - }, - "node_modules/cheerio/node_modules/domutils": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", - "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", - "dev": true, - "dependencies": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "node_modules/cheerio/node_modules/entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", - "dev": true - }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -8967,15 +8825,6 @@ "node": ">=8.0.0" } }, - "node_modules/css-what": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", - "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -9426,12 +9275,6 @@ "node": ">=8" } }, - "node_modules/discontinuous-range": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", - "integrity": "sha1-44Mx8IRLukm5qctxx3FYWqsbxlo=", - "dev": true - }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -9533,15 +9376,6 @@ "node": ">=12" } }, - "node_modules/domhandler": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", - "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", - "dev": true, - "dependencies": { - "domelementtype": "1" - } - }, "node_modules/domutils": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", @@ -9685,52 +9519,6 @@ "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==", "dev": true }, - "node_modules/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, - "dependencies": { - "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" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/enzyme-shallow-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.1.tgz", - "integrity": "sha512-hGA3i1so8OrYOZSM9whlkNmVHOicJpsjgTzC+wn2JMJXhq1oO4kA4bJ5MsfzSIcC71aLDKzJ6gZpIxrqt3QTAQ==", - "dev": true, - "dependencies": { - "has": "^1.0.3", - "object-is": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz", @@ -10812,9 +10600,9 @@ } }, "node_modules/eventsource": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.0.tgz", - "integrity": "sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.1.tgz", + "integrity": "sha512-qV5ZC0h7jYIAOhArFJgSfdyz6rALJyb270714o7ZtNnw2WSJ+eexhKtE0O8LYPRsHZHf2osHKZBxGPvm3kPkCA==", "dev": true, "optional": true, "peer": true, @@ -11844,38 +11632,12 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, - "node_modules/function.prototype.name": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.2.tgz", - "integrity": "sha512-C8A+LlHBJjB2AdcRPorc5JvJ5VUoWlXdEHLOJdCI7kjHEtGTpHQUiqMvCIKUwIsGwZX2jZJy761AXsn356bJQg==", - "dev": true, - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1", - "functions-have-names": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, - "node_modules/functions-have-names": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.1.tgz", - "integrity": "sha512-j48B/ZI7VKs3sgeI2cZp7WXWmZXu7Iq5pl5/vptV5N2mq+DGFuS/ulaDjtaoLpYzuD6u8UgrUKHfgo7fDTSiBA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -12240,18 +12002,6 @@ "wbuf": "^1.1.0" } }, - "node_modules/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, - "dependencies": { - "array-filter": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/html-encoding-sniffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", @@ -12288,40 +12038,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/htmlparser2": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", - "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", - "dev": true, - "dependencies": { - "domelementtype": "^1.3.1", - "domhandler": "^2.3.0", - "domutils": "^1.5.1", - "entities": "^1.1.1", - "inherits": "^2.0.1", - "readable-stream": "^3.1.1" - } - }, - "node_modules/htmlparser2/node_modules/entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", - "dev": true - }, - "node_modules/htmlparser2/node_modules/readable-stream": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", - "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/http-deceiver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", @@ -12644,14 +12360,10 @@ "dev": true }, "node_modules/ini": { - "version": "1.3.5", - "resolved": "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz", - "integrity": "sha1-7uJfVtscnsYIXgwid4CD9Zar+Sc=", - "deprecated": "Please update to ini >=1.3.6 to avoid a prototype pollution issue", - "dev": true, - "engines": { - "node": "*" - } + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true }, "node_modules/internal-slot": { "version": "1.0.3", @@ -12963,12 +12675,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/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 - }, "node_modules/is-symbol": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", @@ -17176,24 +16882,12 @@ "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", "dev": true }, - "node_modules/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 - }, "node_modules/lodash.flatmap": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.flatmap/-/lodash.flatmap-4.5.0.tgz", "integrity": "sha1-74y/QI9uSCaGYzRTBcaswLd4cC4=", "dev": true }, - "node_modules/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 - }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -17205,12 +16899,6 @@ "integrity": "sha1-Cwih3PaDl8OXhVwyOXg4Mt90A9E=", "dev": true }, - "node_modules/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 - }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -17685,12 +17373,6 @@ "mkdirp": "bin/cmd.js" } }, - "node_modules/moo": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz", - "integrity": "sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w==", - "dev": true - }, "node_modules/moo-color": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/moo-color/-/moo-color-1.0.3.tgz", @@ -17770,31 +17452,6 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, - "node_modules/nearley": { - "version": "2.19.1", - "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.19.1.tgz", - "integrity": "sha512-xq47GIUGXxU9vQg7g/y1o1xuKnkO7ev4nRWqftmQrLkfnE/FjRqDaGOUakM8XHPn/6pW3bGjU2wgoJyId90rqg==", - "dev": true, - "dependencies": { - "commander": "^2.19.0", - "moo": "^0.5.0", - "railroad-diagrams": "^1.0.0", - "randexp": "0.4.6", - "semver": "^5.4.1" - }, - "bin": { - "nearley-railroad": "bin/nearley-railroad.js", - "nearley-test": "bin/nearley-test.js", - "nearley-unparse": "bin/nearley-unparse.js", - "nearleyc": "bin/nearleyc.js" - } - }, - "node_modules/nearley/node_modules/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 - }, "node_modules/negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", @@ -18449,15 +18106,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parse5": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz", - "integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -19570,30 +19218,11 @@ "performance-now": "^2.1.0" } }, - "node_modules/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 - }, "node_modules/ramda": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.2.tgz", "integrity": "sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA==" }, - "node_modules/randexp": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", - "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", - "dev": true, - "dependencies": { - "discontinuous-range": "1.0.0", - "ret": "~0.1.10" - }, - "engines": { - "node": ">=0.12" - } - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -19709,21 +19338,21 @@ } }, "node_modules/react-copy-to-clipboard": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.4.tgz", - "integrity": "sha512-IeVAiNVKjSPeGax/Gmkqfa/+PuMTBhutEvFUaMQLwE2tS0EXrAdgOpWDX26bWTXF3HrioorR7lr08NqeYUWQCQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz", + "integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==", "dependencies": { - "copy-to-clipboard": "^3", - "prop-types": "^15.5.8" + "copy-to-clipboard": "^3.3.1", + "prop-types": "^15.8.1" }, "peerDependencies": { - "react": "^15.3.0 || ^16.0.0 || ^17.0.0" + "react": "^15.3.0 || 16 || 17 || 18" } }, "node_modules/react-datepicker": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.7.0.tgz", - "integrity": "sha512-FS8KgbwqpxmJBv/bUdA42MYqYZa+fEYcpc746DZiHvVE2nhjrW/dg7c5B5fIUuI8gZET6FOzuDgezNcj568Czw==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.8.0.tgz", + "integrity": "sha512-u69zXGHMpxAa4LeYR83vucQoUCJQ6m/WBsSxmUMu/M8ahTSVMMyiyQzauHgZA2NUr9y0FUgOAix71hGYUb6tvg==", "dependencies": { "@popperjs/core": "^2.9.2", "classnames": "^2.2.6", @@ -19733,8 +19362,8 @@ "react-popper": "^2.2.5" }, "peerDependencies": { - "react": "^16.9.0 || ^17", - "react-dom": "^16.9.0 || ^17" + "react": "^16.9.0 || ^17 || ^18", + "react-dom": "^16.9.0 || ^17 || ^18" } }, "node_modules/react-datepicker/node_modules/react-onclickoutside": { @@ -22525,11 +22154,11 @@ } }, "node_modules/react-swipeable": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/react-swipeable/-/react-swipeable-6.2.0.tgz", - "integrity": "sha512-nWQ8dEM8e/uswZLSIkXUsAnQmnX4MTcryOHBQIQYRMJFDpgDBSiVbKsz/BZVCIScF4NtJh16oyxwaNOepR6xSw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/react-swipeable/-/react-swipeable-7.0.0.tgz", + "integrity": "sha512-NI7KGfQ6gwNFN0Hor3vytYW3iRfMMaivGEuxcADOOfBCx/kqwXE8IfHFxEcxSUkxCYf38COLKYd9EMYZghqaUA==", "peerDependencies": { - "react": "^16.8.3 || ^17" + "react": "^16.8.3 || ^17 || ^18" } }, "node_modules/react-tag-autocomplete": { @@ -23103,15 +22732,6 @@ "node": ">=4" } }, - "node_modules/ret": { - "version": "0.1.15", - "resolved": "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz", - "integrity": "sha1-uKSCXVvbH8P29Twrwz+BOIaBx7w=", - "dev": true, - "engines": { - "node": ">=0.12" - } - }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -23191,16 +22811,6 @@ "randombytes": "^2.1.0" } }, - "node_modules/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, - "dependencies": { - "lodash.flattendeep": "^4.4.0", - "nearley": "^2.7.10" - } - }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -23281,16 +22891,6 @@ "node": ">=10" } }, - "node_modules/scheduler": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", - "dev": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, "node_modules/schema-utils": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", @@ -24099,23 +23699,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/string.prototype.trim": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.1.tgz", - "integrity": "sha512-MjGFEeqixw47dAMFMtgUro/I0+wNqZB5GKXGt1fFr24u3TzDXCPu7J9Buppzoe3r/LqkSDLDDJzE15RGWDGAVw==", - "dev": true, - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1", - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/string.prototype.trimend": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", @@ -24954,14 +24537,14 @@ } }, "node_modules/terser": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.12.1.tgz", - "integrity": "sha512-NXbs+7nisos5E+yXwAD+y7zrcTkMqb0dEJxIGtSKPdCBzopf7ni4odPul2aechpV7EXNvOudYOX2bb5tln1jbQ==", + "version": "5.14.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz", + "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", "dev": true, "dependencies": { + "@jridgewell/source-map": "^0.3.2", "acorn": "^8.5.0", "commander": "^2.20.0", - "source-map": "~0.7.2", "source-map-support": "~0.5.20" }, "bin": { @@ -25061,15 +24644,6 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, - "node_modules/terser/node_modules/source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -29726,6 +29300,29 @@ "integrity": "sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ==", "dev": true }, + "@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "dependencies": { + "@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + } + } + }, "@jridgewell/sourcemap-codec": { "version": "1.4.11", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz", @@ -30724,15 +30321,6 @@ "@types/node": "*" } }, - "@types/cheerio": { - "version": "0.22.22", - "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.22.tgz", - "integrity": "sha512-05DYX4zU96IBfZFY+t3Mh88nlwSMtmmzSYaQkKN48T495VV1dkHSah6qYyDTN5ngaS0i0VonH37m+RuzSM0YiA==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, "@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -30752,16 +30340,6 @@ "@types/node": "*" } }, - "@types/enzyme": { - "version": "3.10.11", - "resolved": "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.10.11.tgz", - "integrity": "sha512-LEtC7zXsQlbGXWGcnnmOI7rTyP+i1QzQv4Va91RKXDEukLDaNyxu0rXlfMiGEhJwfgTPCTb0R+Pnlj//oM9e/w==", - "dev": true, - "requires": { - "@types/cheerio": "*", - "@types/react": "*" - } - }, "@types/eslint": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.29.0.tgz", @@ -31677,76 +31255,6 @@ "@xtuc/long": "4.2.2" } }, - "@wojtekmaj/enzyme-adapter-react-17": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@wojtekmaj/enzyme-adapter-react-17/-/enzyme-adapter-react-17-0.6.5.tgz", - "integrity": "sha512-ChIObUiXXYUiqzXPqOai+p6KF5dlbItpDDYsftUOQiAiygbMDlLeJIjynC6ZrJIa2U2MpRp4YJmtR2GQyIHjgA==", - "dev": true, - "requires": { - "@wojtekmaj/enzyme-adapter-utils": "^0.1.1", - "enzyme-shallow-equal": "^1.0.0", - "has": "^1.0.0", - "object.assign": "^4.1.0", - "object.values": "^1.1.0", - "prop-types": "^15.7.0", - "react-is": "^17.0.2", - "react-test-renderer": "^17.0.0" - }, - "dependencies": { - "@wojtekmaj/enzyme-adapter-utils": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@wojtekmaj/enzyme-adapter-utils/-/enzyme-adapter-utils-0.1.4.tgz", - "integrity": "sha512-ARGIQSIIv3oBia1m5Ihn1VU0FGmft6KPe39SBKTb8p7LSXO23YI4kNtc4M/cKoIY7P+IYdrZcgMObvedyjoSQA==", - "dev": true, - "requires": { - "function.prototype.name": "^1.1.0", - "has": "^1.0.0", - "object.fromentries": "^2.0.0", - "prop-types": "^15.7.0" - } - }, - "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" - } - }, - "react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true - }, - "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-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" - } - } - } - } - } - }, "@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -32062,12 +31570,6 @@ "@babel/runtime-corejs3": "^7.10.2" } }, - "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-flatten": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", @@ -32134,9 +31636,9 @@ "dev": true }, "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", "dev": true, "requires": { "lodash": "^4.17.14" @@ -32894,60 +32396,6 @@ "integrity": "sha512-tzWzvgePgLORb9/3a0YenggReLKAIb2owL03H2Xdoe5pKcUyWRSEQ8xfCar8t2SIAuEDwtmx2da1YB52YuHQMQ==", "dev": true }, - "cheerio": { - "version": "1.0.0-rc.3", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.3.tgz", - "integrity": "sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA==", - "dev": true, - "requires": { - "css-select": "~1.2.0", - "dom-serializer": "~0.1.1", - "entities": "~1.1.1", - "htmlparser2": "^3.9.1", - "lodash": "^4.15.0", - "parse5": "^3.0.1" - }, - "dependencies": { - "css-select": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", - "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", - "dev": true, - "requires": { - "boolbase": "~1.0.0", - "css-what": "2.1", - "domutils": "1.5.1", - "nth-check": "~1.0.1" - } - }, - "dom-serializer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", - "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", - "dev": true, - "requires": { - "domelementtype": "^1.3.0", - "entities": "^1.1.1" - } - }, - "domutils": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", - "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", - "dev": true, - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", - "dev": true - } - } - }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -33452,12 +32900,6 @@ "source-map": "^0.6.1" } }, - "css-what": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", - "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", - "dev": true - }, "css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -33798,12 +33240,6 @@ "path-type": "^4.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 - }, "dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -33901,15 +33337,6 @@ "webidl-conversions": "^7.0.0" } }, - "domhandler": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", - "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", - "dev": true, - "requires": { - "domelementtype": "1" - } - }, "domutils": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", @@ -34033,46 +33460,6 @@ "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==", "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" - } - }, - "enzyme-shallow-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.1.tgz", - "integrity": "sha512-hGA3i1so8OrYOZSM9whlkNmVHOicJpsjgTzC+wn2JMJXhq1oO4kA4bJ5MsfzSIcC71aLDKzJ6gZpIxrqt3QTAQ==", - "dev": true, - "requires": { - "has": "^1.0.3", - "object-is": "^1.0.2" - } - }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz", @@ -34866,9 +34253,9 @@ "dev": true }, "eventsource": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.0.tgz", - "integrity": "sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.1.tgz", + "integrity": "sha512-qV5ZC0h7jYIAOhArFJgSfdyz6rALJyb270714o7ZtNnw2WSJ+eexhKtE0O8LYPRsHZHf2osHKZBxGPvm3kPkCA==", "dev": true, "optional": true, "peer": true, @@ -35645,29 +35032,12 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, - "function.prototype.name": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.2.tgz", - "integrity": "sha512-C8A+LlHBJjB2AdcRPorc5JvJ5VUoWlXdEHLOJdCI7kjHEtGTpHQUiqMvCIKUwIsGwZX2jZJy761AXsn356bJQg==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1", - "functions-have-names": "^1.2.0" - } - }, "functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, - "functions-have-names": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.1.tgz", - "integrity": "sha512-j48B/ZI7VKs3sgeI2cZp7WXWmZXu7Iq5pl5/vptV5N2mq+DGFuS/ulaDjtaoLpYzuD6u8UgrUKHfgo7fDTSiBA==", - "dev": true - }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -35932,15 +35302,6 @@ "wbuf": "^1.1.0" } }, - "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": "3.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", @@ -35968,39 +35329,6 @@ "integrity": "sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==", "dev": true }, - "htmlparser2": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", - "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", - "dev": true, - "requires": { - "domelementtype": "^1.3.1", - "domhandler": "^2.3.0", - "domutils": "^1.5.1", - "entities": "^1.1.1", - "inherits": "^2.0.1", - "readable-stream": "^3.1.1" - }, - "dependencies": { - "entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", - "dev": true - }, - "readable-stream": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", - "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, "http-deceiver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", @@ -36228,9 +35556,9 @@ "dev": true }, "ini": { - "version": "1.3.5", - "resolved": "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz", - "integrity": "sha1-7uJfVtscnsYIXgwid4CD9Zar+Sc=", + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, "internal-slot": { @@ -36441,12 +35769,6 @@ "has-tostringtag": "^1.0.0" } }, - "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-symbol": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", @@ -39654,24 +38976,12 @@ "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", "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.flatmap": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.flatmap/-/lodash.flatmap-4.5.0.tgz", "integrity": "sha1-74y/QI9uSCaGYzRTBcaswLd4cC4=", "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.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -39683,12 +38993,6 @@ "integrity": "sha1-Cwih3PaDl8OXhVwyOXg4Mt90A9E=", "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", @@ -40053,12 +39357,6 @@ "minimist": "^1.2.5" } }, - "moo": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz", - "integrity": "sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w==", - "dev": true - }, "moo-color": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/moo-color/-/moo-color-1.0.3.tgz", @@ -40129,27 +39427,6 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, - "nearley": { - "version": "2.19.1", - "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.19.1.tgz", - "integrity": "sha512-xq47GIUGXxU9vQg7g/y1o1xuKnkO7ev4nRWqftmQrLkfnE/FjRqDaGOUakM8XHPn/6pW3bGjU2wgoJyId90rqg==", - "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", @@ -40645,15 +39922,6 @@ "lines-and-columns": "^1.1.6" } }, - "parse5": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz", - "integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -41433,27 +40701,11 @@ "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 - }, "ramda": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.2.tgz", "integrity": "sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA==" }, - "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", @@ -41544,18 +40796,18 @@ "requires": {} }, "react-copy-to-clipboard": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.4.tgz", - "integrity": "sha512-IeVAiNVKjSPeGax/Gmkqfa/+PuMTBhutEvFUaMQLwE2tS0EXrAdgOpWDX26bWTXF3HrioorR7lr08NqeYUWQCQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz", + "integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==", "requires": { - "copy-to-clipboard": "^3", - "prop-types": "^15.5.8" + "copy-to-clipboard": "^3.3.1", + "prop-types": "^15.8.1" } }, "react-datepicker": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.7.0.tgz", - "integrity": "sha512-FS8KgbwqpxmJBv/bUdA42MYqYZa+fEYcpc746DZiHvVE2nhjrW/dg7c5B5fIUuI8gZET6FOzuDgezNcj568Czw==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.8.0.tgz", + "integrity": "sha512-u69zXGHMpxAa4LeYR83vucQoUCJQ6m/WBsSxmUMu/M8ahTSVMMyiyQzauHgZA2NUr9y0FUgOAix71hGYUb6tvg==", "requires": { "@popperjs/core": "^2.9.2", "classnames": "^2.2.6", @@ -43512,9 +42764,9 @@ } }, "react-swipeable": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/react-swipeable/-/react-swipeable-6.2.0.tgz", - "integrity": "sha512-nWQ8dEM8e/uswZLSIkXUsAnQmnX4MTcryOHBQIQYRMJFDpgDBSiVbKsz/BZVCIScF4NtJh16oyxwaNOepR6xSw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/react-swipeable/-/react-swipeable-7.0.0.tgz", + "integrity": "sha512-NI7KGfQ6gwNFN0Hor3vytYW3iRfMMaivGEuxcADOOfBCx/kqwXE8IfHFxEcxSUkxCYf38COLKYd9EMYZghqaUA==", "requires": {} }, "react-tag-autocomplete": { @@ -43960,12 +43212,6 @@ "signal-exit": "^3.0.2" } }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz", - "integrity": "sha1-uKSCXVvbH8P29Twrwz+BOIaBx7w=", - "dev": true - }, "retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -44025,16 +43271,6 @@ } } }, - "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" - } - }, "run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -44100,16 +43336,6 @@ "xmlchars": "^2.2.0" } }, - "scheduler": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", - "dev": true, - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, "schema-utils": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", @@ -44786,17 +44012,6 @@ } } }, - "string.prototype.trim": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.1.tgz", - "integrity": "sha512-MjGFEeqixw47dAMFMtgUro/I0+wNqZB5GKXGt1fFr24u3TzDXCPu7J9Buppzoe3r/LqkSDLDDJzE15RGWDGAVw==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1", - "function-bind": "^1.1.1" - } - }, "string.prototype.trimend": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", @@ -45426,14 +44641,14 @@ } }, "terser": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.12.1.tgz", - "integrity": "sha512-NXbs+7nisos5E+yXwAD+y7zrcTkMqb0dEJxIGtSKPdCBzopf7ni4odPul2aechpV7EXNvOudYOX2bb5tln1jbQ==", + "version": "5.14.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz", + "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", "dev": true, "requires": { + "@jridgewell/source-map": "^0.3.2", "acorn": "^8.5.0", "commander": "^2.20.0", - "source-map": "~0.7.2", "source-map-support": "~0.5.20" }, "dependencies": { @@ -45448,12 +44663,6 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true - }, - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true } } }, diff --git a/package.json b/package.json index 6f86668e..c151625e 100644 --- a/package.json +++ b/package.json @@ -16,10 +16,11 @@ "build": "DISABLE_ESLINT_PLUGIN=true react-scripts build && node scripts/replace-version.mjs", "build:dist": "npm run build && node scripts/create-dist-file.mjs", "build:serve": "serve -p 5000 ./build", - "test": "jest --env=jsdom --colors --verbose", + "test": "jest --env=jsdom --colors", "test:coverage": "npm run test -- --coverage --coverageReporters=text --coverageReporters=text-summary", "test:ci": "npm run test:coverage -- --coverageReporters=clover --ci", "test:pretty": "npm run test:coverage -- --coverageReporters=html", + "test:verbose": "npm run test -- --verbose", "mutate": "./node_modules/.bin/stryker run --concurrency 4 --ignoreStatic" }, "dependencies": { @@ -45,14 +46,14 @@ "react": "^18.1.0", "react-chartjs-2": "^4.1.0", "react-colorful": "^5.5.1", - "react-copy-to-clipboard": "^5.0.4", - "react-datepicker": "^4.7.0", + "react-copy-to-clipboard": "^5.1.0", + "react-datepicker": "^4.8.0", "react-dom": "^18.1.0", "react-external-link": "^2.0.0", "react-leaflet": "^4.0.0", "react-redux": "^8.0.0", "react-router-dom": "^6.3.0", - "react-swipeable": "^6.2.0", + "react-swipeable": "^7.0.0", "react-tag-autocomplete": "^6.3.0", "reactstrap": "^9.0.1", "redux": "^4.2.0", @@ -75,7 +76,6 @@ "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^13.1.1", "@testing-library/user-event": "^14.1.1", - "@types/enzyme": "^3.10.11", "@types/jest": "^27.4.1", "@types/json2csv": "^5.0.3", "@types/leaflet": "^1.7.9", @@ -88,11 +88,9 @@ "@types/react-dom": "^18.0.3", "@types/react-tag-autocomplete": "^6.1.1", "@types/uuid": "^8.3.4", - "@wojtekmaj/enzyme-adapter-react-17": "0.6.5", "adm-zip": "^0.5.9", "babel-jest": "^28.0.3", "chalk": "^5.0.1", - "enzyme": "^3.11.0", "eslint": "^8.12.0", "identity-obj-proxy": "^3.0.0", "jest": "^28.0.3", diff --git a/src/api/services/ShlinkApiClient.ts b/src/api/services/ShlinkApiClient.ts index 54682e06..3e4c1b51 100644 --- a/src/api/services/ShlinkApiClient.ts +++ b/src/api/services/ShlinkApiClient.ts @@ -29,7 +29,7 @@ const normalizeOrderByInParams = (params: ShlinkShortUrlsListParams): ShlinkShor return { ...rest, orderBy: orderToString(orderBy) }; }; -export default class ShlinkApiClient { +export class ShlinkApiClient { public constructor( private readonly axios: AxiosInstance, private readonly baseUrl: string, diff --git a/src/api/services/ShlinkApiClientBuilder.ts b/src/api/services/ShlinkApiClientBuilder.ts index d2ba24cd..0d7efc0f 100644 --- a/src/api/services/ShlinkApiClientBuilder.ts +++ b/src/api/services/ShlinkApiClientBuilder.ts @@ -2,7 +2,7 @@ import { AxiosInstance } from 'axios'; import { prop } from 'ramda'; import { hasServerData, SelectedServer, ServerWithId } from '../../servers/data'; import { GetState } from '../../container/types'; -import ShlinkApiClient from './ShlinkApiClient'; +import { ShlinkApiClient } from './ShlinkApiClient'; const apiClients: Record = {}; @@ -12,7 +12,7 @@ const getSelectedServerFromState = (getState: GetState): SelectedServer => prop( export type ShlinkApiClientBuilder = (getStateOrSelectedServer: GetState | ServerWithId) => ShlinkApiClient; -const buildShlinkApiClient = (axios: AxiosInstance): ShlinkApiClientBuilder => ( +export const buildShlinkApiClient = (axios: AxiosInstance): ShlinkApiClientBuilder => ( getStateOrSelectedServer: GetState | ServerWithId, ) => { const server = isGetState(getStateOrSelectedServer) @@ -32,5 +32,3 @@ const buildShlinkApiClient = (axios: AxiosInstance): ShlinkApiClientBuilder => ( return apiClients[clientKey]; }; - -export default buildShlinkApiClient; diff --git a/src/api/services/provideServices.ts b/src/api/services/provideServices.ts index 2d0abf4e..3ddb60e7 100644 --- a/src/api/services/provideServices.ts +++ b/src/api/services/provideServices.ts @@ -1,5 +1,5 @@ import Bottle from 'bottlejs'; -import buildShlinkApiClient from './ShlinkApiClientBuilder'; +import { buildShlinkApiClient } from './ShlinkApiClientBuilder'; const provideServices = (bottle: Bottle) => { bottle.serviceFactory('buildShlinkApiClient', buildShlinkApiClient, 'axios'); diff --git a/src/common/AsideMenu.tsx b/src/common/AsideMenu.tsx index 826ff3b6..f7c81965 100644 --- a/src/common/AsideMenu.tsx +++ b/src/common/AsideMenu.tsx @@ -34,7 +34,7 @@ const AsideMenuItem: FC = ({ children, to, className, ...res ); -const AsideMenu = (DeleteServerButton: FC) => ( +export const AsideMenu = (DeleteServerButton: FC) => ( { selectedServer, showOnMobile = false }: AsideMenuProps, ) => { const hasId = isServerWithId(selectedServer); @@ -89,5 +89,3 @@ const AsideMenu = (DeleteServerButton: FC) => ( ); }; - -export default AsideMenu; diff --git a/src/common/Home.tsx b/src/common/Home.tsx index cd8ab483..018f9ce3 100644 --- a/src/common/Home.tsx +++ b/src/common/Home.tsx @@ -5,7 +5,7 @@ import { Card, Row } from 'reactstrap'; import { ExternalLink } from 'react-external-link'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faExternalLinkAlt, faPlus } from '@fortawesome/free-solid-svg-icons'; -import ServersListGroup from '../servers/ServersListGroup'; +import { ServersListGroup } from '../servers/ServersListGroup'; import { ServersMap } from '../servers/data'; import { ShlinkLogo } from './img/ShlinkLogo'; import './Home.scss'; diff --git a/src/common/ShlinkVersions.tsx b/src/common/ShlinkVersions.tsx index 5210e50e..e750b25f 100644 --- a/src/common/ShlinkVersions.tsx +++ b/src/common/ShlinkVersions.tsx @@ -17,7 +17,7 @@ const VersionLink = ({ project, version }: { project: 'shlink' | 'shlink-web-cli ); -const ShlinkVersions = ({ selectedServer, clientVersion = SHLINK_WEB_CLIENT_VERSION }: ShlinkVersionsProps) => { +export const ShlinkVersions = ({ selectedServer, clientVersion = SHLINK_WEB_CLIENT_VERSION }: ShlinkVersionsProps) => { const normalizedClientVersion = normalizeVersion(clientVersion); return ( @@ -29,5 +29,3 @@ const ShlinkVersions = ({ selectedServer, clientVersion = SHLINK_WEB_CLIENT_VERS ); }; - -export default ShlinkVersions; diff --git a/src/common/ShlinkVersionsContainer.tsx b/src/common/ShlinkVersionsContainer.tsx index 378283e8..0cdf8405 100644 --- a/src/common/ShlinkVersionsContainer.tsx +++ b/src/common/ShlinkVersionsContainer.tsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; import { SelectedServer } from '../servers/data'; -import ShlinkVersions from './ShlinkVersions'; +import { ShlinkVersions } from './ShlinkVersions'; import { Sidebar } from './reducers/sidebar'; import './ShlinkVersionsContainer.scss'; @@ -9,7 +9,7 @@ export interface ShlinkVersionsContainerProps { sidebar: Sidebar; } -const ShlinkVersionsContainer = ({ selectedServer, sidebar }: ShlinkVersionsContainerProps) => { +export const ShlinkVersionsContainer = ({ selectedServer, sidebar }: ShlinkVersionsContainerProps) => { const classes = classNames('text-center', { 'shlink-versions-container--with-sidebar': sidebar.sidebarPresent, }); @@ -20,5 +20,3 @@ const ShlinkVersionsContainer = ({ selectedServer, sidebar }: ShlinkVersionsCont ); }; - -export default ShlinkVersionsContainer; diff --git a/src/common/services/provideServices.ts b/src/common/services/provideServices.ts index 604ab336..3a613bd2 100644 --- a/src/common/services/provideServices.ts +++ b/src/common/services/provideServices.ts @@ -4,9 +4,9 @@ import { ScrollToTop } from '../ScrollToTop'; import { MainHeader } from '../MainHeader'; import { Home } from '../Home'; import { MenuLayout } from '../MenuLayout'; -import AsideMenu from '../AsideMenu'; +import { AsideMenu } from '../AsideMenu'; import { ErrorHandler } from '../ErrorHandler'; -import ShlinkVersionsContainer from '../ShlinkVersionsContainer'; +import { ShlinkVersionsContainer } from '../ShlinkVersionsContainer'; import { ConnectDecorator } from '../../container/types'; import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer'; import { sidebarNotPresent, sidebarPresent } from '../reducers/sidebar'; diff --git a/src/domains/ManageDomains.tsx b/src/domains/ManageDomains.tsx index 3c149dd1..a6170595 100644 --- a/src/domains/ManageDomains.tsx +++ b/src/domains/ManageDomains.tsx @@ -1,5 +1,5 @@ import { FC, useEffect } from 'react'; -import Message from '../utils/Message'; +import { Message } from '../utils/Message'; import { Result } from '../utils/Result'; import { ShlinkApiError } from '../api/ShlinkApiError'; import { SimpleCard } from '../utils/SimpleCard'; diff --git a/src/domains/helpers/DomainStatusIcon.tsx b/src/domains/helpers/DomainStatusIcon.tsx index 03ebf168..79fe86ea 100644 --- a/src/domains/helpers/DomainStatusIcon.tsx +++ b/src/domains/helpers/DomainStatusIcon.tsx @@ -8,6 +8,7 @@ import { faCircleNotch as loadingStatusIcon, } from '@fortawesome/free-solid-svg-icons'; import { MediaMatcher } from '../../utils/types'; +import { mutableRefToElementRef } from '../../utils/helpers/components'; import { DomainStatus } from '../data'; interface DomainStatusIconProps { @@ -34,11 +35,7 @@ export const DomainStatusIcon: FC = ({ status, matchMedia return ( <> - { - ref.current = el; - }} - > + {status === 'valid' ? : } diff --git a/src/domains/reducers/domainsList.ts b/src/domains/reducers/domainsList.ts index 1868c689..b2349810 100644 --- a/src/domains/reducers/domainsList.ts +++ b/src/domains/reducers/domainsList.ts @@ -65,7 +65,7 @@ export default buildReducer({ ({ ...initialState, domains, filteredDomains: domains, defaultRedirects }), [FILTER_DOMAINS]: (state, { searchTerm }) => ({ ...state, - filteredDomains: state.domains.filter(({ domain }) => domain.toLowerCase().match(searchTerm)), + filteredDomains: state.domains.filter(({ domain }) => domain.toLowerCase().match(searchTerm.toLowerCase())), }), [EDIT_DOMAIN_REDIRECTS]: (state, { domain, redirects }) => ({ ...state, diff --git a/src/index.scss b/src/index.scss index f6f09ed3..f9cb0643 100644 --- a/src/index.scss +++ b/src/index.scss @@ -13,6 +13,7 @@ :root { scroll-behavior: auto; + color-scheme: var(--color-scheme); } html, diff --git a/src/servers/CreateServer.tsx b/src/servers/CreateServer.tsx index 6bdf75d7..df96e838 100644 --- a/src/servers/CreateServer.tsx +++ b/src/servers/CreateServer.tsx @@ -4,7 +4,7 @@ import { Button } from 'reactstrap'; import { useNavigate } from 'react-router-dom'; import { Result } from '../utils/Result'; import { NoMenuLayout } from '../common/NoMenuLayout'; -import { StateFlagTimeout, useGoBack, useToggle } from '../utils/helpers/hooks'; +import { TimeoutToggle, useGoBack, useToggle } from '../utils/helpers/hooks'; import { ServerForm } from './helpers/ServerForm'; import { ImportServersBtnProps } from './helpers/ImportServersBtn'; import { ServerData, ServersMap, ServerWithId } from './data'; @@ -26,14 +26,14 @@ const ImportResult = ({ type }: { type: 'error' | 'success' }) => ( ); -const CreateServer = (ImportServersBtn: FC, useStateFlagTimeout: StateFlagTimeout) => ( +export const CreateServer = (ImportServersBtn: FC, useTimeoutToggle: TimeoutToggle) => ( { servers, createServer }: CreateServerProps, ) => { const navigate = useNavigate(); const goBack = useGoBack(); const hasServers = !!Object.keys(servers).length; - const [serversImported, setServersImported] = useStateFlagTimeout(false, SHOW_IMPORT_MSG_TIME); - const [errorImporting, setErrorImporting] = useStateFlagTimeout(false, SHOW_IMPORT_MSG_TIME); + const [serversImported, setServersImported] = useTimeoutToggle(false, SHOW_IMPORT_MSG_TIME); + const [errorImporting, setErrorImporting] = useTimeoutToggle(false, SHOW_IMPORT_MSG_TIME); const [isConfirmModalOpen, toggleConfirmModal] = useToggle(); const [serverData, setServerData] = useState(); const save = () => { @@ -77,5 +77,3 @@ const CreateServer = (ImportServersBtn: FC, useStateFlagT ); }; - -export default CreateServer; diff --git a/src/servers/DeleteServerButton.tsx b/src/servers/DeleteServerButton.tsx index a40e6ecc..4759938c 100644 --- a/src/servers/DeleteServerButton.tsx +++ b/src/servers/DeleteServerButton.tsx @@ -11,7 +11,7 @@ export type DeleteServerButtonProps = PropsWithChildren<{ textClassName?: string; }>; -const DeleteServerButton = (DeleteServerModal: FC): FC => ( +export const DeleteServerButton = (DeleteServerModal: FC): FC => ( { server, className, children, textClassName }, ) => { const [isModalOpen, , showModal, hideModal] = useToggle(); @@ -27,5 +27,3 @@ const DeleteServerButton = (DeleteServerModal: FC): FC ); }; - -export default DeleteServerButton; diff --git a/src/servers/DeleteServerModal.tsx b/src/servers/DeleteServerModal.tsx index 9679dd7c..8fba141c 100644 --- a/src/servers/DeleteServerModal.tsx +++ b/src/servers/DeleteServerModal.tsx @@ -14,7 +14,7 @@ interface DeleteServerModalConnectProps extends DeleteServerModalProps { deleteServer: (server: ServerWithId) => void; } -const DeleteServerModal: FC = ( +export const DeleteServerModal: FC = ( { server, toggle, isOpen, deleteServer, redirectHome = true }, ) => { const navigate = useNavigate(); @@ -26,7 +26,7 @@ const DeleteServerModal: FC = ( return ( - Remove server + Remove server

Are you sure you want to remove {server ? server.name : ''}?

@@ -43,5 +43,3 @@ const DeleteServerModal: FC = ( ); }; - -export default DeleteServerModal; diff --git a/src/servers/ManageServers.tsx b/src/servers/ManageServers.tsx index 53c364ce..80f91d61 100644 --- a/src/servers/ManageServers.tsx +++ b/src/servers/ManageServers.tsx @@ -7,7 +7,7 @@ import { NoMenuLayout } from '../common/NoMenuLayout'; import { SimpleCard } from '../utils/SimpleCard'; import { SearchField } from '../utils/SearchField'; import { Result } from '../utils/Result'; -import { StateFlagTimeout } from '../utils/helpers/hooks'; +import { TimeoutToggle } from '../utils/helpers/hooks'; import { ImportServersBtnProps } from './helpers/ImportServersBtn'; import { ServersMap } from './data'; import { ManageServersRowProps } from './ManageServersRow'; @@ -22,16 +22,16 @@ const SHOW_IMPORT_MSG_TIME = 4000; export const ManageServers = ( serversExporter: ServersExporter, ImportServersBtn: FC, - useStateFlagTimeout: StateFlagTimeout, + useTimeoutToggle: TimeoutToggle, ManageServersRow: FC, ): FC => ({ servers }) => { const allServers = Object.values(servers); const [serversList, setServersList] = useState(allServers); const filterServers = (searchTerm: string) => setServersList( - allServers.filter(({ name, url }) => `${name} ${url}`.match(searchTerm)), + allServers.filter(({ name, url }) => `${name} ${url}`.toLowerCase().match(searchTerm.toLowerCase())), ); const hasAutoConnect = serversList.some(({ autoConnect }) => !!autoConnect); - const [errorImporting, setErrorImporting] = useStateFlagTimeout(false, SHOW_IMPORT_MSG_TIME); + const [errorImporting, setErrorImporting] = useTimeoutToggle(false, SHOW_IMPORT_MSG_TIME); useEffect(() => { setServersList(Object.values(servers)); diff --git a/src/servers/ManageServersRowDropdown.tsx b/src/servers/ManageServersRowDropdown.tsx index 51863d58..f0da17a5 100644 --- a/src/servers/ManageServersRowDropdown.tsx +++ b/src/servers/ManageServersRowDropdown.tsx @@ -39,7 +39,7 @@ export const ManageServersRowDropdown = ( Edit server - setAutoConnect(server, !server.autoConnect)}> + setAutoConnect(server, !isAutoConnect)}> {isAutoConnect ? 'Do not a' : 'A'}uto-connect diff --git a/src/servers/ServersListGroup.tsx b/src/servers/ServersListGroup.tsx index a88787a7..36bac3bf 100644 --- a/src/servers/ServersListGroup.tsx +++ b/src/servers/ServersListGroup.tsx @@ -19,7 +19,7 @@ const ServerListItem = ({ id, name }: { id: string; name: string }) => ( ); -const ServersListGroup: FC = ({ servers, children, embedded = false }) => ( +export const ServersListGroup: FC = ({ servers, children, embedded = false }) => ( <> {children &&

{children}
} {servers.length > 0 && ( @@ -31,5 +31,3 @@ const ServersListGroup: FC = ({ servers, children, embedd )} ); - -export default ServersListGroup; diff --git a/src/servers/helpers/ImportServersBtn.tsx b/src/servers/helpers/ImportServersBtn.tsx index e01500c6..7df560ac 100644 --- a/src/servers/helpers/ImportServersBtn.tsx +++ b/src/servers/helpers/ImportServersBtn.tsx @@ -1,16 +1,15 @@ -import { useRef, RefObject, ChangeEvent, MutableRefObject, useState, useEffect, FC, PropsWithChildren } from 'react'; +import { useRef, ChangeEvent, useState, useEffect, FC, PropsWithChildren } from 'react'; import { Button, UncontrolledTooltip } from 'reactstrap'; import { complement, pipe } from 'ramda'; import { faFileUpload as importIcon } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { useToggle } from '../../utils/helpers/hooks'; +import { mutableRefToElementRef } from '../../utils/helpers/components'; import { ServersImporter } from '../services/ServersImporter'; import { ServerData, ServersMap } from '../data'; import { DuplicatedServersModal } from './DuplicatedServersModal'; import './ImportServersBtn.scss'; -type Ref = RefObject | MutableRefObject; - export type ImportServersBtnProps = PropsWithChildren<{ onImport?: () => void; onImportError?: (error: Error) => void; @@ -21,23 +20,21 @@ export type ImportServersBtnProps = PropsWithChildren<{ interface ImportServersBtnConnectProps extends ImportServersBtnProps { createServers: (servers: ServerData[]) => void; servers: ServersMap; - fileRef: Ref; } const serversFiltering = (servers: ServerData[]) => ({ url, apiKey }: ServerData) => servers.some((server) => server.url === url && server.apiKey === apiKey); -const ImportServersBtn = ({ importServersFromFile }: ServersImporter): FC => ({ +export const ImportServersBtn = ({ importServersFromFile }: ServersImporter): FC => ({ createServers, servers, - fileRef, children, onImport = () => {}, onImportError = () => {}, tooltipPlacement = 'bottom', className = '', }) => { - const ref = fileRef ?? useRef(); + const ref = useRef(); const [serversToCreate, setServersToCreate] = useState(); const [duplicatedServers, setDuplicatedServers] = useState([]); const [isModalOpen,, showModal, hideModal] = useToggle(); @@ -78,7 +75,13 @@ const ImportServersBtn = ({ importServersFromFile }: ServersImporter): FCname, apiKey and url. - + ); }; - -export default ImportServersBtn; diff --git a/src/servers/helpers/ServerError.tsx b/src/servers/helpers/ServerError.tsx index c84a5708..6f605969 100644 --- a/src/servers/helpers/ServerError.tsx +++ b/src/servers/helpers/ServerError.tsx @@ -1,7 +1,7 @@ import { FC } from 'react'; import { Link } from 'react-router-dom'; -import Message from '../../utils/Message'; -import ServersListGroup from '../ServersListGroup'; +import { Message } from '../../utils/Message'; +import { ServersListGroup } from '../ServersListGroup'; import { DeleteServerButtonProps } from '../DeleteServerButton'; import { isServerWithId, SelectedServer, ServersMap } from '../data'; import { NoMenuLayout } from '../../common/NoMenuLayout'; diff --git a/src/servers/helpers/withSelectedServer.tsx b/src/servers/helpers/withSelectedServer.tsx index 6c41667e..dcd7e228 100644 --- a/src/servers/helpers/withSelectedServer.tsx +++ b/src/servers/helpers/withSelectedServer.tsx @@ -1,6 +1,6 @@ import { FC, useEffect } from 'react'; import { useParams } from 'react-router-dom'; -import Message from '../../utils/Message'; +import { Message } from '../../utils/Message'; import { isNotFoundServer, SelectedServer } from '../data'; import { NoMenuLayout } from '../../common/NoMenuLayout'; diff --git a/src/servers/services/ServersExporter.ts b/src/servers/services/ServersExporter.ts index 54e2ccf7..df4fa680 100644 --- a/src/servers/services/ServersExporter.ts +++ b/src/servers/services/ServersExporter.ts @@ -1,5 +1,5 @@ import { values } from 'ramda'; -import LocalStorage from '../../utils/services/LocalStorage'; +import { LocalStorage } from '../../utils/services/LocalStorage'; import { ServersMap, serverWithIdToServerData } from '../data'; import { saveCsv } from '../../utils/helpers/files'; import { JsonToCsv } from '../../utils/helpers/csvjson'; diff --git a/src/servers/services/provideServices.ts b/src/servers/services/provideServices.ts index 9f177cb6..6f56cc84 100644 --- a/src/servers/services/provideServices.ts +++ b/src/servers/services/provideServices.ts @@ -1,10 +1,10 @@ import Bottle from 'bottlejs'; -import CreateServer from '../CreateServer'; +import { CreateServer } from '../CreateServer'; import { ServersDropdown } from '../ServersDropdown'; -import DeleteServerModal from '../DeleteServerModal'; -import DeleteServerButton from '../DeleteServerButton'; +import { DeleteServerModal } from '../DeleteServerModal'; +import { DeleteServerButton } from '../DeleteServerButton'; import { EditServer } from '../EditServer'; -import ImportServersBtn from '../helpers/ImportServersBtn'; +import { ImportServersBtn } from '../helpers/ImportServersBtn'; import { resetSelectedServer, selectServer } from '../reducers/selectedServer'; import { createServer, createServers, deleteServer, editServer, setAutoConnect } from '../reducers/servers'; import { fetchServers } from '../reducers/remoteServers'; @@ -25,7 +25,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { ManageServers, 'ServersExporter', 'ImportServersBtn', - 'useStateFlagTimeout', + 'useTimeoutToggle', 'ManageServersRow', ); bottle.decorator('ManageServers', withoutSelectedServer); @@ -36,7 +36,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.serviceFactory('ManageServersRowDropdown', ManageServersRowDropdown, 'DeleteServerModal'); bottle.decorator('ManageServersRowDropdown', connect(null, ['setAutoConnect'])); - bottle.serviceFactory('CreateServer', CreateServer, 'ImportServersBtn', 'useStateFlagTimeout'); + bottle.serviceFactory('CreateServer', CreateServer, 'ImportServersBtn', 'useTimeoutToggle'); bottle.decorator('CreateServer', withoutSelectedServer); bottle.decorator('CreateServer', connect(['selectedServer', 'servers'], ['createServer', 'resetSelectedServer'])); diff --git a/src/settings/RealTimeUpdatesSettings.tsx b/src/settings/RealTimeUpdatesSettings.tsx index e2424e5a..f587baa8 100644 --- a/src/settings/RealTimeUpdatesSettings.tsx +++ b/src/settings/RealTimeUpdatesSettings.tsx @@ -1,10 +1,11 @@ import { FormGroup, Input } from 'reactstrap'; import classNames from 'classnames'; -import ToggleSwitch from '../utils/ToggleSwitch'; +import { ToggleSwitch } from '../utils/ToggleSwitch'; import { SimpleCard } from '../utils/SimpleCard'; import { FormText } from '../utils/forms/FormText'; import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup'; import { Settings } from './reducers/settings'; +import { useDomId } from '../utils/helpers/hooks'; interface RealTimeUpdatesProps { settings: Settings; @@ -14,43 +15,48 @@ interface RealTimeUpdatesProps { const intervalValue = (interval?: number) => (!interval ? '' : `${interval}`); -const RealTimeUpdatesSettings = ( +export const RealTimeUpdatesSettings = ( { settings: { realTimeUpdates }, toggleRealTimeUpdates, setRealTimeUpdatesInterval }: RealTimeUpdatesProps, -) => ( - - - - Enable or disable real-time updates. - - Real-time updates are currently being {realTimeUpdates.enabled ? 'processed' : 'ignored'}. - - - - - setRealTimeUpdatesInterval(Number(target.value))} - /> - {realTimeUpdates.enabled && ( - - {realTimeUpdates.interval !== undefined && realTimeUpdates.interval > 0 && ( - - Updates will be reflected in the UI every {realTimeUpdates.interval} minute{realTimeUpdates.interval > 1 && 's'}. - - )} - {!realTimeUpdates.interval && 'Updates will be reflected in the UI as soon as they happen.'} - - )} - - -); +) => { + const inputId = useDomId(); -export default RealTimeUpdatesSettings; + return ( + + + + Enable or disable real-time updates. + + Real-time updates are currently being {realTimeUpdates.enabled ? 'processed' : 'ignored'}. + + + + + setRealTimeUpdatesInterval(Number(target.value))} + /> + {realTimeUpdates.enabled && ( + + {realTimeUpdates.interval !== undefined && realTimeUpdates.interval > 0 && ( + + Updates will be reflected in the UI + every {realTimeUpdates.interval} minute{realTimeUpdates.interval > 1 && 's'}. + + )} + {!realTimeUpdates.interval && 'Updates will be reflected in the UI as soon as they happen.'} + + )} + + + ); +}; diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index ff4aed50..d1c8fd72 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -9,7 +9,7 @@ const SettingsSections: FC<{ items: ReactNode[] }> = ({ items }) => ( ); -const Settings = ( +export const Settings = ( RealTimeUpdates: FC, ShortUrlCreation: FC, ShortUrlsList: FC, @@ -32,5 +32,3 @@ const Settings = ( ); - -export default Settings; diff --git a/src/settings/ShortUrlCreationSettings.tsx b/src/settings/ShortUrlCreationSettings.tsx index a369e90f..d5523550 100644 --- a/src/settings/ShortUrlCreationSettings.tsx +++ b/src/settings/ShortUrlCreationSettings.tsx @@ -1,7 +1,7 @@ import { FC, ReactNode } from 'react'; import { DropdownItem, FormGroup } from 'reactstrap'; import { SimpleCard } from '../utils/SimpleCard'; -import ToggleSwitch from '../utils/ToggleSwitch'; +import { ToggleSwitch } from '../utils/ToggleSwitch'; import { DropdownBtn } from '../utils/DropdownBtn'; import { FormText } from '../utils/forms/FormText'; import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup'; diff --git a/src/settings/UserInterfaceSettings.tsx b/src/settings/UserInterfaceSettings.tsx index 5ca2e462..e21b3460 100644 --- a/src/settings/UserInterfaceSettings.tsx +++ b/src/settings/UserInterfaceSettings.tsx @@ -2,7 +2,7 @@ import { FC } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faSun, faMoon } from '@fortawesome/free-solid-svg-icons'; import { SimpleCard } from '../utils/SimpleCard'; -import ToggleSwitch from '../utils/ToggleSwitch'; +import { ToggleSwitch } from '../utils/ToggleSwitch'; import { changeThemeInMarkup, Theme } from '../utils/theme'; import { Settings, UiSettings } from './reducers/settings'; import './UserInterfaceSettings.scss'; diff --git a/src/settings/services/provideServices.ts b/src/settings/services/provideServices.ts index 344ad8a8..d64b8592 100644 --- a/src/settings/services/provideServices.ts +++ b/src/settings/services/provideServices.ts @@ -1,6 +1,6 @@ import Bottle from 'bottlejs'; -import RealTimeUpdatesSettings from '../RealTimeUpdatesSettings'; -import Settings from '../Settings'; +import { RealTimeUpdatesSettings } from '../RealTimeUpdatesSettings'; +import { Settings } from '../Settings'; import { setRealTimeUpdatesInterval, setShortUrlCreationSettings, diff --git a/src/short-urls/CreateShortUrl.tsx b/src/short-urls/CreateShortUrl.tsx index ef9694f4..8da5a480 100644 --- a/src/short-urls/CreateShortUrl.tsx +++ b/src/short-urls/CreateShortUrl.tsx @@ -33,7 +33,10 @@ const getInitialState = (settings?: ShortUrlCreationSettings): ShortUrlData => ( forwardQuery: settings?.forwardQuery ?? true, }); -const CreateShortUrl = (ShortUrlForm: FC, CreateShortUrlResult: FC) => ({ +export const CreateShortUrl = ( + ShortUrlForm: FC, + CreateShortUrlResult: FC, +) => ({ createShortUrl, shortUrlCreationResult, resetCreateShortUrl, @@ -52,7 +55,6 @@ const CreateShortUrl = (ShortUrlForm: FC, CreateShortUrlResul mode={basicMode ? 'create-basic' : 'create'} onSave={async (data: ShortUrlData) => { resetCreateShortUrl(); - return createShortUrl(data); }} /> @@ -64,5 +66,3 @@ const CreateShortUrl = (ShortUrlForm: FC, CreateShortUrlResul ); }; - -export default CreateShortUrl; diff --git a/src/short-urls/EditShortUrl.tsx b/src/short-urls/EditShortUrl.tsx index 35a93d04..3c243a25 100644 --- a/src/short-urls/EditShortUrl.tsx +++ b/src/short-urls/EditShortUrl.tsx @@ -5,17 +5,18 @@ import { faArrowLeft } from '@fortawesome/free-solid-svg-icons'; import { ExternalLink } from 'react-external-link'; import { useLocation, useParams } from 'react-router-dom'; import { SelectedServer } from '../servers/data'; -import { Settings, ShortUrlCreationSettings } from '../settings/reducers/settings'; +import { Settings } from '../settings/reducers/settings'; import { OptionalString } from '../utils/utils'; import { parseQuery } from '../utils/helpers/query'; -import Message from '../utils/Message'; +import { Message } from '../utils/Message'; import { Result } from '../utils/Result'; import { ShlinkApiError } from '../api/ShlinkApiError'; import { useGoBack, useToggle } from '../utils/helpers/hooks'; import { ShortUrlFormProps } from './ShortUrlForm'; import { ShortUrlDetail } from './reducers/shortUrlDetail'; -import { EditShortUrlData, ShortUrl, ShortUrlData } from './data'; +import { EditShortUrlData } from './data'; import { ShortUrlEdition } from './reducers/shortUrlEdition'; +import { shortUrlDataFromShortUrl, urlDecodeShortCode } from './helpers'; interface EditShortUrlConnectProps { settings: Settings; @@ -26,27 +27,6 @@ interface EditShortUrlConnectProps { editShortUrl: (shortUrl: string, domain: OptionalString, data: EditShortUrlData) => Promise; } -const getInitialState = (shortUrl?: ShortUrl, settings?: ShortUrlCreationSettings): ShortUrlData => { - const validateUrl = settings?.validateUrls ?? false; - - if (!shortUrl) { - return { longUrl: '', validateUrl }; - } - - return { - longUrl: shortUrl.longUrl, - tags: shortUrl.tags, - title: shortUrl.title ?? undefined, - domain: shortUrl.domain ?? undefined, - validSince: shortUrl.meta.validSince ?? undefined, - validUntil: shortUrl.meta.validUntil ?? undefined, - maxVisits: shortUrl.meta.maxVisits ?? undefined, - crawlable: shortUrl.crawlable, - forwardQuery: shortUrl.forwardQuery, - validateUrl, - }; -}; - export const EditShortUrl = (ShortUrlForm: FC) => ({ settings: { shortUrlCreation: shortUrlCreationSettings }, selectedServer, @@ -62,13 +42,13 @@ export const EditShortUrl = (ShortUrlForm: FC) => ({ const { saving, error: savingError, errorData: savingErrorData } = shortUrlEdition; const { domain } = parseQuery<{ domain?: string }>(search); const initialState = useMemo( - () => getInitialState(shortUrl, shortUrlCreationSettings), + () => shortUrlDataFromShortUrl(shortUrl, shortUrlCreationSettings), [shortUrl, shortUrlCreationSettings], ); const [savingSucceeded,, isSuccessful, isNotSuccessful] = useToggle(); useEffect(() => { - params.shortCode && getShortUrlDetail(params.shortCode, domain); + params.shortCode && getShortUrlDetail(urlDecodeShortCode(params.shortCode), domain); }, []); if (loading) { diff --git a/src/short-urls/Paginator.tsx b/src/short-urls/Paginator.tsx index 8103623f..45c2fd10 100644 --- a/src/short-urls/Paginator.tsx +++ b/src/short-urls/Paginator.tsx @@ -15,7 +15,7 @@ interface PaginatorProps { currentQueryString?: string; } -const Paginator = ({ paginator, serverId, currentQueryString = '' }: PaginatorProps) => { +export const Paginator = ({ paginator, serverId, currentQueryString = '' }: PaginatorProps) => { const { currentPage = 0, pagesCount = 0 } = paginator ?? {}; const urlForPage = (pageNumber: NumberOrEllipsis) => `/server/${serverId}/list-short-urls/${pageNumber}${currentQueryString}`; @@ -49,5 +49,3 @@ const Paginator = ({ paginator, serverId, currentQueryString = '' }: PaginatorPr ); }; - -export default Paginator; diff --git a/src/short-urls/ShortUrlForm.tsx b/src/short-urls/ShortUrlForm.tsx index 23406833..5385d784 100644 --- a/src/short-urls/ShortUrlForm.tsx +++ b/src/short-urls/ShortUrlForm.tsx @@ -3,11 +3,11 @@ import { InputType } from 'reactstrap/types/lib/Input'; import { Button, FormGroup, Input, Row } from 'reactstrap'; import { cond, isEmpty, pipe, replace, trim, T } from 'ramda'; import { parseISO } from 'date-fns'; -import DateInput, { DateInputProps } from '../utils/DateInput'; +import { DateInput, DateInputProps } from '../utils/DateInput'; import { supportsCrawlableVisits, supportsForwardQuery } from '../utils/helpers/features'; import { SimpleCard } from '../utils/SimpleCard'; import { handleEventPreventingDefault, hasValue, OptionalString } from '../utils/utils'; -import Checkbox from '../utils/Checkbox'; +import { Checkbox } from '../utils/Checkbox'; import { SelectedServer } from '../servers/data'; import { TagsSelectorProps } from '../tags/helpers/TagsSelector'; import { DomainSelectorProps } from '../domains/DomainSelector'; @@ -118,7 +118,7 @@ export const ShortUrlForm = ( const showBehaviorCard = showCrawlableControl || showForwardQueryControl; return ( -
+ {isBasicMode && basicComponents} {!isBasicMode && ( <> diff --git a/src/short-urls/ShortUrlsList.tsx b/src/short-urls/ShortUrlsList.tsx index b46652ba..6b3f7a82 100644 --- a/src/short-urls/ShortUrlsList.tsx +++ b/src/short-urls/ShortUrlsList.tsx @@ -11,7 +11,7 @@ import { ShlinkShortUrlsListParams } from '../api/types'; import { DEFAULT_SHORT_URLS_ORDERING, Settings } from '../settings/reducers/settings'; import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList'; import { ShortUrlsTableProps } from './ShortUrlsTable'; -import Paginator from './Paginator'; +import { Paginator } from './Paginator'; import { useShortUrlsQuery } from './helpers/hooks'; import { ShortUrlsOrderableFields } from './data'; import { ShortUrlsFilteringProps } from './ShortUrlsFilteringBar'; @@ -23,7 +23,7 @@ interface ShortUrlsListProps { settings: Settings; } -const ShortUrlsList = ( +export const ShortUrlsList = ( ShortUrlsTable: FC, ShortUrlsFilteringBar: FC, ) => boundToMercureHub(({ listShortUrls, shortUrlsList, selectedServer, settings }: ShortUrlsListProps) => { @@ -83,5 +83,3 @@ const ShortUrlsList = ( ); }, () => [Topics.visits]); - -export default ShortUrlsList; diff --git a/src/short-urls/helpers/CreateShortUrlResult.tsx b/src/short-urls/helpers/CreateShortUrlResult.tsx index f3e2cbfd..218755b3 100644 --- a/src/short-urls/helpers/CreateShortUrlResult.tsx +++ b/src/short-urls/helpers/CreateShortUrlResult.tsx @@ -6,7 +6,7 @@ import { useEffect } from 'react'; import CopyToClipboard from 'react-copy-to-clipboard'; import { Tooltip } from 'reactstrap'; import { ShortUrlCreation } from '../reducers/shortUrlCreation'; -import { StateFlagTimeout } from '../../utils/helpers/hooks'; +import { TimeoutToggle } from '../../utils/helpers/hooks'; import { Result } from '../../utils/Result'; import './CreateShortUrlResult.scss'; import { ShlinkApiError } from '../../api/ShlinkApiError'; @@ -16,10 +16,10 @@ export interface CreateShortUrlResultProps extends ShortUrlCreation { canBeClosed?: boolean; } -const CreateShortUrlResult = (useStateFlagTimeout: StateFlagTimeout) => ( +export const CreateShortUrlResult = (useTimeoutToggle: TimeoutToggle) => ( { error, errorData, result, resetCreateShortUrl, canBeClosed = false }: CreateShortUrlResultProps, ) => { - const [showCopyTooltip, setShowCopyTooltip] = useStateFlagTimeout(); + const [showCopyTooltip, setShowCopyTooltip] = useTimeoutToggle(); useEffect(() => { resetCreateShortUrl(); @@ -43,7 +43,7 @@ const CreateShortUrlResult = (useStateFlagTimeout: StateFlagTimeout) => ( return ( {canBeClosed && } - Great! The short URL is {shortUrl} + Great! The short URL is {shortUrl} -
{ - titleRef.current = el ?? undefined; - }} + ref={mutableRefToElementRef(titleRef)} > {tag.tag} @@ -82,5 +87,3 @@ const TagCard = ( ); }; - -export default TagCard; diff --git a/src/tags/TagsList.tsx b/src/tags/TagsList.tsx index ca8c3ca7..2c825931 100644 --- a/src/tags/TagsList.tsx +++ b/src/tags/TagsList.tsx @@ -1,7 +1,7 @@ import { FC, useEffect, useState } from 'react'; import { Row } from 'reactstrap'; import { pipe } from 'ramda'; -import Message from '../utils/Message'; +import { Message } from '../utils/Message'; import { SearchField } from '../utils/SearchField'; import { SelectedServer } from '../servers/data'; import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; @@ -30,7 +30,7 @@ export interface TagsListProps { settings: Settings; } -const TagsList = (TagsCards: FC, TagsTable: FC) => boundToMercureHub(( +export const TagsList = (TagsCards: FC, TagsTable: FC) => boundToMercureHub(( { filterTags, forceListTags, tagsList, selectedServer, settings }: TagsListProps, ) => { const [mode, setMode] = useState(settings.tags?.defaultMode ?? 'cards'); @@ -104,5 +104,3 @@ const TagsList = (TagsCards: FC, TagsTable: FC ); }, () => [Topics.visits]); - -export default TagsList; diff --git a/src/tags/TagsTableRow.tsx b/src/tags/TagsTableRow.tsx index 55bf4ba9..1bad323e 100644 --- a/src/tags/TagsTableRow.tsx +++ b/src/tags/TagsTableRow.tsx @@ -4,11 +4,11 @@ import { DropdownItem } from 'reactstrap'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faTrash as deleteIcon, faPencilAlt as editIcon } from '@fortawesome/free-solid-svg-icons'; import { getServerId, SelectedServer } from '../servers/data'; -import ColorGenerator from '../utils/services/ColorGenerator'; +import { ColorGenerator } from '../utils/services/ColorGenerator'; import { prettify } from '../utils/helpers/numbers'; import { useToggle } from '../utils/helpers/hooks'; import { DropdownBtnMenu } from '../utils/DropdownBtnMenu'; -import TagBullet from './helpers/TagBullet'; +import { TagBullet } from './helpers/TagBullet'; import { NormalizedTag, TagModalProps } from './data'; export interface TagsTableRowProps { diff --git a/src/tags/helpers/DeleteTagConfirmModal.tsx b/src/tags/helpers/DeleteTagConfirmModal.tsx index 61027e88..6828af13 100644 --- a/src/tags/helpers/DeleteTagConfirmModal.tsx +++ b/src/tags/helpers/DeleteTagConfirmModal.tsx @@ -10,7 +10,7 @@ interface DeleteTagConfirmModalProps extends TagModalProps { tagDelete: TagDeletion; } -const DeleteTagConfirmModal = ( +export const DeleteTagConfirmModal = ( { tag, toggle, isOpen, deleteTag, tagDelete, tagDeleted }: DeleteTagConfirmModalProps, ) => { const { deleting, error, errorData } = tagDelete; @@ -22,9 +22,7 @@ const DeleteTagConfirmModal = ( return ( - - Delete tag - + Delete tag Are you sure you want to delete tag {tag}? {error && ( @@ -42,5 +40,3 @@ const DeleteTagConfirmModal = ( ); }; - -export default DeleteTagConfirmModal; diff --git a/src/tags/helpers/EditTagModal.tsx b/src/tags/helpers/EditTagModal.tsx index 6bd676ae..4b6af3d7 100644 --- a/src/tags/helpers/EditTagModal.tsx +++ b/src/tags/helpers/EditTagModal.tsx @@ -5,7 +5,7 @@ import { faPalette as colorIcon } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { useToggle } from '../../utils/helpers/hooks'; import { handleEventPreventingDefault } from '../../utils/utils'; -import ColorGenerator from '../../utils/services/ColorGenerator'; +import { ColorGenerator } from '../../utils/services/ColorGenerator'; import { TagModalProps } from '../data'; import { TagEdition } from '../reducers/tagEdit'; import { Result } from '../../utils/Result'; @@ -18,7 +18,7 @@ interface EditTagModalProps extends TagModalProps { tagEdited: (oldName: string, newName: string, color: string) => void; } -const EditTagModal = ({ getColorForKey }: ColorGenerator) => ( +export const EditTagModal = ({ getColorForKey }: ColorGenerator) => ( { tag, editTag, toggle, tagEdited, isOpen, tagEdit }: EditTagModalProps, ) => { const [newTagName, setNewTagName] = useState(tag); @@ -34,7 +34,7 @@ const EditTagModal = ({ getColorForKey }: ColorGenerator) => ( return ( - + Edit tag @@ -78,5 +78,3 @@ const EditTagModal = ({ getColorForKey }: ColorGenerator) => ( ); }; - -export default EditTagModal; diff --git a/src/tags/helpers/Tag.tsx b/src/tags/helpers/Tag.tsx index 1bfa2daf..61ab3057 100644 --- a/src/tags/helpers/Tag.tsx +++ b/src/tags/helpers/Tag.tsx @@ -1,6 +1,6 @@ import { FC, MouseEventHandler, PropsWithChildren } from 'react'; import classNames from 'classnames'; -import ColorGenerator from '../../utils/services/ColorGenerator'; +import { ColorGenerator } from '../../utils/services/ColorGenerator'; import './Tag.scss'; type TagProps = PropsWithChildren<{ @@ -12,15 +12,15 @@ type TagProps = PropsWithChildren<{ onClose?: MouseEventHandler; }>; -const Tag: FC = ({ text, children, clearable, className = '', colorGenerator, onClick, onClose }) => ( +export const Tag: FC = ({ text, children, clearable, className = '', colorGenerator, onClick, onClose }) => ( {children ?? text} - {clearable && ×} + {clearable && ( + × + )} ); - -export default Tag; diff --git a/src/tags/helpers/TagBullet.tsx b/src/tags/helpers/TagBullet.tsx index d7769262..dc84f729 100644 --- a/src/tags/helpers/TagBullet.tsx +++ b/src/tags/helpers/TagBullet.tsx @@ -1,4 +1,4 @@ -import ColorGenerator from '../../utils/services/ColorGenerator'; +import { ColorGenerator } from '../../utils/services/ColorGenerator'; import './TagBullet.scss'; interface TagBulletProps { @@ -6,11 +6,9 @@ interface TagBulletProps { colorGenerator: ColorGenerator; } -const TagBullet = ({ tag, colorGenerator }: TagBulletProps) => ( +export const TagBullet = ({ tag, colorGenerator }: TagBulletProps) => (
); - -export default TagBullet; diff --git a/src/tags/helpers/TagsSelector.tsx b/src/tags/helpers/TagsSelector.tsx index 6f47b58f..3d785af3 100644 --- a/src/tags/helpers/TagsSelector.tsx +++ b/src/tags/helpers/TagsSelector.tsx @@ -1,10 +1,10 @@ import { useEffect } from 'react'; import ReactTags, { SuggestionComponentProps, TagComponentProps } from 'react-tag-autocomplete'; -import ColorGenerator from '../../utils/services/ColorGenerator'; +import { ColorGenerator } from '../../utils/services/ColorGenerator'; import { Settings } from '../../settings/reducers/settings'; import { TagsList } from '../reducers/tagsList'; -import TagBullet from './TagBullet'; -import Tag from './Tag'; +import { TagBullet } from './TagBullet'; +import { Tag } from './Tag'; export interface TagsSelectorProps { selectedTags: string[]; @@ -21,7 +21,7 @@ interface TagsSelectorConnectProps extends TagsSelectorProps { const toComponentTag = (tag: string) => ({ id: tag, name: tag }); -const TagsSelector = (colorGenerator: ColorGenerator) => ( +export const TagsSelector = (colorGenerator: ColorGenerator) => ( { selectedTags, onChange, placeholder, listTags, tagsList, settings, allowNew = true }: TagsSelectorConnectProps, ) => { useEffect(() => { @@ -68,5 +68,3 @@ const TagsSelector = (colorGenerator: ColorGenerator) => ( /> ); }; - -export default TagsSelector; diff --git a/src/tags/reducers/tagEdit.ts b/src/tags/reducers/tagEdit.ts index 6e9829a8..ea58ad8c 100644 --- a/src/tags/reducers/tagEdit.ts +++ b/src/tags/reducers/tagEdit.ts @@ -2,7 +2,7 @@ import { pick } from 'ramda'; import { Action, Dispatch } from 'redux'; import { buildReducer } from '../../utils/helpers/redux'; import { GetState } from '../../container/types'; -import ColorGenerator from '../../utils/services/ColorGenerator'; +import { ColorGenerator } from '../../utils/services/ColorGenerator'; import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; import { ProblemDetailsError } from '../../api/types'; import { parseApiError } from '../../api/utils'; diff --git a/src/tags/reducers/tagsList.ts b/src/tags/reducers/tagsList.ts index afc6e620..de84c709 100644 --- a/src/tags/reducers/tagsList.ts +++ b/src/tags/reducers/tagsList.ts @@ -96,7 +96,7 @@ export default buildReducer({ }), [FILTER_TAGS]: (state, { searchTerm }) => ({ ...state, - filteredTags: state.tags.filter((tag) => tag.toLowerCase().match(searchTerm)), + filteredTags: state.tags.filter((tag) => tag.toLowerCase().match(searchTerm.toLowerCase())), }), [CREATE_VISITS]: (state, { createdVisits }) => ({ ...state, diff --git a/src/tags/services/provideServices.ts b/src/tags/services/provideServices.ts index 5a1a4221..e7d58453 100644 --- a/src/tags/services/provideServices.ts +++ b/src/tags/services/provideServices.ts @@ -1,9 +1,9 @@ import Bottle, { IContainer } from 'bottlejs'; -import TagsSelector from '../helpers/TagsSelector'; -import TagCard from '../TagCard'; -import DeleteTagConfirmModal from '../helpers/DeleteTagConfirmModal'; -import EditTagModal from '../helpers/EditTagModal'; -import TagsList from '../TagsList'; +import { TagsSelector } from '../helpers/TagsSelector'; +import { TagCard } from '../TagCard'; +import { DeleteTagConfirmModal } from '../helpers/DeleteTagConfirmModal'; +import { EditTagModal } from '../helpers/EditTagModal'; +import { TagsList } from '../TagsList'; import { filterTags, listTags } from '../reducers/tagsList'; import { deleteTag, tagDeleted } from '../reducers/tagDelete'; import { editTag, tagEdited } from '../reducers/tagEdit'; diff --git a/src/theme/theme.scss b/src/theme/theme.scss index aecf5243..7d797202 100644 --- a/src/theme/theme.scss +++ b/src/theme/theme.scss @@ -31,6 +31,7 @@ $darkBorderInputColor: $darkBorderColor; $darkTableHighlightColor: $darkBorderColor; html:not([data-theme='dark']) { + --color-scheme: initial; --primary-color: #{$lightPrimaryColor}; --primary-color-alfa: #{$lightPrimaryColorAlfa}; --secondary-color: #{$lightSecondaryColor}; @@ -48,6 +49,7 @@ html:not([data-theme='dark']) { } html[data-theme='dark'] { + --color-scheme: dark; --primary-color: #{$darkPrimaryColor}; --primary-color-alfa: #{$darkPrimaryColorAlfa}; --secondary-color: #{$darkSecondaryColor}; diff --git a/src/utils/BooleanControl.tsx b/src/utils/BooleanControl.tsx index 76360a7c..fd93a8d0 100644 --- a/src/utils/BooleanControl.tsx +++ b/src/utils/BooleanControl.tsx @@ -14,7 +14,7 @@ interface BooleanControlWithTypeProps extends BooleanControlProps { type: 'switch' | 'checkbox'; } -const BooleanControl: FC = ( +export const BooleanControl: FC = ( { checked = false, onChange = identity, className, children, type, inline = false }, ) => { const id = useDomId(); @@ -32,5 +32,3 @@ const BooleanControl: FC = ( ); }; - -export default BooleanControl; diff --git a/src/utils/Checkbox.tsx b/src/utils/Checkbox.tsx index de13b008..4e4ef083 100644 --- a/src/utils/Checkbox.tsx +++ b/src/utils/Checkbox.tsx @@ -1,6 +1,4 @@ import { FC } from 'react'; -import BooleanControl, { BooleanControlProps } from './BooleanControl'; +import { BooleanControl, BooleanControlProps } from './BooleanControl'; -const Checkbox: FC = (props) => ; - -export default Checkbox; +export const Checkbox: FC = (props) => ; diff --git a/src/utils/DateInput.tsx b/src/utils/DateInput.tsx index f365d477..f2b7cc96 100644 --- a/src/utils/DateInput.tsx +++ b/src/utils/DateInput.tsx @@ -8,7 +8,7 @@ import './DateInput.scss'; export type DateInputProps = ReactDatePickerProps; -const DateInput = (props: DateInputProps) => { +export const DateInput = (props: DateInputProps) => { const { className, isClearable, selected } = props; const showCalendarIcon = !isClearable || isNil(selected); const ref = useRef<{ input: HTMLInputElement }>(); @@ -32,5 +32,3 @@ const DateInput = (props: DateInputProps) => {
); }; - -export default DateInput; diff --git a/src/utils/InfoTooltip.tsx b/src/utils/InfoTooltip.tsx index 34cc7239..15ecae62 100644 --- a/src/utils/InfoTooltip.tsx +++ b/src/utils/InfoTooltip.tsx @@ -3,21 +3,19 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faInfoCircle as infoIcon } from '@fortawesome/free-solid-svg-icons'; import { UncontrolledTooltip } from 'reactstrap'; import { Placement } from '@popperjs/core'; +import { mutableRefToElementRef } from './helpers/components'; -type InfoTooltipProps = PropsWithChildren<{ +export type InfoTooltipProps = PropsWithChildren<{ className?: string; placement: Placement; }>; export const InfoTooltip: FC = ({ className = '', placement, children }) => { - const ref = useRef(); - const refCallback = (el: HTMLSpanElement) => { - ref.current = el; - }; + const ref = useRef(); return ( <> - + ref.current) as any} placement={placement}>{children} diff --git a/src/utils/Message.tsx b/src/utils/Message.tsx index 7d267ca4..e171e93c 100644 --- a/src/utils/Message.tsx +++ b/src/utils/Message.tsx @@ -30,7 +30,9 @@ export type MessageProps = PropsWithChildren<{ type?: MessageType; }>; -const Message: FC = ({ className, children, loading = false, type = 'default', fullWidth = false }) => { +export const Message: FC = ( + { className, children, loading = false, type = 'default', fullWidth = false }, +) => { const classes = classNames({ 'col-md-12': fullWidth, 'col-md-10 offset-md-1': !fullWidth, @@ -50,5 +52,3 @@ const Message: FC = ({ className, children, loading = false, type ); }; - -export default Message; diff --git a/src/utils/PaginationDropdown.tsx b/src/utils/PaginationDropdown.tsx index f98af1a9..c349dc52 100644 --- a/src/utils/PaginationDropdown.tsx +++ b/src/utils/PaginationDropdown.tsx @@ -7,7 +7,7 @@ interface PaginationDropdownProps { toggleClassName?: string; } -const PaginationDropdown = ({ toggleClassName, ranges, value, setValue }: PaginationDropdownProps) => ( +export const PaginationDropdown = ({ toggleClassName, ranges, value, setValue }: PaginationDropdownProps) => ( Paginate @@ -23,5 +23,3 @@ const PaginationDropdown = ({ toggleClassName, ranges, value, setValue }: Pagina ); - -export default PaginationDropdown; diff --git a/src/utils/Result.tsx b/src/utils/Result.tsx index eacf40f0..e32872a8 100644 --- a/src/utils/Result.tsx +++ b/src/utils/Result.tsx @@ -15,6 +15,7 @@ export const Result: FC = ({ children, type, className, small = fal
{ +export const Time = ({ date, format = 'yyyy-MM-dd HH:mm', relative = false }: TimeProps) => { const dateObject = isDateObject(date) ? date : parseISO(date); return ( diff --git a/src/utils/ToggleSwitch.tsx b/src/utils/ToggleSwitch.tsx index e5b30b0e..797e9b5d 100644 --- a/src/utils/ToggleSwitch.tsx +++ b/src/utils/ToggleSwitch.tsx @@ -1,6 +1,4 @@ import { FC } from 'react'; -import BooleanControl, { BooleanControlProps } from './BooleanControl'; +import { BooleanControl, BooleanControlProps } from './BooleanControl'; -const ToggleSwitch: FC = (props) => ; - -export default ToggleSwitch; +export const ToggleSwitch: FC = (props) => ; diff --git a/src/utils/dates/DateRangeRow.tsx b/src/utils/dates/DateRangeRow.tsx index a3a2e917..3712fb86 100644 --- a/src/utils/dates/DateRangeRow.tsx +++ b/src/utils/dates/DateRangeRow.tsx @@ -1,5 +1,5 @@ import { endOfDay } from 'date-fns'; -import DateInput from '../DateInput'; +import { DateInput } from '../DateInput'; import { DateRange } from './types'; interface DateRangeRowProps extends DateRange { @@ -8,7 +8,7 @@ interface DateRangeRowProps extends DateRange { disabled?: boolean; } -const DateRangeRow = ( +export const DateRangeRow = ( { startDate = null, endDate = null, disabled = false, onStartDateChange, onEndDateChange }: DateRangeRowProps, ) => (
@@ -35,5 +35,3 @@ const DateRangeRow = (
); - -export default DateRangeRow; diff --git a/src/utils/dates/DateRangeSelector.tsx b/src/utils/dates/DateRangeSelector.tsx index 08a569b4..1b2a89cd 100644 --- a/src/utils/dates/DateRangeSelector.tsx +++ b/src/utils/dates/DateRangeSelector.tsx @@ -10,7 +10,7 @@ import { rangeIsInterval, dateRangeIsEmpty, } from './types'; -import DateRangeRow from './DateRangeRow'; +import { DateRangeRow } from './DateRangeRow'; import { DateIntervalDropdownItems } from './DateIntervalDropdownItems'; export interface DateRangeSelectorProps { diff --git a/src/utils/forms/InputFormGroup.tsx b/src/utils/forms/InputFormGroup.tsx index 9e047d3a..9dabb6b1 100644 --- a/src/utils/forms/InputFormGroup.tsx +++ b/src/utils/forms/InputFormGroup.tsx @@ -1,6 +1,7 @@ import { FC, PropsWithChildren } from 'react'; import { InputType } from 'reactstrap/types/lib/Input'; import { LabeledFormGroup } from './LabeledFormGroup'; +import { useDomId } from '../helpers/hooks'; export type InputFormGroupProps = PropsWithChildren<{ value: string; @@ -14,15 +15,20 @@ export type InputFormGroupProps = PropsWithChildren<{ export const InputFormGroup: FC = ( { children, value, onChange, type, required, placeholder, className, labelClassName }, -) => ( - {children}:} className={className ?? ''} labelClassName={labelClassName}> - onChange(e.target.value)} - /> - -); +) => { + const id = useDomId(); + + return ( + {children}:} className={className ?? ''} labelClassName={labelClassName} id={id}> + onChange(e.target.value)} + /> + + ); +}; diff --git a/src/utils/forms/LabeledFormGroup.tsx b/src/utils/forms/LabeledFormGroup.tsx index ea11c1ee..dfc68729 100644 --- a/src/utils/forms/LabeledFormGroup.tsx +++ b/src/utils/forms/LabeledFormGroup.tsx @@ -5,14 +5,15 @@ type LabeledFormGroupProps = PropsWithChildren<{ noMargin?: boolean; className?: string; labelClassName?: string; + id?: string; }>; /* eslint-disable jsx-a11y/label-has-associated-control */ export const LabeledFormGroup: FC = ( - { children, label, className = '', labelClassName = '', noMargin = false }, + { children, label, className = '', labelClassName = '', noMargin = false, id }, ) => (
- + {children}
); diff --git a/src/utils/helpers/components.ts b/src/utils/helpers/components.ts new file mode 100644 index 00000000..0abe4bbc --- /dev/null +++ b/src/utils/helpers/components.ts @@ -0,0 +1,5 @@ +import { MutableRefObject, Ref } from 'react'; + +export const mutableRefToElementRef = (ref: MutableRefObject): Ref => (el) => { + ref.current = el ?? undefined; // eslint-disable-line no-param-reassign +}; diff --git a/src/utils/helpers/hooks.ts b/src/utils/helpers/hooks.ts index 90c232d2..877f8412 100644 --- a/src/utils/helpers/hooks.ts +++ b/src/utils/helpers/hooks.ts @@ -6,12 +6,12 @@ import { parseQuery, stringifyQuery } from './query'; const DEFAULT_DELAY = 2000; -export type StateFlagTimeout = (initialValue?: boolean, delay?: number) => [ boolean, () => void ]; +export type TimeoutToggle = (initialValue?: boolean, delay?: number) => [boolean, () => void]; -export const useStateFlagTimeout = ( +export const useTimeoutToggle = ( setTimeout: (callback: Function, timeout: number) => number, clearTimeout: (timer: number) => void, -): StateFlagTimeout => (initialValue = false, delay = DEFAULT_DELAY) => { +): TimeoutToggle => (initialValue = false, delay = DEFAULT_DELAY) => { const [flag, setFlag] = useState(initialValue); const timeout = useRef(undefined); const callback = () => { @@ -31,7 +31,6 @@ type ToggleResult = [ boolean, () => void, () => void, () => void ]; export const useToggle = (initialValue = false): ToggleResult => { const [flag, setFlag] = useState(initialValue); - return [flag, () => setFlag(!flag), () => setFlag(true), () => setFlag(false)]; }; @@ -80,7 +79,6 @@ export const useEffectExceptFirstTime = (callback: EffectCallback, deps: Depende export const useGoBack = () => { const navigate = useNavigate(); - return () => navigate(-1); }; diff --git a/src/utils/services/ColorGenerator.ts b/src/utils/services/ColorGenerator.ts index 75fbb04c..b440ab52 100644 --- a/src/utils/services/ColorGenerator.ts +++ b/src/utils/services/ColorGenerator.ts @@ -1,6 +1,6 @@ import { isNil } from 'ramda'; import { rangeOf } from '../utils'; -import LocalStorage from './LocalStorage'; +import { LocalStorage } from './LocalStorage'; const HEX_COLOR_LENGTH = 6; const HEX_DIGITS = '0123456789ABCDEF'; @@ -15,7 +15,7 @@ const hexColorToRgbArray = (colorHex: string): number[] => // HSP by Darel Rex Finley https://alienryderflex.com/hsp.html const perceivedLightness = (r = 0, g = 0, b = 0) => round(sqrt(0.299 * r ** 2 + 0.587 * g ** 2 + 0.114 * b ** 2)); -export default class ColorGenerator { +export class ColorGenerator { private readonly colors: Record; private readonly lights: Record; diff --git a/src/utils/services/LocalStorage.ts b/src/utils/services/LocalStorage.ts index 914a5a2c..9c404e07 100644 --- a/src/utils/services/LocalStorage.ts +++ b/src/utils/services/LocalStorage.ts @@ -1,7 +1,7 @@ const PREFIX = 'shlink'; const buildPath = (path: string) => `${PREFIX}.${path}`; -export default class LocalStorage { +export class LocalStorage { public constructor(private readonly localStorage: Storage) {} public readonly get = (key: string): T | undefined => { diff --git a/src/utils/services/provideServices.ts b/src/utils/services/provideServices.ts index f9fdce0e..53037c02 100644 --- a/src/utils/services/provideServices.ts +++ b/src/utils/services/provideServices.ts @@ -1,7 +1,7 @@ import Bottle from 'bottlejs'; -import { useStateFlagTimeout } from '../helpers/hooks'; -import LocalStorage from './LocalStorage'; -import ColorGenerator from './ColorGenerator'; +import { useTimeoutToggle } from '../helpers/hooks'; +import { LocalStorage } from './LocalStorage'; +import { ColorGenerator } from './ColorGenerator'; import { csvToJson, jsonToCsv } from '../helpers/csvjson'; const provideServices = (bottle: Bottle) => { @@ -14,7 +14,7 @@ const provideServices = (bottle: Bottle) => { bottle.constant('setTimeout', global.setTimeout); bottle.constant('clearTimeout', global.clearTimeout); - bottle.serviceFactory('useStateFlagTimeout', useStateFlagTimeout, 'setTimeout', 'clearTimeout'); + bottle.serviceFactory('useTimeoutToggle', useTimeoutToggle, 'setTimeout', 'clearTimeout'); }; export default provideServices; diff --git a/src/visits/DomainVisits.tsx b/src/visits/DomainVisits.tsx index b0bdca5f..b759f167 100644 --- a/src/visits/DomainVisits.tsx +++ b/src/visits/DomainVisits.tsx @@ -8,8 +8,8 @@ import { Topics } from '../mercure/helpers/Topics'; import { useGoBack } from '../utils/helpers/hooks'; import { toApiParams } from './types/helpers'; import { NormalizedVisit } from './types'; -import VisitsStats from './VisitsStats'; -import VisitsHeader from './VisitsHeader'; +import { VisitsStats } from './VisitsStats'; +import { VisitsHeader } from './VisitsHeader'; export interface DomainVisitsProps extends CommonVisitsProps { getDomainVisits: (domain: string, query?: ShlinkVisitsParams, doIntervalFallback?: boolean) => void; diff --git a/src/visits/NonOrphanVisits.tsx b/src/visits/NonOrphanVisits.tsx index 2ebfe913..6113cfd6 100644 --- a/src/visits/NonOrphanVisits.tsx +++ b/src/visits/NonOrphanVisits.tsx @@ -3,11 +3,11 @@ import { ShlinkVisitsParams } from '../api/types'; import { Topics } from '../mercure/helpers/Topics'; import { useGoBack } from '../utils/helpers/hooks'; import { ReportExporter } from '../common/services/ReportExporter'; -import VisitsStats from './VisitsStats'; +import { VisitsStats } from './VisitsStats'; import { NormalizedVisit, VisitsInfo, VisitsParams } from './types'; import { CommonVisitsProps } from './types/CommonVisitsProps'; import { toApiParams } from './types/helpers'; -import VisitsHeader from './VisitsHeader'; +import { VisitsHeader } from './VisitsHeader'; export interface NonOrphanVisitsProps extends CommonVisitsProps { getNonOrphanVisits: (params?: ShlinkVisitsParams, doIntervalFallback?: boolean) => void; diff --git a/src/visits/OrphanVisits.tsx b/src/visits/OrphanVisits.tsx index bc2ee1d1..dbd3d8ba 100644 --- a/src/visits/OrphanVisits.tsx +++ b/src/visits/OrphanVisits.tsx @@ -3,11 +3,11 @@ import { ShlinkVisitsParams } from '../api/types'; import { Topics } from '../mercure/helpers/Topics'; import { useGoBack } from '../utils/helpers/hooks'; import { ReportExporter } from '../common/services/ReportExporter'; -import VisitsStats from './VisitsStats'; +import { VisitsStats } from './VisitsStats'; import { NormalizedVisit, OrphanVisitType, VisitsInfo, VisitsParams } from './types'; import { CommonVisitsProps } from './types/CommonVisitsProps'; import { toApiParams } from './types/helpers'; -import VisitsHeader from './VisitsHeader'; +import { VisitsHeader } from './VisitsHeader'; export interface OrphanVisitsProps extends CommonVisitsProps { getOrphanVisits: ( diff --git a/src/visits/ShortUrlVisits.tsx b/src/visits/ShortUrlVisits.tsx index 55323aec..99bed7d3 100644 --- a/src/visits/ShortUrlVisits.tsx +++ b/src/visits/ShortUrlVisits.tsx @@ -8,11 +8,12 @@ import { ShortUrlDetail } from '../short-urls/reducers/shortUrlDetail'; import { useGoBack } from '../utils/helpers/hooks'; import { ReportExporter } from '../common/services/ReportExporter'; import { ShortUrlVisits as ShortUrlVisitsState } from './reducers/shortUrlVisits'; -import ShortUrlVisitsHeader from './ShortUrlVisitsHeader'; -import VisitsStats from './VisitsStats'; +import { ShortUrlVisitsHeader } from './ShortUrlVisitsHeader'; +import { VisitsStats } from './VisitsStats'; import { NormalizedVisit, VisitsParams } from './types'; import { CommonVisitsProps } from './types/CommonVisitsProps'; import { toApiParams } from './types/helpers'; +import { urlDecodeShortCode } from '../short-urls/helpers'; export interface ShortUrlVisitsProps extends CommonVisitsProps { getShortUrlVisits: (shortCode: string, query?: ShlinkVisitsParams, doIntervalFallback?: boolean) => void; @@ -22,7 +23,7 @@ export interface ShortUrlVisitsProps extends CommonVisitsProps { cancelGetShortUrlVisits: () => void; } -const ShortUrlVisits = ({ exportVisits }: ReportExporter) => boundToMercureHub(({ +export const ShortUrlVisits = ({ exportVisits }: ReportExporter) => boundToMercureHub(({ shortUrlVisits, shortUrlDetail, getShortUrlVisits, @@ -36,14 +37,14 @@ const ShortUrlVisits = ({ exportVisits }: ReportExporter) => boundToMercureHub(( const goBack = useGoBack(); const { domain } = parseQuery<{ domain?: string }>(search); const loadVisits = (params: VisitsParams, doIntervalFallback?: boolean) => - getShortUrlVisits(shortCode, { ...toApiParams(params), domain }, doIntervalFallback); + getShortUrlVisits(urlDecodeShortCode(shortCode), { ...toApiParams(params), domain }, doIntervalFallback); const exportCsv = (visits: NormalizedVisit[]) => exportVisits( `short-url_${shortUrlDetail.shortUrl?.shortUrl.replace(/https?:\/\//g, '')}_visits.csv`, visits, ); useEffect(() => { - getShortUrlDetail(shortCode, domain); + getShortUrlDetail(urlDecodeShortCode(shortCode), domain); }, []); return ( @@ -59,6 +60,4 @@ const ShortUrlVisits = ({ exportVisits }: ReportExporter) => boundToMercureHub(( ); -}, (_, params) => [Topics.shortUrlVisits(params.shortCode)]); - -export default ShortUrlVisits; +}, (_, params) => (params.shortCode ? [Topics.shortUrlVisits(urlDecodeShortCode(params.shortCode))] : [])); diff --git a/src/visits/ShortUrlVisitsHeader.tsx b/src/visits/ShortUrlVisitsHeader.tsx index f7d0bf5a..96046d9f 100644 --- a/src/visits/ShortUrlVisitsHeader.tsx +++ b/src/visits/ShortUrlVisitsHeader.tsx @@ -3,7 +3,7 @@ import { ExternalLink } from 'react-external-link'; import { ShortUrlDetail } from '../short-urls/reducers/shortUrlDetail'; import { Time } from '../utils/Time'; import { ShortUrlVisits } from './reducers/shortUrlVisits'; -import VisitsHeader from './VisitsHeader'; +import { VisitsHeader } from './VisitsHeader'; import './ShortUrlVisitsHeader.scss'; interface ShortUrlVisitsHeaderProps { @@ -12,7 +12,7 @@ interface ShortUrlVisitsHeaderProps { goBack: () => void; } -const ShortUrlVisitsHeader = ({ shortUrlDetail, shortUrlVisits, goBack }: ShortUrlVisitsHeaderProps) => { +export const ShortUrlVisitsHeader = ({ shortUrlDetail, shortUrlVisits, goBack }: ShortUrlVisitsHeaderProps) => { const { shortUrl, loading } = shortUrlDetail; const { visits } = shortUrlVisits; const shortLink = shortUrl?.shortUrl ?? ''; @@ -35,7 +35,7 @@ const ShortUrlVisitsHeader = ({ shortUrlDetail, shortUrlVisits, goBack }: ShortU
Created: {renderDate()}
-
+
{`${title ? 'Title' : 'Long URL'}: `} {loading && Loading...} {!loading && {title ?? longLink}} @@ -43,5 +43,3 @@ const ShortUrlVisitsHeader = ({ shortUrlDetail, shortUrlVisits, goBack }: ShortU ); }; - -export default ShortUrlVisitsHeader; diff --git a/src/visits/TagVisits.tsx b/src/visits/TagVisits.tsx index 74018ec5..a7ce5ba1 100644 --- a/src/visits/TagVisits.tsx +++ b/src/visits/TagVisits.tsx @@ -1,13 +1,13 @@ import { useParams } from 'react-router-dom'; import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; -import ColorGenerator from '../utils/services/ColorGenerator'; +import { ColorGenerator } from '../utils/services/ColorGenerator'; import { ShlinkVisitsParams } from '../api/types'; import { Topics } from '../mercure/helpers/Topics'; import { useGoBack } from '../utils/helpers/hooks'; import { ReportExporter } from '../common/services/ReportExporter'; import { TagVisits as TagVisitsState } from './reducers/tagVisits'; -import TagVisitsHeader from './TagVisitsHeader'; -import VisitsStats from './VisitsStats'; +import { TagVisitsHeader } from './TagVisitsHeader'; +import { VisitsStats } from './VisitsStats'; import { NormalizedVisit } from './types'; import { CommonVisitsProps } from './types/CommonVisitsProps'; import { toApiParams } from './types/helpers'; @@ -18,7 +18,7 @@ export interface TagVisitsProps extends CommonVisitsProps { cancelGetTagVisits: () => void; } -const TagVisits = (colorGenerator: ColorGenerator, { exportVisits }: ReportExporter) => boundToMercureHub(({ +export const TagVisits = (colorGenerator: ColorGenerator, { exportVisits }: ReportExporter) => boundToMercureHub(({ getTagVisits, tagVisits, cancelGetTagVisits, @@ -44,5 +44,3 @@ const TagVisits = (colorGenerator: ColorGenerator, { exportVisits }: ReportExpor ); }, () => [Topics.visits]); - -export default TagVisits; diff --git a/src/visits/TagVisitsHeader.tsx b/src/visits/TagVisitsHeader.tsx index 88536a72..0de9df7c 100644 --- a/src/visits/TagVisitsHeader.tsx +++ b/src/visits/TagVisitsHeader.tsx @@ -1,6 +1,6 @@ -import Tag from '../tags/helpers/Tag'; -import ColorGenerator from '../utils/services/ColorGenerator'; -import VisitsHeader from './VisitsHeader'; +import { Tag } from '../tags/helpers/Tag'; +import { ColorGenerator } from '../utils/services/ColorGenerator'; +import { VisitsHeader } from './VisitsHeader'; import { TagVisits } from './reducers/tagVisits'; import './ShortUrlVisitsHeader.scss'; @@ -10,9 +10,8 @@ interface TagVisitsHeaderProps { colorGenerator: ColorGenerator; } -const TagVisitsHeader = ({ tagVisits, goBack, colorGenerator }: TagVisitsHeaderProps) => { +export const TagVisitsHeader = ({ tagVisits, goBack, colorGenerator }: TagVisitsHeaderProps) => { const { visits, tag } = tagVisits; - const visitsStatsTitle = ( Visits for @@ -22,5 +21,3 @@ const TagVisitsHeader = ({ tagVisits, goBack, colorGenerator }: TagVisitsHeaderP return ; }; - -export default TagVisitsHeader; diff --git a/src/visits/VisitsHeader.tsx b/src/visits/VisitsHeader.tsx index 85398a6b..7dca6459 100644 --- a/src/visits/VisitsHeader.tsx +++ b/src/visits/VisitsHeader.tsx @@ -2,7 +2,7 @@ import { Button, Card } from 'reactstrap'; import { FC, PropsWithChildren, ReactNode } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faArrowLeft } from '@fortawesome/free-solid-svg-icons'; -import ShortUrlVisitsCount from '../short-urls/helpers/ShortUrlVisitsCount'; +import { ShortUrlVisitsCount } from '../short-urls/helpers/ShortUrlVisitsCount'; import { ShortUrl } from '../short-urls/data'; import { Visit } from './types'; @@ -13,7 +13,7 @@ type VisitsHeaderProps = PropsWithChildren<{ shortUrl?: ShortUrl; }>; -const VisitsHeader: FC = ({ visits, goBack, shortUrl, children, title }) => ( +export const VisitsHeader: FC = ({ visits, goBack, shortUrl, children, title }) => (

@@ -36,5 +36,3 @@ const VisitsHeader: FC = ({ visits, goBack, shortUrl, childre

); - -export default VisitsHeader; diff --git a/src/visits/VisitsStats.tsx b/src/visits/VisitsStats.tsx index 59885d5f..1980ded8 100644 --- a/src/visits/VisitsStats.tsx +++ b/src/visits/VisitsStats.tsx @@ -7,7 +7,7 @@ import { IconDefinition } from '@fortawesome/fontawesome-common-types'; import { Route, Routes, Navigate } from 'react-router-dom'; import classNames from 'classnames'; import { DateRangeSelector } from '../utils/dates/DateRangeSelector'; -import Message from '../utils/Message'; +import { Message } from '../utils/Message'; import { DateInterval, DateRange, intervalToDateRange } from '../utils/dates/types'; import { Result } from '../utils/Result'; import { ShlinkApiError } from '../api/ShlinkApiError'; @@ -17,10 +17,10 @@ import { supportsBotVisits } from '../utils/helpers/features'; import { prettify } from '../utils/helpers/numbers'; import { NavPillItem, NavPills } from '../utils/NavPills'; import { ExportBtn } from '../utils/ExportBtn'; -import LineChartCard from './charts/LineChartCard'; -import VisitsTable from './VisitsTable'; +import { LineChartCard } from './charts/LineChartCard'; +import { VisitsTable } from './VisitsTable'; import { NormalizedOrphanVisit, NormalizedVisit, VisitsFilter, VisitsInfo, VisitsParams } from './types'; -import OpenMapModalBtn from './helpers/OpenMapModalBtn'; +import { OpenMapModalBtn } from './helpers/OpenMapModalBtn'; import { normalizeVisits, processStatsFromVisits } from './services/VisitsParser'; import { VisitsFilterDropdown } from './helpers/VisitsFilterDropdown'; import { HighlightableProps, highlightedVisitsToStats } from './types/helpers'; @@ -55,7 +55,7 @@ const sections: Record = { let selectedBar: string | undefined; -const VisitsStats: FC = ({ +export const VisitsStats: FC = ({ children, visitsInfo, getVisits, @@ -139,7 +139,7 @@ const VisitsStats: FC = ({ } if (isEmpty(visits)) { - return There are no visits matching current filter :(; + return There are no visits matching current filter; } return ( @@ -235,7 +235,7 @@ const VisitsStats: FC = ({ stats={cities} highlightedStats={highlightedVisitsToStats(highlightedVisits, 'city')} highlightedLabel={highlightedLabel} - extraHeaderContent={(activeCities: string[]) => mapLocations.length > 0 && ( + extraHeaderContent={(activeCities) => mapLocations.length > 0 && ( )} sortingItems={{ @@ -325,5 +325,3 @@ const VisitsStats: FC = ({ ); }; - -export default VisitsStats; diff --git a/src/visits/VisitsTable.tsx b/src/visits/VisitsTable.tsx index 28ca9c61..39e30c48 100644 --- a/src/visits/VisitsTable.tsx +++ b/src/visits/VisitsTable.tsx @@ -45,7 +45,7 @@ const calculateVisits = (allVisits: NormalizedVisit[], searchTerm: string | unde return { visitsGroups, total }; }; -const VisitsTable = ({ +export const VisitsTable = ({ visits, selectedVisits = [], setSelectedVisits, @@ -222,5 +222,3 @@ const VisitsTable = ({
); }; - -export default VisitsTable; diff --git a/src/visits/charts/ChartCard.tsx b/src/visits/charts/ChartCard.tsx index c68da31a..bb1e8253 100644 --- a/src/visits/charts/ChartCard.tsx +++ b/src/visits/charts/ChartCard.tsx @@ -8,7 +8,7 @@ type ChartCardProps = PropsWithChildren<{ }>; export const ChartCard: FC = ({ title, footer, children }) => ( - + {typeof title === 'function' ? title() : title} {children} {footer && {footer}} diff --git a/src/visits/charts/LineChartCard.tsx b/src/visits/charts/LineChartCard.tsx index 335dd742..a6e4b4ba 100644 --- a/src/visits/charts/LineChartCard.tsx +++ b/src/visits/charts/LineChartCard.tsx @@ -26,7 +26,7 @@ import { NormalizedVisit, Stats } from '../types'; import { fillTheGaps } from '../../utils/helpers/visits'; import { useToggle } from '../../utils/helpers/hooks'; import { rangeOf } from '../../utils/utils'; -import ToggleSwitch from '../../utils/ToggleSwitch'; +import { ToggleSwitch } from '../../utils/ToggleSwitch'; import { prettify } from '../../utils/helpers/numbers'; import { pointerOnHover, renderChartLabel } from '../../utils/helpers/charts'; import { HIGHLIGHTED_COLOR, MAIN_COLOR } from '../../utils/theme'; @@ -166,7 +166,7 @@ const chartElementAtEvent = ( } }; -const LineChartCard = ( +export const LineChartCard = ( { title, visits, highlightedVisits, highlightedLabel = 'Selected', setSelectedVisits }: LineChartCardProps, ) => { const [step, setStep] = useState( @@ -235,7 +235,7 @@ const LineChartCard = ( return ( - + {title}
@@ -266,5 +266,3 @@ const LineChartCard = ( ); }; - -export default LineChartCard; diff --git a/src/visits/charts/SortableBarChartCard.tsx b/src/visits/charts/SortableBarChartCard.tsx index 23790ec3..8d43bdb3 100644 --- a/src/visits/charts/SortableBarChartCard.tsx +++ b/src/visits/charts/SortableBarChartCard.tsx @@ -1,11 +1,11 @@ -import { FC, useState } from 'react'; +import { FC, ReactNode, useState } from 'react'; import { fromPairs, pipe, reverse, sortBy, splitEvery, toLower, toPairs, type, zipObj } from 'ramda'; import { rangeOf } from '../../utils/utils'; import { Order } from '../../utils/helpers/ordering'; import { SimplePaginator } from '../../common/SimplePaginator'; import { roundTen } from '../../utils/helpers/numbers'; import { OrderingDropdown } from '../../utils/OrderingDropdown'; -import PaginationDropdown from '../../utils/PaginationDropdown'; +import { PaginationDropdown } from '../../utils/PaginationDropdown'; import { Stats, StatsRow } from '../types'; import { HorizontalBarChart, HorizontalBarChartProps } from './HorizontalBarChart'; import { ChartCard } from './ChartCard'; @@ -14,7 +14,7 @@ interface SortableBarChartCardProps extends Omit title: Function | string; sortingItems: Record; withPagination?: boolean; - extraHeaderContent?: Function; + extraHeaderContent?: (activeCities?: string[]) => ReactNode; } const toLowerIfString = (value: any) => (type(value) === 'String' ? toLower(value) : value); diff --git a/src/visits/helpers/MapModal.tsx b/src/visits/helpers/MapModal.tsx index b672b1af..27296ff9 100644 --- a/src/visits/helpers/MapModal.tsx +++ b/src/visits/helpers/MapModal.tsx @@ -29,18 +29,18 @@ const calculateMapProps = (locations: CityStats[]): MapContainerProps => { } // When there's only one location, an error is thrown if trying to calculate the bounds. - // When that happens, we use zoom and center as a workaround + // When that happens, we use "zoom" and "center" as a workaround const [{ latLong: center }] = locations; return { zoom: 10, center }; }; -const MapModal = ({ toggle, isOpen, title, locations = [] }: MapModalProps) => ( +export const MapModal = ({ toggle, isOpen, title, locations = [] }: MapModalProps) => (

{title} -

@@ -53,5 +53,3 @@ const MapModal = ({ toggle, isOpen, title, locations = [] }: MapModalProps) => (
); - -export default MapModal; diff --git a/src/visits/helpers/OpenMapModalBtn.tsx b/src/visits/helpers/OpenMapModalBtn.tsx index 1afc8583..9d3032e7 100644 --- a/src/visits/helpers/OpenMapModalBtn.tsx +++ b/src/visits/helpers/OpenMapModalBtn.tsx @@ -4,22 +4,24 @@ import { faMapMarkedAlt as mapIcon } from '@fortawesome/free-solid-svg-icons'; import { Button, Dropdown, DropdownItem, DropdownMenu, UncontrolledTooltip } from 'reactstrap'; import { useDomId, useToggle } from '../../utils/helpers/hooks'; import { CityStats } from '../types'; -import MapModal from './MapModal'; +import { MapModal } from './MapModal'; import './OpenMapModalBtn.scss'; interface OpenMapModalBtnProps { modalTitle: string; - activeCities: string[]; + activeCities?: string[]; locations?: CityStats[]; } -const OpenMapModalBtn = ({ modalTitle, activeCities, locations = [] }: OpenMapModalBtnProps) => { +export const OpenMapModalBtn = ({ modalTitle, activeCities, locations = [] }: OpenMapModalBtnProps) => { const [mapIsOpened, , openMap, closeMap] = useToggle(); const [dropdownIsOpened, toggleDropdown, openDropdown] = useToggle(); const [locationsToShow, setLocationsToShow] = useState([]); const id = useDomId(); - const filterLocations = (cities: CityStats[]) => cities.filter(({ cityName }) => activeCities.includes(cityName)); + const filterLocations = (cities: CityStats[]) => ( + !activeCities ? cities : cities.filter(({ cityName }) => activeCities?.includes(cityName)) + ); const onClick = () => { if (!activeCities) { setLocationsToShow(locations); @@ -51,5 +53,3 @@ const OpenMapModalBtn = ({ modalTitle, activeCities, locations = [] }: OpenMapMo ); }; - -export default OpenMapModalBtn; diff --git a/src/visits/services/provideServices.ts b/src/visits/services/provideServices.ts index efc9d620..a6b0d931 100644 --- a/src/visits/services/provideServices.ts +++ b/src/visits/services/provideServices.ts @@ -1,8 +1,8 @@ import Bottle from 'bottlejs'; -import MapModal from '../helpers/MapModal'; +import { MapModal } from '../helpers/MapModal'; import { createNewVisits } from '../reducers/visitCreation'; -import ShortUrlVisits from '../ShortUrlVisits'; -import TagVisits from '../TagVisits'; +import { ShortUrlVisits } from '../ShortUrlVisits'; +import { TagVisits } from '../TagVisits'; import { OrphanVisits } from '../OrphanVisits'; import { NonOrphanVisits } from '../NonOrphanVisits'; import { cancelGetShortUrlVisits, getShortUrlVisits } from '../reducers/shortUrlVisits'; diff --git a/test/__helpers__/setUpTest.ts b/test/__helpers__/setUpTest.ts new file mode 100644 index 00000000..210a88ff --- /dev/null +++ b/test/__helpers__/setUpTest.ts @@ -0,0 +1,16 @@ +import { ReactElement } from 'react'; +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +export const setUpCanvas = (element: ReactElement) => { + const result = render(element); + const { container } = result; + const getEvents = () => container.querySelector('canvas')?.getContext('2d')?.__getEvents(); // eslint-disable-line no-underscore-dangle + + return { ...result, events: getEvents(), getEvents }; +}; + +export const renderWithEvents = (element: ReactElement) => ({ + user: userEvent.setup(), + ...render(element), +}); diff --git a/test/mocks/WindowMock.ts b/test/__mocks__/Window.mock.ts similarity index 100% rename from test/mocks/WindowMock.ts rename to test/__mocks__/Window.mock.ts diff --git a/test/__mocks__/setUpCanvas.ts b/test/__mocks__/setUpCanvas.ts deleted file mode 100644 index 9308a25d..00000000 --- a/test/__mocks__/setUpCanvas.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ReactElement } from 'react'; -import { render } from '@testing-library/react'; - -export const setUpCanvas = (element: ReactElement) => { - const result = render(element); - const { container } = result; - const events = container.querySelector('canvas')?.getContext('2d')?.__getEvents(); // eslint-disable-line no-underscore-dangle - - return { ...result, events }; -}; diff --git a/test/api/services/ShlinkApiClient.test.ts b/test/api/services/ShlinkApiClient.test.ts index 91153fb4..f5f7b647 100644 --- a/test/api/services/ShlinkApiClient.test.ts +++ b/test/api/services/ShlinkApiClient.test.ts @@ -1,6 +1,6 @@ import { AxiosInstance, AxiosRequestConfig } from 'axios'; import { Mock } from 'ts-mockery'; -import ShlinkApiClient from '../../../src/api/services/ShlinkApiClient'; +import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; import { OptionalString } from '../../../src/utils/utils'; import { ShlinkDomain, ShlinkVisitsOverview } from '../../../src/api/types'; import { ShortUrl, ShortUrlsOrder } from '../../../src/short-urls/data'; diff --git a/test/api/services/ShlinkApiClientBuilder.test.ts b/test/api/services/ShlinkApiClientBuilder.test.ts index 6967b0f4..b67bd7e6 100644 --- a/test/api/services/ShlinkApiClientBuilder.test.ts +++ b/test/api/services/ShlinkApiClientBuilder.test.ts @@ -1,6 +1,6 @@ import { Mock } from 'ts-mockery'; import { AxiosInstance } from 'axios'; -import buildShlinkApiClient from '../../../src/api/services/ShlinkApiClientBuilder'; +import { buildShlinkApiClient } from '../../../src/api/services/ShlinkApiClientBuilder'; import { ReachableServer, SelectedServer } from '../../../src/servers/data'; import { ShlinkState } from '../../../src/container/types'; diff --git a/test/common/AppUpdateBanner.test.tsx b/test/common/AppUpdateBanner.test.tsx index 5e9d8d83..4dfcd207 100644 --- a/test/common/AppUpdateBanner.test.tsx +++ b/test/common/AppUpdateBanner.test.tsx @@ -1,14 +1,11 @@ -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { screen } from '@testing-library/react'; import { AppUpdateBanner } from '../../src/common/AppUpdateBanner'; +import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { const toggle = jest.fn(); const forceUpdate = jest.fn(); - const setUp = () => ({ - user: userEvent.setup(), - ...render(), - }); + const setUp = () => renderWithEvents(); afterEach(jest.clearAllMocks); diff --git a/test/common/AsideMenu.test.tsx b/test/common/AsideMenu.test.tsx index 6ebc0a31..df49244e 100644 --- a/test/common/AsideMenu.test.tsx +++ b/test/common/AsideMenu.test.tsx @@ -1,12 +1,12 @@ import { render, screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; import { MemoryRouter } from 'react-router-dom'; -import asideMenuCreator from '../../src/common/AsideMenu'; +import { AsideMenu as createAsideMenu } from '../../src/common/AsideMenu'; import { ReachableServer } from '../../src/servers/data'; import { SemVer } from '../../src/utils/helpers/version'; describe('', () => { - const AsideMenu = asideMenuCreator(() => <>DeleteServerButton); + const AsideMenu = createAsideMenu(() => <>DeleteServerButton); const setUp = (version: SemVer, id: string | false = 'abc123') => render( ({ id: id || undefined, version })} /> diff --git a/test/common/ErrorHandler.test.tsx b/test/common/ErrorHandler.test.tsx index e9afd6e3..67d271c4 100644 --- a/test/common/ErrorHandler.test.tsx +++ b/test/common/ErrorHandler.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; import { Mock } from 'ts-mockery'; import { ErrorHandler as createErrorHandler } from '../../src/common/ErrorHandler'; +import { renderWithEvents } from '../__helpers__/setUpTest'; const ComponentWithError = () => { throw new Error('Error!!'); @@ -36,8 +36,7 @@ describe('', () => { }); it('reloads page on button click', async () => { - const user = userEvent.setup(); - render(} />); + const { user } = renderWithEvents(} />); expect(reload).not.toHaveBeenCalled(); await user.click(screen.getByRole('button')); diff --git a/test/common/MainHeader.test.tsx b/test/common/MainHeader.test.tsx index 6aa280a8..e3187c7e 100644 --- a/test/common/MainHeader.test.tsx +++ b/test/common/MainHeader.test.tsx @@ -1,8 +1,8 @@ -import { render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { screen, waitFor } from '@testing-library/react'; import { Router } from 'react-router-dom'; import { createMemoryHistory } from 'history'; import { MainHeader as createMainHeader } from '../../src/common/MainHeader'; +import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { const MainHeader = createMainHeader(() => <>ServersDropdown); @@ -10,14 +10,11 @@ describe('', () => { const history = createMemoryHistory(); history.push(pathname); - const user = userEvent.setup(); - const renderResult = render( + return renderWithEvents( , ); - - return { user, ...renderResult }; }; it('renders ServersDropdown', () => { diff --git a/test/common/ShlinkVersions.test.tsx b/test/common/ShlinkVersions.test.tsx index 04273b32..7c6127f1 100644 --- a/test/common/ShlinkVersions.test.tsx +++ b/test/common/ShlinkVersions.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; -import ShlinkVersions, { ShlinkVersionsProps } from '../../src/common/ShlinkVersions'; +import { ShlinkVersions, ShlinkVersionsProps } from '../../src/common/ShlinkVersions'; import { NonReachableServer, NotFoundServer, ReachableServer } from '../../src/servers/data'; describe('', () => { diff --git a/test/common/ShlinkVersionsContainer.test.tsx b/test/common/ShlinkVersionsContainer.test.tsx index d264051d..1920fdc2 100644 --- a/test/common/ShlinkVersionsContainer.test.tsx +++ b/test/common/ShlinkVersionsContainer.test.tsx @@ -1,6 +1,6 @@ import { render } from '@testing-library/react'; import { Mock } from 'ts-mockery'; -import ShlinkVersionsContainer from '../../src/common/ShlinkVersionsContainer'; +import { ShlinkVersionsContainer } from '../../src/common/ShlinkVersionsContainer'; import { SelectedServer } from '../../src/servers/data'; import { Sidebar } from '../../src/common/reducers/sidebar'; diff --git a/test/common/SimplePaginator.test.tsx b/test/common/SimplePaginator.test.tsx index 128221d9..7b2c060a 100644 --- a/test/common/SimplePaginator.test.tsx +++ b/test/common/SimplePaginator.test.tsx @@ -9,7 +9,7 @@ describe('', () => { it.each([-3, -2, 0, 1])('renders empty when the amount of pages is smaller than 2', (pagesCount) => { const { container } = setUp(pagesCount); - expect(container.firstChild).toEqual(null); + expect(container.firstChild).toBeNull(); }); describe('ELLIPSIS are rendered where expected', () => { diff --git a/test/common/services/ImageDownloader.test.ts b/test/common/services/ImageDownloader.test.ts index 9a640bcd..20a381cf 100644 --- a/test/common/services/ImageDownloader.test.ts +++ b/test/common/services/ImageDownloader.test.ts @@ -1,7 +1,7 @@ import { Mock } from 'ts-mockery'; import { AxiosInstance } from 'axios'; import { ImageDownloader } from '../../../src/common/services/ImageDownloader'; -import { windowMock } from '../../mocks/WindowMock'; +import { windowMock } from '../../__mocks__/Window.mock'; describe('ImageDownloader', () => { const get = jest.fn(); diff --git a/test/common/services/ReportExporter.test.ts b/test/common/services/ReportExporter.test.ts index 5fd5f941..02097005 100644 --- a/test/common/services/ReportExporter.test.ts +++ b/test/common/services/ReportExporter.test.ts @@ -1,6 +1,6 @@ import { ReportExporter } from '../../../src/common/services/ReportExporter'; import { NormalizedVisit } from '../../../src/visits/types'; -import { windowMock } from '../../mocks/WindowMock'; +import { windowMock } from '../../__mocks__/Window.mock'; import { ExportableShortUrl } from '../../../src/short-urls/data'; describe('ReportExporter', () => { diff --git a/test/domains/DomainSelector.test.tsx b/test/domains/DomainSelector.test.tsx index c18e650b..e2effdf4 100644 --- a/test/domains/DomainSelector.test.tsx +++ b/test/domains/DomainSelector.test.tsx @@ -1,9 +1,9 @@ -import { render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { screen, waitFor } from '@testing-library/react'; import { Mock } from 'ts-mockery'; import { DomainSelector } from '../../src/domains/DomainSelector'; import { DomainsList } from '../../src/domains/reducers/domainsList'; import { ShlinkDomain } from '../../src/api/types'; +import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { const domainsList = Mock.of({ @@ -13,10 +13,9 @@ describe('', () => { Mock.of({ domain: 'bar.com' }), ], }); - const setUp = (value = '') => ({ - user: userEvent.setup(), - ...render(), - }); + const setUp = (value = '') => renderWithEvents( + , + ); afterEach(jest.clearAllMocks); diff --git a/test/domains/ManageDomains.test.tsx b/test/domains/ManageDomains.test.tsx index 85fc2e80..509ee2c7 100644 --- a/test/domains/ManageDomains.test.tsx +++ b/test/domains/ManageDomains.test.tsx @@ -1,27 +1,24 @@ -import { render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { screen, waitFor } from '@testing-library/react'; import { Mock } from 'ts-mockery'; import { DomainsList } from '../../src/domains/reducers/domainsList'; import { ManageDomains } from '../../src/domains/ManageDomains'; import { ProblemDetailsError, ShlinkDomain } from '../../src/api/types'; import { SelectedServer } from '../../src/servers/data'; +import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { const listDomains = jest.fn(); const filterDomains = jest.fn(); - const setUp = (domainsList: DomainsList) => ({ - user: userEvent.setup(), - ...render( - ()} - />, - ), - }); + const setUp = (domainsList: DomainsList) => renderWithEvents( + ()} + />, + ); afterEach(jest.clearAllMocks); diff --git a/test/domains/helpers/DomainDropdown.test.tsx b/test/domains/helpers/DomainDropdown.test.tsx index 44ccbc58..90b5658b 100644 --- a/test/domains/helpers/DomainDropdown.test.tsx +++ b/test/domains/helpers/DomainDropdown.test.tsx @@ -1,26 +1,23 @@ -import { render, screen, waitForElementToBeRemoved } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { screen, waitForElementToBeRemoved } from '@testing-library/react'; import { Mock } from 'ts-mockery'; import { MemoryRouter } from 'react-router-dom'; import { DomainDropdown } from '../../../src/domains/helpers/DomainDropdown'; import { Domain } from '../../../src/domains/data'; import { ReachableServer, SelectedServer } from '../../../src/servers/data'; import { SemVer } from '../../../src/utils/helpers/version'; +import { renderWithEvents } from '../../__helpers__/setUpTest'; describe('', () => { const editDomainRedirects = jest.fn().mockResolvedValue(undefined); - const setUp = (domain?: Domain, selectedServer?: SelectedServer) => ({ - user: userEvent.setup(), - ...render( - - ()} - selectedServer={selectedServer ?? Mock.all()} - editDomainRedirects={editDomainRedirects} - /> - , - ), - }); + const setUp = (domain?: Domain, selectedServer?: SelectedServer) => renderWithEvents( + + ()} + selectedServer={selectedServer ?? Mock.all()} + editDomainRedirects={editDomainRedirects} + /> + , + ); afterEach(jest.clearAllMocks); diff --git a/test/domains/helpers/DomainStatusIcon.test.tsx b/test/domains/helpers/DomainStatusIcon.test.tsx index 51840c1c..a95ab027 100644 --- a/test/domains/helpers/DomainStatusIcon.test.tsx +++ b/test/domains/helpers/DomainStatusIcon.test.tsx @@ -1,15 +1,14 @@ -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; import { DomainStatus } from '../../../src/domains/data'; import { DomainStatusIcon } from '../../../src/domains/helpers/DomainStatusIcon'; +import { renderWithEvents } from '../../__helpers__/setUpTest'; describe('', () => { const matchMedia = jest.fn().mockReturnValue(Mock.of({ matches: false })); - const setUp = (status: DomainStatus) => ({ - user: userEvent.setup(), - ...render(), - }); + const setUp = (status: DomainStatus) => renderWithEvents( + , + ); beforeEach(jest.clearAllMocks); diff --git a/test/domains/helpers/EditDomainRedirectsModal.test.tsx b/test/domains/helpers/EditDomainRedirectsModal.test.tsx index 3fc7205a..6f402350 100644 --- a/test/domains/helpers/EditDomainRedirectsModal.test.tsx +++ b/test/domains/helpers/EditDomainRedirectsModal.test.tsx @@ -1,8 +1,8 @@ -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { fireEvent, screen, waitFor } from '@testing-library/react'; import { Mock } from 'ts-mockery'; import { ShlinkDomain } from '../../../src/api/types'; import { EditDomainRedirectsModal } from '../../../src/domains/helpers/EditDomainRedirectsModal'; +import { renderWithEvents } from '../../__helpers__/setUpTest'; describe('', () => { const editDomainRedirects = jest.fn().mockResolvedValue(undefined); @@ -13,12 +13,9 @@ describe('', () => { baseUrlRedirect: 'baz', }, }); - const setUp = () => ({ - user: userEvent.setup(), - ...render( - , - ), - }); + const setUp = () => renderWithEvents( + , + ); afterEach(jest.clearAllMocks); diff --git a/test/domains/reducers/domainRedirects.test.ts b/test/domains/reducers/domainRedirects.test.ts index 3cb56393..f77f8377 100644 --- a/test/domains/reducers/domainRedirects.test.ts +++ b/test/domains/reducers/domainRedirects.test.ts @@ -1,5 +1,5 @@ import { Mock } from 'ts-mockery'; -import ShlinkApiClient from '../../../src/api/services/ShlinkApiClient'; +import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; import { EDIT_DOMAIN_REDIRECTS, EDIT_DOMAIN_REDIRECTS_ERROR, diff --git a/test/domains/reducers/domainsList.test.ts b/test/domains/reducers/domainsList.test.ts index 5c1730f5..b00704bd 100644 --- a/test/domains/reducers/domainsList.test.ts +++ b/test/domains/reducers/domainsList.test.ts @@ -15,7 +15,7 @@ import reducer, { } from '../../../src/domains/reducers/domainsList'; import { EDIT_DOMAIN_REDIRECTS } from '../../../src/domains/reducers/domainRedirects'; import { ShlinkDomainRedirects } from '../../../src/api/types'; -import ShlinkApiClient from '../../../src/api/services/ShlinkApiClient'; +import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; import { Domain } from '../../../src/domains/data'; import { ShlinkState } from '../../../src/container/types'; import { SelectedServer, ServerData } from '../../../src/servers/data'; @@ -28,7 +28,7 @@ describe('domainsListReducer', () => { const buildShlinkApiClient = () => Mock.of({ listDomains, health }); const filteredDomains = [ Mock.of({ domain: 'foo', status: 'validating' }), - Mock.of({ domain: 'boo', status: 'validating' }), + Mock.of({ domain: 'Boo', status: 'validating' }), ]; const domains = [...filteredDomains, Mock.of({ domain: 'bar', status: 'validating' })]; @@ -58,7 +58,7 @@ describe('domainsListReducer', () => { }); it('filters domains on FILTER_DOMAINS', () => { - expect(reducer(Mock.of({ domains }), action(FILTER_DOMAINS, { searchTerm: 'oo' }))).toEqual( + expect(reducer(Mock.of({ domains }), action(FILTER_DOMAINS, { searchTerm: 'oO' }))).toEqual( { domains, filteredDomains }, ); }); diff --git a/test/mercure/reducers/mercureInfo.test.ts b/test/mercure/reducers/mercureInfo.test.ts index 239a9ba1..d5cb2972 100644 --- a/test/mercure/reducers/mercureInfo.test.ts +++ b/test/mercure/reducers/mercureInfo.test.ts @@ -7,7 +7,7 @@ import reducer, { GetMercureInfoAction, } from '../../../src/mercure/reducers/mercureInfo'; import { ShlinkMercureInfo } from '../../../src/api/types'; -import ShlinkApiClient from '../../../src/api/services/ShlinkApiClient'; +import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; import { GetState } from '../../../src/container/types'; describe('mercureInfoReducer', () => { diff --git a/test/servers/CreateServer.test.tsx b/test/servers/CreateServer.test.tsx index ba9abb4a..dd559dc3 100644 --- a/test/servers/CreateServer.test.tsx +++ b/test/servers/CreateServer.test.tsx @@ -1,83 +1,81 @@ -import { shallow, ShallowWrapper } from 'enzyme'; +import { fireEvent, screen, waitFor } from '@testing-library/react'; import { Mock } from 'ts-mockery'; import { useNavigate } from 'react-router-dom'; -import createServerConstruct from '../../src/servers/CreateServer'; -import { ServerForm } from '../../src/servers/helpers/ServerForm'; +import { CreateServer as createCreateServer } from '../../src/servers/CreateServer'; import { ServerWithId } from '../../src/servers/data'; -import { DuplicatedServersModal } from '../../src/servers/helpers/DuplicatedServersModal'; +import { renderWithEvents } from '../__helpers__/setUpTest'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useNavigate: jest.fn() })); describe('', () => { - let wrapper: ShallowWrapper; - const ImportServersBtn = () => null; const createServerMock = jest.fn(); const navigate = jest.fn(); - const servers = { foo: Mock.all() }; - const createWrapper = (serversImported = false, importFailed = false) => { + const servers = { foo: Mock.of({ url: 'https://existing_url.com', apiKey: 'existing_api_key' }) }; + const setUp = (serversImported = false, importFailed = false) => { (useNavigate as any).mockReturnValue(navigate); - const useStateFlagTimeout = jest.fn() - .mockReturnValueOnce([serversImported, () => '']) - .mockReturnValueOnce([importFailed, () => '']) - .mockReturnValue([]); - const CreateServer = createServerConstruct(ImportServersBtn, useStateFlagTimeout); + let callCount = 0; + const useTimeoutToggle = jest.fn().mockImplementation(() => { + const result = [callCount % 2 === 0 ? serversImported : importFailed, () => null]; + callCount += 1; + return result; + }); + const CreateServer = createCreateServer(() => <>ImportServersBtn, useTimeoutToggle); - wrapper = shallow(); - - return wrapper; + return renderWithEvents(); }; beforeEach(jest.clearAllMocks); - afterEach(() => wrapper?.unmount()); - - it('renders components', () => { - const wrapper = createWrapper(); - - expect(wrapper.find(ServerForm)).toHaveLength(1); - expect(wrapper.find('ImportResult')).toHaveLength(0); - }); it('shows success message when imported is true', () => { - const wrapper = createWrapper(true); - const result = wrapper.find('ImportResult'); + setUp(true); - expect(result).toHaveLength(1); - expect(result.prop('type')).toEqual('success'); + expect(screen.getByText('Servers properly imported. You can now select one from the list :)')).toBeInTheDocument(); + expect( + screen.queryByText('The servers could not be imported. Make sure the format is correct.'), + ).not.toBeInTheDocument(); }); it('shows error message when import failed', () => { - const wrapper = createWrapper(false, true); - const result = wrapper.find('ImportResult'); + setUp(false, true); - expect(result).toHaveLength(1); - expect(result.prop('type')).toEqual('error'); + expect( + screen.queryByText('Servers properly imported. You can now select one from the list :)'), + ).not.toBeInTheDocument(); + expect(screen.getByText('The servers could not be imported. Make sure the format is correct.')).toBeInTheDocument(); }); - it('creates server data when form is submitted', () => { - const wrapper = createWrapper(); - const form = wrapper.find(ServerForm); + it('creates server data when form is submitted', async () => { + const { user } = setUp(); - expect(wrapper.find(DuplicatedServersModal).prop('duplicatedServers')).toEqual([]); - form.simulate('submit', {}); - expect(wrapper.find(DuplicatedServersModal).prop('duplicatedServers')).toEqual([{}]); + expect(createServerMock).not.toHaveBeenCalled(); + + await user.type(screen.getByLabelText(/^Name/), 'the_name'); + await user.type(screen.getByLabelText(/^URL/), 'https://the_url.com'); + await user.type(screen.getByLabelText(/^API key/), 'the_api_key'); + fireEvent.submit(screen.getByRole('form')); + + expect(createServerMock).toHaveBeenCalledWith(expect.objectContaining({ + name: 'the_name', + url: 'https://the_url.com', + apiKey: 'the_api_key', + })); + expect(navigate).toHaveBeenCalledWith(expect.stringMatching(/^\/server\//)); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); }); - it('saves server and redirects on modal save', () => { - const wrapper = createWrapper(); + it('displays dialog when trying to create a duplicated server', async () => { + const { user } = setUp(); - wrapper.find(ServerForm).simulate('submit', {}); - wrapper.find(DuplicatedServersModal).simulate('save'); + await user.type(screen.getByLabelText(/^Name/), 'the_name'); + await user.type(screen.getByLabelText(/^URL/), 'https://existing_url.com'); + await user.type(screen.getByLabelText(/^API key/), 'existing_api_key'); + fireEvent.submit(screen.getByRole('form')); - expect(createServerMock).toHaveBeenCalledTimes(1); - expect(navigate).toHaveBeenCalledTimes(1); - }); - - it('goes back on modal discard', () => { - const wrapper = createWrapper(); - - wrapper.find(DuplicatedServersModal).simulate('discard'); + await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()); + await user.click(screen.getByRole('button', { name: 'Discard' })); + expect(createServerMock).not.toHaveBeenCalled(); expect(navigate).toHaveBeenCalledWith(-1); }); }); diff --git a/test/servers/DeleteServerButton.test.tsx b/test/servers/DeleteServerButton.test.tsx index f66e8a95..c26431e5 100644 --- a/test/servers/DeleteServerButton.test.tsx +++ b/test/servers/DeleteServerButton.test.tsx @@ -1,20 +1,17 @@ import { ReactNode } from 'react'; -import userEvent from '@testing-library/user-event'; -import { render, screen, waitFor } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; import { Mock } from 'ts-mockery'; -import deleteServerButtonConstruct from '../../src/servers/DeleteServerButton'; +import { DeleteServerButton as createDeleteServerButton } from '../../src/servers/DeleteServerButton'; import { ServerWithId } from '../../src/servers/data'; +import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { - const DeleteServerButton = deleteServerButtonConstruct( + const DeleteServerButton = createDeleteServerButton( ({ isOpen }) => <>DeleteServerModal {isOpen ? '[Open]' : '[Closed]'}, ); - const setUp = (children?: ReactNode) => ({ - user: userEvent.setup(), - ...render( - ()} textClassName="button">{children}, - ), - }); + const setUp = (children?: ReactNode) => renderWithEvents( + ()} textClassName="button">{children}, + ); it.each([ ['Foo bar'], diff --git a/test/servers/DeleteServerModal.test.tsx b/test/servers/DeleteServerModal.test.tsx index 83c917c7..cf509b41 100644 --- a/test/servers/DeleteServerModal.test.tsx +++ b/test/servers/DeleteServerModal.test.tsx @@ -1,23 +1,21 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; +import { screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; import { useNavigate } from 'react-router-dom'; -import DeleteServerModal from '../../src/servers/DeleteServerModal'; +import { DeleteServerModal } from '../../src/servers/DeleteServerModal'; import { ServerWithId } from '../../src/servers/data'; +import { renderWithEvents } from '../__helpers__/setUpTest'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useNavigate: jest.fn() })); describe('', () => { - let wrapper: ShallowWrapper; const deleteServerMock = jest.fn(); const navigate = jest.fn(); const toggleMock = jest.fn(); const serverName = 'the_server_name'; - - beforeEach(() => { + const setUp = () => { (useNavigate as any).mockReturnValue(navigate); - wrapper = shallow( + return renderWithEvents( ({ name: serverName })} toggle={toggleMock} @@ -25,39 +23,45 @@ describe('', () => { deleteServer={deleteServerMock} />, ); - }); - afterEach(() => wrapper.unmount()); + }; + afterEach(jest.clearAllMocks); it('renders a modal window', () => { - expect(wrapper.find(Modal)).toHaveLength(1); - expect(wrapper.find(ModalHeader)).toHaveLength(1); - expect(wrapper.find(ModalBody)).toHaveLength(1); - expect(wrapper.find(ModalFooter)).toHaveLength(1); + setUp(); + + expect(screen.getByRole('dialog')).toBeInTheDocument(); + expect(screen.getByRole('heading')).toHaveTextContent('Remove server'); }); it('displays the name of the server as part of the content', () => { - const modalBody = wrapper.find(ModalBody); + setUp(); - expect(modalBody.find('p').first().text()).toEqual( - `Are you sure you want to remove ${serverName}?`, - ); + expect(screen.getByText(/^Are you sure you want to remove/)).toBeInTheDocument(); + expect(screen.getByText(serverName)).toBeInTheDocument(); }); - it('toggles when clicking cancel button', () => { - const cancelBtn = wrapper.find(Button).first(); + it.each([ + [() => screen.getByRole('button', { name: 'Cancel' })], + [() => screen.getByLabelText('Close')], + ])('toggles when clicking cancel button', async (getButton) => { + const { user } = setUp(); - cancelBtn.simulate('click'); + expect(toggleMock).not.toHaveBeenCalled(); + await user.click(getButton()); expect(toggleMock).toHaveBeenCalledTimes(1); expect(deleteServerMock).not.toHaveBeenCalled(); expect(navigate).not.toHaveBeenCalled(); }); - it('deletes server when clicking accept button', () => { - const acceptBtn = wrapper.find(Button).last(); + it('deletes server when clicking accept button', async () => { + const { user } = setUp(); - acceptBtn.simulate('click'); + expect(toggleMock).not.toHaveBeenCalled(); + expect(deleteServerMock).not.toHaveBeenCalled(); + expect(navigate).not.toHaveBeenCalled(); + await user.click(screen.getByRole('button', { name: 'Delete' })); expect(toggleMock).toHaveBeenCalledTimes(1); expect(deleteServerMock).toHaveBeenCalledTimes(1); diff --git a/test/servers/EditServer.test.tsx b/test/servers/EditServer.test.tsx index 106fc5c0..771a6210 100644 --- a/test/servers/EditServer.test.tsx +++ b/test/servers/EditServer.test.tsx @@ -1,9 +1,9 @@ -import { fireEvent, render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { fireEvent, screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; import { useNavigate } from 'react-router-dom'; import { EditServer as editServerConstruct } from '../../src/servers/EditServer'; import { ReachableServer, SelectedServer } from '../../src/servers/data'; +import { renderWithEvents } from '../__helpers__/setUpTest'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useNavigate: jest.fn() })); @@ -18,10 +18,9 @@ describe('', () => { apiKey: 'the_api_key', }); const EditServer = editServerConstruct(ServerError); - const setUp = (selectedServer: SelectedServer = defaultSelectedServer) => ({ - user: userEvent.setup(), - ...render(), - }); + const setUp = (selectedServer: SelectedServer = defaultSelectedServer) => renderWithEvents( + , + ); beforeEach(() => { (useNavigate as any).mockReturnValue(navigate); diff --git a/test/servers/ManageServers.test.tsx b/test/servers/ManageServers.test.tsx index 49292a1e..f34ad465 100644 --- a/test/servers/ManageServers.test.tsx +++ b/test/servers/ManageServers.test.tsx @@ -1,93 +1,102 @@ import { Mock } from 'ts-mockery'; -import { shallow, ShallowWrapper } from 'enzyme'; -import { Button } from 'reactstrap'; +import { screen, waitFor } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; import ServersExporter from '../../src/servers/services/ServersExporter'; import { ManageServers as createManageServers } from '../../src/servers/ManageServers'; import { ServersMap, ServerWithId } from '../../src/servers/data'; -import { SearchField } from '../../src/utils/SearchField'; -import { Result } from '../../src/utils/Result'; +import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { const exportServers = jest.fn(); const serversExporter = Mock.of({ exportServers }); - const ImportServersBtn = () => null; - const ManageServersRow = () => null; - const useStateFlagTimeout = jest.fn().mockReturnValue([false, jest.fn()]); - const ManageServers = createManageServers(serversExporter, ImportServersBtn, useStateFlagTimeout, ManageServersRow); - let wrapper: ShallowWrapper; + const useTimeoutToggle = jest.fn().mockReturnValue([false, jest.fn()]); + const ManageServers = createManageServers( + serversExporter, + () => ImportServersBtn, + useTimeoutToggle, + ({ hasAutoConnect }) => ManageServersRow {hasAutoConnect ? '[YES]' : '[NO]'}, + ); const createServerMock = (value: string, autoConnect = false) => Mock.of( { id: value, name: value, url: value, autoConnect }, ); - const createWrapper = (servers: ServersMap = {}) => { - wrapper = shallow(); - - return wrapper; - }; + const setUp = (servers: ServersMap = {}) => renderWithEvents( + , + ); afterEach(jest.clearAllMocks); - afterEach(() => wrapper?.unmount()); - it('shows search field which allows searching servers, affecting te amount of rendered rows', () => { - const wrapper = createWrapper({ + it('shows search field which allows searching servers, affecting te amount of rendered rows', async () => { + const { user } = setUp({ foo: createServerMock('foo'), bar: createServerMock('bar'), baz: createServerMock('baz'), }); - const searchField = wrapper.find(SearchField); + const search = async (searchTerm: string) => { + await user.clear(screen.getByPlaceholderText('Search...')); + await user.type(screen.getByPlaceholderText('Search...'), searchTerm); + }; - expect(wrapper.find(ManageServersRow)).toHaveLength(3); - expect(wrapper.find('tbody').find('tr')).toHaveLength(0); + expect(screen.getAllByText(/^ManageServersRow/)).toHaveLength(3); + expect(screen.queryByText('No servers found.')).not.toBeInTheDocument(); - searchField.simulate('change', 'foo'); - expect(wrapper.find(ManageServersRow)).toHaveLength(1); - expect(wrapper.find('tbody').find('tr')).toHaveLength(0); + await search('foo'); + await waitFor(() => expect(screen.getAllByText(/^ManageServersRow/)).toHaveLength(1)); + expect(screen.queryByText('No servers found.')).not.toBeInTheDocument(); - searchField.simulate('change', 'ba'); - expect(wrapper.find(ManageServersRow)).toHaveLength(2); - expect(wrapper.find('tbody').find('tr')).toHaveLength(0); + await search('Ba'); + await waitFor(() => expect(screen.getAllByText(/^ManageServersRow/)).toHaveLength(2)); + expect(screen.queryByText('No servers found.')).not.toBeInTheDocument(); - searchField.simulate('change', 'invalid'); - expect(wrapper.find(ManageServersRow)).toHaveLength(0); - expect(wrapper.find('tbody').find('tr')).toHaveLength(1); + await search('invalid'); + await waitFor(() => expect(screen.queryByText(/^ManageServersRow/)).not.toBeInTheDocument()); + expect(screen.getByText('No servers found.')).toBeInTheDocument(); }); it.each([ [createServerMock('foo'), 3], [createServerMock('foo', true), 4], ])('shows different amount of columns if there are at least one auto-connect server', (server, expectedCols) => { - const wrapper = createWrapper({ server }); - const row = wrapper.find(ManageServersRow); + setUp({ server }); - expect(wrapper.find('th')).toHaveLength(expectedCols); - expect(row.prop('hasAutoConnect')).toEqual(server.autoConnect); + expect(screen.getAllByRole('columnheader')).toHaveLength(expectedCols); + if (server.autoConnect) { + expect(screen.getByText(/\[YES\]/)).toBeInTheDocument(); + expect(screen.queryByText(/\[NO\]/)).not.toBeInTheDocument(); + } else { + expect(screen.queryByText(/\[YES\]/)).not.toBeInTheDocument(); + expect(screen.getByText(/\[NO\]/)).toBeInTheDocument(); + } }); it.each([ - [{}, 1], - [{ foo: createServerMock('foo') }, 2], + [{}, 0], + [{ foo: createServerMock('foo') }, 1], ])('shows export button if the list of servers is not empty', (servers, expectedButtons) => { - const wrapper = createWrapper(servers); - const exportBtn = wrapper.find(Button); - - expect(exportBtn).toHaveLength(expectedButtons); + setUp(servers); + expect(screen.queryAllByRole('button', { name: 'Export servers' })).toHaveLength(expectedButtons); }); - it('allows exporting servers when clicking on button', () => { - const wrapper = createWrapper({ foo: createServerMock('foo') }); - const exportBtn = wrapper.find(Button).first(); + it('allows exporting servers when clicking on button', async () => { + const { user } = setUp({ foo: createServerMock('foo') }); expect(exportServers).not.toHaveBeenCalled(); - exportBtn.simulate('click'); + await user.click(screen.getByRole('button', { name: 'Export servers' })); expect(exportServers).toHaveBeenCalled(); }); - it('shows an error message if an error occurs while importing servers', () => { - useStateFlagTimeout.mockReturnValue([true, jest.fn()]); + it.each([[true], [false]])('shows an error message if an error occurs while importing servers', (hasError) => { + useTimeoutToggle.mockReturnValue([hasError, jest.fn()]); - const wrapper = createWrapper({ foo: createServerMock('foo') }); - const result = wrapper.find(Result); + setUp({ foo: createServerMock('foo') }); - expect(result).toHaveLength(1); - expect(result.prop('type')).toEqual('error'); + if (hasError) { + expect( + screen.getByText('The servers could not be imported. Make sure the format is correct.'), + ).toBeInTheDocument(); + } else { + expect( + screen.queryByText('The servers could not be imported. Make sure the format is correct.'), + ).not.toBeInTheDocument(); + } }); }); diff --git a/test/servers/ManageServersRow.test.tsx b/test/servers/ManageServersRow.test.tsx index 2b66b6ab..7c3e24ff 100644 --- a/test/servers/ManageServersRow.test.tsx +++ b/test/servers/ManageServersRow.test.tsx @@ -1,66 +1,58 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import { UncontrolledTooltip } from 'reactstrap'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Link } from 'react-router-dom'; +import { render, screen } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; import { ManageServersRow as createManageServersRow } from '../../src/servers/ManageServersRow'; import { ServerWithId } from '../../src/servers/data'; describe('', () => { - const ManageServersRowDropdown = () => null; - const ManageServersRow = createManageServersRow(ManageServersRowDropdown); + const ManageServersRow = createManageServersRow(() => ManageServersRowDropdown); const server: ServerWithId = { name: 'My server', url: 'https://example.com', apiKey: '123', id: 'abc', }; - let wrapper: ShallowWrapper; - const createWrapper = (hasAutoConnect = false, autoConnect = false) => { - wrapper = shallow(); - - return wrapper; - }; - - afterEach(() => wrapper?.unmount()); + const setUp = (hasAutoConnect = false, autoConnect = false) => render( + + + + + +
+
, + ); it.each([ [true, 4], [false, 3], ])('renders expected amount of columns', (hasAutoConnect, expectedCols) => { - const wrapper = createWrapper(hasAutoConnect); - const td = wrapper.find('td'); - const th = wrapper.find('th'); + setUp(hasAutoConnect); + + const td = screen.getAllByRole('cell'); + const th = screen.getAllByRole('columnheader'); expect(td.length + th.length).toEqual(expectedCols); }); it('renders a dropdown', () => { - const wrapper = createWrapper(); - const dropdown = wrapper.find(ManageServersRowDropdown); - - expect(dropdown).toHaveLength(1); - expect(dropdown.prop('server')).toEqual(expect.objectContaining(server)); + setUp(); + expect(screen.getByText('ManageServersRowDropdown')).toBeInTheDocument(); }); it.each([ - [true, 1], - [false, 0], - ])('renders auto-connect icon only if server is autoConnect', (autoConnect, expectedIcons) => { - const wrapper = createWrapper(true, autoConnect); - const icon = wrapper.find(FontAwesomeIcon); - const iconTooltip = wrapper.find(UncontrolledTooltip); - - expect(icon).toHaveLength(expectedIcons); - expect(iconTooltip).toHaveLength(expectedIcons); + [true], + [false], + ])('renders auto-connect icon only if server is autoConnect', (autoConnect) => { + const { container } = setUp(true, autoConnect); + expect(container).toMatchSnapshot(); }); it('renders server props where appropriate', () => { - const wrapper = createWrapper(); - const link = wrapper.find(Link); - const td = wrapper.find('td').first(); + setUp(); - expect(link.prop('to')).toEqual(`/server/${server.id}`); - expect(link.prop('children')).toEqual(server.name); - expect(td.prop('children')).toEqual(server.url); + const link = screen.getByRole('link'); + + expect(link).toHaveAttribute('href', `/server/${server.id}`); + expect(link).toHaveTextContent(server.name); + expect(screen.getByText(server.url)).toBeInTheDocument(); }); }); diff --git a/test/servers/ManageServersRowDropdown.test.tsx b/test/servers/ManageServersRowDropdown.test.tsx index 95470a2a..043950f0 100644 --- a/test/servers/ManageServersRowDropdown.test.tsx +++ b/test/servers/ManageServersRowDropdown.test.tsx @@ -1,84 +1,57 @@ -import { shallow, ShallowWrapper } from 'enzyme'; +import { screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; -import { DropdownItem } from 'reactstrap'; +import { MemoryRouter } from 'react-router-dom'; import { ServerWithId } from '../../src/servers/data'; import { ManageServersRowDropdown as createManageServersRowDropdown } from '../../src/servers/ManageServersRowDropdown'; +import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { - const DeleteServerModal = () => null; - const ManageServersRowDropdown = createManageServersRowDropdown(DeleteServerModal); + const ManageServersRowDropdown = createManageServersRowDropdown( + ({ isOpen }) => DeleteServerModal {isOpen ? '[OPEN]' : '[CLOSED]'}, + ); const setAutoConnect = jest.fn(); - let wrapper: ShallowWrapper; - const createWrapper = (autoConnect = false) => { + const setUp = (autoConnect = false) => { const server = Mock.of({ id: 'abc123', autoConnect }); - - wrapper = shallow(); - - return wrapper; + return renderWithEvents( + + + , + ); }; afterEach(jest.clearAllMocks); - it('renders expected amount of dropdown items', () => { - const wrapper = createWrapper(); - const items = wrapper.find(DropdownItem); + it('renders expected amount of dropdown items', async () => { + const { user } = setUp(); - expect(items).toHaveLength(5); - expect(items.find('[divider]')).toHaveLength(1); - expect(items.at(0).prop('to')).toEqual('/server/abc123'); - expect(items.at(1).prop('to')).toEqual('/server/abc123/edit'); + expect(screen.queryByRole('menu')).not.toBeInTheDocument(); + await user.click(screen.getByRole('button')); + expect(screen.getByRole('menu')).toBeInTheDocument(); + + expect(screen.getAllByRole('menuitem')).toHaveLength(4); + expect(screen.getByRole('menuitem', { name: 'Connect' })).toHaveAttribute('href', '/server/abc123'); + expect(screen.getByRole('menuitem', { name: 'Edit server' })).toHaveAttribute('href', '/server/abc123/edit'); }); - it('allows toggling auto-connect', () => { - const wrapper = createWrapper(); + it('allows toggling auto-connect', async () => { + const { user } = setUp(); expect(setAutoConnect).not.toHaveBeenCalled(); - wrapper.find(DropdownItem).at(2).simulate('click'); + await user.click(screen.getByRole('button')); + await user.click(screen.getByRole('menuitem', { name: 'Auto-connect' })); expect(setAutoConnect).toHaveBeenCalledWith(expect.objectContaining({ id: 'abc123' }), true); }); - it('renders a modal', () => { - const wrapper = createWrapper(); - const modal = wrapper.find(DeleteServerModal); + it('renders deletion modal', async () => { + const { user } = setUp(); - expect(modal).toHaveLength(1); - expect(modal.prop('redirectHome')).toEqual(false); - expect(modal.prop('server')).toEqual(expect.objectContaining({ id: 'abc123' })); - expect(modal.prop('isOpen')).toEqual(false); - }); + expect(screen.queryByText('DeleteServerModal [OPEN]')).not.toBeInTheDocument(); + expect(screen.getByText('DeleteServerModal [CLOSED]')).toBeInTheDocument(); - it('allows toggling the modal', () => { - const wrapper = createWrapper(); - const modalToggle = wrapper.find(DropdownItem).last(); + await user.click(screen.getByRole('button')); + await user.click(screen.getByRole('menuitem', { name: 'Remove server' })); - expect(wrapper.find(DeleteServerModal).prop('isOpen')).toEqual(false); - - modalToggle.simulate('click'); - expect(wrapper.find(DeleteServerModal).prop('isOpen')).toEqual(true); - - (wrapper.find(DeleteServerModal).prop('toggle') as Function)(); - expect(wrapper.find(DeleteServerModal).prop('isOpen')).toEqual(false); - }); - - it('can be toggled', () => { - const wrapper = createWrapper(); - - expect(wrapper.prop('isOpen')).toEqual(false); - - (wrapper.prop('toggle') as Function)(); - expect(wrapper.prop('isOpen')).toEqual(true); - - (wrapper.prop('toggle') as Function)(); - expect(wrapper.prop('isOpen')).toEqual(false); - }); - - it.each([ - [true, 'Do not auto-connect'], - [false, 'Auto-connect'], - ])('shows different auto-connect toggle text depending on current server status', (autoConnect, expectedText) => { - const wrapper = createWrapper(autoConnect); - const item = wrapper.find(DropdownItem).at(2); - - expect(item.html()).toContain(expectedText); + expect(screen.getByText('DeleteServerModal [OPEN]')).toBeInTheDocument(); + expect(screen.queryByText('DeleteServerModal [CLOSED]')).not.toBeInTheDocument(); }); }); diff --git a/test/servers/ServersDropdown.test.tsx b/test/servers/ServersDropdown.test.tsx index 19e8e616..102cec91 100644 --- a/test/servers/ServersDropdown.test.tsx +++ b/test/servers/ServersDropdown.test.tsx @@ -1,10 +1,10 @@ -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { screen } from '@testing-library/react'; import { values } from 'ramda'; import { Mock } from 'ts-mockery'; import { MemoryRouter } from 'react-router-dom'; import { ServersDropdown } from '../../src/servers/ServersDropdown'; import { ServersMap, ServerWithId } from '../../src/servers/data'; +import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { const fallbackServers: ServersMap = { @@ -12,10 +12,9 @@ describe('', () => { '2b': Mock.of({ name: 'bar', id: '2b' }), '3c': Mock.of({ name: 'baz', id: '3c' }), }; - const setUp = (servers: ServersMap = fallbackServers) => ({ - user: userEvent.setup(), - ...render(), - }); + const setUp = (servers: ServersMap = fallbackServers) => renderWithEvents( + , + ); it('contains the list of servers and the "mange servers" button', async () => { const { user } = setUp(); diff --git a/test/servers/ServersListGroup.test.tsx b/test/servers/ServersListGroup.test.tsx index 35830681..401777e7 100644 --- a/test/servers/ServersListGroup.test.tsx +++ b/test/servers/ServersListGroup.test.tsx @@ -1,7 +1,7 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import { ListGroup } from 'reactstrap'; +import { render, screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; -import ServersListGroup from '../../src/servers/ServersListGroup'; +import { MemoryRouter } from 'react-router-dom'; +import { ServersListGroup } from '../../src/servers/ServersListGroup'; import { ServerWithId } from '../../src/servers/data'; describe('', () => { @@ -9,44 +9,36 @@ describe('', () => { Mock.of({ name: 'foo', id: '123' }), Mock.of({ name: 'bar', id: '456' }), ]; - let wrapped: ShallowWrapper; - const createComponent = (params: { servers?: ServerWithId[]; withChildren?: boolean; embedded?: boolean }) => { + const setUp = (params: { servers?: ServerWithId[]; withChildren?: boolean; embedded?: boolean }) => { const { servers = [], withChildren = true, embedded } = params; - wrapped = shallow( - - {withChildren ? 'The list of servers' : undefined} - , + return render( + + + {withChildren ? 'The list of servers' : undefined} + + , ); - - return wrapped; }; - afterEach(() => wrapped?.unmount()); - it('renders title', () => { - const wrapped = createComponent({}); - const title = wrapped.find('h5'); - - expect(title).toHaveLength(1); - expect(title.text()).toEqual('The list of servers'); + setUp({}); + expect(screen.getByRole('heading')).toHaveTextContent('The list of servers'); }); it('does not render title when children is not provided', () => { - const wrapped = createComponent({ withChildren: false }); - const title = wrapped.find('h5'); - - expect(title).toHaveLength(0); + setUp({ withChildren: false }); + expect(screen.queryByRole('heading')).not.toBeInTheDocument(); }); it.each([ [servers], [[]], ])('shows servers list', (servers) => { - const wrapped = createComponent({ servers }); + setUp({ servers }); - expect(wrapped.find(ListGroup)).toHaveLength(servers.length ? 1 : 0); - expect(wrapped.find('ServerListItem')).toHaveLength(servers.length); + expect(screen.queryAllByRole('list')).toHaveLength(servers.length ? 1 : 0); + expect(screen.queryAllByRole('link')).toHaveLength(servers.length); }); it.each([ @@ -54,9 +46,7 @@ describe('', () => { [false, 'servers-list__list-group'], [undefined, 'servers-list__list-group'], ])('renders proper classes for embedded', (embedded, expectedClasses) => { - const wrapped = createComponent({ servers, embedded }); - const listGroup = wrapped.find(ListGroup); - - expect(listGroup.prop('className')).toEqual(expectedClasses); + setUp({ servers, embedded }); + expect(screen.getByRole('list')).toHaveAttribute('class', `${expectedClasses} list-group`); }); }); diff --git a/test/servers/__snapshots__/ManageServersRow.test.tsx.snap b/test/servers/__snapshots__/ManageServersRow.test.tsx.snap new file mode 100644 index 00000000..850ff084 --- /dev/null +++ b/test/servers/__snapshots__/ManageServersRow.test.tsx.snap @@ -0,0 +1,98 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders auto-connect icon only if server is autoConnect 1`] = ` +
+ + + + + + + + + +
+ + + + My server + + + https://example.com + + + ManageServersRowDropdown + +
+
+`; + +exports[` renders auto-connect icon only if server is autoConnect 2`] = ` +
+ + + + + + + + +
+ + + My server + + + https://example.com + + + ManageServersRowDropdown + +
+
+`; diff --git a/test/servers/helpers/DuplicatedServersModal.test.tsx b/test/servers/helpers/DuplicatedServersModal.test.tsx index d57af0e2..0441b4e5 100644 --- a/test/servers/helpers/DuplicatedServersModal.test.tsx +++ b/test/servers/helpers/DuplicatedServersModal.test.tsx @@ -1,23 +1,17 @@ -import { shallow, ShallowWrapper } from 'enzyme'; +import { screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; -import { Button, ModalHeader } from 'reactstrap'; import { DuplicatedServersModal } from '../../../src/servers/helpers/DuplicatedServersModal'; import { ServerData } from '../../../src/servers/data'; +import { renderWithEvents } from '../../__helpers__/setUpTest'; describe('', () => { const onDiscard = jest.fn(); const onSave = jest.fn(); - let wrapper: ShallowWrapper; - const createWrapper = (duplicatedServers: ServerData[] = []) => { - wrapper = shallow( - , - ); - - return wrapper; - }; + const setUp = (duplicatedServers: ServerData[] = []) => renderWithEvents( + , + ); beforeEach(jest.clearAllMocks); - afterEach(() => wrapper?.unmount()); it.each([ [[], 0], @@ -26,10 +20,8 @@ describe('', () => { [[Mock.all(), Mock.all(), Mock.all()], 3], [[Mock.all(), Mock.all(), Mock.all(), Mock.all()], 4], ])('renders expected amount of items', (duplicatedServers, expectedItems) => { - const wrapper = createWrapper(duplicatedServers); - const li = wrapper.find('li'); - - expect(li).toHaveLength(expectedItems); + setUp(duplicatedServers); + expect(screen.queryAllByRole('listitem')).toHaveLength(expectedItems); }); it.each([ @@ -52,55 +44,53 @@ describe('', () => { }, ], ])('renders expected texts based on amount of servers', (duplicatedServers, assertions) => { - const wrapper = createWrapper(duplicatedServers); - const header = wrapper.find(ModalHeader); - const p = wrapper.find('p'); - const span = wrapper.find('span'); - const discardBtn = wrapper.find(Button).first(); + setUp(duplicatedServers); - expect(header.html()).toContain(assertions.header); - expect(p.html()).toContain(assertions.firstParagraph); - expect(span.html()).toContain(assertions.lastParagraph); - expect(discardBtn.html()).toContain(assertions.discardBtn); + expect(screen.getByRole('heading')).toHaveTextContent(assertions.header); + expect(screen.getByText(assertions.firstParagraph)).toBeInTheDocument(); + expect(screen.getByText(assertions.lastParagraph)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: assertions.discardBtn })).toBeInTheDocument(); }); it.each([ [[]], [[Mock.of({ url: 'url', apiKey: 'apiKey' })]], + [[ + Mock.of({ url: 'url_1', apiKey: 'apiKey_1' }), + Mock.of({ url: 'url_2', apiKey: 'apiKey_2' }), + ]], ])('displays provided server data', (duplicatedServers) => { - const wrapper = createWrapper(duplicatedServers); - const li = wrapper.find('li'); + setUp(duplicatedServers); if (duplicatedServers.length === 0) { - expect(li).toHaveLength(0); + expect(screen.queryByRole('listitem')).not.toBeInTheDocument(); } else if (duplicatedServers.length === 1) { - expect(li.first().find('b').html()).toEqual(`${duplicatedServers[0].url}`); - expect(li.last().find('b').html()).toEqual(`${duplicatedServers[0].apiKey}`); + const [firstItem, secondItem] = screen.getAllByRole('listitem'); + + expect(firstItem).toHaveTextContent(`URL: ${duplicatedServers[0].url}`); + expect(secondItem).toHaveTextContent(`API key: ${duplicatedServers[0].apiKey}`); } else { expect.assertions(duplicatedServers.length); - li.forEach((item, index) => { + screen.getAllByRole('listitem').forEach((item, index) => { const server = duplicatedServers[index]; - - expect(item.html()).toContain(`${server.url} - ${server.apiKey}`); + expect(item).toHaveTextContent(`${server.url} - ${server.apiKey}`); }); } }); - it('invokes onDiscard when appropriate button is clicked', () => { - const wrapper = createWrapper(); - const btn = wrapper.find(Button).first(); - - btn.simulate('click'); + it('invokes onDiscard when appropriate button is clicked', async () => { + const { user } = setUp(); + expect(onDiscard).not.toHaveBeenCalled(); + await user.click(screen.getByRole('button', { name: 'Discard' })); expect(onDiscard).toHaveBeenCalled(); }); - it('invokes onSave when appropriate button is clicked', () => { - const wrapper = createWrapper(); - const btn = wrapper.find(Button).last(); - - btn.simulate('click'); + it('invokes onSave when appropriate button is clicked', async () => { + const { user } = setUp(); + expect(onSave).not.toHaveBeenCalled(); + await user.click(screen.getByRole('button', { name: 'Save anyway' })); expect(onSave).toHaveBeenCalled(); }); }); diff --git a/test/servers/helpers/HighlightCard.test.tsx b/test/servers/helpers/HighlightCard.test.tsx index 52815fd9..199fe4c9 100644 --- a/test/servers/helpers/HighlightCard.test.tsx +++ b/test/servers/helpers/HighlightCard.test.tsx @@ -1,32 +1,23 @@ -import { shallow, ShallowWrapper } from 'enzyme'; +import { render, screen } from '@testing-library/react'; import { ReactNode } from 'react'; -import { Card, CardText, CardTitle } from 'reactstrap'; -import { Link } from 'react-router-dom'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { MemoryRouter } from 'react-router-dom'; import { HighlightCard, HighlightCardProps } from '../../../src/servers/helpers/HighlightCard'; describe('', () => { - let wrapper: ShallowWrapper; - const createWrapper = (props: HighlightCardProps & { children?: ReactNode }) => { - wrapper = shallow(); - - return wrapper; - }; - - afterEach(() => wrapper?.unmount()); + const setUp = (props: HighlightCardProps & { children?: ReactNode }) => render( + + + , + ); it.each([ [undefined], [false], - ])('renders expected components', (link) => { - const wrapper = createWrapper({ title: 'foo', link: link as undefined | false }); + ])('does not render icon when there is no link', (link) => { + setUp({ title: 'foo', link: link as undefined | false }); - expect(wrapper.find(Card)).toHaveLength(1); - expect(wrapper.find(CardTitle)).toHaveLength(1); - expect(wrapper.find(CardText)).toHaveLength(1); - expect(wrapper.find(FontAwesomeIcon)).toHaveLength(0); - expect(wrapper.prop('tag')).not.toEqual(Link); - expect(wrapper.prop('to')).not.toBeDefined(); + expect(screen.queryByRole('img', { hidden: true })).not.toBeInTheDocument(); + expect(screen.queryByRole('link')).not.toBeInTheDocument(); }); it.each([ @@ -34,10 +25,8 @@ describe('', () => { ['bar'], ['baz'], ])('renders provided title', (title) => { - const wrapper = createWrapper({ title }); - const cardTitle = wrapper.find(CardTitle); - - expect(cardTitle.html()).toContain(`>${title}<`); + setUp({ title }); + expect(screen.getByText(title)).toHaveAttribute('class', expect.stringContaining('highlight-card__title')); }); it.each([ @@ -45,10 +34,8 @@ describe('', () => { ['bar'], ['baz'], ])('renders provided children', (children) => { - const wrapper = createWrapper({ title: 'foo', children }); - const cardText = wrapper.find(CardText); - - expect(cardText.html()).toContain(`>${children}<`); + setUp({ title: 'title', children }); + expect(screen.getByText(children)).toHaveAttribute('class', expect.stringContaining('card-text')); }); it.each([ @@ -56,10 +43,9 @@ describe('', () => { ['bar'], ['baz'], ])('adds extra props when a link is provided', (link) => { - const wrapper = createWrapper({ title: 'foo', link }); + setUp({ title: 'title', link }); - expect(wrapper.find(FontAwesomeIcon)).toHaveLength(1); - expect(wrapper.prop('tag')).toEqual(Link); - expect(wrapper.prop('to')).toEqual(link); + expect(screen.getByRole('img', { hidden: true })).toBeInTheDocument(); + expect(screen.getByRole('link')).toHaveAttribute('href', `/${link}`); }); }); diff --git a/test/servers/helpers/ImportServersBtn.test.tsx b/test/servers/helpers/ImportServersBtn.test.tsx index 636ff295..f60d21ad 100644 --- a/test/servers/helpers/ImportServersBtn.test.tsx +++ b/test/servers/helpers/ImportServersBtn.test.tsx @@ -1,43 +1,38 @@ -import { ReactNode } from 'react'; -import { shallow, ShallowWrapper } from 'enzyme'; -import { UncontrolledTooltip } from 'reactstrap'; +import { fireEvent, screen, waitFor } from '@testing-library/react'; import { Mock } from 'ts-mockery'; -import importServersBtnConstruct, { ImportServersBtnProps } from '../../../src/servers/helpers/ImportServersBtn'; +import { + ImportServersBtn as createImportServersBtn, + ImportServersBtnProps, +} from '../../../src/servers/helpers/ImportServersBtn'; import { ServersImporter } from '../../../src/servers/services/ServersImporter'; -import { DuplicatedServersModal } from '../../../src/servers/helpers/DuplicatedServersModal'; +import { ServersMap, ServerWithId } from '../../../src/servers/data'; +import { renderWithEvents } from '../../__helpers__/setUpTest'; describe('', () => { - let wrapper: ShallowWrapper; const onImportMock = jest.fn(); const createServersMock = jest.fn(); const importServersFromFile = jest.fn().mockResolvedValue([]); const serversImporterMock = Mock.of({ importServersFromFile }); - const click = jest.fn(); - const fileRef = { current: Mock.of({ click }) }; - const ImportServersBtn = importServersBtnConstruct(serversImporterMock); - const createWrapper = (props: Partial = {}) => { - wrapper = shallow( - , - ); - - return wrapper; - }; + const ImportServersBtn = createImportServersBtn(serversImporterMock); + const setUp = (props: Partial = {}, servers: ServersMap = {}) => renderWithEvents( + , + ); afterEach(jest.clearAllMocks); - afterEach(() => wrapper.unmount()); - it('renders a button, a tooltip and a file input', () => { - const wrapper = createWrapper(); + it('shows tooltip on button hover', async () => { + const { user } = setUp(); - expect(wrapper.find('#importBtn')).toHaveLength(1); - expect(wrapper.find(UncontrolledTooltip)).toHaveLength(1); - expect(wrapper.find('.import-servers-btn__csv-select')).toHaveLength(1); + expect(screen.queryByText(/^You can create servers by importing a CSV file/)).not.toBeInTheDocument(); + await user.hover(screen.getByRole('button')); + await waitFor( + () => expect(screen.getByText(/^You can create servers by importing a CSV file/)).toBeInTheDocument(), + ); }); it.each([ @@ -45,53 +40,44 @@ describe('', () => { ['foo', 'foo'], ['bar', 'bar'], ])('allows a class name to be provided', (providedClassName, expectedClassName) => { - const wrapper = createWrapper({ className: providedClassName }); - - expect(wrapper.find('#importBtn').prop('className')).toEqual(expectedClassName); + setUp({ className: providedClassName }); + expect(screen.getByRole('button')).toHaveAttribute('class', expect.stringContaining(expectedClassName)); }); it.each([ - [undefined, true], - ['foo', false], - ['bar', false], - ])('has expected text', (children, expectToHaveDefaultText) => { - const wrapper = createWrapper({ children }); - - if (expectToHaveDefaultText) { - expect(wrapper.find('#importBtn').html()).toContain('Import from file'); - } else { - expect(wrapper.find('#importBtn').html()).toContain(children); - expect(wrapper.find('#importBtn').html()).not.toContain('Import from file'); - } - }); - - it('triggers click on file ref when button is clicked', () => { - const wrapper = createWrapper(); - const btn = wrapper.find('#importBtn'); - - btn.simulate('click'); - - expect(click).toHaveBeenCalledTimes(1); + [undefined, 'Import from file'], + ['foo', 'foo'], + ['bar', 'bar'], + ])('has expected text', (children, expectedText) => { + setUp({ children }); + expect(screen.getByRole('button')).toHaveTextContent(expectedText); }); it('imports servers when file input changes', async () => { - const wrapper = createWrapper(); - const file = wrapper.find('.import-servers-btn__csv-select'); - - await file.simulate('change', { target: { files: [''] } }); + const { container } = setUp(); + const input = container.querySelector('[type=file]'); + input && fireEvent.change(input, { target: { files: [''] } }); expect(importServersFromFile).toHaveBeenCalledTimes(1); + await waitFor(() => expect(createServersMock).toHaveBeenCalledTimes(1)); }); it.each([ - ['discard'], - ['save'], - ])('invokes callback in DuplicatedServersModal events', (event) => { - const wrapper = createWrapper(); + ['Save anyway', true], + ['Discard', false], + ])('creates expected servers depending on selected option in modal', async (btnName, savesDuplicatedServers) => { + const existingServer = Mock.of({ id: 'abc', url: 'existingUrl', apiKey: 'existingApiKey' }); + const newServer = Mock.of({ url: 'newUrl', apiKey: 'newApiKey' }); + const { container, user } = setUp({}, { abc: existingServer }); + const input = container.querySelector('[type=file]'); + importServersFromFile.mockResolvedValue([existingServer, newServer]); - wrapper.find(DuplicatedServersModal).simulate(event); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + input && fireEvent.change(input, { target: { files: [''] } }); + await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()); + await user.click(screen.getByRole('button', { name: btnName })); - expect(createServersMock).toHaveBeenCalledTimes(1); + expect(createServersMock).toHaveBeenCalledWith(savesDuplicatedServers ? [existingServer, newServer] : [newServer]); expect(onImportMock).toHaveBeenCalledTimes(1); }); }); diff --git a/test/servers/helpers/ServerError.test.tsx b/test/servers/helpers/ServerError.test.tsx index 014d6ff3..88847c39 100644 --- a/test/servers/helpers/ServerError.test.tsx +++ b/test/servers/helpers/ServerError.test.tsx @@ -1,49 +1,43 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import { BrowserRouter } from 'react-router-dom'; +import { render, screen } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; import { Mock } from 'ts-mockery'; import { ServerError as createServerError } from '../../../src/servers/helpers/ServerError'; import { NonReachableServer, NotFoundServer } from '../../../src/servers/data'; describe('', () => { - let wrapper: ShallowWrapper; const ServerError = createServerError(() => null); - afterEach(() => wrapper?.unmount()); - it.each([ [ Mock.all(), { - 'Could not find this Shlink server.': true, - 'Oops! Could not connect to this Shlink server.': false, - 'Make sure you have internet connection, and the server is properly configured and on-line.': false, - 'Alternatively, if you think you may have miss-configured this server': false, + found: ['Could not find this Shlink server.'], + notFound: [ + 'Oops! Could not connect to this Shlink server.', + 'Make sure you have internet connection, and the server is properly configured and on-line.', + /^Alternatively, if you think you may have miss-configured this server/, + ], }, ], [ Mock.of({ id: 'abc123' }), { - 'Could not find this Shlink server.': false, - 'Oops! Could not connect to this Shlink server.': true, - 'Make sure you have internet connection, and the server is properly configured and on-line.': true, - 'Alternatively, if you think you may have miss-configured this server': true, + found: [ + 'Oops! Could not connect to this Shlink server.', + 'Make sure you have internet connection, and the server is properly configured and on-line.', + /^Alternatively, if you think you may have miss-configured this server/, + ], + notFound: ['Could not find this Shlink server.'], }, ], - ])('renders expected information based on provided server type', (selectedServer, textsToFind) => { - wrapper = shallow( - + ])('renders expected information based on provided server type', (selectedServer, { found, notFound }) => { + render( + - , +
, ); - const wrapperText = wrapper.html(); - const textPairs = Object.entries(textsToFind); - textPairs.forEach(([text, shouldBeFound]) => { - if (shouldBeFound) { - expect(wrapperText).toContain(text); - } else { - expect(wrapperText).not.toContain(text); - } - }); + found.forEach((text) => expect(screen.getByText(text)).toBeInTheDocument()); + notFound.forEach((text) => expect(screen.queryByText(text)).not.toBeInTheDocument()); }); }); diff --git a/test/servers/helpers/ServerForm.test.tsx b/test/servers/helpers/ServerForm.test.tsx index e15318e9..ad8c543c 100644 --- a/test/servers/helpers/ServerForm.test.tsx +++ b/test/servers/helpers/ServerForm.test.tsx @@ -1,30 +1,24 @@ -import { shallow, ShallowWrapper } from 'enzyme'; +import { fireEvent, render, screen } from '@testing-library/react'; import { ServerForm } from '../../../src/servers/helpers/ServerForm'; -import { InputFormGroup } from '../../../src/utils/forms/InputFormGroup'; describe('', () => { - let wrapper: ShallowWrapper; const onSubmit = jest.fn(); + const setUp = () => render(Something); - beforeEach(() => { - wrapper = shallow(Something); - }); - - afterEach(() => wrapper?.unmount()); afterEach(jest.resetAllMocks); it('renders components', () => { - expect(wrapper.find(InputFormGroup)).toHaveLength(3); - expect(wrapper.find('span')).toHaveLength(1); + setUp(); + + expect(screen.getAllByRole('textbox')).toHaveLength(3); + expect(screen.getByText('Something')).toBeInTheDocument(); }); - it('invokes submit callback when submit event is triggered', () => { - const form = wrapper.find('form'); - const preventDefault = jest.fn(); + it('invokes submit callback when submit event is triggered', async () => { + setUp(); - form.simulate('submit', { preventDefault }); - - expect(preventDefault).toHaveBeenCalled(); + expect(onSubmit).not.toHaveBeenCalled(); + fireEvent.submit(screen.getByRole('form'), { preventDefault: jest.fn() }); expect(onSubmit).toHaveBeenCalled(); }); }); diff --git a/test/servers/reducers/selectedServer.test.ts b/test/servers/reducers/selectedServer.test.ts index f50a12ac..2074bcd3 100644 --- a/test/servers/reducers/selectedServer.test.ts +++ b/test/servers/reducers/selectedServer.test.ts @@ -14,7 +14,7 @@ import { NonReachableServer, NotFoundServer, RegularServer } from '../../../src/ describe('selectedServerReducer', () => { describe('reducer', () => { it('returns default when action is RESET_SELECTED_SERVER', () => - expect(reducer(null, { type: RESET_SELECTED_SERVER, selectedServer: null })).toEqual(null)); + expect(reducer(null, { type: RESET_SELECTED_SERVER, selectedServer: null })).toBeNull()); it('returns selected server when action is SELECT_SERVER', () => { const selectedServer = Mock.of({ id: 'abc123' }); diff --git a/test/servers/services/ServersExporter.test.ts b/test/servers/services/ServersExporter.test.ts index ffe7596a..0aa73ec1 100644 --- a/test/servers/services/ServersExporter.test.ts +++ b/test/servers/services/ServersExporter.test.ts @@ -1,7 +1,7 @@ import { Mock } from 'ts-mockery'; import ServersExporter from '../../../src/servers/services/ServersExporter'; -import LocalStorage from '../../../src/utils/services/LocalStorage'; -import { appendChild, removeChild, windowMock } from '../../mocks/WindowMock'; +import { LocalStorage } from '../../../src/utils/services/LocalStorage'; +import { appendChild, removeChild, windowMock } from '../../__mocks__/Window.mock'; describe('ServersExporter', () => { const storageMock = Mock.of({ diff --git a/test/settings/RealTimeUpdatesSettings.test.tsx b/test/settings/RealTimeUpdatesSettings.test.tsx index bd7c908a..943f8157 100644 --- a/test/settings/RealTimeUpdatesSettings.test.tsx +++ b/test/settings/RealTimeUpdatesSettings.test.tsx @@ -1,64 +1,51 @@ -import { shallow, ShallowWrapper } from 'enzyme'; +import { screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; -import { Input } from 'reactstrap'; -import { FormText } from '../../src/utils/forms/FormText'; import { RealTimeUpdatesSettings as RealTimeUpdatesSettingsOptions, Settings, } from '../../src/settings/reducers/settings'; -import RealTimeUpdatesSettings from '../../src/settings/RealTimeUpdatesSettings'; -import ToggleSwitch from '../../src/utils/ToggleSwitch'; -import { LabeledFormGroup } from '../../src/utils/forms/LabeledFormGroup'; +import { RealTimeUpdatesSettings } from '../../src/settings/RealTimeUpdatesSettings'; +import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { const toggleRealTimeUpdates = jest.fn(); const setRealTimeUpdatesInterval = jest.fn(); - let wrapper: ShallowWrapper; - const createWrapper = (realTimeUpdates: Partial = {}) => { - const settings = Mock.of({ realTimeUpdates }); - - wrapper = shallow( - , - ); - - return wrapper; - }; + const setUp = (realTimeUpdates: Partial = {}) => renderWithEvents( + ({ realTimeUpdates })} + toggleRealTimeUpdates={toggleRealTimeUpdates} + setRealTimeUpdatesInterval={setRealTimeUpdatesInterval} + />, + ); afterEach(jest.clearAllMocks); - afterEach(() => wrapper?.unmount()); it('renders enabled real time updates as expected', () => { - const wrapper = createWrapper({ enabled: true }); - const toggle = wrapper.find(ToggleSwitch); - const label = wrapper.find(LabeledFormGroup); - const input = wrapper.find(Input); - const formText = wrapper.find(FormText); + setUp({ enabled: true }); - expect(toggle.prop('checked')).toEqual(true); - expect(toggle.html()).toContain('processed'); - expect(toggle.html()).not.toContain('ignored'); - expect(label.prop('labelClassName')).not.toContain('text-muted'); - expect(input.prop('disabled')).toEqual(false); - expect(formText).toHaveLength(2); + expect(screen.getByLabelText(/^Enable or disable real-time updates./)).toBeChecked(); + expect(screen.getByText(/^Real-time updates are currently being/)).toHaveTextContent('processed'); + expect(screen.getByText(/^Real-time updates are currently being/)).not.toHaveTextContent('ignored'); + expect(screen.getByText('Real-time updates frequency (in minutes):')).not.toHaveAttribute( + 'class', + expect.stringContaining('text-muted'), + ); + expect(screen.getByLabelText('Real-time updates frequency (in minutes):')).not.toHaveAttribute('disabled'); + expect(screen.getByText('Updates will be reflected in the UI as soon as they happen.')).toBeInTheDocument(); }); it('renders disabled real time updates as expected', () => { - const wrapper = createWrapper({ enabled: false }); - const toggle = wrapper.find(ToggleSwitch); - const label = wrapper.find(LabeledFormGroup); - const input = wrapper.find(Input); - const formText = wrapper.find(FormText); + setUp({ enabled: false }); - expect(toggle.prop('checked')).toEqual(false); - expect(toggle.html()).not.toContain('processed'); - expect(toggle.html()).toContain('ignored'); - expect(label.prop('labelClassName')).toContain('text-muted'); - expect(input.prop('disabled')).toEqual(true); - expect(formText).toHaveLength(1); + expect(screen.getByLabelText(/^Enable or disable real-time updates./)).not.toBeChecked(); + expect(screen.getByText(/^Real-time updates are currently being/)).not.toHaveTextContent('processed'); + expect(screen.getByText(/^Real-time updates are currently being/)).toHaveTextContent('ignored'); + expect(screen.getByText('Real-time updates frequency (in minutes):')).toHaveAttribute( + 'class', + expect.stringContaining('text-muted'), + ); + expect(screen.getByLabelText('Real-time updates frequency (in minutes):')).toHaveAttribute('disabled'); + expect(screen.queryByText('Updates will be reflected in the UI as soon as they happen.')).not.toBeInTheDocument(); }); it.each([ @@ -67,43 +54,35 @@ describe('', () => { [10, 'minutes'], [100, 'minutes'], ])('shows expected children when interval is greater than 0', (interval, minutesWord) => { - const wrapper = createWrapper({ enabled: true, interval }); - const span = wrapper.find('span'); - const input = wrapper.find(Input); + setUp({ enabled: true, interval }); - expect(span).toHaveLength(1); - expect(span.html()).toEqual( - `Updates will be reflected in the UI every ${interval} ${minutesWord}.`, + expect(screen.getByText(/^Updates will be reflected in the UI every/)).toHaveTextContent( + `${interval} ${minutesWord}`, ); - expect(input.prop('value')).toEqual(`${interval}`); + expect(screen.getByLabelText('Real-time updates frequency (in minutes):')).toHaveValue(interval); + expect(screen.queryByText('Updates will be reflected in the UI as soon as they happen.')).not.toBeInTheDocument(); }); it.each([[undefined], [0]])('shows expected children when interval is 0 or undefined', (interval) => { - const wrapper = createWrapper({ enabled: true, interval }); - const span = wrapper.find('span'); - const formText = wrapper.find(FormText).at(1); - const input = wrapper.find(Input); + setUp({ enabled: true, interval }); - expect(span).toHaveLength(0); - expect(formText.html()).toContain('Updates will be reflected in the UI as soon as they happen.'); - expect(input.prop('value')).toEqual(''); + expect(screen.queryByText(/^Updates will be reflected in the UI every/)).not.toBeInTheDocument(); + expect(screen.getByText('Updates will be reflected in the UI as soon as they happen.')).toBeInTheDocument(); }); - it('updates real time updates on input change', () => { - const wrapper = createWrapper(); - const input = wrapper.find(Input); + it('updates real time updates when typing on input', async () => { + const { user } = setUp({ enabled: true }); expect(setRealTimeUpdatesInterval).not.toHaveBeenCalled(); - input.simulate('change', { target: { value: '10' } }); - expect(setRealTimeUpdatesInterval).toHaveBeenCalledWith(10); + await user.type(screen.getByLabelText('Real-time updates frequency (in minutes):'), '5'); + expect(setRealTimeUpdatesInterval).toHaveBeenCalledWith(5); }); - it('toggles real time updates on switch change', () => { - const wrapper = createWrapper(); - const toggle = wrapper.find(ToggleSwitch); + it('toggles real time updates on switch change', async () => { + const { user } = setUp({ enabled: true }); expect(toggleRealTimeUpdates).not.toHaveBeenCalled(); - toggle.simulate('change'); + await user.click(screen.getByText(/^Enable or disable real-time updates./)); expect(toggleRealTimeUpdates).toHaveBeenCalled(); }); }); diff --git a/test/settings/Settings.test.tsx b/test/settings/Settings.test.tsx index e3fd951d..593dd943 100644 --- a/test/settings/Settings.test.tsx +++ b/test/settings/Settings.test.tsx @@ -1,29 +1,48 @@ -import { shallow } from 'enzyme'; -import { Route } from 'react-router-dom'; -import createSettings from '../../src/settings/Settings'; -import { NoMenuLayout } from '../../src/common/NoMenuLayout'; -import { NavPillItem } from '../../src/utils/NavPills'; +import { render, screen } from '@testing-library/react'; +import { Router } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; +import { Settings as createSettings } from '../../src/settings/Settings'; describe('', () => { - const Component = () => null; - const Settings = createSettings(Component, Component, Component, Component, Component, Component); + const Settings = createSettings( + () => RealTimeUpdates, + () => ShortUrlCreation, + () => ShortUrlsList, + () => UserInterface, + () => Visits, + () => Tags, + ); + const setUp = (activeRoute = '/') => { + const history = createMemoryHistory(); + history.push(activeRoute); + return render(); + }; - it('renders a no-menu layout with the expected settings sections', () => { - const wrapper = shallow(); - const layout = wrapper.find(NoMenuLayout); - const sections = wrapper.find(Route); + it.each([ + ['/general', { + visibleComps: ['UserInterface', 'RealTimeUpdates'], + hiddenComps: ['ShortUrlCreation', 'ShortUrlsList', 'Tags', 'Visits'], + }], + ['/short-urls', { + visibleComps: ['ShortUrlCreation', 'ShortUrlsList'], + hiddenComps: ['UserInterface', 'RealTimeUpdates', 'Tags', 'Visits'], + }], + ['/other-items', { + visibleComps: ['Tags', 'Visits'], + hiddenComps: ['UserInterface', 'RealTimeUpdates', 'ShortUrlCreation', 'ShortUrlsList'], + }], + ])('renders expected sections based on route', (activeRoute, { visibleComps, hiddenComps }) => { + setUp(activeRoute); - expect(layout).toHaveLength(1); - expect(sections).toHaveLength(4); + visibleComps.forEach((comp) => expect(screen.getByText(comp)).toBeInTheDocument()); + hiddenComps.forEach((comp) => expect(screen.queryByText(comp)).not.toBeInTheDocument()); }); it('renders expected menu', () => { - const wrapper = shallow(); - const items = wrapper.find(NavPillItem); + setUp(); - expect(items).toHaveLength(3); - expect(items.first().prop('to')).toEqual('general'); - expect(items.at(1).prop('to')).toEqual('short-urls'); - expect(items.last().prop('to')).toEqual('other-items'); + expect(screen.getByRole('link', { name: 'General' })).toHaveAttribute('href', '/general'); + expect(screen.getByRole('link', { name: 'Short URLs' })).toHaveAttribute('href', '/short-urls'); + expect(screen.getByRole('link', { name: 'Other items' })).toHaveAttribute('href', '/other-items'); }); }); diff --git a/test/settings/ShortUrlCreationSettings.test.tsx b/test/settings/ShortUrlCreationSettings.test.tsx index b98be1a1..57ea949a 100644 --- a/test/settings/ShortUrlCreationSettings.test.tsx +++ b/test/settings/ShortUrlCreationSettings.test.tsx @@ -1,20 +1,17 @@ -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; import { ShortUrlCreationSettings as ShortUrlsSettings, Settings } from '../../src/settings/reducers/settings'; import { ShortUrlCreationSettings } from '../../src/settings/ShortUrlCreationSettings'; +import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { const setShortUrlCreationSettings = jest.fn(); - const setUp = (shortUrlCreation?: ShortUrlsSettings) => ({ - user: userEvent.setup(), - ...render( - ({ shortUrlCreation })} - setShortUrlCreationSettings={setShortUrlCreationSettings} - />, - ), - }); + const setUp = (shortUrlCreation?: ShortUrlsSettings) => renderWithEvents( + ({ shortUrlCreation })} + setShortUrlCreationSettings={setShortUrlCreationSettings} + />, + ); afterEach(jest.clearAllMocks); diff --git a/test/settings/ShortUrlsListSettings.test.tsx b/test/settings/ShortUrlsListSettings.test.tsx index c167ea4e..53350a0e 100644 --- a/test/settings/ShortUrlsListSettings.test.tsx +++ b/test/settings/ShortUrlsListSettings.test.tsx @@ -1,52 +1,40 @@ -import { shallow, ShallowWrapper } from 'enzyme'; +import { screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; -import { - DEFAULT_SHORT_URLS_ORDERING, - Settings, - ShortUrlsListSettings as ShortUrlsSettings, -} from '../../src/settings/reducers/settings'; +import { Settings, ShortUrlsListSettings as ShortUrlsSettings } from '../../src/settings/reducers/settings'; import { ShortUrlsListSettings } from '../../src/settings/ShortUrlsListSettings'; -import { OrderingDropdown } from '../../src/utils/OrderingDropdown'; import { ShortUrlsOrder } from '../../src/short-urls/data'; +import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { - let wrapper: ShallowWrapper; const setSettings = jest.fn(); - const createWrapper = (shortUrlsList?: ShortUrlsSettings) => { - wrapper = shallow( - ({ shortUrlsList })} setShortUrlsListSettings={setSettings} />, - ); + const setUp = (shortUrlsList?: ShortUrlsSettings) => renderWithEvents( + ({ shortUrlsList })} setShortUrlsListSettings={setSettings} />, + ); - return wrapper; - }; - - afterEach(() => wrapper?.unmount()); afterEach(jest.clearAllMocks); it.each([ - [undefined, DEFAULT_SHORT_URLS_ORDERING], - [{}, DEFAULT_SHORT_URLS_ORDERING], - [{ defaultOrdering: {} }, {}], - [{ defaultOrdering: { field: 'longUrl', dir: 'DESC' } as ShortUrlsOrder }, { field: 'longUrl', dir: 'DESC' }], - [{ defaultOrdering: { field: 'visits', dir: 'ASC' } as ShortUrlsOrder }, { field: 'visits', dir: 'ASC' }], + [undefined, 'Order by: Created at - DESC'], + [{}, 'Order by: Created at - DESC'], + [{ defaultOrdering: {} }, 'Order by...'], + [{ defaultOrdering: { field: 'longUrl', dir: 'DESC' } as ShortUrlsOrder }, 'Order by: Long URL - DESC'], + [{ defaultOrdering: { field: 'visits', dir: 'ASC' } as ShortUrlsOrder }, 'Order by: Visits - ASC'], ])('shows expected ordering', (shortUrlsList, expectedOrder) => { - const wrapper = createWrapper(shortUrlsList); - const dropdown = wrapper.find(OrderingDropdown); - - expect(dropdown.prop('order')).toEqual(expectedOrder); + setUp(shortUrlsList); + expect(screen.getByRole('button')).toHaveTextContent(expectedOrder); }); it.each([ - [undefined, undefined], - ['longUrl', 'ASC'], - ['visits', undefined], - ['title', 'DESC'], - ])('invokes setSettings when ordering changes', (field, dir) => { - const wrapper = createWrapper(); - const dropdown = wrapper.find(OrderingDropdown); + ['Clear selection', undefined, undefined], + ['Long URL', 'longUrl', 'ASC'], + ['Visits', 'visits', 'ASC'], + ['Title', 'title', 'ASC'], + ])('invokes setSettings when ordering changes', async (name, field, dir) => { + const { user } = setUp(); expect(setSettings).not.toHaveBeenCalled(); - dropdown.simulate('change', field, dir); + await user.click(screen.getByRole('button')); + await user.click(screen.getByRole('menuitem', { name })); expect(setSettings).toHaveBeenCalledWith({ defaultOrdering: { field, dir } }); }); }); diff --git a/test/settings/TagsSettings.test.tsx b/test/settings/TagsSettings.test.tsx index 62de1292..854ebf9f 100644 --- a/test/settings/TagsSettings.test.tsx +++ b/test/settings/TagsSettings.test.tsx @@ -1,30 +1,25 @@ -import { shallow, ShallowWrapper } from 'enzyme'; +import { screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; import { Settings, TagsMode, TagsSettings as TagsSettingsOptions } from '../../src/settings/reducers/settings'; -import { TagsModeDropdown } from '../../src/tags/TagsModeDropdown'; import { TagsSettings } from '../../src/settings/TagsSettings'; -import { OrderingDropdown } from '../../src/utils/OrderingDropdown'; import { TagsOrder } from '../../src/tags/data/TagsListChildrenProps'; -import { LabeledFormGroup } from '../../src/utils/forms/LabeledFormGroup'; -import { FormText } from '../../src/utils/forms/FormText'; +import { capitalize } from '../../src/utils/utils'; +import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { - let wrapper: ShallowWrapper; const setTagsSettings = jest.fn(); - const createWrapper = (tags?: TagsSettingsOptions) => { - wrapper = shallow(({ tags })} setTagsSettings={setTagsSettings} />); + const setUp = (tags?: TagsSettingsOptions) => renderWithEvents( + ({ tags })} setTagsSettings={setTagsSettings} />, + ); - return wrapper; - }; - - afterEach(() => wrapper?.unmount()); afterEach(jest.clearAllMocks); it('renders expected amount of groups', () => { - const wrapper = createWrapper(); - const groups = wrapper.find(LabeledFormGroup); + setUp(); - expect(groups).toHaveLength(2); + expect(screen.getByText('Default display mode when managing tags:')).toBeInTheDocument(); + expect(screen.getByText('Default ordering for tags list:')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Order by...' })).toBeInTheDocument(); }); it.each([ @@ -33,50 +28,45 @@ describe('', () => { [{ defaultMode: 'cards' as TagsMode }, 'cards'], [{ defaultMode: 'list' as TagsMode }, 'list'], ])('shows expected tags displaying mode', (tags, expectedMode) => { - const wrapper = createWrapper(tags); - const dropdown = wrapper.find(TagsModeDropdown); - const formText = wrapper.find(FormText); + const { container } = setUp(tags); - expect(dropdown.prop('mode')).toEqual(expectedMode); - expect(formText.html()).toContain(`Tags will be displayed as ${expectedMode}.`); + expect(screen.getByRole('button', { name: capitalize(expectedMode) })).toBeInTheDocument(); + expect(container.querySelector('.form-text')).toHaveTextContent(`Tags will be displayed as ${expectedMode}.`); }); it.each([ ['cards' as TagsMode], ['list' as TagsMode], - ])('invokes setTagsSettings when tags mode changes', (defaultMode) => { - const wrapper = createWrapper(); - const dropdown = wrapper.find(TagsModeDropdown); + ])('invokes setTagsSettings when tags mode changes', async (defaultMode) => { + const { user } = setUp(); expect(setTagsSettings).not.toHaveBeenCalled(); - dropdown.simulate('change', defaultMode); + await user.click(screen.getByText('List')); + await user.click(screen.getByRole('menuitem', { name: capitalize(defaultMode) })); expect(setTagsSettings).toHaveBeenCalledWith({ defaultMode }); }); it.each([ - [undefined, {}], - [{}, {}], - [{ defaultOrdering: {} }, {}], - [{ defaultOrdering: { field: 'tag', dir: 'DESC' } as TagsOrder }, { field: 'tag', dir: 'DESC' }], - [{ defaultOrdering: { field: 'visits', dir: 'ASC' } as TagsOrder }, { field: 'visits', dir: 'ASC' }], + [undefined, 'Order by...'], + [{}, 'Order by...'], + [{ defaultOrdering: {} }, 'Order by...'], + [{ defaultOrdering: { field: 'tag', dir: 'DESC' } as TagsOrder }, 'Order by: Tag - DESC'], + [{ defaultOrdering: { field: 'visits', dir: 'ASC' } as TagsOrder }, 'Order by: Visits - ASC'], ])('shows expected ordering', (tags, expectedOrder) => { - const wrapper = createWrapper(tags); - const dropdown = wrapper.find(OrderingDropdown); - - expect(dropdown.prop('order')).toEqual(expectedOrder); + setUp(tags); + expect(screen.getByRole('button', { name: expectedOrder })).toBeInTheDocument(); }); it.each([ - [undefined, undefined], - ['tag', 'ASC'], - ['visits', undefined], - ['shortUrls', 'DESC'], - ])('invokes setTagsSettings when ordering changes', (field, dir) => { - const wrapper = createWrapper(); - const dropdown = wrapper.find(OrderingDropdown); + ['Tag', 'tag', 'ASC'], + ['Visits', 'visits', 'ASC'], + ['Short URLs', 'shortUrls', 'ASC'], + ])('invokes setTagsSettings when ordering changes', async (name, field, dir) => { + const { user } = setUp(); expect(setTagsSettings).not.toHaveBeenCalled(); - dropdown.simulate('change', field, dir); + await user.click(screen.getByText('Order by...')); + await user.click(screen.getByRole('menuitem', { name })); expect(setTagsSettings).toHaveBeenCalledWith({ defaultOrdering: { field, dir } }); }); }); diff --git a/test/settings/UserInterfaceSettings.test.tsx b/test/settings/UserInterfaceSettings.test.tsx index 48bd7f43..12270b37 100644 --- a/test/settings/UserInterfaceSettings.test.tsx +++ b/test/settings/UserInterfaceSettings.test.tsx @@ -1,22 +1,16 @@ -import { shallow, ShallowWrapper } from 'enzyme'; +import { screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; -import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Settings, UiSettings } from '../../src/settings/reducers/settings'; import { UserInterfaceSettings } from '../../src/settings/UserInterfaceSettings'; -import ToggleSwitch from '../../src/utils/ToggleSwitch'; import { Theme } from '../../src/utils/theme'; +import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { - let wrapper: ShallowWrapper; const setUiSettings = jest.fn(); - const createWrapper = (ui?: UiSettings) => { - wrapper = shallow(({ ui })} setUiSettings={setUiSettings} />); + const setUp = (ui?: UiSettings) => renderWithEvents( + ({ ui })} setUiSettings={setUiSettings} />, + ); - return wrapper; - }; - - afterEach(() => wrapper?.unmount()); afterEach(jest.clearAllMocks); it.each([ @@ -24,32 +18,32 @@ describe('', () => { [{ theme: 'light' as Theme }, false], [undefined, false], ])('toggles switch if theme is dark', (ui, expectedChecked) => { - const wrapper = createWrapper(ui); - const toggle = wrapper.find(ToggleSwitch); + setUp(ui); - expect(toggle.prop('checked')).toEqual(expectedChecked); + if (expectedChecked) { + expect(screen.getByLabelText('Use dark theme.')).toBeChecked(); + } else { + expect(screen.getByLabelText('Use dark theme.')).not.toBeChecked(); + } }); it.each([ - [{ theme: 'dark' as Theme }, faMoon], - [{ theme: 'light' as Theme }, faSun], - [undefined, faSun], - ])('shows different icons based on theme', (ui, expectedIcon) => { - const wrapper = createWrapper(ui); - const icon = wrapper.find(FontAwesomeIcon); - - expect(icon.prop('icon')).toEqual(expectedIcon); + [{ theme: 'dark' as Theme }], + [{ theme: 'light' as Theme }], + [undefined], + ])('shows different icons based on theme', (ui) => { + setUp(ui); + expect(screen.getByRole('img', { hidden: true })).toMatchSnapshot(); }); it.each([ - [true, 'dark'], - [false, 'light'], - ])('invokes setUiSettings when theme toggle value changes', (checked, theme) => { - const wrapper = createWrapper(); - const toggle = wrapper.find(ToggleSwitch); + ['light' as Theme, 'dark' as Theme], + ['dark' as Theme, 'light' as Theme], + ])('invokes setUiSettings when theme toggle value changes', async (initialTheme, expectedTheme) => { + const { user } = setUp({ theme: initialTheme }); expect(setUiSettings).not.toHaveBeenCalled(); - toggle.simulate('change', checked); - expect(setUiSettings).toHaveBeenCalledWith({ theme }); + await user.click(screen.getByLabelText('Use dark theme.')); + expect(setUiSettings).toHaveBeenCalledWith({ theme: expectedTheme }); }); }); diff --git a/test/settings/VisitsSettings.test.tsx b/test/settings/VisitsSettings.test.tsx index 0e750340..8d198272 100644 --- a/test/settings/VisitsSettings.test.tsx +++ b/test/settings/VisitsSettings.test.tsx @@ -1,41 +1,34 @@ -import { shallow, ShallowWrapper } from 'enzyme'; +import { screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; import { Settings } from '../../src/settings/reducers/settings'; import { VisitsSettings } from '../../src/settings/VisitsSettings'; -import { SimpleCard } from '../../src/utils/SimpleCard'; -import { DateIntervalSelector } from '../../src/utils/dates/DateIntervalSelector'; -import { LabeledFormGroup } from '../../src/utils/forms/LabeledFormGroup'; +import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { - let wrapper: ShallowWrapper; const setVisitsSettings = jest.fn(); - const createWrapper = (settings: Partial = {}) => { - wrapper = shallow((settings)} setVisitsSettings={setVisitsSettings} />); - - return wrapper; - }; + const setUp = (settings: Partial = {}) => renderWithEvents( + (settings)} setVisitsSettings={setVisitsSettings} />, + ); afterEach(jest.clearAllMocks); - afterEach(() => wrapper?.unmount()); it('renders expected components', () => { - const wrapper = createWrapper(); + setUp(); - expect(wrapper.find(SimpleCard).prop('title')).toEqual('Visits'); - expect(wrapper.find(LabeledFormGroup).prop('label')).toEqual('Default interval to load on visits sections:'); - expect(wrapper.find(DateIntervalSelector)).toHaveLength(1); + expect(screen.getByRole('heading')).toHaveTextContent('Visits'); + expect(screen.getByText('Default interval to load on visits sections:')).toBeInTheDocument(); }); it.each([ - [Mock.all(), 'last30Days'], - [Mock.of({ visits: {} }), 'last30Days'], + [Mock.all(), 'Last 30 days'], + [Mock.of({ visits: {} }), 'Last 30 days'], [ Mock.of({ visits: { defaultInterval: 'last7Days', }, }), - 'last7Days', + 'Last 7 days', ], [ Mock.of({ @@ -43,21 +36,23 @@ describe('', () => { defaultInterval: 'today', }, }), - 'today', + 'Today', ], ])('sets expected interval as active', (settings, expectedInterval) => { - const wrapper = createWrapper(settings); - - expect(wrapper.find(DateIntervalSelector).prop('active')).toEqual(expectedInterval); + setUp(settings); + expect(screen.getByRole('button')).toHaveTextContent(expectedInterval); }); - it('invokes setVisitsSettings when interval changes', () => { - const wrapper = createWrapper(); - const selector = wrapper.find(DateIntervalSelector); + it('invokes setVisitsSettings when interval changes', async () => { + const { user } = setUp(); + const selectOption = async (name: string) => { + await user.click(screen.getByRole('button')); + await user.click(screen.getByRole('menuitem', { name })); + }; - selector.simulate('change', 'last7Days'); - selector.simulate('change', 'last180Days'); - selector.simulate('change', 'yesterday'); + await selectOption('Last 7 days'); + await selectOption('Last 180 days'); + await selectOption('Yesterday'); expect(setVisitsSettings).toHaveBeenCalledTimes(3); expect(setVisitsSettings).toHaveBeenNthCalledWith(1, { defaultInterval: 'last7Days' }); diff --git a/test/settings/__snapshots__/UserInterfaceSettings.test.tsx.snap b/test/settings/__snapshots__/UserInterfaceSettings.test.tsx.snap new file mode 100644 index 00000000..907a8b66 --- /dev/null +++ b/test/settings/__snapshots__/UserInterfaceSettings.test.tsx.snap @@ -0,0 +1,55 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` shows different icons based on theme 1`] = ` + +`; + +exports[` shows different icons based on theme 2`] = ` + +`; + +exports[` shows different icons based on theme 3`] = ` + +`; diff --git a/test/short-urls/CreateShortUrl.test.tsx b/test/short-urls/CreateShortUrl.test.tsx index 59cb732e..553d1a65 100644 --- a/test/short-urls/CreateShortUrl.test.tsx +++ b/test/short-urls/CreateShortUrl.test.tsx @@ -1,38 +1,30 @@ -import { shallow, ShallowWrapper } from 'enzyme'; +import { render, screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; -import createShortUrlsCreator from '../../src/short-urls/CreateShortUrl'; +import { CreateShortUrl as createShortUrlsCreator } from '../../src/short-urls/CreateShortUrl'; import { ShortUrlCreation } from '../../src/short-urls/reducers/shortUrlCreation'; import { Settings } from '../../src/settings/reducers/settings'; describe('', () => { - let wrapper: ShallowWrapper; - const ShortUrlForm = () => null; - const CreateShortUrlResult = () => null; + const ShortUrlForm = () => ShortUrlForm; + const CreateShortUrlResult = () => CreateShortUrlResult; const shortUrlCreation = { validateUrls: true }; const shortUrlCreationResult = Mock.all(); const createShortUrl = jest.fn(async () => Promise.resolve()); + const CreateShortUrl = createShortUrlsCreator(ShortUrlForm, CreateShortUrlResult); + const setUp = () => render( + {}} + settings={Mock.of({ shortUrlCreation })} + />, + ); - beforeEach(() => { - const CreateShortUrl = createShortUrlsCreator(ShortUrlForm, CreateShortUrlResult); + it('renders computed initial state', () => { + setUp(); - wrapper = shallow( - {}} - settings={Mock.of({ shortUrlCreation })} - />, - ); - }); - afterEach(() => wrapper.unmount()); - afterEach(jest.clearAllMocks); - - it('renders a ShortUrlForm with a computed initial state', () => { - const form = wrapper.find(ShortUrlForm); - const result = wrapper.find(CreateShortUrlResult); - - expect(form).toHaveLength(1); - expect(result).toHaveLength(1); + expect(screen.getByText('ShortUrlForm')).toBeInTheDocument(); + expect(screen.getByText('CreateShortUrlResult')).toBeInTheDocument(); }); }); diff --git a/test/short-urls/EditShortUrl.test.tsx b/test/short-urls/EditShortUrl.test.tsx index 71fcb80e..e97152fd 100644 --- a/test/short-urls/EditShortUrl.test.tsx +++ b/test/short-urls/EditShortUrl.test.tsx @@ -1,107 +1,54 @@ -import { shallow, ShallowWrapper } from 'enzyme'; +import { render, screen } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; import { Mock } from 'ts-mockery'; -import { useLocation, useParams } from 'react-router-dom'; import { EditShortUrl as createEditShortUrl } from '../../src/short-urls/EditShortUrl'; import { Settings } from '../../src/settings/reducers/settings'; import { ShortUrlDetail } from '../../src/short-urls/reducers/shortUrlDetail'; import { ShortUrlEdition } from '../../src/short-urls/reducers/shortUrlEdition'; -import { ShlinkApiError } from '../../src/api/ShlinkApiError'; import { ShortUrl } from '../../src/short-urls/data'; -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useNavigate: jest.fn().mockReturnValue(jest.fn()), - useParams: jest.fn().mockReturnValue({}), - useLocation: jest.fn().mockReturnValue({}), -})); - describe('', () => { - let wrapper: ShallowWrapper; - const ShortUrlForm = () => null; - const getShortUrlDetail = jest.fn(); - const editShortUrl = jest.fn(async () => Promise.resolve()); const shortUrlCreation = { validateUrls: true }; - const EditShortUrl = createEditShortUrl(ShortUrlForm); - const createWrapper = (detail: Partial = {}, edition: Partial = {}) => { - (useParams as any).mockReturnValue({ shortCode: 'the_base_url' }); - (useLocation as any).mockReturnValue({ search: '' }); - - wrapper = shallow( + const EditShortUrl = createEditShortUrl(() => ShortUrlForm); + const setUp = (detail: Partial = {}, edition: Partial = {}) => render( + ({ shortUrlCreation })} selectedServer={null} shortUrlDetail={Mock.of(detail)} shortUrlEdition={Mock.of(edition)} - getShortUrlDetail={getShortUrlDetail} - editShortUrl={editShortUrl} - />, - ); - - return wrapper; - }; - - beforeEach(jest.clearAllMocks); - afterEach(() => wrapper?.unmount()); + getShortUrlDetail={jest.fn()} + editShortUrl={jest.fn(async () => Promise.resolve())} + /> + , + ); it('renders loading message while loading detail', () => { - const wrapper = createWrapper({ loading: true }); + setUp({ loading: true }); - expect(wrapper.prop('loading')).toEqual(true); + expect(screen.getByText('Loading...')).toBeInTheDocument(); + expect(screen.queryByText('ShortUrlForm')).not.toBeInTheDocument(); }); it('renders error when loading detail fails', () => { - const wrapper = createWrapper({ error: true }); - const form = wrapper.find(ShortUrlForm); - const apiError = wrapper.find(ShlinkApiError); + setUp({ error: true }); - expect(form).toHaveLength(0); - expect(apiError).toHaveLength(1); - expect(apiError.prop('fallbackMessage')).toEqual('An error occurred while loading short URL detail :('); + expect(screen.getByText('An error occurred while loading short URL detail :(')).toBeInTheDocument(); + expect(screen.queryByText('ShortUrlForm')).not.toBeInTheDocument(); }); - it.each([ - [undefined, { longUrl: '', validateUrl: true }, true], - [ - Mock.of({ meta: {} }), - { - longUrl: undefined, - tags: undefined, - title: undefined, - domain: undefined, - validSince: undefined, - validUntil: undefined, - maxVisits: undefined, - validateUrl: true, - }, - false, - ], - ])('renders form when detail properly loads', (shortUrl, expectedInitialState, saving) => { - const wrapper = createWrapper({ shortUrl }, { saving }); - const form = wrapper.find(ShortUrlForm); - const apiError = wrapper.find(ShlinkApiError); + it('renders form when detail properly loads', () => { + setUp({ shortUrl: Mock.of({ meta: {} }) }); - expect(form).toHaveLength(1); - expect(apiError).toHaveLength(0); - expect(form.prop('initialState')).toEqual(expectedInitialState); - expect(form.prop('saving')).toEqual(saving); - expect(editShortUrl).not.toHaveBeenCalled(); - - form.simulate('save', {}); - - if (shortUrl) { - expect(editShortUrl).toHaveBeenCalledWith(shortUrl.shortCode, shortUrl.domain, {}); - } else { - expect(editShortUrl).not.toHaveBeenCalled(); - } + expect(screen.getByText('ShortUrlForm')).toBeInTheDocument(); + expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); + expect(screen.queryByText('An error occurred while loading short URL detail :(')).not.toBeInTheDocument(); }); it('shows error when saving data has failed', () => { - const wrapper = createWrapper({}, { error: true }); - const form = wrapper.find(ShortUrlForm); - const apiError = wrapper.find(ShlinkApiError); + setUp({}, { error: true }); - expect(form).toHaveLength(1); - expect(apiError).toHaveLength(1); - expect(apiError.prop('fallbackMessage')).toEqual('An error occurred while updating short URL :('); + expect(screen.getByText('An error occurred while updating short URL :(')).toBeInTheDocument(); + expect(screen.getByText('ShortUrlForm')).toBeInTheDocument(); }); }); diff --git a/test/short-urls/Paginator.test.tsx b/test/short-urls/Paginator.test.tsx index ed147a19..a7a12ab5 100644 --- a/test/short-urls/Paginator.test.tsx +++ b/test/short-urls/Paginator.test.tsx @@ -1,15 +1,17 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import { PaginationItem, PaginationLink } from 'reactstrap'; +import { render, screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; -import Paginator from '../../src/short-urls/Paginator'; +import { MemoryRouter } from 'react-router-dom'; +import { Paginator } from '../../src/short-urls/Paginator'; import { ShlinkPaginator } from '../../src/api/types'; import { ELLIPSIS } from '../../src/utils/helpers/pagination'; describe('', () => { - let wrapper: ShallowWrapper; const buildPaginator = (pagesCount?: number) => Mock.of({ pagesCount, currentPage: 1 }); - - afterEach(() => wrapper?.unmount()); + const setUp = (paginator?: ShlinkPaginator, currentQueryString?: string) => render( + + + , + ); it.each([ [undefined], @@ -17,8 +19,8 @@ describe('', () => { [buildPaginator(0)], [buildPaginator(1)], ])('renders nothing if the number of pages is below 2', (paginator) => { - wrapper = shallow(); - expect(wrapper.text()).toEqual(''); + const { container } = setUp(paginator); + expect(container.firstChild).toBeNull(); }); it.each([ @@ -33,11 +35,12 @@ describe('', () => { expectedPages, expectedEllipsis, ) => { - wrapper = shallow(); - const items = wrapper.find(PaginationItem); - const ellipsis = items.filterWhere((item) => item.find(PaginationLink).prop('children') === ELLIPSIS); + setUp(paginator); - expect(items).toHaveLength(expectedPages); + const links = screen.getAllByRole('link'); + const ellipsis = screen.queryAllByText(ELLIPSIS); + + expect(links).toHaveLength(expectedPages); expect(ellipsis).toHaveLength(expectedEllipsis); }); @@ -45,10 +48,10 @@ describe('', () => { const paginator = buildPaginator(3); const currentQueryString = '?foo=bar'; - wrapper = shallow(); - const links = wrapper.find(PaginationLink); + setUp(paginator, currentQueryString); + const links = screen.getAllByRole('link'); expect(links).toHaveLength(5); - links.forEach((link) => expect(link.prop('to')).toContain(currentQueryString)); + links.forEach((link) => expect(link).toHaveAttribute('href', expect.stringContaining(currentQueryString))); }); }); diff --git a/test/short-urls/ShortUrlForm.test.tsx b/test/short-urls/ShortUrlForm.test.tsx index 13f424e4..60b983c4 100644 --- a/test/short-urls/ShortUrlForm.test.tsx +++ b/test/short-urls/ShortUrlForm.test.tsx @@ -1,67 +1,65 @@ -import { shallow, ShallowWrapper } from 'enzyme'; +import { screen } from '@testing-library/react'; +import { UserEvent } from '@testing-library/user-event/dist/types/setup'; import { formatISO } from 'date-fns'; -import { identity } from 'ramda'; import { Mock } from 'ts-mockery'; -import { Input } from 'reactstrap'; import { ShortUrlForm as createShortUrlForm, Mode } from '../../src/short-urls/ShortUrlForm'; -import DateInput from '../../src/utils/DateInput'; -import { ShortUrlData } from '../../src/short-urls/data'; import { ReachableServer, SelectedServer } from '../../src/servers/data'; -import { SimpleCard } from '../../src/utils/SimpleCard'; import { parseDate } from '../../src/utils/helpers/date'; import { OptionalString } from '../../src/utils/utils'; +import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { - let wrapper: ShallowWrapper; - const TagsSelector = () => null; - const DomainSelector = () => null; const createShortUrl = jest.fn(async () => Promise.resolve()); - const createWrapper = (selectedServer: SelectedServer = null, mode: Mode = 'create', title?: OptionalString) => { - const ShortUrlForm = createShortUrlForm(TagsSelector, DomainSelector); - - wrapper = shallow( + const ShortUrlForm = createShortUrlForm(() => TagsSelector, () => DomainSelector); + const setUp = (selectedServer: SelectedServer = null, mode: Mode = 'create', title?: OptionalString) => + renderWithEvents( ({ validateUrl: true, findIfExists: false, title })} + initialState={{ validateUrl: true, findIfExists: false, title, longUrl: '' }} onSave={createShortUrl} />, ); - return wrapper; - }; - - afterEach(() => wrapper.unmount()); afterEach(jest.clearAllMocks); - it('saves short URL with data set in form controls', () => { - const wrapper = createWrapper(); + it.each([ + [ + async (user: UserEvent) => { + await user.type(screen.getByPlaceholderText('Custom slug'), 'my-slug'); + }, + { customSlug: 'my-slug' }, + ], + [ + async (user: UserEvent) => { + await user.type(screen.getByPlaceholderText('Short code length'), '15'); + }, + { shortCodeLength: '15' }, + ], + ])('saves short URL with data set in form controls', async (extraFields, extraExpectedValues) => { + const { user } = setUp(); const validSince = parseDate('2017-01-01', 'yyyy-MM-dd'); const validUntil = parseDate('2017-01-06', 'yyyy-MM-dd'); - wrapper.find(Input).first().simulate('change', { target: { value: 'https://long-domain.com/foo/bar' } }); - wrapper.find('TagsSelector').simulate('change', ['tag_foo', 'tag_bar']); - wrapper.find('#customSlug').simulate('change', { target: { value: 'my-slug' } }); - wrapper.find(DomainSelector).simulate('change', 'example.com'); - wrapper.find('#maxVisits').simulate('change', { target: { value: '20' } }); - wrapper.find('#shortCodeLength').simulate('change', { target: { value: 15 } }); - wrapper.find(DateInput).at(0).simulate('change', validSince); - wrapper.find(DateInput).at(1).simulate('change', validUntil); - wrapper.find('form').simulate('submit', { preventDefault: identity }); + await user.type(screen.getByPlaceholderText('URL to be shortened'), 'https://long-domain.com/foo/bar'); + await user.type(screen.getByPlaceholderText('Title'), 'the title'); + await user.type(screen.getByPlaceholderText('Maximum number of visits allowed'), '20'); + await user.type(screen.getByPlaceholderText('Enabled since...'), '2017-01-01'); + await user.type(screen.getByPlaceholderText('Enabled until...'), '2017-01-06'); + await extraFields(user); - expect(createShortUrl).toHaveBeenCalledTimes(1); + expect(createShortUrl).not.toHaveBeenCalled(); + await user.click(screen.getByRole('button', { name: 'Save' })); expect(createShortUrl).toHaveBeenCalledWith({ longUrl: 'https://long-domain.com/foo/bar', - tags: ['tag_foo', 'tag_bar'], - customSlug: 'my-slug', - domain: 'example.com', + title: 'the title', validSince: formatISO(validSince), validUntil: formatISO(validUntil), maxVisits: 20, findIfExists: false, - shortCodeLength: 15, validateUrl: true, + ...extraExpectedValues, }); }); @@ -71,29 +69,30 @@ describe('', () => { ])( 'renders expected amount of cards based on server capabilities and mode', (mode, expectedAmountOfCards) => { - const wrapper = createWrapper(null, mode); - const cards = wrapper.find(SimpleCard); + setUp(null, mode); + const cards = screen.queryAllByRole('heading'); expect(cards).toHaveLength(expectedAmountOfCards); }, ); it.each([ - [null, 'new title', 'new title'], - [undefined, 'new title', 'new title'], - ['', 'new title', 'new title'], - [null, '', undefined], - [null, null, undefined], - ['', '', undefined], - [undefined, undefined, undefined], - ['old title', null, null], - ['old title', undefined, null], - ['old title', '', null], - ])('sends expected title based on original and new values', (originalTitle, newTitle, expectedSentTitle) => { - const wrapper = createWrapper(Mock.of({ version: '2.6.0' }), 'create', originalTitle); + [null, true, 'new title'], + [undefined, true, 'new title'], + ['', true, 'new title'], + [null, false, undefined], + ['', false, undefined], + ['old title', false, null], + ])('sends expected title based on original and new values', async (originalTitle, withNewTitle, expectedSentTitle) => { + const { user } = setUp(Mock.of({ version: '2.6.0' }), 'create', originalTitle); - wrapper.find('#title').simulate('change', { target: { value: newTitle } }); - wrapper.find('form').simulate('submit', { preventDefault: identity }); + await user.type(screen.getByPlaceholderText('URL to be shortened'), 'https://long-domain.com/foo/bar'); + if (withNewTitle) { + await user.type(screen.getByPlaceholderText('Title'), 'new title'); + } else { + await user.clear(screen.getByPlaceholderText('Title')); + } + await user.click(screen.getByRole('button', { name: 'Save' })); expect(createShortUrl).toHaveBeenCalledWith(expect.objectContaining({ title: expectedSentTitle, diff --git a/test/short-urls/ShortUrlsFilteringBar.test.tsx b/test/short-urls/ShortUrlsFilteringBar.test.tsx index 113196cf..10adb9fc 100644 --- a/test/short-urls/ShortUrlsFilteringBar.test.tsx +++ b/test/short-urls/ShortUrlsFilteringBar.test.tsx @@ -1,5 +1,4 @@ -import { render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { screen, waitFor } from '@testing-library/react'; import { Mock } from 'ts-mockery'; import { endOfDay, formatISO, startOfDay } from 'date-fns'; import { MemoryRouter, useLocation, useNavigate } from 'react-router-dom'; @@ -7,6 +6,7 @@ import { ShortUrlsFilteringBar as filteringBarCreator } from '../../src/short-ur import { ReachableServer, SelectedServer } from '../../src/servers/data'; import { DateRange } from '../../src/utils/dates/types'; import { formatDate } from '../../src/utils/helpers/date'; +import { renderWithEvents } from '../__helpers__/setUpTest'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), @@ -24,18 +24,15 @@ describe('', () => { (useLocation as any).mockReturnValue({ search }); (useNavigate as any).mockReturnValue(navigate); - return { - user: userEvent.setup(), - ...render( - - ()} - order={{}} - handleOrderBy={handleOrderBy} - /> - , - ), - }; + return renderWithEvents( + + ()} + order={{}} + handleOrderBy={handleOrderBy} + /> + , + ); }; afterEach(jest.clearAllMocks); diff --git a/test/short-urls/ShortUrlsList.test.tsx b/test/short-urls/ShortUrlsList.test.tsx index 426c94ce..06b062ed 100644 --- a/test/short-urls/ShortUrlsList.test.tsx +++ b/test/short-urls/ShortUrlsList.test.tsx @@ -1,26 +1,25 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import { ReactElement } from 'react'; +import { screen } from '@testing-library/react'; +import { FC } from 'react'; import { Mock } from 'ts-mockery'; -import { useNavigate } from 'react-router-dom'; -import shortUrlsListCreator from '../../src/short-urls/ShortUrlsList'; -import { ShortUrlsOrderableFields, ShortUrl, ShortUrlsOrder } from '../../src/short-urls/data'; +import { MemoryRouter, useNavigate } from 'react-router-dom'; +import { ShortUrlsList as createShortUrlsList } from '../../src/short-urls/ShortUrlsList'; +import { ShortUrl, ShortUrlsOrder } from '../../src/short-urls/data'; import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub'; import { ShortUrlsList as ShortUrlsListModel } from '../../src/short-urls/reducers/shortUrlsList'; -import Paginator from '../../src/short-urls/Paginator'; import { ReachableServer } from '../../src/servers/data'; import { Settings } from '../../src/settings/reducers/settings'; +import { ShortUrlsTableProps } from '../../src/short-urls/ShortUrlsTable'; +import { renderWithEvents } from '../__helpers__/setUpTest'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useNavigate: jest.fn().mockReturnValue(jest.fn()), - useParams: jest.fn().mockReturnValue({}), useLocation: jest.fn().mockReturnValue({ search: '?tags=test%20tag&search=example.com' }), })); describe('', () => { - let wrapper: ShallowWrapper; - const ShortUrlsTable = () => null; - const ShortUrlsFilteringBar = () => null; + const ShortUrlsTable: FC = ({ onTagClick }) => onTagClick?.('foo')}>ShortUrlsTable; + const ShortUrlsFilteringBar = () => ShortUrlsFilteringBar; const listShortUrlsMock = jest.fn(); const navigate = jest.fn(); const shortUrlsList = Mock.of({ @@ -33,85 +32,62 @@ describe('', () => { tags: ['test tag'], }), ], - pagination: {}, + pagination: { pagesCount: 3 }, }, }); - const ShortUrlsList = shortUrlsListCreator(ShortUrlsTable, ShortUrlsFilteringBar); - const createWrapper = (defaultOrdering: ShortUrlsOrder = {}) => shallow( - ({ mercureInfo: { loading: true } })} - listShortUrls={listShortUrlsMock} - shortUrlsList={shortUrlsList} - selectedServer={Mock.of({ id: '1' })} - settings={Mock.of({ shortUrlsList: { defaultOrdering } })} - />, - ).dive(); // Dive is needed as this component is wrapped in a HOC + const ShortUrlsList = createShortUrlsList(ShortUrlsTable, ShortUrlsFilteringBar); + const setUp = (defaultOrdering: ShortUrlsOrder = {}) => renderWithEvents( + + ({ mercureInfo: { loading: true } })} + listShortUrls={listShortUrlsMock} + shortUrlsList={shortUrlsList} + selectedServer={Mock.of({ id: '1' })} + settings={Mock.of({ shortUrlsList: { defaultOrdering } })} + /> + , + ); beforeEach(() => { (useNavigate as any).mockReturnValue(navigate); - - wrapper = createWrapper(); }); afterEach(jest.clearAllMocks); - afterEach(() => wrapper?.unmount()); it('wraps expected components', () => { - expect(wrapper.find(ShortUrlsTable)).toHaveLength(1); - expect(wrapper.find(Paginator)).toHaveLength(1); - expect(wrapper.find(ShortUrlsFilteringBar)).toHaveLength(1); + setUp(); + + expect(screen.getByText('ShortUrlsTable')).toBeInTheDocument(); + expect(screen.getByText('ShortUrlsFilteringBar')).toBeInTheDocument(); }); it('passes current query to paginator', () => { - expect(wrapper.find(Paginator).prop('currentQueryString')).toEqual('?tags=test%20tag&search=example.com'); + setUp(); + + const links = screen.getAllByRole('link'); + + expect(links.length > 0).toEqual(true); + links.forEach( + (link) => expect(link).toHaveAttribute('href', expect.stringContaining('?tags=test%20tag&search=example.com')), + ); }); - it('gets list refreshed every time a tag is clicked', () => { - wrapper.find(ShortUrlsTable).simulate('tagClick', 'foo'); - wrapper.find(ShortUrlsTable).simulate('tagClick', 'bar'); - wrapper.find(ShortUrlsTable).simulate('tagClick', 'baz'); + it('gets list refreshed every time a tag is clicked', async () => { + const { user } = setUp(); - expect(navigate).toHaveBeenCalledTimes(3); - expect(navigate).toHaveBeenNthCalledWith(1, expect.stringContaining(`tags=${encodeURIComponent('test tag,foo')}`)); - expect(navigate).toHaveBeenNthCalledWith(2, expect.stringContaining(`tags=${encodeURIComponent('test tag,bar')}`)); - expect(navigate).toHaveBeenNthCalledWith(3, expect.stringContaining(`tags=${encodeURIComponent('test tag,baz')}`)); - }); - - it('invokes order icon rendering', () => { - const renderIcon = (field: ShortUrlsOrderableFields) => - (wrapper.find(ShortUrlsTable).prop('renderOrderIcon') as (field: ShortUrlsOrderableFields) => ReactElement)(field); - - expect(renderIcon('visits').props.currentOrder).toEqual({}); - - (wrapper.find(ShortUrlsFilteringBar).prop('handleOrderBy') as Function)('visits'); - expect(renderIcon('visits').props.currentOrder).toEqual({ field: 'visits' }); - - (wrapper.find(ShortUrlsFilteringBar).prop('handleOrderBy') as Function)('visits', 'ASC'); - expect(renderIcon('visits').props.currentOrder).toEqual({ field: 'visits', dir: 'ASC' }); - }); - - it('handles order through table', () => { - const orderByColumn: (field: ShortUrlsOrderableFields) => Function = wrapper.find(ShortUrlsTable).prop('orderByColumn'); - - expect(wrapper.find(ShortUrlsFilteringBar).prop('order')).toEqual({}); - - orderByColumn('visits')(); - expect(wrapper.find(ShortUrlsFilteringBar).prop('order')).toEqual({ field: 'visits', dir: 'ASC' }); - - orderByColumn('title')(); - expect(wrapper.find(ShortUrlsFilteringBar).prop('order')).toEqual({ field: 'title', dir: 'ASC' }); - - orderByColumn('shortCode')(); - expect(wrapper.find(ShortUrlsFilteringBar).prop('order')).toEqual({ field: 'shortCode', dir: 'ASC' }); + expect(navigate).not.toHaveBeenCalled(); + await user.click(screen.getByText('ShortUrlsTable')); + expect(navigate).toHaveBeenCalledWith(expect.stringContaining(`tags=${encodeURIComponent('test tag,foo')}`)); }); it.each([ [Mock.of({ field: 'visits', dir: 'ASC' }), 'visits', 'ASC'], [Mock.of({ field: 'title', dir: 'DESC' }), 'title', 'DESC'], [Mock.of(), undefined, undefined], - ])('has expected initial ordering', (initialOrderBy, field, dir) => { - const wrapper = createWrapper(initialOrderBy); - - expect(wrapper.find(ShortUrlsFilteringBar).prop('order')).toEqual({ field, dir }); + ])('has expected initial ordering based on settings', (initialOrderBy, field, dir) => { + setUp(initialOrderBy); + expect(listShortUrlsMock).toHaveBeenCalledWith(expect.objectContaining({ + orderBy: { field, dir }, + })); }); }); diff --git a/test/short-urls/ShortUrlsTable.test.tsx b/test/short-urls/ShortUrlsTable.test.tsx index bf980927..c3fded0a 100644 --- a/test/short-urls/ShortUrlsTable.test.tsx +++ b/test/short-urls/ShortUrlsTable.test.tsx @@ -1,71 +1,65 @@ -import { shallow, ShallowWrapper } from 'enzyme'; +import { fireEvent, screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { ShortUrlsTable as shortUrlsTableCreator } from '../../src/short-urls/ShortUrlsTable'; import { ShortUrlsList } from '../../src/short-urls/reducers/shortUrlsList'; import { ReachableServer, SelectedServer } from '../../src/servers/data'; import { ShortUrlsOrderableFields, SHORT_URLS_ORDERABLE_FIELDS } from '../../src/short-urls/data'; +import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { - let wrapper: ShallowWrapper; const shortUrlsList = Mock.all(); const orderByColumn = jest.fn(); - const ShortUrlsRow = () => null; - const ShortUrlsTable = shortUrlsTableCreator(ShortUrlsRow); + const ShortUrlsTable = shortUrlsTableCreator(() => ShortUrlsRow); + const setUp = (server: SelectedServer = null) => renderWithEvents( + orderByColumn} />, + ); - const createWrapper = (server: SelectedServer = null) => { - wrapper = shallow( - orderByColumn} />, - ); - - return wrapper; - }; - - beforeEach(() => createWrapper()); afterEach(jest.resetAllMocks); - afterEach(() => wrapper?.unmount()); it('should render inner table by default', () => { - expect(wrapper.find('table')).toHaveLength(1); + setUp(); + expect(screen.getByRole('table')).toBeInTheDocument(); }); - it('should render table header by default', () => { - expect(wrapper.find('table').find('thead')).toHaveLength(1); + it('should render row groups by default', () => { + setUp(); + expect(screen.getAllByRole('rowgroup')).toHaveLength(2); }); it('should render 6 table header cells by default', () => { - expect(wrapper.find('table').find('thead').find('tr').find('th')).toHaveLength(6); + setUp(); + expect(screen.getAllByRole('columnheader')).toHaveLength(6); }); - it('should render 6 table header cells without order by icon by default', () => { - const thElements = wrapper.find('table').find('thead').find('tr').find('th'); - - thElements.forEach((thElement) => { - expect(thElement.find(FontAwesomeIcon)).toHaveLength(0); - }); + it('should render table header cells without "order by" icon by default', () => { + setUp(); + expect(screen.queryByRole('img', { hidden: true })).not.toBeInTheDocument(); }); it('should render table header cells with conditional order by icon', () => { - const getThElementForSortableField = (orderableField: string) => wrapper.find('table') - .find('thead') - .find('tr') - .find('th') - .filterWhere((e) => e.text().includes(SHORT_URLS_ORDERABLE_FIELDS[orderableField as ShortUrlsOrderableFields])); + setUp(); + + const getThElementForSortableField = (orderableField: string) => screen.getAllByRole('columnheader').find( + ({ innerHTML }) => innerHTML.includes(SHORT_URLS_ORDERABLE_FIELDS[orderableField as ShortUrlsOrderableFields]), + ); const sortableFields = Object.keys(SHORT_URLS_ORDERABLE_FIELDS).filter((sortableField) => sortableField !== 'title'); - expect.assertions(sortableFields.length); + expect.assertions(sortableFields.length * 2); sortableFields.forEach((sortableField) => { - getThElementForSortableField(sortableField).simulate('click'); + const element = getThElementForSortableField(sortableField); + + expect(element).toBeDefined(); + element && fireEvent.click(element); expect(orderByColumn).toHaveBeenCalled(); }); }); it('should render composed title column', () => { - const wrapper = createWrapper(Mock.of({ version: '2.0.0' })); - const composedColumn = wrapper.find('table').find('th').at(2); - const text = composedColumn.text(); + setUp(Mock.of({ version: '2.0.0' })); - expect(text).toContain('Title'); - expect(text).toContain('Long URL'); + const { innerHTML } = screen.getAllByRole('columnheader')[2]; + + expect(innerHTML).toContain('Title'); + expect(innerHTML).toContain('Long URL'); }); }); diff --git a/test/short-urls/UseExistingIfFoundInfoIcon.test.tsx b/test/short-urls/UseExistingIfFoundInfoIcon.test.tsx index 9104f1c9..43d2319a 100644 --- a/test/short-urls/UseExistingIfFoundInfoIcon.test.tsx +++ b/test/short-urls/UseExistingIfFoundInfoIcon.test.tsx @@ -1,11 +1,10 @@ -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { screen } from '@testing-library/react'; import { UseExistingIfFoundInfoIcon } from '../../src/short-urls/UseExistingIfFoundInfoIcon'; +import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { it('shows modal when icon is clicked', async () => { - const user = userEvent.setup(); - render(); + const { user } = renderWithEvents(); expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); await user.click(screen.getByTitle('What does this mean?').firstElementChild as Element); diff --git a/test/short-urls/helpers/CreateShortUrlResult.test.tsx b/test/short-urls/helpers/CreateShortUrlResult.test.tsx index 0f74d06f..03082a93 100644 --- a/test/short-urls/helpers/CreateShortUrlResult.test.tsx +++ b/test/short-urls/helpers/CreateShortUrlResult.test.tsx @@ -1,56 +1,40 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import CopyToClipboard from 'react-copy-to-clipboard'; -import { Tooltip } from 'reactstrap'; +import { screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; -import createCreateShortUrlResult from '../../../src/short-urls/helpers/CreateShortUrlResult'; +import { CreateShortUrlResult as createResult } from '../../../src/short-urls/helpers/CreateShortUrlResult'; import { ShortUrl } from '../../../src/short-urls/data'; -import { StateFlagTimeout } from '../../../src/utils/helpers/hooks'; -import { Result } from '../../../src/utils/Result'; +import { TimeoutToggle } from '../../../src/utils/helpers/hooks'; +import { renderWithEvents } from '../../__helpers__/setUpTest'; describe('', () => { - let wrapper: ShallowWrapper; const copyToClipboard = jest.fn(); - const useStateFlagTimeout = jest.fn(() => [false, copyToClipboard]) as StateFlagTimeout; - const CreateShortUrlResult = createCreateShortUrlResult(useStateFlagTimeout); - const createWrapper = (result: ShortUrl | null = null, error = false) => { - wrapper = shallow( - {}} result={result} error={error} saving={false} />, - ); - - return wrapper; - }; + const useTimeoutToggle = jest.fn(() => [false, copyToClipboard]) as TimeoutToggle; + const CreateShortUrlResult = createResult(useTimeoutToggle); + const setUp = (result: ShortUrl | null = null, error = false) => renderWithEvents( + {}} result={result} error={error} saving={false} />, + ); afterEach(jest.clearAllMocks); - afterEach(() => wrapper?.unmount()); it('renders an error when error is true', () => { - const wrapper = createWrapper(Mock.all(), true); - const errorCard = wrapper.find(Result).filterWhere((result) => result.prop('type') === 'error'); - - expect(errorCard).toHaveLength(1); - expect(errorCard.html()).toContain('An error occurred while creating the URL :('); + setUp(Mock.all(), true); + expect(screen.getByText('An error occurred while creating the URL :(')).toBeInTheDocument(); }); it('renders nothing when no result is provided', () => { - const wrapper = createWrapper(); - - expect(wrapper.html()).toBeNull(); + const { container } = setUp(); + expect(container.firstChild).toBeNull(); }); it('renders a result message when result is provided', () => { - const wrapper = createWrapper(Mock.of({ shortUrl: 'https://doma.in/abc123' })); - - expect(wrapper.html()).toContain('Great! The short URL is https://doma.in/abc123'); - expect(wrapper.find(CopyToClipboard)).toHaveLength(1); - expect(wrapper.find(Tooltip)).toHaveLength(1); + setUp(Mock.of({ shortUrl: 'https://doma.in/abc123' })); + expect(screen.getByText(/The short URL is/)).toHaveTextContent('Great! The short URL is https://doma.in/abc123'); }); - it('Invokes tooltip timeout when copy to clipboard button is clicked', () => { - const wrapper = createWrapper(Mock.of({ shortUrl: 'https://doma.in/abc123' })); - const copyBtn = wrapper.find(CopyToClipboard); + it('Invokes tooltip timeout when copy to clipboard button is clicked', async () => { + const { user } = setUp(Mock.of({ shortUrl: 'https://doma.in/abc123' })); expect(copyToClipboard).not.toHaveBeenCalled(); - copyBtn.simulate('copy'); + await user.click(screen.getByRole('button')); expect(copyToClipboard).toHaveBeenCalledTimes(1); }); }); diff --git a/test/short-urls/helpers/DeleteShortUrlModal.test.tsx b/test/short-urls/helpers/DeleteShortUrlModal.test.tsx index af4a6014..c3a3bd51 100644 --- a/test/short-urls/helpers/DeleteShortUrlModal.test.tsx +++ b/test/short-urls/helpers/DeleteShortUrlModal.test.tsx @@ -1,87 +1,75 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import { identity } from 'ramda'; +import { screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; -import DeleteShortUrlModal from '../../../src/short-urls/helpers/DeleteShortUrlModal'; +import { DeleteShortUrlModal } from '../../../src/short-urls/helpers/DeleteShortUrlModal'; import { ShortUrl } from '../../../src/short-urls/data'; import { ShortUrlDeletion } from '../../../src/short-urls/reducers/shortUrlDeletion'; import { ProblemDetailsError } from '../../../src/api/types'; -import { Result } from '../../../src/utils/Result'; +import { renderWithEvents } from '../../__helpers__/setUpTest'; describe('', () => { - let wrapper: ShallowWrapper; const shortUrl = Mock.of({ tags: [], shortCode: 'abc123', longUrl: 'https://long-domain.com/foo/bar', }); const deleteShortUrl = jest.fn(async () => Promise.resolve()); - const createWrapper = (shortUrlDeletion: Partial) => { - wrapper = shallow( - (shortUrlDeletion)} - toggle={() => {}} - deleteShortUrl={deleteShortUrl} - resetDeleteShortUrl={() => {}} - />, - ); + const setUp = (shortUrlDeletion: Partial) => renderWithEvents( + (shortUrlDeletion)} + deleteShortUrl={deleteShortUrl} + toggle={() => {}} + resetDeleteShortUrl={() => {}} + />, + ); - return wrapper; - }; - - afterEach(() => wrapper?.unmount()); afterEach(jest.clearAllMocks); it('shows generic error when non-threshold error occurs', () => { - const wrapper = createWrapper({ + setUp({ loading: false, error: true, shortCode: 'abc123', errorData: Mock.of({ type: 'OTHER_ERROR' }), }); - const error = wrapper.find(Result).filterWhere((result) => result.prop('type') === 'error'); - - expect(error).toHaveLength(1); - expect(error.html()).toContain('Something went wrong while deleting the URL :('); + expect(screen.getByText('Something went wrong while deleting the URL :(')).toBeInTheDocument(); }); it('disables submit button when loading', () => { - const wrapper = createWrapper({ + setUp({ loading: true, error: false, shortCode: 'abc123', }); - const submit = wrapper.find('.btn-danger'); - - expect(submit).toHaveLength(1); - expect(submit.prop('disabled')).toEqual(true); - expect(submit.html()).toContain('Deleting...'); + expect(screen.getByRole('button', { name: 'Deleting...' })).toHaveAttribute('disabled'); }); - it('enables submit button when proper short code is provided', () => { + it('enables submit button when proper short code is provided', async () => { const shortCode = 'abc123'; - const wrapper = createWrapper({ + const { user } = setUp({ loading: false, error: false, shortCode, }); + const getDeleteBtn = () => screen.getByRole('button', { name: 'Delete' }); - expect(wrapper.find('.btn-danger').prop('disabled')).toEqual(true); - wrapper.find('.form-control').simulate('change', { target: { value: shortCode } }); - expect(wrapper.find('.btn-danger').prop('disabled')).toEqual(false); + expect(getDeleteBtn()).toHaveAttribute('disabled'); + await user.type(screen.getByPlaceholderText(/^Insert the short code/), shortCode); + expect(getDeleteBtn()).not.toHaveAttribute('disabled'); }); - it('tries to delete short URL when form is submit', () => { + it('tries to delete short URL when form is submit', async () => { const shortCode = 'abc123'; - const wrapper = createWrapper({ + const { user } = setUp({ loading: false, error: false, shortCode, }); expect(deleteShortUrl).not.toHaveBeenCalled(); - wrapper.find('form').simulate('submit', { preventDefault: identity }); + await user.type(screen.getByPlaceholderText(/^Insert the short code/), shortCode); + await user.click(screen.getByRole('button', { name: 'Delete' })); expect(deleteShortUrl).toHaveBeenCalledTimes(1); }); }); diff --git a/test/short-urls/helpers/ExportShortUrlsBtn.test.tsx b/test/short-urls/helpers/ExportShortUrlsBtn.test.tsx index c157b6a7..c3abad98 100644 --- a/test/short-urls/helpers/ExportShortUrlsBtn.test.tsx +++ b/test/short-urls/helpers/ExportShortUrlsBtn.test.tsx @@ -1,15 +1,10 @@ import { Mock } from 'ts-mockery'; -import { shallow, ShallowWrapper } from 'enzyme'; +import { screen, waitForElementToBeRemoved } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; import { ReportExporter } from '../../../src/common/services/ReportExporter'; import { ExportShortUrlsBtn as createExportShortUrlsBtn } from '../../../src/short-urls/helpers/ExportShortUrlsBtn'; import { NotFoundServer, ReachableServer, SelectedServer } from '../../../src/servers/data'; - -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useNavigate: jest.fn().mockReturnValue(jest.fn()), - useParams: jest.fn().mockReturnValue({}), - useLocation: jest.fn().mockReturnValue({}), -})); +import { renderWithEvents } from '../../__helpers__/setUpTest'; describe('', () => { const listShortUrls = jest.fn(); @@ -17,35 +12,30 @@ describe('', () => { const exportShortUrls = jest.fn(); const reportExporter = Mock.of({ exportShortUrls }); const ExportShortUrlsBtn = createExportShortUrlsBtn(buildShlinkApiClient, reportExporter); - let wrapper: ShallowWrapper; - const createWrapper = (amount?: number, selectedServer?: SelectedServer) => { - wrapper = shallow( - ()} amount={amount} />, - ); - - return wrapper; - }; + const setUp = (amount?: number, selectedServer?: SelectedServer) => renderWithEvents( + + ()} amount={amount} /> + , + ); afterEach(jest.clearAllMocks); - afterEach(() => wrapper?.unmount()); it.each([ - [undefined, 0], - [1, 1], - [4578, 4578], + [undefined, '0'], + [1, '1'], + [4578, '4,578'], ])('renders expected amount', (amount, expectedAmount) => { - const wrapper = createWrapper(amount); - - expect(wrapper.prop('amount')).toEqual(expectedAmount); + setUp(amount); + expect(screen.getByText(/Export/)).toHaveTextContent(`Export (${expectedAmount})`); }); it.each([ [null], [Mock.of()], - ])('does nothing on click if selected server is not reachable', (selectedServer) => { - const wrapper = createWrapper(0, selectedServer); + ])('does nothing on click if selected server is not reachable', async (selectedServer) => { + const { user } = setUp(0, selectedServer); - wrapper.simulate('click'); + await user.click(screen.getByRole('button')); expect(listShortUrls).not.toHaveBeenCalled(); expect(exportShortUrls).not.toHaveBeenCalled(); }); @@ -58,13 +48,13 @@ describe('', () => { [41, 3], [385, 20], ])('loads proper amount of pages based on the amount of results', async (amount, expectedPageLoads) => { - const wrapper = createWrapper(amount, Mock.of({ id: '123' })); - listShortUrls.mockResolvedValue({ data: [] }); + const { user } = setUp(amount, Mock.of({ id: '123' })); - await (wrapper.prop('onClick') as Function)(); + await user.click(screen.getByRole('button')); + await waitForElementToBeRemoved(() => screen.getByText('Exporting...')); expect(listShortUrls).toHaveBeenCalledTimes(expectedPageLoads); - expect(exportShortUrls).toHaveBeenCalledTimes(1); + expect(exportShortUrls).toHaveBeenCalled(); }); }); diff --git a/test/short-urls/helpers/QrCodeModal.test.tsx b/test/short-urls/helpers/QrCodeModal.test.tsx index fad659dc..bdf120e3 100644 --- a/test/short-urls/helpers/QrCodeModal.test.tsx +++ b/test/short-urls/helpers/QrCodeModal.test.tsx @@ -1,45 +1,34 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import { ExternalLink } from 'react-external-link'; -import { Button, FormGroup, Modal, ModalBody, ModalHeader, Row } from 'reactstrap'; +import { fireEvent, screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; -import createQrCodeModal from '../../../src/short-urls/helpers/QrCodeModal'; +import { QrCodeModal as createQrCodeModal } from '../../../src/short-urls/helpers/QrCodeModal'; import { ShortUrl } from '../../../src/short-urls/data'; import { ReachableServer } from '../../../src/servers/data'; -import { CopyToClipboardIcon } from '../../../src/utils/CopyToClipboardIcon'; import { SemVer } from '../../../src/utils/helpers/version'; import { ImageDownloader } from '../../../src/common/services/ImageDownloader'; -import { QrFormatDropdown } from '../../../src/short-urls/helpers/qr-codes/QrFormatDropdown'; -import { QrErrorCorrectionDropdown } from '../../../src/short-urls/helpers/qr-codes/QrErrorCorrectionDropdown'; +import { renderWithEvents } from '../../__helpers__/setUpTest'; describe('', () => { - let wrapper: ShallowWrapper; const saveImage = jest.fn().mockReturnValue(Promise.resolve()); const QrCodeModal = createQrCodeModal(Mock.of({ saveImage })); const shortUrl = 'https://doma.in/abc123'; - const createWrapper = (version: SemVer = '2.6.0') => { - const selectedServer = Mock.of({ version }); + const setUp = (version: SemVer = '2.6.0') => renderWithEvents( + ({ shortUrl })} + selectedServer={Mock.of({ version })} + toggle={() => {}} + />, + ); - wrapper = shallow( - ({ shortUrl })} - isOpen - toggle={() => {}} - selectedServer={selectedServer} - />, - ); - - return wrapper; - }; - - afterEach(() => wrapper?.unmount()); afterEach(jest.clearAllMocks); it('shows an external link to the URL in the header', () => { - const wrapper = createWrapper(); - const externalLink = wrapper.find(ModalHeader).find(ExternalLink); + setUp(); + const externalLink = screen.getByRole('heading').querySelector('a'); - expect(externalLink).toHaveLength(1); - expect(externalLink.prop('href')).toEqual(shortUrl); + expect(externalLink).toBeInTheDocument(); + expect(externalLink).toHaveAttribute('href', shortUrl); + expect(externalLink).toHaveAttribute('rel', 'noopener noreferrer'); }); it.each([ @@ -47,22 +36,16 @@ describe('', () => { ['2.6.0' as SemVer, 0, '/qr-code?size=300&format=png'], ['2.6.0' as SemVer, 10, '/qr-code?size=300&format=png&margin=10'], ['2.8.0' as SemVer, 0, '/qr-code?size=300&format=png&errorCorrection=L'], - ])('displays an image with the QR code of the URL', (version, margin, expectedUrl) => { - const wrapper = createWrapper(version); - const formControls = wrapper.find('.form-control-range'); + ])('displays an image with the QR code of the URL', async (version, margin, expectedUrl) => { + const { container } = setUp(version); + const marginControl = container.parentNode?.querySelectorAll('.form-control-range').item(1); - if (formControls.length > 1) { - formControls.at(1).simulate('change', { target: { value: `${margin}` } }); + if (marginControl) { + fireEvent.change(marginControl, { target: { value: `${margin}` } }); } - const modalBody = wrapper.find(ModalBody); - const img = modalBody.find('img'); - const linkInBody = modalBody.find(ExternalLink); - const copyToClipboard = modalBody.find(CopyToClipboardIcon); - - expect(img.prop('src')).toEqual(`${shortUrl}${expectedUrl}`); - expect(linkInBody.prop('href')).toEqual(`${shortUrl}${expectedUrl}`); - expect(copyToClipboard.prop('text')).toEqual(`${shortUrl}${expectedUrl}`); + expect(screen.getByRole('img')).toHaveAttribute('src', `${shortUrl}${expectedUrl}`); + expect(screen.getByText(`${shortUrl}${expectedUrl}`)).toHaveAttribute('href', `${shortUrl}${expectedUrl}`); }); it.each([ @@ -73,37 +56,36 @@ describe('', () => { [200, 50, undefined], [720, 100, 'xl'], ])('renders expected size', (size, margin, modalSize) => { - const wrapper = createWrapper(); - const formControls = wrapper.find('.form-control-range'); - const sizeInput = formControls.at(0); - const marginInput = formControls.at(1); + const { container } = setUp(); + const formControls = container.parentNode?.querySelectorAll('.form-control-range'); + const sizeInput = formControls?.[0]; + const marginInput = formControls?.[1]; - sizeInput.simulate('change', { target: { value: `${size}` } }); - marginInput.simulate('change', { target: { value: `${margin}` } }); + sizeInput && fireEvent.change(sizeInput, { target: { value: `${size}` } }); + marginInput && fireEvent.change(marginInput, { target: { value: `${margin}` } }); - expect(wrapper.find('label').at(0).text()).toEqual(`Size: ${size}px`); - expect(wrapper.find('label').at(1).text()).toEqual(`Margin: ${margin}px`); - expect(wrapper.find(Modal).prop('size')).toEqual(modalSize); + expect(screen.getByText(`Size: ${size}px`)).toBeInTheDocument(); + expect(screen.getByText(`Margin: ${margin}px`)).toBeInTheDocument(); + modalSize && expect(screen.getByRole('document')).toHaveClass(`modal-${modalSize}`); }); it.each([ ['2.6.0' as SemVer, 1, 'col-md-4'], ['2.8.0' as SemVer, 2, 'col-md-6'], ])('shows expected components based on server version', (version, expectedAmountOfDropdowns, expectedRangeClass) => { - const wrapper = createWrapper(version); - const dropdownsLength = wrapper.find(QrFormatDropdown).length + wrapper.find(QrErrorCorrectionDropdown).length; - const firstCol = wrapper.find(Row).find(FormGroup).first(); + const { container } = setUp(version); + const dropdowns = screen.getAllByRole('button'); + const firstCol = container.parentNode?.querySelectorAll('.d-grid').item(0); - expect(dropdownsLength).toEqual(expectedAmountOfDropdowns); - expect(firstCol.prop('className')).toEqual(`d-grid ${expectedRangeClass}`); + expect(dropdowns).toHaveLength(expectedAmountOfDropdowns + 1); // Add one because of the close button + expect(firstCol).toHaveClass(expectedRangeClass); }); - it('saves the QR code image when clicking the Download button', () => { - const wrapper = createWrapper('2.9.0'); - const downloadBtn = wrapper.find(Button); + it('saves the QR code image when clicking the Download button', async () => { + const { user } = setUp('2.9.0'); expect(saveImage).not.toHaveBeenCalled(); - downloadBtn.simulate('click'); + await user.click(screen.getByRole('button', { name: /^Download/ })); expect(saveImage).toHaveBeenCalledTimes(1); }); }); diff --git a/test/short-urls/helpers/ShortUrlDetailLink.test.tsx b/test/short-urls/helpers/ShortUrlDetailLink.test.tsx index 5dfe257b..3c38216e 100644 --- a/test/short-urls/helpers/ShortUrlDetailLink.test.tsx +++ b/test/short-urls/helpers/ShortUrlDetailLink.test.tsx @@ -1,15 +1,11 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import { Link } from 'react-router-dom'; +import { render, screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; -import ShortUrlDetailLink, { LinkSuffix } from '../../../src/short-urls/helpers/ShortUrlDetailLink'; +import { MemoryRouter } from 'react-router-dom'; +import { ShortUrlDetailLink, LinkSuffix } from '../../../src/short-urls/helpers/ShortUrlDetailLink'; import { NotFoundServer, ReachableServer } from '../../../src/servers/data'; import { ShortUrl } from '../../../src/short-urls/data'; describe('', () => { - let wrapper: ShallowWrapper; - - afterEach(() => wrapper?.unmount()); - it.each([ [undefined, undefined], [null, null], @@ -19,15 +15,14 @@ describe('', () => { [null, Mock.all()], [undefined, Mock.all()], ])('only renders a plain span when either server or short URL are not set', (selectedServer, shortUrl) => { - wrapper = shallow( + render( Something , ); - const link = wrapper.find(Link); - expect(link).toHaveLength(0); - expect(wrapper.html()).toEqual('Something'); + expect(screen.queryByRole('link')).not.toBeInTheDocument(); + expect(screen.getByText('Something')).toBeInTheDocument(); }); it.each([ @@ -56,15 +51,13 @@ describe('', () => { '/server/3/short-code/def456/edit?domain=example.com', ], ])('renders link with expected query when', (selectedServer, shortUrl, suffix, expectedLink) => { - wrapper = shallow( - - Something - , + render( + + + Something + + , ); - const link = wrapper.find(Link); - const to = link.prop('to'); - - expect(link).toHaveLength(1); - expect(to).toEqual(expectedLink); + expect(screen.getByRole('link')).toHaveProperty('href', expect.stringContaining(expectedLink)); }); }); diff --git a/test/short-urls/helpers/ShortUrlFormCheckboxGroup.test.tsx b/test/short-urls/helpers/ShortUrlFormCheckboxGroup.test.tsx index da7aec6c..ec3276d0 100644 --- a/test/short-urls/helpers/ShortUrlFormCheckboxGroup.test.tsx +++ b/test/short-urls/helpers/ShortUrlFormCheckboxGroup.test.tsx @@ -1,17 +1,17 @@ -import { shallow } from 'enzyme'; +import { render, screen } from '@testing-library/react'; import { ShortUrlFormCheckboxGroup } from '../../../src/short-urls/helpers/ShortUrlFormCheckboxGroup'; -import Checkbox from '../../../src/utils/Checkbox'; -import { InfoTooltip } from '../../../src/utils/InfoTooltip'; describe('', () => { it.each([ [undefined, '', 0], ['This is the tooltip', 'me-2', 1], ])('renders tooltip only when provided', (infoTooltip, expectedClassName, expectedAmountOfTooltips) => { - const wrapper = shallow(); - const checkbox = wrapper.find(Checkbox); + render(); - expect(checkbox.prop('className')).toEqual(expectedClassName); - expect(wrapper.find(InfoTooltip)).toHaveLength(expectedAmountOfTooltips); + expect(screen.getByRole('checkbox').parentNode).toHaveAttribute( + 'class', + expect.stringContaining(expectedClassName), + ); + expect(screen.queryAllByRole('img', { hidden: true })).toHaveLength(expectedAmountOfTooltips); }); }); diff --git a/test/short-urls/helpers/ShortUrlVisitsCount.test.tsx b/test/short-urls/helpers/ShortUrlVisitsCount.test.tsx index e6420e9b..1fb8bf49 100644 --- a/test/short-urls/helpers/ShortUrlVisitsCount.test.tsx +++ b/test/short-urls/helpers/ShortUrlVisitsCount.test.tsx @@ -1,43 +1,28 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import { UncontrolledTooltip } from 'reactstrap'; +import { render } from '@testing-library/react'; import { Mock } from 'ts-mockery'; -import ShortUrlVisitsCount from '../../../src/short-urls/helpers/ShortUrlVisitsCount'; +import { ShortUrlVisitsCount } from '../../../src/short-urls/helpers/ShortUrlVisitsCount'; import { ShortUrl } from '../../../src/short-urls/data'; describe('', () => { - let wrapper: ShallowWrapper; - - const createWrapper = (visitsCount: number, shortUrl: ShortUrl) => { - wrapper = shallow(); - - return wrapper; - }; - - afterEach(() => wrapper?.unmount()); + const setUp = (visitsCount: number, shortUrl: ShortUrl) => render( + , + ); it.each([undefined, {}])('just returns visits when no maxVisits is provided', (meta) => { const visitsCount = 45; - const wrapper = createWrapper(visitsCount, Mock.of({ meta })); - const maxVisitsHelper = wrapper.find('.short-urls-visits-count__max-visits-control'); - const maxVisitsTooltip = wrapper.find(UncontrolledTooltip); + const { container } = setUp(visitsCount, Mock.of({ meta })); - expect(wrapper.html()).toEqual( - `${visitsCount}`, - ); - expect(maxVisitsHelper).toHaveLength(0); - expect(maxVisitsTooltip).toHaveLength(0); + expect(container.firstChild).toHaveTextContent(`${visitsCount}`); + expect(container.querySelector('.short-urls-visits-count__max-visits-control')).not.toBeInTheDocument(); }); it('displays the maximum amount of visits when present', () => { const visitsCount = 45; const maxVisits = 500; const meta = { maxVisits }; - const wrapper = createWrapper(visitsCount, Mock.of({ meta })); - const maxVisitsHelper = wrapper.find('.short-urls-visits-count__max-visits-control'); - const maxVisitsTooltip = wrapper.find(UncontrolledTooltip); + const { container } = setUp(visitsCount, Mock.of({ meta })); - expect(wrapper.html()).toContain(`/ ${maxVisits}`); - expect(maxVisitsHelper).toHaveLength(1); - expect(maxVisitsTooltip).toHaveLength(1); + expect(container.firstChild).toHaveTextContent(`/ ${maxVisits}`); + expect(container.querySelector('.short-urls-visits-count__max-visits-control')).toBeInTheDocument(); }); }); diff --git a/test/short-urls/helpers/ShortUrlsRow.test.tsx b/test/short-urls/helpers/ShortUrlsRow.test.tsx index fcb9b562..98b4cfdd 100644 --- a/test/short-urls/helpers/ShortUrlsRow.test.tsx +++ b/test/short-urls/helpers/ShortUrlsRow.test.tsx @@ -1,35 +1,25 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import { assoc, toString } from 'ramda'; +import { screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; -import { ExternalLink } from 'react-external-link'; import { formatISO } from 'date-fns'; -import createShortUrlsRow from '../../../src/short-urls/helpers/ShortUrlsRow'; -import Tag from '../../../src/tags/helpers/Tag'; -import ColorGenerator from '../../../src/utils/services/ColorGenerator'; -import { StateFlagTimeout } from '../../../src/utils/helpers/hooks'; +import { ShortUrlsRow as createShortUrlsRow } from '../../../src/short-urls/helpers/ShortUrlsRow'; +import { TimeoutToggle } from '../../../src/utils/helpers/hooks'; import { ShortUrl } from '../../../src/short-urls/data'; import { ReachableServer } from '../../../src/servers/data'; -import { CopyToClipboardIcon } from '../../../src/utils/CopyToClipboardIcon'; -import { Time } from '../../../src/utils/Time'; import { parseDate } from '../../../src/utils/helpers/date'; +import { renderWithEvents } from '../../__helpers__/setUpTest'; +import { OptionalString } from '../../../src/utils/utils'; +import { colorGeneratorMock } from '../../utils/services/__mocks__/ColorGenerator.mock'; describe('', () => { - let wrapper: ShallowWrapper; - const mockFunction = () => null; - const ShortUrlsRowMenu = mockFunction; - const stateFlagTimeout = jest.fn(() => true); - const useStateFlagTimeout = jest.fn(() => [false, stateFlagTimeout]) as StateFlagTimeout; - const colorGenerator = Mock.of({ - getColorForKey: jest.fn(), - setColorForKey: jest.fn(), - }); + const timeoutToggle = jest.fn(() => true); + const useTimeoutToggle = jest.fn(() => [false, timeoutToggle]) as TimeoutToggle; const server = Mock.of({ url: 'https://doma.in' }); const shortUrl: ShortUrl = { shortCode: 'abc123', - shortUrl: 'http://doma.in/abc123', - longUrl: 'http://foo.com/bar', + shortUrl: 'https://doma.in/abc123', + longUrl: 'https://foo.com/bar', dateCreated: formatISO(parseDate('2018-05-23 18:30:41', 'yyyy-MM-dd HH:mm:ss')), - tags: ['nodejs', 'reactjs'], + tags: [], visitsCount: 45, domain: null, meta: { @@ -38,99 +28,73 @@ describe('', () => { maxVisits: null, }, }; - const ShortUrlsRow = createShortUrlsRow(ShortUrlsRowMenu, colorGenerator, useStateFlagTimeout); - const createWrapper = (title?: string | null) => { - wrapper = shallow( - , - ); - - return wrapper; - }; - - beforeEach(() => createWrapper()); - afterEach(() => wrapper.unmount()); + const ShortUrlsRow = createShortUrlsRow(() => ShortUrlsRowMenu, colorGeneratorMock, useTimeoutToggle); + const setUp = (title?: OptionalString, tags: string[] = []) => renderWithEvents( + + + null} /> + +
, + ); it.each([ [null, 6], [undefined, 6], ['The title', 7], ])('renders expected amount of columns', (title, expectedAmount) => { - const wrapper = createWrapper(title); - const cols = wrapper.find('td'); - - expect(cols).toHaveLength(expectedAmount); + setUp(title); + expect(screen.getAllByRole('cell')).toHaveLength(expectedAmount); }); it('renders date in first column', () => { - const col = wrapper.find('td').first(); - const date = col.find(Time); - - expect(date.html()).toContain('>2018-05-23 18:30'); + setUp(); + expect(screen.getAllByRole('cell')[0]).toHaveTextContent('2018-05-23 18:30'); }); - it('renders short URL in second row', () => { - const col = wrapper.find('td').at(1); - const link = col.find(ExternalLink); + it.each([ + [1, shortUrl.shortUrl], + [2, shortUrl.longUrl], + ])('renders expected links on corresponding columns', (colIndex, expectedLink) => { + setUp(); - expect(link.prop('href')).toEqual(shortUrl.shortUrl); + const col = screen.getAllByRole('cell')[colIndex]; + const link = col.querySelector('a'); + + expect(link).toHaveAttribute('href', expectedLink); }); - it('renders long URL in third row', () => { - const col = wrapper.find('td').at(2); - const link = col.find(ExternalLink); + it.each([ + ['My super cool title', 'My super cool title'], + [undefined, shortUrl.longUrl], + ])('renders title when short URL has it', (title, expectedContent) => { + setUp(title); - expect(link.prop('href')).toEqual(shortUrl.longUrl); + const titleSharedCol = screen.getAllByRole('cell')[2]; + + expect(titleSharedCol.querySelector('a')).toHaveAttribute('href', shortUrl.longUrl); + expect(titleSharedCol).toHaveTextContent(expectedContent); }); - it('renders title when short URL has it', () => { - const wrapper = createWrapper('My super cool title'); - const cols = wrapper.find('td'); - const titleSharedCol = cols.at(2).find(ExternalLink); - const dedicatedShortUrlCol = cols.at(3).find(ExternalLink); + it.each([ + [[], ['No tags']], + [['nodejs', 'reactjs'], ['nodejs', 'reactjs']], + ])('renders list of tags in fourth row', (tags, expectedContents) => { + setUp(undefined, tags); + const cell = screen.getAllByRole('cell')[3]; - expect(titleSharedCol).toHaveLength(1); - expect(dedicatedShortUrlCol).toHaveLength(1); - expect(titleSharedCol.prop('href')).toEqual(shortUrl.longUrl); - expect(dedicatedShortUrlCol.prop('href')).toEqual(shortUrl.longUrl); - expect(titleSharedCol.html()).toContain('My super cool title'); - expect(dedicatedShortUrlCol.prop('children')).not.toBeDefined(); - }); - - describe('renders list of tags in fourth row', () => { - it('with tags', () => { - const col = wrapper.find('td').at(3); - const tags = col.find(Tag); - - expect(tags).toHaveLength(shortUrl.tags.length); - shortUrl.tags.forEach((tagValue, index) => { - const tag = tags.at(index); - - expect(tag.prop('text')).toEqual(tagValue); - }); - }); - - it('without tags', () => { - wrapper.setProps({ shortUrl: assoc('tags', [], shortUrl) }); - - const col = wrapper.find('td').at(3); - - expect(col.text()).toContain('No tags'); - }); + expectedContents.forEach((content) => expect(cell).toHaveTextContent(content)); }); it('renders visits count in fifth row', () => { - const col = wrapper.find('td').at(4); - - expect(col.html()).toContain(toString(shortUrl.visitsCount)); + setUp(); + expect(screen.getAllByRole('cell')[4]).toHaveTextContent(`${shortUrl.visitsCount}`); }); - it('updates state when copied to clipboard', () => { - const col = wrapper.find('td').at(1); - const menu = col.find(CopyToClipboardIcon); + it('updates state when copied to clipboard', async () => { + const { user } = setUp(); - expect(menu).toHaveLength(1); - expect(stateFlagTimeout).not.toHaveBeenCalled(); - menu.simulate('copy'); - expect(stateFlagTimeout).toHaveBeenCalledTimes(1); + expect(timeoutToggle).not.toHaveBeenCalled(); + await user.click(screen.getByRole('img', { hidden: true })); + expect(timeoutToggle).toHaveBeenCalledTimes(1); }); }); diff --git a/test/short-urls/helpers/ShortUrlsRowMenu.test.tsx b/test/short-urls/helpers/ShortUrlsRowMenu.test.tsx index 6476e312..3a996b3c 100644 --- a/test/short-urls/helpers/ShortUrlsRowMenu.test.tsx +++ b/test/short-urls/helpers/ShortUrlsRowMenu.test.tsx @@ -1,58 +1,35 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import { DropdownItem } from 'reactstrap'; +import { screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; -import createShortUrlsRowMenu from '../../../src/short-urls/helpers/ShortUrlsRowMenu'; +import { MemoryRouter } from 'react-router-dom'; +import { ShortUrlsRowMenu as createShortUrlsRowMenu } from '../../../src/short-urls/helpers/ShortUrlsRowMenu'; import { ReachableServer } from '../../../src/servers/data'; import { ShortUrl } from '../../../src/short-urls/data'; -import { DropdownBtnMenu } from '../../../src/utils/DropdownBtnMenu'; +import { renderWithEvents } from '../../__helpers__/setUpTest'; describe('', () => { - let wrapper: ShallowWrapper; - const DeleteShortUrlModal = () => null; - const QrCodeModal = () => null; + const ShortUrlsRowMenu = createShortUrlsRowMenu(() => DeleteShortUrlModal, () => QrCodeModal); const selectedServer = Mock.of({ id: 'abc123' }); const shortUrl = Mock.of({ shortCode: 'abc123', shortUrl: 'https://doma.in/abc123', }); - const createWrapper = () => { - const ShortUrlsRowMenu = createShortUrlsRowMenu(DeleteShortUrlModal, QrCodeModal); - - wrapper = shallow(); - - return wrapper; - }; - - afterEach(() => wrapper?.unmount()); + const setUp = () => renderWithEvents( + + + , + ); it('renders modal windows', () => { - const wrapper = createWrapper(); - const deleteShortUrlModal = wrapper.find(DeleteShortUrlModal); - const qrCodeModal = wrapper.find(QrCodeModal); + setUp(); - expect(deleteShortUrlModal).toHaveLength(1); - expect(qrCodeModal).toHaveLength(1); + expect(screen.getByText('DeleteShortUrlModal')).toBeInTheDocument(); + expect(screen.getByText('QrCodeModal')).toBeInTheDocument(); }); - it('renders correct amount of menu items', () => { - const wrapper = createWrapper(); - const items = wrapper.find(DropdownItem); + it('renders correct amount of menu items', async () => { + const { user } = setUp(); - expect(items).toHaveLength(5); - expect(items.find('[divider]')).toHaveLength(1); - }); - - describe('toggles state when toggling modals or the dropdown', () => { - const assert = (modalComponent: Function) => { - const wrapper = createWrapper(); - - expect(wrapper.find(modalComponent).prop('isOpen')).toEqual(false); - (wrapper.find(modalComponent).prop('toggle') as Function)(); - expect(wrapper.find(modalComponent).prop('isOpen')).toEqual(true); - }; - - it('DeleteShortUrlModal', () => assert(DeleteShortUrlModal)); - it('QrCodeModal', () => assert(QrCodeModal)); - it('ShortUrlRowMenu', () => assert(DropdownBtnMenu)); + await user.click(screen.getByRole('button')); + expect(screen.getAllByRole('menuitem')).toHaveLength(4); }); }); diff --git a/test/short-urls/helpers/index.test.ts b/test/short-urls/helpers/index.test.ts new file mode 100644 index 00000000..06415123 --- /dev/null +++ b/test/short-urls/helpers/index.test.ts @@ -0,0 +1,48 @@ +import { Mock } from 'ts-mockery'; +import { ShortUrl } from '../../../src/short-urls/data'; +import { shortUrlDataFromShortUrl, urlDecodeShortCode, urlEncodeShortCode } from '../../../src/short-urls/helpers'; + +describe('helpers', () => { + describe('shortUrlDataFromShortUrl', () => { + it.each([ + [undefined, { validateUrls: true }, { longUrl: '', validateUrl: true }], + [undefined, undefined, { longUrl: '', validateUrl: false }], + [ + Mock.of({ meta: {} }), + { validateUrls: false }, + { + longUrl: undefined, + tags: undefined, + title: undefined, + domain: undefined, + validSince: undefined, + validUntil: undefined, + maxVisits: undefined, + validateUrl: false, + }, + ], + ])('returns expected data', (shortUrl, settings, expectedInitialState) => { + expect(shortUrlDataFromShortUrl(shortUrl, settings)).toEqual(expectedInitialState); + }); + }); + + describe('urlEncodeShortCode', () => { + it.each([ + ['foo', 'foo'], + ['foo/bar', 'foo__bar'], + ['foo/bar/baz', 'foo__bar__baz'], + ])('parses shortCode as expected', (shortCode, result) => { + expect(urlEncodeShortCode(shortCode)).toEqual(result); + }); + }); + + describe('urlDecodeShortCode', () => { + it.each([ + ['foo', 'foo'], + ['foo__bar', 'foo/bar'], + ['foo__bar__baz', 'foo/bar/baz'], + ])('parses shortCode as expected', (shortCode, result) => { + expect(urlDecodeShortCode(shortCode)).toEqual(result); + }); + }); +}); diff --git a/test/short-urls/helpers/qr-codes/QrErrorCorrectionDropdown.test.tsx b/test/short-urls/helpers/qr-codes/QrErrorCorrectionDropdown.test.tsx index 32db92f8..ae20d61f 100644 --- a/test/short-urls/helpers/qr-codes/QrErrorCorrectionDropdown.test.tsx +++ b/test/short-urls/helpers/qr-codes/QrErrorCorrectionDropdown.test.tsx @@ -1,47 +1,50 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import { DropdownItem } from 'reactstrap'; +import { screen } from '@testing-library/react'; import { QrErrorCorrection } from '../../../../src/utils/helpers/qrCodes'; import { QrErrorCorrectionDropdown } from '../../../../src/short-urls/helpers/qr-codes/QrErrorCorrectionDropdown'; +import { renderWithEvents } from '../../../__helpers__/setUpTest'; describe('', () => { const initialErrorCorrection: QrErrorCorrection = 'Q'; const setErrorCorrection = jest.fn(); - let wrapper: ShallowWrapper; + const setUp = () => renderWithEvents( + , + ); - beforeEach(() => { - wrapper = shallow( - , - ); - }); - - afterEach(() => wrapper?.unmount()); afterEach(jest.clearAllMocks); - it('renders initial state', () => { - const items = wrapper.find(DropdownItem); + it('renders initial state', async () => { + const { user } = setUp(); + const btn = screen.getByRole('button'); - expect(wrapper.prop('text')).toEqual('Error correction (Q)'); - expect(items.at(0).prop('active')).toEqual(false); - expect(items.at(1).prop('active')).toEqual(false); - expect(items.at(2).prop('active')).toEqual(true); - expect(items.at(3).prop('active')).toEqual(false); + expect(btn).toHaveTextContent('Error correction (Q)'); + await user.click(btn); + const items = screen.getAllByRole('menuitem'); + + expect(items[0]).not.toHaveClass('active'); + expect(items[1]).not.toHaveClass('active'); + expect(items[2]).toHaveClass('active'); + expect(items[3]).not.toHaveClass('active'); }); - it('invokes callback when items are clicked', () => { - const items = wrapper.find(DropdownItem); + it('invokes callback when items are clicked', async () => { + const { user } = setUp(); + const clickItem = async (name: RegExp) => { + await user.click(screen.getByRole('button')); + await user.click(screen.getByRole('menuitem', { name })); + }; expect(setErrorCorrection).not.toHaveBeenCalled(); - items.at(0).simulate('click'); + await clickItem(/ow/); expect(setErrorCorrection).toHaveBeenCalledWith('L'); - items.at(1).simulate('click'); + await clickItem(/edium/); expect(setErrorCorrection).toHaveBeenCalledWith('M'); - items.at(2).simulate('click'); + await clickItem(/uartile/); expect(setErrorCorrection).toHaveBeenCalledWith('Q'); - items.at(3).simulate('click'); + await clickItem(/igh/); expect(setErrorCorrection).toHaveBeenCalledWith('H'); }); }); diff --git a/test/short-urls/helpers/qr-codes/QrFormatDropdown.test.tsx b/test/short-urls/helpers/qr-codes/QrFormatDropdown.test.tsx index c40e05d0..499a5e89 100644 --- a/test/short-urls/helpers/qr-codes/QrFormatDropdown.test.tsx +++ b/test/short-urls/helpers/qr-codes/QrFormatDropdown.test.tsx @@ -1,37 +1,40 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import { DropdownItem } from 'reactstrap'; +import { screen } from '@testing-library/react'; import { QrCodeFormat } from '../../../../src/utils/helpers/qrCodes'; import { QrFormatDropdown } from '../../../../src/short-urls/helpers/qr-codes/QrFormatDropdown'; +import { renderWithEvents } from '../../../__helpers__/setUpTest'; describe('', () => { const initialFormat: QrCodeFormat = 'svg'; const setFormat = jest.fn(); - let wrapper: ShallowWrapper; + const setUp = () => renderWithEvents(); - beforeEach(() => { - wrapper = shallow(); - }); - - afterEach(() => wrapper?.unmount()); afterEach(jest.clearAllMocks); - it('renders initial state', () => { - const items = wrapper.find(DropdownItem); + it('renders initial state', async () => { + const { user } = setUp(); + const btn = screen.getByRole('button'); - expect(wrapper.prop('text')).toEqual('Format (svg)'); - expect(items.at(0).prop('active')).toEqual(false); - expect(items.at(1).prop('active')).toEqual(true); + expect(btn).toHaveTextContent('Format (svg'); + await user.click(btn); + const items = screen.getAllByRole('menuitem'); + + expect(items[0]).not.toHaveClass('active'); + expect(items[1]).toHaveClass('active'); }); - it('invokes callback when items are clicked', () => { - const items = wrapper.find(DropdownItem); + it('invokes callback when items are clicked', async () => { + const { user } = setUp(); + const clickItem = async (name: string) => { + await user.click(screen.getByRole('button')); + await user.click(screen.getByRole('menuitem', { name })); + }; expect(setFormat).not.toHaveBeenCalled(); - items.at(0).simulate('click'); + await clickItem('PNG'); expect(setFormat).toHaveBeenCalledWith('png'); - items.at(1).simulate('click'); + await clickItem('SVG'); expect(setFormat).toHaveBeenCalledWith('svg'); }); }); diff --git a/test/short-urls/reducers/shortUrlCreation.test.ts b/test/short-urls/reducers/shortUrlCreation.test.ts index 002cb368..25128bc4 100644 --- a/test/short-urls/reducers/shortUrlCreation.test.ts +++ b/test/short-urls/reducers/shortUrlCreation.test.ts @@ -9,7 +9,7 @@ import reducer, { CreateShortUrlAction, } from '../../../src/short-urls/reducers/shortUrlCreation'; import { ShortUrl } from '../../../src/short-urls/data'; -import ShlinkApiClient from '../../../src/api/services/ShlinkApiClient'; +import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; import { ShlinkState } from '../../../src/container/types'; describe('shortUrlCreationReducer', () => { diff --git a/test/short-urls/reducers/shortUrlDeletion.test.ts b/test/short-urls/reducers/shortUrlDeletion.test.ts index 588dc790..0eb61b25 100644 --- a/test/short-urls/reducers/shortUrlDeletion.test.ts +++ b/test/short-urls/reducers/shortUrlDeletion.test.ts @@ -8,7 +8,7 @@ import reducer, { deleteShortUrl, } from '../../../src/short-urls/reducers/shortUrlDeletion'; import { ProblemDetailsError } from '../../../src/api/types'; -import ShlinkApiClient from '../../../src/api/services/ShlinkApiClient'; +import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; describe('shortUrlDeletionReducer', () => { describe('reducer', () => { diff --git a/test/short-urls/reducers/shortUrlDetail.test.ts b/test/short-urls/reducers/shortUrlDetail.test.ts index d83d035a..2a6b9df7 100644 --- a/test/short-urls/reducers/shortUrlDetail.test.ts +++ b/test/short-urls/reducers/shortUrlDetail.test.ts @@ -7,7 +7,7 @@ import reducer, { ShortUrlDetailAction, } from '../../../src/short-urls/reducers/shortUrlDetail'; import { ShortUrl } from '../../../src/short-urls/data'; -import ShlinkApiClient from '../../../src/api/services/ShlinkApiClient'; +import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; import { ShlinkState } from '../../../src/container/types'; import { ShortUrlsList } from '../../../src/short-urls/reducers/shortUrlsList'; diff --git a/test/short-urls/reducers/shortUrlsList.test.ts b/test/short-urls/reducers/shortUrlsList.test.ts index 4aac6f92..fbff5de0 100644 --- a/test/short-urls/reducers/shortUrlsList.test.ts +++ b/test/short-urls/reducers/shortUrlsList.test.ts @@ -8,7 +8,7 @@ import reducer, { import { SHORT_URL_DELETED } from '../../../src/short-urls/reducers/shortUrlDeletion'; import { CREATE_VISITS } from '../../../src/visits/reducers/visitCreation'; import { ShortUrl } from '../../../src/short-urls/data'; -import ShlinkApiClient from '../../../src/api/services/ShlinkApiClient'; +import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; import { ShlinkPaginator, ShlinkShortUrlsResponse } from '../../../src/api/types'; import { CREATE_SHORT_URL } from '../../../src/short-urls/reducers/shortUrlCreation'; import { SHORT_URL_EDITED } from '../../../src/short-urls/reducers/shortUrlEdition'; diff --git a/test/tags/TagCard.test.tsx b/test/tags/TagCard.test.tsx index 94189d12..18dcb66c 100644 --- a/test/tags/TagCard.test.tsx +++ b/test/tags/TagCard.test.tsx @@ -1,69 +1,57 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import { Link } from 'react-router-dom'; +import { screen } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; import { Mock } from 'ts-mockery'; -import createTagCard from '../../src/tags/TagCard'; -import TagBullet from '../../src/tags/helpers/TagBullet'; -import ColorGenerator from '../../src/utils/services/ColorGenerator'; +import { TagCard as createTagCard } from '../../src/tags/TagCard'; import { ReachableServer } from '../../src/servers/data'; +import { renderWithEvents } from '../__helpers__/setUpTest'; +import { colorGeneratorMock } from '../utils/services/__mocks__/ColorGenerator.mock'; describe('', () => { - let wrapper: ShallowWrapper; - const DeleteTagConfirmModal = jest.fn(); - const EditTagModal = jest.fn(); - const TagCard = createTagCard(DeleteTagConfirmModal, EditTagModal, Mock.all()); - const createWrapper = (tag = 'ssr') => { - wrapper = shallow( + const TagCard = createTagCard( + ({ isOpen }) => DeleteTagConfirmModal {isOpen ? '[Open]' : '[Closed]'}, + ({ isOpen }) => EditTagModal {isOpen ? '[Open]' : '[Closed]'}, + colorGeneratorMock, + ); + const setUp = (tag = 'ssr') => renderWithEvents( + ({ id: '1' })} displayed toggle={() => {}} - />, - ); + /> + , + ); - return wrapper; - }; - - beforeEach(() => createWrapper()); - - afterEach(() => wrapper.unmount()); afterEach(jest.resetAllMocks); it.each([ - ['ssr', '/server/1/list-short-urls/1?tags=ssr'], - ['ssr-&-foo', '/server/1/list-short-urls/1?tags=ssr-%26-foo'], - ])('shows a TagBullet and a link to the list filtering by the tag', (tag, expectedLink) => { - const wrapper = createWrapper(tag); - const links = wrapper.find(Link); - const bullet = wrapper.find(TagBullet); + ['ssr', '/server/1/list-short-urls/1?tags=ssr', '/server/1/tag/ssr/visits'], + ['ssr-&-foo', '/server/1/list-short-urls/1?tags=ssr-%26-foo', '/server/1/tag/ssr-&-foo/visits'], + ])('shows expected links for provided tags', (tag, shortUrlsLink, visitsLink) => { + setUp(tag); - expect(links.at(0).prop('to')).toEqual(expectedLink); - expect(bullet.prop('tag')).toEqual(tag); + expect(screen.getByText('48').parentNode).toHaveAttribute('href', shortUrlsLink); + expect(screen.getByText('23,257').parentNode).toHaveAttribute('href', visitsLink); }); - it('displays delete modal when delete btn is clicked', () => { - const delBtn = wrapper.find('.tag-card__btn').at(0); + it('displays delete modal when delete btn is clicked', async () => { + const { user } = setUp(); - expect(wrapper.find(DeleteTagConfirmModal).prop('isOpen')).toEqual(false); - delBtn.simulate('click'); - expect(wrapper.find(DeleteTagConfirmModal).prop('isOpen')).toEqual(true); + expect(screen.getByText(/^DeleteTagConfirmModal/)).not.toHaveTextContent('[Open]'); + expect(screen.getByText(/^DeleteTagConfirmModal/)).toHaveTextContent('[Closed]'); + await user.click(screen.getByLabelText('Delete tag')); + expect(screen.getByText(/^DeleteTagConfirmModal/)).toHaveTextContent('[Open]'); + expect(screen.getByText(/^DeleteTagConfirmModal/)).not.toHaveTextContent('[Closed]'); }); - it('displays edit modal when edit btn is clicked', () => { - const editBtn = wrapper.find('.tag-card__btn').at(1); + it('displays edit modal when edit btn is clicked', async () => { + const { user } = setUp(); - expect(wrapper.find(EditTagModal).prop('isOpen')).toEqual(false); - editBtn.simulate('click'); - expect(wrapper.find(EditTagModal).prop('isOpen')).toEqual(true); - }); - - it('shows expected tag stats', () => { - const links = wrapper.find(Link); - - expect(links).toHaveLength(2); - expect(links.at(0).prop('to')).toEqual('/server/1/list-short-urls/1?tags=ssr'); - expect(links.at(0).text()).toContain('48'); - expect(links.at(1).prop('to')).toEqual('/server/1/tag/ssr/visits'); - expect(links.at(1).text()).toContain('23,257'); + expect(screen.getByText(/^EditTagModal/)).not.toHaveTextContent('[Open]'); + expect(screen.getByText(/^EditTagModal/)).toHaveTextContent('[Closed]'); + await user.click(screen.getByLabelText('Edit tag')); + expect(screen.getByText(/^EditTagModal/)).toHaveTextContent('[Open]'); + expect(screen.getByText(/^EditTagModal/)).not.toHaveTextContent('[Closed]'); }); }); diff --git a/test/tags/TagsCards.test.tsx b/test/tags/TagsCards.test.tsx index e41e7d5e..36d6f286 100644 --- a/test/tags/TagsCards.test.tsx +++ b/test/tags/TagsCards.test.tsx @@ -1,37 +1,26 @@ -import { shallow, ShallowWrapper } from 'enzyme'; +import { screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; import { TagsCards as createTagsCards } from '../../src/tags/TagsCards'; import { SelectedServer } from '../../src/servers/data'; import { rangeOf } from '../../src/utils/utils'; import { NormalizedTag } from '../../src/tags/data'; +import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { const amountOfTags = 10; const sortedTags = rangeOf(amountOfTags, (i) => Mock.of({ tag: `tag_${i}` })); - const TagCard = () => null; - const TagsCards = createTagsCards(TagCard); - let wrapper: ShallowWrapper; - - beforeEach(() => { - wrapper = shallow(()} />); - }); - - afterEach(() => wrapper?.unmount()); + const TagsCards = createTagsCards(() => TagCard); + const setUp = () => renderWithEvents( + ()} />, + ); it('renders the proper amount of groups and cards based on the amount of tags', () => { + const { container } = setUp(); const amountOfGroups = 4; - const cards = wrapper.find(TagCard); - const groups = wrapper.find('.col-md-6'); + const cards = screen.getAllByText('TagCard'); + const groups = container.querySelectorAll('.col-md-6'); expect(cards).toHaveLength(amountOfTags); expect(groups).toHaveLength(amountOfGroups); }); - - it('displays card on toggle', () => { - const card = () => wrapper.find(TagCard).at(5); - - expect(card().prop('displayed')).toEqual(false); - (card().prop('toggle') as Function)(); - expect(card().prop('displayed')).toEqual(true); - }); }); diff --git a/test/tags/TagsList.test.tsx b/test/tags/TagsList.test.tsx index d642a060..03caedcd 100644 --- a/test/tags/TagsList.test.tsx +++ b/test/tags/TagsList.test.tsx @@ -1,116 +1,68 @@ -import { shallow, ShallowWrapper } from 'enzyme'; +import { screen, waitFor } from '@testing-library/react'; import { identity } from 'ramda'; import { Mock } from 'ts-mockery'; -import createTagsList, { TagsListProps } from '../../src/tags/TagsList'; -import Message from '../../src/utils/Message'; +import { TagsList as createTagsList, TagsListProps } from '../../src/tags/TagsList'; import { TagsList } from '../../src/tags/reducers/tagsList'; import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub'; -import { Result } from '../../src/utils/Result'; -import { TagsModeDropdown } from '../../src/tags/TagsModeDropdown'; -import { SearchField } from '../../src/utils/SearchField'; import { Settings } from '../../src/settings/reducers/settings'; -import { TagsOrderableFields } from '../../src/tags/data/TagsListChildrenProps'; -import { OrderingDropdown } from '../../src/utils/OrderingDropdown'; +import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { - let wrapper: ShallowWrapper; const filterTags = jest.fn(); - const TagsCards = () => null; - const TagsTable = () => null; - const TagsListComp = createTagsList(TagsCards, TagsTable); - const createWrapper = (tagsList: Partial) => { - wrapper = shallow( - ()} - {...Mock.of({ mercureInfo: {} })} - forceListTags={identity} - filterTags={filterTags} - tagsList={Mock.of(tagsList)} - settings={Mock.all()} - />, - ).dive(); // Dive is needed as this component is wrapped in a HOC + const TagsListComp = createTagsList(() => <>TagsCards, () => <>TagsTable); + const setUp = (tagsList: Partial) => renderWithEvents( + ()} + {...Mock.of({ mercureInfo: {} })} + forceListTags={identity} + filterTags={filterTags} + tagsList={Mock.of(tagsList)} + settings={Mock.all()} + />, + ); - return wrapper; - }; - - afterEach(() => wrapper?.unmount()); afterEach(jest.clearAllMocks); it('shows a loading message when tags are being loaded', () => { - const wrapper = createWrapper({ loading: true }); - const loadingMsg = wrapper.find(Message); - const searchField = wrapper.find(SearchField); + setUp({ loading: true }); - expect(loadingMsg).toHaveLength(1); - expect(loadingMsg.html()).toContain('Loading...'); - expect(searchField).toHaveLength(0); + expect(screen.getByText('Loading...')).toBeInTheDocument(); + expect(screen.queryByText('Error loading tags :(')).not.toBeInTheDocument(); }); it('shows an error when tags failed to be loaded', () => { - const wrapper = createWrapper({ error: true }); - const errorMsg = wrapper.find(Result).filterWhere((result) => result.prop('type') === 'error'); - const searchField = wrapper.find(SearchField); + setUp({ error: true }); - expect(errorMsg).toHaveLength(1); - expect(errorMsg.html()).toContain('Error loading tags :('); - expect(searchField).toHaveLength(0); + expect(screen.getByText('Error loading tags :(')).toBeInTheDocument(); + expect(screen.queryByText('Loading')).not.toBeInTheDocument(); }); it('shows a message when the list of tags is empty', () => { - const wrapper = createWrapper({ filteredTags: [] }); - const msg = wrapper.find(Message); + setUp({ filteredTags: [] }); - expect(msg).toHaveLength(1); - expect(msg.html()).toContain('No tags found'); + expect(screen.getByText('No tags found')).toBeInTheDocument(); + expect(screen.queryByText('Error loading tags :(')).not.toBeInTheDocument(); + expect(screen.queryByText('Loading')).not.toBeInTheDocument(); }); - it('renders proper component based on the display mode', () => { - const wrapper = createWrapper({ filteredTags: ['foo', 'bar'], stats: {} }); + it('renders proper component based on the display mode', async () => { + const { user } = setUp({ filteredTags: ['foo', 'bar'], stats: {} }); - expect(wrapper.find(TagsCards)).toHaveLength(1); - expect(wrapper.find(TagsTable)).toHaveLength(0); + expect(screen.getByText('TagsCards')).toBeInTheDocument(); + expect(screen.queryByText('TagsTable')).not.toBeInTheDocument(); - wrapper.find(TagsModeDropdown).simulate('change'); + await user.click(screen.getByRole('button', { name: /^Display mode/ })); + await user.click(screen.getByRole('menuitem', { name: /List/ })); - expect(wrapper.find(TagsCards)).toHaveLength(0); - expect(wrapper.find(TagsTable)).toHaveLength(1); + expect(screen.queryByText('TagsCards')).not.toBeInTheDocument(); + expect(screen.getByText('TagsTable')).toBeInTheDocument(); }); - it('triggers tags filtering when search field changes', () => { - const wrapper = createWrapper({ filteredTags: [] }); - const searchField = wrapper.find(SearchField); + it('triggers tags filtering when search field changes', async () => { + const { user } = setUp({ filteredTags: [] }); - expect(searchField).toHaveLength(1); expect(filterTags).not.toHaveBeenCalled(); - searchField.simulate('change'); - expect(filterTags).toHaveBeenCalledTimes(1); - }); - - it('triggers ordering when sorting dropdown changes', () => { - const wrapper = createWrapper({ filteredTags: [] }); - - expect(wrapper.find(OrderingDropdown).prop('order')).toEqual({}); - wrapper.find(OrderingDropdown).simulate('change', 'tag', 'DESC'); - expect(wrapper.find(OrderingDropdown).prop('order')).toEqual({ field: 'tag', dir: 'DESC' }); - wrapper.find(OrderingDropdown).simulate('change', 'visits', 'ASC'); - expect(wrapper.find(OrderingDropdown).prop('order')).toEqual({ field: 'visits', dir: 'ASC' }); - }); - - it('can update current order via orderByColumn from table component', () => { - const wrapper = createWrapper({ filteredTags: ['foo', 'bar'], stats: {} }); - const callOrderBy = (field: TagsOrderableFields) => { - ((wrapper.find(TagsTable).prop('orderByColumn') as Function)(field) as Function)(); - }; - - wrapper.find(TagsModeDropdown).simulate('change'); // Make sure table is rendered - - callOrderBy('visits'); - expect(wrapper.find(TagsTable).prop('currentOrder')).toEqual({ field: 'visits', dir: 'ASC' }); - callOrderBy('visits'); - expect(wrapper.find(TagsTable).prop('currentOrder')).toEqual({ field: 'visits', dir: 'DESC' }); - callOrderBy('tag'); - expect(wrapper.find(TagsTable).prop('currentOrder')).toEqual({ field: 'tag', dir: 'ASC' }); - callOrderBy('shortUrls'); - expect(wrapper.find(TagsTable).prop('currentOrder')).toEqual({ field: 'shortUrls', dir: 'ASC' }); + await user.type(screen.getByPlaceholderText('Search...'), 'Hello'); + await waitFor(() => expect(filterTags).toHaveBeenCalledTimes(1)); }); }); diff --git a/test/tags/TagsModeDropdown.test.tsx b/test/tags/TagsModeDropdown.test.tsx index 97f170b8..9550a04b 100644 --- a/test/tags/TagsModeDropdown.test.tsx +++ b/test/tags/TagsModeDropdown.test.tsx @@ -1,42 +1,34 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import { DropdownItem } from 'reactstrap'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faBars as listIcon, faThLarge as cardsIcon } from '@fortawesome/free-solid-svg-icons'; +import { screen } from '@testing-library/react'; import { TagsModeDropdown } from '../../src/tags/TagsModeDropdown'; -import { DropdownBtn } from '../../src/utils/DropdownBtn'; +import { TagsMode } from '../../src/settings/reducers/settings'; +import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { const onChange = jest.fn(); - let wrapper: ShallowWrapper; - - beforeEach(() => { - wrapper = shallow(); - }); + const setUp = (mode: TagsMode) => renderWithEvents(); afterEach(jest.clearAllMocks); - afterEach(() => wrapper?.unmount()); - it('renders expected items', () => { - const btn = wrapper.find(DropdownBtn); - const items = wrapper.find(DropdownItem); - const icons = wrapper.find(FontAwesomeIcon); - - expect(btn).toHaveLength(1); - expect(btn.prop('text')).toEqual('Display mode: list'); - expect(items).toHaveLength(2); - expect(icons).toHaveLength(2); - expect(icons.first().prop('icon')).toEqual(cardsIcon); - expect(icons.last().prop('icon')).toEqual(listIcon); + it.each([ + ['cards' as TagsMode], + ['list' as TagsMode], + ])('renders expected initial value', (mode) => { + setUp(mode); + expect(screen.getByRole('button')).toHaveTextContent(`Display mode: ${mode}`); }); - it('changes active element on click', () => { - const items = wrapper.find(DropdownItem); + it('changes active element on click', async () => { + const { user } = setUp('list'); + const clickItem = async (index: 0 | 1) => { + await user.click(screen.getByRole('button')); + await user.click(screen.getAllByRole('menuitem')[index]); + }; expect(onChange).not.toHaveBeenCalled(); - items.first().simulate('click'); + await clickItem(0); expect(onChange).toHaveBeenCalledWith('cards'); - items.last().simulate('click'); + await clickItem(1); expect(onChange).toHaveBeenCalledWith('list'); }); }); diff --git a/test/tags/TagsTable.test.tsx b/test/tags/TagsTable.test.tsx index 130bf71f..aae618ec 100644 --- a/test/tags/TagsTable.test.tsx +++ b/test/tags/TagsTable.test.tsx @@ -1,24 +1,21 @@ +import { screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; -import { shallow, ShallowWrapper } from 'enzyme'; import { useLocation } from 'react-router-dom'; import { TagsTable as createTagsTable } from '../../src/tags/TagsTable'; import { SelectedServer } from '../../src/servers/data'; import { rangeOf } from '../../src/utils/utils'; -import { SimplePaginator } from '../../src/common/SimplePaginator'; import { NormalizedTag } from '../../src/tags/data'; +import { renderWithEvents } from '../__helpers__/setUpTest'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useLocation: jest.fn() })); describe('', () => { - const TagsTableRow = () => null; const orderByColumn = jest.fn(); - const TagsTable = createTagsTable(TagsTableRow); + const TagsTable = createTagsTable(({ tag }) => TagsTableRow [{tag.tag}]); const tags = (amount: number) => rangeOf(amount, (i) => `tag_${i}`); - let wrapper: ShallowWrapper; - const createWrapper = (sortedTags: string[] = [], search = '') => { + const setUp = (sortedTags: string[] = [], search = '') => { (useLocation as any).mockReturnValue({ search }); - - wrapper = shallow( + return renderWithEvents( Mock.of({ tag }))} selectedServer={Mock.all()} @@ -26,20 +23,15 @@ describe('', () => { orderByColumn={() => orderByColumn} />, ); - - return wrapper; }; afterEach(jest.clearAllMocks); - afterEach(() => wrapper?.unmount()); it('renders empty result if there are no tags', () => { - const wrapper = createWrapper(); - const regularRows = wrapper.find('tbody').find('tr'); - const tagRows = wrapper.find(TagsTableRow); + setUp(); - expect(regularRows).toHaveLength(1); - expect(tagRows).toHaveLength(0); + expect(screen.queryByText(/^TagsTableRow/)).not.toBeInTheDocument(); + expect(screen.getByText('No results found')).toBeInTheDocument(); }); it.each([ @@ -50,10 +42,10 @@ describe('', () => { [tags(30), 20], [tags(100), 20], ])('renders as many rows as there are in current page', (filteredTags, expectedRows) => { - const wrapper = createWrapper(filteredTags); - const tagRows = wrapper.find(TagsTableRow); + setUp(filteredTags); - expect(tagRows).toHaveLength(expectedRows); + expect(screen.getAllByText(/^TagsTableRow/)).toHaveLength(expectedRows); + expect(screen.queryByText('No results found')).not.toBeInTheDocument(); }); it.each([ @@ -64,10 +56,8 @@ describe('', () => { [tags(30), 1], [tags(100), 1], ])('renders paginator if there are more than one page', (filteredTags, expectedPaginators) => { - const wrapper = createWrapper(filteredTags); - const paginator = wrapper.find(SimplePaginator); - - expect(paginator).toHaveLength(expectedPaginators); + const { container } = setUp(filteredTags); + expect(container.querySelectorAll('.sticky-card-paginator')).toHaveLength(expectedPaginators); }); it.each([ @@ -78,30 +68,30 @@ describe('', () => { [5, 7, 80], [6, 0, 0], ])('renders page from query if present', (page, expectedRows, offset) => { - const wrapper = createWrapper(tags(87), `page=${page}`); - const tagRows = wrapper.find(TagsTableRow); + setUp(tags(87), `page=${page}`); + + const tagRows = screen.queryAllByText(/^TagsTableRow/); expect(tagRows).toHaveLength(expectedRows); - tagRows.forEach((row, index) => { - expect(row.prop('tag')).toEqual(expect.objectContaining({ tag: `tag_${index + offset + 1}` })); - }); + tagRows.forEach((row, index) => expect(row).toHaveTextContent(`[tag_${index + offset + 1}]`)); }); - it('allows changing current page in paginator', () => { - const wrapper = createWrapper(tags(100)); + it('allows changing current page in paginator', async () => { + const { user, container } = setUp(tags(100)); - expect(wrapper.find(SimplePaginator).prop('currentPage')).toEqual(1); - (wrapper.find(SimplePaginator).prop('setCurrentPage') as Function)(5); - expect(wrapper.find(SimplePaginator).prop('currentPage')).toEqual(5); + expect(container.querySelector('.active')).toHaveTextContent('1'); + await user.click(screen.getByText('5')); + expect(container.querySelector('.active')).toHaveTextContent('5'); }); - it('orders tags when column is clicked', () => { - const wrapper = createWrapper(tags(100)); + it('orders tags when column is clicked', async () => { + const { user } = setUp(tags(100)); + const headers = screen.getAllByRole('columnheader'); expect(orderByColumn).not.toHaveBeenCalled(); - wrapper.find('thead').find('th').first().simulate('click'); - wrapper.find('thead').find('th').at(2).simulate('click'); - wrapper.find('thead').find('th').at(1).simulate('click'); + await user.click(headers[0]); + await user.click(headers[2]); + await user.click(headers[1]); expect(orderByColumn).toHaveBeenCalledTimes(3); }); }); diff --git a/test/tags/TagsTableRow.test.tsx b/test/tags/TagsTableRow.test.tsx index bd8c5191..4974f621 100644 --- a/test/tags/TagsTableRow.test.tsx +++ b/test/tags/TagsTableRow.test.tsx @@ -1,75 +1,72 @@ -import { shallow, ShallowWrapper } from 'enzyme'; +import { screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; -import { Link } from 'react-router-dom'; -import { DropdownItem } from 'reactstrap'; +import { MemoryRouter } from 'react-router-dom'; import { TagsTableRow as createTagsTableRow } from '../../src/tags/TagsTableRow'; import { ReachableServer } from '../../src/servers/data'; -import ColorGenerator from '../../src/utils/services/ColorGenerator'; -import { DropdownBtnMenu } from '../../src/utils/DropdownBtnMenu'; +import { renderWithEvents } from '../__helpers__/setUpTest'; +import { colorGeneratorMock } from '../utils/services/__mocks__/ColorGenerator.mock'; describe('', () => { - const DeleteTagConfirmModal = () => null; - const EditTagModal = () => null; - const TagsTableRow = createTagsTableRow(DeleteTagConfirmModal, EditTagModal, Mock.all()); - let wrapper: ShallowWrapper; - const createWrapper = (tagStats?: { visits?: number; shortUrls?: number }) => { - wrapper = shallow( - ({ id: 'abc123' })} - />, - ); - - return wrapper; - }; - - afterEach(() => wrapper?.unmount()); + const TagsTableRow = createTagsTableRow( + ({ isOpen }) => DeleteTagConfirmModal {isOpen ? 'OPEN' : 'CLOSED'}, + ({ isOpen }) => EditTagModal {isOpen ? 'OPEN' : 'CLOSED'}, + colorGeneratorMock, + ); + const setUp = (tagStats?: { visits?: number; shortUrls?: number }) => renderWithEvents( + + + + ({ id: 'abc123' })} + /> + +
+
, + ); it.each([ [undefined, '0', '0'], [{ shortUrls: 10, visits: 3480 }, '10', '3,480'], ])('shows expected tag stats', (stats, expectedShortUrls, expectedVisits) => { - const wrapper = createWrapper(stats); - const links = wrapper.find(Link); - const shortUrlsLink = links.first(); - const visitsLink = links.last(); + setUp(stats); - expect(shortUrlsLink.prop('children')).toEqual(expectedShortUrls); - expect(shortUrlsLink.prop('to')).toEqual(`/server/abc123/list-short-urls/1?tags=${encodeURIComponent('foo&bar')}`); - expect(visitsLink.prop('children')).toEqual(expectedVisits); - expect(visitsLink.prop('to')).toEqual('/server/abc123/tag/foo&bar/visits'); + const [shortUrlsLink, visitsLink] = screen.getAllByRole('link'); + + expect(shortUrlsLink).toHaveTextContent(expectedShortUrls); + expect(shortUrlsLink).toHaveAttribute( + 'href', + `/server/abc123/list-short-urls/1?tags=${encodeURIComponent('foo&bar')}`, + ); + expect(visitsLink).toHaveTextContent(expectedVisits); + expect(visitsLink).toHaveAttribute('href', '/server/abc123/tag/foo&bar/visits'); }); - it('allows toggling dropdown menu', () => { - const wrapper = createWrapper(); + it('allows toggling dropdown menu', async () => { + const { user } = setUp(); - expect(wrapper.find(DropdownBtnMenu).prop('isOpen')).toEqual(false); - (wrapper.find(DropdownBtnMenu).prop('toggle') as Function)(); - expect(wrapper.find(DropdownBtnMenu).prop('isOpen')).toEqual(true); + expect(screen.queryByRole('menu')).not.toBeInTheDocument(); + await user.click(screen.getByRole('button')); + expect(screen.queryByRole('menu')).toBeInTheDocument(); }); - it('allows toggling modals through dropdown items', () => { - const wrapper = createWrapper(); - const items = wrapper.find(DropdownItem); + it('allows toggling modals through dropdown items', async () => { + const { user } = setUp(); + const clickItemOnIndex = async (index: 0 | 1) => { + await user.click(screen.getByRole('button')); + await user.click(screen.getAllByRole('menuitem')[index]); + }; - expect(wrapper.find(EditTagModal).prop('isOpen')).toEqual(false); - items.first().simulate('click'); - expect(wrapper.find(EditTagModal).prop('isOpen')).toEqual(true); + expect(screen.getByText(/^EditTagModal/)).toHaveTextContent('CLOSED'); + expect(screen.getByText(/^EditTagModal/)).not.toHaveTextContent('OPEN'); + await clickItemOnIndex(0); + expect(screen.getByText(/^EditTagModal/)).toHaveTextContent('OPEN'); + expect(screen.getByText(/^EditTagModal/)).not.toHaveTextContent('CLOSED'); - expect(wrapper.find(DeleteTagConfirmModal).prop('isOpen')).toEqual(false); - items.last().simulate('click'); - expect(wrapper.find(DeleteTagConfirmModal).prop('isOpen')).toEqual(true); - }); - - it('allows toggling modals through the modals themselves', () => { - const wrapper = createWrapper(); - - expect(wrapper.find(EditTagModal).prop('isOpen')).toEqual(false); - (wrapper.find(EditTagModal).prop('toggle') as Function)(); - expect(wrapper.find(EditTagModal).prop('isOpen')).toEqual(true); - - expect(wrapper.find(DeleteTagConfirmModal).prop('isOpen')).toEqual(false); - (wrapper.find(DeleteTagConfirmModal).prop('toggle') as Function)(); - expect(wrapper.find(DeleteTagConfirmModal).prop('isOpen')).toEqual(true); + expect(screen.getByText(/^DeleteTagConfirmModal/)).toHaveTextContent('CLOSED'); + expect(screen.getByText(/^DeleteTagConfirmModal/)).not.toHaveTextContent('OPEN'); + await clickItemOnIndex(1); + expect(screen.getByText(/^DeleteTagConfirmModal/)).toHaveTextContent('OPEN'); + expect(screen.getByText(/^DeleteTagConfirmModal/)).not.toHaveTextContent('CLOSED'); }); }); diff --git a/test/tags/helpers/DeleteTagConfirmModal.test.tsx b/test/tags/helpers/DeleteTagConfirmModal.test.tsx index f9bb7925..8ca667ea 100644 --- a/test/tags/helpers/DeleteTagConfirmModal.test.tsx +++ b/test/tags/helpers/DeleteTagConfirmModal.test.tsx @@ -1,64 +1,56 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import { Button, Modal, ModalBody, ModalFooter } from 'reactstrap'; -import DeleteTagConfirmModal from '../../../src/tags/helpers/DeleteTagConfirmModal'; +import { screen } from '@testing-library/react'; +import { DeleteTagConfirmModal } from '../../../src/tags/helpers/DeleteTagConfirmModal'; import { TagDeletion } from '../../../src/tags/reducers/tagDelete'; +import { renderWithEvents } from '../../__helpers__/setUpTest'; describe('', () => { - let wrapper: ShallowWrapper; const tag = 'nodejs'; const deleteTag = jest.fn(); const tagDeleted = jest.fn(); - const createWrapper = (tagDelete: TagDeletion) => { - wrapper = shallow( - ''} - isOpen - deleteTag={deleteTag} - tagDeleted={tagDeleted} - tagDelete={tagDelete} - />, - ); + const setUp = (tagDelete: TagDeletion) => renderWithEvents( + ''} + isOpen + deleteTag={deleteTag} + tagDeleted={tagDeleted} + tagDelete={tagDelete} + />, + ); - return wrapper; - }; - - afterEach(() => wrapper?.unmount()); afterEach(jest.resetAllMocks); it('asks confirmation for provided tag to be deleted', () => { - wrapper = createWrapper({ error: false, deleting: false }); - const body = wrapper.find(ModalBody); - const footer = wrapper.find(ModalFooter); - const delBtn = footer.find(Button).last(); + setUp({ error: false, deleting: false }); - expect(body.html()).toContain(`Are you sure you want to delete tag ${tag}?`); - expect(delBtn.prop('disabled')).toEqual(false); - expect(delBtn.html()).toContain('>Delete tag<'); + const delBtn = screen.getByRole('button', { name: 'Delete tag' }); + + expect(screen.getByText(/^Are you sure you want to delete tag/)).toBeInTheDocument(); + expect(screen.queryByText('Something went wrong while deleting the tag :(')).not.toBeInTheDocument(); + expect(delBtn).toBeInTheDocument(); + expect(delBtn).not.toHaveClass('disabled'); + expect(delBtn).not.toHaveAttribute('disabled'); }); it('shows error message when deletion failed', () => { - wrapper = createWrapper({ error: true, deleting: false }); - const body = wrapper.find(ModalBody); - - expect(body.html()).toContain('Something went wrong while deleting the tag :('); + setUp({ error: true, deleting: false }); + expect(screen.getByText('Something went wrong while deleting the tag :(')).toBeInTheDocument(); }); it('shows loading status while deleting', () => { - wrapper = createWrapper({ error: false, deleting: true }); - const footer = wrapper.find(ModalFooter); - const delBtn = footer.find(Button).last(); + setUp({ error: false, deleting: true }); - expect(delBtn.prop('disabled')).toEqual(true); - expect(delBtn.html()).toContain('>Deleting tag...<'); + const delBtn = screen.getByRole('button', { name: 'Deleting tag...' }); + + expect(delBtn).toBeInTheDocument(); + expect(delBtn).toHaveClass('disabled'); + expect(delBtn).toHaveAttribute('disabled'); }); - it('deletes tag modal when btn is clicked', async () => { - wrapper = createWrapper({ error: false, deleting: true }); - const footer = wrapper.find(ModalFooter); - const delBtn = footer.find(Button).last(); + it('hides tag modal when btn is clicked', async () => { + const { user } = setUp({ error: false, deleting: false }); - await delBtn.simulate('click'); + await user.click(screen.getByRole('button', { name: 'Delete tag' })); expect(deleteTag).toHaveBeenCalledTimes(1); expect(deleteTag).toHaveBeenCalledWith(tag); @@ -66,11 +58,11 @@ describe('', () => { expect(tagDeleted).toHaveBeenCalledWith(tag); }); - it('does no further actions when modal is closed without deleting tag', () => { - wrapper = createWrapper({ error: false, deleting: false }); - const modal = wrapper.find(Modal); + it('does no further actions when modal is closed without deleting tag', async () => { + const { user } = setUp({ error: false, deleting: false }); + + await user.click(screen.getByLabelText('Close')); - modal.simulate('closed'); expect(deleteTag).not.toHaveBeenCalled(); expect(tagDeleted).not.toHaveBeenCalled(); }); diff --git a/test/tags/helpers/EditTagModal.test.tsx b/test/tags/helpers/EditTagModal.test.tsx index 266f5d7a..d3f11979 100644 --- a/test/tags/helpers/EditTagModal.test.tsx +++ b/test/tags/helpers/EditTagModal.test.tsx @@ -1,46 +1,34 @@ -import { shallow, ShallowWrapper } from 'enzyme'; +import { screen, waitFor } from '@testing-library/react'; import { Mock } from 'ts-mockery'; -import { Button, Input, Modal, ModalHeader, Popover } from 'reactstrap'; -import { HexColorPicker } from 'react-colorful'; import { TagEdition } from '../../../src/tags/reducers/tagEdit'; -import createEditTagModal from '../../../src/tags/helpers/EditTagModal'; -import ColorGenerator from '../../../src/utils/services/ColorGenerator'; -import { Result } from '../../../src/utils/Result'; +import { EditTagModal as createEditTagModal } from '../../../src/tags/helpers/EditTagModal'; +import { ColorGenerator } from '../../../src/utils/services/ColorGenerator'; import { ProblemDetailsError } from '../../../src/api/types'; -import { ShlinkApiError } from '../../../src/api/ShlinkApiError'; +import { renderWithEvents } from '../../__helpers__/setUpTest'; describe('', () => { - const EditTagModal = createEditTagModal(Mock.of({ getColorForKey: jest.fn(() => 'red') })); + const EditTagModal = createEditTagModal(Mock.of({ getColorForKey: jest.fn(() => 'green') })); const editTag = jest.fn().mockReturnValue(Promise.resolve()); const tagEdited = jest.fn().mockReturnValue(Promise.resolve()); const toggle = jest.fn(); - let wrapper: ShallowWrapper; - const createWrapper = (tagEdit: Partial = {}) => { + const setUp = (tagEdit: Partial = {}) => { const edition = Mock.of(tagEdit); - - wrapper = shallow( + return renderWithEvents( , ); - - return wrapper; }; afterEach(jest.clearAllMocks); - afterEach(() => wrapper?.unmount()); - it('allows modal to be toggled with different mechanisms', () => { - const wrapper = createWrapper(); - const modal = wrapper.find(Modal); - const modalHeader = wrapper.find(ModalHeader); - const cancelBtn = wrapper.find(Button).findWhere((btn) => btn.prop('type') === 'button'); + it('allows modal to be toggled with different mechanisms', async () => { + const { user } = setUp(); expect(toggle).not.toHaveBeenCalled(); - (modal.prop('toggle') as Function)(); - (modalHeader.prop('toggle') as Function)(); - cancelBtn.simulate('click'); + await user.click(screen.getByLabelText('Close')); + await user.click(screen.getByRole('button', { name: 'Cancel' })); - expect(toggle).toHaveBeenCalledTimes(3); + expect(toggle).toHaveBeenCalledTimes(2); expect(editTag).not.toHaveBeenCalled(); expect(tagEdited).not.toHaveBeenCalled(); }); @@ -48,62 +36,54 @@ describe('', () => { it.each([ [true, 'Saving...'], [false, 'Save'], - ])('renders submit button in expected state', (editing, expectedText) => { - const wrapper = createWrapper({ editing }); - const submitBtn = wrapper.find(Button).findWhere((btn) => btn.prop('color') === 'primary'); - - expect(submitBtn.html()).toContain(expectedText); - expect(submitBtn.prop('disabled')).toEqual(editing); + ])('renders submit button in expected state', (editing, name) => { + setUp({ editing }); + expect(screen.getByRole('button', { name })).toBeInTheDocument(); }); it.each([ [true, 1], [false, 0], ])('displays error result in case of error', (error, expectedResultCount) => { - const wrapper = createWrapper({ error, errorData: Mock.all() }); - const result = wrapper.find(Result); - const apiError = wrapper.find(ShlinkApiError); - - expect(result).toHaveLength(expectedResultCount); - expect(apiError).toHaveLength(expectedResultCount); + setUp({ error, errorData: Mock.all() }); + expect(screen.queryAllByText('Something went wrong while editing the tag :(')).toHaveLength(expectedResultCount); }); - it('updates tag value when text changes', () => { - const wrapper = createWrapper(); + it('updates tag value when text changes', async () => { + const { user } = setUp(); + const getInput = () => screen.getByPlaceholderText('Tag'); - expect(wrapper.find(Input).prop('value')).toEqual('foo'); - wrapper.find(Input).simulate('change', { target: { value: 'bar' } }); - expect(wrapper.find(Input).prop('value')).toEqual('bar'); + expect(getInput()).toHaveValue('foo'); + await user.clear(getInput()); + await user.type(getInput(), 'bar'); + expect(getInput()).toHaveValue('bar'); }); it('invokes all functions on form submit', async () => { - const wrapper = createWrapper(); - const form = wrapper.find('form'); + const { user } = setUp(); expect(editTag).not.toHaveBeenCalled(); expect(tagEdited).not.toHaveBeenCalled(); - await form.simulate('submit', { preventDefault: jest.fn() }); + await user.click(screen.getByRole('button', { name: 'Save' })); expect(editTag).toHaveBeenCalled(); expect(tagEdited).toHaveBeenCalled(); }); - it('changes color when changing on color picker', () => { - const wrapper = createWrapper(); + it('changes color when changing on color picker', async () => { + const { user } = setUp(); + const colorBtn = screen.getByRole('img', { hidden: true }); + // const initialColor = colorBtn.parentElement?.style.backgroundColor; - expect(wrapper.find(HexColorPicker).prop('color')).toEqual('red'); - wrapper.find(HexColorPicker).simulate('change', 'blue'); - expect(wrapper.find(HexColorPicker).prop('color')).toEqual('blue'); - }); + await user.click(colorBtn); + await waitFor(() => screen.getByRole('tooltip')); + await user.click(screen.getByLabelText('Hue')); + await user.click(screen.getByLabelText('Color')); + await user.click(colorBtn); + await waitFor(() => expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()); - it('allows toggling popover with different mechanisms', () => { - const wrapper = createWrapper(); - - expect(wrapper.find(Popover).prop('isOpen')).toEqual(false); - (wrapper.find(Popover).prop('toggle') as Function)(); - expect(wrapper.find(Popover).prop('isOpen')).toEqual(true); - wrapper.find('div').simulate('click'); - expect(wrapper.find(Popover).prop('isOpen')).toEqual(false); + // I need to figure this one out + // await waitFor(() => expect(initialColor).not.toEqual(colorBtn.parentElement?.style.backgroundColor)); }); }); diff --git a/test/tags/helpers/Tag.test.tsx b/test/tags/helpers/Tag.test.tsx index 53a4bb4b..edd4d20c 100644 --- a/test/tags/helpers/Tag.test.tsx +++ b/test/tags/helpers/Tag.test.tsx @@ -1,9 +1,23 @@ +import { screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; -import { shallow, ShallowWrapper } from 'enzyme'; import { ReactNode } from 'react'; -import ColorGenerator from '../../../src/utils/services/ColorGenerator'; +import { ColorGenerator } from '../../../src/utils/services/ColorGenerator'; import { MAIN_COLOR } from '../../../src/utils/theme'; -import Tag from '../../../src/tags/helpers/Tag'; +import { Tag } from '../../../src/tags/helpers/Tag'; +import { renderWithEvents } from '../../__helpers__/setUpTest'; + +const hexToRgb = (hex: string) => { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + if (!result) { + throw new Error((`Could not convert color ${hex} to RGB`)); + } + + return { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16), + }; +}; describe('', () => { const onClick = jest.fn(); @@ -11,19 +25,13 @@ describe('', () => { const isColorLightForKey = jest.fn(() => false); const getColorForKey = jest.fn(() => MAIN_COLOR); const colorGenerator = Mock.of({ getColorForKey, isColorLightForKey }); - let wrapper: ShallowWrapper; - const createWrapper = (text: string, clearable?: boolean, children?: ReactNode) => { - wrapper = shallow( - - {children} - , - ); - - return wrapper; - }; + const setUp = (text: string, clearable?: boolean, children?: ReactNode) => renderWithEvents( + + {children} + , + ); afterEach(jest.clearAllMocks); - afterEach(() => wrapper?.unmount()); it.each([ [true], @@ -31,9 +39,13 @@ describe('', () => { ])('includes an extra class when the color is light', (isLight) => { isColorLightForKey.mockReturnValue(isLight); - const wrapper = createWrapper('foo'); + const { container } = setUp('foo'); - expect((wrapper.prop('className') as string).includes('tag--light-bg')).toEqual(isLight); + if (isLight) { + expect(container.firstChild).toHaveClass('tag--light-bg'); + } else { + expect(container.firstChild).not.toHaveClass('tag--light-bg'); + } }); it.each([ @@ -45,21 +57,25 @@ describe('', () => { ])('includes generated color as backgroundColor', (generatedColor) => { getColorForKey.mockReturnValue(generatedColor); - const wrapper = createWrapper('foo'); + const { container } = setUp('foo'); + const { r, g, b } = hexToRgb(generatedColor); - expect((wrapper.prop('style') as any).backgroundColor).toEqual(generatedColor); + expect(container.firstChild).toHaveAttribute( + 'style', + expect.stringContaining(`background-color: rgb(${r}, ${g}, ${b})`), + ); }); - it('invokes expected callbacks when appropriate events are triggered', () => { - const wrapper = createWrapper('foo', true); + it('invokes expected callbacks when appropriate events are triggered', async () => { + const { container, user } = setUp('foo', true); - expect(onClick).not.toBeCalled(); - expect(onClose).not.toBeCalled(); + expect(onClick).not.toHaveBeenCalled(); + expect(onClose).not.toHaveBeenCalled(); - wrapper.simulate('click'); + container.firstElementChild && await user.click(container.firstElementChild); expect(onClick).toHaveBeenCalledTimes(1); - wrapper.find('.tag__close-selected-tag').simulate('click'); + await user.click(screen.getByLabelText(/^Remove/)); expect(onClose).toHaveBeenCalledTimes(1); }); @@ -68,18 +84,17 @@ describe('', () => { [false, 0, 'pointer'], [undefined, 0, 'pointer'], ])('includes a close component when the tag is clearable', (clearable, expectedCloseBtnAmount, expectedCursor) => { - const wrapper = createWrapper('foo', clearable); + const { container } = setUp('foo', clearable); - expect(wrapper.find('.tag__close-selected-tag')).toHaveLength(expectedCloseBtnAmount); - expect((wrapper.prop('style') as any).cursor).toEqual(expectedCursor); + expect(screen.queryAllByLabelText(/^Remove/)).toHaveLength(expectedCloseBtnAmount); + expect(container.firstChild).toHaveAttribute('style', expect.stringContaining(`cursor: ${expectedCursor}`)); }); it.each([ [undefined, 'foo'], ['bar', 'bar'], ])('falls back to text as children when no children are provided', (children, expectedChildren) => { - const wrapper = createWrapper('foo', false, children); - - expect(wrapper.html()).toContain(`>${expectedChildren}`); + const { container } = setUp('foo', false, children); + expect(container.firstChild).toHaveTextContent(expectedChildren); }); }); diff --git a/test/tags/helpers/TagsSelector.test.tsx b/test/tags/helpers/TagsSelector.test.tsx index b4d1f6fa..e6bed7ec 100644 --- a/test/tags/helpers/TagsSelector.test.tsx +++ b/test/tags/helpers/TagsSelector.test.tsx @@ -1,77 +1,81 @@ -import { shallow, ShallowWrapper } from 'enzyme'; +import { screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; -import createTagsSelector from '../../../src/tags/helpers/TagsSelector'; -import ColorGenerator from '../../../src/utils/services/ColorGenerator'; +import { TagsSelector as createTagsSelector } from '../../../src/tags/helpers/TagsSelector'; import { TagsList } from '../../../src/tags/reducers/tagsList'; import { Settings } from '../../../src/settings/reducers/settings'; +import { renderWithEvents } from '../../__helpers__/setUpTest'; +import { colorGeneratorMock } from '../../utils/services/__mocks__/ColorGenerator.mock'; describe('', () => { const onChange = jest.fn(); - const TagsSelector = createTagsSelector(Mock.all()); + const TagsSelector = createTagsSelector(colorGeneratorMock); const tags = ['foo', 'bar']; const tagsList = Mock.of({ tags: [...tags, 'baz'] }); - let wrapper: ShallowWrapper; + const setUp = () => renderWithEvents( + ()} + listTags={jest.fn()} + onChange={onChange} + />, + ); - beforeEach(jest.clearAllMocks); - beforeEach(() => { - wrapper = shallow( - ()} - listTags={jest.fn()} - onChange={onChange} - />, - ); - }); + afterEach(jest.clearAllMocks); - afterEach(() => wrapper?.unmount()); - - it('has expected props', () => { - expect(wrapper.prop('placeholderText')).toEqual('Add tags to the URL'); - expect(wrapper.prop('allowNew')).toEqual(true); - expect(wrapper.prop('addOnBlur')).toEqual(true); - expect(wrapper.prop('minQueryLength')).toEqual(1); + it('has an input for tags', () => { + setUp(); + expect(screen.getByPlaceholderText('Add tags to the URL')).toBeInTheDocument(); }); it('contains expected tags', () => { - expect(wrapper.prop('tags')).toEqual([ - { - id: 'foo', - name: 'foo', - }, - { - id: 'bar', - name: 'bar', - }, - ]); + setUp(); + + expect(screen.getByText('foo')).toBeInTheDocument(); + expect(screen.getByText('bar')).toBeInTheDocument(); }); - it('contains expected suggestions', () => { - expect(wrapper.prop('suggestions')).toEqual([ - { - id: 'baz', - name: 'baz', - }, - ]); + it('contains expected suggestions', async () => { + const { container, user } = setUp(); + + expect(container.querySelector('.react-tags__suggestions')).not.toBeInTheDocument(); + expect(screen.queryByText('baz')).not.toBeInTheDocument(); + + await user.type(screen.getByPlaceholderText('Add tags to the URL'), 'ba'); + + expect(container.querySelector('.react-tags__suggestions')).toBeInTheDocument(); + expect(screen.getByText('baz')).toBeInTheDocument(); }); it.each([ ['The-New-Tag', [...tags, 'the-new-tag']], - ['comma,separated,tags', [...tags, 'comma', 'separated', 'tags']], ['foo', tags], - ])('invokes onChange when new tags are added', (newTag, expectedTags) => { - wrapper.simulate('addition', { name: newTag }); + ])('invokes onChange when new tags are added', async (newTag, expectedTags) => { + const { user } = setUp(); + expect(onChange).not.toHaveBeenCalled(); + await user.type(screen.getByPlaceholderText('Add tags to the URL'), newTag); + await user.type(screen.getByPlaceholderText('Add tags to the URL'), '{Enter}'); expect(onChange).toHaveBeenCalledWith(expectedTags); }); - it.each([ - [0, 'bar'], - [1, 'foo'], - ])('invokes onChange when tags are deleted', (index, expected) => { - wrapper.simulate('delete', index); + it('splits tags when several comma-separated ones are pasted', async () => { + const { user } = setUp(); + expect(onChange).not.toHaveBeenCalled(); + await user.click(screen.getByPlaceholderText('Add tags to the URL')); + await user.paste('comma,separated,tags'); + await user.type(screen.getByPlaceholderText('Add tags to the URL'), '{Enter}'); + expect(onChange).toHaveBeenCalledWith([...tags, 'comma', 'separated', 'tags']); + }); + + it.each([ + ['foo', 'bar'], + ['bar', 'foo'], + ])('invokes onChange when tags are deleted', async (removedLabel, expected) => { + const { user } = setUp(); + + await user.click(screen.getByLabelText(`Remove ${removedLabel}`)); expect(onChange).toHaveBeenCalledWith([expected]); }); }); diff --git a/test/tags/reducers/tagDelete.test.ts b/test/tags/reducers/tagDelete.test.ts index c68578a2..a4726bf1 100644 --- a/test/tags/reducers/tagDelete.test.ts +++ b/test/tags/reducers/tagDelete.test.ts @@ -7,7 +7,7 @@ import reducer, { tagDeleted, deleteTag, } from '../../../src/tags/reducers/tagDelete'; -import ShlinkApiClient from '../../../src/api/services/ShlinkApiClient'; +import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; import { ShlinkState } from '../../../src/container/types'; describe('tagDeleteReducer', () => { diff --git a/test/tags/reducers/tagEdit.test.ts b/test/tags/reducers/tagEdit.test.ts index 08fbc1b9..d000b343 100644 --- a/test/tags/reducers/tagEdit.test.ts +++ b/test/tags/reducers/tagEdit.test.ts @@ -8,8 +8,8 @@ import reducer, { editTag, EditTagAction, } from '../../../src/tags/reducers/tagEdit'; -import ShlinkApiClient from '../../../src/api/services/ShlinkApiClient'; -import ColorGenerator from '../../../src/utils/services/ColorGenerator'; +import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; +import { ColorGenerator } from '../../../src/utils/services/ColorGenerator'; import { ShlinkState } from '../../../src/container/types'; describe('tagEditReducer', () => { diff --git a/test/tags/reducers/tagsList.test.ts b/test/tags/reducers/tagsList.test.ts index a0251203..767cdc3d 100644 --- a/test/tags/reducers/tagsList.test.ts +++ b/test/tags/reducers/tagsList.test.ts @@ -67,9 +67,9 @@ describe('tagsListReducer', () => { }); it('filters original list of tags by provided search term on FILTER_TAGS', () => { - const tags = ['foo', 'bar', 'baz', 'foo2', 'fo']; - const searchTerm = 'fo'; - const filteredTags = ['foo', 'foo2', 'fo']; + const tags = ['foo', 'bar', 'baz', 'Foo2', 'fo']; + const searchTerm = 'Fo'; + const filteredTags = ['foo', 'Foo2', 'fo']; expect(reducer(state({ tags }), { type: FILTER_TAGS, searchTerm } as any)).toEqual({ tags, diff --git a/test/utils/Checkbox.test.tsx b/test/utils/Checkbox.test.tsx index e98390f0..d290b319 100644 --- a/test/utils/Checkbox.test.tsx +++ b/test/utils/Checkbox.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import Checkbox from '../../src/utils/Checkbox'; +import { Checkbox } from '../../src/utils/Checkbox'; +import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { it.each([['foo'], ['bar'], ['baz']])('includes extra class names when provided', (className) => { @@ -24,9 +24,8 @@ describe('', () => { }); it.each([[true], [false]])('changes checked status on input change', async (checked) => { - const user = userEvent.setup(); const onChange = jest.fn(); - render(Foo); + const { user } = renderWithEvents(Foo); expect(onChange).not.toHaveBeenCalled(); await user.click(screen.getByLabelText('Foo')); diff --git a/test/utils/CopyToClipboardIcon.test.tsx b/test/utils/CopyToClipboardIcon.test.tsx index f286e724..3423a3d9 100644 --- a/test/utils/CopyToClipboardIcon.test.tsx +++ b/test/utils/CopyToClipboardIcon.test.tsx @@ -1,27 +1,26 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import CopyToClipboard from 'react-copy-to-clipboard'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faCopy as copyIcon } from '@fortawesome/free-regular-svg-icons'; import { CopyToClipboardIcon } from '../../src/utils/CopyToClipboardIcon'; +import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { - let wrapper: ShallowWrapper; - const onCopy = () => {}; + const onCopy = jest.fn(); + const setUp = (text = 'foo') => renderWithEvents(); - beforeEach(() => { - wrapper = shallow(); - }); - afterEach(() => wrapper?.unmount()); + afterEach(jest.clearAllMocks); it('wraps expected components', () => { - const copyToClipboard = wrapper.find(CopyToClipboard); - const icon = wrapper.find(FontAwesomeIcon); + const { container } = setUp(); + expect(container).toMatchSnapshot(); + }); - expect(copyToClipboard).toHaveLength(1); - expect(copyToClipboard.prop('text')).toEqual('foo'); - expect(copyToClipboard.prop('onCopy')).toEqual(onCopy); - expect(icon).toHaveLength(1); - expect(icon.prop('icon')).toEqual(copyIcon); - expect(icon.prop('className')).toEqual('ms-2 copy-to-clipboard-icon'); + it.each([ + ['text'], + ['bar'], + ['baz'], + ])('copies content to clipboard when clicked', async (text) => { + const { user, container } = setUp(text); + + expect(onCopy).not.toHaveBeenCalled(); + container.firstElementChild && await user.click(container.firstElementChild); + expect(onCopy).toHaveBeenCalledWith(text, false); }); }); diff --git a/test/utils/DateInput.test.tsx b/test/utils/DateInput.test.tsx index 4ec93544..e036433c 100644 --- a/test/utils/DateInput.test.tsx +++ b/test/utils/DateInput.test.tsx @@ -1,35 +1,33 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { screen, waitFor } from '@testing-library/react'; import { Mock } from 'ts-mockery'; -import DateInput, { DateInputProps } from '../../src/utils/DateInput'; +import { DateInput, DateInputProps } from '../../src/utils/DateInput'; +import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { - let wrapped: ShallowWrapper; - - const createComponent = (props: Partial = {}) => { - wrapped = shallow((props)} />); - - return wrapped; - }; - - afterEach(() => wrapped?.unmount()); - - it('wraps a DatePicker', () => { - wrapped = createComponent(); - }); + const setUp = (props: Partial = {}) => renderWithEvents( + (props)} />, + ); it('shows calendar icon when input is not clearable', () => { - wrapped = createComponent({ isClearable: false }); - expect(wrapped.find(FontAwesomeIcon)).toHaveLength(1); + setUp({ isClearable: false }); + expect(screen.getByRole('img', { hidden: true })).toBeInTheDocument(); }); it('shows calendar icon when input is clearable but selected value is nil', () => { - wrapped = createComponent({ isClearable: true, selected: null }); - expect(wrapped.find(FontAwesomeIcon)).toHaveLength(1); + setUp({ isClearable: true, selected: null }); + expect(screen.getByRole('img', { hidden: true })).toBeInTheDocument(); }); it('does not show calendar icon when input is clearable', () => { - wrapped = createComponent({ isClearable: true, selected: new Date() }); - expect(wrapped.find(FontAwesomeIcon)).toHaveLength(0); + setUp({ isClearable: true, selected: new Date() }); + expect(screen.queryByRole('img', { hidden: true })).not.toBeInTheDocument(); + }); + + it('shows popper on element click', async () => { + const { user, container } = setUp({ placeholderText: 'foo' }); + + expect(container.querySelector('.react-datepicker')).not.toBeInTheDocument(); + await user.click(screen.getByPlaceholderText('foo')); + await waitFor(() => expect(container.querySelector('.react-datepicker')).toBeInTheDocument()); }); }); diff --git a/test/utils/DropdownBtn.test.tsx b/test/utils/DropdownBtn.test.tsx index 0a6d301a..cfca2128 100644 --- a/test/utils/DropdownBtn.test.tsx +++ b/test/utils/DropdownBtn.test.tsx @@ -1,30 +1,23 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import { DropdownMenu, DropdownToggle } from 'reactstrap'; +import { screen } from '@testing-library/react'; import { PropsWithChildren } from 'react'; import { DropdownBtn, DropdownBtnProps } from '../../src/utils/DropdownBtn'; +import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { - let wrapper: ShallowWrapper; - const createWrapper = (props: PropsWithChildren) => { - wrapper = shallow(); + const setUp = (props: PropsWithChildren) => renderWithEvents( + , + ); - return wrapper; - }; - - afterEach(() => wrapper?.unmount()); - - it.each([['foo'], ['bar'], ['baz']])('displays provided text', (text) => { - const wrapper = createWrapper({ text }); - const toggle = wrapper.find(DropdownToggle); - - expect(toggle.prop('children')).toContain(text); + it.each([['foo'], ['bar'], ['baz']])('displays provided text in button', (text) => { + setUp({ text }); + expect(screen.getByRole('button')).toHaveTextContent(text); }); - it.each([['foo'], ['bar'], ['baz']])('displays provided children', (children) => { - const wrapper = createWrapper({ text: '', children }); - const menu = wrapper.find(DropdownMenu); + it.each([['foo'], ['bar'], ['baz']])('displays provided children in menu', async (children) => { + const { user } = setUp({ text: '', children }); - expect(menu.html()).toContain(children); + await user.click(screen.getByRole('button')); + expect(screen.getByRole('menu')).toHaveTextContent(children); }); it.each([ @@ -33,20 +26,21 @@ describe('', () => { ['foo', 'dropdown-btn__toggle btn-block foo'], ['bar', 'dropdown-btn__toggle btn-block bar'], ])('includes provided classes', (className, expectedClasses) => { - const wrapper = createWrapper({ text: '', className }); - const toggle = wrapper.find(DropdownToggle); - - expect(toggle.prop('className')?.trim()).toEqual(expectedClasses); + setUp({ text: '', className }); + expect(screen.getByRole('button')).toHaveClass(expectedClasses); }); it.each([ - [100, { minWidth: '100px' }], - [250, { minWidth: '250px' }], - [undefined, {}], - ])('renders proper styles when minWidth is provided', (minWidth, expectedStyle) => { - const wrapper = createWrapper({ text: '', minWidth }); - const style = wrapper.find(DropdownMenu).prop('style'); + [100, 'min-width: 100px; '], + [250, 'min-width: 250px; '], + [undefined, ''], + ])('renders proper styles when minWidth is provided', async (minWidth, expectedStyle) => { + const { user } = setUp({ text: '', minWidth }); - expect(style).toEqual(expectedStyle); + await user.click(screen.getByRole('button')); + expect(screen.getByRole('menu')).toHaveAttribute( + 'style', + `${expectedStyle}position: absolute; left: 0px; top: 0px;`, + ); }); }); diff --git a/test/utils/DropdownBtnMenu.test.tsx b/test/utils/DropdownBtnMenu.test.tsx index 42f43f00..d7ef86b8 100644 --- a/test/utils/DropdownBtnMenu.test.tsx +++ b/test/utils/DropdownBtnMenu.test.tsx @@ -1,48 +1,39 @@ -import { shallow, ShallowWrapper } from 'enzyme'; +import { screen } from '@testing-library/react'; import { Mock } from 'ts-mockery'; -import { ButtonDropdown, DropdownMenu, DropdownToggle } from 'reactstrap'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faEllipsisV as menuIcon } from '@fortawesome/free-solid-svg-icons'; import { DropdownBtnMenu, DropdownBtnMenuProps } from '../../src/utils/DropdownBtnMenu'; +import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { - let wrapper: ShallowWrapper; - const createWrapper = (props: Partial) => { - wrapper = shallow((props)}>the children); - - return wrapper; - }; - - afterAll(() => wrapper?.unmount()); + const setUp = (props: Partial = {}) => renderWithEvents( + ({ toggle: jest.fn(), ...props })}>the children, + ); it('renders expected components', () => { - const wrapper = createWrapper({}); - const toggle = wrapper.find(DropdownToggle); - const icon = wrapper.find(FontAwesomeIcon); + setUp(); + const toggle = screen.getByRole('button'); - expect(wrapper.find(ButtonDropdown)).toHaveLength(1); - expect(toggle).toHaveLength(1); - expect(toggle.prop('size')).toEqual('sm'); - expect(toggle.prop('caret')).toEqual(true); - expect(toggle.prop('outline')).toEqual(true); - expect(toggle.prop('className')).toEqual('dropdown-btn-menu__dropdown-toggle'); - expect(icon).toHaveLength(1); - expect(icon.prop('icon')).toEqual(menuIcon); + expect(toggle).toBeInTheDocument(); + expect(toggle).toHaveClass('btn-sm'); + expect(toggle).toHaveClass('dropdown-btn-menu__dropdown-toggle'); + expect(screen.getByRole('img', { hidden: true })).toBeInTheDocument(); }); it('renders expected children', () => { - const menu = createWrapper({}).find(DropdownMenu); - - expect(menu.prop('children')).toEqual('the children'); + setUp(); + expect(screen.getByText('the children')).toBeInTheDocument(); }); it.each([ [undefined, true], [true, true], [false, false], - ])('renders menu to right when expected', (right, expectedRight) => { - const wrapper = createWrapper({ right }); + ])('renders menu to the end when expected', (right, expectedEnd) => { + setUp({ right }); - expect(wrapper.find(DropdownMenu).prop('end')).toEqual(expectedRight); + if (expectedEnd) { + expect(screen.getByRole('menu', { hidden: true })).toHaveClass('dropdown-menu-end'); + } else { + expect(screen.getByRole('menu', { hidden: true })).not.toHaveClass('dropdown-menu-end'); + } }); }); diff --git a/test/utils/InfoTooltip.test.tsx b/test/utils/InfoTooltip.test.tsx index efbe3af3..1bda4e60 100644 --- a/test/utils/InfoTooltip.test.tsx +++ b/test/utils/InfoTooltip.test.tsx @@ -1,30 +1,38 @@ -import { shallow } from 'enzyme'; -import { UncontrolledTooltip } from 'reactstrap'; +import { screen, waitFor } from '@testing-library/react'; import { Placement } from '@popperjs/core'; -import { InfoTooltip } from '../../src/utils/InfoTooltip'; +import { InfoTooltip, InfoTooltipProps } from '../../src/utils/InfoTooltip'; +import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { + const setUp = (props: Partial = {}) => renderWithEvents( + , + ); + it.each([ [undefined], ['foo'], ['bar'], ])('renders expected className on span', (className) => { - const wrapper = shallow(); - const span = wrapper.find('span'); + const { container } = setUp({ className }); - expect(span.prop('className')).toEqual(className ?? ''); + if (className) { + expect(container.firstChild).toHaveClass(className); + } else { + expect(container.firstChild).toHaveAttribute('class', ''); + } }); it.each([ - [], - ['Foo'], - ['Hello'], - [['One', 'Two', ]], - ])('passes children down to the nested tooltip component', (children) => { - const wrapper = shallow({children}); - const tooltip = wrapper.find(UncontrolledTooltip); + [foo, 'foo'], + ['Foo', 'Foo'], + ['Hello', 'Hello'], + [['One', 'Two', ], 'OneTwo'], + ])('passes children down to the nested tooltip component', async (children, expectedContent) => { + const { container, user } = setUp({ children }); - expect(tooltip.prop('children')).toEqual(children); + container.firstElementChild && await user.hover(container.firstElementChild); + await waitFor(() => expect(screen.getByRole('tooltip')).toBeInTheDocument()); + expect(screen.getByRole('tooltip')).toHaveTextContent(expectedContent); }); it.each([ @@ -32,10 +40,11 @@ describe('', () => { ['left' as Placement], ['top' as Placement], ['bottom' as Placement], - ])('places tooltip where requested', (placement) => { - const wrapper = shallow(); - const tooltip = wrapper.find(UncontrolledTooltip); + ])('places tooltip where requested', async (placement) => { + const { container, user } = setUp({ placement }); - expect(tooltip.prop('placement')).toEqual(placement); + container.firstElementChild && await user.hover(container.firstElementChild); + await waitFor(() => expect(screen.getByRole('tooltip')).toBeInTheDocument()); + expect(screen.getByRole('tooltip').parentNode).toHaveAttribute('data-popper-placement', placement); }); }); diff --git a/test/utils/Message.test.tsx b/test/utils/Message.test.tsx index 2ad9d531..d800a66d 100644 --- a/test/utils/Message.test.tsx +++ b/test/utils/Message.test.tsx @@ -1,28 +1,17 @@ -import { shallow, ShallowWrapper } from 'enzyme'; +import { render, screen } from '@testing-library/react'; import { PropsWithChildren } from 'react'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Card } from 'reactstrap'; -import Message, { MessageProps } from '../../src/utils/Message'; +import { Message, MessageProps } from '../../src/utils/Message'; describe('', () => { - let wrapper: ShallowWrapper; - const createWrapper = (props: PropsWithChildren = {}) => { - wrapper = shallow(); - - return wrapper; - }; - - afterEach(() => wrapper?.unmount()); + const setUp = (props: PropsWithChildren = {}) => render(); it.each([ - [true, 1, 0], - [false, 0, 1], - [undefined, 0, 1], - ])('renders expected classes based on width', (fullWidth, expectedFull, expectedNonFull) => { - const wrapper = createWrapper({ fullWidth }); - - expect(wrapper.find('.col-md-12')).toHaveLength(expectedFull); - expect(wrapper.find('.col-md-10')).toHaveLength(expectedNonFull); + [true, 'col-md-12'], + [false, 'col-md-10 offset-md-1'], + [undefined, 'col-md-10 offset-md-1'], + ])('renders expected classes based on width', (fullWidth, expectedClass) => { + const { container } = setUp({ fullWidth }); + expect(container.firstChild?.firstChild).toHaveClass(expectedClass); }); it.each([ @@ -31,15 +20,14 @@ describe('', () => { [true, undefined], [false, undefined], ])('renders expected content', (loading, children) => { - const wrapper = createWrapper({ loading, children }); + setUp({ loading, children }); - expect(wrapper.find(FontAwesomeIcon)).toHaveLength(loading ? 1 : 0); + expect(screen.queryAllByRole('img', { hidden: true })).toHaveLength(loading ? 1 : 0); if (loading) { - expect(wrapper.find('span').text()).toContain(children || 'Loading...'); + expect(screen.getByText(children || 'Loading...')).toHaveClass('ms-2'); } else { - expect(wrapper.find('span')).toHaveLength(0); - expect(wrapper.find('h3').text()).toContain(children || ''); + expect(screen.getByRole('heading')).toHaveTextContent(children || ''); } }); @@ -48,17 +36,14 @@ describe('', () => { ['default', '', 'text-muted'], [undefined, '', 'text-muted'], ])('renders proper classes based on message type', (type, expectedCardClass, expectedH3Class) => { - const wrapper = createWrapper({ type: type as 'default' | 'error' | undefined }); - const card = wrapper.find(Card); - const h3 = wrapper.find('h3'); + const { container } = setUp({ type: type as 'default' | 'error' | undefined }); - expect(card.prop('className')).toEqual(expectedCardClass); - expect(h3.prop('className')).toEqual(`text-center mb-0 ${expectedH3Class}`); + expect(container.querySelector('.card-body')).toHaveAttribute('class', expect.stringContaining(expectedCardClass)); + expect(screen.getByRole('heading')).toHaveClass(`text-center mb-0 ${expectedH3Class}`); }); it.each([{ className: 'foo' }, { className: 'bar' }, {}])('renders provided classes', ({ className }) => { - const wrapper = createWrapper({ className }); - - expect(wrapper.prop('className')).toEqual(`g-0${className ? ` ${className}` : ''}`); + const { container } = setUp({ className }); + expect(container.firstChild).toHaveClass(`g-0${className ? ` ${className}` : ''}`); }); }); diff --git a/test/utils/NavPills.test.tsx b/test/utils/NavPills.test.tsx index b06d1524..63f45f9f 100644 --- a/test/utils/NavPills.test.tsx +++ b/test/utils/NavPills.test.tsx @@ -1,20 +1,28 @@ -import { shallow } from 'enzyme'; -import { Card, Nav } from 'reactstrap'; +/* eslint-disable no-console */ +import { render, screen } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; import { NavPillItem, NavPills } from '../../src/utils/NavPills'; describe('', () => { + let originalError: typeof console.error; + + beforeEach(() => { + originalError = console.error; + console.error = () => {}; // Suppress errors logged during this test + }); + afterEach(() => { + console.error = originalError; + }); + it.each([ ['Foo'], [Hi!], [[, Hi!]], ])('throws error when any of the children is not a NavPillItem', (children) => { expect.assertions(1); - - try { - shallow({children}); - } catch (e: any) { - expect(e.message).toEqual('Only NavPillItem children are allowed inside NavPills.'); - } + expect(() => render({children})).toThrow( + 'Only NavPillItem children are allowed inside NavPills.', + ); }); it.each([ @@ -22,20 +30,27 @@ describe('', () => { [true], [false], ])('renders provided items', (fill) => { - const wrapper = shallow( - - 1 - 2 - 3 - , + const { container } = render( + + + 1 + 2 + 3 + + , ); - const card = wrapper.find(Card); - const nav = wrapper.find(Nav); - expect(card).toHaveLength(1); - expect(card.prop('body')).toEqual(true); - expect(nav).toHaveLength(1); - expect(nav.prop('pills')).toEqual(true); - expect(nav.prop('fill')).toEqual(!!fill); + const links = screen.getAllByRole('link'); + expect(links).toHaveLength(3); + links.forEach((link, index) => { + expect(link).toHaveTextContent(`${index + 1}`); + expect(link).toHaveAttribute('href', `/${index + 1}`); + }); + + if (fill) { + expect(container.querySelector('.nav')).toHaveClass('nav-fill'); + } else { + expect(container.querySelector('.nav')).not.toHaveClass('nav-fill'); + } }); }); diff --git a/test/utils/OrderingDropdown.test.tsx b/test/utils/OrderingDropdown.test.tsx index 3c978e1a..f8e78944 100644 --- a/test/utils/OrderingDropdown.test.tsx +++ b/test/utils/OrderingDropdown.test.tsx @@ -1,8 +1,8 @@ -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { screen } from '@testing-library/react'; import { values } from 'ramda'; import { OrderingDropdown, OrderingDropdownProps } from '../../src/utils/OrderingDropdown'; import { OrderDir } from '../../src/utils/helpers/ordering'; +import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { const items = { @@ -10,10 +10,9 @@ describe('', () => { bar: 'Bar', baz: 'Hello World', }; - const setUp = (props: Partial = {}) => ({ - user: userEvent.setup(), - ...render(), - }); + const setUp = (props: Partial = {}) => renderWithEvents( + , + ); const setUpWithDisplayedMenu = async (props: Partial = {}) => { const result = setUp(props); const { user } = result; diff --git a/test/utils/PaginationDropdown.test.tsx b/test/utils/PaginationDropdown.test.tsx index 5a413a86..02ff5d61 100644 --- a/test/utils/PaginationDropdown.test.tsx +++ b/test/utils/PaginationDropdown.test.tsx @@ -1,22 +1,23 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import { DropdownItem } from 'reactstrap'; -import PaginationDropdown from '../../src/utils/PaginationDropdown'; +import { screen } from '@testing-library/react'; +import { PaginationDropdown } from '../../src/utils/PaginationDropdown'; +import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { const setValue = jest.fn(); - let wrapper: ShallowWrapper; + const setUp = async () => { + const result = renderWithEvents(); + const { user } = result; - beforeEach(() => { - wrapper = shallow(); - }); + await user.click(screen.getByRole('button')); + + return result; + }; afterEach(jest.clearAllMocks); - afterEach(() => wrapper?.unmount()); - it('renders expected amount of items', () => { - const items = wrapper.find(DropdownItem); - - expect(items).toHaveLength(6); + it('renders expected amount of items', async () => { + await setUp(); + expect(screen.getAllByRole('menuitem')).toHaveLength(5); }); it.each([ @@ -24,12 +25,11 @@ describe('', () => { [1, 50], [2, 100], [3, 200], - [5, Infinity], - ])('sets expected value when an item is clicked', (index, expectedValue) => { - const item = wrapper.find(DropdownItem).at(index); + ])('sets expected value when an item is clicked', async (index, expectedValue) => { + const { user } = await setUp(); expect(setValue).not.toHaveBeenCalled(); - item.simulate('click'); + await user.click(screen.getAllByRole('menuitem')[index]); expect(setValue).toHaveBeenCalledWith(expectedValue); }); }); diff --git a/test/utils/Result.test.tsx b/test/utils/Result.test.tsx index b8a17cbb..0ba9b3f3 100644 --- a/test/utils/Result.test.tsx +++ b/test/utils/Result.test.tsx @@ -1,46 +1,32 @@ -import { shallow, ShallowWrapper } from 'enzyme'; +import { render, screen } from '@testing-library/react'; import { Result, ResultProps, ResultType } from '../../src/utils/Result'; -import { SimpleCard } from '../../src/utils/SimpleCard'; describe('', () => { - let wrapper: ShallowWrapper; - const createWrapper = (props: ResultProps) => { - wrapper = shallow(); - - return wrapper; - }; - - afterEach(() => wrapper?.unmount()); + const setUp = (props: ResultProps) => render(); it.each([ ['success' as ResultType, 'bg-main text-white'], ['error' as ResultType, 'bg-danger text-white'], ['warning' as ResultType, 'bg-warning'], ])('renders expected classes based on type', (type, expectedClasses) => { - const wrapper = createWrapper({ type }); - const innerCard = wrapper.find(SimpleCard); - - expect(innerCard.prop('className')).toEqual(`text-center ${expectedClasses}`); + setUp({ type }); + expect(screen.getByRole('document')).toHaveClass(expectedClasses); }); it.each([ - [undefined], ['foo'], ['bar'], ])('renders provided classes in root element', (className) => { - const wrapper = createWrapper({ type: 'success', className }); - - expect(wrapper.prop('className')).toEqual(className); + const { container } = setUp({ type: 'success', className }); + expect(container.firstChild).toHaveClass(className); }); it.each([{ small: true }, { small: false }])('renders small results properly', ({ small }) => { - const wrapper = createWrapper({ type: 'success', small }); - const bigElement = wrapper.find('.col-md-10'); - const smallElement = wrapper.find('.col-12'); - const innerCard = wrapper.find(SimpleCard); + const { container } = setUp({ type: 'success', small }); + const bigElement = container.querySelectorAll('.col-md-10'); + const smallElement = container.querySelectorAll('.col-12'); expect(bigElement).toHaveLength(small ? 0 : 1); expect(smallElement).toHaveLength(small ? 1 : 0); - expect(innerCard.prop('bodyClassName')).toEqual(small ? 'p-2' : ''); }); }); diff --git a/test/utils/Time.test.tsx b/test/utils/Time.test.tsx index a62dc06c..cb1f9c8f 100644 --- a/test/utils/Time.test.tsx +++ b/test/utils/Time.test.tsx @@ -1,30 +1,22 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import { DateProps, Time } from '../../src/utils/Time'; +import { render } from '@testing-library/react'; +import { TimeProps, Time } from '../../src/utils/Time'; import { parseDate } from '../../src/utils/helpers/date'; describe(' + )); - expect(header.find('.foo-span')).toHaveLength(1); - expect(header.find('.bar-span')).toHaveLength(1); + expect(screen.getByText('Foo in header')).toHaveClass('foo-span'); + expect(screen.getByText('Bar in header')).toHaveClass('bar-span'); }); }); diff --git a/test/visits/charts/__snapshots__/LineChartCard.test.tsx.snap b/test/visits/charts/__snapshots__/LineChartCard.test.tsx.snap new file mode 100644 index 00000000..105b15cf --- /dev/null +++ b/test/visits/charts/__snapshots__/LineChartCard.test.tsx.snap @@ -0,0 +1,461 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders chart with expected data 1`] = ` +Array [ + Object { + "props": Object { + "a": 1, + "b": 0, + "c": 0, + "d": 1, + "e": 0, + "f": 0, + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "setTransform", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, + Object { + "props": Object { + "text": "0", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "measureText", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, + Object { + "props": Object { + "text": "1", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "measureText", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, +] +`; + +exports[` renders chart with expected data 2`] = ` +Array [ + Object { + "props": Object { + "a": 1, + "b": 0, + "c": 0, + "d": 1, + "e": 0, + "f": 0, + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "setTransform", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, + Object { + "props": Object { + "text": "0", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "measureText", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, + Object { + "props": Object { + "text": "1", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "measureText", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, +] +`; + +exports[` renders chart with expected data 3`] = ` +Array [ + Object { + "props": Object { + "a": 1, + "b": 0, + "c": 0, + "d": 1, + "e": 0, + "f": 0, + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "setTransform", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, + Object { + "props": Object { + "text": "0", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "measureText", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, + Object { + "props": Object { + "text": "1", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "measureText", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, + Object { + "props": Object { + "text": "2016-04", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "measureText", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, +] +`; + +exports[` renders chart with expected data 4`] = ` +Array [ + Object { + "props": Object { + "a": 1, + "b": 0, + "c": 0, + "d": 1, + "e": 0, + "f": 0, + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "setTransform", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, + Object { + "props": Object { + "text": "0", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "measureText", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, + Object { + "props": Object { + "text": "1", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "measureText", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, + Object { + "props": Object { + "text": "2016-04", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "measureText", + }, + Object { + "props": Object { + "value": "12px \\"Helvetica Neue\\", 'Helvetica', 'Arial', sans-serif", + }, + "transform": Array [ + 1, + 0, + 0, + 1, + 0, + 0, + ], + "type": "font", + }, +] +`; diff --git a/test/visits/charts/__snapshots__/SortableBarChartCard.test.tsx.snap b/test/visits/charts/__snapshots__/SortableBarChartCard.test.tsx.snap new file mode 100644 index 00000000..a5274662 --- /dev/null +++ b/test/visits/charts/__snapshots__/SortableBarChartCard.test.tsx.snap @@ -0,0 +1,1231 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders properly ordered stats when ordering is set 1`] = ` +
+
+ Foo +
+ +
+
+ +
+
+`; + +exports[` renders properly ordered stats when ordering is set 2`] = ` +
+
+ Foo +
+ +
+
+ +
+
+`; + +exports[` renders properly ordered stats when ordering is set 3`] = ` +
+
+ Foo +
+ +
+
+ +
+
+`; + +exports[` renders properly ordered stats when ordering is set 4`] = ` +
+
+ Foo +
+ +
+
+ +
+
+`; + +exports[` renders properly paginated stats when pagination is set 1`] = ` +
+
+ Foo +
+ +
+ +
+
+ +
+ +
+`; + +exports[` renders properly paginated stats when pagination is set 2`] = ` +
+
+ Foo +
+ +
+ +
+
+ +
+ +
+`; + +exports[` renders properly paginated stats when pagination is set 3`] = ` +
+
+ Foo +
+ +
+ +
+
+ +
+
+`; + +exports[` renders properly paginated stats when pagination is set 4`] = ` +
+
+ Foo +
+ +
+ +
+
+ +
+
+`; + +exports[` renders stats unchanged when no ordering is set 1`] = ` +
+
+ Foo +
+ +
+
+ +
+
+`; diff --git a/test/visits/helpers/MapModal.test.tsx b/test/visits/helpers/MapModal.test.tsx index 08f5d86e..2b053896 100644 --- a/test/visits/helpers/MapModal.test.tsx +++ b/test/visits/helpers/MapModal.test.tsx @@ -1,14 +1,9 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import { Marker, Popup } from 'react-leaflet'; -import { Modal } from 'reactstrap'; -import MapModal from '../../../src/visits/helpers/MapModal'; +import { render, screen } from '@testing-library/react'; +import { MapModal } from '../../../src/visits/helpers/MapModal'; import { CityStats } from '../../../src/visits/types'; describe('', () => { - let wrapper: ShallowWrapper; - const toggle = () => ''; - const isOpen = true; - const title = 'Foobar'; + const toggle = jest.fn(); const zaragozaLat = 41.6563497; const zaragozaLong = -0.876566; const newYorkLat = 40.730610; @@ -26,36 +21,8 @@ describe('', () => { }, ]; - beforeEach(() => { - wrapper = shallow(); - }); - - afterEach(() => wrapper.unmount()); - - it('renders modal with provided props', () => { - const modal = wrapper.find(Modal); - const header = wrapper.find('.map-modal__modal-title'); - - expect(modal.prop('toggle')).toEqual(toggle); - expect(modal.prop('isOpen')).toEqual(isOpen); - expect(header.find('.btn-close').prop('onClick')).toEqual(toggle); - expect(header.text()).toContain(title); - }); - - it('renders open street map tile', () => { - expect(wrapper.find('OpenStreetMapTile')).toHaveLength(1); - }); - - it('renders proper amount of markers', () => { - const markers = wrapper.find(Marker); - - expect(markers).toHaveLength(locations.length); - locations.forEach(({ latLong, count, cityName }, index) => { - const marker = markers.at(index); - const popup = marker.find(Popup); - - expect(marker.prop('position')).toEqual(latLong); - expect(popup.text()).toEqual(`${count} visits from ${cityName}`); - }); + it('renders expected map', () => { + render(); + expect(screen.getByRole('dialog')).toMatchSnapshot(); }); }); diff --git a/test/visits/helpers/OpenMapModalBtn.test.tsx b/test/visits/helpers/OpenMapModalBtn.test.tsx index 12fd9443..2e83dc71 100644 --- a/test/visits/helpers/OpenMapModalBtn.test.tsx +++ b/test/visits/helpers/OpenMapModalBtn.test.tsx @@ -1,61 +1,53 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import { Dropdown, DropdownItem, UncontrolledTooltip } from 'reactstrap'; +import { screen, waitFor } from '@testing-library/react'; import { Mock } from 'ts-mockery'; -import OpenMapModalBtn from '../../../src/visits/helpers/OpenMapModalBtn'; -import MapModal from '../../../src/visits/helpers/MapModal'; +import { OpenMapModalBtn } from '../../../src/visits/helpers/OpenMapModalBtn'; import { CityStats } from '../../../src/visits/types'; +import { renderWithEvents } from '../../__helpers__/setUpTest'; describe('', () => { - let wrapper: ShallowWrapper; const title = 'Foo'; const locations = [ - Mock.of({ cityName: 'foo', count: 30 }), - Mock.of({ cityName: 'bar', count: 45 }), + Mock.of({ cityName: 'foo', count: 30, latLong: [5, 5] }), + Mock.of({ cityName: 'bar', count: 45, latLong: [88, 88] }), ]; - const createWrapper = (activeCities: string[] = []) => { - wrapper = shallow(); + const setUp = (activeCities?: string[]) => renderWithEvents( + , + ); - return wrapper; - }; + it('renders tooltip on button hover and opens modal on click', async () => { + const { user } = setUp(); - afterEach(() => wrapper?.unmount()); + expect(screen.queryByRole('tooltip')).not.toBeInTheDocument(); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + expect(screen.queryByRole('menu')).not.toBeInTheDocument(); - it('renders expected content', () => { - const wrapper = createWrapper(); - const button = wrapper.find('.open-map-modal-btn__btn'); - const tooltip = wrapper.find(UncontrolledTooltip); - const dropdown = wrapper.find(Dropdown); - const modal = wrapper.find(MapModal); - - expect(button).toHaveLength(1); - expect(tooltip).toHaveLength(1); - expect(dropdown).toHaveLength(1); - expect(modal).toHaveLength(1); + await user.click(screen.getByRole('button')); + await waitFor(() => expect(screen.getByRole('tooltip')).toBeInTheDocument()); + await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()); + expect(screen.queryByRole('menu')).not.toBeInTheDocument(); }); - it('opens dropdown instead of modal when a list of active cities has been provided', () => { - const wrapper = createWrapper(['bar']); + it('opens dropdown instead of modal when a list of active cities has been provided', async () => { + const { user } = setUp(['bar']); - wrapper.find('.open-map-modal-btn__btn').simulate('click'); + expect(screen.queryByRole('menu')).not.toBeInTheDocument(); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); - expect(wrapper.find(Dropdown).prop('isOpen')).toEqual(true); - expect(wrapper.find(MapModal).prop('isOpen')).toEqual(false); + await user.click(screen.getByRole('button')); + + await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument()); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); }); - it('filters out non-active cities from list of locations', () => { - const wrapper = createWrapper(['bar']); + it.each([ + ['Show all locations'], + ['Show locations in current page'], + ])('filters out non-active cities from list of locations', async (name) => { + const { user } = setUp(['bar']); - wrapper.find('.open-map-modal-btn__btn').simulate('click'); - wrapper.find(Dropdown).find(DropdownItem).at(1).simulate('click'); + await user.click(screen.getByRole('button')); + await user.click(screen.getByRole('menuitem', { name })); - const modal = wrapper.find(MapModal); - - expect(modal.prop('title')).toEqual(title); - expect(modal.prop('locations')).toEqual([ - { - cityName: 'bar', - count: 45, - }, - ]); + expect(await screen.findByRole('dialog')).toMatchSnapshot(); }); }); diff --git a/test/visits/helpers/VisitsFilterDropdown.test.tsx b/test/visits/helpers/VisitsFilterDropdown.test.tsx index 91fc29cc..eb3f38a2 100644 --- a/test/visits/helpers/VisitsFilterDropdown.test.tsx +++ b/test/visits/helpers/VisitsFilterDropdown.test.tsx @@ -1,89 +1,77 @@ -import { shallow, ShallowWrapper } from 'enzyme'; -import { DropdownItem } from 'reactstrap'; +import { screen } from '@testing-library/react'; import { OrphanVisitType, VisitsFilter } from '../../../src/visits/types'; import { VisitsFilterDropdown } from '../../../src/visits/helpers/VisitsFilterDropdown'; +import { renderWithEvents } from '../../__helpers__/setUpTest'; describe('', () => { - let wrapper: ShallowWrapper; const onChange = jest.fn(); - const createWrapper = (selected: VisitsFilter = {}, isOrphanVisits = true) => { - wrapper = shallow( - , - ); - - return wrapper; - }; + const setUp = (selected: VisitsFilter = {}, isOrphanVisits = true, botsSupported = true) => renderWithEvents( + , + ); beforeEach(jest.clearAllMocks); - afterEach(() => wrapper?.unmount()); it('has expected text', () => { - const wrapper = createWrapper(); - - expect(wrapper.prop('text')).toEqual('Filters'); + setUp(); + expect(screen.getByRole('button', { name: 'Filters' })).toBeInTheDocument(); }); it.each([ - [false, 4, 1], - [true, 9, 2], - ])('renders expected amount of items', (isOrphanVisits, expectedItemsAmount, expectedHeadersAmount) => { - const wrapper = createWrapper({}, isOrphanVisits); - const items = wrapper.find(DropdownItem); - const headers = items.filterWhere((item) => !!item.prop('header')); + [false, 1, 1], + [true, 4, 2], + ])('renders expected amount of items', async (isOrphanVisits, expectedItemsAmount, expectedHeadersAmount) => { + const { user } = setUp({}, isOrphanVisits); - expect(items).toHaveLength(expectedItemsAmount); - expect(headers).toHaveLength(expectedHeadersAmount); + await user.click(screen.getByRole('button', { name: 'Filters' })); + + expect(screen.getAllByRole('menuitem')).toHaveLength(expectedItemsAmount); + expect(screen.getAllByRole('heading')).toHaveLength(expectedHeadersAmount); }); it.each([ - ['base_url' as OrphanVisitType, 4, 1], - ['invalid_short_url' as OrphanVisitType, 5, 1], - ['regular_404' as OrphanVisitType, 6, 1], + ['base_url' as OrphanVisitType, 1, 1], + ['invalid_short_url' as OrphanVisitType, 2, 1], + ['regular_404' as OrphanVisitType, 3, 1], [undefined, -1, 0], - ])('sets expected item as active', (orphanVisitsType, expectedSelectedIndex, expectedActiveItems) => { - const wrapper = createWrapper({ orphanVisitsType }); - const items = wrapper.find(DropdownItem); - const activeItem = items.filterWhere((item) => !!item.prop('active')); + ])('sets expected item as active', async (orphanVisitsType, expectedSelectedIndex, expectedActiveItems) => { + const { user } = setUp({ orphanVisitsType }); + + await user.click(screen.getByRole('button', { name: 'Filters' })); + + const items = screen.getAllByRole('menuitem'); + const activeItem = items.filter((item) => item.classList.contains('active')); expect.assertions(expectedActiveItems + 1); expect(activeItem).toHaveLength(expectedActiveItems); items.forEach((item, index) => { - if (item.prop('active')) { + if (item.classList.contains('active')) { expect(index).toEqual(expectedSelectedIndex); } }); }); it.each([ - [1, { excludeBots: true }], - [4, { orphanVisitsType: 'base_url' }], - [5, { orphanVisitsType: 'invalid_short_url' }], - [6, { orphanVisitsType: 'regular_404' }], - [8, {}], - ])('invokes onChange with proper selection when an item is clicked', (index, expectedSelection) => { - const wrapper = createWrapper(); - const itemToClick = wrapper.find(DropdownItem).at(index); - - itemToClick.simulate('click'); + [0, { excludeBots: true }, {}], + [1, { orphanVisitsType: 'base_url' }, {}], + [2, { orphanVisitsType: 'invalid_short_url' }, {}], + [3, { orphanVisitsType: 'regular_404' }, {}], + [4, {}, { excludeBots: true }], + ])('invokes onChange with proper selection when an item is clicked', async (index, expectedSelection, selected) => { + const { user } = setUp(selected); + expect(onChange).not.toHaveBeenCalled(); + await user.click(screen.getByRole('button', { name: 'Filters' })); + await user.click(screen.getAllByRole('menuitem')[index]); expect(onChange).toHaveBeenCalledWith(expectedSelection); }); it('does not render the component when neither orphan visits or bots filtering will be displayed', () => { - const wrapper = shallow( - , - ); - - expect(wrapper.text()).toEqual(''); + const { container } = setUp({}, false, false); + expect(container.firstChild).toBeNull(); }); }); diff --git a/test/visits/helpers/__snapshots__/MapModal.test.tsx.snap b/test/visits/helpers/__snapshots__/MapModal.test.tsx.snap new file mode 100644 index 00000000..73591165 --- /dev/null +++ b/test/visits/helpers/__snapshots__/MapModal.test.tsx.snap @@ -0,0 +1,184 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders expected map 1`] = ` +