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,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('<AppUpdateBanner />', () => {
const onClose = vi.fn();
const forceUpdate = vi.fn();
const setUp = async () => {
const result = await act(() =>
renderWithEvents(<AppUpdateBanner isOpen onClose={onClose} forceUpdate={forceUpdate} />),
);
await waitFor(() => screen.getByRole('alert'));
return result;
};
const setUp = () => renderWithEvents(<AppUpdateBanner isOpen onClose={onClose} forceUpdate={forceUpdate} />);
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();
});
});

View File

@@ -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('<ErrorHandler />', () => {
const reload = vi.fn();
const location = fromPartial<Window['location']>({ reload });
const cons = fromPartial<Console>({ error: vi.fn() });
const ErrorHandler = (props: PropsWithChildren) => <BaseErrorHandler console={cons} location={location} {...props} />;
const setUp = (children: ReactNode = 'Error') => renderWithEvents(<ErrorHandler>{children}</ErrorHandler>);
const setUp = (children: ReactNode = 'Error') =>
renderWithEvents(
<ErrorHandler console={cons} location={location}>
{children}
</ErrorHandler>,
);
beforeEach(() => {
vi.spyOn(console, 'error').mockImplementation(() => {}); // Silence react errors
@@ -22,26 +26,26 @@ describe('<ErrorHandler />', () => {
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>);
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(<ComponentWithError />);
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(<ComponentWithError />);
expect(reload).not.toHaveBeenCalled();
await user.click(screen.getByRole('button'));
await user.click(page.getByRole('button'));
expect(reload).toHaveBeenCalled();
});
});

View File

@@ -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('<Home />', () => {
it('passes a11y checks', () =>
checkAccessibility(setUp({ '1a': fromPartial<ServerWithId>({ 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('<Home />', () => {
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();
}
});
});

View File

@@ -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('<MainHeader />', () => {
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('<MainHeader />', () => {
['/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');
});
});

View File

@@ -13,29 +13,31 @@ describe('<NotFound />', () => {
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);
});
});

View File

@@ -13,8 +13,8 @@ describe('<ScrollToTop />', () => {
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();
});
});

View File

@@ -19,20 +19,18 @@ describe('<ShlinkVersions />', () => {
['not-semver', fromPartial<ReachableServer>({ 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('<ShlinkVersions />', () => {
['1.2.3', null],
['1.2.3', fromPartial<NotFoundServer>({ serverNotFound: 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 });
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');
});
});

View File

@@ -20,13 +20,13 @@ describe('<ShlinkVersionsContainer />', () => {
[null, false],
[fromPartial<SelectedServer>({}), false],
[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);
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)');
}
});
});

View File

@@ -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('<ShlinkWebComponentContainer />', () => {
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('<ShlinkWebComponentContainer />', () => {
fromPartial<NonReachableServer>({ 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();
});
});

View File

@@ -13,22 +13,22 @@ describe('<ShlinkLogo />', () => {
[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');
}
});
});