diff --git a/config/test/setupTests.ts b/config/test/setupTests.ts deleted file mode 100644 index cce5708f..00000000 --- a/config/test/setupTests.ts +++ /dev/null @@ -1,9 +0,0 @@ -import '@testing-library/jest-dom/vitest'; -import { cleanup } from '@testing-library/react'; -import { afterEach } from 'vitest'; - -// Clear all mocks and cleanup DOM after every test -afterEach(() => { - vi.clearAllMocks(); - cleanup(); -}); diff --git a/package-lock.json b/package-lock.json index 34a21dc3..71c06cb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,7 +40,6 @@ "devDependencies": { "@shlinkio/eslint-config-js-coding-standard": "~4.1.0", "@tailwindcss/vite": "^4.3.3", - "@testing-library/jest-dom": "^7.0.0", "@testing-library/react": "^16.3.2", "@total-typescript/shoehorn": "^0.1.2", "@types/node": "^26.1.2", @@ -64,12 +63,6 @@ "vitest": "^4.0.3" } }, - "node_modules/@adobe/css-tools": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz", - "integrity": "sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==", - "dev": true - }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -3690,34 +3683,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@testing-library/jest-dom": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.0.tgz", - "integrity": "sha512-HKAH9C6mBo5yBG6yRO5i43L2iisencAo5z+o5P/saHUoY+miC5ivXRxHBJcFyB5ypPNxHJdK3BoF/3O4DIptMg==", - "dev": true, - "dependencies": { - "@adobe/css-tools": "^4.4.0", - "aria-query": "^5.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.6.3", - "picocolors": "^1.1.1", - "redent": "^3.0.0" - }, - "engines": { - "node": ">=22", - "npm": ">=6", - "yarn": ">=1" - }, - "peerDependencies": { - "@testing-library/dom": ">=10 <11" - } - }, - "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", - "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", - "dev": true - }, "node_modules/@testing-library/react": { "version": "16.3.2", "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", @@ -4476,6 +4441,7 @@ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, + "peer": true, "dependencies": { "dequal": "^2.0.3" } @@ -4896,11 +4862,6 @@ "node": ">=8" } }, - "node_modules/css.escape": { - "version": "1.5.1", - "dev": true, - "license": "MIT" - }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -5173,6 +5134,7 @@ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "dev": true, + "peer": true, "engines": { "node": ">=6" } @@ -5871,14 +5833,6 @@ "url": "https://opencollective.com/immer" } }, - "node_modules/indent-string": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -6756,14 +6710,6 @@ "version": "2.1.1", "license": "MIT" }, - "node_modules/min-indent": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", @@ -7325,18 +7271,6 @@ "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/redent": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", @@ -7977,17 +7911,6 @@ "node": ">=10" } }, - "node_modules/strip-indent": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -9268,12 +9191,6 @@ } }, "dependencies": { - "@adobe/css-tools": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz", - "integrity": "sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==", - "dev": true - }, "@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -11435,28 +11352,6 @@ } } }, - "@testing-library/jest-dom": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.0.tgz", - "integrity": "sha512-HKAH9C6mBo5yBG6yRO5i43L2iisencAo5z+o5P/saHUoY+miC5ivXRxHBJcFyB5ypPNxHJdK3BoF/3O4DIptMg==", - "dev": true, - "requires": { - "@adobe/css-tools": "^4.4.0", - "aria-query": "^5.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.6.3", - "picocolors": "^1.1.1", - "redent": "^3.0.0" - }, - "dependencies": { - "dom-accessibility-api": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", - "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", - "dev": true - } - } - }, "@testing-library/react": { "version": "16.3.2", "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", @@ -11906,6 +11801,7 @@ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, + "peer": true, "requires": { "dequal": "^2.0.3" } @@ -12197,10 +12093,6 @@ "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", "dev": true }, - "css.escape": { - "version": "1.5.1", - "dev": true - }, "csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -12376,7 +12268,8 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true + "dev": true, + "peer": true }, "detect-libc": { "version": "2.1.2", @@ -12868,10 +12761,6 @@ "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.16.tgz", "integrity": "sha512-Xs7H9rBc+kti1J6RueUvbEBkmOz7jqj11XYgf+YMXAYzu8EeE7hwZ9poLXdVfVnGmJu7QAf41T7H2KuF6QoK6Q==" }, - "indent-string": { - "version": "4.0.0", - "dev": true - }, "internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -13380,10 +13269,6 @@ "merge": { "version": "2.1.1" }, - "min-indent": { - "version": "1.0.1", - "dev": true - }, "minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", @@ -13718,14 +13603,6 @@ "victory-vendor": "^37.0.2" } }, - "redent": { - "version": "3.0.0", - "dev": true, - "requires": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - } - }, "redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", @@ -14188,13 +14065,6 @@ "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", "dev": true }, - "strip-indent": { - "version": "3.0.0", - "dev": true, - "requires": { - "min-indent": "^1.0.0" - } - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", diff --git a/package.json b/package.json index 84cc07c4..d2a32829 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,6 @@ "devDependencies": { "@shlinkio/eslint-config-js-coding-standard": "~4.1.0", "@tailwindcss/vite": "^4.3.3", - "@testing-library/jest-dom": "^7.0.0", "@testing-library/react": "^16.3.2", "@total-typescript/shoehorn": "^0.1.2", "@types/node": "^26.1.2", diff --git a/src/servers/helpers/ServerForm.tsx b/src/servers/helpers/ServerForm.tsx index a615aede..019f3ea7 100644 --- a/src/servers/helpers/ServerForm.tsx +++ b/src/servers/helpers/ServerForm.tsx @@ -28,7 +28,7 @@ export const ServerForm: FC = ({ onSubmit, initialValues, child const handleSubmit = usePreventDefault(() => onSubmit({ name, url, apiKey, forwardCredentials })); return ( -
+ setName(e.target.value)} required /> setUrl(e.target.value)} required /> diff --git a/test/app/App.test.tsx b/test/app/App.test.tsx index b768994f..e75a2858 100644 --- a/test/app/App.test.tsx +++ b/test/app/App.test.tsx @@ -1,10 +1,10 @@ import type { HttpClient } from '@shlinkio/shlink-js-sdk'; -import { act, screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; +import { act } from 'react'; import { MemoryRouter } from 'react-router'; +import { page } from 'vitest/browser'; import { App } from '../../src/app/App'; import { ContainerProvider } from '../../src/container/context'; -import type { ServerWithId } from '../../src/servers/data'; import { checkAccessibility } from '../__helpers__/accessibility'; import { renderWithStore } from '../__helpers__/setUpTest'; @@ -30,8 +30,8 @@ describe('', () => { { initialState: { servers: { - abc123: fromPartial({ id: 'abc123', name: 'abc123 server' }), - def456: fromPartial({ id: 'def456', name: 'def456 server' }), + abc123: fromPartial({ id: 'abc123', name: 'abc123 server' }), + def456: fromPartial({ id: 'def456', name: 'def456 server' }), }, settings: fromPartial({}), appUpdated: false, @@ -54,7 +54,7 @@ describe('', () => { ['/other', 'Oops! We could not find requested route.'], ])('renders expected route', async (activeRoute, expectedComponent) => { await setUp(activeRoute); - expect(screen.getByText(expectedComponent)).toBeInTheDocument(); + await expect.element(page.getByText(expectedComponent)).toBeInTheDocument(); }); it.each([ @@ -63,12 +63,12 @@ describe('', () => { ['/', true], ])('renders expected classes on shlink-wrapper based on current pathname', async (pathname, isFlex) => { await setUp(pathname); - const shlinkWrapper = screen.getByTestId('shlink-wrapper'); + const shlinkWrapper = page.getByTestId('shlink-wrapper'); if (isFlex) { - expect(shlinkWrapper).toHaveClass('flex'); + await expect.element(shlinkWrapper).toHaveClass('flex'); } else { - expect(shlinkWrapper).not.toHaveClass('flex'); + await expect.element(shlinkWrapper).not.toHaveClass('flex'); } }); }); diff --git a/test/common/AppUpdateBanner.test.tsx b/test/common/AppUpdateBanner.test.tsx index 24af528d..a2e87c9c 100644 --- a/test/common/AppUpdateBanner.test.tsx +++ b/test/common/AppUpdateBanner.test.tsx @@ -1,4 +1,4 @@ -import { act, screen, waitFor } from '@testing-library/react'; +import { page } from 'vitest/browser'; import { AppUpdateBanner } from '../../src/common/AppUpdateBanner'; import { checkAccessibility } from '../__helpers__/accessibility'; import { renderWithEvents } from '../__helpers__/setUpTest'; @@ -6,40 +6,33 @@ import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { const onClose = vi.fn(); const forceUpdate = vi.fn(); - const setUp = async () => { - const result = await act(() => - renderWithEvents(), - ); - await waitFor(() => screen.getByRole('alert')); - - return result; - }; + const setUp = () => renderWithEvents(); it('passes a11y checks', () => checkAccessibility(setUp())); it('renders initial state', async () => { - await setUp(); + setUp(); - expect(screen.getByRole('heading')).toHaveTextContent('This app has just been updated!'); - expect(screen.queryByText('Restarting...')).not.toBeInTheDocument(); - expect(screen.getByText('Restart now')).not.toHaveAttribute('disabled'); + await expect.element(page.getByRole('heading')).toHaveTextContent('This app has just been updated!'); + await expect.element(page.getByText('Restarting...')).not.toBeInTheDocument(); + await expect.element(page.getByText('Restart now')).not.toHaveAttribute('disabled'); }); it('invokes toggle when alert is closed', async () => { - const { user } = await setUp(); + const { user } = setUp(); expect(onClose).not.toHaveBeenCalled(); - await user.click(screen.getByLabelText('Close')); + await user.click(page.getByLabelText('Close')); expect(onClose).toHaveBeenCalled(); }); it('triggers the update when clicking the button', async () => { - const { user } = await setUp(); + const { user } = setUp(); expect(forceUpdate).not.toHaveBeenCalled(); - await user.click(screen.getByText(/^Restart now/)); + await user.click(page.getByText(/^Restart now/)); expect(forceUpdate).toHaveBeenCalled(); - expect(await screen.findByText('Restarting...')).toBeInTheDocument(); - expect(screen.queryByText(/^Restart now/)).not.toBeInTheDocument(); + await expect.element(page.getByText('Restarting...')).toBeInTheDocument(); + await expect.element(page.getByText(/^Restart now/)).not.toBeInTheDocument(); }); }); diff --git a/test/common/ErrorHandler.test.tsx b/test/common/ErrorHandler.test.tsx index 2bbd85e2..1218c0e1 100644 --- a/test/common/ErrorHandler.test.tsx +++ b/test/common/ErrorHandler.test.tsx @@ -1,7 +1,7 @@ -import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; -import type { PropsWithChildren, ReactNode } from 'react'; -import { ErrorHandler as BaseErrorHandler } from '../../src/common/ErrorHandler'; +import type { ReactNode } from 'react'; +import { page } from 'vitest/browser'; +import { ErrorHandler } from '../../src/common/ErrorHandler'; import { checkAccessibility } from '../__helpers__/accessibility'; import { renderWithEvents } from '../__helpers__/setUpTest'; @@ -13,8 +13,12 @@ describe('', () => { const reload = vi.fn(); const location = fromPartial({ reload }); const cons = fromPartial({ error: vi.fn() }); - const ErrorHandler = (props: PropsWithChildren) => ; - const setUp = (children: ReactNode = 'Error') => renderWithEvents({children}); + const setUp = (children: ReactNode = 'Error') => + renderWithEvents( + + {children} + , + ); beforeEach(() => { vi.spyOn(console, 'error').mockImplementation(() => {}); // Silence react errors @@ -22,26 +26,26 @@ describe('', () => { it('passes a11y checks', () => checkAccessibility(setUp())); - it('renders children when no error has occurred', () => { + it('renders children when no error has occurred', async () => { setUp(Foo); - expect(screen.getByText('Foo')).toBeInTheDocument(); - expect(screen.queryByText('Oops! This is awkward :S')).not.toBeInTheDocument(); - expect(screen.queryByRole('button')).not.toBeInTheDocument(); + await expect.element(page.getByText('Foo')).toBeInTheDocument(); + await expect.element(page.getByText('Oops! This is awkward :S')).not.toBeInTheDocument(); + await expect.element(page.getByRole('button')).not.toBeInTheDocument(); }); - it('renders error page when error has occurred', () => { + it('renders error page when error has occurred', async () => { setUp(); - expect(screen.getByText('Oops! This is awkward :S')).toBeInTheDocument(); - expect(screen.getByRole('button')).toBeInTheDocument(); + await expect.element(page.getByText('Oops! This is awkward :S')).toBeInTheDocument(); + await expect.element(page.getByRole('button')).toBeInTheDocument(); }); it('reloads page on button click', async () => { const { user } = setUp(); expect(reload).not.toHaveBeenCalled(); - await user.click(screen.getByRole('button')); + await user.click(page.getByRole('button')); expect(reload).toHaveBeenCalled(); }); }); diff --git a/test/common/Home.test.tsx b/test/common/Home.test.tsx index 5a2d6579..30fd5b76 100644 --- a/test/common/Home.test.tsx +++ b/test/common/Home.test.tsx @@ -1,6 +1,6 @@ -import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { MemoryRouter } from 'react-router'; +import { page } from 'vitest/browser'; import { Home } from '../../src/common/Home'; import type { ServersMap, ServerWithId } from '../../src/servers/data'; import { checkAccessibility } from '../__helpers__/accessibility'; @@ -20,9 +20,9 @@ describe('', () => { it('passes a11y checks', () => checkAccessibility(setUp({ '1a': fromPartial({ name: 'foo', id: '1' }) }))); - it('renders title', () => { + it('renders title', async () => { setUp(); - expect(screen.getByRole('heading', { name: 'Welcome!' })).toBeInTheDocument(); + await expect.element(page.getByRole('heading', { name: 'Welcome!' })).toBeInTheDocument(); }); it.each([ @@ -35,15 +35,17 @@ describe('', () => { 3, ], [{}, 2], - ])('shows link to create or set-up server only when no servers exist', (servers, expectedServers) => { + ])('shows link to create or set-up server only when no servers exist', async (servers, expectedServers) => { setUp(servers); - const links = screen.getAllByRole('link'); + const links = page.getByRole('link').elements(); expect(links).toHaveLength(expectedServers); if (Object.keys(servers).length === 0) { - expect(screen.getByText('This application will help you manage your Shlink servers.')).toBeInTheDocument(); - expect(screen.getByText('Learn more about Shlink')).toBeInTheDocument(); + await expect + .element(page.getByText('This application will help you manage your Shlink servers.')) + .toBeInTheDocument(); + await expect.element(page.getByText('Learn more about Shlink')).toBeInTheDocument(); } }); }); diff --git a/test/common/MainHeader.test.tsx b/test/common/MainHeader.test.tsx index 5517d85f..06567e2d 100644 --- a/test/common/MainHeader.test.tsx +++ b/test/common/MainHeader.test.tsx @@ -1,6 +1,6 @@ -import { screen } from '@testing-library/react'; import { createMemoryHistory } from 'history'; import { Router } from 'react-router'; +import { page } from 'vitest/browser'; import { MainHeader } from '../../src/common/MainHeader'; import { checkAccessibility } from '../__helpers__/accessibility'; import { renderWithStore } from '../__helpers__/setUpTest'; @@ -19,9 +19,9 @@ describe('', () => { it('passes a11y checks', () => checkAccessibility(setUp())); - it('renders ServersDropdown', () => { + it('renders ServersDropdown', async () => { setUp(); - expect(screen.getByRole('button', { name: 'Servers' })).toBeInTheDocument(); + await expect.element(page.getByRole('button', { name: 'Servers' })).toBeInTheDocument(); }); it.each([ @@ -30,11 +30,10 @@ describe('', () => { ['/settings', true], ['/settings/foo', true], ['/settings/bar', true], - ])('sets link to settings as active only when current path is settings', (currentPath, isActive) => { + ])('sets link to settings as active only when current path is settings', async (currentPath, isActive) => { setUp(currentPath); - expect(screen.getByRole('menuitem', { name: /Settings$/ })).toHaveAttribute( - 'data-active', - isActive ? 'true' : 'false', - ); + await expect + .element(page.getByRole('menuitem', { name: /Settings$/ })) + .toHaveAttribute('data-active', isActive ? 'true' : 'false'); }); }); diff --git a/test/common/NotFound.test.tsx b/test/common/NotFound.test.tsx index 32e1fe85..2a1e6a19 100644 --- a/test/common/NotFound.test.tsx +++ b/test/common/NotFound.test.tsx @@ -13,29 +13,31 @@ describe('', () => { it('passes a11y checks', () => checkAccessibility(setUp())); - it('shows expected error title', () => { + it('shows expected error title', async () => { setUp(); - expect(screen.getByText('Oops! We could not find requested route.')).toBeInTheDocument(); + await expect.element(screen.getByText('Oops! We could not find requested route.')).toBeInTheDocument(); }); - it('shows expected error message', () => { + it('shows expected error message', async () => { setUp(); - expect( - screen.getByText( - "Use your browser's back button to navigate to the page you have previously come from, or just press this button.", - ), - ).toBeInTheDocument(); + await expect + .element( + screen.getByText( + "Use your browser's back button to navigate to the page you have previously come from, or just press this button.", + ), + ) + .toBeInTheDocument(); }); it.each([ [{}, '/', 'Home'], [{ to: '/foo/bar', children: 'Hello' }, '/foo/bar', 'Hello'], [{ to: '/baz-bar', children: <>Foo }, '/baz-bar', 'Foo'], - ])('shows expected link and text', (props, expectedLink, expectedText) => { + ])('shows expected link and text', async (props, expectedLink, expectedText) => { setUp(props); const link = screen.getByRole('link'); - expect(link).toHaveAttribute('href', expectedLink); - expect(link).toHaveTextContent(expectedText); + await expect.element(link).toHaveAttribute('href', expectedLink); + await expect.element(link).toHaveTextContent(expectedText); }); }); diff --git a/test/common/ScrollToTop.test.tsx b/test/common/ScrollToTop.test.tsx index 5afe9416..4b2eae1f 100644 --- a/test/common/ScrollToTop.test.tsx +++ b/test/common/ScrollToTop.test.tsx @@ -13,8 +13,8 @@ describe('', () => { it('passes a11y checks', () => checkAccessibility(setUp())); - it.each([['Foobar'], ['Barfoo'], ['Something']])('just renders children', (children) => { + it.each([['Foobar'], ['Barfoo'], ['Something']])('just renders children', async (children) => { setUp(children); - expect(screen.getByText(children)).toBeInTheDocument(); + await expect.element(screen.getByText(children)).toBeInTheDocument(); }); }); diff --git a/test/common/ShlinkVersions.test.tsx b/test/common/ShlinkVersions.test.tsx index 7cd203fb..9d7b5ec0 100644 --- a/test/common/ShlinkVersions.test.tsx +++ b/test/common/ShlinkVersions.test.tsx @@ -19,20 +19,18 @@ describe('', () => { ['not-semver', fromPartial({ version: '1.0.0', printableVersion: 'some' }), 'latest', 'some'], ])( 'displays expected versions when selected server is reachable', - (clientVersion, selectedServer, expectedClientVersion, expectedServerVersion) => { + async (clientVersion, selectedServer, expectedClientVersion, expectedServerVersion) => { setUp({ clientVersion, selectedServer }); const [serverLink, clientLink] = screen.getAllByRole('link'); - expect(serverLink).toHaveAttribute( - 'href', - `https://github.com/shlinkio/shlink/releases/${expectedServerVersion}`, - ); - expect(serverLink).toHaveTextContent(expectedServerVersion); - expect(clientLink).toHaveAttribute( - 'href', - `https://github.com/shlinkio/shlink-web-client/releases/${expectedClientVersion}`, - ); - expect(clientLink).toHaveTextContent(expectedClientVersion); + await expect + .element(serverLink) + .toHaveAttribute('href', `https://github.com/shlinkio/shlink/releases/${expectedServerVersion}`); + await expect.element(serverLink).toHaveTextContent(expectedServerVersion); + await expect + .element(clientLink) + .toHaveAttribute('href', `https://github.com/shlinkio/shlink-web-client/releases/${expectedClientVersion}`); + await expect.element(clientLink).toHaveTextContent(expectedClientVersion); }, ); @@ -40,11 +38,13 @@ describe('', () => { ['1.2.3', null], ['1.2.3', fromPartial({ serverNotFound: true })], ['1.2.3', fromPartial({ serverNotReachable: true })], - ])('displays only client version when selected server is not reachable', (clientVersion, selectedServer) => { + ])('displays only client version when selected server is not reachable', async (clientVersion, selectedServer) => { setUp({ clientVersion, selectedServer }); const links = screen.getAllByRole('link'); expect(links).toHaveLength(1); - expect(links[0]).toHaveAttribute('href', 'https://github.com/shlinkio/shlink-web-client/releases/v1.2.3'); + await expect + .element(links[0]) + .toHaveAttribute('href', 'https://github.com/shlinkio/shlink-web-client/releases/v1.2.3'); }); }); diff --git a/test/common/ShlinkVersionsContainer.test.tsx b/test/common/ShlinkVersionsContainer.test.tsx index dabdeeb8..6d5f0dc2 100644 --- a/test/common/ShlinkVersionsContainer.test.tsx +++ b/test/common/ShlinkVersionsContainer.test.tsx @@ -20,13 +20,13 @@ describe('', () => { [null, false], [fromPartial({}), false], [fromPartial({ version: '1.0.0' }), true], - ])('renders proper col classes based on sidebar status', (selectedServer, shouldAddMargin) => { + ])('renders proper col classes based on sidebar status', async (selectedServer, shouldAddMargin) => { const { container } = setUp(selectedServer); if (shouldAddMargin) { - expect(container.firstChild).toHaveClass('md:ml-(--aside-menu-width)'); + await expect.element(container.firstChild as HTMLElement).toHaveClass('md:ml-(--aside-menu-width)'); } else { - expect(container.firstChild).not.toHaveClass('md:ml-(--aside-menu-width)'); + await expect.element(container.firstChild as HTMLElement).not.toHaveClass('md:ml-(--aside-menu-width)'); } }); }); diff --git a/test/common/ShlinkWebComponentContainer.test.tsx b/test/common/ShlinkWebComponentContainer.test.tsx index f892f128..145d9500 100644 --- a/test/common/ShlinkWebComponentContainer.test.tsx +++ b/test/common/ShlinkWebComponentContainer.test.tsx @@ -1,6 +1,6 @@ -import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { MemoryRouter } from 'react-router'; +import { page } from 'vitest/browser'; import { ShlinkWebComponentContainer } from '../../src/common/ShlinkWebComponentContainer'; import type { NonReachableServer, NotFoundServer, SelectedServer } from '../../src/servers/data'; import { checkAccessibility } from '../__helpers__/accessibility'; @@ -25,11 +25,11 @@ describe('', () => { it('passes a11y checks', () => checkAccessibility(setUp(fromPartial({ version: '3.0.0' })))); - it('shows loading indicator while loading server', () => { + it('shows loading indicator while loading server', async () => { setUp(null); - expect(screen.getByText('Loading...')).toBeInTheDocument(); - expect(screen.queryByText('ShlinkWebComponent')).not.toBeInTheDocument(); + await expect.element(page.getByText('Loading...')).toBeInTheDocument(); + await expect.element(page.getByText('ShlinkWebComponent')).not.toBeInTheDocument(); }); it.each([ @@ -38,18 +38,18 @@ describe('', () => { fromPartial({ id: 'foo', serverNotReachable: true }), /Could not connect to this Shlink server/, ], - ])('shows error for non reachable servers', (selectedServer, expectedError) => { + ])('shows error for non reachable servers', async (selectedServer, expectedError) => { setUp(selectedServer); - expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); - expect(screen.getByText(expectedError)).toBeInTheDocument(); - expect(screen.queryByText('ShlinkWebComponent')).not.toBeInTheDocument(); + await expect.element(page.getByText('Loading...')).not.toBeInTheDocument(); + await expect.element(page.getByText(expectedError)).toBeInTheDocument(); + await expect.element(page.getByText('ShlinkWebComponent')).not.toBeInTheDocument(); }); - it('renders ShlinkWebComponent for reachable servers', () => { + it('renders ShlinkWebComponent for reachable servers', async () => { setUp(fromPartial({ version: '3.0.0' })); - expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); - expect(screen.getByText('ShlinkWebComponent')).toBeInTheDocument(); + await expect.element(page.getByText('Loading...')).not.toBeInTheDocument(); + await expect.element(page.getByText('ShlinkWebComponent')).toBeInTheDocument(); }); }); diff --git a/test/common/img/ShlinkLogo.test.tsx b/test/common/img/ShlinkLogo.test.tsx index 4b4fbd55..ec982bdc 100644 --- a/test/common/img/ShlinkLogo.test.tsx +++ b/test/common/img/ShlinkLogo.test.tsx @@ -13,22 +13,22 @@ describe('', () => { [undefined, brandColor()], ['red', 'red'], ['white', 'white'], - ])('renders expected color', (color, expectedColor) => { + ])('renders expected color', async (color, expectedColor) => { const { container } = setUp({ color }); - expect(container.querySelector('g')).toHaveAttribute('fill', expectedColor); + await expect.element(container.querySelector('g')).toHaveAttribute('fill', expectedColor); }); it.each([ [undefined, undefined], ['foo', 'foo'], ['bar', 'bar'], - ])('renders expected class', (className, expectedClassName) => { + ])('renders expected class', async (className, expectedClassName) => { const { container } = setUp({ className }); if (expectedClassName) { - expect(container.firstChild).toHaveAttribute('class', expectedClassName); + await expect.element(container.firstChild as HTMLElement).toHaveAttribute('class', expectedClassName); } else { - expect(container.firstChild).not.toHaveAttribute('class'); + await expect.element(container.firstChild as HTMLElement).not.toHaveAttribute('class'); } }); }); diff --git a/test/container/context.test.tsx b/test/container/context.test.tsx index bdcfb4ba..655124ec 100644 --- a/test/container/context.test.tsx +++ b/test/container/context.test.tsx @@ -13,7 +13,7 @@ describe('context', () => { } it('throws when used outside of ContainerProvider', () => { - expect(() => render()).toThrowError( + expect(() => render()).toThrow( 'You cannot use "useDependencies" outside of a ContainerProvider', ); }); @@ -25,7 +25,7 @@ describe('context', () => { , ), - ).toThrowError('Dependency with name "foo" not found in container'); + ).toThrow('Dependency with name "foo" not found in container'); }); it('gets dependency from container', () => { diff --git a/test/servers/CreateServer.test.tsx b/test/servers/CreateServer.test.tsx index a1d4cb5c..ded7a93a 100644 --- a/test/servers/CreateServer.test.tsx +++ b/test/servers/CreateServer.test.tsx @@ -1,7 +1,8 @@ -import { fireEvent, screen, waitFor } from '@testing-library/react'; +import { fireEvent, waitFor } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { createMemoryHistory } from 'history'; import { Router } from 'react-router'; +import { page } from 'vitest/browser'; import { CreateServer } from '../../src/servers/CreateServer'; import type { ServersMap } from '../../src/servers/data'; import { checkAccessibility } from '../__helpers__/accessibility'; @@ -41,35 +42,39 @@ describe('', () => { it('passes a11y checks', () => checkAccessibility(setUp())); - it('shows success message when imported is true', () => { + it('shows success message when imported is true', async () => { setUp({ serversImported: true }); - expect(screen.getByText('Servers properly imported. You can now select one from the list :)')).toBeInTheDocument(); - expect( - screen.queryByText('The servers could not be imported. Make sure the format is correct.'), - ).not.toBeInTheDocument(); - expect(screen.queryByText('ImportServersBtn')).not.toBeInTheDocument(); + await expect + .element(page.getByText('Servers properly imported. You can now select one from the list :)')) + .toBeInTheDocument(); + await expect + .element(page.getByText('The servers could not be imported. Make sure the format is correct.')) + .not.toBeInTheDocument(); + await expect.element(page.getByText('ImportServersBtn')).not.toBeInTheDocument(); }); - it('shows error message when import failed', () => { + it('shows error message when import failed', async () => { setUp({ importFailed: true }); - expect( - screen.queryByText('Servers properly imported. You can now select one from the list :)'), - ).not.toBeInTheDocument(); - expect(screen.getByText('The servers could not be imported. Make sure the format is correct.')).toBeInTheDocument(); + await expect + .element(page.getByText('Servers properly imported. You can now select one from the list :)')) + .not.toBeInTheDocument(); + await expect + .element(page.getByText('The servers could not be imported. Make sure the format is correct.')) + .toBeInTheDocument(); }); it('creates server data when form is submitted', async () => { const { user, history, store } = setUp(); const expectedServerId = 'the_name-the_url.com'; - await user.type(screen.getByLabelText(/^Name/), 'the_name'); - await user.type(screen.getByLabelText(/^URL/), 'https://the_url.com'); - await user.type(screen.getByLabelText(/^API key/), 'the_api_key'); + await user.type(page.getByLabelText(/^Name/), 'the_name'); + await user.type(page.getByLabelText(/^URL/), 'https://the_url.com'); + await user.type(page.getByLabelText(/^API key/), 'the_api_key'); expect(store.getState().servers[expectedServerId]).not.toBeDefined(); - fireEvent.submit(screen.getByRole('form')); + fireEvent.submit(page.getByTestId('server-form').element()); expect(store.getState().servers[expectedServerId]).toEqual( expect.objectContaining({ id: expectedServerId, @@ -80,20 +85,20 @@ describe('', () => { ); expect(history.location.pathname).toEqual(`/server/${expectedServerId}`); - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + await expect.element(page.getByRole('dialog')).not.toBeInTheDocument(); }); it('displays dialog when trying to create a duplicated server', async () => { const { user, history } = setUp(); - await user.type(screen.getByLabelText(/^Name/), 'the_name'); - await user.type(screen.getByLabelText(/^URL/), 'https://existing_url.com'); - await user.type(screen.getByLabelText(/^API key/), 'existing_api_key'); + await user.type(page.getByLabelText(/^Name/), 'the_name'); + await user.type(page.getByLabelText(/^URL/), 'https://existing_url.com'); + await user.type(page.getByLabelText(/^API key/), 'existing_api_key'); - fireEvent.submit(screen.getByRole('form')); + fireEvent.submit(page.getByTestId('server-form').element()); - await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()); - await user.click(screen.getByRole('button', { name: 'Discard' })); + await waitFor(() => expect(page.getByRole('dialog')).toBeInTheDocument()); + await user.click(page.getByRole('button', { name: 'Discard' })); expect(history.location.pathname).toEqual('/foo'); // Goes back to first route from history's initialEntries }); diff --git a/test/servers/DeleteServerButton.test.tsx b/test/servers/DeleteServerButton.test.tsx index 689b9fa6..ad832d33 100644 --- a/test/servers/DeleteServerButton.test.tsx +++ b/test/servers/DeleteServerButton.test.tsx @@ -1,8 +1,8 @@ -import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { createMemoryHistory } from 'history'; import type { ReactNode } from 'react'; import { Router } from 'react-router'; +import { page } from 'vitest/browser'; import { DeleteServerButton } from '../../src/servers/DeleteServerButton'; import { checkAccessibility } from '../__helpers__/accessibility'; import { renderWithStore } from '../__helpers__/setUpTest'; @@ -21,8 +21,9 @@ describe('', () => { it('passes a11y checks', () => checkAccessibility(setUp('Delete me'))); - it.each([['Foo bar'], ['baz'], ['something']])('renders expected content', (children) => { + it.each([['Foo bar'], ['baz'], ['something']])('renders expected content', async (children) => { const { container } = setUp(children); + expect(container.firstChild).toBeTruthy(); expect(container.firstChild).toMatchSnapshot(); }); @@ -30,19 +31,19 @@ describe('', () => { it('displays modal when button is clicked', async () => { const { user } = setUp(); - expect(screen.queryByText(/Are you sure you want to remove/)).not.toBeInTheDocument(); - await user.click(screen.getByText('Remove this server')); - expect(screen.getByText(/Are you sure you want to remove/)).toBeInTheDocument(); + await expect.element(page.getByText(/Are you sure you want to remove/)).not.toBeInTheDocument(); + await user.click(page.getByText('Remove this server')); + await expect.element(page.getByText(/Are you sure you want to remove/)).toBeInTheDocument(); }); it('navigates to home when deletion is confirmed', async () => { const { user, history } = setUp(); // Open modal - await user.click(screen.getByText('Remove this server')); + await user.click(page.getByText('Remove this server')); expect(history.location.pathname).toEqual('/foo'); - await user.click(screen.getByRole('button', { name: 'Delete' })); + await user.click(page.getByRole('button', { name: 'Delete' })); expect(history.location.pathname).toEqual('/'); }); }); diff --git a/test/servers/DeleteServerModal.test.tsx b/test/servers/DeleteServerModal.test.tsx index b6e9560c..425ceaa8 100644 --- a/test/servers/DeleteServerModal.test.tsx +++ b/test/servers/DeleteServerModal.test.tsx @@ -1,5 +1,5 @@ -import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; +import { page } from 'vitest/browser'; import type { ServerWithId } from '../../src/servers/data'; import { DeleteServerModal } from '../../src/servers/DeleteServerModal'; import { checkAccessibility } from '../__helpers__/accessibility'; @@ -18,28 +18,28 @@ describe('', () => { it('passes a11y checks', () => checkAccessibility(setUp())); - it('renders a modal window', () => { + it('renders a modal window', async () => { setUp(); - expect(screen.getByRole('dialog')).toBeInTheDocument(); - expect(screen.getByRole('heading')).toHaveTextContent('Remove server'); + await expect.element(page.getByRole('dialog')).toBeInTheDocument(); + await expect.element(page.getByRole('heading')).toHaveTextContent('Remove server'); }); - it('displays the name of the server as part of the content', () => { + it('displays the name of the server as part of the content', async () => { setUp(); - expect(screen.getByText(/^Are you sure you want to remove/)).toBeInTheDocument(); - expect(screen.getByText(serverName)).toBeInTheDocument(); + await expect.element(page.getByText(/^Are you sure you want to remove/)).toBeInTheDocument(); + await expect.element(page.getByText(serverName)).toBeInTheDocument(); }); - it.each([[() => screen.getByRole('button', { name: 'Cancel' })], [() => screen.getByLabelText('Close dialog')]])( + it.each([[() => page.getByRole('button', { name: 'Cancel' })], [() => page.getByLabelText('Close dialog')]])( 'closes dialog when clicking cancel button', async (getButton) => { const { user, store } = setUp(); - expect(screen.getByRole('dialog')).toBeInTheDocument(); + await expect.element(page.getByRole('dialog')).toBeInTheDocument(); await user.click(getButton()); - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + await expect.element(page.getByRole('dialog')).not.toBeInTheDocument(); // No server has been deleted expect(Object.keys(store.getState().servers)).toHaveLength(1); @@ -50,7 +50,7 @@ describe('', () => { const { user, store } = setUp(); expect(Object.keys(store.getState().servers)).toHaveLength(1); - await user.click(screen.getByRole('button', { name: 'Delete' })); + await user.click(page.getByRole('button', { name: 'Delete' })); expect(Object.keys(store.getState().servers)).toHaveLength(0); }); }); diff --git a/test/servers/EditServer.test.tsx b/test/servers/EditServer.test.tsx index 86744176..fbe3d0ec 100644 --- a/test/servers/EditServer.test.tsx +++ b/test/servers/EditServer.test.tsx @@ -1,7 +1,8 @@ -import { fireEvent, screen, waitFor } from '@testing-library/react'; +import { fireEvent, waitFor } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { createMemoryHistory } from 'history'; import { Router } from 'react-router'; +import { page } from 'vitest/browser'; import type { ReachableServer, SelectedServer } from '../../src/servers/data'; import { isServerWithId } from '../../src/servers/data'; import { EditServer } from '../../src/servers/EditServer'; @@ -35,35 +36,35 @@ describe('', () => { it('passes a11y checks', () => checkAccessibility(setUp())); - it('renders nothing if selected server is not reachable', () => { + it('renders nothing if selected server is not reachable', async () => { setUp(fromPartial({})); - expect(screen.queryByText('Edit')).not.toBeInTheDocument(); - expect(screen.queryByText('Cancel')).not.toBeInTheDocument(); - expect(screen.queryByText('Save')).not.toBeInTheDocument(); + await expect.element(page.getByText('Edit')).not.toBeInTheDocument(); + await expect.element(page.getByText('Cancel')).not.toBeInTheDocument(); + await expect.element(page.getByText('Save')).not.toBeInTheDocument(); }); it('renders server title', () => { setUp(); - expect(screen.getByText(`Edit "${defaultSelectedServer.name}"`)).toBeInTheDocument(); + expect(page.getByText(`Edit "${defaultSelectedServer.name}"`)).toBeInTheDocument(); }); - it('display the server info in the form components', () => { + it('display the server info in the form components', async () => { setUp(); - expect(screen.getByLabelText(/^Name/)).toBeInTheDocument(); - expect(screen.getByLabelText(/^URL/)).toBeInTheDocument(); - expect(screen.getByLabelText(/^API key/)).toBeInTheDocument(); + await expect.element(page.getByLabelText(/^Name/)).toBeInTheDocument(); + await expect.element(page.getByLabelText(/^URL/)).toBeInTheDocument(); + await expect.element(page.getByLabelText(/^API key/)).toBeInTheDocument(); }); it('edits server and redirects to it when form is submitted', async () => { const { user, history, store } = setUp(); - await user.type(screen.getByLabelText(/^Name/), ' edited'); - await user.type(screen.getByLabelText(/^URL/), ' edited'); + await user.type(page.getByLabelText(/^Name/), ' edited'); + await user.type(page.getByLabelText(/^URL/), ' edited'); // TODO Using fire event because userEvent.click on the Submit button does not submit the form // await user.click(screen.getByRole('button', { name: 'Save' })); - fireEvent.submit(screen.getByRole('form')); + fireEvent.submit(page.getByTestId('server-form').element()); expect(store.getState().servers[defaultSelectedServer.id]).toEqual( expect.objectContaining({ @@ -81,9 +82,9 @@ describe('', () => { async ({ forwardCredentials }) => { const { user, store } = setUp({ ...defaultSelectedServer, forwardCredentials }); - await user.click(screen.getByText('Advanced options')); - await user.click(screen.getByLabelText('Forward credentials to this server on every request.')); - fireEvent.submit(screen.getByRole('form')); + await user.click(page.getByText('Advanced options')); + await user.click(page.getByLabelText('Forward credentials to this server on every request.')); + fireEvent.submit(page.getByTestId('server-form').element()); await waitFor(() => expect(store.getState().servers[defaultSelectedServer.id]).toEqual( diff --git a/test/servers/ManageServers.test.tsx b/test/servers/ManageServers.test.tsx index 7d63d57c..f87c341f 100644 --- a/test/servers/ManageServers.test.tsx +++ b/test/servers/ManageServers.test.tsx @@ -1,6 +1,7 @@ -import { screen, waitFor } from '@testing-library/react'; +import { waitFor } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { MemoryRouter } from 'react-router'; +import { page } from 'vitest/browser'; import type { ServersMap, ServerWithId } from '../../src/servers/data'; import { ManageServers } from '../../src/servers/ManageServers'; import type { ServersExporter } from '../../src/servers/services/ServersExporter'; @@ -39,71 +40,70 @@ describe('', () => { baz: createServerMock('baz'), }); const search = async (searchTerm: string) => { - await user.clear(screen.getByPlaceholderText('Search...')); - await user.type(screen.getByPlaceholderText('Search...'), searchTerm); + await user.clear(page.getByPlaceholder('Search...')); + await user.type(page.getByPlaceholder('Search...'), searchTerm); }; // Add one for the header row - const expectRows = (amount: number) => expect(screen.getAllByRole('row')).toHaveLength(amount + 1); + const expectRows = (amount: number) => expect(page.getByRole('row').elements()).toHaveLength(amount + 1); expectRows(3); - expect(screen.queryByText('No servers found.')).not.toBeInTheDocument(); + await expect.element(page.getByText('No servers found.')).not.toBeInTheDocument(); await search('foo'); await waitFor(() => expectRows(1)); - expect(screen.queryByText('No servers found.')).not.toBeInTheDocument(); + await expect.element(page.getByText('No servers found.')).not.toBeInTheDocument(); await search('Ba'); await waitFor(() => expectRows(2)); - expect(screen.queryByText('No servers found.')).not.toBeInTheDocument(); + await expect.element(page.getByText('No servers found.')).not.toBeInTheDocument(); await search('invalid'); await waitFor(() => expectRows(1)); - expect(screen.getByText('No servers found.')).toBeInTheDocument(); + await expect.element(page.getByText('No servers found.')).toBeInTheDocument(); }); - it.each([ - [createServerMock('foo'), 3], - [createServerMock('foo', true), 4], - ])('shows different amount of columns if there are at least one auto-connect server', (server, expectedCols) => { - setUp({ server }); + it.only.each([[createServerMock('foo')], [createServerMock('foo', true)]])( + 'shows different amount of columns if there are at least one auto-connect server', + async (server) => { + setUp({ server }); - expect(screen.getAllByRole('columnheader')).toHaveLength(expectedCols); - if (server.autoConnect) { - expect(screen.getByTestId('auto-connect')).toBeInTheDocument(); - } else { - expect(screen.queryByTestId('auto-connect')).not.toBeInTheDocument(); - } - }); + if (server.autoConnect) { + await expect.element(page.getByTestId('auto-connect')).toBeInTheDocument(); + } else { + await expect.element(page.getByTestId('auto-connect')).not.toBeInTheDocument(); + } + }, + ); it.each([ [{}, 0], [{ foo: createServerMock('foo') }, 1], ])('shows export button if the list of servers is not empty', (servers, expectedButtons) => { setUp(servers); - expect(screen.queryAllByRole('button', { name: 'Export servers' })).toHaveLength(expectedButtons); + expect(page.getByRole('button', { name: 'Export servers' }).elements()).toHaveLength(expectedButtons); }); it('allows exporting servers when clicking on button', async () => { const { user } = setUp({ foo: createServerMock('foo') }); expect(exportServers).not.toHaveBeenCalled(); - await user.click(screen.getByRole('button', { name: 'Export servers' })); + await user.click(page.getByRole('button', { name: 'Export servers' })); expect(exportServers).toHaveBeenCalled(); }); - 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', async (hasError) => { useTimeoutToggle.mockReturnValue([hasError, vi.fn()]); setUp({ foo: createServerMock('foo') }); if (hasError) { - expect( - screen.getByText('The servers could not be imported. Make sure the format is correct.'), - ).toBeInTheDocument(); + await expect + .element(page.getByText('The servers could not be imported. Make sure the format is correct.')) + .toBeInTheDocument(); } else { - expect( - screen.queryByText('The servers could not be imported. Make sure the format is correct.'), - ).not.toBeInTheDocument(); + await expect + .element(page.getByText('The servers could not be imported. Make sure the format is correct.')) + .not.toBeInTheDocument(); } }); }); diff --git a/test/servers/ManageServersRow.test.tsx b/test/servers/ManageServersRow.test.tsx index 4bd81b80..4d984ec2 100644 --- a/test/servers/ManageServersRow.test.tsx +++ b/test/servers/ManageServersRow.test.tsx @@ -1,6 +1,6 @@ import { Table } from '@shlinkio/shlink-frontend-kit'; -import { screen } from '@testing-library/react'; import { MemoryRouter } from 'react-router'; +import { page } from 'vitest/browser'; import type { ServerWithId } from '../../src/servers/data'; import { ManageServersRow } from '../../src/servers/ManageServersRow'; import { checkAccessibility } from '../__helpers__/accessibility'; @@ -29,12 +29,12 @@ describe('', () => { [false, 3], ])('renders expected amount of columns', (hasAutoConnect, expectedCols) => { setUp(hasAutoConnect); - expect(screen.getAllByRole('cell')).toHaveLength(expectedCols); + expect(page.getByRole('cell').elements()).toHaveLength(expectedCols); }); - it('renders an options dropdown', () => { + it('renders an options dropdown', async () => { setUp(); - expect(screen.getByRole('button', { name: 'Options' })).toBeInTheDocument(); + await expect.element(page.getByRole('button', { name: 'Options' })).toBeInTheDocument(); }); it.each([[true], [false]])('renders auto-connect icon only if server is autoConnect', (autoConnect) => { @@ -42,13 +42,13 @@ describe('', () => { expect(container).toMatchSnapshot(); }); - it('renders server props where appropriate', () => { + it('renders server props where appropriate', async () => { setUp(); - const link = screen.getByRole('link'); + const link = page.getByRole('link'); - expect(link).toHaveAttribute('href', `/server/${server.id}`); - expect(link).toHaveTextContent(server.name); - expect(screen.getByText(server.url)).toBeInTheDocument(); + await expect.element(link).toHaveAttribute('href', `/server/${server.id}`); + await expect.element(link).toHaveTextContent(server.name); + await expect.element(page.getByText(server.url)).toBeInTheDocument(); }); }); diff --git a/test/servers/ManageServersRowDropdown.test.tsx b/test/servers/ManageServersRowDropdown.test.tsx index d347df4f..74607349 100644 --- a/test/servers/ManageServersRowDropdown.test.tsx +++ b/test/servers/ManageServersRowDropdown.test.tsx @@ -1,6 +1,6 @@ -import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { MemoryRouter } from 'react-router'; +import { page } from 'vitest/browser'; import type { UserEvent } from 'vitest/browser'; import type { ServerWithId } from '../../src/servers/data'; import { ManageServersRowDropdown } from '../../src/servers/ManageServersRowDropdown'; @@ -21,7 +21,7 @@ describe('', () => { }, ); }; - const toggleDropdown = (user: UserEvent) => user.click(screen.getByRole('button')); + const toggleDropdown = (user: UserEvent) => user.click(page.getByRole('button')); it('passes a11y checks', async () => { const { user, container } = setUp(); @@ -34,20 +34,24 @@ describe('', () => { it('renders expected amount of dropdown items', async () => { const { user } = setUp(); - expect(screen.queryByRole('menu')).not.toBeInTheDocument(); + await expect.element(page.getByRole('menu')).not.toBeInTheDocument(); await toggleDropdown(user); - expect(screen.getByRole('menu')).toBeInTheDocument(); + await expect.element(page.getByRole('menu')).toBeInTheDocument(); - expect(screen.getAllByRole('menuitem')).toHaveLength(4); - expect(screen.getByRole('menuitem', { name: 'Connect' })).toHaveAttribute('href', '/server/abc123'); - expect(screen.getByRole('menuitem', { name: 'Edit server' })).toHaveAttribute('href', '/server/abc123/edit'); + expect(page.getByRole('menuitem').elements()).toHaveLength(4); + await expect + .element(page.getByRole('menuitem', { name: 'Connect', exact: true })) + .toHaveAttribute('href', '/server/abc123'); + await expect + .element(page.getByRole('menuitem', { name: 'Edit server' })) + .toHaveAttribute('href', '/server/abc123/edit'); }); it.each([true, false])('allows toggling auto-connect', async (autoConnect) => { const { user, store } = setUp(autoConnect); await toggleDropdown(user); - await user.click(screen.getByRole('menuitem', { name: autoConnect ? 'Do not auto-connect' : 'Auto-connect' })); + await user.click(page.getByRole('menuitem', { name: autoConnect ? 'Do not auto-connect' : 'Auto-connect' })); expect(Object.values(store.getState().servers)[0].autoConnect).toEqual(!autoConnect); }); @@ -55,12 +59,12 @@ describe('', () => { it('renders deletion modal', async () => { const { user } = setUp(); - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + expect(page.getByRole('dialog')).not.toBeInTheDocument(); await toggleDropdown(user); - await user.click(screen.getByRole('menuitem', { name: 'Remove server' })); + await user.click(page.getByRole('menuitem', { name: 'Remove server' })); - expect(screen.getByRole('dialog')).toBeInTheDocument(); + await expect.element(page.getByRole('dialog')).toBeInTheDocument(); }); it.each([[true], [false]])('renders expected size and icon', (autoConnect) => { diff --git a/test/servers/ServersDropdown.test.tsx b/test/servers/ServersDropdown.test.tsx index e9962c46..ab10f3c3 100644 --- a/test/servers/ServersDropdown.test.tsx +++ b/test/servers/ServersDropdown.test.tsx @@ -1,6 +1,6 @@ -import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { MemoryRouter } from 'react-router'; +import { page } from 'vitest/browser'; import type { ServersMap } from '../../src/servers/data'; import { ServersDropdown } from '../../src/servers/ServersDropdown'; import { checkAccessibility } from '../__helpers__/accessibility'; @@ -27,7 +27,7 @@ describe('', () => { it('passes a11y checks', async () => { const { user, ...rest } = setUp(); // Open menu - await user.click(screen.getByText('Servers')); + await user.click(page.getByText('Servers')); return checkAccessibility(rest); }); @@ -35,33 +35,35 @@ describe('', () => { it('contains the list of servers and the "mange servers" button', async () => { const { user } = setUp(); - await user.click(screen.getByText('Servers')); - const items = screen.getAllByRole('menuitem'); + await user.click(page.getByText('Servers')); + const items = page.getByRole('menuitem').elements(); // We have to add two for the "Manage servers" and the "Settings" menu items expect(items).toHaveLength(Object.values(fallbackServers).length + 2); - expect(items[1]).toHaveTextContent('foo'); - expect(items[2]).toHaveTextContent('bar'); - expect(items[3]).toHaveTextContent('baz'); - expect(items[4]).toHaveTextContent('Manage servers'); + await expect.element(items[1]).toHaveTextContent('foo'); + await expect.element(items[2]).toHaveTextContent('bar'); + await expect.element(items[3]).toHaveTextContent('baz'); + await expect.element(items[4]).toHaveTextContent('Manage servers'); }); - it('contains a toggle with proper text', () => { + it('contains a toggle with proper text', async () => { setUp(); - expect(screen.getByRole('button')).toHaveTextContent('Servers'); + await expect.element(page.getByRole('button')).toHaveTextContent('Servers'); }); it('contains a button to manage servers', async () => { const { user } = setUp(); - await user.click(screen.getByText('Servers')); - expect(screen.getByRole('menuitem', { name: 'Manage servers' })).toHaveAttribute('href', '/manage-servers'); + await user.click(page.getByText('Servers')); + await expect + .element(page.getByRole('menuitem', { name: 'Manage servers' })) + .toHaveAttribute('href', '/manage-servers'); }); it('shows only create link when no servers exist yet', async () => { const { user } = setUp({}); - await user.click(screen.getByText('Servers')); - expect(screen.getByRole('menuitem', { name: 'Add a server' })).toBeInTheDocument(); + await user.click(page.getByText('Servers')); + await expect.element(page.getByRole('menuitem', { name: 'Add a server' })).toBeInTheDocument(); }); }); diff --git a/test/servers/ServersListGroup.test.tsx b/test/servers/ServersListGroup.test.tsx index 3e2dc26c..176497f9 100644 --- a/test/servers/ServersListGroup.test.tsx +++ b/test/servers/ServersListGroup.test.tsx @@ -26,14 +26,14 @@ describe('', () => { expect(screen.queryAllByRole('link')).toHaveLength(servers.length); }); - it.each([[true], [false], [undefined]])('renders proper classes for embedded', (borderless) => { + it.each([[true], [false], [undefined]])('renders proper classes for embedded', async (borderless) => { setUp({ servers, borderless }); const list = screen.getByTestId('list'); if (!borderless) { - expect(list).toHaveClass('border-y'); + await expect.element(list).toHaveClass('border-y'); } else { - expect(list).not.toHaveClass('border-y'); + await expect.element(list).not.toHaveClass('border-y'); } }); }); diff --git a/test/servers/helpers/DuplicatedServersModal.test.tsx b/test/servers/helpers/DuplicatedServersModal.test.tsx index 34e5d286..3246fa9e 100644 --- a/test/servers/helpers/DuplicatedServersModal.test.tsx +++ b/test/servers/helpers/DuplicatedServersModal.test.tsx @@ -1,5 +1,5 @@ -import { act, screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; +import { page } from 'vitest/browser'; import type { ServerData } from '../../../src/servers/data'; import { DuplicatedServersModal } from '../../../src/servers/helpers/DuplicatedServersModal'; import { checkAccessibility } from '../../__helpers__/accessibility'; @@ -9,10 +9,8 @@ describe('', () => { const onClose = vi.fn(); const onConfirm = vi.fn(); const setUp = (duplicatedServers: ServerData[] = []) => - act(() => - renderWithEvents( - , - ), + renderWithEvents( + , ); const mockServer = (data: Partial = {}) => fromPartial(data); @@ -24,9 +22,9 @@ describe('', () => { [[mockServer(), mockServer()], 2], [[mockServer(), mockServer(), mockServer()], 3], [[mockServer(), mockServer(), mockServer(), mockServer()], 4], - ])('renders expected amount of items', async (duplicatedServers, expectedItems) => { - await setUp(duplicatedServers); - expect(screen.queryAllByRole('listitem')).toHaveLength(expectedItems); + ])('renders expected amount of items', (duplicatedServers, expectedItems) => { + setUp(duplicatedServers); + expect(page.getByRole('listitem')).toHaveLength(expectedItems); }); it.each([ @@ -51,13 +49,13 @@ describe('', () => { }, ], ])('renders expected texts based on amount of servers', async (duplicatedServers, assertions) => { - await setUp(duplicatedServers); + setUp(duplicatedServers); - expect(screen.getByRole('heading')).toHaveTextContent(assertions.header); - expect(screen.getByText(assertions.firstParagraph)).toBeInTheDocument(); - expect(screen.getByText(assertions.lastParagraph)).toBeInTheDocument(); - expect(screen.getByRole('button', { name: assertions.discardBtn })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: assertions.confirmButton })).toBeInTheDocument(); + await expect.element(page.getByRole('heading')).toHaveTextContent(assertions.header); + await expect.element(page.getByText(assertions.firstParagraph)).toBeInTheDocument(); + await expect.element(page.getByText(assertions.lastParagraph)).toBeInTheDocument(); + await expect.element(page.getByRole('button', { name: assertions.discardBtn })).toBeInTheDocument(); + await expect.element(page.getByRole('button', { name: assertions.confirmButton })).toBeInTheDocument(); }); it.each([ @@ -65,37 +63,42 @@ describe('', () => { [[mockServer({ url: 'url', apiKey: 'apiKey' })]], [[mockServer({ url: 'url_1', apiKey: 'apiKey_1' }), mockServer({ url: 'url_2', apiKey: 'apiKey_2' })]], ])('displays provided server data', async (duplicatedServers) => { - await setUp(duplicatedServers); + setUp(duplicatedServers); if (duplicatedServers.length === 0) { - expect(screen.queryByRole('listitem')).not.toBeInTheDocument(); + await expect.element(page.getByRole('listitem')).not.toBeInTheDocument(); } else if (duplicatedServers.length === 1) { - const [firstItem, secondItem] = screen.getAllByRole('listitem'); + const [firstItem, secondItem] = page.getByRole('listitem').elements(); - expect(firstItem).toHaveTextContent(`URL: ${duplicatedServers[0].url}`); - expect(secondItem).toHaveTextContent(`API key: ${duplicatedServers[0].apiKey}`); + await expect.element(firstItem).toHaveTextContent(`URL: ${duplicatedServers[0].url}`); + await expect.element(secondItem).toHaveTextContent(`API key: ${duplicatedServers[0].apiKey}`); } else { expect.assertions(duplicatedServers.length); - screen.getAllByRole('listitem').forEach((item, index) => { - const server = duplicatedServers[index]; - expect(item).toHaveTextContent(`${server.url} - ${server.apiKey}`); - }); + await Promise.all( + page + .getByRole('listitem') + .elements() + .map((item, index) => { + const server = duplicatedServers[index]; + return expect.element(item).toHaveTextContent(`${server.url} - ${server.apiKey}`); + }), + ); } }); it('invokes onClose when appropriate button is clicked', async () => { - const { user } = await setUp(); + const { user } = setUp(); expect(onClose).not.toHaveBeenCalled(); - await user.click(screen.getByRole('button', { name: 'Discard' })); + await user.click(page.getByRole('button', { name: 'Discard' })); expect(onClose).toHaveBeenCalled(); }); it('invokes onConfirm when appropriate button is clicked', async () => { - const { user } = await setUp(); + const { user } = setUp(); expect(onConfirm).not.toHaveBeenCalled(); - await user.click(screen.getByRole('button', { name: 'Save duplicate' })); + await user.click(page.getByRole('button', { name: 'Save duplicate' })); expect(onConfirm).toHaveBeenCalled(); }); }); diff --git a/test/servers/helpers/ImportServersBtn.test.tsx b/test/servers/helpers/ImportServersBtn.test.tsx index e9303fd6..044f08de 100644 --- a/test/servers/helpers/ImportServersBtn.test.tsx +++ b/test/servers/helpers/ImportServersBtn.test.tsx @@ -1,5 +1,5 @@ -import { screen, waitFor } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; +import { page } from 'vitest/browser'; import type { ServerData, ServersMap, ServerWithId } from '../../../src/servers/data'; import type { ImportServersBtnProps } from '../../../src/servers/helpers/ImportServersBtn'; import { ImportServersBtn } from '../../../src/servers/helpers/ImportServersBtn'; @@ -22,35 +22,33 @@ describe('', () => { it('shows tooltip on button hover', async () => { const { user } = setUp(); - expect(screen.queryByText(/^You can create servers by importing a CSV file/)).not.toBeInTheDocument(); - await user.hover(screen.getByRole('button')); - await waitFor(() => - expect(screen.getByText(/^You can create servers by importing a CSV file/)).toBeInTheDocument(), - ); + await expect.element(page.getByText('You can create servers by importing a CSV file')).not.toBeInTheDocument(); + await user.hover(page.getByRole('button')); + await expect.element(page.getByText('You can create servers by importing a CSV file')).toBeInTheDocument(); }); it.each([ [undefined, ''], ['foo', 'foo'], ['bar', 'bar'], - ])('allows a class name to be provided', (providedClassName, expectedClassName) => { + ])('allows a class name to be provided', async (providedClassName, expectedClassName) => { setUp({ className: providedClassName }); - expect(screen.getByRole('button')).toHaveAttribute('class', expect.stringContaining(expectedClassName)); + await expect.element(page.getByRole('button')).toHaveAttribute('class', expect.stringContaining(expectedClassName)); }); it.each([ [undefined, 'Import from file'], ['foo', 'foo'], ['bar', 'bar'], - ])('has expected text', (children, expectedText) => { + ])('has expected text', async (children, expectedText) => { setUp({ children }); - expect(screen.getByRole('button')).toHaveTextContent(expectedText); + await expect.element(page.getByRole('button')).toHaveTextContent(expectedText); }); it('imports servers when file input changes', async () => { const { user } = setUp(); - await user.upload(screen.getByTestId('csv-file-input'), csvFile); + await user.upload(page.getByTestId('csv-file-input'), csvFile); expect(importServersFromFile).toHaveBeenCalledTimes(1); }); @@ -74,15 +72,15 @@ describe('', () => { importServersFromFile.mockResolvedValue([existingServerData, newServer]); - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); - await user.upload(screen.getByTestId('csv-file-input'), csvFile); + await expect.element(page.getByRole('dialog')).not.toBeInTheDocument(); + await user.upload(page.getByTestId('csv-file-input'), csvFile); // Once the file is uploaded, non-duplicated servers are immediately created const { servers } = store.getState(); expect(Object.keys(servers)).toHaveLength(2); - expect(screen.getByRole('dialog')).toBeInTheDocument(); - await user.click(screen.getByRole('button', { name: btnName })); + await expect.element(page.getByRole('dialog')).toBeInTheDocument(); + await user.click(page.getByRole('button', { name: btnName })); // If duplicated servers are saved, there's one extra server creation if (savesDuplicatedServers) { diff --git a/test/servers/helpers/ServerError.test.tsx b/test/servers/helpers/ServerError.test.tsx index 3c4ebfaf..24ef3cd1 100644 --- a/test/servers/helpers/ServerError.test.tsx +++ b/test/servers/helpers/ServerError.test.tsx @@ -45,10 +45,10 @@ describe('', () => { notFound: ['Could not find this Shlink server.'], }, ], - ])('renders expected information based on provided server type', (selectedServer, { found, notFound }) => { + ])('renders expected information based on provided server type', async (selectedServer, { found, notFound }) => { setUp(selectedServer); - found.forEach((text) => expect(screen.getByText(text)).toBeInTheDocument()); - notFound.forEach((text) => expect(screen.queryByText(text)).not.toBeInTheDocument()); + await Promise.all(found.map((text) => expect.element(screen.getByText(text)).toBeInTheDocument())); + await Promise.all(notFound.map((text) => expect.element(screen.queryByText(text)).not.toBeInTheDocument())); }); }); diff --git a/test/servers/helpers/ServerForm.test.tsx b/test/servers/helpers/ServerForm.test.tsx index 1305c51d..30f76a25 100644 --- a/test/servers/helpers/ServerForm.test.tsx +++ b/test/servers/helpers/ServerForm.test.tsx @@ -1,4 +1,5 @@ -import { fireEvent, screen } from '@testing-library/react'; +import { fireEvent } from '@testing-library/react'; +import { page } from 'vitest/browser'; import { ServerForm } from '../../../src/servers/helpers/ServerForm'; import { checkAccessibility } from '../../__helpers__/accessibility'; import { renderWithEvents } from '../../__helpers__/setUpTest'; @@ -9,21 +10,21 @@ describe('', () => { it('passes a11y checks', () => checkAccessibility(setUp())); - it('renders inputs', () => { + it('renders inputs', async () => { setUp(); - expect(screen.getByLabelText(/^Name/)).toBeInTheDocument(); - expect(screen.getByLabelText(/^URL/)).toBeInTheDocument(); - expect(screen.getByLabelText(/^API key/)).toBeInTheDocument(); - expect(screen.getByText('Something')).toBeInTheDocument(); - expect(screen.getByText('Advanced options')).toBeInTheDocument(); + await expect.element(page.getByLabelText(/^Name/)).toBeInTheDocument(); + await expect.element(page.getByLabelText(/^URL/)).toBeInTheDocument(); + await expect.element(page.getByLabelText(/^API key/)).toBeInTheDocument(); + await expect.element(page.getByText('Something')).toBeInTheDocument(); + await expect.element(page.getByText('Advanced options')).toBeInTheDocument(); }); it('invokes submit callback when submit event is triggered', async () => { setUp(); expect(onSubmit).not.toHaveBeenCalled(); - fireEvent.submit(screen.getByRole('form'), { preventDefault: vi.fn() }); + fireEvent.submit(page.getByTestId('server-form').element(), { preventDefault: vi.fn() }); expect(onSubmit).toHaveBeenCalled(); }); @@ -31,8 +32,8 @@ describe('', () => { const { user } = setUp(); const forwardCredentialsLabel = 'Forward credentials to this server on every request.'; - expect(screen.queryByLabelText(forwardCredentialsLabel)).not.toBeInTheDocument(); - await user.click(screen.getByText('Advanced options')); - expect(screen.getByLabelText(forwardCredentialsLabel)).toBeInTheDocument(); + await expect.element(page.getByLabelText(forwardCredentialsLabel)).not.toBeInTheDocument(); + await user.click(page.getByText('Advanced options')); + await expect.element(page.getByLabelText(forwardCredentialsLabel)).toBeInTheDocument(); }); }); diff --git a/vite.config.ts b/vite.config.ts index fc3e19e8..1b404501 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -39,6 +39,9 @@ export default defineConfig({ // Vitest config test: { + globals: true, + clearMocks: true, + // Run tests in an actual browser browser: { provider: playwright({ @@ -50,8 +53,6 @@ export default defineConfig({ screenshotFailures: false, instances: [{ browser: 'chromium' }], }, - globals: true, - setupFiles: './config/test/setupTests.ts', coverage: { provider: 'v8', reporter: ['text', 'text-summary', 'html', 'clover'],