Replace @testing-library/jest-dom with built-in vitest browser assertions

This commit is contained in:
Alejandro Celaya
2026-08-16 19:32:41 +02:00
parent ba73644871
commit 585d7c6f63
30 changed files with 305 additions and 429 deletions

View File

@@ -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();
});

140
package-lock.json generated
View File

@@ -40,7 +40,6 @@
"devDependencies": { "devDependencies": {
"@shlinkio/eslint-config-js-coding-standard": "~4.1.0", "@shlinkio/eslint-config-js-coding-standard": "~4.1.0",
"@tailwindcss/vite": "^4.3.3", "@tailwindcss/vite": "^4.3.3",
"@testing-library/jest-dom": "^7.0.0",
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",
"@total-typescript/shoehorn": "^0.1.2", "@total-typescript/shoehorn": "^0.1.2",
"@types/node": "^26.1.2", "@types/node": "^26.1.2",
@@ -64,12 +63,6 @@
"vitest": "^4.0.3" "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": { "node_modules/@babel/code-frame": {
"version": "7.29.0", "version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", "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" "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": { "node_modules/@testing-library/react": {
"version": "16.3.2", "version": "16.3.2",
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", "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", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
"dev": true, "dev": true,
"peer": true,
"dependencies": { "dependencies": {
"dequal": "^2.0.3" "dequal": "^2.0.3"
} }
@@ -4896,11 +4862,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/css.escape": {
"version": "1.5.1",
"dev": true,
"license": "MIT"
},
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "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", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
"dev": true, "dev": true,
"peer": true,
"engines": { "engines": {
"node": ">=6" "node": ">=6"
} }
@@ -5871,14 +5833,6 @@
"url": "https://opencollective.com/immer" "url": "https://opencollective.com/immer"
} }
}, },
"node_modules/indent-string": {
"version": "4.0.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/internal-slot": { "node_modules/internal-slot": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
@@ -6756,14 +6710,6 @@
"version": "2.1.1", "version": "2.1.1",
"license": "MIT" "license": "MIT"
}, },
"node_modules/min-indent": {
"version": "1.0.1",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/minipass": { "node_modules/minipass": {
"version": "7.1.3", "version": "7.1.3",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "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" "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": { "node_modules/redux": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
@@ -7977,17 +7911,6 @@
"node": ">=10" "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": { "node_modules/supports-color": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -9268,12 +9191,6 @@
} }
}, },
"dependencies": { "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": { "@babel/code-frame": {
"version": "7.29.0", "version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", "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": { "@testing-library/react": {
"version": "16.3.2", "version": "16.3.2",
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", "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", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
"dev": true, "dev": true,
"peer": true,
"requires": { "requires": {
"dequal": "^2.0.3" "dequal": "^2.0.3"
} }
@@ -12197,10 +12093,6 @@
"integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==",
"dev": true "dev": true
}, },
"css.escape": {
"version": "1.5.1",
"dev": true
},
"csstype": { "csstype": {
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -12376,7 +12268,8 @@
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
"dev": true "dev": true,
"peer": true
}, },
"detect-libc": { "detect-libc": {
"version": "2.1.2", "version": "2.1.2",
@@ -12868,10 +12761,6 @@
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.16.tgz", "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.16.tgz",
"integrity": "sha512-Xs7H9rBc+kti1J6RueUvbEBkmOz7jqj11XYgf+YMXAYzu8EeE7hwZ9poLXdVfVnGmJu7QAf41T7H2KuF6QoK6Q==" "integrity": "sha512-Xs7H9rBc+kti1J6RueUvbEBkmOz7jqj11XYgf+YMXAYzu8EeE7hwZ9poLXdVfVnGmJu7QAf41T7H2KuF6QoK6Q=="
}, },
"indent-string": {
"version": "4.0.0",
"dev": true
},
"internal-slot": { "internal-slot": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
@@ -13380,10 +13269,6 @@
"merge": { "merge": {
"version": "2.1.1" "version": "2.1.1"
}, },
"min-indent": {
"version": "1.0.1",
"dev": true
},
"minipass": { "minipass": {
"version": "7.1.3", "version": "7.1.3",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
@@ -13718,14 +13603,6 @@
"victory-vendor": "^37.0.2" "victory-vendor": "^37.0.2"
} }
}, },
"redent": {
"version": "3.0.0",
"dev": true,
"requires": {
"indent-string": "^4.0.0",
"strip-indent": "^3.0.0"
}
},
"redux": { "redux": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
@@ -14188,13 +14065,6 @@
"integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==",
"dev": true "dev": true
}, },
"strip-indent": {
"version": "3.0.0",
"dev": true,
"requires": {
"min-indent": "^1.0.0"
}
},
"supports-color": { "supports-color": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",

View File

@@ -57,7 +57,6 @@
"devDependencies": { "devDependencies": {
"@shlinkio/eslint-config-js-coding-standard": "~4.1.0", "@shlinkio/eslint-config-js-coding-standard": "~4.1.0",
"@tailwindcss/vite": "^4.3.3", "@tailwindcss/vite": "^4.3.3",
"@testing-library/jest-dom": "^7.0.0",
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",
"@total-typescript/shoehorn": "^0.1.2", "@total-typescript/shoehorn": "^0.1.2",
"@types/node": "^26.1.2", "@types/node": "^26.1.2",

View File

@@ -28,7 +28,7 @@ export const ServerForm: FC<ServerFormProps> = ({ onSubmit, initialValues, child
const handleSubmit = usePreventDefault(() => onSubmit({ name, url, apiKey, forwardCredentials })); const handleSubmit = usePreventDefault(() => onSubmit({ name, url, apiKey, forwardCredentials }));
return ( return (
<form name="serverForm" onSubmit={handleSubmit}> <form name="serverForm" onSubmit={handleSubmit} data-testid="server-form">
<SimpleCard className="mb-4" bodyClassName="flex flex-col gap-y-3" title={title}> <SimpleCard className="mb-4" bodyClassName="flex flex-col gap-y-3" title={title}>
<LabelledInput label="Name" value={name} onChange={(e) => setName(e.target.value)} required /> <LabelledInput label="Name" value={name} onChange={(e) => setName(e.target.value)} required />
<LabelledInput label="URL" type="url" value={url} onChange={(e) => setUrl(e.target.value)} required /> <LabelledInput label="URL" type="url" value={url} onChange={(e) => setUrl(e.target.value)} required />

View File

@@ -1,10 +1,10 @@
import type { HttpClient } from '@shlinkio/shlink-js-sdk'; import type { HttpClient } from '@shlinkio/shlink-js-sdk';
import { act, screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { act } from 'react';
import { MemoryRouter } from 'react-router'; import { MemoryRouter } from 'react-router';
import { page } from 'vitest/browser';
import { App } from '../../src/app/App'; import { App } from '../../src/app/App';
import { ContainerProvider } from '../../src/container/context'; import { ContainerProvider } from '../../src/container/context';
import type { ServerWithId } from '../../src/servers/data';
import { checkAccessibility } from '../__helpers__/accessibility'; import { checkAccessibility } from '../__helpers__/accessibility';
import { renderWithStore } from '../__helpers__/setUpTest'; import { renderWithStore } from '../__helpers__/setUpTest';
@@ -30,8 +30,8 @@ describe('<App />', () => {
{ {
initialState: { initialState: {
servers: { servers: {
abc123: fromPartial<ServerWithId>({ id: 'abc123', name: 'abc123 server' }), abc123: fromPartial({ id: 'abc123', name: 'abc123 server' }),
def456: fromPartial<ServerWithId>({ id: 'def456', name: 'def456 server' }), def456: fromPartial({ id: 'def456', name: 'def456 server' }),
}, },
settings: fromPartial({}), settings: fromPartial({}),
appUpdated: false, appUpdated: false,
@@ -54,7 +54,7 @@ describe('<App />', () => {
['/other', 'Oops! We could not find requested route.'], ['/other', 'Oops! We could not find requested route.'],
])('renders expected route', async (activeRoute, expectedComponent) => { ])('renders expected route', async (activeRoute, expectedComponent) => {
await setUp(activeRoute); await setUp(activeRoute);
expect(screen.getByText(expectedComponent)).toBeInTheDocument(); await expect.element(page.getByText(expectedComponent)).toBeInTheDocument();
}); });
it.each([ it.each([
@@ -63,12 +63,12 @@ describe('<App />', () => {
['/', true], ['/', true],
])('renders expected classes on shlink-wrapper based on current pathname', async (pathname, isFlex) => { ])('renders expected classes on shlink-wrapper based on current pathname', async (pathname, isFlex) => {
await setUp(pathname); await setUp(pathname);
const shlinkWrapper = screen.getByTestId('shlink-wrapper'); const shlinkWrapper = page.getByTestId('shlink-wrapper');
if (isFlex) { if (isFlex) {
expect(shlinkWrapper).toHaveClass('flex'); await expect.element(shlinkWrapper).toHaveClass('flex');
} else { } else {
expect(shlinkWrapper).not.toHaveClass('flex'); await expect.element(shlinkWrapper).not.toHaveClass('flex');
} }
}); });
}); });

View File

@@ -1,4 +1,4 @@
import { act, screen, waitFor } from '@testing-library/react'; import { page } from 'vitest/browser';
import { AppUpdateBanner } from '../../src/common/AppUpdateBanner'; import { AppUpdateBanner } from '../../src/common/AppUpdateBanner';
import { checkAccessibility } from '../__helpers__/accessibility'; import { checkAccessibility } from '../__helpers__/accessibility';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
@@ -6,40 +6,33 @@ import { renderWithEvents } from '../__helpers__/setUpTest';
describe('<AppUpdateBanner />', () => { describe('<AppUpdateBanner />', () => {
const onClose = vi.fn(); const onClose = vi.fn();
const forceUpdate = vi.fn(); const forceUpdate = vi.fn();
const setUp = async () => { const setUp = () => renderWithEvents(<AppUpdateBanner isOpen onClose={onClose} forceUpdate={forceUpdate} />);
const result = await act(() =>
renderWithEvents(<AppUpdateBanner isOpen onClose={onClose} forceUpdate={forceUpdate} />),
);
await waitFor(() => screen.getByRole('alert'));
return result;
};
it('passes a11y checks', () => checkAccessibility(setUp())); it('passes a11y checks', () => checkAccessibility(setUp()));
it('renders initial state', async () => { it('renders initial state', async () => {
await setUp(); setUp();
expect(screen.getByRole('heading')).toHaveTextContent('This app has just been updated!'); await expect.element(page.getByRole('heading')).toHaveTextContent('This app has just been updated!');
expect(screen.queryByText('Restarting...')).not.toBeInTheDocument(); await expect.element(page.getByText('Restarting...')).not.toBeInTheDocument();
expect(screen.getByText('Restart now')).not.toHaveAttribute('disabled'); await expect.element(page.getByText('Restart now')).not.toHaveAttribute('disabled');
}); });
it('invokes toggle when alert is closed', async () => { it('invokes toggle when alert is closed', async () => {
const { user } = await setUp(); const { user } = setUp();
expect(onClose).not.toHaveBeenCalled(); expect(onClose).not.toHaveBeenCalled();
await user.click(screen.getByLabelText('Close')); await user.click(page.getByLabelText('Close'));
expect(onClose).toHaveBeenCalled(); expect(onClose).toHaveBeenCalled();
}); });
it('triggers the update when clicking the button', async () => { it('triggers the update when clicking the button', async () => {
const { user } = await setUp(); const { user } = setUp();
expect(forceUpdate).not.toHaveBeenCalled(); expect(forceUpdate).not.toHaveBeenCalled();
await user.click(screen.getByText(/^Restart now/)); await user.click(page.getByText(/^Restart now/));
expect(forceUpdate).toHaveBeenCalled(); expect(forceUpdate).toHaveBeenCalled();
expect(await screen.findByText('Restarting...')).toBeInTheDocument(); await expect.element(page.getByText('Restarting...')).toBeInTheDocument();
expect(screen.queryByText(/^Restart now/)).not.toBeInTheDocument(); await expect.element(page.getByText(/^Restart now/)).not.toBeInTheDocument();
}); });
}); });

View File

@@ -1,7 +1,7 @@
import { screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import type { PropsWithChildren, ReactNode } from 'react'; import type { ReactNode } from 'react';
import { ErrorHandler as BaseErrorHandler } from '../../src/common/ErrorHandler'; import { page } from 'vitest/browser';
import { ErrorHandler } from '../../src/common/ErrorHandler';
import { checkAccessibility } from '../__helpers__/accessibility'; import { checkAccessibility } from '../__helpers__/accessibility';
import { renderWithEvents } from '../__helpers__/setUpTest'; import { renderWithEvents } from '../__helpers__/setUpTest';
@@ -13,8 +13,12 @@ describe('<ErrorHandler />', () => {
const reload = vi.fn(); const reload = vi.fn();
const location = fromPartial<Window['location']>({ reload }); const location = fromPartial<Window['location']>({ reload });
const cons = fromPartial<Console>({ error: vi.fn() }); const cons = fromPartial<Console>({ error: vi.fn() });
const ErrorHandler = (props: PropsWithChildren) => <BaseErrorHandler console={cons} location={location} {...props} />; const setUp = (children: ReactNode = 'Error') =>
const setUp = (children: ReactNode = 'Error') => renderWithEvents(<ErrorHandler>{children}</ErrorHandler>); renderWithEvents(
<ErrorHandler console={cons} location={location}>
{children}
</ErrorHandler>,
);
beforeEach(() => { beforeEach(() => {
vi.spyOn(console, 'error').mockImplementation(() => {}); // Silence react errors vi.spyOn(console, 'error').mockImplementation(() => {}); // Silence react errors
@@ -22,26 +26,26 @@ describe('<ErrorHandler />', () => {
it('passes a11y checks', () => checkAccessibility(setUp())); it('passes a11y checks', () => checkAccessibility(setUp()));
it('renders children when no error has occurred', () => { it('renders children when no error has occurred', async () => {
setUp(<span>Foo</span>); setUp(<span>Foo</span>);
expect(screen.getByText('Foo')).toBeInTheDocument(); await expect.element(page.getByText('Foo')).toBeInTheDocument();
expect(screen.queryByText('Oops! This is awkward :S')).not.toBeInTheDocument(); await expect.element(page.getByText('Oops! This is awkward :S')).not.toBeInTheDocument();
expect(screen.queryByRole('button')).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(<ComponentWithError />); setUp(<ComponentWithError />);
expect(screen.getByText('Oops! This is awkward :S')).toBeInTheDocument(); await expect.element(page.getByText('Oops! This is awkward :S')).toBeInTheDocument();
expect(screen.getByRole('button')).toBeInTheDocument(); await expect.element(page.getByRole('button')).toBeInTheDocument();
}); });
it('reloads page on button click', async () => { it('reloads page on button click', async () => {
const { user } = setUp(<ComponentWithError />); const { user } = setUp(<ComponentWithError />);
expect(reload).not.toHaveBeenCalled(); expect(reload).not.toHaveBeenCalled();
await user.click(screen.getByRole('button')); await user.click(page.getByRole('button'));
expect(reload).toHaveBeenCalled(); expect(reload).toHaveBeenCalled();
}); });
}); });

View File

@@ -1,6 +1,6 @@
import { screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router'; import { MemoryRouter } from 'react-router';
import { page } from 'vitest/browser';
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';
import { checkAccessibility } from '../__helpers__/accessibility'; import { checkAccessibility } from '../__helpers__/accessibility';
@@ -20,9 +20,9 @@ describe('<Home />', () => {
it('passes a11y checks', () => it('passes a11y checks', () =>
checkAccessibility(setUp({ '1a': fromPartial<ServerWithId>({ name: 'foo', id: '1' }) }))); checkAccessibility(setUp({ '1a': fromPartial<ServerWithId>({ name: 'foo', id: '1' }) })));
it('renders title', () => { it('renders title', async () => {
setUp(); setUp();
expect(screen.getByRole('heading', { name: 'Welcome!' })).toBeInTheDocument(); await expect.element(page.getByRole('heading', { name: 'Welcome!' })).toBeInTheDocument();
}); });
it.each([ it.each([
@@ -35,15 +35,17 @@ describe('<Home />', () => {
3, 3,
], ],
[{}, 2], [{}, 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); setUp(servers);
const links = screen.getAllByRole('link'); const links = page.getByRole('link').elements();
expect(links).toHaveLength(expectedServers); expect(links).toHaveLength(expectedServers);
if (Object.keys(servers).length === 0) { if (Object.keys(servers).length === 0) {
expect(screen.getByText('This application will help you manage your Shlink servers.')).toBeInTheDocument(); await expect
expect(screen.getByText('Learn more about Shlink')).toBeInTheDocument(); .element(page.getByText('This application will help you manage your Shlink servers.'))
.toBeInTheDocument();
await expect.element(page.getByText('Learn more about Shlink')).toBeInTheDocument();
} }
}); });
}); });

View File

@@ -1,6 +1,6 @@
import { screen } from '@testing-library/react';
import { createMemoryHistory } from 'history'; import { createMemoryHistory } from 'history';
import { Router } from 'react-router'; import { Router } from 'react-router';
import { page } from 'vitest/browser';
import { MainHeader } from '../../src/common/MainHeader'; import { MainHeader } from '../../src/common/MainHeader';
import { checkAccessibility } from '../__helpers__/accessibility'; import { checkAccessibility } from '../__helpers__/accessibility';
import { renderWithStore } from '../__helpers__/setUpTest'; import { renderWithStore } from '../__helpers__/setUpTest';
@@ -19,9 +19,9 @@ describe('<MainHeader />', () => {
it('passes a11y checks', () => checkAccessibility(setUp())); it('passes a11y checks', () => checkAccessibility(setUp()));
it('renders ServersDropdown', () => { it('renders ServersDropdown', async () => {
setUp(); setUp();
expect(screen.getByRole('button', { name: 'Servers' })).toBeInTheDocument(); await expect.element(page.getByRole('button', { name: 'Servers' })).toBeInTheDocument();
}); });
it.each([ it.each([
@@ -30,11 +30,10 @@ describe('<MainHeader />', () => {
['/settings', true], ['/settings', true],
['/settings/foo', true], ['/settings/foo', true],
['/settings/bar', 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); setUp(currentPath);
expect(screen.getByRole('menuitem', { name: /Settings$/ })).toHaveAttribute( await expect
'data-active', .element(page.getByRole('menuitem', { name: /Settings$/ }))
isActive ? 'true' : 'false', .toHaveAttribute('data-active', isActive ? 'true' : 'false');
);
}); });
}); });

View File

@@ -13,29 +13,31 @@ describe('<NotFound />', () => {
it('passes a11y checks', () => checkAccessibility(setUp())); it('passes a11y checks', () => checkAccessibility(setUp()));
it('shows expected error title', () => { it('shows expected error title', async () => {
setUp(); 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(); setUp();
expect( await expect
.element(
screen.getByText( screen.getByText(
"Use your browser's back button to navigate to the page you have previously come from, or just press this button.", "Use your browser's back button to navigate to the page you have previously come from, or just press this button.",
), ),
).toBeInTheDocument(); )
.toBeInTheDocument();
}); });
it.each([ it.each([
[{}, '/', 'Home'], [{}, '/', 'Home'],
[{ to: '/foo/bar', children: 'Hello' }, '/foo/bar', 'Hello'], [{ to: '/foo/bar', children: 'Hello' }, '/foo/bar', 'Hello'],
[{ to: '/baz-bar', children: <>Foo</> }, '/baz-bar', 'Foo'], [{ 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); setUp(props);
const link = screen.getByRole('link'); const link = screen.getByRole('link');
expect(link).toHaveAttribute('href', expectedLink); await expect.element(link).toHaveAttribute('href', expectedLink);
expect(link).toHaveTextContent(expectedText); await expect.element(link).toHaveTextContent(expectedText);
}); });
}); });

View File

@@ -13,8 +13,8 @@ describe('<ScrollToTop />', () => {
it('passes a11y checks', () => checkAccessibility(setUp())); 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); setUp(children);
expect(screen.getByText(children)).toBeInTheDocument(); await expect.element(screen.getByText(children)).toBeInTheDocument();
}); });
}); });

View File

@@ -19,20 +19,18 @@ describe('<ShlinkVersions />', () => {
['not-semver', fromPartial<ReachableServer>({ version: '1.0.0', printableVersion: 'some' }), 'latest', 'some'], ['not-semver', fromPartial<ReachableServer>({ version: '1.0.0', printableVersion: 'some' }), 'latest', 'some'],
])( ])(
'displays expected versions when selected server is reachable', 'displays expected versions when selected server is reachable',
(clientVersion, selectedServer, expectedClientVersion, expectedServerVersion) => { async (clientVersion, selectedServer, expectedClientVersion, expectedServerVersion) => {
setUp({ clientVersion, selectedServer }); setUp({ clientVersion, selectedServer });
const [serverLink, clientLink] = screen.getAllByRole('link'); const [serverLink, clientLink] = screen.getAllByRole('link');
expect(serverLink).toHaveAttribute( await expect
'href', .element(serverLink)
`https://github.com/shlinkio/shlink/releases/${expectedServerVersion}`, .toHaveAttribute('href', `https://github.com/shlinkio/shlink/releases/${expectedServerVersion}`);
); await expect.element(serverLink).toHaveTextContent(expectedServerVersion);
expect(serverLink).toHaveTextContent(expectedServerVersion); await expect
expect(clientLink).toHaveAttribute( .element(clientLink)
'href', .toHaveAttribute('href', `https://github.com/shlinkio/shlink-web-client/releases/${expectedClientVersion}`);
`https://github.com/shlinkio/shlink-web-client/releases/${expectedClientVersion}`, await expect.element(clientLink).toHaveTextContent(expectedClientVersion);
);
expect(clientLink).toHaveTextContent(expectedClientVersion);
}, },
); );
@@ -40,11 +38,13 @@ describe('<ShlinkVersions />', () => {
['1.2.3', null], ['1.2.3', null],
['1.2.3', fromPartial<NotFoundServer>({ serverNotFound: true })], ['1.2.3', fromPartial<NotFoundServer>({ serverNotFound: true })],
['1.2.3', fromPartial<NonReachableServer>({ serverNotReachable: true })], ['1.2.3', fromPartial<NonReachableServer>({ serverNotReachable: true })],
])('displays only client version when selected server is not reachable', (clientVersion, selectedServer) => { ])('displays only client version when selected server is not reachable', async (clientVersion, selectedServer) => {
setUp({ clientVersion, selectedServer }); setUp({ clientVersion, selectedServer });
const links = screen.getAllByRole('link'); const links = screen.getAllByRole('link');
expect(links).toHaveLength(1); 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');
}); });
}); });

View File

@@ -20,13 +20,13 @@ describe('<ShlinkVersionsContainer />', () => {
[null, false], [null, false],
[fromPartial<SelectedServer>({}), false], [fromPartial<SelectedServer>({}), false],
[fromPartial<ReachableServer>({ version: '1.0.0' }), true], [fromPartial<ReachableServer>({ 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); const { container } = setUp(selectedServer);
if (shouldAddMargin) { 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 { } 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)');
} }
}); });
}); });

View File

@@ -1,6 +1,6 @@
import { screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router'; import { MemoryRouter } from 'react-router';
import { page } from 'vitest/browser';
import { ShlinkWebComponentContainer } from '../../src/common/ShlinkWebComponentContainer'; import { ShlinkWebComponentContainer } from '../../src/common/ShlinkWebComponentContainer';
import type { NonReachableServer, NotFoundServer, SelectedServer } from '../../src/servers/data'; import type { NonReachableServer, NotFoundServer, SelectedServer } from '../../src/servers/data';
import { checkAccessibility } from '../__helpers__/accessibility'; import { checkAccessibility } from '../__helpers__/accessibility';
@@ -25,11 +25,11 @@ describe('<ShlinkWebComponentContainer />', () => {
it('passes a11y checks', () => checkAccessibility(setUp(fromPartial({ version: '3.0.0' })))); 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); setUp(null);
expect(screen.getByText('Loading...')).toBeInTheDocument(); await expect.element(page.getByText('Loading...')).toBeInTheDocument();
expect(screen.queryByText('ShlinkWebComponent')).not.toBeInTheDocument(); await expect.element(page.getByText('ShlinkWebComponent')).not.toBeInTheDocument();
}); });
it.each([ it.each([
@@ -38,18 +38,18 @@ describe('<ShlinkWebComponentContainer />', () => {
fromPartial<NonReachableServer>({ id: 'foo', serverNotReachable: true }), fromPartial<NonReachableServer>({ id: 'foo', serverNotReachable: true }),
/Could not connect to this Shlink server/, /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); setUp(selectedServer);
expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); await expect.element(page.getByText('Loading...')).not.toBeInTheDocument();
expect(screen.getByText(expectedError)).toBeInTheDocument(); await expect.element(page.getByText(expectedError)).toBeInTheDocument();
expect(screen.queryByText('ShlinkWebComponent')).not.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' })); setUp(fromPartial({ version: '3.0.0' }));
expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); await expect.element(page.getByText('Loading...')).not.toBeInTheDocument();
expect(screen.getByText('ShlinkWebComponent')).toBeInTheDocument(); await expect.element(page.getByText('ShlinkWebComponent')).toBeInTheDocument();
}); });
}); });

View File

@@ -13,22 +13,22 @@ describe('<ShlinkLogo />', () => {
[undefined, brandColor()], [undefined, brandColor()],
['red', 'red'], ['red', 'red'],
['white', 'white'], ['white', 'white'],
])('renders expected color', (color, expectedColor) => { ])('renders expected color', async (color, expectedColor) => {
const { container } = setUp({ color }); const { container } = setUp({ color });
expect(container.querySelector('g')).toHaveAttribute('fill', expectedColor); await expect.element(container.querySelector('g')).toHaveAttribute('fill', expectedColor);
}); });
it.each([ it.each([
[undefined, undefined], [undefined, undefined],
['foo', 'foo'], ['foo', 'foo'],
['bar', 'bar'], ['bar', 'bar'],
])('renders expected class', (className, expectedClassName) => { ])('renders expected class', async (className, expectedClassName) => {
const { container } = setUp({ className }); const { container } = setUp({ className });
if (expectedClassName) { if (expectedClassName) {
expect(container.firstChild).toHaveAttribute('class', expectedClassName); await expect.element(container.firstChild as HTMLElement).toHaveAttribute('class', expectedClassName);
} else { } else {
expect(container.firstChild).not.toHaveAttribute('class'); await expect.element(container.firstChild as HTMLElement).not.toHaveAttribute('class');
} }
}); });
}); });

View File

@@ -13,7 +13,7 @@ describe('context', () => {
} }
it('throws when used outside of ContainerProvider', () => { it('throws when used outside of ContainerProvider', () => {
expect(() => render(<TestComponent name="foo" />)).toThrowError( expect(() => render(<TestComponent name="foo" />)).toThrow(
'You cannot use "useDependencies" outside of a ContainerProvider', 'You cannot use "useDependencies" outside of a ContainerProvider',
); );
}); });
@@ -25,7 +25,7 @@ describe('context', () => {
<TestComponent name="foo" /> <TestComponent name="foo" />
</ContainerProvider>, </ContainerProvider>,
), ),
).toThrowError('Dependency with name "foo" not found in container'); ).toThrow('Dependency with name "foo" not found in container');
}); });
it('gets dependency from container', () => { it('gets dependency from container', () => {

View File

@@ -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 { fromPartial } from '@total-typescript/shoehorn';
import { createMemoryHistory } from 'history'; import { createMemoryHistory } from 'history';
import { Router } from 'react-router'; import { Router } from 'react-router';
import { page } from 'vitest/browser';
import { CreateServer } from '../../src/servers/CreateServer'; import { CreateServer } from '../../src/servers/CreateServer';
import type { ServersMap } from '../../src/servers/data'; import type { ServersMap } from '../../src/servers/data';
import { checkAccessibility } from '../__helpers__/accessibility'; import { checkAccessibility } from '../__helpers__/accessibility';
@@ -41,35 +42,39 @@ describe('<CreateServer />', () => {
it('passes a11y checks', () => checkAccessibility(setUp())); 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 }); setUp({ serversImported: true });
expect(screen.getByText('Servers properly imported. You can now select one from the list :)')).toBeInTheDocument(); await expect
expect( .element(page.getByText('Servers properly imported. You can now select one from the list :)'))
screen.queryByText('The servers could not be imported. Make sure the format is correct.'), .toBeInTheDocument();
).not.toBeInTheDocument(); await expect
expect(screen.queryByText('ImportServersBtn')).not.toBeInTheDocument(); .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 }); setUp({ importFailed: true });
expect( await expect
screen.queryByText('Servers properly imported. You can now select one from the list :)'), .element(page.getByText('Servers properly imported. You can now select one from the list :)'))
).not.toBeInTheDocument(); .not.toBeInTheDocument();
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();
}); });
it('creates server data when form is submitted', async () => { it('creates server data when form is submitted', async () => {
const { user, history, store } = setUp(); const { user, history, store } = setUp();
const expectedServerId = 'the_name-the_url.com'; const expectedServerId = 'the_name-the_url.com';
await user.type(screen.getByLabelText(/^Name/), 'the_name'); await user.type(page.getByLabelText(/^Name/), 'the_name');
await user.type(screen.getByLabelText(/^URL/), 'https://the_url.com'); await user.type(page.getByLabelText(/^URL/), 'https://the_url.com');
await user.type(screen.getByLabelText(/^API key/), 'the_api_key'); await user.type(page.getByLabelText(/^API key/), 'the_api_key');
expect(store.getState().servers[expectedServerId]).not.toBeDefined(); 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(store.getState().servers[expectedServerId]).toEqual(
expect.objectContaining({ expect.objectContaining({
id: expectedServerId, id: expectedServerId,
@@ -80,20 +85,20 @@ describe('<CreateServer />', () => {
); );
expect(history.location.pathname).toEqual(`/server/${expectedServerId}`); 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 () => { it('displays dialog when trying to create a duplicated server', async () => {
const { user, history } = setUp(); const { user, history } = setUp();
await user.type(screen.getByLabelText(/^Name/), 'the_name'); await user.type(page.getByLabelText(/^Name/), 'the_name');
await user.type(screen.getByLabelText(/^URL/), 'https://existing_url.com'); await user.type(page.getByLabelText(/^URL/), 'https://existing_url.com');
await user.type(screen.getByLabelText(/^API key/), 'existing_api_key'); 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 waitFor(() => expect(page.getByRole('dialog')).toBeInTheDocument());
await user.click(screen.getByRole('button', { name: 'Discard' })); await user.click(page.getByRole('button', { name: 'Discard' }));
expect(history.location.pathname).toEqual('/foo'); // Goes back to first route from history's initialEntries expect(history.location.pathname).toEqual('/foo'); // Goes back to first route from history's initialEntries
}); });

View File

@@ -1,8 +1,8 @@
import { screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { createMemoryHistory } from 'history'; import { createMemoryHistory } from 'history';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { Router } from 'react-router'; import { Router } from 'react-router';
import { page } from 'vitest/browser';
import { DeleteServerButton } from '../../src/servers/DeleteServerButton'; import { DeleteServerButton } from '../../src/servers/DeleteServerButton';
import { checkAccessibility } from '../__helpers__/accessibility'; import { checkAccessibility } from '../__helpers__/accessibility';
import { renderWithStore } from '../__helpers__/setUpTest'; import { renderWithStore } from '../__helpers__/setUpTest';
@@ -21,8 +21,9 @@ describe('<DeleteServerButton />', () => {
it('passes a11y checks', () => checkAccessibility(setUp('Delete me'))); 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); const { container } = setUp(children);
expect(container.firstChild).toBeTruthy(); expect(container.firstChild).toBeTruthy();
expect(container.firstChild).toMatchSnapshot(); expect(container.firstChild).toMatchSnapshot();
}); });
@@ -30,19 +31,19 @@ describe('<DeleteServerButton />', () => {
it('displays modal when button is clicked', async () => { it('displays modal when button is clicked', async () => {
const { user } = setUp(); const { user } = setUp();
expect(screen.queryByText(/Are you sure you want to remove/)).not.toBeInTheDocument(); await expect.element(page.getByText(/Are you sure you want to remove/)).not.toBeInTheDocument();
await user.click(screen.getByText('Remove this server')); await user.click(page.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/)).toBeInTheDocument();
}); });
it('navigates to home when deletion is confirmed', async () => { it('navigates to home when deletion is confirmed', async () => {
const { user, history } = setUp(); const { user, history } = setUp();
// Open modal // Open modal
await user.click(screen.getByText('Remove this server')); await user.click(page.getByText('Remove this server'));
expect(history.location.pathname).toEqual('/foo'); 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('/'); expect(history.location.pathname).toEqual('/');
}); });
}); });

View File

@@ -1,5 +1,5 @@
import { screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { page } from 'vitest/browser';
import type { ServerWithId } from '../../src/servers/data'; import type { ServerWithId } from '../../src/servers/data';
import { DeleteServerModal } from '../../src/servers/DeleteServerModal'; import { DeleteServerModal } from '../../src/servers/DeleteServerModal';
import { checkAccessibility } from '../__helpers__/accessibility'; import { checkAccessibility } from '../__helpers__/accessibility';
@@ -18,28 +18,28 @@ describe('<DeleteServerModal />', () => {
it('passes a11y checks', () => checkAccessibility(setUp())); it('passes a11y checks', () => checkAccessibility(setUp()));
it('renders a modal window', () => { it('renders a modal window', async () => {
setUp(); setUp();
expect(screen.getByRole('dialog')).toBeInTheDocument(); await expect.element(page.getByRole('dialog')).toBeInTheDocument();
expect(screen.getByRole('heading')).toHaveTextContent('Remove server'); 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(); setUp();
expect(screen.getByText(/^Are you sure you want to remove/)).toBeInTheDocument(); await expect.element(page.getByText(/^Are you sure you want to remove/)).toBeInTheDocument();
expect(screen.getByText(serverName)).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', 'closes dialog when clicking cancel button',
async (getButton) => { async (getButton) => {
const { user, store } = setUp(); const { user, store } = setUp();
expect(screen.getByRole('dialog')).toBeInTheDocument(); await expect.element(page.getByRole('dialog')).toBeInTheDocument();
await user.click(getButton()); await user.click(getButton());
expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); await expect.element(page.getByRole('dialog')).not.toBeInTheDocument();
// No server has been deleted // No server has been deleted
expect(Object.keys(store.getState().servers)).toHaveLength(1); expect(Object.keys(store.getState().servers)).toHaveLength(1);
@@ -50,7 +50,7 @@ describe('<DeleteServerModal />', () => {
const { user, store } = setUp(); const { user, store } = setUp();
expect(Object.keys(store.getState().servers)).toHaveLength(1); 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); expect(Object.keys(store.getState().servers)).toHaveLength(0);
}); });
}); });

View File

@@ -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 { fromPartial } from '@total-typescript/shoehorn';
import { createMemoryHistory } from 'history'; import { createMemoryHistory } from 'history';
import { Router } from 'react-router'; import { Router } from 'react-router';
import { page } from 'vitest/browser';
import type { ReachableServer, SelectedServer } from '../../src/servers/data'; import type { ReachableServer, SelectedServer } from '../../src/servers/data';
import { isServerWithId } from '../../src/servers/data'; import { isServerWithId } from '../../src/servers/data';
import { EditServer } from '../../src/servers/EditServer'; import { EditServer } from '../../src/servers/EditServer';
@@ -35,35 +36,35 @@ describe('<EditServer />', () => {
it('passes a11y checks', () => checkAccessibility(setUp())); 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<SelectedServer>({})); setUp(fromPartial<SelectedServer>({}));
expect(screen.queryByText('Edit')).not.toBeInTheDocument(); await expect.element(page.getByText('Edit')).not.toBeInTheDocument();
expect(screen.queryByText('Cancel')).not.toBeInTheDocument(); await expect.element(page.getByText('Cancel')).not.toBeInTheDocument();
expect(screen.queryByText('Save')).not.toBeInTheDocument(); await expect.element(page.getByText('Save')).not.toBeInTheDocument();
}); });
it('renders server title', () => { it('renders server title', () => {
setUp(); 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(); setUp();
expect(screen.getByLabelText(/^Name/)).toBeInTheDocument(); await expect.element(page.getByLabelText(/^Name/)).toBeInTheDocument();
expect(screen.getByLabelText(/^URL/)).toBeInTheDocument(); await expect.element(page.getByLabelText(/^URL/)).toBeInTheDocument();
expect(screen.getByLabelText(/^API key/)).toBeInTheDocument(); await expect.element(page.getByLabelText(/^API key/)).toBeInTheDocument();
}); });
it('edits server and redirects to it when form is submitted', async () => { it('edits server and redirects to it when form is submitted', async () => {
const { user, history, store } = setUp(); const { user, history, store } = setUp();
await user.type(screen.getByLabelText(/^Name/), ' edited'); await user.type(page.getByLabelText(/^Name/), ' edited');
await user.type(screen.getByLabelText(/^URL/), ' edited'); await user.type(page.getByLabelText(/^URL/), ' edited');
// TODO Using fire event because userEvent.click on the Submit button does not submit the form // TODO Using fire event because userEvent.click on the Submit button does not submit the form
// await user.click(screen.getByRole('button', { name: 'Save' })); // 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(store.getState().servers[defaultSelectedServer.id]).toEqual(
expect.objectContaining({ expect.objectContaining({
@@ -81,9 +82,9 @@ describe('<EditServer />', () => {
async ({ forwardCredentials }) => { async ({ forwardCredentials }) => {
const { user, store } = setUp({ ...defaultSelectedServer, forwardCredentials }); const { user, store } = setUp({ ...defaultSelectedServer, forwardCredentials });
await user.click(screen.getByText('Advanced options')); await user.click(page.getByText('Advanced options'));
await user.click(screen.getByLabelText('Forward credentials to this server on every request.')); await user.click(page.getByLabelText('Forward credentials to this server on every request.'));
fireEvent.submit(screen.getByRole('form')); fireEvent.submit(page.getByTestId('server-form').element());
await waitFor(() => await waitFor(() =>
expect(store.getState().servers[defaultSelectedServer.id]).toEqual( expect(store.getState().servers[defaultSelectedServer.id]).toEqual(

View File

@@ -1,6 +1,7 @@
import { screen, waitFor } from '@testing-library/react'; import { waitFor } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router'; import { MemoryRouter } from 'react-router';
import { page } from 'vitest/browser';
import type { ServersMap, ServerWithId } from '../../src/servers/data'; import type { ServersMap, ServerWithId } from '../../src/servers/data';
import { ManageServers } from '../../src/servers/ManageServers'; import { ManageServers } from '../../src/servers/ManageServers';
import type { ServersExporter } from '../../src/servers/services/ServersExporter'; import type { ServersExporter } from '../../src/servers/services/ServersExporter';
@@ -39,71 +40,70 @@ describe('<ManageServers />', () => {
baz: createServerMock('baz'), baz: createServerMock('baz'),
}); });
const search = async (searchTerm: string) => { const search = async (searchTerm: string) => {
await user.clear(screen.getByPlaceholderText('Search...')); await user.clear(page.getByPlaceholder('Search...'));
await user.type(screen.getByPlaceholderText('Search...'), searchTerm); await user.type(page.getByPlaceholder('Search...'), searchTerm);
}; };
// Add one for the header row // 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); expectRows(3);
expect(screen.queryByText('No servers found.')).not.toBeInTheDocument(); await expect.element(page.getByText('No servers found.')).not.toBeInTheDocument();
await search('foo'); await search('foo');
await waitFor(() => expectRows(1)); 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 search('Ba');
await waitFor(() => expectRows(2)); 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 search('invalid');
await waitFor(() => expectRows(1)); await waitFor(() => expectRows(1));
expect(screen.getByText('No servers found.')).toBeInTheDocument(); await expect.element(page.getByText('No servers found.')).toBeInTheDocument();
}); });
it.each([ it.only.each([[createServerMock('foo')], [createServerMock('foo', true)]])(
[createServerMock('foo'), 3], 'shows different amount of columns if there are at least one auto-connect server',
[createServerMock('foo', true), 4], async (server) => {
])('shows different amount of columns if there are at least one auto-connect server', (server, expectedCols) => {
setUp({ server }); setUp({ server });
expect(screen.getAllByRole('columnheader')).toHaveLength(expectedCols);
if (server.autoConnect) { if (server.autoConnect) {
expect(screen.getByTestId('auto-connect')).toBeInTheDocument(); await expect.element(page.getByTestId('auto-connect')).toBeInTheDocument();
} else { } else {
expect(screen.queryByTestId('auto-connect')).not.toBeInTheDocument(); await expect.element(page.getByTestId('auto-connect')).not.toBeInTheDocument();
} }
}); },
);
it.each([ it.each([
[{}, 0], [{}, 0],
[{ foo: createServerMock('foo') }, 1], [{ foo: createServerMock('foo') }, 1],
])('shows export button if the list of servers is not empty', (servers, expectedButtons) => { ])('shows export button if the list of servers is not empty', (servers, expectedButtons) => {
setUp(servers); 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 () => { it('allows exporting servers when clicking on button', async () => {
const { user } = setUp({ foo: createServerMock('foo') }); const { user } = setUp({ foo: createServerMock('foo') });
expect(exportServers).not.toHaveBeenCalled(); 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(); 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()]); useTimeoutToggle.mockReturnValue([hasError, vi.fn()]);
setUp({ foo: createServerMock('foo') }); setUp({ foo: createServerMock('foo') });
if (hasError) { if (hasError) {
expect( await expect
screen.getByText('The servers could not be imported. Make sure the format is correct.'), .element(page.getByText('The servers could not be imported. Make sure the format is correct.'))
).toBeInTheDocument(); .toBeInTheDocument();
} else { } else {
expect( await expect
screen.queryByText('The servers could not be imported. Make sure the format is correct.'), .element(page.getByText('The servers could not be imported. Make sure the format is correct.'))
).not.toBeInTheDocument(); .not.toBeInTheDocument();
} }
}); });
}); });

View File

@@ -1,6 +1,6 @@
import { Table } from '@shlinkio/shlink-frontend-kit'; import { Table } from '@shlinkio/shlink-frontend-kit';
import { screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router'; import { MemoryRouter } from 'react-router';
import { page } from 'vitest/browser';
import type { ServerWithId } from '../../src/servers/data'; import type { ServerWithId } from '../../src/servers/data';
import { ManageServersRow } from '../../src/servers/ManageServersRow'; import { ManageServersRow } from '../../src/servers/ManageServersRow';
import { checkAccessibility } from '../__helpers__/accessibility'; import { checkAccessibility } from '../__helpers__/accessibility';
@@ -29,12 +29,12 @@ describe('<ManageServersRow />', () => {
[false, 3], [false, 3],
])('renders expected amount of columns', (hasAutoConnect, expectedCols) => { ])('renders expected amount of columns', (hasAutoConnect, expectedCols) => {
setUp(hasAutoConnect); 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(); 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) => { it.each([[true], [false]])('renders auto-connect icon only if server is autoConnect', (autoConnect) => {
@@ -42,13 +42,13 @@ describe('<ManageServersRow />', () => {
expect(container).toMatchSnapshot(); expect(container).toMatchSnapshot();
}); });
it('renders server props where appropriate', () => { it('renders server props where appropriate', async () => {
setUp(); setUp();
const link = screen.getByRole('link'); const link = page.getByRole('link');
expect(link).toHaveAttribute('href', `/server/${server.id}`); await expect.element(link).toHaveAttribute('href', `/server/${server.id}`);
expect(link).toHaveTextContent(server.name); await expect.element(link).toHaveTextContent(server.name);
expect(screen.getByText(server.url)).toBeInTheDocument(); await expect.element(page.getByText(server.url)).toBeInTheDocument();
}); });
}); });

View File

@@ -1,6 +1,6 @@
import { screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router'; import { MemoryRouter } from 'react-router';
import { page } from 'vitest/browser';
import type { UserEvent } from 'vitest/browser'; import type { UserEvent } from 'vitest/browser';
import type { ServerWithId } from '../../src/servers/data'; import type { ServerWithId } from '../../src/servers/data';
import { ManageServersRowDropdown } from '../../src/servers/ManageServersRowDropdown'; import { ManageServersRowDropdown } from '../../src/servers/ManageServersRowDropdown';
@@ -21,7 +21,7 @@ describe('<ManageServersRowDropdown />', () => {
}, },
); );
}; };
const toggleDropdown = (user: UserEvent) => user.click(screen.getByRole('button')); const toggleDropdown = (user: UserEvent) => user.click(page.getByRole('button'));
it('passes a11y checks', async () => { it('passes a11y checks', async () => {
const { user, container } = setUp(); const { user, container } = setUp();
@@ -34,20 +34,24 @@ describe('<ManageServersRowDropdown />', () => {
it('renders expected amount of dropdown items', async () => { it('renders expected amount of dropdown items', async () => {
const { user } = setUp(); const { user } = setUp();
expect(screen.queryByRole('menu')).not.toBeInTheDocument(); await expect.element(page.getByRole('menu')).not.toBeInTheDocument();
await toggleDropdown(user); await toggleDropdown(user);
expect(screen.getByRole('menu')).toBeInTheDocument(); await expect.element(page.getByRole('menu')).toBeInTheDocument();
expect(screen.getAllByRole('menuitem')).toHaveLength(4); expect(page.getByRole('menuitem').elements()).toHaveLength(4);
expect(screen.getByRole('menuitem', { name: 'Connect' })).toHaveAttribute('href', '/server/abc123'); await expect
expect(screen.getByRole('menuitem', { name: 'Edit server' })).toHaveAttribute('href', '/server/abc123/edit'); .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) => { it.each([true, false])('allows toggling auto-connect', async (autoConnect) => {
const { user, store } = setUp(autoConnect); const { user, store } = setUp(autoConnect);
await toggleDropdown(user); 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); expect(Object.values(store.getState().servers)[0].autoConnect).toEqual(!autoConnect);
}); });
@@ -55,12 +59,12 @@ describe('<ManageServersRowDropdown />', () => {
it('renders deletion modal', async () => { it('renders deletion modal', async () => {
const { user } = setUp(); const { user } = setUp();
expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); expect(page.getByRole('dialog')).not.toBeInTheDocument();
await toggleDropdown(user); 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) => { it.each([[true], [false]])('renders expected size and icon', (autoConnect) => {

View File

@@ -1,6 +1,6 @@
import { screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router'; import { MemoryRouter } from 'react-router';
import { page } from 'vitest/browser';
import type { ServersMap } from '../../src/servers/data'; import type { ServersMap } from '../../src/servers/data';
import { ServersDropdown } from '../../src/servers/ServersDropdown'; import { ServersDropdown } from '../../src/servers/ServersDropdown';
import { checkAccessibility } from '../__helpers__/accessibility'; import { checkAccessibility } from '../__helpers__/accessibility';
@@ -27,7 +27,7 @@ describe('<ServersDropdown />', () => {
it('passes a11y checks', async () => { it('passes a11y checks', async () => {
const { user, ...rest } = setUp(); const { user, ...rest } = setUp();
// Open menu // Open menu
await user.click(screen.getByText('Servers')); await user.click(page.getByText('Servers'));
return checkAccessibility(rest); return checkAccessibility(rest);
}); });
@@ -35,33 +35,35 @@ describe('<ServersDropdown />', () => {
it('contains the list of servers and the "mange servers" button', async () => { it('contains the list of servers and the "mange servers" button', async () => {
const { user } = setUp(); const { user } = setUp();
await user.click(screen.getByText('Servers')); await user.click(page.getByText('Servers'));
const items = screen.getAllByRole('menuitem'); const items = page.getByRole('menuitem').elements();
// We have to add two for the "Manage servers" and the "Settings" menu items // We have to add two for the "Manage servers" and the "Settings" menu items
expect(items).toHaveLength(Object.values(fallbackServers).length + 2); expect(items).toHaveLength(Object.values(fallbackServers).length + 2);
expect(items[1]).toHaveTextContent('foo'); await expect.element(items[1]).toHaveTextContent('foo');
expect(items[2]).toHaveTextContent('bar'); await expect.element(items[2]).toHaveTextContent('bar');
expect(items[3]).toHaveTextContent('baz'); await expect.element(items[3]).toHaveTextContent('baz');
expect(items[4]).toHaveTextContent('Manage servers'); await expect.element(items[4]).toHaveTextContent('Manage servers');
}); });
it('contains a toggle with proper text', () => { it('contains a toggle with proper text', async () => {
setUp(); setUp();
expect(screen.getByRole('button')).toHaveTextContent('Servers'); await expect.element(page.getByRole('button')).toHaveTextContent('Servers');
}); });
it('contains a button to manage servers', async () => { it('contains a button to manage servers', async () => {
const { user } = setUp(); const { user } = setUp();
await user.click(screen.getByText('Servers')); await user.click(page.getByText('Servers'));
expect(screen.getByRole('menuitem', { name: 'Manage servers' })).toHaveAttribute('href', '/manage-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 () => { it('shows only create link when no servers exist yet', async () => {
const { user } = setUp({}); const { user } = setUp({});
await user.click(screen.getByText('Servers')); await user.click(page.getByText('Servers'));
expect(screen.getByRole('menuitem', { name: 'Add a server' })).toBeInTheDocument(); await expect.element(page.getByRole('menuitem', { name: 'Add a server' })).toBeInTheDocument();
}); });
}); });

View File

@@ -26,14 +26,14 @@ describe('<ServersListGroup />', () => {
expect(screen.queryAllByRole('link')).toHaveLength(servers.length); 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 }); setUp({ servers, borderless });
const list = screen.getByTestId('list'); const list = screen.getByTestId('list');
if (!borderless) { if (!borderless) {
expect(list).toHaveClass('border-y'); await expect.element(list).toHaveClass('border-y');
} else { } else {
expect(list).not.toHaveClass('border-y'); await expect.element(list).not.toHaveClass('border-y');
} }
}); });
}); });

View File

@@ -1,5 +1,5 @@
import { act, screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { page } from 'vitest/browser';
import type { ServerData } from '../../../src/servers/data'; import type { ServerData } from '../../../src/servers/data';
import { DuplicatedServersModal } from '../../../src/servers/helpers/DuplicatedServersModal'; import { DuplicatedServersModal } from '../../../src/servers/helpers/DuplicatedServersModal';
import { checkAccessibility } from '../../__helpers__/accessibility'; import { checkAccessibility } from '../../__helpers__/accessibility';
@@ -9,10 +9,8 @@ describe('<DuplicatedServersModal />', () => {
const onClose = vi.fn(); const onClose = vi.fn();
const onConfirm = vi.fn(); const onConfirm = vi.fn();
const setUp = (duplicatedServers: ServerData[] = []) => const setUp = (duplicatedServers: ServerData[] = []) =>
act(() =>
renderWithEvents( renderWithEvents(
<DuplicatedServersModal open duplicatedServers={duplicatedServers} onClose={onClose} onConfirm={onConfirm} />, <DuplicatedServersModal open duplicatedServers={duplicatedServers} onClose={onClose} onConfirm={onConfirm} />,
),
); );
const mockServer = (data: Partial<ServerData> = {}) => fromPartial<ServerData>(data); const mockServer = (data: Partial<ServerData> = {}) => fromPartial<ServerData>(data);
@@ -24,9 +22,9 @@ describe('<DuplicatedServersModal />', () => {
[[mockServer(), mockServer()], 2], [[mockServer(), mockServer()], 2],
[[mockServer(), mockServer(), mockServer()], 3], [[mockServer(), mockServer(), mockServer()], 3],
[[mockServer(), mockServer(), mockServer(), mockServer()], 4], [[mockServer(), mockServer(), mockServer(), mockServer()], 4],
])('renders expected amount of items', async (duplicatedServers, expectedItems) => { ])('renders expected amount of items', (duplicatedServers, expectedItems) => {
await setUp(duplicatedServers); setUp(duplicatedServers);
expect(screen.queryAllByRole('listitem')).toHaveLength(expectedItems); expect(page.getByRole('listitem')).toHaveLength(expectedItems);
}); });
it.each([ it.each([
@@ -51,13 +49,13 @@ describe('<DuplicatedServersModal />', () => {
}, },
], ],
])('renders expected texts based on amount of servers', async (duplicatedServers, assertions) => { ])('renders expected texts based on amount of servers', async (duplicatedServers, assertions) => {
await setUp(duplicatedServers); setUp(duplicatedServers);
expect(screen.getByRole('heading')).toHaveTextContent(assertions.header); await expect.element(page.getByRole('heading')).toHaveTextContent(assertions.header);
expect(screen.getByText(assertions.firstParagraph)).toBeInTheDocument(); await expect.element(page.getByText(assertions.firstParagraph)).toBeInTheDocument();
expect(screen.getByText(assertions.lastParagraph)).toBeInTheDocument(); await expect.element(page.getByText(assertions.lastParagraph)).toBeInTheDocument();
expect(screen.getByRole('button', { name: assertions.discardBtn })).toBeInTheDocument(); await expect.element(page.getByRole('button', { name: assertions.discardBtn })).toBeInTheDocument();
expect(screen.getByRole('button', { name: assertions.confirmButton })).toBeInTheDocument(); await expect.element(page.getByRole('button', { name: assertions.confirmButton })).toBeInTheDocument();
}); });
it.each([ it.each([
@@ -65,37 +63,42 @@ describe('<DuplicatedServersModal />', () => {
[[mockServer({ url: 'url', apiKey: 'apiKey' })]], [[mockServer({ url: 'url', apiKey: 'apiKey' })]],
[[mockServer({ url: 'url_1', apiKey: 'apiKey_1' }), mockServer({ url: 'url_2', apiKey: 'apiKey_2' })]], [[mockServer({ url: 'url_1', apiKey: 'apiKey_1' }), mockServer({ url: 'url_2', apiKey: 'apiKey_2' })]],
])('displays provided server data', async (duplicatedServers) => { ])('displays provided server data', async (duplicatedServers) => {
await setUp(duplicatedServers); setUp(duplicatedServers);
if (duplicatedServers.length === 0) { if (duplicatedServers.length === 0) {
expect(screen.queryByRole('listitem')).not.toBeInTheDocument(); await expect.element(page.getByRole('listitem')).not.toBeInTheDocument();
} else if (duplicatedServers.length === 1) { } 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}`); await expect.element(firstItem).toHaveTextContent(`URL: ${duplicatedServers[0].url}`);
expect(secondItem).toHaveTextContent(`API key: ${duplicatedServers[0].apiKey}`); await expect.element(secondItem).toHaveTextContent(`API key: ${duplicatedServers[0].apiKey}`);
} else { } else {
expect.assertions(duplicatedServers.length); expect.assertions(duplicatedServers.length);
screen.getAllByRole('listitem').forEach((item, index) => { await Promise.all(
page
.getByRole('listitem')
.elements()
.map((item, index) => {
const server = duplicatedServers[index]; const server = duplicatedServers[index];
expect(item).toHaveTextContent(`${server.url} - ${server.apiKey}`); return expect.element(item).toHaveTextContent(`${server.url} - ${server.apiKey}`);
}); }),
);
} }
}); });
it('invokes onClose when appropriate button is clicked', async () => { it('invokes onClose when appropriate button is clicked', async () => {
const { user } = await setUp(); const { user } = setUp();
expect(onClose).not.toHaveBeenCalled(); expect(onClose).not.toHaveBeenCalled();
await user.click(screen.getByRole('button', { name: 'Discard' })); await user.click(page.getByRole('button', { name: 'Discard' }));
expect(onClose).toHaveBeenCalled(); expect(onClose).toHaveBeenCalled();
}); });
it('invokes onConfirm when appropriate button is clicked', async () => { it('invokes onConfirm when appropriate button is clicked', async () => {
const { user } = await setUp(); const { user } = setUp();
expect(onConfirm).not.toHaveBeenCalled(); 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(); expect(onConfirm).toHaveBeenCalled();
}); });
}); });

View File

@@ -1,5 +1,5 @@
import { screen, waitFor } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn'; import { fromPartial } from '@total-typescript/shoehorn';
import { page } from 'vitest/browser';
import type { ServerData, ServersMap, ServerWithId } from '../../../src/servers/data'; import type { ServerData, ServersMap, ServerWithId } from '../../../src/servers/data';
import type { ImportServersBtnProps } from '../../../src/servers/helpers/ImportServersBtn'; import type { ImportServersBtnProps } from '../../../src/servers/helpers/ImportServersBtn';
import { ImportServersBtn } from '../../../src/servers/helpers/ImportServersBtn'; import { ImportServersBtn } from '../../../src/servers/helpers/ImportServersBtn';
@@ -22,35 +22,33 @@ describe('<ImportServersBtn />', () => {
it('shows tooltip on button hover', async () => { it('shows tooltip on button hover', async () => {
const { user } = setUp(); const { user } = setUp();
expect(screen.queryByText(/^You can create servers by importing a CSV file/)).not.toBeInTheDocument(); await expect.element(page.getByText('You can create servers by importing a CSV file')).not.toBeInTheDocument();
await user.hover(screen.getByRole('button')); await user.hover(page.getByRole('button'));
await waitFor(() => await expect.element(page.getByText('You can create servers by importing a CSV file')).toBeInTheDocument();
expect(screen.getByText(/^You can create servers by importing a CSV file/)).toBeInTheDocument(),
);
}); });
it.each([ it.each([
[undefined, ''], [undefined, ''],
['foo', 'foo'], ['foo', 'foo'],
['bar', 'bar'], ['bar', 'bar'],
])('allows a class name to be provided', (providedClassName, expectedClassName) => { ])('allows a class name to be provided', async (providedClassName, expectedClassName) => {
setUp({ className: providedClassName }); 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([ it.each([
[undefined, 'Import from file'], [undefined, 'Import from file'],
['foo', 'foo'], ['foo', 'foo'],
['bar', 'bar'], ['bar', 'bar'],
])('has expected text', (children, expectedText) => { ])('has expected text', async (children, expectedText) => {
setUp({ children }); setUp({ children });
expect(screen.getByRole('button')).toHaveTextContent(expectedText); await expect.element(page.getByRole('button')).toHaveTextContent(expectedText);
}); });
it('imports servers when file input changes', async () => { it('imports servers when file input changes', async () => {
const { user } = setUp(); 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); expect(importServersFromFile).toHaveBeenCalledTimes(1);
}); });
@@ -74,15 +72,15 @@ describe('<ImportServersBtn />', () => {
importServersFromFile.mockResolvedValue([existingServerData, newServer]); importServersFromFile.mockResolvedValue([existingServerData, newServer]);
expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); await expect.element(page.getByRole('dialog')).not.toBeInTheDocument();
await user.upload(screen.getByTestId('csv-file-input'), csvFile); await user.upload(page.getByTestId('csv-file-input'), csvFile);
// Once the file is uploaded, non-duplicated servers are immediately created // Once the file is uploaded, non-duplicated servers are immediately created
const { servers } = store.getState(); const { servers } = store.getState();
expect(Object.keys(servers)).toHaveLength(2); expect(Object.keys(servers)).toHaveLength(2);
expect(screen.getByRole('dialog')).toBeInTheDocument(); await expect.element(page.getByRole('dialog')).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: btnName })); await user.click(page.getByRole('button', { name: btnName }));
// If duplicated servers are saved, there's one extra server creation // If duplicated servers are saved, there's one extra server creation
if (savesDuplicatedServers) { if (savesDuplicatedServers) {

View File

@@ -45,10 +45,10 @@ describe('<ServerError />', () => {
notFound: ['Could not find this Shlink server.'], 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); setUp(selectedServer);
found.forEach((text) => expect(screen.getByText(text)).toBeInTheDocument()); await Promise.all(found.map((text) => expect.element(screen.getByText(text)).toBeInTheDocument()));
notFound.forEach((text) => expect(screen.queryByText(text)).not.toBeInTheDocument()); await Promise.all(notFound.map((text) => expect.element(screen.queryByText(text)).not.toBeInTheDocument()));
}); });
}); });

View File

@@ -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 { ServerForm } from '../../../src/servers/helpers/ServerForm';
import { checkAccessibility } from '../../__helpers__/accessibility'; import { checkAccessibility } from '../../__helpers__/accessibility';
import { renderWithEvents } from '../../__helpers__/setUpTest'; import { renderWithEvents } from '../../__helpers__/setUpTest';
@@ -9,21 +10,21 @@ describe('<ServerForm />', () => {
it('passes a11y checks', () => checkAccessibility(setUp())); it('passes a11y checks', () => checkAccessibility(setUp()));
it('renders inputs', () => { it('renders inputs', async () => {
setUp(); setUp();
expect(screen.getByLabelText(/^Name/)).toBeInTheDocument(); await expect.element(page.getByLabelText(/^Name/)).toBeInTheDocument();
expect(screen.getByLabelText(/^URL/)).toBeInTheDocument(); await expect.element(page.getByLabelText(/^URL/)).toBeInTheDocument();
expect(screen.getByLabelText(/^API key/)).toBeInTheDocument(); await expect.element(page.getByLabelText(/^API key/)).toBeInTheDocument();
expect(screen.getByText('Something')).toBeInTheDocument(); await expect.element(page.getByText('Something')).toBeInTheDocument();
expect(screen.getByText('Advanced options')).toBeInTheDocument(); await expect.element(page.getByText('Advanced options')).toBeInTheDocument();
}); });
it('invokes submit callback when submit event is triggered', async () => { it('invokes submit callback when submit event is triggered', async () => {
setUp(); setUp();
expect(onSubmit).not.toHaveBeenCalled(); 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(); expect(onSubmit).toHaveBeenCalled();
}); });
@@ -31,8 +32,8 @@ describe('<ServerForm />', () => {
const { user } = setUp(); const { user } = setUp();
const forwardCredentialsLabel = 'Forward credentials to this server on every request.'; const forwardCredentialsLabel = 'Forward credentials to this server on every request.';
expect(screen.queryByLabelText(forwardCredentialsLabel)).not.toBeInTheDocument(); await expect.element(page.getByLabelText(forwardCredentialsLabel)).not.toBeInTheDocument();
await user.click(screen.getByText('Advanced options')); await user.click(page.getByText('Advanced options'));
expect(screen.getByLabelText(forwardCredentialsLabel)).toBeInTheDocument(); await expect.element(page.getByLabelText(forwardCredentialsLabel)).toBeInTheDocument();
}); });
}); });

View File

@@ -39,6 +39,9 @@ export default defineConfig({
// Vitest config // Vitest config
test: { test: {
globals: true,
clearMocks: true,
// Run tests in an actual browser // Run tests in an actual browser
browser: { browser: {
provider: playwright({ provider: playwright({
@@ -50,8 +53,6 @@ export default defineConfig({
screenshotFailures: false, screenshotFailures: false,
instances: [{ browser: 'chromium' }], instances: [{ browser: 'chromium' }],
}, },
globals: true,
setupFiles: './config/test/setupTests.ts',
coverage: { coverage: {
provider: 'v8', provider: 'v8',
reporter: ['text', 'text-summary', 'html', 'clover'], reporter: ['text', 'text-summary', 'html', 'clover'],