Merge pull request #848 from shlinkio/develop

Release 3.10.2
This commit is contained in:
Alejandro Celaya 2023-07-09 10:17:23 +02:00 committed by GitHub
commit f5e92c6897
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
149 changed files with 3148 additions and 6628 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,24 @@ 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 ## [3.10.1] - 2023-04-23
### Added ### Added
* *Nothing* * *Nothing*

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'],
};

8509
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",
@ -61,9 +57,7 @@
"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,10 +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",
"@total-typescript/shoehorn": "^0.1.0", "@total-typescript/shoehorn": "^0.1.0",
"@types/jest": "^29.2.4",
"@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",
@ -91,20 +84,19 @@
"@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": "^4.0.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",
"typescript": "^5.0.2", "typescript": "^5.0.2",
"vite": "^4.3.1", "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

@ -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

@ -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

@ -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 { fromAny, fromPartial } from '@total-typescript/shoehorn'; 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 = fromPartial<Window>({ export const windowMock = fromPartial<Window>({
document: fromAny({ document: fromAny({
createElement: jest.fn(createLinkMock), createElement: vi.fn(createLinkMock),
body: { appendChild, removeChild }, body: { appendChild, removeChild },
}), }),
}); });

View file

@ -7,8 +7,8 @@ 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 = fromPartial<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][] = [
@ -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'];

View file

@ -32,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,6 +1,6 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router';
import { AsideMenu as createAsideMenu } from '../../src/common/AsideMenu'; import { AsideMenu as createAsideMenu } from '../../src/common/AsideMenu';
describe('<AsideMenu />', () => { describe('<AsideMenu />', () => {

View file

@ -8,17 +8,16 @@ const ComponentWithError = () => {
}; };
describe('<ErrorHandler />', () => { describe('<ErrorHandler />', () => {
const reload = jest.fn(); const reload = vi.fn();
const window = fromPartial<Window>({ const window = fromPartial<Window>({
location: { reload }, location: { reload },
}); });
const cons = fromPartial<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 { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router-dom'; 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';

View file

@ -6,7 +6,7 @@ import { MenuLayout as createMenuLayout } from '../../src/common/MenuLayout';
import type { NonReachableServer, NotFoundServer, 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);

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

@ -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

@ -4,12 +4,11 @@ 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 = fromPartial<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: () => '' };

View file

@ -22,8 +22,8 @@ describe('<DomainRow />', () => {
domain={domain} domain={domain}
defaultRedirects={defaultRedirects} defaultRedirects={defaultRedirects}
selectedServer={fromPartial({})} selectedServer={fromPartial({})}
editDomainRedirects={jest.fn()} editDomainRedirects={vi.fn()}
checkDomainHealth={jest.fn()} checkDomainHealth={vi.fn()}
/> />
</tbody> </tbody>
</table>, </table>,

View file

@ -13,11 +13,9 @@ describe('<DomainSelector />', () => {
], ],
}); });
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'],
@ -26,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

@ -7,21 +7,19 @@ import type { DomainsList } from '../../src/domains/reducers/domainsList';
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={fromPartial({})} selectedServer={fromPartial({})}
/>, />,
); );
afterEach(jest.clearAllMocks);
it('shows loading message while domains are loading', () => { it('shows loading message while domains are loading', () => {
setUp(fromPartial({ loading: true, filteredDomains: [] })); setUp(fromPartial({ loading: true, filteredDomains: [] }));

View file

@ -8,7 +8,7 @@ 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
@ -19,8 +19,6 @@ describe('<DomainDropdown />', () => {
</MemoryRouter>, </MemoryRouter>,
); );
afterEach(jest.clearAllMocks);
it('renders expected menu items', () => { it('renders expected menu items', () => {
setUp(); setUp();

View file

@ -5,13 +5,11 @@ 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(fromPartial<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

@ -5,8 +5,8 @@ import { EditDomainRedirectsModal } from '../../../src/domains/helpers/EditDomai
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 = fromPartial<ShlinkDomain>({ const domain = fromPartial<ShlinkDomain>({
domain: 'foo.com', domain: 'foo.com',
redirects: { redirects: {
@ -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

@ -4,14 +4,12 @@ import type { ShlinkDomainRedirects } from '../../../src/api/types';
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 = fromPartial<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 = () => fromPartial<ShlinkApiClient>({ editDomainRedirects: editDomainRedirectsCall }); const buildShlinkApiClient = () => fromPartial<ShlinkApiClient>({ editDomainRedirects: editDomainRedirectsCall });
const editDomainRedirectsAction = editDomainRedirects(buildShlinkApiClient); const editDomainRedirectsAction = editDomainRedirects(buildShlinkApiClient);

View file

@ -13,10 +13,10 @@ import {
} from '../../../src/domains/reducers/domainsList'; } from '../../../src/domains/reducers/domainsList';
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 = () => fromPartial<ShlinkApiClient>({ listDomains, health }); const buildShlinkApiClient = () => fromPartial<ShlinkApiClient>({ listDomains, health });
const filteredDomains: Domain[] = [ const filteredDomains: Domain[] = [
fromPartial({ domain: 'foo', status: 'validating' }), fromPartial({ domain: 'foo', status: 'validating' }),
@ -30,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(

View file

@ -4,14 +4,12 @@ import { identity } from 'ramda';
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([
[fromPartial<MercureInfo>({ loading: true, error: false, mercureHubUrl: 'foo' })], [fromPartial<MercureInfo>({ loading: true, error: false, mercureHubUrl: 'foo' })],

View file

@ -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 = () => fromPartial<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

@ -5,17 +5,20 @@ import { CreateServer as createCreateServer } from '../../src/servers/CreateServ
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: fromPartial<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

@ -5,11 +5,14 @@ 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);
@ -27,8 +30,6 @@ describe('<DeleteServerModal />', () => {
); );
}; };
afterEach(jest.clearAllMocks);
it('renders a modal window', () => { it('renders a modal window', () => {
setUp(); setUp();

View file

@ -5,12 +5,15 @@ 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 = fromPartial<ReachableServer>({ const defaultSelectedServer = fromPartial<ReachableServer>({
id: 'abc123', id: 'abc123',
name: 'the_name', name: 'the_name',
@ -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,8 +31,6 @@ 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(fromPartial<SelectedServer>({})); setUp(fromPartial<SelectedServer>({}));

View file

@ -7,9 +7,9 @@ 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 = fromPartial<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>,
@ -23,8 +23,6 @@ describe('<ManageServers />', () => {
<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

@ -9,7 +9,7 @@ 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 = fromPartial<ServerWithId>({ id: 'abc123', autoConnect }); const server = fromPartial<ServerWithId>({ id: 'abc123', autoConnect });
return renderWithEvents( return renderWithEvents(
@ -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

@ -9,9 +9,9 @@ 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 },
@ -31,8 +31,8 @@ describe('<Overview />', () => {
orphanVisits: { total: 28, bots: 15, nonBots: 13 }, orphanVisits: { total: 28, bots: 15, nonBots: 13 },
})} })}
selectedServer={fromPartial({ id: serverId })} selectedServer={fromPartial({ id: serverId })}
createNewVisits={jest.fn()} createNewVisits={vi.fn()}
loadMercureInfo={jest.fn()} loadMercureInfo={vi.fn()}
mercureInfo={fromPartial<MercureInfo>({})} mercureInfo={fromPartial<MercureInfo>({})}
settings={fromPartial({ visits: { excludeBots } })} settings={fromPartial({ visits: { excludeBots } })}
/> />

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

@ -5,15 +5,13 @@ import { DuplicatedServersModal } from '../../../src/servers/helpers/DuplicatedS
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); const mockServer = (data: Partial<ServerData> = {}) => fromPartial<ServerData>(data);
beforeEach(jest.clearAllMocks);
it.each([ it.each([
[[], 0], [[], 0],
[[mockServer()], 2], [[mockServer()], 2],

View file

@ -10,9 +10,9 @@ 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 = fromPartial<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(
@ -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();

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

@ -3,11 +3,9 @@ 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 = fromPartial<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

@ -13,14 +13,12 @@ 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(fromPartial<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());
@ -33,7 +31,7 @@ describe('selectedServerReducer', () => {
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 },
}, },
@ -77,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(() => fromPartial<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, {});
@ -89,8 +87,8 @@ 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([
@ -98,7 +96,7 @@ describe('selectedServerReducer', () => {
[fromPartial<NotFoundServer>({ serverNotFound: true }), 0], [fromPartial<NotFoundServer>({ serverNotFound: true }), 0],
[fromPartial<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(),
}); });
@ -108,7 +106,7 @@ 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: fromPartial<ReachableServer>({ version: '1.2.3' }), payload: fromPartial<ReachableServer>({ version: '1.2.3' }),
type: 'something_else', type: 'something_else',
}); });

View file

@ -15,8 +15,6 @@ describe('serversReducer', () => {
def456: fromPartial({ 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({

View file

@ -5,7 +5,7 @@ import { appendChild, removeChild, windowMock } from '../../__mocks__/Window.moc
describe('ServersExporter', () => { describe('ServersExporter', () => {
const storageMock = fromPartial<LocalStorage>({ const storageMock = fromPartial<LocalStorage>({
get: jest.fn(() => ({ get: vi.fn(() => ({
abc123: { abc123: {
id: 'abc123', id: 'abc123',
name: 'foo', name: 'foo',
@ -18,16 +18,14 @@ describe('ServersExporter', () => {
}, },
} as any)), } 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;

View file

@ -4,8 +4,8 @@ import { ServersImporter } from '../../../src/servers/services/ServersImporter';
describe('ServersImporter', () => { describe('ServersImporter', () => {
const servers: RegularServer[] = [fromPartial<RegularServer>({}), fromPartial<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 = fromPartial<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(
@ -14,8 +14,6 @@ describe('ServersImporter', () => {
}); });
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(

View file

@ -7,8 +7,8 @@ import type {
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={fromPartial({ realTimeUpdates })} settings={fromPartial({ realTimeUpdates })}
@ -17,8 +17,6 @@ describe('<RealTimeUpdatesSettings />', () => {
/>, />,
); );
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

@ -5,7 +5,7 @@ import { ShortUrlCreationSettings } from '../../src/settings/ShortUrlCreationSet
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={fromPartial({ shortUrlCreation })} settings={fromPartial({ shortUrlCreation })}
@ -13,8 +13,6 @@ describe('<ShortUrlCreationSettings />', () => {
/>, />,
); );
afterEach(jest.clearAllMocks);
it.each([ it.each([
[{ validateUrls: true }, true], [{ validateUrls: true }, true],
[{ validateUrls: false }, false], [{ validateUrls: false }, false],

View file

@ -6,13 +6,11 @@ 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={fromPartial({ 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

@ -6,13 +6,11 @@ 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={fromPartial({ 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

@ -6,13 +6,11 @@ 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={fromPartial({ 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

@ -5,13 +5,11 @@ 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={fromPartial(settings)} setVisitsSettings={setVisitsSettings} />, <VisitsSettings settings={fromPartial(settings)} setVisitsSettings={setVisitsSettings} />,
); );
afterEach(jest.clearAllMocks);
it('renders expected components', () => { it('renders expected components', () => {
setUp(); setUp();

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

@ -8,7 +8,7 @@ describe('<CreateShortUrl />', () => {
const CreateShortUrlResult = () => <span>CreateShortUrlResult</span>; const CreateShortUrlResult = () => <span>CreateShortUrlResult</span>;
const shortUrlCreation = { validateUrls: true }; const shortUrlCreation = { validateUrls: true };
const shortUrlCreationResult = fromPartial<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

View file

@ -15,8 +15,8 @@ describe('<EditShortUrl />', () => {
selectedServer={null} selectedServer={null}
shortUrlDetail={fromPartial(detail)} shortUrlDetail={fromPartial(detail)}
shortUrlEdition={fromPartial(edition)} shortUrlEdition={fromPartial(edition)}
getShortUrlDetail={jest.fn()} getShortUrlDetail={vi.fn()}
editShortUrl={jest.fn(async () => Promise.resolve())} editShortUrl={vi.fn(async () => Promise.resolve())}
/> />
</MemoryRouter>, </MemoryRouter>,
); );

View file

@ -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) => {

View file

@ -8,17 +8,17 @@ 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 });
@ -36,8 +36,6 @@ describe('<ShortUrlsFilteringBar />', () => {
); );
}; };
afterEach(jest.clearAllMocks);
it('renders expected children components', () => { it('renders expected children components', () => {
setUp(); setUp();

View file

@ -10,17 +10,17 @@ 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 = fromPartial<ShortUrlsListModel>({ const shortUrlsList = fromPartial<ShortUrlsListModel>({
shortUrls: { shortUrls: {
data: [ data: [
@ -51,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();

View file

@ -9,14 +9,12 @@ import { renderWithEvents } from '../__helpers__/setUpTest';
describe('<ShortUrlsTable />', () => { describe('<ShortUrlsTable />', () => {
const shortUrlsList = fromPartial<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();

View file

@ -6,15 +6,13 @@ 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();

View file

@ -14,8 +14,8 @@ describe('<DeleteShortUrlModal />', () => {
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) => (
@ -25,14 +25,12 @@ describe('<DeleteShortUrlModal />', () => {
shortUrlDeletion={fromPartial(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,

View file

@ -8,9 +8,9 @@ import { ExportShortUrlsBtn as createExportShortUrlsBtn } from '../../../src/sho
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 = fromPartial<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(
@ -19,8 +19,6 @@ describe('<ExportShortUrlsBtn />', () => {
</MemoryRouter>, </MemoryRouter>,
); );
afterEach(jest.clearAllMocks);
it.each([ it.each([
[undefined, '0'], [undefined, '0'],
[1, '1'], [1, '1'],

View file

@ -5,7 +5,7 @@ import type { SemVer } from '../../../src/utils/helpers/version';
import { renderWithEvents } from '../../__helpers__/setUpTest'; import { renderWithEvents } from '../../__helpers__/setUpTest';
describe('<QrCodeModal />', () => { describe('<QrCodeModal />', () => {
const saveImage = jest.fn().mockReturnValue(Promise.resolve()); const saveImage = vi.fn().mockReturnValue(Promise.resolve());
const QrCodeModal = createQrCodeModal(fromPartial({ saveImage })); const QrCodeModal = createQrCodeModal(fromPartial({ saveImage }));
const shortUrl = 'https://s.test/abc123'; const shortUrl = 'https://s.test/abc123';
const setUp = (version: SemVer = '2.8.0') => renderWithEvents( const setUp = (version: SemVer = '2.8.0') => renderWithEvents(
@ -17,8 +17,6 @@ describe('<QrCodeModal />', () => {
/>, />,
); );
afterEach(jest.clearAllMocks);
it('shows an external link to the URL in the header', () => { it('shows an external link to the URL in the header', () => {
setUp(); setUp();
const externalLink = screen.getByRole('heading').querySelector('a'); const externalLink = screen.getByRole('heading').querySelector('a');

View file

@ -4,7 +4,7 @@ import { renderWithEvents } from '../../__helpers__/setUpTest';
describe('<ShortUrlsFilterDropdown />', () => { describe('<ShortUrlsFilterDropdown />', () => {
const setUp = (supportsDisabledFiltering: boolean) => renderWithEvents( const setUp = (supportsDisabledFiltering: boolean) => renderWithEvents(
<ShortUrlsFilterDropdown onChange={jest.fn()} supportsDisabledFiltering={supportsDisabledFiltering} />, <ShortUrlsFilterDropdown onChange={vi.fn()} supportsDisabledFiltering={supportsDisabledFiltering} />,
); );
it.each([ it.each([

View file

@ -20,14 +20,14 @@ interface SetUpOptions {
settings?: Partial<Settings>; settings?: Partial<Settings>;
} }
jest.mock('react-router-dom', () => ({ vi.mock('react-router-dom', async () => ({
...jest.requireActual('react-router-dom'), ...(await vi.importActual<any>('react-router-dom')),
useLocation: jest.fn().mockReturnValue({}), useLocation: vi.fn().mockReturnValue({}),
})); }));
describe('<ShortUrlsRow />', () => { describe('<ShortUrlsRow />', () => {
const timeoutToggle = jest.fn(() => true); const timeoutToggle = vi.fn(() => true);
const useTimeoutToggle = jest.fn(() => [false, timeoutToggle]) as TimeoutToggle; const useTimeoutToggle = vi.fn(() => [false, timeoutToggle]) as TimeoutToggle;
const server = fromPartial<ReachableServer>({ url: 'https://s.test' }); const server = fromPartial<ReachableServer>({ url: 'https://s.test' });
const shortUrl: ShortUrl = { const shortUrl: ShortUrl = {
shortCode: 'abc123', shortCode: 'abc123',

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<ShortUrlsRow /> displays expected status icon 1`] = ` exports[`<ShortUrlsRow /> > displays expected status icon 1`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="svg-inline--fa fa-calendar-xmark text-danger" class="svg-inline--fa fa-calendar-xmark text-danger"
@ -18,7 +18,7 @@ exports[`<ShortUrlsRow /> displays expected status icon 1`] = `
</svg> </svg>
`; `;
exports[`<ShortUrlsRow /> displays expected status icon 2`] = ` exports[`<ShortUrlsRow /> > displays expected status icon 2`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="svg-inline--fa fa-calendar-xmark text-warning" class="svg-inline--fa fa-calendar-xmark text-warning"
@ -36,7 +36,7 @@ exports[`<ShortUrlsRow /> displays expected status icon 2`] = `
</svg> </svg>
`; `;
exports[`<ShortUrlsRow /> displays expected status icon 3`] = ` exports[`<ShortUrlsRow /> > displays expected status icon 3`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="svg-inline--fa fa-link-slash text-danger" class="svg-inline--fa fa-link-slash text-danger"
@ -54,7 +54,7 @@ exports[`<ShortUrlsRow /> displays expected status icon 3`] = `
</svg> </svg>
`; `;
exports[`<ShortUrlsRow /> displays expected status icon 4`] = ` exports[`<ShortUrlsRow /> > displays expected status icon 4`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="svg-inline--fa fa-link-slash text-danger" class="svg-inline--fa fa-link-slash text-danger"
@ -72,7 +72,7 @@ exports[`<ShortUrlsRow /> displays expected status icon 4`] = `
</svg> </svg>
`; `;
exports[`<ShortUrlsRow /> displays expected status icon 5`] = ` exports[`<ShortUrlsRow /> > displays expected status icon 5`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="svg-inline--fa fa-calendar-xmark text-danger" class="svg-inline--fa fa-calendar-xmark text-danger"
@ -90,7 +90,7 @@ exports[`<ShortUrlsRow /> displays expected status icon 5`] = `
</svg> </svg>
`; `;
exports[`<ShortUrlsRow /> displays expected status icon 6`] = ` exports[`<ShortUrlsRow /> > displays expected status icon 6`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="svg-inline--fa fa-check text-primary" class="svg-inline--fa fa-check text-primary"
@ -108,7 +108,7 @@ exports[`<ShortUrlsRow /> displays expected status icon 6`] = `
</svg> </svg>
`; `;
exports[`<ShortUrlsRow /> displays expected status icon 7`] = ` exports[`<ShortUrlsRow /> > displays expected status icon 7`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="svg-inline--fa fa-check text-primary" class="svg-inline--fa fa-check text-primary"
@ -126,7 +126,7 @@ exports[`<ShortUrlsRow /> displays expected status icon 7`] = `
</svg> </svg>
`; `;
exports[`<ShortUrlsRow /> displays expected status icon 8`] = ` exports[`<ShortUrlsRow /> > displays expected status icon 8`] = `
<svg <svg
aria-hidden="true" aria-hidden="true"
class="svg-inline--fa fa-check text-primary" class="svg-inline--fa fa-check text-primary"

View file

@ -5,13 +5,11 @@ import { renderWithEvents } from '../../../__helpers__/setUpTest';
describe('<QrErrorCorrectionDropdown />', () => { describe('<QrErrorCorrectionDropdown />', () => {
const initialErrorCorrection: QrErrorCorrection = 'Q'; const initialErrorCorrection: QrErrorCorrection = 'Q';
const setErrorCorrection = jest.fn(); const setErrorCorrection = vi.fn();
const setUp = () => renderWithEvents( const setUp = () => renderWithEvents(
<QrErrorCorrectionDropdown errorCorrection={initialErrorCorrection} setErrorCorrection={setErrorCorrection} />, <QrErrorCorrectionDropdown errorCorrection={initialErrorCorrection} setErrorCorrection={setErrorCorrection} />,
); );
afterEach(jest.clearAllMocks);
it('renders initial state', async () => { it('renders initial state', async () => {
const { user } = setUp(); const { user } = setUp();
const btn = screen.getByRole('button'); const btn = screen.getByRole('button');

View file

@ -5,11 +5,9 @@ import { renderWithEvents } from '../../../__helpers__/setUpTest';
describe('<QrFormatDropdown />', () => { describe('<QrFormatDropdown />', () => {
const initialFormat: QrCodeFormat = 'svg'; const initialFormat: QrCodeFormat = 'svg';
const setFormat = jest.fn(); const setFormat = vi.fn();
const setUp = () => renderWithEvents(<QrFormatDropdown format={initialFormat} setFormat={setFormat} />); const setUp = () => renderWithEvents(<QrFormatDropdown format={initialFormat} setFormat={setFormat} />);
afterEach(jest.clearAllMocks);
it('renders initial state', async () => { it('renders initial state', async () => {
const { user } = setUp(); const { user } = setUp();
const btn = screen.getByRole('button'); const btn = screen.getByRole('button');

View file

@ -9,13 +9,11 @@ import {
describe('shortUrlCreationReducer', () => { describe('shortUrlCreationReducer', () => {
const shortUrl = fromPartial<ShortUrl>({}); const shortUrl = fromPartial<ShortUrl>({});
const createShortUrlCall = jest.fn(); const createShortUrlCall = vi.fn();
const buildShlinkApiClient = () => fromPartial<ShlinkApiClient>({ createShortUrl: createShortUrlCall }); const buildShlinkApiClient = () => fromPartial<ShlinkApiClient>({ createShortUrl: createShortUrlCall });
const createShortUrl = createShortUrlCreator(buildShlinkApiClient); const createShortUrl = createShortUrlCreator(buildShlinkApiClient);
const { reducer, resetCreateShortUrl } = shortUrlCreationReducerCreator(createShortUrl); const { reducer, resetCreateShortUrl } = shortUrlCreationReducerCreator(createShortUrl);
afterEach(jest.resetAllMocks);
describe('reducer', () => { describe('reducer', () => {
it('returns loading on CREATE_SHORT_URL_START', () => { it('returns loading on CREATE_SHORT_URL_START', () => {
expect(reducer(undefined, createShortUrl.pending('', fromPartial({})))).toEqual({ expect(reducer(undefined, createShortUrl.pending('', fromPartial({})))).toEqual({
@ -52,7 +50,7 @@ describe('shortUrlCreationReducer', () => {
}); });
describe('createShortUrl', () => { describe('createShortUrl', () => {
const dispatch = jest.fn(); const dispatch = vi.fn();
const getState = () => fromPartial<ShlinkState>({}); const getState = () => fromPartial<ShlinkState>({});
it('calls API on success', async () => { it('calls API on success', async () => {

View file

@ -7,13 +7,11 @@ import {
} from '../../../src/short-urls/reducers/shortUrlDeletion'; } from '../../../src/short-urls/reducers/shortUrlDeletion';
describe('shortUrlDeletionReducer', () => { describe('shortUrlDeletionReducer', () => {
const deleteShortUrlCall = jest.fn(); const deleteShortUrlCall = vi.fn();
const buildShlinkApiClient = () => fromPartial<ShlinkApiClient>({ deleteShortUrl: deleteShortUrlCall }); const buildShlinkApiClient = () => fromPartial<ShlinkApiClient>({ deleteShortUrl: deleteShortUrlCall });
const deleteShortUrl = deleteShortUrlCreator(buildShlinkApiClient); const deleteShortUrl = deleteShortUrlCreator(buildShlinkApiClient);
const { reducer, resetDeleteShortUrl } = shortUrlDeletionReducerCreator(deleteShortUrl); const { reducer, resetDeleteShortUrl } = shortUrlDeletionReducerCreator(deleteShortUrl);
beforeEach(jest.clearAllMocks);
describe('reducer', () => { describe('reducer', () => {
it('returns loading on DELETE_SHORT_URL_START', () => it('returns loading on DELETE_SHORT_URL_START', () =>
expect(reducer(undefined, deleteShortUrl.pending('', { shortCode: '' }))).toEqual({ expect(reducer(undefined, deleteShortUrl.pending('', { shortCode: '' }))).toEqual({
@ -56,8 +54,8 @@ describe('shortUrlDeletionReducer', () => {
}); });
describe('deleteShortUrl', () => { describe('deleteShortUrl', () => {
const dispatch = jest.fn(); const dispatch = vi.fn();
const getState = jest.fn().mockReturnValue({ selectedServer: {} }); const getState = vi.fn().mockReturnValue({ selectedServer: {} });
it.each( it.each(
[[undefined], [null], ['example.com']], [[undefined], [null], ['example.com']],

View file

@ -6,12 +6,10 @@ import { shortUrlDetailReducerCreator } from '../../../src/short-urls/reducers/s
import type { ShortUrlsList } from '../../../src/short-urls/reducers/shortUrlsList'; import type { ShortUrlsList } from '../../../src/short-urls/reducers/shortUrlsList';
describe('shortUrlDetailReducer', () => { describe('shortUrlDetailReducer', () => {
const getShortUrlCall = jest.fn(); const getShortUrlCall = vi.fn();
const buildShlinkApiClient = () => fromPartial<ShlinkApiClient>({ getShortUrl: getShortUrlCall }); const buildShlinkApiClient = () => fromPartial<ShlinkApiClient>({ getShortUrl: getShortUrlCall });
const { reducer, getShortUrlDetail } = shortUrlDetailReducerCreator(buildShlinkApiClient); const { reducer, getShortUrlDetail } = shortUrlDetailReducerCreator(buildShlinkApiClient);
beforeEach(jest.clearAllMocks);
describe('reducer', () => { describe('reducer', () => {
it('returns loading on GET_SHORT_URL_DETAIL_START', () => { it('returns loading on GET_SHORT_URL_DETAIL_START', () => {
const { loading } = reducer({ loading: false, error: false }, getShortUrlDetail.pending('', { shortCode: '' })); const { loading } = reducer({ loading: false, error: false }, getShortUrlDetail.pending('', { shortCode: '' }));
@ -41,7 +39,7 @@ describe('shortUrlDetailReducer', () => {
}); });
describe('getShortUrlDetail', () => { describe('getShortUrlDetail', () => {
const dispatchMock = jest.fn(); const dispatchMock = vi.fn();
const buildGetState = (shortUrlsList?: ShortUrlsList) => () => fromPartial<ShlinkState>({ shortUrlsList }); const buildGetState = (shortUrlsList?: ShortUrlsList) => () => fromPartial<ShlinkState>({ shortUrlsList });
it.each([ it.each([

View file

@ -11,13 +11,11 @@ describe('shortUrlEditionReducer', () => {
const longUrl = 'https://shlink.io'; const longUrl = 'https://shlink.io';
const shortCode = 'abc123'; const shortCode = 'abc123';
const shortUrl = fromPartial<ShortUrl>({ longUrl, shortCode }); const shortUrl = fromPartial<ShortUrl>({ longUrl, shortCode });
const updateShortUrl = jest.fn().mockResolvedValue(shortUrl); const updateShortUrl = vi.fn().mockResolvedValue(shortUrl);
const buildShlinkApiClient = jest.fn().mockReturnValue({ updateShortUrl }); const buildShlinkApiClient = vi.fn().mockReturnValue({ updateShortUrl });
const editShortUrl = editShortUrlCreator(buildShlinkApiClient); const editShortUrl = editShortUrlCreator(buildShlinkApiClient);
const { reducer } = shortUrlEditionReducerCreator(editShortUrl); const { reducer } = shortUrlEditionReducerCreator(editShortUrl);
afterEach(jest.clearAllMocks);
describe('reducer', () => { describe('reducer', () => {
it('returns loading on EDIT_SHORT_URL_START', () => { it('returns loading on EDIT_SHORT_URL_START', () => {
expect(reducer(undefined, editShortUrl.pending('', fromPartial({})))).toEqual({ expect(reducer(undefined, editShortUrl.pending('', fromPartial({})))).toEqual({
@ -46,13 +44,11 @@ describe('shortUrlEditionReducer', () => {
}); });
describe('editShortUrl', () => { describe('editShortUrl', () => {
const dispatch = jest.fn(); const dispatch = vi.fn();
const createGetState = (selectedServer: SelectedServer = null) => () => fromPartial<ShlinkState>({ const createGetState = (selectedServer: SelectedServer = null) => () => fromPartial<ShlinkState>({
selectedServer, selectedServer,
}); });
afterEach(jest.clearAllMocks);
it.each([[undefined], [null], ['example.com']])('dispatches short URL on success', async (domain) => { it.each([[undefined], [null], ['example.com']])('dispatches short URL on success', async (domain) => {
await editShortUrl({ shortCode, domain, data: { longUrl } })(dispatch, createGetState(), {}); await editShortUrl({ shortCode, domain, data: { longUrl } })(dispatch, createGetState(), {});

View file

@ -14,15 +14,13 @@ import type { CreateVisit } from '../../../src/visits/types';
describe('shortUrlsListReducer', () => { describe('shortUrlsListReducer', () => {
const shortCode = 'abc123'; const shortCode = 'abc123';
const listShortUrlsMock = jest.fn(); const listShortUrlsMock = vi.fn();
const buildShlinkApiClient = () => fromPartial<ShlinkApiClient>({ listShortUrls: listShortUrlsMock }); const buildShlinkApiClient = () => fromPartial<ShlinkApiClient>({ listShortUrls: listShortUrlsMock });
const listShortUrls = listShortUrlsCreator(buildShlinkApiClient); const listShortUrls = listShortUrlsCreator(buildShlinkApiClient);
const editShortUrl = editShortUrlCreator(buildShlinkApiClient); const editShortUrl = editShortUrlCreator(buildShlinkApiClient);
const createShortUrl = createShortUrlCreator(buildShlinkApiClient); const createShortUrl = createShortUrlCreator(buildShlinkApiClient);
const { reducer } = shortUrlsListReducerCreator(listShortUrls, editShortUrl, createShortUrl); const { reducer } = shortUrlsListReducerCreator(listShortUrls, editShortUrl, createShortUrl);
afterEach(jest.clearAllMocks);
describe('reducer', () => { describe('reducer', () => {
it('returns loading on LIST_SHORT_URLS_START', () => it('returns loading on LIST_SHORT_URLS_START', () =>
expect(reducer(undefined, listShortUrls.pending(''))).toEqual({ expect(reducer(undefined, listShortUrls.pending(''))).toEqual({
@ -188,8 +186,8 @@ describe('shortUrlsListReducer', () => {
}); });
describe('listShortUrls', () => { describe('listShortUrls', () => {
const dispatch = jest.fn(); const dispatch = vi.fn();
const getState = jest.fn().mockReturnValue({ selectedServer: {} }); const getState = vi.fn().mockReturnValue({ selectedServer: {} });
it('dispatches proper actions if API client request succeeds', async () => { it('dispatches proper actions if API client request succeeds', async () => {
listShortUrlsMock.mockResolvedValue({}); listShortUrlsMock.mockResolvedValue({});

View file

@ -8,7 +8,7 @@ import { TagsList as createTagsList } from '../../src/tags/TagsList';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
describe('<TagsList />', () => { describe('<TagsList />', () => {
const filterTags = jest.fn(); const filterTags = vi.fn();
const TagsListComp = createTagsList(({ sortedTags }) => <>TagsTable ({sortedTags.map((t) => t.visits).join(',')})</>); const TagsListComp = createTagsList(({ sortedTags }) => <>TagsTable ({sortedTags.map((t) => t.visits).join(',')})</>);
const setUp = (tagsList: Partial<TagsList>, excludeBots = false) => renderWithEvents( const setUp = (tagsList: Partial<TagsList>, excludeBots = false) => renderWithEvents(
<TagsListComp <TagsListComp
@ -21,8 +21,6 @@ describe('<TagsList />', () => {
/>, />,
); );
afterEach(jest.clearAllMocks);
it('shows a loading message when tags are being loaded', () => { it('shows a loading message when tags are being loaded', () => {
setUp({ loading: true }); setUp({ loading: true });

View file

@ -5,10 +5,13 @@ import { TagsTable as createTagsTable } from '../../src/tags/TagsTable';
import { rangeOf } from '../../src/utils/utils'; import { rangeOf } from '../../src/utils/utils';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useLocation: jest.fn() })); vi.mock('react-router-dom', async () => ({
...(await vi.importActual<any>('react-router-dom')),
useLocation: vi.fn(),
}));
describe('<TagsTable />', () => { describe('<TagsTable />', () => {
const orderByColumn = jest.fn(); const orderByColumn = vi.fn();
const TagsTable = createTagsTable(({ tag }) => <tr><td>TagsTableRow [{tag.tag}]</td></tr>); const TagsTable = createTagsTable(({ tag }) => <tr><td>TagsTableRow [{tag.tag}]</td></tr>);
const tags = (amount: number) => rangeOf(amount, (i) => `tag_${i}`); const tags = (amount: number) => rangeOf(amount, (i) => `tag_${i}`);
const setUp = (sortedTags: string[] = [], search = '') => { const setUp = (sortedTags: string[] = [], search = '') => {
@ -23,8 +26,6 @@ describe('<TagsTable />', () => {
); );
}; };
afterEach(jest.clearAllMocks);
it('renders empty result if there are no tags', () => { it('renders empty result if there are no tags', () => {
setUp(); setUp();

View file

@ -5,21 +5,19 @@ import { renderWithEvents } from '../../__helpers__/setUpTest';
describe('<DeleteTagConfirmModal />', () => { describe('<DeleteTagConfirmModal />', () => {
const tag = 'nodejs'; const tag = 'nodejs';
const deleteTag = jest.fn(); const deleteTag = vi.fn();
const toggle = jest.fn(); const toggle = vi.fn();
const setUp = (tagDelete: TagDeletion) => renderWithEvents( const setUp = (tagDelete: TagDeletion) => renderWithEvents(
<DeleteTagConfirmModal <DeleteTagConfirmModal
tag={tag} tag={tag}
toggle={toggle} toggle={toggle}
isOpen isOpen
deleteTag={deleteTag} deleteTag={deleteTag}
tagDeleted={jest.fn()} tagDeleted={vi.fn()}
tagDelete={tagDelete} tagDelete={tagDelete}
/>, />,
); );
afterEach(jest.resetAllMocks);
it('asks confirmation for provided tag to be deleted', () => { it('asks confirmation for provided tag to be deleted', () => {
setUp({ error: false, deleted: false, deleting: false }); setUp({ error: false, deleted: false, deleting: false });

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