mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-12-10 14:16:59 -06:00
Run tests in a headless browser with vitest browser mode and playwright
This commit is contained in:
parent
09559c78af
commit
691e6c1afb
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@ -13,3 +13,4 @@ jobs:
|
||||
with:
|
||||
node-version: 22.x
|
||||
publish-coverage: true
|
||||
install-playwright: true
|
||||
|
||||
@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
* Update to `@shlinkio/shlink-web-component` 0.13.0
|
||||
* Update to `@shlinkio/shlink-js-sdk` 2.0.0
|
||||
* Add `eslint-plugin-react-compiler`
|
||||
* Run unit tests in a headless browser using vitest browser mode and playwright.
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
@ -1,33 +1,9 @@
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
import { cleanup } from '@testing-library/react';
|
||||
import axe from 'axe-core';
|
||||
import { afterEach } from 'vitest';
|
||||
|
||||
axe.configure({
|
||||
checks: [
|
||||
{
|
||||
// Disable color contrast checking, as it doesn't work in jsdom
|
||||
id: 'color-contrast',
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Clear all mocks and cleanup DOM after every test
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
cleanup();
|
||||
});
|
||||
|
||||
HTMLCanvasElement.prototype.getContext = (() => {}) as any;
|
||||
(global as any).scrollTo = () => {};
|
||||
(global as any).matchMedia = () => ({ matches: false });
|
||||
|
||||
HTMLDialogElement.prototype.showModal = function() {
|
||||
this.setAttribute('open', '');
|
||||
};
|
||||
HTMLDialogElement.prototype.close = function() {
|
||||
this.removeAttribute('open');
|
||||
this.dispatchEvent(new CloseEvent('close'));
|
||||
this.dispatchEvent(new CloseEvent('cancel'));
|
||||
};
|
||||
|
||||
8
dev.Dockerfile
Normal file
8
dev.Dockerfile
Normal file
@ -0,0 +1,8 @@
|
||||
FROM mcr.microsoft.com/playwright:v1.51.1-noble
|
||||
|
||||
ENV NODE_VERSION 22.14
|
||||
|
||||
# Install Node.js
|
||||
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.2/install.sh | bash && \
|
||||
\. "$HOME/.nvm/nvm.sh" && \
|
||||
nvm install ${NODE_VERSION}
|
||||
@ -2,7 +2,9 @@ services:
|
||||
shlink_web_client_node:
|
||||
container_name: shlink_web_client_node
|
||||
user: 1000:1000 # With this, files created via `indocker` script will belong to the host user
|
||||
image: node:22.12-alpine
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./dev.Dockerfile
|
||||
command: /bin/sh -c "cd /home/shlink/www && npm install && npm run start"
|
||||
volumes:
|
||||
- ./:/home/shlink/www
|
||||
|
||||
476
package-lock.json
generated
476
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -62,6 +62,7 @@
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"@vitest/browser": "^3.1.1",
|
||||
"@vitest/coverage-v8": "^3.1.1",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axe-core": "^4.10.3",
|
||||
@ -74,7 +75,7 @@
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"history": "^5.3.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"playwright": "^1.51.1",
|
||||
"sass": "^1.86.3",
|
||||
"tailwindcss": "^4.0.17",
|
||||
"typescript": "^5.8.3",
|
||||
|
||||
@ -42,7 +42,7 @@ export const ServersDropdown = ({ servers, selectedServer }: ServersDropdownProp
|
||||
<DropdownToggle nav caret>
|
||||
<FontAwesomeIcon icon={serverIcon} /> <span className="tw:ml-1">Servers</span>
|
||||
</DropdownToggle>
|
||||
<DropdownMenu end classNam="tw:right-0">{renderServers()}</DropdownMenu>
|
||||
<DropdownMenu end className="tw:right-0">{renderServers()}</DropdownMenu>
|
||||
</UncontrolledDropdown>
|
||||
);
|
||||
};
|
||||
|
||||
@ -96,6 +96,7 @@ const ImportServersBtn: FCWithDeps<ImportServersBtnConnectProps, ImportServersBt
|
||||
accept=".csv"
|
||||
className="tw:hidden"
|
||||
aria-hidden
|
||||
tabIndex={-1}
|
||||
ref={ref as any /* TODO Remove After updating to React 19 */}
|
||||
onChange={onFile}
|
||||
data-testid="csv-file-input"
|
||||
|
||||
@ -1,46 +0,0 @@
|
||||
import { DropdownBtn } from '@shlinkio/shlink-frontend-kit';
|
||||
import type { VisitsSettings } from '@shlinkio/shlink-web-component/settings';
|
||||
import type { FC } from 'react';
|
||||
import { DropdownItem } from 'reactstrap';
|
||||
|
||||
export type DateInterval = VisitsSettings['defaultInterval'];
|
||||
|
||||
export interface DateIntervalSelectorProps {
|
||||
active?: DateInterval;
|
||||
allText: string;
|
||||
onChange: (interval: DateInterval) => void;
|
||||
}
|
||||
|
||||
export const INTERVAL_TO_STRING_MAP: Record<Exclude<DateInterval, 'all'>, string> = {
|
||||
today: 'Today',
|
||||
yesterday: 'Yesterday',
|
||||
last7Days: 'Last 7 days',
|
||||
last30Days: 'Last 30 days',
|
||||
last90Days: 'Last 90 days',
|
||||
last180Days: 'Last 180 days',
|
||||
last365Days: 'Last 365 days',
|
||||
};
|
||||
|
||||
const intervalToString = (interval: DateInterval | undefined, fallback: string): string => {
|
||||
if (!interval || interval === 'all') {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return INTERVAL_TO_STRING_MAP[interval];
|
||||
};
|
||||
|
||||
export const DateIntervalSelector: FC<DateIntervalSelectorProps> = ({ onChange, active, allText }) => (
|
||||
<DropdownBtn text={intervalToString(active, allText)}>
|
||||
<DropdownItem active={active === 'all'} onClick={() => onChange('all')}>
|
||||
{allText}
|
||||
</DropdownItem>
|
||||
<DropdownItem divider />
|
||||
{Object.entries(INTERVAL_TO_STRING_MAP).map(
|
||||
([interval, name]) => (
|
||||
<DropdownItem key={interval} active={active === interval} onClick={() => onChange(interval as DateInterval)}>
|
||||
{name}
|
||||
</DropdownItem>
|
||||
),
|
||||
)}
|
||||
</DropdownBtn>
|
||||
);
|
||||
@ -24,15 +24,13 @@ describe('<ManageServersRowDropdown />', () => {
|
||||
};
|
||||
const toggleDropdown = (user: UserEvent) => user.click(screen.getByRole('button'));
|
||||
|
||||
it.each([
|
||||
[setUp],
|
||||
[async () => {
|
||||
const { user, container } = setUp();
|
||||
await toggleDropdown(user);
|
||||
it('passes a11y checks', async () => {
|
||||
const { user, container } = setUp();
|
||||
// Open menu
|
||||
await toggleDropdown(user);
|
||||
|
||||
return { container };
|
||||
}],
|
||||
])('passes a11y checks', (setUp) => checkAccessibility(setUp()));
|
||||
return checkAccessibility({ container });
|
||||
});
|
||||
|
||||
it('renders expected amount of dropdown items', async () => {
|
||||
const { user } = setUp();
|
||||
|
||||
@ -20,7 +20,13 @@ describe('<ServersDropdown />', () => {
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
it('passes a11y checks', () => checkAccessibility(setUp()));
|
||||
it('passes a11y checks', async () => {
|
||||
const { user, ...rest } = setUp();
|
||||
// Open menu
|
||||
await user.click(screen.getByText('Servers'));
|
||||
|
||||
return checkAccessibility(rest);
|
||||
});
|
||||
|
||||
it('contains the list of servers and the "mange servers" button', async () => {
|
||||
const { user } = setUp();
|
||||
|
||||
@ -24,17 +24,13 @@ describe('ServersExporter', () => {
|
||||
const createCsvjsonMock = (throwError = false) => (throwError ? erroneousToCsv : vi.fn(() => ''));
|
||||
|
||||
describe('exportServers', () => {
|
||||
let originalConsole: Console;
|
||||
const error = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
originalConsole = global.console;
|
||||
global.console = fromPartial<Console>({ error });
|
||||
(global as any).Blob = class Blob {};
|
||||
(global as any).URL = { createObjectURL: () => '' };
|
||||
vi.stubGlobal('console', fromPartial<Console>({ error }));
|
||||
});
|
||||
afterEach(() => {
|
||||
global.console = originalConsole;
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('logs an error if something fails', () => {
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import type { DateInterval } from '../../../src/utils/dates/DateIntervalSelector';
|
||||
import { DateIntervalSelector, INTERVAL_TO_STRING_MAP } from '../../../src/utils/dates/DateIntervalSelector';
|
||||
import { checkAccessibility } from '../../__helpers__/accessibility';
|
||||
import { renderWithEvents } from '../../__helpers__/setUpTest';
|
||||
|
||||
describe('<DateIntervalSelector />', () => {
|
||||
const activeInterval: DateInterval = 'last7Days';
|
||||
const onChange = vi.fn();
|
||||
const setUp = () => renderWithEvents(
|
||||
<DateIntervalSelector allText="All text" active={activeInterval} onChange={onChange} />,
|
||||
);
|
||||
|
||||
it('passes a11y checks', () => checkAccessibility(setUp()));
|
||||
|
||||
it('passes props down to nested DateIntervalDropdownItems', async () => {
|
||||
const { user } = setUp();
|
||||
const btn = screen.getByRole('button');
|
||||
|
||||
await user.click(btn);
|
||||
await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument());
|
||||
|
||||
const items = screen.getAllByRole('menuitem');
|
||||
|
||||
expect(btn).toHaveTextContent(INTERVAL_TO_STRING_MAP[activeInterval] ?? '');
|
||||
expect(items).toHaveLength(8);
|
||||
});
|
||||
});
|
||||
@ -35,9 +35,16 @@ export default defineConfig({
|
||||
|
||||
// Vitest config
|
||||
test: {
|
||||
// Run tests in an actual browser
|
||||
browser: {
|
||||
provider: 'playwright',
|
||||
enabled: true,
|
||||
headless: true,
|
||||
screenshotFailures: false,
|
||||
instances: [{ browser: 'chromium' }],
|
||||
},
|
||||
globals: true,
|
||||
allowOnly: true,
|
||||
environment: 'jsdom',
|
||||
setupFiles: './config/test/setupTests.ts',
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user