mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-12-11 02:09:53 -06:00
Merge pull request #1504 from acelaya-forks/feature/browser-tests
Run tests in a headless browser with vitest browser mode and playwright
This commit is contained in:
commit
7880bb4abe
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@ -13,3 +13,4 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 22.x
|
node-version: 22.x
|
||||||
publish-coverage: true
|
publish-coverage: true
|
||||||
|
install-playwright: true
|
||||||
|
|||||||
@ -10,10 +10,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* Update to `react-router` 7.0
|
* Update to `react-router` 7.0
|
||||||
* Update to `@shlinkio/shlink-frontend-kit` 0.7.0
|
* Update to `@shlinkio/shlink-frontend-kit` 0.8.x
|
||||||
* Update to `@shlinkio/shlink-web-component` 0.13.0
|
* Update to `@shlinkio/shlink-web-component` 0.13.x
|
||||||
* Update to `@shlinkio/shlink-js-sdk` 2.0.0
|
* Update to `@shlinkio/shlink-js-sdk` 2.0.0
|
||||||
* Add `eslint-plugin-react-compiler`
|
* Add `eslint-plugin-react-compiler`
|
||||||
|
* Run unit tests in a headless browser using vitest browser mode and playwright.
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
* *Nothing*
|
* *Nothing*
|
||||||
|
|||||||
@ -1,33 +1,9 @@
|
|||||||
import '@testing-library/jest-dom/vitest';
|
import '@testing-library/jest-dom/vitest';
|
||||||
import { cleanup } from '@testing-library/react';
|
import { cleanup } from '@testing-library/react';
|
||||||
import axe from 'axe-core';
|
|
||||||
import { afterEach } from 'vitest';
|
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
|
// Clear all mocks and cleanup DOM after every test
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
cleanup();
|
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:
|
shlink_web_client_node:
|
||||||
container_name: 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
|
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"
|
command: /bin/sh -c "cd /home/shlink/www && npm install && npm run start"
|
||||||
volumes:
|
volumes:
|
||||||
- ./:/home/shlink/www
|
- ./:/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": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"@vitest/browser": "^3.1.1",
|
||||||
"@vitest/coverage-v8": "^3.1.1",
|
"@vitest/coverage-v8": "^3.1.1",
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
"axe-core": "^4.10.3",
|
"axe-core": "^4.10.3",
|
||||||
@ -74,7 +75,7 @@
|
|||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"history": "^5.3.0",
|
"history": "^5.3.0",
|
||||||
"jsdom": "^26.0.0",
|
"playwright": "^1.51.1",
|
||||||
"sass": "^1.86.3",
|
"sass": "^1.86.3",
|
||||||
"tailwindcss": "^4.0.17",
|
"tailwindcss": "^4.0.17",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
|
|||||||
@ -42,7 +42,7 @@ export const ServersDropdown = ({ servers, selectedServer }: ServersDropdownProp
|
|||||||
<DropdownToggle nav caret>
|
<DropdownToggle nav caret>
|
||||||
<FontAwesomeIcon icon={serverIcon} /> <span className="tw:ml-1">Servers</span>
|
<FontAwesomeIcon icon={serverIcon} /> <span className="tw:ml-1">Servers</span>
|
||||||
</DropdownToggle>
|
</DropdownToggle>
|
||||||
<DropdownMenu end classNam="tw:right-0">{renderServers()}</DropdownMenu>
|
<DropdownMenu end className="tw:right-0">{renderServers()}</DropdownMenu>
|
||||||
</UncontrolledDropdown>
|
</UncontrolledDropdown>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -96,6 +96,7 @@ const ImportServersBtn: FCWithDeps<ImportServersBtnConnectProps, ImportServersBt
|
|||||||
accept=".csv"
|
accept=".csv"
|
||||||
className="tw:hidden"
|
className="tw:hidden"
|
||||||
aria-hidden
|
aria-hidden
|
||||||
|
tabIndex={-1}
|
||||||
ref={ref as any /* TODO Remove After updating to React 19 */}
|
ref={ref as any /* TODO Remove After updating to React 19 */}
|
||||||
onChange={onFile}
|
onChange={onFile}
|
||||||
data-testid="csv-file-input"
|
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'));
|
const toggleDropdown = (user: UserEvent) => user.click(screen.getByRole('button'));
|
||||||
|
|
||||||
it.each([
|
it('passes a11y checks', async () => {
|
||||||
[setUp],
|
const { user, container } = setUp();
|
||||||
[async () => {
|
// Open menu
|
||||||
const { user, container } = setUp();
|
await toggleDropdown(user);
|
||||||
await toggleDropdown(user);
|
|
||||||
|
|
||||||
return { container };
|
return checkAccessibility({ container });
|
||||||
}],
|
});
|
||||||
])('passes a11y checks', (setUp) => checkAccessibility(setUp()));
|
|
||||||
|
|
||||||
it('renders expected amount of dropdown items', async () => {
|
it('renders expected amount of dropdown items', async () => {
|
||||||
const { user } = setUp();
|
const { user } = setUp();
|
||||||
|
|||||||
@ -20,7 +20,13 @@ describe('<ServersDropdown />', () => {
|
|||||||
</MemoryRouter>,
|
</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 () => {
|
it('contains the list of servers and the "mange servers" button', async () => {
|
||||||
const { user } = setUp();
|
const { user } = setUp();
|
||||||
|
|||||||
@ -24,17 +24,13 @@ describe('ServersExporter', () => {
|
|||||||
const createCsvjsonMock = (throwError = false) => (throwError ? erroneousToCsv : vi.fn(() => ''));
|
const createCsvjsonMock = (throwError = false) => (throwError ? erroneousToCsv : vi.fn(() => ''));
|
||||||
|
|
||||||
describe('exportServers', () => {
|
describe('exportServers', () => {
|
||||||
let originalConsole: Console;
|
|
||||||
const error = vi.fn();
|
const error = vi.fn();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
originalConsole = global.console;
|
vi.stubGlobal('console', fromPartial<Console>({ error }));
|
||||||
global.console = fromPartial<Console>({ error });
|
|
||||||
(global as any).Blob = class Blob {};
|
|
||||||
(global as any).URL = { createObjectURL: () => '' };
|
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
global.console = originalConsole;
|
vi.unstubAllGlobals();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('logs an error if something fails', () => {
|
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
|
// Vitest config
|
||||||
test: {
|
test: {
|
||||||
|
// Run tests in an actual browser
|
||||||
|
browser: {
|
||||||
|
provider: 'playwright',
|
||||||
|
enabled: true,
|
||||||
|
headless: true,
|
||||||
|
screenshotFailures: false,
|
||||||
|
instances: [{ browser: 'chromium' }],
|
||||||
|
},
|
||||||
globals: true,
|
globals: true,
|
||||||
allowOnly: true,
|
allowOnly: true,
|
||||||
environment: 'jsdom',
|
|
||||||
setupFiles: './config/test/setupTests.ts',
|
setupFiles: './config/test/setupTests.ts',
|
||||||
coverage: {
|
coverage: {
|
||||||
provider: 'v8',
|
provider: 'v8',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user