Compare commits

...

41 Commits

Author SHA1 Message Date
Alejandro Celaya
f5e92c6897 Merge pull request #848 from shlinkio/develop
Release 3.10.2
2023-07-09 10:17:23 +02:00
Alejandro Celaya
f1014a4810 Build docker image for linux/arm64/v8 and linux/amd64 only 2023-07-09 10:09:35 +02:00
Alejandro Celaya
1793424658 Merge pull request #847 from acelaya-forks/feature/docker-build-on-tag
Build docker image only on tags
2023-07-09 09:58:55 +02:00
Alejandro Celaya
c94a5b948e Build docker image only on tags 2023-07-09 09:48:54 +02:00
Alejandro Celaya
107cabcd8b Merge pull request #845 from shlinkio/dependabot/npm_and_yarn/tough-cookie-4.1.3
Bump tough-cookie from 4.1.2 to 4.1.3
2023-07-08 07:50:43 +02:00
Alejandro Celaya
99bc769894 Merge pull request #844 from shlinkio/dependabot/npm_and_yarn/stylelint-15.10.1
Bump stylelint from 14.16.0 to 15.10.1
2023-07-08 07:50:18 +02:00
dependabot[bot]
e8f1964941 Bump tough-cookie from 4.1.2 to 4.1.3
Bumps [tough-cookie](https://github.com/salesforce/tough-cookie) from 4.1.2 to 4.1.3.
- [Release notes](https://github.com/salesforce/tough-cookie/releases)
- [Changelog](https://github.com/salesforce/tough-cookie/blob/master/CHANGELOG.md)
- [Commits](https://github.com/salesforce/tough-cookie/compare/v4.1.2...v4.1.3)

---
updated-dependencies:
- dependency-name: tough-cookie
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-08 00:17:47 +00:00
dependabot[bot]
1a491bec1c Bump stylelint from 14.16.0 to 15.10.1
Bumps [stylelint](https://github.com/stylelint/stylelint) from 14.16.0 to 15.10.1.
- [Release notes](https://github.com/stylelint/stylelint/releases)
- [Changelog](https://github.com/stylelint/stylelint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stylelint/stylelint/compare/14.16.0...15.10.1)

---
updated-dependencies:
- dependency-name: stylelint
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-07 22:00:52 +00:00
Alejandro Celaya
4ba63cdbf8 Merge pull request #837 from acelaya-forks/feature/vitest-0.32
Feature/vitest 0.32
2023-06-12 09:47:20 +02:00
Alejandro Celaya
b2c2af3ebb Exclude sw helper from code coverage 2023-06-12 08:59:38 +02:00
Alejandro Celaya
9dca19fcb5 Replace coverage-c8 with coverage-v8 2023-06-12 08:51:37 +02:00
Alejandro Celaya
7c2dab43e1 Update to vitest 0.32 2023-06-09 09:07:20 +02:00
Alejandro Celaya
a8c6d9b034 Merge pull request #836 from shlinkio/dependabot/npm_and_yarn/vite-4.3.9
Bump vite from 4.3.1 to 4.3.9
2023-06-06 05:38:06 +02:00
dependabot[bot]
be0cb20fa2 Bump vite from 4.3.1 to 4.3.9
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.3.1 to 4.3.9.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.3.9/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-06 02:03:33 +00:00
Alejandro Celaya
2ac1025e9f Merge pull request #834 from acelaya-forks/feature/vitest-migration
Feature/vitest migration
2023-05-27 14:21:33 +02:00
Alejandro Celaya
27d79b574e Correct test:watch command so that it actually watches 2023-05-27 14:15:38 +02:00
Alejandro Celaya
1b82f42a33 Remove direct dependency on redux and redux-thunk, as those are now required by @reduxjs/toolkit 2023-05-27 14:10:37 +02:00
Alejandro Celaya
f3f9eac67b Fix comments 2023-05-27 13:12:38 +02:00
Alejandro Celaya
2a86a0e540 Fix remaining type errors in tests 2023-05-27 12:45:12 +02:00
Alejandro Celaya
d14aea708e Fix incorrect types between testing library and vitest 2023-05-27 12:29:03 +02:00
Alejandro Celaya
12a05b422d Fix merge conflicts 2023-05-27 12:04:01 +02:00
Alejandro Celaya
a5abe9dbf2 Update test snapshots 2023-05-27 12:02:13 +02:00
Alejandro Celaya
07fcb4e016 Update tests to use vi instead of jest 2023-05-27 11:57:26 +02:00
Alejandro Celaya
e2cbb2713a Replace jest config with vitest config 2023-05-27 11:36:18 +02:00
Alejandro Celaya
706e00ace0 Merge pull request #833 from acelaya-forks/feature/menus
Feature/menus
2023-05-27 10:57:14 +02:00
Alejandro Celaya
655fbf94c1 Normalize and consolidate dropdown menus 2023-05-27 10:40:07 +02:00
Alejandro Celaya
afc574aceb Fixed block and inline dropdown buttons 2023-05-27 09:40:49 +02:00
Alejandro Celaya
3da2b56426 Update actions dependencies and node 2023-05-27 09:40:10 +02:00
Alejandro Celaya
5f91ad8819 Merge pull request #829 from shlinkio/develop
Release 3.10.1
2023-04-23 15:56:15 +02:00
Alejandro Celaya
131a745514 Add v3.10.1 to changelog 2023-04-23 15:24:48 +02:00
Alejandro Celaya
86349f1ad3 Merge pull request #828 from acelaya-forks/feature/short-url-export
Feature/short url export
2023-04-22 12:28:12 +02:00
Alejandro Celaya
72e4a7b062 Fix incorrect type 2023-04-22 12:09:15 +02:00
Alejandro Celaya
0a0165df45 Update changelog 2023-04-22 12:05:31 +02:00
Alejandro Celaya
992b22fd24 Refactor short URL export so that it is compatible with what Shlink expects 2023-04-21 09:36:51 +02:00
Alejandro Celaya
6fbe6c673b Merge pull request #827 from acelaya-forks/feature/vite-latest
Update vite deps
2023-04-21 08:20:42 +02:00
Alejandro Celaya
ff22e54b59 Update vite deps 2023-04-20 22:57:49 +02:00
Alejandro Celaya
578365ab68 Delete outdated stryker config file 2023-04-20 09:28:39 +02:00
Alejandro Celaya
22905a2efc Merge pull request #825 from acelaya-forks/feature/shoehorn
Introduce shoehorn as a possible replacement for ts-mockery
2023-04-14 09:34:45 +02:00
Alejandro Celaya
26bad75a1a Finish replacing ts-mockery with shoehorn 2023-04-14 09:28:53 +02:00
Alejandro Celaya
04e1950591 Migrate more tests to shoehorn 2023-04-13 22:47:13 +02:00
Alejandro Celaya
340f4b8fb5 Introduce shoehorn as a possible replacement for ts-mockery 2023-04-13 21:48:29 +02:00
177 changed files with 4017 additions and 7691 deletions

View File

@@ -11,6 +11,6 @@ jobs:
ci: ci:
uses: shlinkio/github-actions/.github/workflows/web-app-ci.yml@main uses: shlinkio/github-actions/.github/workflows/web-app-ci.yml@main
with: with:
node-version: 18.12 node-version: 20.2
publish-coverage: true publish-coverage: true
force-install: true force-install: true

View File

@@ -9,14 +9,14 @@ jobs:
continue-on-error: true continue-on-error: true
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
repository: ${{ github.event.pull_request.head.repo.full_name }} repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.ref }} ref: ${{ github.event.pull_request.head.ref }}
- name: Use node.js - name: Use node.js
uses: actions/setup-node@v1 uses: actions/setup-node@v3
with: with:
node-version: 18.12 node-version: 20.2
- name: Build - name: Build
run: | run: |
npm ci --force && \ npm ci --force && \

View File

@@ -2,8 +2,6 @@ name: Build and publish docker image
on: on:
push: push:
branches:
- develop
tags: tags:
- 'v*' - 'v*'
@@ -14,3 +12,4 @@ jobs:
with: with:
image-name: shlinkio/shlink-web-client image-name: shlinkio/shlink-web-client
version-arg-name: VERSION version-arg-name: VERSION
platforms: 'linux/arm64/v8,linux/amd64'

View File

@@ -10,11 +10,11 @@ jobs:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Use node.js - name: Use node.js
uses: actions/setup-node@v1 uses: actions/setup-node@v3
with: with:
node-version: 18.12 node-version: 20.2
- name: Generate release assets - name: Generate release assets
run: npm ci --force && 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 - name: Publish release with assets

View File

@@ -4,6 +4,41 @@ 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). 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.10.2] - 2023-07-09
### Added
* *Nothing*
### Changed
* [#781](https://github.com/shlinkio/shlink-web-client/issues/781) Migrate tests from jest to vitest.
* [#843](https://github.com/shlinkio/shlink-web-client/issues/843) Build docker image only for new tags, making sure it always includes an actual version number.
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* *Nothing*
## [3.10.1] - 2023-04-23
### Added
* *Nothing*
### Changed
* *Nothing*
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* [#826](https://github.com/shlinkio/shlink-web-client/issues/826) Fix generated short URLs CSV so that it can be used to import on Shlink.
## [3.10.0] - 2023-03-19 ## [3.10.0] - 2023-03-19
### Added ### Added
* [#807](https://github.com/shlinkio/shlink-web-client/issues/807) Add support for device-specific long-URLs when creating or editing short URLs. * [#807](https://github.com/shlinkio/shlink-web-client/issues/807) Add support for device-specific long-URLs when creating or editing short URLs.

View File

@@ -1,4 +1,4 @@
FROM node:18.12-alpine as node FROM node:20.2-alpine as node
COPY . /shlink-web-client COPY . /shlink-web-client
ARG VERSION="latest" ARG VERSION="latest"
ENV VERSION ${VERSION} ENV VERSION ${VERSION}

View File

@@ -1,9 +0,0 @@
module.exports = {
presets: [
['@babel/preset-env', {
targets: { esmodules: true }
}],
['@babel/preset-react', { runtime: 'automatic' }],
'@babel/preset-typescript',
],
};

View File

@@ -1,12 +0,0 @@
// This is a custom Jest transformer turning style imports into empty objects.
// http://facebook.github.io/jest/docs/en/webpack.html
module.exports = {
process() {
return { code: 'module.exports = {};' };
},
getCacheKey() {
// The output is always the same.
return 'cssTransform';
},
};

View File

@@ -1,31 +0,0 @@
const path = require('path');
// This is a custom Jest transformer turning file imports into filenames.
// http://facebook.github.io/jest/docs/en/webpack.html
module.exports = {
process(src, filename) {
const assetFilename = JSON.stringify(path.basename(filename));
if (filename.match(/\.svg$/)) {
return `module.exports = {
__esModule: true,
default: ${assetFilename},
ReactComponent: (props) => ({
$$typeof: Symbol.for('react.element'),
type: 'svg',
ref: null,
key: null,
props: Object.assign({}, props, {
children: ${assetFilename}
})
}),
};`;
}
return {
code: `module.exports = ${assetFilename};`
};
},
};

View File

@@ -1,12 +0,0 @@
import '@testing-library/jest-dom';
import 'jest-canvas-mock';
import 'chart.js/auto';
import ResizeObserver from 'resize-observer-polyfill';
import { setAutoFreeze } from 'immer';
(global as any).ResizeObserver = ResizeObserver;
(global as any).scrollTo = () => {};
(global as any).prompt = () => {};
(global as any).matchMedia = (media: string) => ({ matches: false, media });
setAutoFreeze(false); // TODO Bypassing a bug on jest

27
config/test/setupTests.ts Normal file
View File

@@ -0,0 +1,27 @@
import 'vitest-canvas-mock';
import 'chart.js/auto';
import type { TestingLibraryMatchers } from '@testing-library/jest-dom/matchers';
import matchers from '@testing-library/jest-dom/matchers';
import { cleanup } from '@testing-library/react';
import ResizeObserver from 'resize-observer-polyfill';
import { afterEach, expect } from 'vitest';
// Workaround for TypeScript error: https://github.com/testing-library/jest-dom/issues/439#issuecomment-1536524120
declare module 'vitest' {
interface Assertion<T = any> extends jest.Matchers<void, T>, TestingLibraryMatchers<T, void> {}
}
// Extends Vitest's expect method with methods from react-testing-library
expect.extend(matchers);
afterEach(() => {
// Clears all mocks after every test
vi.clearAllMocks();
// Run a cleanup after each test case (e.g. clearing jsdom)
cleanup();
});
(global as any).ResizeObserver = ResizeObserver;
(global as any).scrollTo = () => {};
(global as any).prompt = () => {};
(global as any).matchMedia = (media: string) => ({ matches: false, media });

View File

@@ -3,7 +3,7 @@ version: '3'
services: services:
shlink_web_client_node: shlink_web_client_node:
container_name: shlink_web_client_node container_name: shlink_web_client_node
image: node:18.12-alpine image: node:20.2-alpine
command: /bin/sh -c "cd /home/shlink/www && npm install --force && npm run start" command: /bin/sh -c "cd /home/shlink/www && npm install --force && npm run start"
volumes: volumes:
- ./:/home/shlink/www - ./:/home/shlink/www

View File

@@ -1,39 +0,0 @@
module.exports = {
coverageDirectory: '<rootDir>/coverage',
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/*.{ts,tsx}',
'!src/reducers/index.ts',
'!src/**/provideServices.ts',
'!src/container/*.ts',
],
coverageThreshold: {
global: {
statements: 90,
branches: 85,
functions: 90,
lines: 90,
},
},
setupFilesAfterEnv: ['<rootDir>/config/jest/setupTests.ts'],
testMatch: ['<rootDir>/test/**/*.test.{ts,tsx}'],
testEnvironment: 'jsdom',
testEnvironmentOptions: {
url: 'http://localhost',
},
transform: {
'^.+\\.(ts|tsx|js)$': '<rootDir>/node_modules/babel-jest',
'^.+\\.scss$': '<rootDir>/config/jest/cssTransform.js',
'^(?!.*\\.(ts|tsx|js|json|scss)$)': '<rootDir>/config/jest/fileTransform.js',
},
transformIgnorePatterns: [
'node_modules\/(?!(\@react-leaflet|react-leaflet|leaflet|react-chartjs-2|react-colorful)\/)',
'^.+\\.module\\.scss$',
],
moduleNameMapper: {
'^.+\\.module\\.scss$': 'identity-obj-proxy',
'react-chartjs-2': '<rootDir>/node_modules/react-chartjs-2/dist/index.js',
'uuid': '<rootDir>/node_modules/uuid/dist/index.js',
},
moduleFileExtensions: ['js', 'ts', 'tsx', 'json'],
};

8903
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,16 +17,12 @@
"preview": "vite preview --host=0.0.0.0", "preview": "vite preview --host=0.0.0.0",
"build": "npm run types && vite build && node scripts/replace-version.mjs", "build": "npm run types && vite build && node scripts/replace-version.mjs",
"build:dist": "npm run build && node scripts/create-dist-file.mjs", "build:dist": "npm run build && node scripts/create-dist-file.mjs",
"test": "jest --env=jsdom --colors", "test": "vitest run --run",
"test:coverage": "npm run test -- --coverage --coverageReporters=text --coverageReporters=text-summary", "test:watch": "vitest --watch",
"test:ci": "npm run test:coverage -- --coverageReporters=clover --ci", "test:ci": "npm run test -- --coverage",
"test:pretty": "npm run test:coverage -- --coverageReporters=html",
"test:verbose": "npm run test -- --verbose" "test:verbose": "npm run test -- --verbose"
}, },
"dependencies": { "dependencies": {
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.21.0",
"@fortawesome/fontawesome-free": "^6.3.0", "@fortawesome/fontawesome-free": "^6.3.0",
"@fortawesome/fontawesome-svg-core": "^6.3.0", "@fortawesome/fontawesome-svg-core": "^6.3.0",
"@fortawesome/free-brands-svg-icons": "^6.3.0", "@fortawesome/free-brands-svg-icons": "^6.3.0",
@@ -54,16 +50,14 @@
"react-copy-to-clipboard": "^5.1.0", "react-copy-to-clipboard": "^5.1.0",
"react-datepicker": "^4.8.0", "react-datepicker": "^4.8.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-external-link": "^2.0.0", "react-external-link": "^2.2.0",
"react-leaflet": "^4.2.0", "react-leaflet": "^4.2.0",
"react-redux": "^8.0.5", "react-redux": "^8.0.5",
"react-router-dom": "^6.6.1", "react-router-dom": "^6.6.1",
"react-swipeable": "^7.0.0", "react-swipeable": "^7.0.0",
"react-tag-autocomplete": "^6.3.0", "react-tag-autocomplete": "^6.3.0",
"reactstrap": "^9.1.5", "reactstrap": "^9.1.5",
"redux": "^4.2.0",
"redux-localstorage-simple": "^2.5.1", "redux-localstorage-simple": "^2.5.1",
"redux-thunk": "^2.4.2",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"workbox-core": "^6.5.4", "workbox-core": "^6.5.4",
"workbox-expiration": "^6.5.4", "workbox-expiration": "^6.5.4",
@@ -75,9 +69,9 @@
"@shlinkio/eslint-config-js-coding-standard": "~2.1.0", "@shlinkio/eslint-config-js-coding-standard": "~2.1.0",
"@shlinkio/stylelint-config-css-coding-standard": "~1.0.1", "@shlinkio/stylelint-config-css-coding-standard": "~1.0.1",
"@testing-library/jest-dom": "^5.16.5", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3", "@testing-library/user-event": "^14.4.3",
"@types/jest": "^29.2.4", "@total-typescript/shoehorn": "^0.1.0",
"@types/json2csv": "^5.0.3", "@types/json2csv": "^5.0.3",
"@types/leaflet": "^1.9.0", "@types/leaflet": "^1.9.0",
"@types/qs": "^6.9.7", "@types/qs": "^6.9.7",
@@ -89,22 +83,20 @@
"@types/react-dom": "^18.0.10", "@types/react-dom": "^18.0.10",
"@types/react-tag-autocomplete": "^6.3.0", "@types/react-tag-autocomplete": "^6.3.0",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"@vitejs/plugin-react": "^3.1.0", "@vitejs/plugin-react": "^4.0.0",
"@vitest/coverage-v8": "^0.32.0",
"adm-zip": "^0.5.10", "adm-zip": "^0.5.10",
"babel-jest": "^29.5.0",
"chalk": "^5.2.0", "chalk": "^5.2.0",
"eslint": "^8.30.0", "eslint": "^8.30.0",
"identity-obj-proxy": "^3.0.0", "jsdom": "^22.0.0",
"jest": "^29.3.1",
"jest-canvas-mock": "^2.4.0",
"jest-environment-jsdom": "^29.3.1",
"resize-observer-polyfill": "^1.5.1", "resize-observer-polyfill": "^1.5.1",
"sass": "^1.57.1", "sass": "^1.57.1",
"stylelint": "^14.16.0", "stylelint": "^15.10.1",
"ts-mockery": "^1.2.0",
"typescript": "^5.0.2", "typescript": "^5.0.2",
"vite": "^4.2.0", "vite": "^4.3.9",
"vite-plugin-pwa": "^0.14.4" "vite-plugin-pwa": "^0.14.4",
"vitest": "^0.32.0",
"vitest-canvas-mock": "^0.2.2"
}, },
"browserslist": [ "browserslist": [
">0.2%", ">0.2%",

View File

@@ -24,7 +24,6 @@ export class ReportExporter {
private readonly exportCsv = (filename: string, rows: object[]) => { private readonly exportCsv = (filename: string, rows: object[]) => {
const csv = this.jsonToCsv(rows); const csv = this.jsonToCsv(rows);
saveCsv(this.window, csv, filename); saveCsv(this.window, csv, filename);
}; };
} }

View File

@@ -5,9 +5,9 @@ import { Link } from 'react-router-dom';
import { DropdownItem } from 'reactstrap'; import { DropdownItem } from 'reactstrap';
import type { SelectedServer } from '../../servers/data'; import type { SelectedServer } from '../../servers/data';
import { getServerId } from '../../servers/data'; import { getServerId } from '../../servers/data';
import { DropdownBtnMenu } from '../../utils/DropdownBtnMenu';
import { useFeature } from '../../utils/helpers/features'; import { useFeature } from '../../utils/helpers/features';
import { useToggle } from '../../utils/helpers/hooks'; import { useToggle } from '../../utils/helpers/hooks';
import { RowDropdownBtn } from '../../utils/RowDropdownBtn';
import { DEFAULT_DOMAIN } from '../../visits/reducers/domainVisits'; import { DEFAULT_DOMAIN } from '../../visits/reducers/domainVisits';
import type { Domain } from '../data'; import type { Domain } from '../data';
import type { EditDomainRedirects } from '../reducers/domainRedirects'; import type { EditDomainRedirects } from '../reducers/domainRedirects';
@@ -20,7 +20,6 @@ interface DomainDropdownProps {
} }
export const DomainDropdown: FC<DomainDropdownProps> = ({ domain, editDomainRedirects, selectedServer }) => { export const DomainDropdown: FC<DomainDropdownProps> = ({ domain, editDomainRedirects, selectedServer }) => {
const [isOpen, toggle] = useToggle();
const [isModalOpen, toggleModal] = useToggle(); const [isModalOpen, toggleModal] = useToggle();
const { isDefault } = domain; const { isDefault } = domain;
const canBeEdited = !isDefault || useFeature('defaultDomainRedirectsEdition', selectedServer); const canBeEdited = !isDefault || useFeature('defaultDomainRedirectsEdition', selectedServer);
@@ -28,7 +27,7 @@ export const DomainDropdown: FC<DomainDropdownProps> = ({ domain, editDomainRedi
const serverId = getServerId(selectedServer); const serverId = getServerId(selectedServer);
return ( return (
<DropdownBtnMenu isOpen={isOpen} toggle={toggle}> <RowDropdownBtn>
{withVisits && ( {withVisits && (
<DropdownItem <DropdownItem
tag={Link} tag={Link}
@@ -47,6 +46,6 @@ export const DomainDropdown: FC<DomainDropdownProps> = ({ domain, editDomainRedi
toggle={toggleModal} toggle={toggleModal}
editDomainRedirects={editDomainRedirects} editDomainRedirects={editDomainRedirects}
/> />
</DropdownBtnMenu> </RowDropdownBtn>
); );
}; };

View File

@@ -9,8 +9,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { FC } from 'react'; import type { FC } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { DropdownItem } from 'reactstrap'; import { DropdownItem } from 'reactstrap';
import { DropdownBtnMenu } from '../utils/DropdownBtnMenu';
import { useToggle } from '../utils/helpers/hooks'; import { useToggle } from '../utils/helpers/hooks';
import { RowDropdownBtn } from '../utils/RowDropdownBtn';
import type { ServerWithId } from './data'; import type { ServerWithId } from './data';
import type { DeleteServerModalProps } from './DeleteServerModal'; import type { DeleteServerModalProps } from './DeleteServerModal';
@@ -25,14 +25,13 @@ interface ManageServersRowDropdownConnectProps extends ManageServersRowDropdownP
export const ManageServersRowDropdown = ( export const ManageServersRowDropdown = (
DeleteServerModal: FC<DeleteServerModalProps>, DeleteServerModal: FC<DeleteServerModalProps>,
): FC<ManageServersRowDropdownConnectProps> => ({ server, setAutoConnect }) => { ): FC<ManageServersRowDropdownConnectProps> => ({ server, setAutoConnect }) => {
const [isMenuOpen, toggleMenu] = useToggle();
const [isModalOpen,, showModal, hideModal] = useToggle(); const [isModalOpen,, showModal, hideModal] = useToggle();
const serverUrl = `/server/${server.id}`; const serverUrl = `/server/${server.id}`;
const { autoConnect: isAutoConnect } = server; const { autoConnect: isAutoConnect } = server;
const autoConnectIcon = isAutoConnect ? toggleOffIcon : toggleOnIcon; const autoConnectIcon = isAutoConnect ? toggleOffIcon : toggleOnIcon;
return ( return (
<DropdownBtnMenu isOpen={isMenuOpen} toggle={toggleMenu}> <RowDropdownBtn minWidth={170}>
<DropdownItem tag={Link} to={serverUrl}> <DropdownItem tag={Link} to={serverUrl}>
<FontAwesomeIcon icon={connectIcon} fixedWidth /> Connect <FontAwesomeIcon icon={connectIcon} fixedWidth /> Connect
</DropdownItem> </DropdownItem>
@@ -48,6 +47,6 @@ export const ManageServersRowDropdown = (
</DropdownItem> </DropdownItem>
<DeleteServerModal redirectHome={false} server={server} isOpen={isModalOpen} toggle={hideModal} /> <DeleteServerModal redirectHome={false} server={server} isOpen={isModalOpen} toggle={hideModal} />
</DropdownBtnMenu> </RowDropdownBtn>
); );
}; };

View File

@@ -79,6 +79,8 @@ export interface ExportableShortUrl {
createdAt: string; createdAt: string;
title: string; title: string;
shortUrl: string; shortUrl: string;
domain?: string;
shortCode: string;
longUrl: string; longUrl: string;
tags: string; tags: string;
visits: number; visits: number;

View File

@@ -1,4 +1,5 @@
import type { FC } from 'react'; import type { FC } from 'react';
import { useCallback } from 'react';
import type { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; import type { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import type { ReportExporter } from '../../common/services/ReportExporter'; import type { ReportExporter } from '../../common/services/ReportExporter';
import type { SelectedServer } from '../../servers/data'; import type { SelectedServer } from '../../servers/data';
@@ -24,7 +25,7 @@ export const ExportShortUrlsBtn = (
): FC<ExportShortUrlsBtnConnectProps> => ({ amount = 0, selectedServer }) => { ): FC<ExportShortUrlsBtnConnectProps> => ({ amount = 0, selectedServer }) => {
const [{ tags, search, startDate, endDate, orderBy, tagsMode }] = useShortUrlsQuery(); const [{ tags, search, startDate, endDate, orderBy, tagsMode }] = useShortUrlsQuery();
const [loading,, startLoading, stopLoading] = useToggle(); const [loading,, startLoading, stopLoading] = useToggle();
const exportAllUrls = async () => { const exportAllUrls = useCallback(async () => {
if (!isServerWithId(selectedServer)) { if (!isServerWithId(selectedServer)) {
return; return;
} }
@@ -47,16 +48,23 @@ export const ExportShortUrlsBtn = (
startLoading(); startLoading();
const shortUrls = await loadAllUrls(); const shortUrls = await loadAllUrls();
exportShortUrls(shortUrls.map((shortUrl) => ({ exportShortUrls(shortUrls.map((shortUrl) => {
createdAt: shortUrl.dateCreated, const { hostname: domain, pathname } = new URL(shortUrl.shortUrl);
shortUrl: shortUrl.shortUrl, const shortCode = pathname.substring(1); // Remove trailing slash
longUrl: shortUrl.longUrl,
title: shortUrl.title ?? '', return {
tags: shortUrl.tags.join(','), createdAt: shortUrl.dateCreated,
visits: shortUrl?.visitsSummary?.total ?? shortUrl.visitsCount, domain,
}))); shortCode,
shortUrl: shortUrl.shortUrl,
longUrl: shortUrl.longUrl,
title: shortUrl.title ?? '',
tags: shortUrl.tags.join('|'),
visits: shortUrl?.visitsSummary?.total ?? shortUrl.visitsCount,
};
}));
stopLoading(); stopLoading();
}; }, [selectedServer]);
return <ExportBtn loading={loading} className="btn-md-block" amount={amount} onClick={exportAllUrls} />; return <ExportBtn loading={loading} className="btn-md-block" amount={amount} onClick={exportAllUrls} />;
}; };

View File

@@ -17,7 +17,7 @@ export const ShortUrlsFilterDropdown = (
const onFilterClick = (key: keyof ShortUrlsFilter) => () => onChange({ ...selected, [key]: !selected?.[key] }); const onFilterClick = (key: keyof ShortUrlsFilter) => () => onChange({ ...selected, [key]: !selected?.[key] });
return ( return (
<DropdownBtn text="Filters" dropdownClassName={className} className="me-3" right minWidth={250}> <DropdownBtn text="Filters" dropdownClassName={className} inline end minWidth={250}>
<DropdownItem header>Visits:</DropdownItem> <DropdownItem header>Visits:</DropdownItem>
<DropdownItem active={excludeBots} onClick={onFilterClick('excludeBots')}>Ignore visits from bots</DropdownItem> <DropdownItem active={excludeBots} onClick={onFilterClick('excludeBots')}>Ignore visits from bots</DropdownItem>

View File

@@ -87,7 +87,7 @@ export const ShortUrlsRow = (
<td className="responsive-table__cell short-urls-row__cell" data-th="Status"> <td className="responsive-table__cell short-urls-row__cell" data-th="Status">
<ShortUrlStatus shortUrl={shortUrl} /> <ShortUrlStatus shortUrl={shortUrl} />
</td> </td>
<td className="responsive-table__cell short-urls-row__cell"> <td className="responsive-table__cell short-urls-row__cell text-end">
<ShortUrlsRowMenu selectedServer={selectedServer} shortUrl={shortUrl} /> <ShortUrlsRowMenu selectedServer={selectedServer} shortUrl={shortUrl} />
</td> </td>
</tr> </tr>

View File

@@ -8,8 +8,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { FC } from 'react'; import type { FC } from 'react';
import { DropdownItem } from 'reactstrap'; import { DropdownItem } from 'reactstrap';
import type { SelectedServer } from '../../servers/data'; import type { SelectedServer } from '../../servers/data';
import { DropdownBtnMenu } from '../../utils/DropdownBtnMenu';
import { useToggle } from '../../utils/helpers/hooks'; import { useToggle } from '../../utils/helpers/hooks';
import { RowDropdownBtn } from '../../utils/RowDropdownBtn';
import type { ShortUrl, ShortUrlModalProps } from '../data'; import type { ShortUrl, ShortUrlModalProps } from '../data';
import { ShortUrlDetailLink } from './ShortUrlDetailLink'; import { ShortUrlDetailLink } from './ShortUrlDetailLink';
@@ -23,12 +23,11 @@ export const ShortUrlsRowMenu = (
DeleteShortUrlModal: ShortUrlModal, DeleteShortUrlModal: ShortUrlModal,
QrCodeModal: ShortUrlModal, QrCodeModal: ShortUrlModal,
) => ({ shortUrl, selectedServer }: ShortUrlsRowMenuProps) => { ) => ({ shortUrl, selectedServer }: ShortUrlsRowMenuProps) => {
const [isOpen, toggle] = useToggle();
const [isQrModalOpen,, openQrCodeModal, closeQrCodeModal] = useToggle(); const [isQrModalOpen,, openQrCodeModal, closeQrCodeModal] = useToggle();
const [isDeleteModalOpen,, openDeleteModal, closeDeleteModal] = useToggle(); const [isDeleteModalOpen,, openDeleteModal, closeDeleteModal] = useToggle();
return ( return (
<DropdownBtnMenu toggle={toggle} isOpen={isOpen}> <RowDropdownBtn minWidth={190}>
<DropdownItem tag={ShortUrlDetailLink} selectedServer={selectedServer} shortUrl={shortUrl} suffix="visits"> <DropdownItem tag={ShortUrlDetailLink} selectedServer={selectedServer} shortUrl={shortUrl} suffix="visits">
<FontAwesomeIcon icon={pieChartIcon} fixedWidth /> Visit stats <FontAwesomeIcon icon={pieChartIcon} fixedWidth /> Visit stats
</DropdownItem> </DropdownItem>
@@ -48,7 +47,7 @@ export const ShortUrlsRowMenu = (
<FontAwesomeIcon icon={deleteIcon} fixedWidth /> Delete short URL <FontAwesomeIcon icon={deleteIcon} fixedWidth /> Delete short URL
</DropdownItem> </DropdownItem>
<DeleteShortUrlModal shortUrl={shortUrl} isOpen={isDeleteModalOpen} toggle={closeDeleteModal} /> <DeleteShortUrlModal shortUrl={shortUrl} isOpen={isDeleteModalOpen} toggle={closeDeleteModal} />
</DropdownBtnMenu> </RowDropdownBtn>
); );
}; };

View File

@@ -5,9 +5,9 @@ import { Link } from 'react-router-dom';
import { DropdownItem } from 'reactstrap'; import { DropdownItem } from 'reactstrap';
import type { SelectedServer } from '../servers/data'; import type { SelectedServer } from '../servers/data';
import { getServerId } from '../servers/data'; import { getServerId } from '../servers/data';
import { DropdownBtnMenu } from '../utils/DropdownBtnMenu';
import { useToggle } from '../utils/helpers/hooks'; import { useToggle } from '../utils/helpers/hooks';
import { prettify } from '../utils/helpers/numbers'; import { prettify } from '../utils/helpers/numbers';
import { RowDropdownBtn } from '../utils/RowDropdownBtn';
import type { ColorGenerator } from '../utils/services/ColorGenerator'; import type { ColorGenerator } from '../utils/services/ColorGenerator';
import type { SimplifiedTag, TagModalProps } from './data'; import type { SimplifiedTag, TagModalProps } from './data';
import { TagBullet } from './helpers/TagBullet'; import { TagBullet } from './helpers/TagBullet';
@@ -24,7 +24,6 @@ export const TagsTableRow = (
) => ({ tag, selectedServer }: TagsTableRowProps) => { ) => ({ tag, selectedServer }: TagsTableRowProps) => {
const [isDeleteModalOpen, toggleDelete] = useToggle(); const [isDeleteModalOpen, toggleDelete] = useToggle();
const [isEditModalOpen, toggleEdit] = useToggle(); const [isEditModalOpen, toggleEdit] = useToggle();
const [isDropdownOpen, toggleDropdown] = useToggle();
const serverId = getServerId(selectedServer); const serverId = getServerId(selectedServer);
return ( return (
@@ -43,14 +42,14 @@ export const TagsTableRow = (
</Link> </Link>
</td> </td>
<td className="responsive-table__cell text-lg-end"> <td className="responsive-table__cell text-lg-end">
<DropdownBtnMenu toggle={toggleDropdown} isOpen={isDropdownOpen}> <RowDropdownBtn>
<DropdownItem onClick={toggleEdit}> <DropdownItem onClick={toggleEdit}>
<FontAwesomeIcon icon={editIcon} fixedWidth className="me-1" /> Edit <FontAwesomeIcon icon={editIcon} fixedWidth className="me-1" /> Edit
</DropdownItem> </DropdownItem>
<DropdownItem onClick={toggleDelete}> <DropdownItem onClick={toggleDelete}>
<FontAwesomeIcon icon={deleteIcon} fixedWidth className="me-1" /> Delete <FontAwesomeIcon icon={deleteIcon} fixedWidth className="me-1" /> Delete
</DropdownItem> </DropdownItem>
</DropdownBtnMenu> </RowDropdownBtn>
</td> </td>
<EditTagModal tag={tag.tag} toggle={toggleEdit} isOpen={isEditModalOpen} /> <EditTagModal tag={tag.tag} toggle={toggleEdit} isOpen={isEditModalOpen} />

View File

@@ -2,13 +2,20 @@
@import '../utils/mixins/vertical-align'; @import '../utils/mixins/vertical-align';
.dropdown-btn__toggle.dropdown-btn__toggle {
text-align: left;
}
.dropdown-btn__toggle.dropdown-btn__toggle--with-caret {
padding-right: 1.75rem;
}
.dropdown-btn__toggle.dropdown-btn__toggle, .dropdown-btn__toggle.dropdown-btn__toggle,
.dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled).active, .dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled).active,
.dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled):active, .dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled):active,
.dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled):focus, .dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled):focus,
.dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled):hover, .dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled):hover,
.show > .dropdown-btn__toggle.dropdown-btn__toggle.dropdown-toggle { .show > .dropdown-btn__toggle.dropdown-btn__toggle.dropdown-toggle {
text-align: left;
color: var(--input-text-color); color: var(--input-text-color);
background-color: var(--primary-color); background-color: var(--primary-color);
border-color: var(--input-border-color); border-color: var(--input-border-color);

View File

@@ -1,28 +1,45 @@
import type { FC, PropsWithChildren } from 'react'; import classNames from 'classnames';
import type { FC, PropsWithChildren, ReactNode } from 'react';
import { Dropdown, DropdownMenu, DropdownToggle } from 'reactstrap'; import { Dropdown, DropdownMenu, DropdownToggle } from 'reactstrap';
import type { DropdownToggleProps } from 'reactstrap/types/lib/DropdownToggle';
import { useToggle } from './helpers/hooks'; import { useToggle } from './helpers/hooks';
import './DropdownBtn.scss'; import './DropdownBtn.scss';
export type DropdownBtnProps = PropsWithChildren<{ export type DropdownBtnProps = PropsWithChildren<Omit<DropdownToggleProps, 'caret' | 'size' | 'outline'> & {
text: string; text: ReactNode;
disabled?: boolean; noCaret?: boolean;
className?: string; className?: string;
dropdownClassName?: string; dropdownClassName?: string;
right?: boolean; inline?: boolean;
minWidth?: number; minWidth?: number;
size?: 'sm' | 'md' | 'lg';
}>; }>;
export const DropdownBtn: FC<DropdownBtnProps> = ( export const DropdownBtn: FC<DropdownBtnProps> = ({
{ text, disabled = false, className = '', children, dropdownClassName, right = false, minWidth }, text,
) => { disabled = false,
className,
children,
dropdownClassName,
noCaret,
end = false,
minWidth,
inline,
size,
}) => {
const [isOpen, toggle] = useToggle(); const [isOpen, toggle] = useToggle();
const toggleClasses = `dropdown-btn__toggle btn-block ${className}`; const toggleClasses = classNames('dropdown-btn__toggle', className, {
const style = { minWidth: minWidth && `${minWidth}px` }; 'btn-block': !inline,
'dropdown-btn__toggle--with-caret': !noCaret,
});
const menuStyle = { minWidth: minWidth && `${minWidth}px` };
return ( return (
<Dropdown isOpen={isOpen} toggle={toggle} disabled={disabled} className={dropdownClassName}> <Dropdown isOpen={isOpen} toggle={toggle} disabled={disabled} className={dropdownClassName}>
<DropdownToggle caret className={toggleClasses} color="primary">{text}</DropdownToggle> <DropdownToggle size={size} caret={!noCaret} className={toggleClasses} color="primary">
<DropdownMenu className="w-100" end={right} style={style}>{children}</DropdownMenu> {text}
</DropdownToggle>
<DropdownMenu className="w-100" end={end} style={menuStyle}>{children}</DropdownMenu>
</Dropdown> </Dropdown>
); );
}; };

View File

@@ -1,3 +0,0 @@
.dropdown-btn-menu__dropdown-toggle:after {
display: none !important;
}

View File

@@ -1,20 +0,0 @@
import { faEllipsisV as menuIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { FC, PropsWithChildren } from 'react';
import { ButtonDropdown, DropdownMenu, DropdownToggle } from 'reactstrap';
import './DropdownBtnMenu.scss';
export type DropdownBtnMenuProps = PropsWithChildren<{
isOpen: boolean;
toggle: () => void;
right?: boolean;
}>;
export const DropdownBtnMenu: FC<DropdownBtnMenuProps> = ({ isOpen, toggle, children, right = true }) => (
<ButtonDropdown toggle={toggle} isOpen={isOpen}>
<DropdownToggle size="sm" caret outline className="dropdown-btn-menu__dropdown-toggle">
&nbsp;<FontAwesomeIcon icon={menuIcon} />&nbsp;
</DropdownToggle>
<DropdownMenu end={right}>{children}</DropdownMenu>
</ButtonDropdown>
);

View File

@@ -5,10 +5,10 @@ import type { ButtonProps } from 'reactstrap';
import { Button } from 'reactstrap'; import { Button } from 'reactstrap';
import { prettify } from './helpers/numbers'; import { prettify } from './helpers/numbers';
interface ExportBtnProps extends Omit<ButtonProps, 'outline' | 'color' | 'disabled'> { type ExportBtnProps = Omit<ButtonProps, 'outline' | 'color' | 'disabled'> & {
amount?: number; amount?: number;
loading?: boolean; loading?: boolean;
} };
export const ExportBtn: FC<ExportBtnProps> = ({ amount = 0, loading = false, ...rest }) => ( export const ExportBtn: FC<ExportBtnProps> = ({ amount = 0, loading = false, ...rest }) => (
<Button {...rest} outline color="primary" disabled={loading}> <Button {...rest} outline color="primary" disabled={loading}>

View File

@@ -0,0 +1,21 @@
import { faEllipsisV as menuIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { FC, PropsWithChildren } from 'react';
import { DropdownBtn } from './DropdownBtn';
export type DropdownBtnMenuProps = PropsWithChildren<{
minWidth?: number;
}>;
export const RowDropdownBtn: FC<DropdownBtnMenuProps> = ({ children, minWidth }) => (
<DropdownBtn
text={<FontAwesomeIcon className="px-1" icon={menuIcon} />}
size="sm"
minWidth={minWidth}
end
noCaret
inline
>
{children}
</DropdownBtn>
);

View File

@@ -22,7 +22,7 @@ export const VisitsFilterDropdown = (
const onBotsClick = () => onChange({ ...selected, excludeBots: !selected?.excludeBots }); const onBotsClick = () => onChange({ ...selected, excludeBots: !selected?.excludeBots });
return ( return (
<DropdownBtn text="Filters" dropdownClassName={className} className="me-3" right minWidth={250}> <DropdownBtn text="Filters" dropdownClassName={className} inline end minWidth={250}>
<DropdownItem header>Bots:</DropdownItem> <DropdownItem header>Bots:</DropdownItem>
<DropdownItem active={excludeBots} onClick={onBotsClick}>Exclude potential bots</DropdownItem> <DropdownItem active={excludeBots} onClick={onBotsClick}>Exclude potential bots</DropdownItem>

View File

@@ -1,32 +0,0 @@
const jestConfig = require(`${__dirname}/jest.config.js`);
module.exports = {
mutate: jestConfig.collectCoverageFrom,
checkers: [ 'typescript' ],
tsconfigFile: 'tsconfig.json',
testRunner: 'jest',
reporters: [ 'progress', 'clear-text' ],
ignorePatterns: [
'coverage',
'reports',
'build',
'dist',
'home',
'scripts',
'docker-compose.*',
'public/servers.json*',
],
jest: {
projectType: 'custom',
config: jestConfig,
enableFindRelatedTests: true,
},
thresholds: {
high: 80,
low: 60,
break: null,
},
clearTextReporter: {
logTests: false,
},
};

View File

@@ -5,7 +5,11 @@ import type { ReactElement } from 'react';
export const setUpCanvas = (element: ReactElement) => { export const setUpCanvas = (element: ReactElement) => {
const result = render(element); const result = render(element);
const { container } = result; const { container } = result;
const getEvents = () => container.querySelector('canvas')?.getContext('2d')?.__getEvents(); // eslint-disable-line no-underscore-dangle const getEvents = () => {
const context = container.querySelector('canvas')?.getContext('2d');
// @ts-expect-error __getEvents is set by vitest-canvas-mock
return context?.__getEvents(); // eslint-disable-line no-underscore-dangle
};
return { ...result, events: getEvents(), getEvents }; return { ...result, events: getEvents(), getEvents };
}; };

View File

@@ -1,18 +1,18 @@
import { Mock } from 'ts-mockery'; import { fromAny, fromPartial } from '@total-typescript/shoehorn';
const createLinkMock = () => ({ const createLinkMock = () => ({
setAttribute: jest.fn(), setAttribute: vi.fn(),
click: jest.fn(), click: vi.fn(),
style: {}, style: {},
}); });
export const appendChild = jest.fn(); export const appendChild = vi.fn();
export const removeChild = jest.fn(); export const removeChild = vi.fn();
export const windowMock = Mock.of<Window>({ export const windowMock = fromPartial<Window>({
document: { document: fromAny({
createElement: jest.fn(createLinkMock), createElement: vi.fn(createLinkMock),
body: { appendChild, removeChild }, body: { appendChild, removeChild },
}, }),
}); });

View File

@@ -1,5 +1,5 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { Mock } from 'ts-mockery'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ShlinkApiErrorProps } from '../../src/api/ShlinkApiError'; import type { ShlinkApiErrorProps } from '../../src/api/ShlinkApiError';
import { ShlinkApiError } from '../../src/api/ShlinkApiError'; import { ShlinkApiError } from '../../src/api/ShlinkApiError';
import type { InvalidArgumentError, ProblemDetailsError } from '../../src/api/types/errors'; import type { InvalidArgumentError, ProblemDetailsError } from '../../src/api/types/errors';
@@ -10,8 +10,8 @@ describe('<ShlinkApiError />', () => {
it.each([ it.each([
[undefined, 'the fallback', 'the fallback'], [undefined, 'the fallback', 'the fallback'],
[Mock.all<ProblemDetailsError>(), 'the fallback', 'the fallback'], [fromPartial<ProblemDetailsError>({}), 'the fallback', 'the fallback'],
[Mock.of<ProblemDetailsError>({ detail: 'the detail' }), 'the fallback', 'the detail'], [fromPartial<ProblemDetailsError>({ detail: 'the detail' }), 'the fallback', 'the detail'],
])('renders proper message', (errorData, fallbackMessage, expectedMessage) => { ])('renders proper message', (errorData, fallbackMessage, expectedMessage) => {
const { container } = setUp({ errorData, fallbackMessage }); const { container } = setUp({ errorData, fallbackMessage });
@@ -21,9 +21,9 @@ describe('<ShlinkApiError />', () => {
it.each([ it.each([
[undefined, 0], [undefined, 0],
[Mock.all<ProblemDetailsError>(), 0], [fromPartial<ProblemDetailsError>({}), 0],
[Mock.of<InvalidArgumentError>({ type: ErrorTypeV2.INVALID_ARGUMENT, invalidElements: [] }), 1], [fromPartial<InvalidArgumentError>({ type: ErrorTypeV2.INVALID_ARGUMENT, invalidElements: [] }), 1],
[Mock.of<InvalidArgumentError>({ type: ErrorTypeV3.INVALID_ARGUMENT, invalidElements: [] }), 1], [fromPartial<InvalidArgumentError>({ type: ErrorTypeV3.INVALID_ARGUMENT, invalidElements: [] }), 1],
])('renders list of invalid elements when provided error is an InvalidError', (errorData, expectedElementsCount) => { ])('renders list of invalid elements when provided error is an InvalidError', (errorData, expectedElementsCount) => {
setUp({ errorData }); setUp({ errorData });
expect(screen.queryAllByText(/^Invalid elements/)).toHaveLength(expectedElementsCount); expect(screen.queryAllByText(/^Invalid elements/)).toHaveLength(expectedElementsCount);

View File

@@ -1,4 +1,4 @@
import { Mock } from 'ts-mockery'; import { fromPartial } from '@total-typescript/shoehorn';
import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient';
import type { ShlinkDomain, ShlinkVisits, ShlinkVisitsOverview } from '../../../src/api/types'; import type { ShlinkDomain, ShlinkVisits, ShlinkVisitsOverview } from '../../../src/api/types';
import { ErrorTypeV2, ErrorTypeV3 } from '../../../src/api/types/errors'; import { ErrorTypeV2, ErrorTypeV3 } from '../../../src/api/types/errors';
@@ -7,9 +7,9 @@ import type { ShortUrl, ShortUrlsOrder } from '../../../src/short-urls/data';
import type { OptionalString } from '../../../src/utils/utils'; import type { OptionalString } from '../../../src/utils/utils';
describe('ShlinkApiClient', () => { describe('ShlinkApiClient', () => {
const fetchJson = jest.fn().mockResolvedValue({}); const fetchJson = vi.fn().mockResolvedValue({});
const fetchEmpty = jest.fn().mockResolvedValue(undefined); const fetchEmpty = vi.fn().mockResolvedValue(undefined);
const httpClient = Mock.of<HttpClient>({ fetchJson, fetchEmpty }); const httpClient = fromPartial<HttpClient>({ fetchJson, fetchEmpty });
const buildApiClient = () => new ShlinkApiClient(httpClient, '', ''); const buildApiClient = () => new ShlinkApiClient(httpClient, '', '');
const shortCodesWithDomainCombinations: [string, OptionalString][] = [ const shortCodesWithDomainCombinations: [string, OptionalString][] = [
['abc123', null], ['abc123', null],
@@ -17,8 +17,6 @@ describe('ShlinkApiClient', () => {
['abc123', 'example.com'], ['abc123', 'example.com'],
]; ];
beforeEach(jest.clearAllMocks);
describe('listShortUrls', () => { describe('listShortUrls', () => {
const expectedList = ['foo', 'bar']; const expectedList = ['foo', 'bar'];
@@ -177,7 +175,7 @@ describe('ShlinkApiClient', () => {
maxVisits: 50, maxVisits: 50,
validSince: '2025-01-01T10:00:00+01:00', validSince: '2025-01-01T10:00:00+01:00',
}; };
const expectedResp = Mock.of<ShortUrl>(); const expectedResp = fromPartial<ShortUrl>({});
fetchJson.mockResolvedValue(expectedResp); fetchJson.mockResolvedValue(expectedResp);
const { updateShortUrl } = buildApiClient(); const { updateShortUrl } = buildApiClient();
const expectedQuery = domain ? `?domain=${domain}` : ''; const expectedQuery = domain ? `?domain=${domain}` : '';
@@ -311,7 +309,7 @@ describe('ShlinkApiClient', () => {
describe('listDomains', () => { describe('listDomains', () => {
it('returns domains', async () => { it('returns domains', async () => {
const expectedData = { data: [Mock.all<ShlinkDomain>(), Mock.all<ShlinkDomain>()] }; const expectedData = { data: [fromPartial<ShlinkDomain>({}), fromPartial<ShlinkDomain>({})] };
fetchJson.mockResolvedValue({ domains: expectedData }); fetchJson.mockResolvedValue({ domains: expectedData });
const { listDomains } = buildApiClient(); const { listDomains } = buildApiClient();
@@ -324,7 +322,7 @@ describe('ShlinkApiClient', () => {
describe('getVisitsOverview', () => { describe('getVisitsOverview', () => {
it('returns visits overview', async () => { it('returns visits overview', async () => {
const expectedData = Mock.all<ShlinkVisitsOverview>(); const expectedData = fromPartial<ShlinkVisitsOverview>({});
fetchJson.mockResolvedValue({ visits: expectedData }); fetchJson.mockResolvedValue({ visits: expectedData });
const { getVisitsOverview } = buildApiClient(); const { getVisitsOverview } = buildApiClient();
@@ -337,7 +335,7 @@ describe('ShlinkApiClient', () => {
describe('getOrphanVisits', () => { describe('getOrphanVisits', () => {
it('returns orphan visits', async () => { it('returns orphan visits', async () => {
fetchJson.mockResolvedValue({ visits: Mock.of<ShlinkVisits>({ data: [] }) }); fetchJson.mockResolvedValue({ visits: fromPartial<ShlinkVisits>({ data: [] }) });
const { getOrphanVisits } = buildApiClient(); const { getOrphanVisits } = buildApiClient();
const result = await getOrphanVisits(); const result = await getOrphanVisits();
@@ -349,7 +347,7 @@ describe('ShlinkApiClient', () => {
describe('getNonOrphanVisits', () => { describe('getNonOrphanVisits', () => {
it('returns non-orphan visits', async () => { it('returns non-orphan visits', async () => {
fetchJson.mockResolvedValue({ visits: Mock.of<ShlinkVisits>({ data: [] }) }); fetchJson.mockResolvedValue({ visits: fromPartial<ShlinkVisits>({ data: [] }) });
const { getNonOrphanVisits } = buildApiClient(); const { getNonOrphanVisits } = buildApiClient();
const result = await getNonOrphanVisits(); const result = await getNonOrphanVisits();

View File

@@ -1,15 +1,13 @@
import { Mock } from 'ts-mockery'; import { fromPartial } from '@total-typescript/shoehorn';
import { buildShlinkApiClient } from '../../../src/api/services/ShlinkApiClientBuilder'; import { buildShlinkApiClient } from '../../../src/api/services/ShlinkApiClientBuilder';
import type { HttpClient } from '../../../src/common/services/HttpClient';
import type { ShlinkState } from '../../../src/container/types';
import type { ReachableServer, SelectedServer } from '../../../src/servers/data'; import type { ReachableServer, SelectedServer } from '../../../src/servers/data';
describe('ShlinkApiClientBuilder', () => { describe('ShlinkApiClientBuilder', () => {
const server = (data: Partial<ReachableServer>) => Mock.of<ReachableServer>(data); const server = fromPartial<ReachableServer>;
const createBuilder = () => { const createBuilder = () => {
const builder = buildShlinkApiClient(Mock.of<HttpClient>()); const builder = buildShlinkApiClient(fromPartial({}));
return (selectedServer: SelectedServer) => builder(() => Mock.of<ShlinkState>({ selectedServer })); return (selectedServer: SelectedServer) => builder(() => fromPartial({ selectedServer }));
}; };
it('creates new instances when provided params are different', async () => { it('creates new instances when provided params are different', async () => {
@@ -42,7 +40,7 @@ describe('ShlinkApiClientBuilder', () => {
it('does not fetch from state when provided param is already selected server', () => { it('does not fetch from state when provided param is already selected server', () => {
const url = 'url'; const url = 'url';
const apiKey = 'apiKey'; const apiKey = 'apiKey';
const apiClient = buildShlinkApiClient(Mock.of<HttpClient>())(server({ url, apiKey })); const apiClient = buildShlinkApiClient(fromPartial({}))(server({ url, apiKey }));
expect(apiClient['baseUrl']).toEqual(url); // eslint-disable-line @typescript-eslint/dot-notation expect(apiClient['baseUrl']).toEqual(url); // eslint-disable-line @typescript-eslint/dot-notation
expect(apiClient['apiKey']).toEqual(apiKey); // eslint-disable-line @typescript-eslint/dot-notation expect(apiClient['apiKey']).toEqual(apiKey); // eslint-disable-line @typescript-eslint/dot-notation

View File

@@ -1,9 +1,8 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn';
import { createMemoryHistory } from 'history'; import { createMemoryHistory } from 'history';
import { Router } from 'react-router-dom'; import { Router } from 'react-router-dom';
import { Mock } from 'ts-mockery';
import { App as createApp } from '../../src/app/App'; import { App as createApp } from '../../src/app/App';
import type { Settings } from '../../src/settings/reducers/settings';
describe('<App />', () => { describe('<App />', () => {
const App = createApp( const App = createApp(
@@ -25,7 +24,7 @@ describe('<App />', () => {
<App <App
fetchServers={() => {}} fetchServers={() => {}}
servers={{}} servers={{}}
settings={Mock.all<Settings>()} settings={fromPartial({})}
appUpdated appUpdated
resetAppUpdate={() => {}} resetAppUpdate={() => {}}
/> />
@@ -33,8 +32,6 @@ describe('<App />', () => {
); );
}; };
afterEach(jest.clearAllMocks);
it('renders children components', () => { it('renders children components', () => {
setUp(); setUp();

View File

@@ -3,12 +3,10 @@ import { AppUpdateBanner } from '../../src/common/AppUpdateBanner';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
describe('<AppUpdateBanner />', () => { describe('<AppUpdateBanner />', () => {
const toggle = jest.fn(); const toggle = vi.fn();
const forceUpdate = jest.fn(); const forceUpdate = vi.fn();
const setUp = () => renderWithEvents(<AppUpdateBanner isOpen toggle={toggle} forceUpdate={forceUpdate} />); const setUp = () => renderWithEvents(<AppUpdateBanner isOpen toggle={toggle} forceUpdate={forceUpdate} />);
afterEach(jest.clearAllMocks);
it('renders initial state', () => { it('renders initial state', () => {
setUp(); setUp();

View File

@@ -1,14 +1,13 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom'; import { fromPartial } from '@total-typescript/shoehorn';
import { Mock } from 'ts-mockery'; import { MemoryRouter } from 'react-router';
import { AsideMenu as createAsideMenu } from '../../src/common/AsideMenu'; import { AsideMenu as createAsideMenu } from '../../src/common/AsideMenu';
import type { ReachableServer } from '../../src/servers/data';
describe('<AsideMenu />', () => { describe('<AsideMenu />', () => {
const AsideMenu = createAsideMenu(() => <>DeleteServerButton</>); const AsideMenu = createAsideMenu(() => <>DeleteServerButton</>);
const setUp = (id: string | false = 'abc123') => render( const setUp = (id: string | false = 'abc123') => render(
<MemoryRouter> <MemoryRouter>
<AsideMenu selectedServer={Mock.of<ReachableServer>({ id: id || undefined, version: '2.8.0' })} /> <AsideMenu selectedServer={fromPartial({ id: id || undefined, version: '2.8.0' })} />
</MemoryRouter>, </MemoryRouter>,
); );

View File

@@ -1,5 +1,5 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { Mock } from 'ts-mockery'; import { fromPartial } from '@total-typescript/shoehorn';
import { ErrorHandler as createErrorHandler } from '../../src/common/ErrorHandler'; import { ErrorHandler as createErrorHandler } from '../../src/common/ErrorHandler';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
@@ -8,17 +8,16 @@ const ComponentWithError = () => {
}; };
describe('<ErrorHandler />', () => { describe('<ErrorHandler />', () => {
const reload = jest.fn(); const reload = vi.fn();
const window = Mock.of<Window>({ const window = fromPartial<Window>({
location: { reload }, location: { reload },
}); });
const cons = Mock.of<Console>({ error: jest.fn() }); const cons = fromPartial<Console>({ error: vi.fn() });
const ErrorHandler = createErrorHandler(window, cons); const ErrorHandler = createErrorHandler(window, cons);
beforeEach(() => { beforeEach(() => {
jest.spyOn(console, 'error').mockImplementation(() => {}); // Silence react errors vi.spyOn(console, 'error').mockImplementation(() => {}); // Silence react errors
}); });
afterEach(jest.resetAllMocks);
it('renders children when no error has occurred', () => { it('renders children when no error has occurred', () => {
render(<ErrorHandler children={<span>Foo</span>} />); render(<ErrorHandler children={<span>Foo</span>} />);

View File

@@ -1,6 +1,6 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom'; import { fromPartial } from '@total-typescript/shoehorn';
import { Mock } from 'ts-mockery'; import { MemoryRouter } from 'react-router';
import { Home } from '../../src/common/Home'; import { Home } from '../../src/common/Home';
import type { ServersMap, ServerWithId } from '../../src/servers/data'; import type { ServersMap, ServerWithId } from '../../src/servers/data';
@@ -19,9 +19,9 @@ describe('<Home />', () => {
it.each([ it.each([
[ [
{ {
'1a': Mock.of<ServerWithId>({ name: 'foo', id: '1' }), '1a': fromPartial<ServerWithId>({ name: 'foo', id: '1' }),
'2b': Mock.of<ServerWithId>({ name: 'bar', id: '2' }), '2b': fromPartial<ServerWithId>({ name: 'bar', id: '2' }),
'3c': Mock.of<ServerWithId>({ name: 'baz', id: '3' }), '3c': fromPartial<ServerWithId>({ name: 'baz', id: '3' }),
}, },
3, 3,
], ],

View File

@@ -1,12 +1,12 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn';
import { createMemoryHistory } from 'history'; import { createMemoryHistory } from 'history';
import { Router, useParams } from 'react-router-dom'; import { Router, useParams } from 'react-router-dom';
import { Mock } from 'ts-mockery';
import { MenuLayout as createMenuLayout } from '../../src/common/MenuLayout'; import { MenuLayout as createMenuLayout } from '../../src/common/MenuLayout';
import type { NonReachableServer, NotFoundServer, ReachableServer, SelectedServer } from '../../src/servers/data'; import type { NonReachableServer, NotFoundServer, SelectedServer } from '../../src/servers/data';
import type { SemVer } from '../../src/utils/helpers/version'; import type { SemVer } from '../../src/utils/helpers/version';
jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useParams: jest.fn() })); vi.mock('react-router-dom', async () => ({ ...(await vi.importActual<any>('react-router-dom')), useParams: vi.fn() }));
describe('<MenuLayout />', () => { describe('<MenuLayout />', () => {
const MenuLayout = createMenuLayout( const MenuLayout = createMenuLayout(
@@ -31,9 +31,9 @@ describe('<MenuLayout />', () => {
return render( return render(
<Router location={history.location} navigator={history}> <Router location={history.location} navigator={history}>
<MenuLayout <MenuLayout
sidebarNotPresent={jest.fn()} sidebarNotPresent={vi.fn()}
sidebarPresent={jest.fn()} sidebarPresent={vi.fn()}
selectServer={jest.fn()} selectServer={vi.fn()}
selectedServer={selectedServer} selectedServer={selectedServer}
/> />
</Router>, </Router>,
@@ -44,8 +44,6 @@ describe('<MenuLayout />', () => {
(useParams as any).mockReturnValue({ serverId: 'abc123' }); (useParams as any).mockReturnValue({ serverId: 'abc123' });
}); });
afterEach(jest.clearAllMocks);
it('shows loading indicator while loading server', () => { it('shows loading indicator while loading server', () => {
setUp(null); setUp(null);
@@ -54,8 +52,8 @@ describe('<MenuLayout />', () => {
}); });
it.each([ it.each([
[Mock.of<NotFoundServer>({ serverNotFound: true })], [fromPartial<NotFoundServer>({ serverNotFound: true })],
[Mock.of<NonReachableServer>({ serverNotReachable: true })], [fromPartial<NonReachableServer>({ serverNotReachable: true })],
])('shows error for non reachable servers', (selectedServer) => { ])('shows error for non reachable servers', (selectedServer) => {
setUp(selectedServer); setUp(selectedServer);
@@ -81,7 +79,7 @@ describe('<MenuLayout />', () => {
])( ])(
'renders expected component based on location and server version', 'renders expected component based on location and server version',
(version, currentPath, expectedContent) => { (version, currentPath, expectedContent) => {
setUp(Mock.of<ReachableServer>({ version }), currentPath); setUp(fromPartial({ version }), currentPath);
expect(screen.getByText(expectedContent)).toBeInTheDocument(); expect(screen.getByText(expectedContent)).toBeInTheDocument();
}, },
); );

View File

@@ -1,5 +1,5 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router';
import { NotFound } from '../../src/common/NotFound'; import { NotFound } from '../../src/common/NotFound';
describe('<NotFound />', () => { describe('<NotFound />', () => {

View File

@@ -1,5 +1,5 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router';
import { ScrollToTop } from '../../src/common/ScrollToTop'; import { ScrollToTop } from '../../src/common/ScrollToTop';
describe('<ScrollToTop />', () => { describe('<ScrollToTop />', () => {

View File

@@ -1,5 +1,5 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { Mock } from 'ts-mockery'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ShlinkVersionsProps } from '../../src/common/ShlinkVersions'; import type { ShlinkVersionsProps } from '../../src/common/ShlinkVersions';
import { ShlinkVersions } from '../../src/common/ShlinkVersions'; import { ShlinkVersions } from '../../src/common/ShlinkVersions';
import type { NonReachableServer, NotFoundServer, ReachableServer } from '../../src/servers/data'; import type { NonReachableServer, NotFoundServer, ReachableServer } from '../../src/servers/data';
@@ -8,11 +8,11 @@ describe('<ShlinkVersions />', () => {
const setUp = (props: ShlinkVersionsProps) => render(<ShlinkVersions {...props} />); const setUp = (props: ShlinkVersionsProps) => render(<ShlinkVersions {...props} />);
it.each([ it.each([
['1.2.3', Mock.of<ReachableServer>({ version: '1.0.0', printableVersion: 'foo' }), 'v1.2.3', 'foo'], ['1.2.3', fromPartial<ReachableServer>({ version: '1.0.0', printableVersion: 'foo' }), 'v1.2.3', 'foo'],
['foo', Mock.of<ReachableServer>({ version: '1.0.0', printableVersion: '1.2.3' }), 'latest', '1.2.3'], ['foo', fromPartial<ReachableServer>({ version: '1.0.0', printableVersion: '1.2.3' }), 'latest', '1.2.3'],
['latest', Mock.of<ReachableServer>({ version: '1.0.0', printableVersion: 'latest' }), 'latest', 'latest'], ['latest', fromPartial<ReachableServer>({ version: '1.0.0', printableVersion: 'latest' }), 'latest', 'latest'],
['5.5.0', Mock.of<ReachableServer>({ version: '1.0.0', printableVersion: '0.2.8' }), 'v5.5.0', '0.2.8'], ['5.5.0', fromPartial<ReachableServer>({ version: '1.0.0', printableVersion: '0.2.8' }), 'v5.5.0', '0.2.8'],
['not-semver', Mock.of<ReachableServer>({ version: '1.0.0', printableVersion: 'some' }), 'latest', 'some'], ['not-semver', fromPartial<ReachableServer>({ version: '1.0.0', printableVersion: 'some' }), 'latest', 'some'],
])( ])(
'displays expected versions when selected server is reachable', 'displays expected versions when selected server is reachable',
(clientVersion, selectedServer, expectedClientVersion, expectedServerVersion) => { (clientVersion, selectedServer, expectedClientVersion, expectedServerVersion) => {
@@ -34,8 +34,8 @@ describe('<ShlinkVersions />', () => {
it.each([ it.each([
['1.2.3', null], ['1.2.3', null],
['1.2.3', Mock.of<NotFoundServer>({ serverNotFound: true })], ['1.2.3', fromPartial<NotFoundServer>({ serverNotFound: true })],
['1.2.3', Mock.of<NonReachableServer>({ serverNotReachable: true })], ['1.2.3', fromPartial<NonReachableServer>({ serverNotReachable: true })],
])('displays only client version when selected server is not reachable', (clientVersion, selectedServer) => { ])('displays only client version when selected server is not reachable', (clientVersion, selectedServer) => {
setUp({ clientVersion, selectedServer }); setUp({ clientVersion, selectedServer });
const links = screen.getAllByRole('link'); const links = screen.getAllByRole('link');

View File

@@ -1,12 +1,11 @@
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import { Mock } from 'ts-mockery'; import { fromPartial } from '@total-typescript/shoehorn';
import type { Sidebar } from '../../src/common/reducers/sidebar'; import type { Sidebar } from '../../src/common/reducers/sidebar';
import { ShlinkVersionsContainer } from '../../src/common/ShlinkVersionsContainer'; import { ShlinkVersionsContainer } from '../../src/common/ShlinkVersionsContainer';
import type { SelectedServer } from '../../src/servers/data';
describe('<ShlinkVersionsContainer />', () => { describe('<ShlinkVersionsContainer />', () => {
const setUp = (sidebar: Sidebar) => render( const setUp = (sidebar: Sidebar) => render(
<ShlinkVersionsContainer selectedServer={Mock.all<SelectedServer>()} sidebar={sidebar} />, <ShlinkVersionsContainer selectedServer={fromPartial({})} sidebar={sidebar} />,
); );
it.each([ it.each([

View File

@@ -4,7 +4,7 @@ import { ELLIPSIS } from '../../src/utils/helpers/pagination';
describe('<SimplePaginator />', () => { describe('<SimplePaginator />', () => {
const setUp = (pagesCount: number, currentPage = 1) => render( const setUp = (pagesCount: number, currentPage = 1) => render(
<SimplePaginator pagesCount={pagesCount} currentPage={currentPage} setCurrentPage={jest.fn()} />, <SimplePaginator pagesCount={pagesCount} currentPage={currentPage} setCurrentPage={vi.fn()} />,
); );
it.each([-3, -2, 0, 1])('renders empty when the amount of pages is smaller than 2', (pagesCount) => { it.each([-3, -2, 0, 1])('renders empty when the amount of pages is smaller than 2', (pagesCount) => {

View File

@@ -1,11 +1,9 @@
import { HttpClient } from '../../../src/common/services/HttpClient'; import { HttpClient } from '../../../src/common/services/HttpClient';
describe('HttpClient', () => { describe('HttpClient', () => {
const fetch = jest.fn(); const fetch = vi.fn();
const httpClient = new HttpClient(fetch); const httpClient = new HttpClient(fetch);
beforeEach(jest.clearAllMocks);
describe('fetchJson', () => { describe('fetchJson', () => {
it('throws json on success', async () => { it('throws json on success', async () => {
const theError = { error: true, foo: 'bar' }; const theError = { error: true, foo: 'bar' };

View File

@@ -1,15 +1,14 @@
import { Mock } from 'ts-mockery'; import { fromPartial } from '@total-typescript/shoehorn';
import type { HttpClient } from '../../../src/common/services/HttpClient'; import type { HttpClient } from '../../../src/common/services/HttpClient';
import { ImageDownloader } from '../../../src/common/services/ImageDownloader'; import { ImageDownloader } from '../../../src/common/services/ImageDownloader';
import { windowMock } from '../../__mocks__/Window.mock'; import { windowMock } from '../../__mocks__/Window.mock';
describe('ImageDownloader', () => { describe('ImageDownloader', () => {
const fetchBlob = jest.fn(); const fetchBlob = vi.fn();
const httpClient = Mock.of<HttpClient>({ fetchBlob }); const httpClient = fromPartial<HttpClient>({ fetchBlob });
let imageDownloader: ImageDownloader; let imageDownloader: ImageDownloader;
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks();
(global as any).URL = { createObjectURL: () => '' }; (global as any).URL = { createObjectURL: () => '' };
imageDownloader = new ImageDownloader(httpClient, windowMock); imageDownloader = new ImageDownloader(httpClient, windowMock);

View File

@@ -4,10 +4,9 @@ import type { NormalizedVisit } from '../../../src/visits/types';
import { windowMock } from '../../__mocks__/Window.mock'; import { windowMock } from '../../__mocks__/Window.mock';
describe('ReportExporter', () => { describe('ReportExporter', () => {
const jsonToCsv = jest.fn(); const jsonToCsv = vi.fn();
let exporter: ReportExporter; let exporter: ReportExporter;
beforeEach(jest.clearAllMocks);
beforeEach(() => { beforeEach(() => {
(global as any).Blob = class Blob {}; (global as any).Blob = class Blob {};
(global as any).URL = { createObjectURL: () => '' }; (global as any).URL = { createObjectURL: () => '' };
@@ -53,6 +52,7 @@ describe('ReportExporter', () => {
createdAt: '', createdAt: '',
longUrl: '', longUrl: '',
tags: '', tags: '',
shortCode: '',
}, },
]; ];

View File

@@ -1,17 +1,16 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { Mock } from 'ts-mockery'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ShlinkDomainRedirects } from '../../src/api/types'; import type { ShlinkDomainRedirects } from '../../src/api/types';
import type { Domain } from '../../src/domains/data'; import type { Domain } from '../../src/domains/data';
import { DomainRow } from '../../src/domains/DomainRow'; import { DomainRow } from '../../src/domains/DomainRow';
import type { SelectedServer } from '../../src/servers/data';
describe('<DomainRow />', () => { describe('<DomainRow />', () => {
const redirectsCombinations = [ const redirectsCombinations = [
[Mock.of<ShlinkDomainRedirects>({ baseUrlRedirect: 'foo' })], [fromPartial<ShlinkDomainRedirects>({ baseUrlRedirect: 'foo' })],
[Mock.of<ShlinkDomainRedirects>({ invalidShortUrlRedirect: 'bar' })], [fromPartial<ShlinkDomainRedirects>({ invalidShortUrlRedirect: 'bar' })],
[Mock.of<ShlinkDomainRedirects>({ baseUrlRedirect: 'baz', regular404Redirect: 'foo' })], [fromPartial<ShlinkDomainRedirects>({ baseUrlRedirect: 'baz', regular404Redirect: 'foo' })],
[ [
Mock.of<ShlinkDomainRedirects>( fromPartial<ShlinkDomainRedirects>(
{ baseUrlRedirect: 'baz', regular404Redirect: 'bar', invalidShortUrlRedirect: 'foo' }, { baseUrlRedirect: 'baz', regular404Redirect: 'bar', invalidShortUrlRedirect: 'foo' },
), ),
], ],
@@ -22,16 +21,16 @@ describe('<DomainRow />', () => {
<DomainRow <DomainRow
domain={domain} domain={domain}
defaultRedirects={defaultRedirects} defaultRedirects={defaultRedirects}
selectedServer={Mock.all<SelectedServer>()} selectedServer={fromPartial({})}
editDomainRedirects={jest.fn()} editDomainRedirects={vi.fn()}
checkDomainHealth={jest.fn()} checkDomainHealth={vi.fn()}
/> />
</tbody> </tbody>
</table>, </table>,
); );
it.each(redirectsCombinations)('shows expected redirects', (redirects) => { it.each(redirectsCombinations)('shows expected redirects', (redirects) => {
setUp(Mock.of<Domain>({ domain: '', isDefault: true, redirects })); setUp(fromPartial({ domain: '', isDefault: true, redirects }));
const cells = screen.getAllByRole('cell'); const cells = screen.getAllByRole('cell');
redirects?.baseUrlRedirect && expect(cells[1]).toHaveTextContent(redirects.baseUrlRedirect); redirects?.baseUrlRedirect && expect(cells[1]).toHaveTextContent(redirects.baseUrlRedirect);
@@ -42,9 +41,9 @@ describe('<DomainRow />', () => {
it.each([ it.each([
[undefined], [undefined],
[Mock.of<ShlinkDomainRedirects>()], [fromPartial<ShlinkDomainRedirects>({})],
])('shows expected "no redirects"', (redirects) => { ])('shows expected "no redirects"', (redirects) => {
setUp(Mock.of<Domain>({ domain: '', isDefault: true, redirects })); setUp(fromPartial({ domain: '', isDefault: true, redirects }));
const cells = screen.getAllByRole('cell'); const cells = screen.getAllByRole('cell');
expect(cells[1]).toHaveTextContent('No redirect'); expect(cells[1]).toHaveTextContent('No redirect');
@@ -54,7 +53,7 @@ describe('<DomainRow />', () => {
}); });
it.each(redirectsCombinations)('shows expected fallback redirects', (fallbackRedirects) => { it.each(redirectsCombinations)('shows expected fallback redirects', (fallbackRedirects) => {
setUp(Mock.of<Domain>({ domain: '', isDefault: true }), fallbackRedirects); setUp(fromPartial({ domain: '', isDefault: true }), fallbackRedirects);
const cells = screen.getAllByRole('cell'); const cells = screen.getAllByRole('cell');
fallbackRedirects?.baseUrlRedirect && expect(cells[1]).toHaveTextContent( fallbackRedirects?.baseUrlRedirect && expect(cells[1]).toHaveTextContent(
@@ -69,7 +68,7 @@ describe('<DomainRow />', () => {
}); });
it.each([[true], [false]])('shows icon on default domain only', (isDefault) => { it.each([[true], [false]])('shows icon on default domain only', (isDefault) => {
const { container } = setUp(Mock.of<Domain>({ domain: '', isDefault })); const { container } = setUp(fromPartial({ domain: '', isDefault }));
if (isDefault) { if (isDefault) {
expect(container.querySelector('#defaultDomainIcon')).toBeInTheDocument(); expect(container.querySelector('#defaultDomainIcon')).toBeInTheDocument();

View File

@@ -1,24 +1,21 @@
import { screen, waitFor } from '@testing-library/react'; import { screen, waitFor } from '@testing-library/react';
import { Mock } from 'ts-mockery'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ShlinkDomain } from '../../src/api/types';
import { DomainSelector } from '../../src/domains/DomainSelector'; import { DomainSelector } from '../../src/domains/DomainSelector';
import type { DomainsList } from '../../src/domains/reducers/domainsList'; import type { DomainsList } from '../../src/domains/reducers/domainsList';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
describe('<DomainSelector />', () => { describe('<DomainSelector />', () => {
const domainsList = Mock.of<DomainsList>({ const domainsList = fromPartial<DomainsList>({
domains: [ domains: [
Mock.of<ShlinkDomain>({ domain: 'default.com', isDefault: true }), fromPartial({ domain: 'default.com', isDefault: true }),
Mock.of<ShlinkDomain>({ domain: 'foo.com' }), fromPartial({ domain: 'foo.com' }),
Mock.of<ShlinkDomain>({ domain: 'bar.com' }), fromPartial({ domain: 'bar.com' }),
], ],
}); });
const setUp = (value = '') => renderWithEvents( const setUp = (value = '') => renderWithEvents(
<DomainSelector value={value} domainsList={domainsList} listDomains={jest.fn()} onChange={jest.fn()} />, <DomainSelector value={value} domainsList={domainsList} listDomains={vi.fn()} onChange={vi.fn()} />,
); );
afterEach(jest.clearAllMocks);
it.each([ it.each([
['', 'Domain', 'domains-dropdown__toggle-btn'], ['', 'Domain', 'domains-dropdown__toggle-btn'],
['my-domain.com', 'Domain: my-domain.com', 'domains-dropdown__toggle-btn--active'], ['my-domain.com', 'Domain: my-domain.com', 'domains-dropdown__toggle-btn--active'],
@@ -27,9 +24,8 @@ describe('<DomainSelector />', () => {
const btn = screen.getByRole('button', { name: expectedText }); const btn = screen.getByRole('button', { name: expectedText });
expect(screen.queryByPlaceholderText('Domain')).not.toBeInTheDocument(); expect(screen.queryByPlaceholderText('Domain')).not.toBeInTheDocument();
expect(btn).toHaveAttribute( expect(btn).toHaveClass(
'class', `dropdown-btn__toggle ${expectedClassName} btn-block dropdown-btn__toggle--with-caret dropdown-toggle btn btn-primary`,
`dropdown-btn__toggle btn-block ${expectedClassName} dropdown-toggle btn btn-primary`,
); );
await user.click(btn); await user.click(btn);

View File

@@ -1,30 +1,27 @@
import { screen, waitFor } from '@testing-library/react'; import { screen, waitFor } from '@testing-library/react';
import { Mock } from 'ts-mockery'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ShlinkDomain } from '../../src/api/types'; import type { ShlinkDomain } from '../../src/api/types';
import type { ProblemDetailsError } from '../../src/api/types/errors'; import type { ProblemDetailsError } from '../../src/api/types/errors';
import { ManageDomains } from '../../src/domains/ManageDomains'; import { ManageDomains } from '../../src/domains/ManageDomains';
import type { DomainsList } from '../../src/domains/reducers/domainsList'; import type { DomainsList } from '../../src/domains/reducers/domainsList';
import type { SelectedServer } from '../../src/servers/data';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
describe('<ManageDomains />', () => { describe('<ManageDomains />', () => {
const listDomains = jest.fn(); const listDomains = vi.fn();
const filterDomains = jest.fn(); const filterDomains = vi.fn();
const setUp = (domainsList: DomainsList) => renderWithEvents( const setUp = (domainsList: DomainsList) => renderWithEvents(
<ManageDomains <ManageDomains
listDomains={listDomains} listDomains={listDomains}
filterDomains={filterDomains} filterDomains={filterDomains}
editDomainRedirects={jest.fn()} editDomainRedirects={vi.fn()}
checkDomainHealth={jest.fn()} checkDomainHealth={vi.fn()}
domainsList={domainsList} domainsList={domainsList}
selectedServer={Mock.all<SelectedServer>()} selectedServer={fromPartial({})}
/>, />,
); );
afterEach(jest.clearAllMocks);
it('shows loading message while domains are loading', () => { it('shows loading message while domains are loading', () => {
setUp(Mock.of<DomainsList>({ loading: true, filteredDomains: [] })); setUp(fromPartial({ loading: true, filteredDomains: [] }));
expect(screen.getByText('Loading...')).toBeInTheDocument(); expect(screen.getByText('Loading...')).toBeInTheDocument();
expect(screen.queryByText('Error loading domains :(')).not.toBeInTheDocument(); expect(screen.queryByText('Error loading domains :(')).not.toBeInTheDocument();
@@ -32,17 +29,17 @@ describe('<ManageDomains />', () => {
it.each([ it.each([
[undefined, 'Error loading domains :('], [undefined, 'Error loading domains :('],
[Mock.of<ProblemDetailsError>(), 'Error loading domains :('], [fromPartial<ProblemDetailsError>({}), 'Error loading domains :('],
[Mock.of<ProblemDetailsError>({ detail: 'Foo error!!' }), 'Foo error!!'], [fromPartial<ProblemDetailsError>({ detail: 'Foo error!!' }), 'Foo error!!'],
])('shows error result when domains loading fails', (errorData, expectedErrorMessage) => { ])('shows error result when domains loading fails', (errorData, expectedErrorMessage) => {
setUp(Mock.of<DomainsList>({ loading: false, error: true, errorData, filteredDomains: [] })); setUp(fromPartial({ loading: false, error: true, errorData, filteredDomains: [] }));
expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
expect(screen.getByText(expectedErrorMessage)).toBeInTheDocument(); expect(screen.getByText(expectedErrorMessage)).toBeInTheDocument();
}); });
it('filters domains when SearchField changes', async () => { it('filters domains when SearchField changes', async () => {
const { user } = setUp(Mock.of<DomainsList>({ loading: false, error: false, filteredDomains: [] })); const { user } = setUp(fromPartial({ loading: false, error: false, filteredDomains: [] }));
expect(filterDomains).not.toHaveBeenCalled(); expect(filterDomains).not.toHaveBeenCalled();
await user.type(screen.getByPlaceholderText('Search...'), 'Foo'); await user.type(screen.getByPlaceholderText('Search...'), 'Foo');
@@ -50,19 +47,19 @@ describe('<ManageDomains />', () => {
}); });
it('shows expected headers and one row when list of domains is empty', () => { it('shows expected headers and one row when list of domains is empty', () => {
setUp(Mock.of<DomainsList>({ loading: false, error: false, filteredDomains: [] })); setUp(fromPartial({ loading: false, error: false, filteredDomains: [] }));
expect(screen.getAllByRole('columnheader')).toHaveLength(7); expect(screen.getAllByRole('columnheader')).toHaveLength(7);
expect(screen.getByText('No results found')).toBeInTheDocument(); expect(screen.getByText('No results found')).toBeInTheDocument();
}); });
it('has many rows if multiple domains are provided', () => { it('has many rows if multiple domains are provided', () => {
const filteredDomains = [ const filteredDomains: ShlinkDomain[] = [
Mock.of<ShlinkDomain>({ domain: 'foo' }), fromPartial({ domain: 'foo' }),
Mock.of<ShlinkDomain>({ domain: 'bar' }), fromPartial({ domain: 'bar' }),
Mock.of<ShlinkDomain>({ domain: 'baz' }), fromPartial({ domain: 'baz' }),
]; ];
setUp(Mock.of<DomainsList>({ loading: false, error: false, filteredDomains })); setUp(fromPartial({ loading: false, error: false, filteredDomains }));
expect(screen.getAllByRole('row')).toHaveLength(filteredDomains.length + 1); expect(screen.getAllByRole('row')).toHaveLength(filteredDomains.length + 1);
expect(screen.getByText('foo')).toBeInTheDocument(); expect(screen.getByText('foo')).toBeInTheDocument();

View File

@@ -1,26 +1,24 @@
import { screen, waitForElementToBeRemoved } from '@testing-library/react'; import { screen, waitForElementToBeRemoved } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { Mock } from 'ts-mockery';
import type { Domain } from '../../../src/domains/data'; import type { Domain } from '../../../src/domains/data';
import { DomainDropdown } from '../../../src/domains/helpers/DomainDropdown'; import { DomainDropdown } from '../../../src/domains/helpers/DomainDropdown';
import type { ReachableServer, SelectedServer } from '../../../src/servers/data'; import type { SelectedServer } from '../../../src/servers/data';
import type { SemVer } from '../../../src/utils/helpers/version'; import type { SemVer } from '../../../src/utils/helpers/version';
import { renderWithEvents } from '../../__helpers__/setUpTest'; import { renderWithEvents } from '../../__helpers__/setUpTest';
describe('<DomainDropdown />', () => { describe('<DomainDropdown />', () => {
const editDomainRedirects = jest.fn().mockResolvedValue(undefined); const editDomainRedirects = vi.fn().mockResolvedValue(undefined);
const setUp = (domain?: Domain, selectedServer?: SelectedServer) => renderWithEvents( const setUp = (domain?: Domain, selectedServer?: SelectedServer) => renderWithEvents(
<MemoryRouter> <MemoryRouter>
<DomainDropdown <DomainDropdown
domain={domain ?? Mock.all<Domain>()} domain={domain ?? fromPartial({})}
selectedServer={selectedServer ?? Mock.all<SelectedServer>()} selectedServer={selectedServer ?? fromPartial({})}
editDomainRedirects={editDomainRedirects} editDomainRedirects={editDomainRedirects}
/> />
</MemoryRouter>, </MemoryRouter>,
); );
afterEach(jest.clearAllMocks);
it('renders expected menu items', () => { it('renders expected menu items', () => {
setUp(); setUp();
@@ -33,8 +31,8 @@ describe('<DomainDropdown />', () => {
[false, ''], [false, ''],
])('points first link to the proper section', (isDefault, expectedLink) => { ])('points first link to the proper section', (isDefault, expectedLink) => {
setUp( setUp(
Mock.of<Domain>({ domain: 'foo.com', isDefault }), fromPartial({ domain: 'foo.com', isDefault }),
Mock.of<ReachableServer>({ version: '3.1.0', id: '123' }), fromPartial({ version: '3.1.0', id: '123' }),
); );
expect(screen.getByText('Visit stats')).toHaveAttribute('href', `/server/123/domain/foo.com${expectedLink}/visits`); expect(screen.getByText('Visit stats')).toHaveAttribute('href', `/server/123/domain/foo.com${expectedLink}/visits`);
@@ -46,8 +44,8 @@ describe('<DomainDropdown />', () => {
[false, '2.9.0' as SemVer, true], [false, '2.9.0' as SemVer, true],
])('allows editing certain the domains', (isDefault, serverVersion, canBeEdited) => { ])('allows editing certain the domains', (isDefault, serverVersion, canBeEdited) => {
setUp( setUp(
Mock.of<Domain>({ domain: 'foo.com', isDefault }), fromPartial({ domain: 'foo.com', isDefault }),
Mock.of<ReachableServer>({ version: serverVersion, id: '123' }), fromPartial({ version: serverVersion, id: '123' }),
); );
if (canBeEdited) { if (canBeEdited) {
@@ -62,7 +60,7 @@ describe('<DomainDropdown />', () => {
['bar.org'], ['bar.org'],
['baz.net'], ['baz.net'],
])('displays modal when editing redirects', async (domain) => { ])('displays modal when editing redirects', async (domain) => {
const { user } = setUp(Mock.of<Domain>({ domain, isDefault: false })); const { user } = setUp(fromPartial({ domain, isDefault: false }));
expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
expect(screen.queryByRole('form')).not.toBeInTheDocument(); expect(screen.queryByRole('form')).not.toBeInTheDocument();

View File

@@ -1,17 +1,15 @@
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { Mock } from 'ts-mockery'; import { fromPartial } from '@total-typescript/shoehorn';
import type { DomainStatus } from '../../../src/domains/data'; import type { DomainStatus } from '../../../src/domains/data';
import { DomainStatusIcon } from '../../../src/domains/helpers/DomainStatusIcon'; import { DomainStatusIcon } from '../../../src/domains/helpers/DomainStatusIcon';
import { renderWithEvents } from '../../__helpers__/setUpTest'; import { renderWithEvents } from '../../__helpers__/setUpTest';
describe('<DomainStatusIcon />', () => { describe('<DomainStatusIcon />', () => {
const matchMedia = jest.fn().mockReturnValue(Mock.of<MediaQueryList>({ matches: false })); const matchMedia = vi.fn().mockReturnValue(fromPartial<MediaQueryList>({ matches: false }));
const setUp = (status: DomainStatus) => renderWithEvents( const setUp = (status: DomainStatus) => renderWithEvents(
<DomainStatusIcon status={status} matchMedia={matchMedia} />, <DomainStatusIcon status={status} matchMedia={matchMedia} />,
); );
beforeEach(jest.clearAllMocks);
it.each([ it.each([
['validating' as DomainStatus], ['validating' as DomainStatus],
['invalid' as DomainStatus], ['invalid' as DomainStatus],

View File

@@ -1,13 +1,13 @@
import { fireEvent, screen, waitFor } from '@testing-library/react'; import { fireEvent, screen, waitFor } from '@testing-library/react';
import { Mock } from 'ts-mockery'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ShlinkDomain } from '../../../src/api/types'; import type { ShlinkDomain } from '../../../src/api/types';
import { EditDomainRedirectsModal } from '../../../src/domains/helpers/EditDomainRedirectsModal'; import { EditDomainRedirectsModal } from '../../../src/domains/helpers/EditDomainRedirectsModal';
import { renderWithEvents } from '../../__helpers__/setUpTest'; import { renderWithEvents } from '../../__helpers__/setUpTest';
describe('<EditDomainRedirectsModal />', () => { describe('<EditDomainRedirectsModal />', () => {
const editDomainRedirects = jest.fn().mockResolvedValue(undefined); const editDomainRedirects = vi.fn().mockResolvedValue(undefined);
const toggle = jest.fn(); const toggle = vi.fn();
const domain = Mock.of<ShlinkDomain>({ const domain = fromPartial<ShlinkDomain>({
domain: 'foo.com', domain: 'foo.com',
redirects: { redirects: {
baseUrlRedirect: 'baz', baseUrlRedirect: 'baz',
@@ -17,8 +17,6 @@ describe('<EditDomainRedirectsModal />', () => {
<EditDomainRedirectsModal domain={domain} isOpen toggle={toggle} editDomainRedirects={editDomainRedirects} />, <EditDomainRedirectsModal domain={domain} isOpen toggle={toggle} editDomainRedirects={editDomainRedirects} />,
); );
afterEach(jest.clearAllMocks);
it('renders domain in header', () => { it('renders domain in header', () => {
setUp(); setUp();
expect(screen.getByRole('heading')).toHaveTextContent('Edit redirects for foo.com'); expect(screen.getByRole('heading')).toHaveTextContent('Edit redirects for foo.com');

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<DomainStatusIcon /> renders expected icon and tooltip when status is not validating 1`] = ` exports[`<DomainStatusIcon /> > renders expected icon and tooltip when status is not validating 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="svg-inline--fa fa-circle-notch fa-spin fa-fw " class="svg-inline--fa fa-circle-notch fa-spin fa-fw "
@@ -18,7 +18,7 @@ exports[`<DomainStatusIcon /> renders expected icon and tooltip when status is n
</svg> </svg>
`; `;
exports[`<DomainStatusIcon /> renders expected icon and tooltip when status is not validating 2`] = ` exports[`<DomainStatusIcon /> > renders expected icon and tooltip when status is not validating 2`] = `
<span> <span>
<svg <svg
aria-hidden="true" aria-hidden="true"
@@ -38,7 +38,7 @@ exports[`<DomainStatusIcon /> renders expected icon and tooltip when status is n
</span> </span>
`; `;
exports[`<DomainStatusIcon /> renders expected icon and tooltip when status is not validating 3`] = ` exports[`<DomainStatusIcon /> > renders expected icon and tooltip when status is not validating 3`] = `
<span> <span>
<svg <svg
aria-hidden="true" aria-hidden="true"
@@ -58,7 +58,7 @@ exports[`<DomainStatusIcon /> renders expected icon and tooltip when status is n
</span> </span>
`; `;
exports[`<DomainStatusIcon /> renders proper tooltip based on state 1`] = ` exports[`<DomainStatusIcon /> > renders proper tooltip based on state 1`] = `
<div <div
class="tooltip-inner" class="tooltip-inner"
role="tooltip" role="tooltip"
@@ -79,7 +79,7 @@ exports[`<DomainStatusIcon /> renders proper tooltip based on state 1`] = `
</div> </div>
`; `;
exports[`<DomainStatusIcon /> renders proper tooltip based on state 2`] = ` exports[`<DomainStatusIcon /> > renders proper tooltip based on state 2`] = `
<div <div
class="tooltip-inner" class="tooltip-inner"
role="tooltip" role="tooltip"

View File

@@ -1,25 +1,22 @@
import { Mock } from 'ts-mockery'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; import type { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient';
import type { ShlinkDomainRedirects } from '../../../src/api/types'; import type { ShlinkDomainRedirects } from '../../../src/api/types';
import type { EditDomainRedirects } from '../../../src/domains/reducers/domainRedirects';
import { editDomainRedirects } from '../../../src/domains/reducers/domainRedirects'; import { editDomainRedirects } from '../../../src/domains/reducers/domainRedirects';
describe('domainRedirectsReducer', () => { describe('domainRedirectsReducer', () => {
beforeEach(jest.clearAllMocks);
describe('editDomainRedirects', () => { describe('editDomainRedirects', () => {
const domain = 'example.com'; const domain = 'example.com';
const redirects = Mock.all<ShlinkDomainRedirects>(); const redirects = fromPartial<ShlinkDomainRedirects>({});
const dispatch = jest.fn(); const dispatch = vi.fn();
const getState = jest.fn(); const getState = vi.fn();
const editDomainRedirectsCall = jest.fn(); const editDomainRedirectsCall = vi.fn();
const buildShlinkApiClient = () => Mock.of<ShlinkApiClient>({ editDomainRedirects: editDomainRedirectsCall }); const buildShlinkApiClient = () => fromPartial<ShlinkApiClient>({ editDomainRedirects: editDomainRedirectsCall });
const editDomainRedirectsAction = editDomainRedirects(buildShlinkApiClient); const editDomainRedirectsAction = editDomainRedirects(buildShlinkApiClient);
it('dispatches domain and redirects once loaded', async () => { it('dispatches domain and redirects once loaded', async () => {
editDomainRedirectsCall.mockResolvedValue(redirects); editDomainRedirectsCall.mockResolvedValue(redirects);
await editDomainRedirectsAction(Mock.of<EditDomainRedirects>({ domain }))(dispatch, getState, {}); await editDomainRedirectsAction(fromPartial({ domain }))(dispatch, getState, {});
expect(dispatch).toHaveBeenCalledTimes(2); expect(dispatch).toHaveBeenCalledTimes(2);
expect(dispatch).toHaveBeenLastCalledWith(expect.objectContaining({ expect(dispatch).toHaveBeenLastCalledWith(expect.objectContaining({

View File

@@ -1,4 +1,4 @@
import { Mock } from 'ts-mockery'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; import type { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient';
import type { ShlinkDomainRedirects } from '../../../src/api/types'; import type { ShlinkDomainRedirects } from '../../../src/api/types';
import { parseApiError } from '../../../src/api/utils'; import { parseApiError } from '../../../src/api/utils';
@@ -6,26 +6,23 @@ import type { ShlinkState } from '../../../src/container/types';
import type { Domain } from '../../../src/domains/data'; import type { Domain } from '../../../src/domains/data';
import type { EditDomainRedirects } from '../../../src/domains/reducers/domainRedirects'; import type { EditDomainRedirects } from '../../../src/domains/reducers/domainRedirects';
import { editDomainRedirects } from '../../../src/domains/reducers/domainRedirects'; import { editDomainRedirects } from '../../../src/domains/reducers/domainRedirects';
import type {
DomainsList } from '../../../src/domains/reducers/domainsList';
import { import {
domainsListReducerCreator, domainsListReducerCreator,
replaceRedirectsOnDomain, replaceRedirectsOnDomain,
replaceStatusOnDomain, replaceStatusOnDomain,
} from '../../../src/domains/reducers/domainsList'; } from '../../../src/domains/reducers/domainsList';
import type { SelectedServer, ServerData } from '../../../src/servers/data';
describe('domainsListReducer', () => { describe('domainsListReducer', () => {
const dispatch = jest.fn(); const dispatch = vi.fn();
const getState = jest.fn(); const getState = vi.fn();
const listDomains = jest.fn(); const listDomains = vi.fn();
const health = jest.fn(); const health = vi.fn();
const buildShlinkApiClient = () => Mock.of<ShlinkApiClient>({ listDomains, health }); const buildShlinkApiClient = () => fromPartial<ShlinkApiClient>({ listDomains, health });
const filteredDomains = [ const filteredDomains: Domain[] = [
Mock.of<Domain>({ domain: 'foo', status: 'validating' }), fromPartial({ domain: 'foo', status: 'validating' }),
Mock.of<Domain>({ domain: 'Boo', status: 'validating' }), fromPartial({ domain: 'Boo', status: 'validating' }),
]; ];
const domains = [...filteredDomains, Mock.of<Domain>({ domain: 'bar', status: 'validating' })]; const domains: Domain[] = [...filteredDomains, fromPartial({ domain: 'bar', status: 'validating' })];
const error = { type: 'NOT_FOUND', status: 404 } as unknown as Error; const error = { type: 'NOT_FOUND', status: 404 } as unknown as Error;
const editDomainRedirectsThunk = editDomainRedirects(buildShlinkApiClient); const editDomainRedirectsThunk = editDomainRedirects(buildShlinkApiClient);
const { reducer, listDomains: listDomainsAction, checkDomainHealth, filterDomains } = domainsListReducerCreator( const { reducer, listDomains: listDomainsAction, checkDomainHealth, filterDomains } = domainsListReducerCreator(
@@ -33,8 +30,6 @@ describe('domainsListReducer', () => {
editDomainRedirectsThunk, editDomainRedirectsThunk,
); );
beforeEach(jest.clearAllMocks);
describe('reducer', () => { describe('reducer', () => {
it('returns loading on LIST_DOMAINS_START', () => { it('returns loading on LIST_DOMAINS_START', () => {
expect(reducer(undefined, listDomainsAction.pending(''))).toEqual( expect(reducer(undefined, listDomainsAction.pending(''))).toEqual(
@@ -55,7 +50,7 @@ describe('domainsListReducer', () => {
}); });
it('filters domains on FILTER_DOMAINS', () => { it('filters domains on FILTER_DOMAINS', () => {
expect(reducer(Mock.of<DomainsList>({ domains }), filterDomains('oO'))).toEqual({ domains, filteredDomains }); expect(reducer(fromPartial({ domains }), filterDomains('oO'))).toEqual({ domains, filteredDomains });
}); });
it.each([ it.each([
@@ -71,7 +66,7 @@ describe('domainsListReducer', () => {
const editDomainRedirects: EditDomainRedirects = { domain, redirects }; const editDomainRedirects: EditDomainRedirects = { domain, redirects };
expect(reducer( expect(reducer(
Mock.of<DomainsList>({ domains, filteredDomains }), fromPartial({ domains, filteredDomains }),
editDomainRedirectsThunk.fulfilled(editDomainRedirects, '', editDomainRedirects), editDomainRedirectsThunk.fulfilled(editDomainRedirects, '', editDomainRedirects),
)).toEqual({ )).toEqual({
domains: domains.map(replaceRedirectsOnDomain(editDomainRedirects)), domains: domains.map(replaceRedirectsOnDomain(editDomainRedirects)),
@@ -85,7 +80,7 @@ describe('domainsListReducer', () => {
['does_not_exist'], ['does_not_exist'],
])('replaces status on proper domain on VALIDATE_DOMAIN', (domain) => { ])('replaces status on proper domain on VALIDATE_DOMAIN', (domain) => {
expect(reducer( expect(reducer(
Mock.of<DomainsList>({ domains, filteredDomains }), fromPartial({ domains, filteredDomains }),
checkDomainHealth.fulfilled({ domain, status: 'valid' }, '', ''), checkDomainHealth.fulfilled({ domain, status: 'valid' }, '', ''),
)).toEqual({ )).toEqual({
domains: domains.map(replaceStatusOnDomain(domain, 'valid')), domains: domains.map(replaceStatusOnDomain(domain, 'valid')),
@@ -122,8 +117,8 @@ describe('domainsListReducer', () => {
const domain = 'example.com'; const domain = 'example.com';
it('dispatches invalid status when selected server does not have all required data', async () => { it('dispatches invalid status when selected server does not have all required data', async () => {
getState.mockReturnValue(Mock.of<ShlinkState>({ getState.mockReturnValue(fromPartial<ShlinkState>({
selectedServer: Mock.all<SelectedServer>(), selectedServer: {},
})); }));
await checkDomainHealth(domain)(dispatch, getState, {}); await checkDomainHealth(domain)(dispatch, getState, {});
@@ -136,11 +131,11 @@ describe('domainsListReducer', () => {
}); });
it('dispatches invalid status when health endpoint returns an error', async () => { it('dispatches invalid status when health endpoint returns an error', async () => {
getState.mockReturnValue(Mock.of<ShlinkState>({ getState.mockReturnValue(fromPartial<ShlinkState>({
selectedServer: Mock.of<ServerData>({ selectedServer: {
url: 'https://myerver.com', url: 'https://myerver.com',
apiKey: '123', apiKey: '123',
}), },
})); }));
health.mockRejectedValue({}); health.mockRejectedValue({});
@@ -160,11 +155,11 @@ describe('domainsListReducer', () => {
healthStatus, healthStatus,
expectedStatus, expectedStatus,
) => { ) => {
getState.mockReturnValue(Mock.of<ShlinkState>({ getState.mockReturnValue(fromPartial<ShlinkState>({
selectedServer: Mock.of<ServerData>({ selectedServer: {
url: 'https://myerver.com', url: 'https://myerver.com',
apiKey: '123', apiKey: '123',
}), },
})); }));
health.mockResolvedValue({ status: healthStatus }); health.mockResolvedValue({ status: healthStatus });

View File

@@ -1,24 +1,22 @@
import { fromPartial } from '@total-typescript/shoehorn';
import { EventSourcePolyfill } from 'event-source-polyfill'; import { EventSourcePolyfill } from 'event-source-polyfill';
import { identity } from 'ramda'; import { identity } from 'ramda';
import { Mock } from 'ts-mockery';
import { bindToMercureTopic } from '../../../src/mercure/helpers'; import { bindToMercureTopic } from '../../../src/mercure/helpers';
import type { MercureInfo } from '../../../src/mercure/reducers/mercureInfo'; import type { MercureInfo } from '../../../src/mercure/reducers/mercureInfo';
jest.mock('event-source-polyfill'); vi.mock('event-source-polyfill');
describe('helpers', () => { describe('helpers', () => {
afterEach(jest.resetAllMocks);
describe('bindToMercureTopic', () => { describe('bindToMercureTopic', () => {
const onMessage = jest.fn(); const onMessage = vi.fn();
const onTokenExpired = jest.fn(); const onTokenExpired = vi.fn();
it.each([ it.each([
[Mock.of<MercureInfo>({ loading: true, error: false, mercureHubUrl: 'foo' })], [fromPartial<MercureInfo>({ loading: true, error: false, mercureHubUrl: 'foo' })],
[Mock.of<MercureInfo>({ loading: false, error: true, mercureHubUrl: 'foo' })], [fromPartial<MercureInfo>({ loading: false, error: true, mercureHubUrl: 'foo' })],
[Mock.of<MercureInfo>({ loading: true, error: true, mercureHubUrl: 'foo' })], [fromPartial<MercureInfo>({ loading: true, error: true, mercureHubUrl: 'foo' })],
[Mock.of<MercureInfo>({ loading: false, error: false, mercureHubUrl: undefined })], [fromPartial<MercureInfo>({ loading: false, error: false, mercureHubUrl: undefined })],
[Mock.of<MercureInfo>({ loading: true, error: true, mercureHubUrl: undefined })], [fromPartial<MercureInfo>({ loading: true, error: true, mercureHubUrl: undefined })],
])('does not bind an EventSource when loading, error or no hub URL', (mercureInfo) => { ])('does not bind an EventSource when loading, error or no hub URL', (mercureInfo) => {
bindToMercureTopic(mercureInfo, [''], identity, () => {}); bindToMercureTopic(mercureInfo, [''], identity, () => {});

View File

@@ -1,4 +1,4 @@
import { Mock } from 'ts-mockery'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; import type { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient';
import type { GetState } from '../../../src/container/types'; import type { GetState } from '../../../src/container/types';
import { mercureInfoReducerCreator } from '../../../src/mercure/reducers/mercureInfo'; import { mercureInfoReducerCreator } from '../../../src/mercure/reducers/mercureInfo';
@@ -8,12 +8,10 @@ describe('mercureInfoReducer', () => {
mercureHubUrl: 'http://example.com/.well-known/mercure', mercureHubUrl: 'http://example.com/.well-known/mercure',
token: 'abc.123.def', token: 'abc.123.def',
}; };
const getMercureInfo = jest.fn(); const getMercureInfo = vi.fn();
const buildShlinkApiClient = () => Mock.of<ShlinkApiClient>({ mercureInfo: getMercureInfo }); const buildShlinkApiClient = () => fromPartial<ShlinkApiClient>({ mercureInfo: getMercureInfo });
const { loadMercureInfo, reducer } = mercureInfoReducerCreator(buildShlinkApiClient); const { loadMercureInfo, reducer } = mercureInfoReducerCreator(buildShlinkApiClient);
beforeEach(jest.resetAllMocks);
describe('reducer', () => { describe('reducer', () => {
it('returns loading on GET_MERCURE_INFO_START', () => { it('returns loading on GET_MERCURE_INFO_START', () => {
expect(reducer(undefined, loadMercureInfo.pending(''))).toEqual({ expect(reducer(undefined, loadMercureInfo.pending(''))).toEqual({
@@ -37,8 +35,8 @@ describe('mercureInfoReducer', () => {
}); });
describe('loadMercureInfo', () => { describe('loadMercureInfo', () => {
const dispatch = jest.fn(); const dispatch = vi.fn();
const createGetStateMock = (enabled: boolean): GetState => jest.fn().mockReturnValue({ const createGetStateMock = (enabled: boolean): GetState => vi.fn().mockReturnValue({
settings: { settings: {
realTimeUpdates: { enabled }, realTimeUpdates: { enabled },
}, },

View File

@@ -1,21 +1,24 @@
import { fireEvent, screen, waitFor } from '@testing-library/react'; import { fireEvent, screen, waitFor } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Mock } from 'ts-mockery';
import { CreateServer as createCreateServer } from '../../src/servers/CreateServer'; import { CreateServer as createCreateServer } from '../../src/servers/CreateServer';
import type { ServerWithId } from '../../src/servers/data'; import type { ServerWithId } from '../../src/servers/data';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useNavigate: jest.fn() })); vi.mock('react-router-dom', async () => ({
...(await vi.importActual<any>('react-router-dom')),
useNavigate: vi.fn(),
}));
describe('<CreateServer />', () => { describe('<CreateServer />', () => {
const createServersMock = jest.fn(); const createServersMock = vi.fn();
const navigate = jest.fn(); const navigate = vi.fn();
const servers = { foo: Mock.of<ServerWithId>({ url: 'https://existing_url.com', apiKey: 'existing_api_key' }) }; const servers = { foo: fromPartial<ServerWithId>({ url: 'https://existing_url.com', apiKey: 'existing_api_key' }) };
const setUp = (serversImported = false, importFailed = false) => { const setUp = (serversImported = false, importFailed = false) => {
(useNavigate as any).mockReturnValue(navigate); (useNavigate as any).mockReturnValue(navigate);
let callCount = 0; let callCount = 0;
const useTimeoutToggle = jest.fn().mockImplementation(() => { const useTimeoutToggle = vi.fn().mockImplementation(() => {
const result = [callCount % 2 === 0 ? serversImported : importFailed, () => null]; const result = [callCount % 2 === 0 ? serversImported : importFailed, () => null];
callCount += 1; callCount += 1;
return result; return result;
@@ -25,8 +28,6 @@ describe('<CreateServer />', () => {
return renderWithEvents(<CreateServer createServers={createServersMock} servers={servers} />); return renderWithEvents(<CreateServer createServers={createServersMock} servers={servers} />);
}; };
beforeEach(jest.clearAllMocks);
it('shows success message when imported is true', () => { it('shows success message when imported is true', () => {
setUp(true); setUp(true);

View File

@@ -1,7 +1,6 @@
import { screen, waitFor } from '@testing-library/react'; import { screen, waitFor } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { Mock } from 'ts-mockery';
import type { ServerWithId } from '../../src/servers/data';
import { DeleteServerButton as createDeleteServerButton } from '../../src/servers/DeleteServerButton'; import { DeleteServerButton as createDeleteServerButton } from '../../src/servers/DeleteServerButton';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
@@ -10,7 +9,7 @@ describe('<DeleteServerButton />', () => {
({ isOpen }) => <>DeleteServerModal {isOpen ? '[Open]' : '[Closed]'}</>, ({ isOpen }) => <>DeleteServerModal {isOpen ? '[Open]' : '[Closed]'}</>,
); );
const setUp = (children?: ReactNode) => renderWithEvents( const setUp = (children?: ReactNode) => renderWithEvents(
<DeleteServerButton server={Mock.all<ServerWithId>()} textClassName="button">{children}</DeleteServerButton>, <DeleteServerButton server={fromPartial({})} textClassName="button">{children}</DeleteServerButton>,
); );
it.each([ it.each([

View File

@@ -1,16 +1,18 @@
import { screen, waitFor } from '@testing-library/react'; import { screen, waitFor } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Mock } from 'ts-mockery';
import type { ServerWithId } from '../../src/servers/data';
import { DeleteServerModal } from '../../src/servers/DeleteServerModal'; import { DeleteServerModal } from '../../src/servers/DeleteServerModal';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
import { TestModalWrapper } from '../__helpers__/TestModalWrapper'; import { TestModalWrapper } from '../__helpers__/TestModalWrapper';
jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useNavigate: jest.fn() })); vi.mock('react-router-dom', async () => ({
...(await vi.importActual<any>('react-router-dom')),
useNavigate: vi.fn(),
}));
describe('<DeleteServerModal />', () => { describe('<DeleteServerModal />', () => {
const deleteServerMock = jest.fn(); const deleteServerMock = vi.fn();
const navigate = jest.fn(); const navigate = vi.fn();
const serverName = 'the_server_name'; const serverName = 'the_server_name';
const setUp = () => { const setUp = () => {
(useNavigate as any).mockReturnValue(navigate); (useNavigate as any).mockReturnValue(navigate);
@@ -20,7 +22,7 @@ describe('<DeleteServerModal />', () => {
renderModal={(args) => ( renderModal={(args) => (
<DeleteServerModal <DeleteServerModal
{...args} {...args}
server={Mock.of<ServerWithId>({ name: serverName })} server={fromPartial({ name: serverName })}
deleteServer={deleteServerMock} deleteServer={deleteServerMock}
/> />
)} )}
@@ -28,8 +30,6 @@ describe('<DeleteServerModal />', () => {
); );
}; };
afterEach(jest.clearAllMocks);
it('renders a modal window', () => { it('renders a modal window', () => {
setUp(); setUp();

View File

@@ -1,17 +1,20 @@
import { fireEvent, screen } from '@testing-library/react'; import { fireEvent, screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter, useNavigate } from 'react-router-dom'; import { MemoryRouter, useNavigate } from 'react-router-dom';
import { Mock } from 'ts-mockery';
import type { ReachableServer, SelectedServer } from '../../src/servers/data'; import type { ReachableServer, SelectedServer } from '../../src/servers/data';
import { EditServer as editServerConstruct } from '../../src/servers/EditServer'; import { EditServer as editServerConstruct } from '../../src/servers/EditServer';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useNavigate: jest.fn() })); vi.mock('react-router-dom', async () => ({
...(await vi.importActual<any>('react-router-dom')),
useNavigate: vi.fn(),
}));
describe('<EditServer />', () => { describe('<EditServer />', () => {
const ServerError = jest.fn(); const ServerError = vi.fn();
const editServerMock = jest.fn(); const editServerMock = vi.fn();
const navigate = jest.fn(); const navigate = vi.fn();
const defaultSelectedServer = Mock.of<ReachableServer>({ const defaultSelectedServer = fromPartial<ReachableServer>({
id: 'abc123', id: 'abc123',
name: 'the_name', name: 'the_name',
url: 'the_url', url: 'the_url',
@@ -20,7 +23,7 @@ describe('<EditServer />', () => {
const EditServer = editServerConstruct(ServerError); const EditServer = editServerConstruct(ServerError);
const setUp = (selectedServer: SelectedServer = defaultSelectedServer) => renderWithEvents( const setUp = (selectedServer: SelectedServer = defaultSelectedServer) => renderWithEvents(
<MemoryRouter> <MemoryRouter>
<EditServer editServer={editServerMock} selectedServer={selectedServer} selectServer={jest.fn()} /> <EditServer editServer={editServerMock} selectedServer={selectedServer} selectServer={vi.fn()} />
</MemoryRouter>, </MemoryRouter>,
); );
@@ -28,10 +31,8 @@ describe('<EditServer />', () => {
(useNavigate as any).mockReturnValue(navigate); (useNavigate as any).mockReturnValue(navigate);
}); });
afterEach(jest.clearAllMocks);
it('renders nothing if selected server is not reachable', () => { it('renders nothing if selected server is not reachable', () => {
setUp(Mock.all<SelectedServer>()); setUp(fromPartial<SelectedServer>({}));
expect(screen.queryByText('Edit')).not.toBeInTheDocument(); expect(screen.queryByText('Edit')).not.toBeInTheDocument();
expect(screen.queryByText('Cancel')).not.toBeInTheDocument(); expect(screen.queryByText('Cancel')).not.toBeInTheDocument();

View File

@@ -1,30 +1,28 @@
import { screen, waitFor } from '@testing-library/react'; import { screen, waitFor } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { Mock } from 'ts-mockery';
import type { ServersMap, ServerWithId } from '../../src/servers/data'; import type { ServersMap, ServerWithId } from '../../src/servers/data';
import { ManageServers as createManageServers } from '../../src/servers/ManageServers'; import { ManageServers as createManageServers } from '../../src/servers/ManageServers';
import type { ServersExporter } from '../../src/servers/services/ServersExporter'; import type { ServersExporter } from '../../src/servers/services/ServersExporter';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
describe('<ManageServers />', () => { describe('<ManageServers />', () => {
const exportServers = jest.fn(); const exportServers = vi.fn();
const serversExporter = Mock.of<ServersExporter>({ exportServers }); const serversExporter = fromPartial<ServersExporter>({ exportServers });
const useTimeoutToggle = jest.fn().mockReturnValue([false, jest.fn()]); const useTimeoutToggle = vi.fn().mockReturnValue([false, vi.fn()]);
const ManageServers = createManageServers( const ManageServers = createManageServers(
serversExporter, serversExporter,
() => <span>ImportServersBtn</span>, () => <span>ImportServersBtn</span>,
useTimeoutToggle, useTimeoutToggle,
({ hasAutoConnect }) => <tr><td>ManageServersRow {hasAutoConnect ? '[YES]' : '[NO]'}</td></tr>, ({ hasAutoConnect }) => <tr><td>ManageServersRow {hasAutoConnect ? '[YES]' : '[NO]'}</td></tr>,
); );
const createServerMock = (value: string, autoConnect = false) => Mock.of<ServerWithId>( const createServerMock = (value: string, autoConnect = false) => fromPartial<ServerWithId>(
{ id: value, name: value, url: value, autoConnect }, { id: value, name: value, url: value, autoConnect },
); );
const setUp = (servers: ServersMap = {}) => renderWithEvents( const setUp = (servers: ServersMap = {}) => renderWithEvents(
<MemoryRouter><ManageServers servers={servers} /></MemoryRouter>, <MemoryRouter><ManageServers servers={servers} /></MemoryRouter>,
); );
afterEach(jest.clearAllMocks);
it('shows search field which allows searching servers, affecting te amount of rendered rows', async () => { it('shows search field which allows searching servers, affecting te amount of rendered rows', async () => {
const { user } = setUp({ const { user } = setUp({
foo: createServerMock('foo'), foo: createServerMock('foo'),
@@ -85,7 +83,7 @@ describe('<ManageServers />', () => {
}); });
it.each([[true], [false]])('shows an error message if an error occurs while importing servers', (hasError) => { it.each([[true], [false]])('shows an error message if an error occurs while importing servers', (hasError) => {
useTimeoutToggle.mockReturnValue([hasError, jest.fn()]); useTimeoutToggle.mockReturnValue([hasError, vi.fn()]);
setUp({ foo: createServerMock('foo') }); setUp({ foo: createServerMock('foo') });

View File

@@ -1,6 +1,6 @@
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { Mock } from 'ts-mockery';
import type { ServerWithId } from '../../src/servers/data'; import type { ServerWithId } from '../../src/servers/data';
import { ManageServersRowDropdown as createManageServersRowDropdown } from '../../src/servers/ManageServersRowDropdown'; import { ManageServersRowDropdown as createManageServersRowDropdown } from '../../src/servers/ManageServersRowDropdown';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
@@ -9,9 +9,9 @@ describe('<ManageServersRowDropdown />', () => {
const ManageServersRowDropdown = createManageServersRowDropdown( const ManageServersRowDropdown = createManageServersRowDropdown(
({ isOpen }) => <span>DeleteServerModal {isOpen ? '[OPEN]' : '[CLOSED]'}</span>, ({ isOpen }) => <span>DeleteServerModal {isOpen ? '[OPEN]' : '[CLOSED]'}</span>,
); );
const setAutoConnect = jest.fn(); const setAutoConnect = vi.fn();
const setUp = (autoConnect = false) => { const setUp = (autoConnect = false) => {
const server = Mock.of<ServerWithId>({ id: 'abc123', autoConnect }); const server = fromPartial<ServerWithId>({ id: 'abc123', autoConnect });
return renderWithEvents( return renderWithEvents(
<MemoryRouter> <MemoryRouter>
<ManageServersRowDropdown setAutoConnect={setAutoConnect} server={server} /> <ManageServersRowDropdown setAutoConnect={setAutoConnect} server={server} />
@@ -19,8 +19,6 @@ describe('<ManageServersRowDropdown />', () => {
); );
}; };
afterEach(jest.clearAllMocks);
it('renders expected amount of dropdown items', async () => { it('renders expected amount of dropdown items', async () => {
const { user } = setUp(); const { user } = setUp();

View File

@@ -1,22 +1,17 @@
import { screen, waitFor } from '@testing-library/react'; import { screen, waitFor } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { Mock } from 'ts-mockery';
import type { MercureInfo } from '../../src/mercure/reducers/mercureInfo'; import type { MercureInfo } from '../../src/mercure/reducers/mercureInfo';
import type { ReachableServer } from '../../src/servers/data';
import { Overview as overviewCreator } from '../../src/servers/Overview'; import { Overview as overviewCreator } from '../../src/servers/Overview';
import type { Settings } from '../../src/settings/reducers/settings';
import type { ShortUrlsList as ShortUrlsListState } from '../../src/short-urls/reducers/shortUrlsList';
import type { TagsList } from '../../src/tags/reducers/tagsList';
import { prettify } from '../../src/utils/helpers/numbers'; import { prettify } from '../../src/utils/helpers/numbers';
import type { VisitsOverview } from '../../src/visits/reducers/visitsOverview';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
describe('<Overview />', () => { describe('<Overview />', () => {
const ShortUrlsTable = () => <>ShortUrlsTable</>; const ShortUrlsTable = () => <>ShortUrlsTable</>;
const CreateShortUrl = () => <>CreateShortUrl</>; const CreateShortUrl = () => <>CreateShortUrl</>;
const listShortUrls = jest.fn(); const listShortUrls = vi.fn();
const listTags = jest.fn(); const listTags = vi.fn();
const loadVisitsOverview = jest.fn(); const loadVisitsOverview = vi.fn();
const Overview = overviewCreator(ShortUrlsTable, CreateShortUrl); const Overview = overviewCreator(ShortUrlsTable, CreateShortUrl);
const shortUrls = { const shortUrls = {
pagination: { totalItems: 83710 }, pagination: { totalItems: 83710 },
@@ -28,18 +23,18 @@ describe('<Overview />', () => {
listShortUrls={listShortUrls} listShortUrls={listShortUrls}
listTags={listTags} listTags={listTags}
loadVisitsOverview={loadVisitsOverview} loadVisitsOverview={loadVisitsOverview}
shortUrlsList={Mock.of<ShortUrlsListState>({ loading, shortUrls })} shortUrlsList={fromPartial({ loading, shortUrls })}
tagsList={Mock.of<TagsList>({ loading, tags: ['foo', 'bar', 'baz'] })} tagsList={fromPartial({ loading, tags: ['foo', 'bar', 'baz'] })}
visitsOverview={Mock.of<VisitsOverview>({ visitsOverview={fromPartial({
loading, loading,
nonOrphanVisits: { total: 3456, bots: 1000, nonBots: 2456 }, nonOrphanVisits: { total: 3456, bots: 1000, nonBots: 2456 },
orphanVisits: { total: 28, bots: 15, nonBots: 13 }, orphanVisits: { total: 28, bots: 15, nonBots: 13 },
})} })}
selectedServer={Mock.of<ReachableServer>({ id: serverId })} selectedServer={fromPartial({ id: serverId })}
createNewVisits={jest.fn()} createNewVisits={vi.fn()}
loadMercureInfo={jest.fn()} loadMercureInfo={vi.fn()}
mercureInfo={Mock.all<MercureInfo>()} mercureInfo={fromPartial<MercureInfo>({})}
settings={Mock.of<Settings>({ visits: { excludeBots } })} settings={fromPartial({ visits: { excludeBots } })}
/> />
</MemoryRouter>, </MemoryRouter>,
); );

View File

@@ -1,16 +1,16 @@
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn';
import { values } from 'ramda'; import { values } from 'ramda';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { Mock } from 'ts-mockery'; import type { ServersMap } from '../../src/servers/data';
import type { ServersMap, ServerWithId } from '../../src/servers/data';
import { ServersDropdown } from '../../src/servers/ServersDropdown'; import { ServersDropdown } from '../../src/servers/ServersDropdown';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
describe('<ServersDropdown />', () => { describe('<ServersDropdown />', () => {
const fallbackServers: ServersMap = { const fallbackServers: ServersMap = {
'1a': Mock.of<ServerWithId>({ name: 'foo', id: '1a' }), '1a': fromPartial({ name: 'foo', id: '1a' }),
'2b': Mock.of<ServerWithId>({ name: 'bar', id: '2b' }), '2b': fromPartial({ name: 'bar', id: '2b' }),
'3c': Mock.of<ServerWithId>({ name: 'baz', id: '3c' }), '3c': fromPartial({ name: 'baz', id: '3c' }),
}; };
const setUp = (servers: ServersMap = fallbackServers) => renderWithEvents( const setUp = (servers: ServersMap = fallbackServers) => renderWithEvents(
<MemoryRouter><ServersDropdown servers={servers} selectedServer={null} /></MemoryRouter>, <MemoryRouter><ServersDropdown servers={servers} selectedServer={null} /></MemoryRouter>,

View File

@@ -1,13 +1,13 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { Mock } from 'ts-mockery';
import type { ServerWithId } from '../../src/servers/data'; import type { ServerWithId } from '../../src/servers/data';
import { ServersListGroup } from '../../src/servers/ServersListGroup'; import { ServersListGroup } from '../../src/servers/ServersListGroup';
describe('<ServersListGroup />', () => { describe('<ServersListGroup />', () => {
const servers = [ const servers: ServerWithId[] = [
Mock.of<ServerWithId>({ name: 'foo', id: '123' }), fromPartial({ name: 'foo', id: '123' }),
Mock.of<ServerWithId>({ name: 'bar', id: '456' }), fromPartial({ name: 'bar', id: '456' }),
]; ];
const setUp = (params: { servers?: ServerWithId[]; withChildren?: boolean; embedded?: boolean }) => { const setUp = (params: { servers?: ServerWithId[]; withChildren?: boolean; embedded?: boolean }) => {
const { servers = [], withChildren = true, embedded } = params; const { servers = [], withChildren = true, embedded } = params;

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<DeleteServerButton /> renders expected content 1`] = ` exports[`<DeleteServerButton /> > renders expected content 1`] = `
<span> <span>
<span <span
class="button" class="button"
@@ -10,7 +10,7 @@ exports[`<DeleteServerButton /> renders expected content 1`] = `
</span> </span>
`; `;
exports[`<DeleteServerButton /> renders expected content 2`] = ` exports[`<DeleteServerButton /> > renders expected content 2`] = `
<span> <span>
<span <span
class="button" class="button"
@@ -20,7 +20,7 @@ exports[`<DeleteServerButton /> renders expected content 2`] = `
</span> </span>
`; `;
exports[`<DeleteServerButton /> renders expected content 3`] = ` exports[`<DeleteServerButton /> > renders expected content 3`] = `
<span> <span>
<span <span
class="button" class="button"
@@ -30,7 +30,7 @@ exports[`<DeleteServerButton /> renders expected content 3`] = `
</span> </span>
`; `;
exports[`<DeleteServerButton /> renders expected content 4`] = ` exports[`<DeleteServerButton /> > renders expected content 4`] = `
<span> <span>
<svg <svg
aria-hidden="true" aria-hidden="true"

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<ManageServersRow /> renders auto-connect icon only if server is autoConnect 1`] = ` exports[`<ManageServersRow /> > renders auto-connect icon only if server is autoConnect 1`] = `
<div> <div>
<table> <table>
<tbody> <tbody>
@@ -57,7 +57,7 @@ exports[`<ManageServersRow /> renders auto-connect icon only if server is autoCo
</div> </div>
`; `;
exports[`<ManageServersRow /> renders auto-connect icon only if server is autoConnect 2`] = ` exports[`<ManageServersRow /> > renders auto-connect icon only if server is autoConnect 2`] = `
<div> <div>
<table> <table>
<tbody> <tbody>

View File

@@ -1,24 +1,23 @@
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { Mock } from 'ts-mockery'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ServerData } from '../../../src/servers/data'; import type { ServerData } from '../../../src/servers/data';
import { DuplicatedServersModal } from '../../../src/servers/helpers/DuplicatedServersModal'; import { DuplicatedServersModal } from '../../../src/servers/helpers/DuplicatedServersModal';
import { renderWithEvents } from '../../__helpers__/setUpTest'; import { renderWithEvents } from '../../__helpers__/setUpTest';
describe('<DuplicatedServersModal />', () => { describe('<DuplicatedServersModal />', () => {
const onDiscard = jest.fn(); const onDiscard = vi.fn();
const onSave = jest.fn(); const onSave = vi.fn();
const setUp = (duplicatedServers: ServerData[] = []) => renderWithEvents( const setUp = (duplicatedServers: ServerData[] = []) => renderWithEvents(
<DuplicatedServersModal isOpen duplicatedServers={duplicatedServers} onDiscard={onDiscard} onSave={onSave} />, <DuplicatedServersModal isOpen duplicatedServers={duplicatedServers} onDiscard={onDiscard} onSave={onSave} />,
); );
const mockServer = (data: Partial<ServerData> = {}) => fromPartial<ServerData>(data);
beforeEach(jest.clearAllMocks);
it.each([ it.each([
[[], 0], [[], 0],
[[Mock.all<ServerData>()], 2], [[mockServer()], 2],
[[Mock.all<ServerData>(), Mock.all<ServerData>()], 2], [[mockServer(), mockServer()], 2],
[[Mock.all<ServerData>(), Mock.all<ServerData>(), Mock.all<ServerData>()], 3], [[mockServer(), mockServer(), mockServer()], 3],
[[Mock.all<ServerData>(), Mock.all<ServerData>(), Mock.all<ServerData>(), Mock.all<ServerData>()], 4], [[mockServer(), mockServer(), mockServer(), mockServer()], 4],
])('renders expected amount of items', (duplicatedServers, expectedItems) => { ])('renders expected amount of items', (duplicatedServers, expectedItems) => {
setUp(duplicatedServers); setUp(duplicatedServers);
expect(screen.queryAllByRole('listitem')).toHaveLength(expectedItems); expect(screen.queryAllByRole('listitem')).toHaveLength(expectedItems);
@@ -26,7 +25,7 @@ describe('<DuplicatedServersModal />', () => {
it.each([ it.each([
[ [
[Mock.all<ServerData>()], [mockServer()],
{ {
header: 'Duplicated server', header: 'Duplicated server',
firstParagraph: 'There is already a server with:', firstParagraph: 'There is already a server with:',
@@ -35,7 +34,7 @@ describe('<DuplicatedServersModal />', () => {
}, },
], ],
[ [
[Mock.all<ServerData>(), Mock.all<ServerData>()], [mockServer(), mockServer()],
{ {
header: 'Duplicated servers', header: 'Duplicated servers',
firstParagraph: 'The next servers already exist:', firstParagraph: 'The next servers already exist:',
@@ -54,10 +53,10 @@ describe('<DuplicatedServersModal />', () => {
it.each([ it.each([
[[]], [[]],
[[Mock.of<ServerData>({ url: 'url', apiKey: 'apiKey' })]], [[mockServer({ url: 'url', apiKey: 'apiKey' })]],
[[ [[
Mock.of<ServerData>({ url: 'url_1', apiKey: 'apiKey_1' }), mockServer({ url: 'url_1', apiKey: 'apiKey_1' }),
Mock.of<ServerData>({ url: 'url_2', apiKey: 'apiKey_2' }), mockServer({ url: 'url_2', apiKey: 'apiKey_2' }),
]], ]],
])('displays provided server data', (duplicatedServers) => { ])('displays provided server data', (duplicatedServers) => {
setUp(duplicatedServers); setUp(duplicatedServers);

View File

@@ -1,5 +1,5 @@
import { fireEvent, screen, waitFor } from '@testing-library/react'; import { fireEvent, screen, waitFor } from '@testing-library/react';
import { Mock } from 'ts-mockery'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ServersMap, ServerWithId } from '../../../src/servers/data'; import type { ServersMap, ServerWithId } from '../../../src/servers/data';
import type { import type {
ImportServersBtnProps } from '../../../src/servers/helpers/ImportServersBtn'; ImportServersBtnProps } from '../../../src/servers/helpers/ImportServersBtn';
@@ -10,10 +10,10 @@ import type { ServersImporter } from '../../../src/servers/services/ServersImpor
import { renderWithEvents } from '../../__helpers__/setUpTest'; import { renderWithEvents } from '../../__helpers__/setUpTest';
describe('<ImportServersBtn />', () => { describe('<ImportServersBtn />', () => {
const onImportMock = jest.fn(); const onImportMock = vi.fn();
const createServersMock = jest.fn(); const createServersMock = vi.fn();
const importServersFromFile = jest.fn().mockResolvedValue([]); const importServersFromFile = vi.fn().mockResolvedValue([]);
const serversImporterMock = Mock.of<ServersImporter>({ importServersFromFile }); const serversImporterMock = fromPartial<ServersImporter>({ importServersFromFile });
const ImportServersBtn = createImportServersBtn(serversImporterMock); const ImportServersBtn = createImportServersBtn(serversImporterMock);
const setUp = (props: Partial<ImportServersBtnProps> = {}, servers: ServersMap = {}) => renderWithEvents( const setUp = (props: Partial<ImportServersBtnProps> = {}, servers: ServersMap = {}) => renderWithEvents(
<ImportServersBtn <ImportServersBtn
@@ -24,8 +24,6 @@ describe('<ImportServersBtn />', () => {
/>, />,
); );
afterEach(jest.clearAllMocks);
it('shows tooltip on button hover', async () => { it('shows tooltip on button hover', async () => {
const { user } = setUp(); const { user } = setUp();
@@ -67,8 +65,8 @@ describe('<ImportServersBtn />', () => {
['Save anyway', true], ['Save anyway', true],
['Discard', false], ['Discard', false],
])('creates expected servers depending on selected option in modal', async (btnName, savesDuplicatedServers) => { ])('creates expected servers depending on selected option in modal', async (btnName, savesDuplicatedServers) => {
const existingServer = Mock.of<ServerWithId>({ id: 'abc', url: 'existingUrl', apiKey: 'existingApiKey' }); const existingServer = fromPartial<ServerWithId>({ id: 'abc', url: 'existingUrl', apiKey: 'existingApiKey' });
const newServer = Mock.of<ServerWithId>({ url: 'newUrl', apiKey: 'newApiKey' }); const newServer = fromPartial<ServerWithId>({ url: 'newUrl', apiKey: 'newApiKey' });
const { container, user } = setUp({}, { abc: existingServer }); const { container, user } = setUp({}, { abc: existingServer });
const input = container.querySelector('[type=file]'); const input = container.querySelector('[type=file]');
importServersFromFile.mockResolvedValue([existingServer, newServer]); importServersFromFile.mockResolvedValue([existingServer, newServer]);

View File

@@ -1,6 +1,6 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { Mock } from 'ts-mockery';
import type { NonReachableServer, NotFoundServer } from '../../../src/servers/data'; import type { NonReachableServer, NotFoundServer } from '../../../src/servers/data';
import { ServerError as createServerError } from '../../../src/servers/helpers/ServerError'; import { ServerError as createServerError } from '../../../src/servers/helpers/ServerError';
@@ -9,7 +9,7 @@ describe('<ServerError />', () => {
it.each([ it.each([
[ [
Mock.all<NotFoundServer>(), fromPartial<NotFoundServer>({}),
{ {
found: ['Could not find this Shlink server.'], found: ['Could not find this Shlink server.'],
notFound: [ notFound: [
@@ -20,7 +20,7 @@ describe('<ServerError />', () => {
}, },
], ],
[ [
Mock.of<NonReachableServer>({ id: 'abc123' }), fromPartial<NonReachableServer>({ id: 'abc123' }),
{ {
found: [ found: [
'Oops! Could not connect to this Shlink server.', 'Oops! Could not connect to this Shlink server.',

View File

@@ -2,11 +2,9 @@ import { fireEvent, render, screen } from '@testing-library/react';
import { ServerForm } from '../../../src/servers/helpers/ServerForm'; import { ServerForm } from '../../../src/servers/helpers/ServerForm';
describe('<ServerForm />', () => { describe('<ServerForm />', () => {
const onSubmit = jest.fn(); const onSubmit = vi.fn();
const setUp = () => render(<ServerForm onSubmit={onSubmit}>Something</ServerForm>); const setUp = () => render(<ServerForm onSubmit={onSubmit}>Something</ServerForm>);
afterEach(jest.resetAllMocks);
it('renders components', () => { it('renders components', () => {
setUp(); setUp();
@@ -18,7 +16,7 @@ describe('<ServerForm />', () => {
setUp(); setUp();
expect(onSubmit).not.toHaveBeenCalled(); expect(onSubmit).not.toHaveBeenCalled();
fireEvent.submit(screen.getByRole('form'), { preventDefault: jest.fn() }); fireEvent.submit(screen.getByRole('form'), { preventDefault: vi.fn() });
expect(onSubmit).toHaveBeenCalled(); expect(onSubmit).toHaveBeenCalled();
}); });
}); });

View File

@@ -1,14 +1,12 @@
import { Mock } from 'ts-mockery'; import { fromPartial } from '@total-typescript/shoehorn';
import type { HttpClient } from '../../../src/common/services/HttpClient'; import type { HttpClient } from '../../../src/common/services/HttpClient';
import { fetchServers } from '../../../src/servers/reducers/remoteServers'; import { fetchServers } from '../../../src/servers/reducers/remoteServers';
describe('remoteServersReducer', () => { describe('remoteServersReducer', () => {
afterEach(jest.clearAllMocks);
describe('fetchServers', () => { describe('fetchServers', () => {
const dispatch = jest.fn(); const dispatch = vi.fn();
const fetchJson = jest.fn(); const fetchJson = vi.fn();
const httpClient = Mock.of<HttpClient>({ fetchJson }); const httpClient = fromPartial<HttpClient>({ fetchJson });
it.each([ it.each([
[ [
@@ -81,7 +79,7 @@ describe('remoteServersReducer', () => {
fetchJson.mockResolvedValue(mockedValue); fetchJson.mockResolvedValue(mockedValue);
const doFetchServers = fetchServers(httpClient); const doFetchServers = fetchServers(httpClient);
await doFetchServers()(dispatch, jest.fn(), {}); await doFetchServers()(dispatch, vi.fn(), {});
expect(dispatch).toHaveBeenCalledTimes(3); expect(dispatch).toHaveBeenCalledTimes(3);
expect(dispatch).toHaveBeenNthCalledWith(2, expect.objectContaining({ payload: expectedNewServers })); expect(dispatch).toHaveBeenNthCalledWith(2, expect.objectContaining({ payload: expectedNewServers }));

View File

@@ -1,4 +1,4 @@
import { Mock } from 'ts-mockery'; import { fromPartial } from '@total-typescript/shoehorn';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import type { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient'; import type { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient';
import type { ShlinkState } from '../../../src/container/types'; import type { ShlinkState } from '../../../src/container/types';
@@ -13,28 +13,25 @@ import {
} from '../../../src/servers/reducers/selectedServer'; } from '../../../src/servers/reducers/selectedServer';
describe('selectedServerReducer', () => { describe('selectedServerReducer', () => {
const dispatch = jest.fn(); const dispatch = vi.fn();
const health = jest.fn(); const health = vi.fn();
const buildApiClient = jest.fn().mockReturnValue(Mock.of<ShlinkApiClient>({ health })); const buildApiClient = vi.fn().mockReturnValue(fromPartial<ShlinkApiClient>({ health }));
const selectServer = selectServerCreator(buildApiClient); const selectServer = selectServerCreator(buildApiClient);
const { reducer } = selectedServerReducerCreator(selectServer); const { reducer } = selectedServerReducerCreator(selectServer);
afterEach(jest.clearAllMocks);
describe('reducer', () => { describe('reducer', () => {
it('returns default when action is RESET_SELECTED_SERVER', () => it('returns default when action is RESET_SELECTED_SERVER', () =>
expect(reducer(null, resetSelectedServer())).toBeNull()); expect(reducer(null, resetSelectedServer())).toBeNull());
it('returns selected server when action is SELECT_SERVER', () => { it('returns selected server when action is SELECT_SERVER', () => {
const payload = Mock.of<RegularServer>({ id: 'abc123' }); const payload = fromPartial<RegularServer>({ id: 'abc123' });
expect(reducer(null, selectServer.fulfilled(payload, '', ''))).toEqual(payload); expect(reducer(null, selectServer.fulfilled(payload, '', ''))).toEqual(payload);
}); });
}); });
describe('selectServer', () => { describe('selectServer', () => {
const version = '1.19.0'; const version = '1.19.0';
const createGetStateMock = (id: string) => jest.fn().mockReturnValue({ const createGetStateMock = (id: string) => vi.fn().mockReturnValue({
servers: { servers: {
[id]: { id }, [id]: { id },
}, },
@@ -66,7 +63,7 @@ describe('selectedServerReducer', () => {
it('dispatches error when health endpoint fails', async () => { it('dispatches error when health endpoint fails', async () => {
const id = uuid(); const id = uuid();
const getState = createGetStateMock(id); const getState = createGetStateMock(id);
const expectedSelectedServer = Mock.of<NonReachableServer>({ id, serverNotReachable: true }); const expectedSelectedServer = fromPartial<NonReachableServer>({ id, serverNotReachable: true });
health.mockRejectedValue({}); health.mockRejectedValue({});
@@ -78,7 +75,7 @@ describe('selectedServerReducer', () => {
it('dispatches error when server is not found', async () => { it('dispatches error when server is not found', async () => {
const id = uuid(); const id = uuid();
const getState = jest.fn(() => Mock.of<ShlinkState>({ servers: {} })); const getState = vi.fn(() => fromPartial<ShlinkState>({ servers: {} }));
const expectedSelectedServer: NotFoundServer = { serverNotFound: true }; const expectedSelectedServer: NotFoundServer = { serverNotFound: true };
await selectServer(id)(dispatch, getState, {}); await selectServer(id)(dispatch, getState, {});
@@ -90,16 +87,16 @@ describe('selectedServerReducer', () => {
}); });
describe('selectServerListener', () => { describe('selectServerListener', () => {
const getState = jest.fn(() => ({})); const getState = vi.fn(() => ({}));
const loadMercureInfo = jest.fn(); const loadMercureInfo = vi.fn();
const { middleware } = selectServerListener(selectServer, loadMercureInfo); const { middleware } = selectServerListener(selectServer, loadMercureInfo);
it.each([ it.each([
[Mock.of<ReachableServer>({ version: '1.2.3' }), 1], [fromPartial<ReachableServer>({ version: '1.2.3' }), 1],
[Mock.of<NotFoundServer>({ serverNotFound: true }), 0], [fromPartial<NotFoundServer>({ serverNotFound: true }), 0],
[Mock.of<NonReachableServer>({ serverNotReachable: true }), 0], [fromPartial<NonReachableServer>({ serverNotReachable: true }), 0],
])('dispatches loadMercureInfo when provided server is reachable', (payload, expectedCalls) => { ])('dispatches loadMercureInfo when provided server is reachable', (payload, expectedCalls) => {
middleware({ dispatch, getState })(jest.fn())({ middleware({ dispatch, getState })(vi.fn())({
payload, payload,
type: selectServer.fulfilled.toString(), type: selectServer.fulfilled.toString(),
}); });
@@ -109,8 +106,8 @@ describe('selectedServerReducer', () => {
}); });
it('does not dispatch loadMercureInfo when action is not of the proper type', () => { it('does not dispatch loadMercureInfo when action is not of the proper type', () => {
middleware({ dispatch, getState })(jest.fn())({ middleware({ dispatch, getState })(vi.fn())({
payload: Mock.of<ReachableServer>({ version: '1.2.3' }), payload: fromPartial<ReachableServer>({ version: '1.2.3' }),
type: 'something_else', type: 'something_else',
}); });

View File

@@ -1,6 +1,6 @@
import { fromPartial } from '@total-typescript/shoehorn';
import { dissoc, values } from 'ramda'; import { dissoc, values } from 'ramda';
import { Mock } from 'ts-mockery'; import type { RegularServer, ServersMap, ServerWithId } from '../../../src/servers/data';
import type { RegularServer, ServerWithId } from '../../../src/servers/data';
import { import {
createServers, createServers,
deleteServer, deleteServer,
@@ -10,13 +10,11 @@ import {
} from '../../../src/servers/reducers/servers'; } from '../../../src/servers/reducers/servers';
describe('serversReducer', () => { describe('serversReducer', () => {
const list = { const list: ServersMap = {
abc123: Mock.of<RegularServer>({ id: 'abc123' }), abc123: fromPartial({ id: 'abc123' }),
def456: Mock.of<RegularServer>({ id: 'def456' }), def456: fromPartial({ id: 'def456' }),
}; };
afterEach(jest.clearAllMocks);
describe('reducer', () => { describe('reducer', () => {
it('returns edited server when action is EDIT_SERVER', () => it('returns edited server when action is EDIT_SERVER', () =>
expect(serversReducer(list, editServer('abc123', { name: 'foo' }))).toEqual({ expect(serversReducer(list, editServer('abc123', { name: 'foo' }))).toEqual({
@@ -31,12 +29,12 @@ describe('serversReducer', () => {
})); }));
it('removes server when action is DELETE_SERVER', () => it('removes server when action is DELETE_SERVER', () =>
expect(serversReducer(list, deleteServer(Mock.of<ServerWithId>({ id: 'abc123' })))).toEqual({ expect(serversReducer(list, deleteServer(fromPartial<ServerWithId>({ id: 'abc123' })))).toEqual({
def456: { id: 'def456' }, def456: { id: 'def456' },
})); }));
it('appends server when action is CREATE_SERVERS', () => it('appends server when action is CREATE_SERVERS', () =>
expect(serversReducer(list, createServers([Mock.of<ServerWithId>({ id: 'ghi789' })]))).toEqual({ expect(serversReducer(list, createServers([fromPartial<ServerWithId>({ id: 'ghi789' })]))).toEqual({
abc123: { id: 'abc123' }, abc123: { id: 'abc123' },
def456: { id: 'def456' }, def456: { id: 'def456' },
ghi789: { id: 'ghi789' }, ghi789: { id: 'ghi789' },
@@ -46,7 +44,7 @@ describe('serversReducer', () => {
[true], [true],
[false], [false],
])('returns state as it is when trying to set auto-connect on invalid server', (autoConnect) => ])('returns state as it is when trying to set auto-connect on invalid server', (autoConnect) =>
expect(serversReducer(list, setAutoConnect(Mock.of<ServerWithId>({ id: 'invalid' }), autoConnect))).toEqual({ expect(serversReducer(list, setAutoConnect(fromPartial<ServerWithId>({ id: 'invalid' }), autoConnect))).toEqual({
abc123: { id: 'abc123' }, abc123: { id: 'abc123' },
def456: { id: 'def456' }, def456: { id: 'def456' },
})); }));
@@ -59,7 +57,7 @@ describe('serversReducer', () => {
expect(serversReducer( expect(serversReducer(
listWithDisabledAutoConnect, listWithDisabledAutoConnect,
setAutoConnect(Mock.of<ServerWithId>({ id: 'abc123' }), false), setAutoConnect(fromPartial<ServerWithId>({ id: 'abc123' }), false),
)).toEqual({ )).toEqual({
abc123: { id: 'abc123', autoConnect: false }, abc123: { id: 'abc123', autoConnect: false },
def456: { id: 'def456' }, def456: { id: 'def456' },
@@ -74,7 +72,7 @@ describe('serversReducer', () => {
expect(serversReducer( expect(serversReducer(
listWithEnabledAutoConnect, listWithEnabledAutoConnect,
setAutoConnect(Mock.of<ServerWithId>({ id: 'def456' }), true), setAutoConnect(fromPartial<ServerWithId>({ id: 'def456' }), true),
)).toEqual({ )).toEqual({
abc123: { id: 'abc123', autoConnect: false }, abc123: { id: 'abc123', autoConnect: false },
def456: { id: 'def456', autoConnect: true }, def456: { id: 'def456', autoConnect: true },
@@ -94,7 +92,7 @@ describe('serversReducer', () => {
describe('deleteServer', () => { describe('deleteServer', () => {
it('returns expected action', () => { it('returns expected action', () => {
const serverToDelete = Mock.of<RegularServer>({ id: 'abc123' }); const serverToDelete = fromPartial<RegularServer>({ id: 'abc123' });
const { payload } = deleteServer(serverToDelete); const { payload } = deleteServer(serverToDelete);
expect(payload).toEqual({ id: 'abc123' }); expect(payload).toEqual({ id: 'abc123' });
@@ -122,7 +120,7 @@ describe('serversReducer', () => {
[true], [true],
[false], [false],
])('returns expected action', (autoConnect) => { ])('returns expected action', (autoConnect) => {
const serverToEdit = Mock.of<RegularServer>({ id: 'abc123' }); const serverToEdit = fromPartial<RegularServer>({ id: 'abc123' });
const { payload } = setAutoConnect(serverToEdit, autoConnect); const { payload } = setAutoConnect(serverToEdit, autoConnect);
expect(payload).toEqual({ serverId: 'abc123', autoConnect }); expect(payload).toEqual({ serverId: 'abc123', autoConnect });

View File

@@ -1,11 +1,11 @@
import { Mock } from 'ts-mockery'; import { fromPartial } from '@total-typescript/shoehorn';
import { ServersExporter } from '../../../src/servers/services/ServersExporter'; import { ServersExporter } from '../../../src/servers/services/ServersExporter';
import type { LocalStorage } from '../../../src/utils/services/LocalStorage'; import type { LocalStorage } from '../../../src/utils/services/LocalStorage';
import { appendChild, removeChild, windowMock } from '../../__mocks__/Window.mock'; import { appendChild, removeChild, windowMock } from '../../__mocks__/Window.mock';
describe('ServersExporter', () => { describe('ServersExporter', () => {
const storageMock = Mock.of<LocalStorage>({ const storageMock = fromPartial<LocalStorage>({
get: jest.fn(() => ({ get: vi.fn(() => ({
abc123: { abc123: {
id: 'abc123', id: 'abc123',
name: 'foo', name: 'foo',
@@ -16,22 +16,20 @@ describe('ServersExporter', () => {
name: 'bar', name: 'bar',
autoConnect: false, autoConnect: false,
}, },
})), } as any)),
}); });
const erroneousToCsv = jest.fn(() => { const erroneousToCsv = vi.fn(() => {
throw new Error(''); throw new Error('');
}); });
const createCsvjsonMock = (throwError = false) => (throwError ? erroneousToCsv : jest.fn(() => '')); const createCsvjsonMock = (throwError = false) => (throwError ? erroneousToCsv : vi.fn(() => ''));
beforeEach(jest.clearAllMocks);
describe('exportServers', () => { describe('exportServers', () => {
let originalConsole: Console; let originalConsole: Console;
const error = jest.fn(); const error = vi.fn();
beforeEach(() => { beforeEach(() => {
originalConsole = global.console; originalConsole = global.console;
global.console = Mock.of<Console>({ error }); global.console = fromPartial<Console>({ error });
(global as any).Blob = class Blob {}; (global as any).Blob = class Blob {};
(global as any).URL = { createObjectURL: () => '' }; (global as any).URL = { createObjectURL: () => '' };
}); });

View File

@@ -1,21 +1,19 @@
import { Mock } from 'ts-mockery'; import { fromPartial } from '@total-typescript/shoehorn';
import type { RegularServer } from '../../../src/servers/data'; import type { RegularServer } from '../../../src/servers/data';
import { ServersImporter } from '../../../src/servers/services/ServersImporter'; import { ServersImporter } from '../../../src/servers/services/ServersImporter';
describe('ServersImporter', () => { describe('ServersImporter', () => {
const servers: RegularServer[] = [Mock.all<RegularServer>(), Mock.all<RegularServer>()]; const servers: RegularServer[] = [fromPartial<RegularServer>({}), fromPartial<RegularServer>({})];
const csvjsonMock = jest.fn().mockResolvedValue(servers); const csvjsonMock = vi.fn().mockResolvedValue(servers);
const readAsText = jest.fn(); const readAsText = vi.fn();
const fileReaderMock = Mock.of<FileReader>({ const fileReaderMock = fromPartial<FileReader>({
readAsText, readAsText,
addEventListener: (_eventName: string, listener: (e: ProgressEvent<FileReader>) => void) => listener( addEventListener: ((_eventName: string, listener: (e: ProgressEvent<FileReader>) => void) => listener(
Mock.of<ProgressEvent<FileReader>>({ target: { result: '' } }), fromPartial({ target: { result: '' } }),
), )) as any,
}); });
const importer = new ServersImporter(csvjsonMock, () => fileReaderMock); const importer = new ServersImporter(csvjsonMock, () => fileReaderMock);
beforeEach(jest.clearAllMocks);
describe('importServersFromFile', () => { describe('importServersFromFile', () => {
it('rejects with error if no file was provided', async () => { it('rejects with error if no file was provided', async () => {
await expect(importer.importServersFromFile()).rejects.toEqual( await expect(importer.importServersFromFile()).rejects.toEqual(
@@ -28,7 +26,7 @@ describe('ServersImporter', () => {
csvjsonMock.mockRejectedValue(expectedError); csvjsonMock.mockRejectedValue(expectedError);
await expect(importer.importServersFromFile(Mock.of<File>({ type: 'text/html' }))).rejects.toEqual(expectedError); await expect(importer.importServersFromFile(fromPartial({ type: 'text/html' }))).rejects.toEqual(expectedError);
}); });
it.each([ it.each([
@@ -57,7 +55,7 @@ describe('ServersImporter', () => {
])('rejects with error if provided file does not parse to valid list of servers', async (parsedObject) => { ])('rejects with error if provided file does not parse to valid list of servers', async (parsedObject) => {
csvjsonMock.mockResolvedValue(parsedObject); csvjsonMock.mockResolvedValue(parsedObject);
await expect(importer.importServersFromFile(Mock.of<File>({ type: 'text/html' }))).rejects.toEqual( await expect(importer.importServersFromFile(fromPartial({ type: 'text/html' }))).rejects.toEqual(
new Error('Provided file does not have the right format.'), new Error('Provided file does not have the right format.'),
); );
}); });
@@ -78,7 +76,7 @@ describe('ServersImporter', () => {
csvjsonMock.mockResolvedValue(expectedServers); csvjsonMock.mockResolvedValue(expectedServers);
const result = await importer.importServersFromFile(Mock.all<File>()); const result = await importer.importServersFromFile(fromPartial({}));
expect(result).toEqual(expectedServers); expect(result).toEqual(expectedServers);
expect(readAsText).toHaveBeenCalledTimes(1); expect(readAsText).toHaveBeenCalledTimes(1);

View File

@@ -1,25 +1,22 @@
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { Mock } from 'ts-mockery'; import { fromPartial } from '@total-typescript/shoehorn';
import { RealTimeUpdatesSettings } from '../../src/settings/RealTimeUpdatesSettings'; import { RealTimeUpdatesSettings } from '../../src/settings/RealTimeUpdatesSettings';
import type { import type {
RealTimeUpdatesSettings as RealTimeUpdatesSettingsOptions, RealTimeUpdatesSettings as RealTimeUpdatesSettingsOptions,
Settings,
} from '../../src/settings/reducers/settings'; } from '../../src/settings/reducers/settings';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
describe('<RealTimeUpdatesSettings />', () => { describe('<RealTimeUpdatesSettings />', () => {
const toggleRealTimeUpdates = jest.fn(); const toggleRealTimeUpdates = vi.fn();
const setRealTimeUpdatesInterval = jest.fn(); const setRealTimeUpdatesInterval = vi.fn();
const setUp = (realTimeUpdates: Partial<RealTimeUpdatesSettingsOptions> = {}) => renderWithEvents( const setUp = (realTimeUpdates: Partial<RealTimeUpdatesSettingsOptions> = {}) => renderWithEvents(
<RealTimeUpdatesSettings <RealTimeUpdatesSettings
settings={Mock.of<Settings>({ realTimeUpdates })} settings={fromPartial({ realTimeUpdates })}
toggleRealTimeUpdates={toggleRealTimeUpdates} toggleRealTimeUpdates={toggleRealTimeUpdates}
setRealTimeUpdatesInterval={setRealTimeUpdatesInterval} setRealTimeUpdatesInterval={setRealTimeUpdatesInterval}
/>, />,
); );
afterEach(jest.clearAllMocks);
it('renders enabled real time updates as expected', () => { it('renders enabled real time updates as expected', () => {
setUp({ enabled: true }); setUp({ enabled: true });

View File

@@ -1,20 +1,18 @@
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { Mock } from 'ts-mockery'; import { fromPartial } from '@total-typescript/shoehorn';
import type { Settings, ShortUrlCreationSettings as ShortUrlsSettings } from '../../src/settings/reducers/settings'; import type { ShortUrlCreationSettings as ShortUrlsSettings } from '../../src/settings/reducers/settings';
import { ShortUrlCreationSettings } from '../../src/settings/ShortUrlCreationSettings'; import { ShortUrlCreationSettings } from '../../src/settings/ShortUrlCreationSettings';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
describe('<ShortUrlCreationSettings />', () => { describe('<ShortUrlCreationSettings />', () => {
const setShortUrlCreationSettings = jest.fn(); const setShortUrlCreationSettings = vi.fn();
const setUp = (shortUrlCreation?: ShortUrlsSettings) => renderWithEvents( const setUp = (shortUrlCreation?: ShortUrlsSettings) => renderWithEvents(
<ShortUrlCreationSettings <ShortUrlCreationSettings
settings={Mock.of<Settings>({ shortUrlCreation })} settings={fromPartial({ shortUrlCreation })}
setShortUrlCreationSettings={setShortUrlCreationSettings} setShortUrlCreationSettings={setShortUrlCreationSettings}
/>, />,
); );
afterEach(jest.clearAllMocks);
it.each([ it.each([
[{ validateUrls: true }, true], [{ validateUrls: true }, true],
[{ validateUrls: false }, false], [{ validateUrls: false }, false],

View File

@@ -1,18 +1,16 @@
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { Mock } from 'ts-mockery'; import { fromPartial } from '@total-typescript/shoehorn';
import type { Settings, ShortUrlsListSettings as ShortUrlsSettings } from '../../src/settings/reducers/settings'; import type { ShortUrlsListSettings as ShortUrlsSettings } from '../../src/settings/reducers/settings';
import { ShortUrlsListSettings } from '../../src/settings/ShortUrlsListSettings'; import { ShortUrlsListSettings } from '../../src/settings/ShortUrlsListSettings';
import type { ShortUrlsOrder } from '../../src/short-urls/data'; import type { ShortUrlsOrder } from '../../src/short-urls/data';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
describe('<ShortUrlsListSettings />', () => { describe('<ShortUrlsListSettings />', () => {
const setSettings = jest.fn(); const setSettings = vi.fn();
const setUp = (shortUrlsList?: ShortUrlsSettings) => renderWithEvents( const setUp = (shortUrlsList?: ShortUrlsSettings) => renderWithEvents(
<ShortUrlsListSettings settings={Mock.of<Settings>({ shortUrlsList })} setShortUrlsListSettings={setSettings} />, <ShortUrlsListSettings settings={fromPartial({ shortUrlsList })} setShortUrlsListSettings={setSettings} />,
); );
afterEach(jest.clearAllMocks);
it.each([ it.each([
[undefined, 'Order by: Created at - DESC'], [undefined, 'Order by: Created at - DESC'],
[{}, 'Order by: Created at - DESC'], [{}, 'Order by: Created at - DESC'],

View File

@@ -1,18 +1,16 @@
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { Mock } from 'ts-mockery'; import { fromPartial } from '@total-typescript/shoehorn';
import type { Settings, TagsSettings as TagsSettingsOptions } from '../../src/settings/reducers/settings'; import type { TagsSettings as TagsSettingsOptions } from '../../src/settings/reducers/settings';
import { TagsSettings } from '../../src/settings/TagsSettings'; import { TagsSettings } from '../../src/settings/TagsSettings';
import type { TagsOrder } from '../../src/tags/data/TagsListChildrenProps'; import type { TagsOrder } from '../../src/tags/data/TagsListChildrenProps';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
describe('<TagsSettings />', () => { describe('<TagsSettings />', () => {
const setTagsSettings = jest.fn(); const setTagsSettings = vi.fn();
const setUp = (tags?: TagsSettingsOptions) => renderWithEvents( const setUp = (tags?: TagsSettingsOptions) => renderWithEvents(
<TagsSettings settings={Mock.of<Settings>({ tags })} setTagsSettings={setTagsSettings} />, <TagsSettings settings={fromPartial({ tags })} setTagsSettings={setTagsSettings} />,
); );
afterEach(jest.clearAllMocks);
it('renders expected amount of groups', () => { it('renders expected amount of groups', () => {
setUp(); setUp();

View File

@@ -1,18 +1,16 @@
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { Mock } from 'ts-mockery'; import { fromPartial } from '@total-typescript/shoehorn';
import type { Settings, UiSettings } from '../../src/settings/reducers/settings'; import type { UiSettings } from '../../src/settings/reducers/settings';
import { UserInterfaceSettings } from '../../src/settings/UserInterfaceSettings'; import { UserInterfaceSettings } from '../../src/settings/UserInterfaceSettings';
import type { Theme } from '../../src/utils/theme'; import type { Theme } from '../../src/utils/theme';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
describe('<UserInterfaceSettings />', () => { describe('<UserInterfaceSettings />', () => {
const setUiSettings = jest.fn(); const setUiSettings = vi.fn();
const setUp = (ui?: UiSettings) => renderWithEvents( const setUp = (ui?: UiSettings) => renderWithEvents(
<UserInterfaceSettings settings={Mock.of<Settings>({ ui })} setUiSettings={setUiSettings} />, <UserInterfaceSettings settings={fromPartial({ ui })} setUiSettings={setUiSettings} />,
); );
afterEach(jest.clearAllMocks);
it.each([ it.each([
[{ theme: 'dark' as Theme }, true], [{ theme: 'dark' as Theme }, true],
[{ theme: 'light' as Theme }, false], [{ theme: 'light' as Theme }, false],

View File

@@ -1,17 +1,15 @@
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { Mock } from 'ts-mockery'; import { fromPartial } from '@total-typescript/shoehorn';
import type { Settings } from '../../src/settings/reducers/settings'; import type { Settings } from '../../src/settings/reducers/settings';
import { VisitsSettings } from '../../src/settings/VisitsSettings'; import { VisitsSettings } from '../../src/settings/VisitsSettings';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
describe('<VisitsSettings />', () => { describe('<VisitsSettings />', () => {
const setVisitsSettings = jest.fn(); const setVisitsSettings = vi.fn();
const setUp = (settings: Partial<Settings> = {}) => renderWithEvents( const setUp = (settings: Partial<Settings> = {}) => renderWithEvents(
<VisitsSettings settings={Mock.of<Settings>(settings)} setVisitsSettings={setVisitsSettings} />, <VisitsSettings settings={fromPartial(settings)} setVisitsSettings={setVisitsSettings} />,
); );
afterEach(jest.clearAllMocks);
it('renders expected components', () => { it('renders expected components', () => {
setUp(); setUp();
@@ -21,10 +19,10 @@ describe('<VisitsSettings />', () => {
}); });
it.each([ it.each([
[Mock.all<Settings>(), 'Last 30 days'], [fromPartial<Settings>({}), 'Last 30 days'],
[Mock.of<Settings>({ visits: {} }), 'Last 30 days'], [fromPartial<Settings>({ visits: {} }), 'Last 30 days'],
[ [
Mock.of<Settings>({ fromPartial<Settings>({
visits: { visits: {
defaultInterval: 'last7Days', defaultInterval: 'last7Days',
}, },
@@ -32,7 +30,7 @@ describe('<VisitsSettings />', () => {
'Last 7 days', 'Last 7 days',
], ],
[ [
Mock.of<Settings>({ fromPartial<Settings>({
visits: { visits: {
defaultInterval: 'today', defaultInterval: 'today',
}, },
@@ -63,17 +61,17 @@ describe('<VisitsSettings />', () => {
it.each([ it.each([
[ [
Mock.all<Settings>(), fromPartial<Settings>({}),
/The visits coming from potential bots will be included.$/, /The visits coming from potential bots will be included.$/,
/The visits coming from potential bots will be excluded.$/, /The visits coming from potential bots will be excluded.$/,
], ],
[ [
Mock.of<Settings>({ visits: { excludeBots: false } }), fromPartial<Settings>({ visits: { excludeBots: false } }),
/The visits coming from potential bots will be included.$/, /The visits coming from potential bots will be included.$/,
/The visits coming from potential bots will be excluded.$/, /The visits coming from potential bots will be excluded.$/,
], ],
[ [
Mock.of<Settings>({ visits: { excludeBots: true } }), fromPartial<Settings>({ visits: { excludeBots: true } }),
/The visits coming from potential bots will be excluded.$/, /The visits coming from potential bots will be excluded.$/,
/The visits coming from potential bots will be included.$/, /The visits coming from potential bots will be included.$/,
], ],

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<UserInterfaceSettings /> shows different icons based on theme 1`] = ` exports[`<UserInterfaceSettings /> > shows different icons based on theme 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="svg-inline--fa fa-moon user-interface__theme-icon" class="svg-inline--fa fa-moon user-interface__theme-icon"
@@ -18,7 +18,7 @@ exports[`<UserInterfaceSettings /> shows different icons based on theme 1`] = `
</svg> </svg>
`; `;
exports[`<UserInterfaceSettings /> shows different icons based on theme 2`] = ` exports[`<UserInterfaceSettings /> > shows different icons based on theme 2`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="svg-inline--fa fa-sun user-interface__theme-icon" class="svg-inline--fa fa-sun user-interface__theme-icon"
@@ -36,7 +36,7 @@ exports[`<UserInterfaceSettings /> shows different icons based on theme 2`] = `
</svg> </svg>
`; `;
exports[`<UserInterfaceSettings /> shows different icons based on theme 3`] = ` exports[`<UserInterfaceSettings /> > shows different icons based on theme 3`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="svg-inline--fa fa-sun user-interface__theme-icon" class="svg-inline--fa fa-sun user-interface__theme-icon"

View File

@@ -1,4 +1,4 @@
import { Mock } from 'ts-mockery'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ShlinkState } from '../../../src/container/types'; import type { ShlinkState } from '../../../src/container/types';
import { migrateDeprecatedSettings } from '../../../src/settings/helpers'; import { migrateDeprecatedSettings } from '../../../src/settings/helpers';
@@ -9,7 +9,7 @@ describe('settings-helpers', () => {
}); });
it('updates settings as expected', () => { it('updates settings as expected', () => {
const state = Mock.of<ShlinkState>({ const state = fromPartial<ShlinkState>({
settings: { settings: {
visits: { visits: {
defaultInterval: 'last180days' as any, defaultInterval: 'last180days' as any,

View File

@@ -1,6 +1,5 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { Mock } from 'ts-mockery'; import { fromPartial } from '@total-typescript/shoehorn';
import type { Settings } from '../../src/settings/reducers/settings';
import { CreateShortUrl as createShortUrlsCreator } from '../../src/short-urls/CreateShortUrl'; import { CreateShortUrl as createShortUrlsCreator } from '../../src/short-urls/CreateShortUrl';
import type { ShortUrlCreation } from '../../src/short-urls/reducers/shortUrlCreation'; import type { ShortUrlCreation } from '../../src/short-urls/reducers/shortUrlCreation';
@@ -8,8 +7,8 @@ describe('<CreateShortUrl />', () => {
const ShortUrlForm = () => <span>ShortUrlForm</span>; const ShortUrlForm = () => <span>ShortUrlForm</span>;
const CreateShortUrlResult = () => <span>CreateShortUrlResult</span>; const CreateShortUrlResult = () => <span>CreateShortUrlResult</span>;
const shortUrlCreation = { validateUrls: true }; const shortUrlCreation = { validateUrls: true };
const shortUrlCreationResult = Mock.all<ShortUrlCreation>(); const shortUrlCreationResult = fromPartial<ShortUrlCreation>({});
const createShortUrl = jest.fn(async () => Promise.resolve()); const createShortUrl = vi.fn(async () => Promise.resolve());
const CreateShortUrl = createShortUrlsCreator(ShortUrlForm, CreateShortUrlResult); const CreateShortUrl = createShortUrlsCreator(ShortUrlForm, CreateShortUrlResult);
const setUp = () => render( const setUp = () => render(
<CreateShortUrl <CreateShortUrl
@@ -17,7 +16,7 @@ describe('<CreateShortUrl />', () => {
createShortUrl={createShortUrl} createShortUrl={createShortUrl}
selectedServer={null} selectedServer={null}
resetCreateShortUrl={() => {}} resetCreateShortUrl={() => {}}
settings={Mock.of<Settings>({ shortUrlCreation })} settings={fromPartial({ shortUrlCreation })}
/>, />,
); );

View File

@@ -1,8 +1,6 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { Mock } from 'ts-mockery';
import type { Settings } from '../../src/settings/reducers/settings';
import type { ShortUrl } from '../../src/short-urls/data';
import { EditShortUrl as createEditShortUrl } from '../../src/short-urls/EditShortUrl'; import { EditShortUrl as createEditShortUrl } from '../../src/short-urls/EditShortUrl';
import type { ShortUrlDetail } from '../../src/short-urls/reducers/shortUrlDetail'; import type { ShortUrlDetail } from '../../src/short-urls/reducers/shortUrlDetail';
import type { ShortUrlEdition } from '../../src/short-urls/reducers/shortUrlEdition'; import type { ShortUrlEdition } from '../../src/short-urls/reducers/shortUrlEdition';
@@ -13,12 +11,12 @@ describe('<EditShortUrl />', () => {
const setUp = (detail: Partial<ShortUrlDetail> = {}, edition: Partial<ShortUrlEdition> = {}) => render( const setUp = (detail: Partial<ShortUrlDetail> = {}, edition: Partial<ShortUrlEdition> = {}) => render(
<MemoryRouter> <MemoryRouter>
<EditShortUrl <EditShortUrl
settings={Mock.of<Settings>({ shortUrlCreation })} settings={fromPartial({ shortUrlCreation })}
selectedServer={null} selectedServer={null}
shortUrlDetail={Mock.of<ShortUrlDetail>(detail)} shortUrlDetail={fromPartial(detail)}
shortUrlEdition={Mock.of<ShortUrlEdition>(edition)} shortUrlEdition={fromPartial(edition)}
getShortUrlDetail={jest.fn()} getShortUrlDetail={vi.fn()}
editShortUrl={jest.fn(async () => Promise.resolve())} editShortUrl={vi.fn(async () => Promise.resolve())}
/> />
</MemoryRouter>, </MemoryRouter>,
); );
@@ -38,7 +36,7 @@ describe('<EditShortUrl />', () => {
}); });
it('renders form when detail properly loads', () => { it('renders form when detail properly loads', () => {
setUp({ shortUrl: Mock.of<ShortUrl>({ meta: {} }) }); setUp({ shortUrl: fromPartial({ meta: {} }) });
expect(screen.getByText('ShortUrlForm')).toBeInTheDocument(); expect(screen.getByText('ShortUrlForm')).toBeInTheDocument();
expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); expect(screen.queryByText('Loading...')).not.toBeInTheDocument();

View File

@@ -1,12 +1,12 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { Mock } from 'ts-mockery';
import type { ShlinkPaginator } from '../../src/api/types'; import type { ShlinkPaginator } from '../../src/api/types';
import { Paginator } from '../../src/short-urls/Paginator'; import { Paginator } from '../../src/short-urls/Paginator';
import { ELLIPSIS } from '../../src/utils/helpers/pagination'; import { ELLIPSIS } from '../../src/utils/helpers/pagination';
describe('<Paginator />', () => { describe('<Paginator />', () => {
const buildPaginator = (pagesCount?: number) => Mock.of<ShlinkPaginator>({ pagesCount, currentPage: 1 }); const buildPaginator = (pagesCount?: number) => fromPartial<ShlinkPaginator>({ pagesCount, currentPage: 1 });
const setUp = (paginator?: ShlinkPaginator, currentQueryString?: string) => render( const setUp = (paginator?: ShlinkPaginator, currentQueryString?: string) => render(
<MemoryRouter> <MemoryRouter>
<Paginator serverId="abc123" paginator={paginator} currentQueryString={currentQueryString} /> <Paginator serverId="abc123" paginator={paginator} currentQueryString={currentQueryString} />

View File

@@ -1,7 +1,7 @@
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import type { UserEvent } from '@testing-library/user-event/setup/setup'; import type { UserEvent } from '@testing-library/user-event/setup/setup';
import { fromPartial } from '@total-typescript/shoehorn';
import { formatISO } from 'date-fns'; import { formatISO } from 'date-fns';
import { Mock } from 'ts-mockery';
import type { ReachableServer, SelectedServer } from '../../src/servers/data'; import type { ReachableServer, SelectedServer } from '../../src/servers/data';
import type { Mode } from '../../src/short-urls/ShortUrlForm'; import type { Mode } from '../../src/short-urls/ShortUrlForm';
import { ShortUrlForm as createShortUrlForm } from '../../src/short-urls/ShortUrlForm'; import { ShortUrlForm as createShortUrlForm } from '../../src/short-urls/ShortUrlForm';
@@ -10,7 +10,7 @@ import type { OptionalString } from '../../src/utils/utils';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
describe('<ShortUrlForm />', () => { describe('<ShortUrlForm />', () => {
const createShortUrl = jest.fn(async () => Promise.resolve()); const createShortUrl = vi.fn(async () => Promise.resolve());
const ShortUrlForm = createShortUrlForm(() => <span>TagsSelector</span>, () => <span>DomainSelector</span>); const ShortUrlForm = createShortUrlForm(() => <span>TagsSelector</span>, () => <span>DomainSelector</span>);
const setUp = (selectedServer: SelectedServer = null, mode: Mode = 'create', title?: OptionalString) => const setUp = (selectedServer: SelectedServer = null, mode: Mode = 'create', title?: OptionalString) =>
renderWithEvents( renderWithEvents(
@@ -23,8 +23,6 @@ describe('<ShortUrlForm />', () => {
/>, />,
); );
afterEach(jest.clearAllMocks);
it.each([ it.each([
[ [
async (user: UserEvent) => { async (user: UserEvent) => {
@@ -51,7 +49,7 @@ describe('<ShortUrlForm />', () => {
ios: 'https://ios.com', ios: 'https://ios.com',
}, },
}, },
Mock.of<ReachableServer>({ version: '3.5.0' }), fromPartial<ReachableServer>({ version: '3.5.0' }),
], ],
])('saves short URL with data set in form controls', async (extraFields, extraExpectedValues, selectedServer) => { ])('saves short URL with data set in form controls', async (extraFields, extraExpectedValues, selectedServer) => {
const { user } = setUp(selectedServer); const { user } = setUp(selectedServer);
@@ -102,7 +100,7 @@ describe('<ShortUrlForm />', () => {
[undefined, false, undefined], [undefined, false, undefined],
['old title', false, null], ['old title', false, null],
])('sends expected title based on original and new values', async (originalTitle, withNewTitle, expectedSentTitle) => { ])('sends expected title based on original and new values', async (originalTitle, withNewTitle, expectedSentTitle) => {
const { user } = setUp(Mock.of<ReachableServer>({ version: '2.6.0' }), 'create', originalTitle); const { user } = setUp(fromPartial({ version: '2.6.0' }), 'create', originalTitle);
await user.type(screen.getByPlaceholderText('URL to be shortened'), 'https://long-domain.com/foo/bar'); await user.type(screen.getByPlaceholderText('URL to be shortened'), 'https://long-domain.com/foo/bar');
await user.clear(screen.getByPlaceholderText('Title')); await user.clear(screen.getByPlaceholderText('Title'));
@@ -117,10 +115,10 @@ describe('<ShortUrlForm />', () => {
}); });
it.each([ it.each([
[Mock.of<ReachableServer>({ version: '3.0.0' }), false], [fromPartial<ReachableServer>({ version: '3.0.0' }), false],
[Mock.of<ReachableServer>({ version: '3.4.0' }), false], [fromPartial<ReachableServer>({ version: '3.4.0' }), false],
[Mock.of<ReachableServer>({ version: '3.5.0' }), true], [fromPartial<ReachableServer>({ version: '3.5.0' }), true],
[Mock.of<ReachableServer>({ version: '3.6.0' }), true], [fromPartial<ReachableServer>({ version: '3.6.0' }), true],
])('shows device-specific long URLs only for servers supporting it', (selectedServer, fieldsExist) => { ])('shows device-specific long URLs only for servers supporting it', (selectedServer, fieldsExist) => {
setUp(selectedServer); setUp(selectedServer);
const placeholders = ['Android-specific redirection', 'iOS-specific redirection', 'Desktop-specific redirection']; const placeholders = ['Android-specific redirection', 'iOS-specific redirection', 'Desktop-specific redirection'];

View File

@@ -1,25 +1,24 @@
import { screen, waitFor } from '@testing-library/react'; import { screen, waitFor } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn';
import { endOfDay, formatISO, startOfDay } from 'date-fns'; import { endOfDay, formatISO, startOfDay } from 'date-fns';
import { MemoryRouter, useLocation, useNavigate } from 'react-router-dom'; import { MemoryRouter, useLocation, useNavigate } from 'react-router-dom';
import { Mock } from 'ts-mockery';
import type { ReachableServer, SelectedServer } from '../../src/servers/data'; import type { ReachableServer, SelectedServer } from '../../src/servers/data';
import type { Settings } from '../../src/settings/reducers/settings';
import { ShortUrlsFilteringBar as filteringBarCreator } from '../../src/short-urls/ShortUrlsFilteringBar'; import { ShortUrlsFilteringBar as filteringBarCreator } from '../../src/short-urls/ShortUrlsFilteringBar';
import { formatDate } from '../../src/utils/helpers/date'; import { formatDate } from '../../src/utils/helpers/date';
import type { DateRange } from '../../src/utils/helpers/dateIntervals'; import type { DateRange } from '../../src/utils/helpers/dateIntervals';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
jest.mock('react-router-dom', () => ({ vi.mock('react-router-dom', async () => ({
...jest.requireActual('react-router-dom'), ...(await vi.importActual<any>('react-router-dom')),
useParams: jest.fn().mockReturnValue({ serverId: '1' }), useParams: vi.fn().mockReturnValue({ serverId: '1' }),
useNavigate: jest.fn(), useNavigate: vi.fn(),
useLocation: jest.fn().mockReturnValue({}), useLocation: vi.fn().mockReturnValue({}),
})); }));
describe('<ShortUrlsFilteringBar />', () => { describe('<ShortUrlsFilteringBar />', () => {
const ShortUrlsFilteringBar = filteringBarCreator(() => <>ExportShortUrlsBtn</>, () => <>TagsSelector</>); const ShortUrlsFilteringBar = filteringBarCreator(() => <>ExportShortUrlsBtn</>, () => <>TagsSelector</>);
const navigate = jest.fn(); const navigate = vi.fn();
const handleOrderBy = jest.fn(); const handleOrderBy = vi.fn();
const now = new Date(); const now = new Date();
const setUp = (search = '', selectedServer?: SelectedServer) => { const setUp = (search = '', selectedServer?: SelectedServer) => {
(useLocation as any).mockReturnValue({ search }); (useLocation as any).mockReturnValue({ search });
@@ -28,17 +27,15 @@ describe('<ShortUrlsFilteringBar />', () => {
return renderWithEvents( return renderWithEvents(
<MemoryRouter> <MemoryRouter>
<ShortUrlsFilteringBar <ShortUrlsFilteringBar
selectedServer={selectedServer ?? Mock.all<SelectedServer>()} selectedServer={selectedServer ?? fromPartial({})}
order={{}} order={{}}
handleOrderBy={handleOrderBy} handleOrderBy={handleOrderBy}
settings={Mock.of<Settings>({ visits: {} })} settings={fromPartial({ visits: {} })}
/> />
</MemoryRouter>, </MemoryRouter>,
); );
}; };
afterEach(jest.clearAllMocks);
it('renders expected children components', () => { it('renders expected children components', () => {
setUp(); setUp();
@@ -74,12 +71,12 @@ describe('<ShortUrlsFilteringBar />', () => {
}); });
it.each([ it.each([
['tags=foo,bar,baz', Mock.of<ReachableServer>({ version: '3.0.0' }), true], ['tags=foo,bar,baz', fromPartial<ReachableServer>({ version: '3.0.0' }), true],
['tags=foo,bar', Mock.of<ReachableServer>({ version: '3.1.0' }), true], ['tags=foo,bar', fromPartial<ReachableServer>({ version: '3.1.0' }), true],
['tags=foo', Mock.of<ReachableServer>({ version: '3.0.0' }), false], ['tags=foo', fromPartial<ReachableServer>({ version: '3.0.0' }), false],
['', Mock.of<ReachableServer>({ version: '3.0.0' }), false], ['', fromPartial<ReachableServer>({ version: '3.0.0' }), false],
['tags=foo,bar,baz', Mock.of<ReachableServer>({ version: '2.10.0' }), false], ['tags=foo,bar,baz', fromPartial<ReachableServer>({ version: '2.10.0' }), false],
['', Mock.of<ReachableServer>({ version: '2.10.0' }), false], ['', fromPartial<ReachableServer>({ version: '2.10.0' }), false],
])( ])(
'renders tags mode toggle if the server supports it and there is more than one tag selected', 'renders tags mode toggle if the server supports it and there is more than one tag selected',
(search, selectedServer, shouldHaveComponent) => { (search, selectedServer, shouldHaveComponent) => {
@@ -98,7 +95,7 @@ describe('<ShortUrlsFilteringBar />', () => {
['&tagsMode=all', 'With all the tags.'], ['&tagsMode=all', 'With all the tags.'],
['&tagsMode=any', 'With any of the tags.'], ['&tagsMode=any', 'With any of the tags.'],
])('expected tags mode tooltip title', async (initialTagsMode, expectedToggleText) => { ])('expected tags mode tooltip title', async (initialTagsMode, expectedToggleText) => {
const { user } = setUp(`tags=foo,bar${initialTagsMode}`, Mock.of<ReachableServer>({ version: '3.0.0' })); const { user } = setUp(`tags=foo,bar${initialTagsMode}`, fromPartial({ version: '3.0.0' }));
await user.hover(screen.getByLabelText('Change tags mode')); await user.hover(screen.getByLabelText('Change tags mode'));
expect(await screen.findByRole('tooltip')).toHaveTextContent(expectedToggleText); expect(await screen.findByRole('tooltip')).toHaveTextContent(expectedToggleText);
@@ -109,7 +106,7 @@ describe('<ShortUrlsFilteringBar />', () => {
['&tagsMode=all', 'tagsMode=any'], ['&tagsMode=all', 'tagsMode=any'],
['&tagsMode=any', 'tagsMode=all'], ['&tagsMode=any', 'tagsMode=all'],
])('redirects to first page when tags mode changes', async (initialTagsMode, expectedRedirectTagsMode) => { ])('redirects to first page when tags mode changes', async (initialTagsMode, expectedRedirectTagsMode) => {
const { user } = setUp(`tags=foo,bar${initialTagsMode}`, Mock.of<ReachableServer>({ version: '3.0.0' })); const { user } = setUp(`tags=foo,bar${initialTagsMode}`, fromPartial({ version: '3.0.0' }));
expect(navigate).not.toHaveBeenCalled(); expect(navigate).not.toHaveBeenCalled();
await user.click(screen.getByLabelText('Change tags mode')); await user.click(screen.getByLabelText('Change tags mode'));
@@ -127,7 +124,7 @@ describe('<ShortUrlsFilteringBar />', () => {
['excludePastValidUntil=false', /Exclude enabled in the past/, 'excludePastValidUntil=true'], ['excludePastValidUntil=false', /Exclude enabled in the past/, 'excludePastValidUntil=true'],
['excludePastValidUntil=true', /Exclude enabled in the past/, 'excludePastValidUntil=false'], ['excludePastValidUntil=true', /Exclude enabled in the past/, 'excludePastValidUntil=false'],
])('allows to toggle filters through filtering dropdown', async (search, menuItemName, expectedQuery) => { ])('allows to toggle filters through filtering dropdown', async (search, menuItemName, expectedQuery) => {
const { user } = setUp(search, Mock.of<ReachableServer>({ version: '3.4.0' })); const { user } = setUp(search, fromPartial({ version: '3.4.0' }));
const toggleFilter = async (name: RegExp) => { const toggleFilter = async (name: RegExp) => {
await user.click(screen.getByRole('button', { name: 'Filters' })); await user.click(screen.getByRole('button', { name: 'Filters' }));
await waitFor(() => screen.findByRole('menu')); await waitFor(() => screen.findByRole('menu'));

View File

@@ -1,36 +1,35 @@
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter, useNavigate } from 'react-router-dom'; import { MemoryRouter, useNavigate } from 'react-router-dom';
import { Mock } from 'ts-mockery';
import type { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub'; import type { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
import type { ReachableServer } from '../../src/servers/data';
import type { Settings } from '../../src/settings/reducers/settings'; import type { Settings } from '../../src/settings/reducers/settings';
import type { ShortUrl, ShortUrlsOrder } from '../../src/short-urls/data'; import type { ShortUrlsOrder } from '../../src/short-urls/data';
import type { ShortUrlsList as ShortUrlsListModel } from '../../src/short-urls/reducers/shortUrlsList'; import type { ShortUrlsList as ShortUrlsListModel } from '../../src/short-urls/reducers/shortUrlsList';
import { ShortUrlsList as createShortUrlsList } from '../../src/short-urls/ShortUrlsList'; import { ShortUrlsList as createShortUrlsList } from '../../src/short-urls/ShortUrlsList';
import type { ShortUrlsTableType } from '../../src/short-urls/ShortUrlsTable'; import type { ShortUrlsTableType } from '../../src/short-urls/ShortUrlsTable';
import type { SemVer } from '../../src/utils/helpers/version'; import type { SemVer } from '../../src/utils/helpers/version';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
jest.mock('react-router-dom', () => ({ vi.mock('react-router-dom', async () => ({
...jest.requireActual('react-router-dom'), ...(await vi.importActual<any>('react-router-dom')),
useNavigate: jest.fn().mockReturnValue(jest.fn()), useNavigate: vi.fn().mockReturnValue(vi.fn()),
useLocation: jest.fn().mockReturnValue({ search: '?tags=test%20tag&search=example.com' }), useLocation: vi.fn().mockReturnValue({ search: '?tags=test%20tag&search=example.com' }),
})); }));
describe('<ShortUrlsList />', () => { describe('<ShortUrlsList />', () => {
const ShortUrlsTable: ShortUrlsTableType = ({ onTagClick }) => <span onClick={() => onTagClick?.('foo')}>ShortUrlsTable</span>; const ShortUrlsTable: ShortUrlsTableType = ({ onTagClick }) => <span onClick={() => onTagClick?.('foo')}>ShortUrlsTable</span>;
const ShortUrlsFilteringBar = () => <span>ShortUrlsFilteringBar</span>; const ShortUrlsFilteringBar = () => <span>ShortUrlsFilteringBar</span>;
const listShortUrlsMock = jest.fn(); const listShortUrlsMock = vi.fn();
const navigate = jest.fn(); const navigate = vi.fn();
const shortUrlsList = Mock.of<ShortUrlsListModel>({ const shortUrlsList = fromPartial<ShortUrlsListModel>({
shortUrls: { shortUrls: {
data: [ data: [
Mock.of<ShortUrl>({ {
shortCode: 'testShortCode', shortCode: 'testShortCode',
shortUrl: 'https://www.example.com/testShortUrl', shortUrl: 'https://www.example.com/testShortUrl',
longUrl: 'https://www.example.com/testLongUrl', longUrl: 'https://www.example.com/testLongUrl',
tags: ['test tag'], tags: ['test tag'],
}), },
], ],
pagination: { pagesCount: 3 }, pagination: { pagesCount: 3 },
}, },
@@ -39,11 +38,11 @@ describe('<ShortUrlsList />', () => {
const setUp = (settings: Partial<Settings> = {}, version: SemVer = '3.0.0') => renderWithEvents( const setUp = (settings: Partial<Settings> = {}, version: SemVer = '3.0.0') => renderWithEvents(
<MemoryRouter> <MemoryRouter>
<ShortUrlsList <ShortUrlsList
{...Mock.of<MercureBoundProps>({ mercureInfo: { loading: true } })} {...fromPartial<MercureBoundProps>({ mercureInfo: { loading: true } })}
listShortUrls={listShortUrlsMock} listShortUrls={listShortUrlsMock}
shortUrlsList={shortUrlsList} shortUrlsList={shortUrlsList}
selectedServer={Mock.of<ReachableServer>({ id: '1', version })} selectedServer={fromPartial({ id: '1', version })}
settings={Mock.of<Settings>(settings)} settings={fromPartial(settings)}
/> />
</MemoryRouter>, </MemoryRouter>,
); );
@@ -52,8 +51,6 @@ describe('<ShortUrlsList />', () => {
(useNavigate as any).mockReturnValue(navigate); (useNavigate as any).mockReturnValue(navigate);
}); });
afterEach(jest.clearAllMocks);
it('wraps expected components', () => { it('wraps expected components', () => {
setUp(); setUp();
@@ -81,9 +78,9 @@ describe('<ShortUrlsList />', () => {
}); });
it.each([ it.each([
[Mock.of<ShortUrlsOrder>({ field: 'visits', dir: 'ASC' }), 'visits', 'ASC'], [fromPartial<ShortUrlsOrder>({ field: 'visits', dir: 'ASC' }), 'visits', 'ASC'],
[Mock.of<ShortUrlsOrder>({ field: 'title', dir: 'DESC' }), 'title', 'DESC'], [fromPartial<ShortUrlsOrder>({ field: 'title', dir: 'DESC' }), 'title', 'DESC'],
[Mock.all<ShortUrlsOrder>(), undefined, undefined], [fromPartial<ShortUrlsOrder>({}), undefined, undefined],
])('has expected initial ordering based on settings', (defaultOrdering, field, dir) => { ])('has expected initial ordering based on settings', (defaultOrdering, field, dir) => {
setUp({ shortUrlsList: { defaultOrdering } }); setUp({ shortUrlsList: { defaultOrdering } });
expect(listShortUrlsMock).toHaveBeenCalledWith(expect.objectContaining({ expect(listShortUrlsMock).toHaveBeenCalledWith(expect.objectContaining({
@@ -92,23 +89,23 @@ describe('<ShortUrlsList />', () => {
}); });
it.each([ it.each([
[Mock.of<Settings>({ [fromPartial<Settings>({
shortUrlsList: { shortUrlsList: {
defaultOrdering: { field: 'visits', dir: 'ASC' }, defaultOrdering: { field: 'visits', dir: 'ASC' },
}, },
}), '3.3.0' as SemVer, { field: 'visits', dir: 'ASC' }], }), '3.3.0' as SemVer, { field: 'visits', dir: 'ASC' }],
[Mock.of<Settings>({ [fromPartial<Settings>({
shortUrlsList: { shortUrlsList: {
defaultOrdering: { field: 'visits', dir: 'ASC' }, defaultOrdering: { field: 'visits', dir: 'ASC' },
}, },
visits: { excludeBots: true }, visits: { excludeBots: true },
}), '3.3.0' as SemVer, { field: 'visits', dir: 'ASC' }], }), '3.3.0' as SemVer, { field: 'visits', dir: 'ASC' }],
[Mock.of<Settings>({ [fromPartial<Settings>({
shortUrlsList: { shortUrlsList: {
defaultOrdering: { field: 'visits', dir: 'ASC' }, defaultOrdering: { field: 'visits', dir: 'ASC' },
}, },
}), '3.4.0' as SemVer, { field: 'visits', dir: 'ASC' }], }), '3.4.0' as SemVer, { field: 'visits', dir: 'ASC' }],
[Mock.of<Settings>({ [fromPartial<Settings>({
shortUrlsList: { shortUrlsList: {
defaultOrdering: { field: 'visits', dir: 'ASC' }, defaultOrdering: { field: 'visits', dir: 'ASC' },
}, },

View File

@@ -1,6 +1,6 @@
import { fireEvent, screen } from '@testing-library/react'; import { fireEvent, screen } from '@testing-library/react';
import { Mock } from 'ts-mockery'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ReachableServer, SelectedServer } from '../../src/servers/data'; import type { SelectedServer } from '../../src/servers/data';
import type { ShortUrlsOrderableFields } from '../../src/short-urls/data'; import type { ShortUrlsOrderableFields } from '../../src/short-urls/data';
import { SHORT_URLS_ORDERABLE_FIELDS } from '../../src/short-urls/data'; import { SHORT_URLS_ORDERABLE_FIELDS } from '../../src/short-urls/data';
import type { ShortUrlsList } from '../../src/short-urls/reducers/shortUrlsList'; import type { ShortUrlsList } from '../../src/short-urls/reducers/shortUrlsList';
@@ -8,15 +8,13 @@ import { ShortUrlsTable as shortUrlsTableCreator } from '../../src/short-urls/Sh
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
describe('<ShortUrlsTable />', () => { describe('<ShortUrlsTable />', () => {
const shortUrlsList = Mock.all<ShortUrlsList>(); const shortUrlsList = fromPartial<ShortUrlsList>({});
const orderByColumn = jest.fn(); const orderByColumn = vi.fn();
const ShortUrlsTable = shortUrlsTableCreator(() => <span>ShortUrlsRow</span>); const ShortUrlsTable = shortUrlsTableCreator(() => <span>ShortUrlsRow</span>);
const setUp = (server: SelectedServer = null) => renderWithEvents( const setUp = (server: SelectedServer = null) => renderWithEvents(
<ShortUrlsTable shortUrlsList={shortUrlsList} selectedServer={server} orderByColumn={() => orderByColumn} />, <ShortUrlsTable shortUrlsList={shortUrlsList} selectedServer={server} orderByColumn={() => orderByColumn} />,
); );
afterEach(jest.resetAllMocks);
it('should render inner table by default', () => { it('should render inner table by default', () => {
setUp(); setUp();
expect(screen.getByRole('table')).toBeInTheDocument(); expect(screen.getByRole('table')).toBeInTheDocument();
@@ -56,7 +54,7 @@ describe('<ShortUrlsTable />', () => {
}); });
it('should render composed title column', () => { it('should render composed title column', () => {
setUp(Mock.of<ReachableServer>({ version: '2.0.0' })); setUp(fromPartial({ version: '2.0.0' }));
const { innerHTML } = screen.getAllByRole('columnheader')[2]; const { innerHTML } = screen.getAllByRole('columnheader')[2];

View File

@@ -1,21 +1,18 @@
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { Mock } from 'ts-mockery'; import { fromPartial } from '@total-typescript/shoehorn';
import type { ShortUrl } from '../../../src/short-urls/data';
import { CreateShortUrlResult as createResult } from '../../../src/short-urls/helpers/CreateShortUrlResult'; import { CreateShortUrlResult as createResult } from '../../../src/short-urls/helpers/CreateShortUrlResult';
import type { ShortUrlCreation } from '../../../src/short-urls/reducers/shortUrlCreation'; import type { ShortUrlCreation } from '../../../src/short-urls/reducers/shortUrlCreation';
import type { TimeoutToggle } from '../../../src/utils/helpers/hooks'; import type { TimeoutToggle } from '../../../src/utils/helpers/hooks';
import { renderWithEvents } from '../../__helpers__/setUpTest'; import { renderWithEvents } from '../../__helpers__/setUpTest';
describe('<CreateShortUrlResult />', () => { describe('<CreateShortUrlResult />', () => {
const copyToClipboard = jest.fn(); const copyToClipboard = vi.fn();
const useTimeoutToggle = jest.fn(() => [false, copyToClipboard]) as TimeoutToggle; const useTimeoutToggle = vi.fn(() => [false, copyToClipboard]) as TimeoutToggle;
const CreateShortUrlResult = createResult(useTimeoutToggle); const CreateShortUrlResult = createResult(useTimeoutToggle);
const setUp = (creation: ShortUrlCreation) => renderWithEvents( const setUp = (creation: ShortUrlCreation) => renderWithEvents(
<CreateShortUrlResult resetCreateShortUrl={() => {}} creation={creation} />, <CreateShortUrlResult resetCreateShortUrl={() => {}} creation={creation} />,
); );
afterEach(jest.clearAllMocks);
it('renders an error when error is true', () => { it('renders an error when error is true', () => {
setUp({ error: true, saved: false, saving: false }); setUp({ error: true, saved: false, saving: false });
expect(screen.getByText('An error occurred while creating the URL :(')).toBeInTheDocument(); expect(screen.getByText('An error occurred while creating the URL :(')).toBeInTheDocument();
@@ -28,14 +25,14 @@ describe('<CreateShortUrlResult />', () => {
it('renders a result message when result is provided', () => { it('renders a result message when result is provided', () => {
setUp( setUp(
{ result: Mock.of<ShortUrl>({ shortUrl: 'https://s.test/abc123' }), saving: false, saved: true, error: false }, { result: fromPartial({ shortUrl: 'https://s.test/abc123' }), saving: false, saved: true, error: false },
); );
expect(screen.getByText(/The short URL is/)).toHaveTextContent('Great! The short URL is https://s.test/abc123'); expect(screen.getByText(/The short URL is/)).toHaveTextContent('Great! The short URL is https://s.test/abc123');
}); });
it('Invokes tooltip timeout when copy to clipboard button is clicked', async () => { it('Invokes tooltip timeout when copy to clipboard button is clicked', async () => {
const { user } = setUp( const { user } = setUp(
{ result: Mock.of<ShortUrl>({ shortUrl: 'https://s.test/abc123' }), saving: false, saved: true, error: false }, { result: fromPartial({ shortUrl: 'https://s.test/abc123' }), saving: false, saved: true, error: false },
); );
expect(copyToClipboard).not.toHaveBeenCalled(); expect(copyToClipboard).not.toHaveBeenCalled();

View File

@@ -1,6 +1,6 @@
import { screen, waitFor } from '@testing-library/react'; import { screen, waitFor } from '@testing-library/react';
import { Mock } from 'ts-mockery'; import { fromPartial } from '@total-typescript/shoehorn';
import type { InvalidShortUrlDeletion, ProblemDetailsError } from '../../../src/api/types/errors'; import type { InvalidShortUrlDeletion } from '../../../src/api/types/errors';
import { ErrorTypeV2, ErrorTypeV3 } from '../../../src/api/types/errors'; import { ErrorTypeV2, ErrorTypeV3 } from '../../../src/api/types/errors';
import type { ShortUrl } from '../../../src/short-urls/data'; import type { ShortUrl } from '../../../src/short-urls/data';
import { DeleteShortUrlModal } from '../../../src/short-urls/helpers/DeleteShortUrlModal'; import { DeleteShortUrlModal } from '../../../src/short-urls/helpers/DeleteShortUrlModal';
@@ -9,36 +9,34 @@ import { renderWithEvents } from '../../__helpers__/setUpTest';
import { TestModalWrapper } from '../../__helpers__/TestModalWrapper'; import { TestModalWrapper } from '../../__helpers__/TestModalWrapper';
describe('<DeleteShortUrlModal />', () => { describe('<DeleteShortUrlModal />', () => {
const shortUrl = Mock.of<ShortUrl>({ const shortUrl = fromPartial<ShortUrl>({
tags: [], tags: [],
shortCode: 'abc123', shortCode: 'abc123',
longUrl: 'https://long-domain.com/foo/bar', longUrl: 'https://long-domain.com/foo/bar',
}); });
const deleteShortUrl = jest.fn().mockResolvedValue(undefined); const deleteShortUrl = vi.fn().mockResolvedValue(undefined);
const shortUrlDeleted = jest.fn(); const shortUrlDeleted = vi.fn();
const setUp = (shortUrlDeletion: Partial<ShortUrlDeletion>) => renderWithEvents( const setUp = (shortUrlDeletion: Partial<ShortUrlDeletion>) => renderWithEvents(
<TestModalWrapper <TestModalWrapper
renderModal={(args) => ( renderModal={(args) => (
<DeleteShortUrlModal <DeleteShortUrlModal
{...args} {...args}
shortUrl={shortUrl} shortUrl={shortUrl}
shortUrlDeletion={Mock.of<ShortUrlDeletion>(shortUrlDeletion)} shortUrlDeletion={fromPartial(shortUrlDeletion)}
deleteShortUrl={deleteShortUrl} deleteShortUrl={deleteShortUrl}
shortUrlDeleted={shortUrlDeleted} shortUrlDeleted={shortUrlDeleted}
resetDeleteShortUrl={jest.fn()} resetDeleteShortUrl={vi.fn()}
/> />
)} )}
/>, />,
); );
afterEach(jest.clearAllMocks);
it('shows generic error when non-threshold error occurs', () => { it('shows generic error when non-threshold error occurs', () => {
setUp({ setUp({
loading: false, loading: false,
error: true, error: true,
shortCode: 'abc123', shortCode: 'abc123',
errorData: Mock.of<ProblemDetailsError>({ type: 'OTHER_ERROR' }), errorData: fromPartial({ type: 'OTHER_ERROR' }),
}); });
expect(screen.getByText('Something went wrong while deleting the URL :(').parentElement).not.toHaveClass( expect(screen.getByText('Something went wrong while deleting the URL :(').parentElement).not.toHaveClass(
'bg-warning', 'bg-warning',
@@ -46,8 +44,8 @@ describe('<DeleteShortUrlModal />', () => {
}); });
it.each([ it.each([
[Mock.of<InvalidShortUrlDeletion>({ type: ErrorTypeV3.INVALID_SHORT_URL_DELETION })], [fromPartial<InvalidShortUrlDeletion>({ type: ErrorTypeV3.INVALID_SHORT_URL_DELETION })],
[Mock.of<InvalidShortUrlDeletion>({ type: ErrorTypeV2.INVALID_SHORT_URL_DELETION })], [fromPartial<InvalidShortUrlDeletion>({ type: ErrorTypeV2.INVALID_SHORT_URL_DELETION })],
])('shows specific error when threshold error occurs', (errorData) => { ])('shows specific error when threshold error occurs', (errorData) => {
setUp({ loading: false, error: true, shortCode: 'abc123', errorData }); setUp({ loading: false, error: true, shortCode: 'abc123', errorData });
expect(screen.getByText('Something went wrong while deleting the URL :(').parentElement).toHaveClass('bg-warning'); expect(screen.getByText('Something went wrong while deleting the URL :(').parentElement).toHaveClass('bg-warning');

View File

@@ -1,25 +1,24 @@
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { Mock } from 'ts-mockery';
import type { ReportExporter } from '../../../src/common/services/ReportExporter'; import type { ReportExporter } from '../../../src/common/services/ReportExporter';
import type { NotFoundServer, ReachableServer, SelectedServer } from '../../../src/servers/data'; import type { NotFoundServer, SelectedServer } from '../../../src/servers/data';
import type { ShortUrl } from '../../../src/short-urls/data';
import { ExportShortUrlsBtn as createExportShortUrlsBtn } from '../../../src/short-urls/helpers/ExportShortUrlsBtn'; import { ExportShortUrlsBtn as createExportShortUrlsBtn } from '../../../src/short-urls/helpers/ExportShortUrlsBtn';
import { renderWithEvents } from '../../__helpers__/setUpTest'; import { renderWithEvents } from '../../__helpers__/setUpTest';
describe('<ExportShortUrlsBtn />', () => { describe('<ExportShortUrlsBtn />', () => {
const listShortUrls = jest.fn(); const listShortUrls = vi.fn();
const buildShlinkApiClient = jest.fn().mockReturnValue({ listShortUrls }); const buildShlinkApiClient = vi.fn().mockReturnValue({ listShortUrls });
const exportShortUrls = jest.fn(); const exportShortUrls = vi.fn();
const reportExporter = Mock.of<ReportExporter>({ exportShortUrls }); const reportExporter = fromPartial<ReportExporter>({ exportShortUrls });
const ExportShortUrlsBtn = createExportShortUrlsBtn(buildShlinkApiClient, reportExporter); const ExportShortUrlsBtn = createExportShortUrlsBtn(buildShlinkApiClient, reportExporter);
const setUp = (amount?: number, selectedServer?: SelectedServer) => renderWithEvents( const setUp = (amount?: number, selectedServer?: SelectedServer) => renderWithEvents(
<MemoryRouter> <MemoryRouter>
<ExportShortUrlsBtn selectedServer={selectedServer ?? Mock.all<SelectedServer>()} amount={amount} /> <ExportShortUrlsBtn selectedServer={selectedServer ?? fromPartial({})} amount={amount} />
</MemoryRouter>, </MemoryRouter>,
); );
afterEach(jest.clearAllMocks);
it.each([ it.each([
[undefined, '0'], [undefined, '0'],
[1, '1'], [1, '1'],
@@ -31,7 +30,7 @@ describe('<ExportShortUrlsBtn />', () => {
it.each([ it.each([
[null], [null],
[Mock.of<NotFoundServer>()], [fromPartial<NotFoundServer>({})],
])('does nothing on click if selected server is not reachable', async (selectedServer) => { ])('does nothing on click if selected server is not reachable', async (selectedServer) => {
const { user } = setUp(0, selectedServer); const { user } = setUp(0, selectedServer);
@@ -49,11 +48,29 @@ describe('<ExportShortUrlsBtn />', () => {
[385, 20], [385, 20],
])('loads proper amount of pages based on the amount of results', async (amount, expectedPageLoads) => { ])('loads proper amount of pages based on the amount of results', async (amount, expectedPageLoads) => {
listShortUrls.mockResolvedValue({ data: [] }); listShortUrls.mockResolvedValue({ data: [] });
const { user } = setUp(amount, Mock.of<ReachableServer>({ id: '123' })); const { user } = setUp(amount, fromPartial({ id: '123' }));
await user.click(screen.getByRole('button')); await user.click(screen.getByRole('button'));
expect(listShortUrls).toHaveBeenCalledTimes(expectedPageLoads); expect(listShortUrls).toHaveBeenCalledTimes(expectedPageLoads);
expect(exportShortUrls).toHaveBeenCalled(); expect(exportShortUrls).toHaveBeenCalled();
}); });
it('maps short URLs for exporting', async () => {
listShortUrls.mockResolvedValue({
data: [fromPartial<ShortUrl>({
shortUrl: 'https://s.test/short-code',
tags: [],
})],
});
const { user } = setUp(undefined, fromPartial({ id: '123' }));
await user.click(screen.getByRole('button'));
expect(exportShortUrls).toHaveBeenCalledWith([expect.objectContaining({
shortUrl: 'https://s.test/short-code',
domain: 's.test',
shortCode: 'short-code',
})]);
});
}); });

Some files were not shown because too many files have changed in this diff Show More