diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml
index acc10ceea..f83c3fed1 100644
--- a/.github/workflows/code-quality.yml
+++ b/.github/workflows/code-quality.yml
@@ -11,10 +11,10 @@ jobs:
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Install pnpm
- uses: pnpm/action-setup@v2
+ uses: pnpm/action-setup@v4
with:
version: 10
@@ -38,10 +38,10 @@ jobs:
matrix:
node: [20, 22, 24, 'lts/*']
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Install pnpm
- uses: pnpm/action-setup@v2
+ uses: pnpm/action-setup@v4
with:
version: 10
@@ -61,25 +61,47 @@ jobs:
name: 🔬 Tests
timeout-minutes: 10
runs-on: ubuntu-latest
+ env:
+ VITE_BASE_URL: http://localhost:3000
strategy:
matrix:
node: [20, 22, 24, 'lts/*']
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Install pnpm
- uses: pnpm/action-setup@v2
+ uses: pnpm/action-setup@v4
with:
version: 10
+ - name: Get pnpm store directory
+ shell: bash
+ run: |
+ echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
+
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: 'pnpm'
+ - name: Cache node modules
+ uses: actions/cache@v4
+ env:
+ cache-name: cache-node-modules
+ with:
+ path: ${{ env.STORE_PATH }}
+ key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
+ restore-keys: |
+ ${{ runner.os }}-pnpm-store-${{ env.cache-name }}-
+ ${{ runner.os }}-pnpm-store-
+ ${{ runner.os }}-
+
- name: Install deps
run: pnpm install
+ - name: Install Playwright Chromium
+ run: pnpm exec playwright install chromium --with-deps
+
- name: Run tests
run: SKIP_ENV_VALIDATION=true pnpm run test:ci
diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml
index 959303cca..835e675f8 100644
--- a/.github/workflows/e2e-tests.yml
+++ b/.github/workflows/e2e-tests.yml
@@ -40,9 +40,9 @@ jobs:
- 5432:5432
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- - uses: actions/setup-node@v3
+ - uses: actions/setup-node@v4
with:
node-version: 20
@@ -73,7 +73,7 @@ jobs:
run: pnpm install
- name: Install Playwright Browsers
- run: pnpm playwright install --with-deps
+ run: pnpm exec playwright install --with-deps
- name: Migrate database
run: pnpm db:push
diff --git a/app/components/form/field-checkbox/field-checkbox.spec.tsx b/app/components/form/field-checkbox/field-checkbox.browser.spec.tsx
similarity index 63%
rename from app/components/form/field-checkbox/field-checkbox.spec.tsx
rename to app/components/form/field-checkbox/field-checkbox.browser.spec.tsx
index d8a734a89..18bc97a5a 100644
--- a/app/components/form/field-checkbox/field-checkbox.spec.tsx
+++ b/app/components/form/field-checkbox/field-checkbox.browser.spec.tsx
@@ -1,8 +1,12 @@
import { expect, test, vi } from 'vitest';
-import { axe } from 'vitest-axe';
import { z } from 'zod';
-import { render, screen, setupUser } from '@/tests/utils';
+import {
+ FAILED_CLICK_TIMEOUT_MS,
+ page,
+ render,
+ setupUser,
+} from '@/tests/utils';
import { FormField, FormFieldController } from '..';
import { FormMocked } from '../form-test-utils';
@@ -14,36 +18,6 @@ const zFormSchema = () =>
}),
});
-test('should have no a11y violations', async () => {
- const mockedSubmit = vi.fn();
-
- HTMLCanvasElement.prototype.getContext = vi.fn();
-
- const { container } = render(
-
- {({ form }) => (
-
-
- I love bears
-
-
- )}
-
- );
-
- const results = await axe(container);
-
- expect(results).toHaveNoViolations();
-});
-
test('should select checkbox on button click', async () => {
const user = setupUser();
const mockedSubmit = vi.fn();
@@ -68,13 +42,14 @@ test('should select checkbox on button click', async () => {
);
- const checkbox = screen.getByRole('checkbox', { name: 'I love bears' });
- expect(checkbox).not.toBeChecked();
+ const checkbox = page.getByRole('checkbox', { name: 'I love bears' });
+
+ await expect.element(checkbox).not.toBeChecked();
await user.click(checkbox);
- expect(checkbox).toBeChecked();
+ await expect.element(checkbox).toBeChecked();
- await user.click(screen.getByRole('button', { name: 'Submit' }));
+ await user.click(page.getByRole('button', { name: 'Submit' }));
expect(mockedSubmit).toHaveBeenCalledWith({ lovesBears: true });
});
@@ -102,16 +77,16 @@ test('should select checkbox on label click', async () => {
);
- const checkbox = screen.getByRole('checkbox', { name: 'I love bears' });
- const label = screen.getByText('I love bears');
+ const checkbox = page.getByRole('checkbox', { name: 'I love bears' });
+ const label = page.getByText('I love bears');
- expect(checkbox).not.toBeChecked();
+ await expect.element(checkbox).not.toBeChecked();
// Test clicking the label specifically
await user.click(label);
- expect(checkbox).toBeChecked();
+ await expect.element(checkbox).toBeChecked();
- await user.click(screen.getByRole('button', { name: 'Submit' }));
+ await user.click(page.getByRole('button', { name: 'Submit' }));
expect(mockedSubmit).toHaveBeenCalledWith({ lovesBears: true });
});
@@ -138,10 +113,10 @@ test('default value', async () => {
);
- const checkbox = screen.getByRole('checkbox', { name: 'I love bears' });
- expect(checkbox).toBeChecked();
+ const checkbox = page.getByRole('checkbox', { name: 'I love bears' });
+ await expect.element(checkbox).toBeChecked();
- await user.click(screen.getByRole('button', { name: 'Submit' }));
+ await user.click(page.getByRole('button', { name: 'Submit' }));
expect(mockedSubmit).toHaveBeenCalledWith({ lovesBears: true });
});
@@ -169,12 +144,18 @@ test('disabled', async () => {
);
- const checkbox = screen.getByRole('checkbox', { name: 'I love bears' });
- expect(checkbox).toBeDisabled();
- expect(checkbox).not.toBeChecked();
-
- await user.click(checkbox);
- expect(checkbox).not.toBeChecked();
- await user.click(screen.getByRole('button', { name: 'Submit' }));
+ const checkbox = page.getByRole('checkbox', { name: 'I love bears' });
+ await expect.element(checkbox).toBeDisabled();
+ await expect.element(checkbox).not.toBeChecked();
+
+ try {
+ await user.click(checkbox, {
+ trial: true,
+ timeout: FAILED_CLICK_TIMEOUT_MS,
+ });
+ } catch {
+ await expect.element(checkbox).not.toBeChecked();
+ }
+ await user.click(page.getByRole('button', { name: 'Submit' }));
expect(mockedSubmit).toHaveBeenCalledWith({ lovesBears: undefined });
});
diff --git a/app/components/form/field-otp/field-otp.spec.tsx b/app/components/form/field-otp/field-otp.browser.spec.tsx
similarity index 76%
rename from app/components/form/field-otp/field-otp.spec.tsx
rename to app/components/form/field-otp/field-otp.browser.spec.tsx
index 02a037b60..8928e261b 100644
--- a/app/components/form/field-otp/field-otp.spec.tsx
+++ b/app/components/form/field-otp/field-otp.browser.spec.tsx
@@ -1,7 +1,12 @@
import { expect, test, vi } from 'vitest';
import { z } from 'zod';
-import { render, screen, setupUser } from '@/tests/utils';
+import {
+ FAILED_CLICK_TIMEOUT_MS,
+ page,
+ render,
+ setupUser,
+} from '@/tests/utils';
import { FormField, FormFieldController, FormFieldLabel } from '..';
import { FormMocked } from '../form-test-utils';
@@ -29,10 +34,14 @@ test('update value', async () => {
)}
);
- const input = screen.getByLabelText('Code');
+
+ const input = page.getByLabelText('Code');
await user.click(input);
- await user.paste('000000');
- await user.click(screen.getByRole('button', { name: 'Submit' }));
+ // Add the code to the user clipboard
+ await navigator.clipboard.writeText('000000');
+
+ await user.paste();
+ await user.click(page.getByRole('button', { name: 'Submit' }));
expect(mockedSubmit).toHaveBeenCalledWith({ code: '000000' });
});
@@ -62,7 +71,7 @@ test('default value', async () => {
)}
);
- await user.click(screen.getByRole('button', { name: 'Submit' }));
+ await user.click(page.getByRole('button', { name: 'Submit' }));
expect(mockedSubmit).toHaveBeenCalledWith({ code: '000000' });
});
@@ -90,9 +99,11 @@ test('auto submit', async () => {
)}
);
- const input = screen.getByLabelText('Code');
+ const input = page.getByLabelText('Code');
await user.click(input);
- await user.paste('000000');
+ // Add the code to the user clipboard
+ await navigator.clipboard.writeText('000000');
+ await user.paste();
expect(mockedSubmit).toHaveBeenCalledWith({ code: '000000' });
});
@@ -120,9 +131,17 @@ test('disabled', async () => {
)}
);
- const input = screen.getByLabelText('Code');
- await user.click(input);
- await user.paste('123456');
- await user.click(screen.getByRole('button', { name: 'Submit' }));
+ const input = page.getByLabelText('Code');
+ try {
+ await user.click(input, { timeout: FAILED_CLICK_TIMEOUT_MS });
+ } catch {
+ // Click expected to fail since input is disabled
+ }
+ // Add the code to the user clipboard
+ await navigator.clipboard.writeText('123456');
+ await user.paste();
+
+ await user.click(page.getByRole('button', { name: 'Submit' }));
+
expect(mockedSubmit).toHaveBeenCalledWith({ code: undefined });
});
diff --git a/app/components/form/field-radio-group/field-radio-group.spec.tsx b/app/components/form/field-radio-group/field-radio-group.browser.spec.tsx
similarity index 66%
rename from app/components/form/field-radio-group/field-radio-group.spec.tsx
rename to app/components/form/field-radio-group/field-radio-group.browser.spec.tsx
index 6cc8ee61c..0c764fabc 100644
--- a/app/components/form/field-radio-group/field-radio-group.spec.tsx
+++ b/app/components/form/field-radio-group/field-radio-group.browser.spec.tsx
@@ -1,8 +1,12 @@
import { expect, test, vi } from 'vitest';
-import { axe } from 'vitest-axe';
import { z } from 'zod';
-import { render, screen, setupUser } from '@/tests/utils';
+import {
+ FAILED_CLICK_TIMEOUT_MS,
+ page,
+ render,
+ setupUser,
+} from '@/tests/utils';
import { FormField, FormFieldController, FormFieldLabel } from '..';
import { FormMocked } from '../form-test-utils';
@@ -27,36 +31,6 @@ const options = [
},
];
-test('should have no a11y violations', async () => {
- const mockedSubmit = vi.fn();
-
- HTMLCanvasElement.prototype.getContext = vi.fn();
-
- const { container } = render(
-
- {({ form }) => (
-
- Bearstronaut
-
-
- )}
-
- );
-
- const results = await axe(container);
-
- expect(results).toHaveNoViolations();
-});
-
test('should select radio on button click', async () => {
const user = setupUser();
const mockedSubmit = vi.fn();
@@ -81,13 +55,13 @@ test('should select radio on button click', async () => {
);
- const radio = screen.getByRole('radio', { name: 'Buzz Pawdrin' });
- expect(radio).not.toBeChecked();
+ const radio = page.getByRole('radio', { name: 'Buzz Pawdrin' });
+ await expect.element(radio).not.toBeChecked();
await user.click(radio);
- expect(radio).toBeChecked();
+ await expect.element(radio).toBeChecked();
- await user.click(screen.getByRole('button', { name: 'Submit' }));
+ await user.click(page.getByRole('button', { name: 'Submit' }));
expect(mockedSubmit).toHaveBeenCalledWith({ bear: 'pawdrin' });
});
@@ -115,16 +89,16 @@ test('should select radio on label click', async () => {
);
- const radio = screen.getByRole('radio', { name: 'Buzz Pawdrin' });
- const label = screen.getByText('Buzz Pawdrin');
+ const radio = page.getByRole('radio', { name: 'Buzz Pawdrin' });
+ const label = page.getByText('Buzz Pawdrin');
- expect(radio).not.toBeChecked();
+ await expect.element(radio).not.toBeChecked();
// Test clicking the label specifically
await user.click(label);
- expect(radio).toBeChecked();
+ await expect.element(radio).toBeChecked();
- await user.click(screen.getByRole('button', { name: 'Submit' }));
+ await user.click(page.getByRole('button', { name: 'Submit' }));
expect(mockedSubmit).toHaveBeenCalledWith({ bear: 'pawdrin' });
});
@@ -152,26 +126,26 @@ test('should handle keyboard navigation', async () => {
);
- const firstRadio = screen.getByRole('radio', { name: 'Bearstrong' });
- const secondRadio = screen.getByRole('radio', { name: 'Buzz Pawdrin' });
- const thirdRadio = screen.getByRole('radio', { name: 'Yuri Grizzlyrin' });
+ const firstRadio = page.getByRole('radio', { name: 'Bearstrong' });
+ const secondRadio = page.getByRole('radio', { name: 'Buzz Pawdrin' });
+ const thirdRadio = page.getByRole('radio', { name: 'Yuri Grizzlyrin' });
await user.tab();
- expect(firstRadio).toHaveFocus();
+ await expect.element(firstRadio).toHaveFocus();
await user.keyboard('{ArrowDown}');
- expect(secondRadio).toHaveFocus();
+ await expect.element(secondRadio).toHaveFocus();
await user.keyboard(' ');
- expect(secondRadio).toBeChecked();
+ await expect.element(secondRadio).toBeChecked();
await user.keyboard('{ArrowDown}');
- expect(thirdRadio).toHaveFocus();
+ await expect.element(thirdRadio).toHaveFocus();
await user.keyboard('{ArrowUp}');
- expect(secondRadio).toHaveFocus();
- expect(secondRadio).toBeChecked(); // Second radio should still be checked
+ await expect.element(secondRadio).toHaveFocus();
+ await expect.element(secondRadio).toBeChecked(); // Second radio should still be checked
- await user.click(screen.getByRole('button', { name: 'Submit' }));
+ await user.click(page.getByRole('button', { name: 'Submit' }));
expect(mockedSubmit).toHaveBeenCalledWith({ bear: 'pawdrin' });
});
@@ -202,10 +176,10 @@ test('default value', async () => {
);
- const radio = screen.getByRole('radio', { name: 'Yuri Grizzlyrin' });
- expect(radio).toBeChecked();
+ const radio = page.getByRole('radio', { name: 'Yuri Grizzlyrin' });
+ await expect.element(radio).toBeChecked();
- await user.click(screen.getByRole('button', { name: 'Submit' }));
+ await user.click(page.getByRole('button', { name: 'Submit' }));
expect(mockedSubmit).toHaveBeenCalledWith({ bear: 'grizzlyrin' });
});
@@ -237,10 +211,10 @@ test('disabled', async () => {
);
- const radio = screen.getByRole('radio', { name: 'Buzz Pawdrin' });
- expect(radio).toBeDisabled();
+ const radio = page.getByRole('radio', { name: 'Buzz Pawdrin' });
+ await expect.element(radio).toBeDisabled();
- await user.click(screen.getByRole('button', { name: 'Submit' }));
+ await user.click(page.getByRole('button', { name: 'Submit' }));
expect(mockedSubmit).toHaveBeenCalledWith({ bear: undefined });
});
@@ -271,12 +245,16 @@ test('disabled option', async () => {
);
- const disabledRadio = screen.getByRole('radio', { name: 'Mae Jemibear' });
- expect(disabledRadio).toBeDisabled();
+ const disabledRadio = page.getByRole('radio', { name: 'Mae Jemibear' });
+ await expect.element(disabledRadio).toBeDisabled();
- await user.click(disabledRadio);
- expect(disabledRadio).not.toBeChecked();
+ try {
+ await user.click(disabledRadio, { timeout: FAILED_CLICK_TIMEOUT_MS });
+ } catch {
+ // Expected to fail since input is disabled
+ }
+ await expect.element(disabledRadio).not.toBeChecked();
- await user.click(screen.getByRole('button', { name: 'Submit' }));
+ await user.click(page.getByRole('button', { name: 'Submit' }));
expect(mockedSubmit).toHaveBeenCalledWith({ bear: '' });
});
diff --git a/app/components/form/field-text/field-text.spec.tsx b/app/components/form/field-text/field-text.browser.spec.tsx
similarity index 77%
rename from app/components/form/field-text/field-text.spec.tsx
rename to app/components/form/field-text/field-text.browser.spec.tsx
index 8f88062b6..50df40fb5 100644
--- a/app/components/form/field-text/field-text.spec.tsx
+++ b/app/components/form/field-text/field-text.browser.spec.tsx
@@ -1,7 +1,7 @@
import { expect, test, vi } from 'vitest';
import { z } from 'zod';
-import { render, screen, setupUser } from '@/tests/utils';
+import { page, render, setupUser } from '@/tests/utils';
import { FormField, FormFieldLabel } from '..';
import { FormFieldController } from '../form-field-controller';
@@ -25,10 +25,11 @@ test('update value', async () => {
)}
);
- const input = screen.getByLabelText('Name');
+ const input = page.getByLabelText('Name').element() as HTMLInputElement;
+
await user.type(input, 'new value');
expect(input.value).toBe('new value');
- await user.click(screen.getByRole('button', { name: 'Submit' }));
+ await user.click(page.getByRole('button', { name: 'Submit' }));
expect(mockedSubmit).toHaveBeenCalledWith({ name: 'new value' });
});
@@ -53,9 +54,9 @@ test('default value', async () => {
)}
);
- const input = screen.getByLabelText('Name');
+ const input = page.getByLabelText('Name').element() as HTMLInputElement;
expect(input.value).toBe('default value');
- await user.click(screen.getByRole('button', { name: 'Submit' }));
+ await user.click(page.getByRole('button', { name: 'Submit' }));
expect(mockedSubmit).toHaveBeenCalledWith({ name: 'default value' });
});
@@ -82,9 +83,13 @@ test('disabled', async () => {
)}
);
- const input = screen.getByLabelText('Name');
- await user.type(input, 'another value');
- await user.click(screen.getByRole('button', { name: 'Submit' }));
+ const input = page.getByLabelText('Name');
+ try {
+ await user.type(input, 'another value');
+ } catch {
+ // Expected to fail since input is disabled
+ }
+ await user.click(page.getByRole('button', { name: 'Submit' }));
expect(mockedSubmit).toHaveBeenCalledWith({ name: undefined });
});
@@ -111,8 +116,12 @@ test('readOnly', async () => {
)}
);
- const input = screen.getByLabelText('Name');
- await user.type(input, 'another value');
- await user.click(screen.getByRole('button', { name: 'Submit' }));
+ const input = page.getByLabelText('Name');
+ try {
+ await user.type(input, 'another value');
+ } catch {
+ // Expected to fail since input is readOnly
+ }
+ await user.click(page.getByRole('button', { name: 'Submit' }));
expect(mockedSubmit).toHaveBeenCalledWith({ name: 'new value' });
});
diff --git a/app/components/ui/calendar.spec.tsx b/app/components/ui/calendar.browser.spec.tsx
similarity index 54%
rename from app/components/ui/calendar.spec.tsx
rename to app/components/ui/calendar.browser.spec.tsx
index fe667c7f5..ecdd0f3fb 100644
--- a/app/components/ui/calendar.spec.tsx
+++ b/app/components/ui/calendar.browser.spec.tsx
@@ -1,28 +1,37 @@
-import { render, screen } from '@testing-library/react';
import dayjs from 'dayjs';
-import { describe, expect, it, vitest } from 'vitest';
+import * as module from 'react-i18next';
+import { describe, expect, it, vi } from 'vitest';
+
+import { page, render } from '@/tests/utils';
import { Calendar } from './calendar';
-vitest.mock('react-i18next', () => ({
- // This is a mock, we name it as the hook we want to mock
- // eslint-disable-next-line @eslint-react/hooks-extra/no-unnecessary-use-prefix
- useTranslation: () => ({ t: (key: ExplicitAny) => key }),
-}));
+// https://vitest.dev/guide/browser/#limitations
+vi.mock('react-i18next', { spy: true });
+vi.mocked(module.useTranslation).mockImplementation(
+ // @ts-expect-error We don't bother typing properly for this test
+ () => ({ t: (key) => key })
+);
describe('Calendar', () => {
- it('should render with previous and next button by default', () => {
+ // eslint-disable-next-line sonarjs/assertions-in-tests
+ it('should render with previous and next button by default', async () => {
render();
- expect(screen.getByLabelText('Go to the Previous Month')).toBeDefined();
- expect(screen.getByLabelText('Go to the Next Month')).toBeDefined();
+ await expect
+ .element(page.getByLabelText('Go to the Previous Month'))
+ .toBeDefined();
+
+ await expect
+ .element(page.getByLabelText('Go to the Next Month'))
+ .toBeDefined();
});
it('should render without button date when uncontrolled', () => {
render();
// 3 are the previous, next and year select buttons
- expect(screen.getAllByRole('button').length).toBeLessThanOrEqual(3);
+ expect(page.getByRole('button').all().length).toBeLessThanOrEqual(3);
});
it('should render date buttons when controlled', () => {
@@ -31,11 +40,11 @@ describe('Calendar', () => {
);
// 3 are the previous, next and year select buttons
- expect(screen.getAllByRole('button').length).toBeGreaterThan(3);
+ expect(page.getByRole('button').all().length).toBeGreaterThan(3);
});
- it('should give the selected value on select', () => {
- const onSelect = vitest.fn();
+ it('should give the selected value on select', async () => {
+ const onSelect = vi.fn();
render(
{
const ariaLabel = targetDate.format('dddd, MMMM D');
// exact false because we don't provide the end of the aria-label (the year mostly)
- const button = screen.getByLabelText(ariaLabel, { exact: false });
- expect(button).toBeDefined();
+ const button = page.getByLabelText(ariaLabel, { exact: false });
+ await expect.element(button).toBeDefined();
- button.click();
+ await button.click();
expect(onSelect).toHaveBeenCalledWith(targetDate.startOf('day').toDate());
});
- it('should be able to select today using aria-label', () => {
- const onSelect = vitest.fn();
+ it('should be able to select today using aria-label', async () => {
+ const onSelect = vi.fn();
render(
{
const targetDate = dayjs();
// exact false because we don't provide the end of the aria-label
- const button = screen.getByLabelText('Today', { exact: false });
- expect(button).toBeDefined();
+ const button = page.getByLabelText('Today', { exact: false });
+ await expect.element(button).toBeDefined();
- button.click();
+ await button.click();
expect(onSelect).toHaveBeenCalledWith(targetDate.startOf('day').toDate());
});
diff --git a/app/lib/dayjs/parse-string-to-date.spec.ts b/app/lib/dayjs/parse-string-to-date.unit.spec.ts
similarity index 100%
rename from app/lib/dayjs/parse-string-to-date.spec.ts
rename to app/lib/dayjs/parse-string-to-date.unit.spec.ts
diff --git a/app/lib/zod/zod-utils.spec.ts b/app/lib/zod/zod-utils.unit.spec.ts
similarity index 100%
rename from app/lib/zod/zod-utils.spec.ts
rename to app/lib/zod/zod-utils.unit.spec.ts
diff --git a/app/tests/setup.base.ts b/app/tests/setup.base.ts
new file mode 100644
index 000000000..6428d3588
--- /dev/null
+++ b/app/tests/setup.base.ts
@@ -0,0 +1 @@
+import '@/lib/dayjs/config';
diff --git a/app/tests/setup.browser.ts b/app/tests/setup.browser.ts
new file mode 100644
index 000000000..ba62eceed
--- /dev/null
+++ b/app/tests/setup.browser.ts
@@ -0,0 +1,4 @@
+import { afterEach } from 'vitest';
+import { cleanup } from 'vitest-browser-react';
+
+afterEach(cleanup);
diff --git a/app/tests/setup.ts b/app/tests/setup.ts
deleted file mode 100644
index e62e83aed..000000000
--- a/app/tests/setup.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { cleanup } from '@testing-library/react';
-import ResizeObserver from 'resize-observer-polyfill';
-import { afterEach, vi } from 'vitest';
-import { expect } from 'vitest';
-import * as matchers from 'vitest-axe/matchers';
-import '@/lib/dayjs/config';
-import '@testing-library/jest-dom/vitest';
-import 'vitest-axe/extend-expect';
-
-expect.extend(matchers);
-
-afterEach(cleanup);
-
-global.ResizeObserver = ResizeObserver;
-
-/**
- * scrollTo is not implemented by jsdom
- */
-Element.prototype.scrollTo = vi.fn();
-
-Object.defineProperty(document, 'elementFromPoint', {
- writable: true,
- value: vi.fn().mockImplementation(() => null),
-});
-
-Object.defineProperty(window, 'matchMedia', {
- writable: true,
- value: vi.fn().mockImplementation((query) => ({
- matches: false,
- media: query,
- onchange: null,
- addListener: vi.fn(),
- removeListener: vi.fn(),
- addEventListener: vi.fn(),
- removeEventListener: vi.fn(),
- dispatchEvent: vi.fn(),
- })),
-});
diff --git a/app/tests/utils.tsx b/app/tests/utils.tsx
index 2ee6f0ed7..14e20b30a 100644
--- a/app/tests/utils.tsx
+++ b/app/tests/utils.tsx
@@ -1,6 +1,6 @@
-import { render, RenderOptions } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
+import { userEvent } from '@vitest/browser/context';
import { ReactElement } from 'react';
+import { ComponentRenderOptions, render } from 'vitest-browser-react';
import { Providers } from '@/providers';
@@ -10,13 +10,17 @@ const WithProviders = ({ children }: { children: React.ReactNode }) => {
const customRender = (
ui: ReactElement,
- options?: Omit
+ options?: Omit
) => {
return render(ui, { wrapper: WithProviders, ...options });
};
// Custom Render
// https://testing-library.com/docs/react-testing-library/setup#custom-render
-export * from '@testing-library/react';
+export * from '@vitest/browser/context';
+export * from 'vitest-browser-react';
+
export { customRender as render };
export const setupUser = () => userEvent.setup();
+
+export const FAILED_CLICK_TIMEOUT_MS = 200;
diff --git a/app/tests/vitest.d.ts b/app/tests/vitest.d.ts
index d3d2f5894..a1d31e5a7 100644
--- a/app/tests/vitest.d.ts
+++ b/app/tests/vitest.d.ts
@@ -1,9 +1 @@
-import type { AxeMatchers } from 'vitest-axe/matchers';
-import 'vitest';
-
-declare module 'vitest' {
- // eslint-disable-next-line @typescript-eslint/no-empty-object-type
- export interface Assertion extends AxeMatchers {}
- // eslint-disable-next-line @typescript-eslint/no-empty-object-type
- export interface AsymmetricMatchersContaining extends AxeMatchers {}
-}
+///
diff --git a/package.json b/package.json
index 2fb9b7ffe..1370ef56a 100644
--- a/package.json
+++ b/package.json
@@ -26,9 +26,9 @@
"gen:prisma": "prisma generate --no-hints",
"gen:build-info": "dotenv -- node ./run-jiti ./app/features/build-info/script-to-generate-json.ts",
"gen:icons": "svgr --config-file app/components/icons/svgr.config.cjs app/components/icons/svg-sources && prettier -w app/components/icons/generated",
- "test": "vitest",
+ "test": "vitest --browser.headless",
"test:ci": "vitest run",
- "test:ui": "vitest --ui",
+ "test:ui": "vitest",
"e2e": "dotenv -- cross-env playwright test",
"e2e:ui": "dotenv -- cross-env playwright test --ui",
"dk:init": "docker compose up -d",
@@ -100,7 +100,7 @@
"@eslint-react/eslint-plugin": "1.49.0",
"@eslint/js": "9.26.0",
"@faker-js/faker": "9.7.0",
- "@playwright/test": "1.52.0",
+ "@playwright/test": "1.54.1",
"@storybook/addon-a11y": "9.0.14",
"@storybook/addon-docs": "9.0.14",
"@storybook/react-vite": "9.0.14",
@@ -108,14 +108,11 @@
"@tailwindcss/postcss": "4.1.7",
"@tanstack/eslint-plugin-query": "5.81.2",
"@tanstack/eslint-plugin-router": "1.121.21",
- "@testing-library/jest-dom": "6.6.3",
- "@testing-library/react": "16.3.0",
- "@testing-library/user-event": "14.6.1",
"@types/nodemailer": "6.4.17",
"@types/react": "19.1.3",
"@types/react-dom": "19.1.3",
"@vitejs/plugin-react": "4.4.1",
- "@vitest/ui": "3.1.3",
+ "@vitest/browser": "3.2.4",
"@vueless/storybook-dark-mode": "9.0.5",
"cross-env": "7.0.3",
"dotenv-cli": "8.0.0",
@@ -129,6 +126,7 @@
"lefthook": "1.11.13",
"maildev": "2.2.1",
"npm-run-all": "4.1.5",
+ "playwright": "1.54.1",
"prettier-plugin-tailwindcss": "0.6.11",
"prisma": "6.7.0",
"resize-observer-polyfill": "1.5.1",
@@ -138,8 +136,8 @@
"typescript-eslint": "8.32.0",
"vite": "6.3.5",
"vite-tsconfig-paths": "5.1.4",
- "vitest": "3.1.3",
- "vitest-axe": "0.1.0"
+ "vitest": "3.2.4",
+ "vitest-browser-react": "1.0.1"
},
"repository": {
"type": "git",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 66186f326..fdc713871 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -28,16 +28,16 @@ importers:
version: 1.5.2
'@orpc/openapi':
specifier: 1.5.2
- version: 1.5.2(crossws@0.3.5)(ws@8.18.1)
+ version: 1.5.2(crossws@0.3.5)(ws@8.18.3)
'@orpc/react-query':
specifier: 1.5.2
version: 1.5.2(@orpc/client@1.5.2)(@tanstack/query-core@5.81.5)(@tanstack/react-query@5.81.5(react@19.1.0))(react@19.1.0)
'@orpc/server':
specifier: 1.5.2
- version: 1.5.2(crossws@0.3.5)(ws@8.18.1)
+ version: 1.5.2(crossws@0.3.5)(ws@8.18.3)
'@orpc/zod':
specifier: 1.5.2
- version: 1.5.2(@orpc/contract@1.5.2)(@orpc/server@1.5.2(crossws@0.3.5)(ws@8.18.1))(crossws@0.3.5)(ws@8.18.1)(zod@3.24.4)
+ version: 1.5.2(@orpc/contract@1.5.2)(@orpc/server@1.5.2(crossws@0.3.5)(ws@8.18.3))(crossws@0.3.5)(ws@8.18.3)(zod@3.24.4)
'@prisma/client':
specifier: 6.7.0
version: 6.7.0(prisma@6.7.0(typescript@5.8.3))(typescript@5.8.3)
@@ -181,8 +181,8 @@ importers:
specifier: 9.7.0
version: 9.7.0
'@playwright/test':
- specifier: 1.52.0
- version: 1.52.0
+ specifier: 1.54.1
+ version: 1.54.1
'@storybook/addon-a11y':
specifier: 9.0.14
version: 9.0.14(storybook@9.0.14(@testing-library/dom@10.4.0)(prettier@3.5.3))
@@ -204,15 +204,6 @@ importers:
'@tanstack/eslint-plugin-router':
specifier: 1.121.21
version: 1.121.21(eslint@9.26.0(jiti@2.4.2))(typescript@5.8.3)
- '@testing-library/jest-dom':
- specifier: 6.6.3
- version: 6.6.3
- '@testing-library/react':
- specifier: 16.3.0
- version: 16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
- '@testing-library/user-event':
- specifier: 14.6.1
- version: 14.6.1(@testing-library/dom@10.4.0)
'@types/nodemailer':
specifier: 6.4.17
version: 6.4.17
@@ -225,9 +216,9 @@ importers:
'@vitejs/plugin-react':
specifier: 4.4.1
version: 4.4.1(vite@6.3.5(@types/node@20.14.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.26.0)(tsx@4.19.3)(yaml@2.7.0))
- '@vitest/ui':
- specifier: 3.1.3
- version: 3.1.3(vitest@3.1.3)
+ '@vitest/browser':
+ specifier: 3.2.4
+ version: 3.2.4(playwright@1.54.1)(vite@6.3.5(@types/node@20.14.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.26.0)(tsx@4.19.3)(yaml@2.7.0))(vitest@3.2.4)
'@vueless/storybook-dark-mode':
specifier: 9.0.5
version: 9.0.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -267,6 +258,9 @@ importers:
npm-run-all:
specifier: 4.1.5
version: 4.1.5
+ playwright:
+ specifier: 1.54.1
+ version: 1.54.1
prettier-plugin-tailwindcss:
specifier: 0.6.11
version: 0.6.11(@trivago/prettier-plugin-sort-imports@5.2.2(@vue/compiler-sfc@3.5.17)(prettier@3.5.3))(prettier@3.5.3)
@@ -295,11 +289,11 @@ importers:
specifier: 5.1.4
version: 5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@20.14.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.26.0)(tsx@4.19.3)(yaml@2.7.0))
vitest:
- specifier: 3.1.3
- version: 3.1.3(@types/node@20.14.10)(@vitest/ui@3.1.3)(happy-dom@17.4.4)(jiti@2.4.2)(jsdom@24.1.3)(lightningcss@1.30.1)(terser@5.26.0)(tsx@4.19.3)(yaml@2.7.0)
- vitest-axe:
- specifier: 0.1.0
- version: 0.1.0(vitest@3.1.3)
+ specifier: 3.2.4
+ version: 3.2.4(@types/node@20.14.10)(@vitest/browser@3.2.4)(happy-dom@17.4.4)(jiti@2.4.2)(jsdom@24.1.3)(lightningcss@1.30.1)(terser@5.26.0)(tsx@4.19.3)(yaml@2.7.0)
+ vitest-browser-react:
+ specifier: 1.0.1
+ version: 1.0.1(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(@vitest/browser@3.2.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@3.2.4)
packages:
@@ -1390,8 +1384,8 @@ packages:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
- '@playwright/test@1.52.0':
- resolution: {integrity: sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==}
+ '@playwright/test@1.54.1':
+ resolution: {integrity: sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==}
engines: {node: '>=18'}
hasBin: true
@@ -3040,21 +3034,6 @@ packages:
resolution: {integrity: sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==}
engines: {node: '>=14', npm: '>=6', yarn: '>=1'}
- '@testing-library/react@16.3.0':
- resolution: {integrity: sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==}
- engines: {node: '>=18'}
- peerDependencies:
- '@testing-library/dom': ^10.0.0
- '@types/react': ^18.0.0 || ^19.0.0
- '@types/react-dom': ^18.0.0 || ^19.0.0
- react: ^18.0.0 || ^19.0.0
- react-dom: ^18.0.0 || ^19.0.0
- peerDependenciesMeta:
- '@types/react':
- optional: true
- '@types/react-dom':
- optional: true
-
'@testing-library/user-event@14.6.1':
resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==}
engines: {node: '>=12', npm: '>=6'}
@@ -3220,49 +3199,47 @@ packages:
peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0
- '@vitest/expect@3.1.3':
- resolution: {integrity: sha512-7FTQQuuLKmN1Ig/h+h/GO+44Q1IlglPlR2es4ab7Yvfx+Uk5xsv+Ykk+MEt/M2Yn/xGmzaLKxGw2lgy2bwuYqg==}
+ '@vitest/browser@3.2.4':
+ resolution: {integrity: sha512-tJxiPrWmzH8a+w9nLKlQMzAKX/7VjFs50MWgcAj7p9XQ7AQ9/35fByFYptgPELyLw+0aixTnC4pUWV+APcZ/kw==}
+ peerDependencies:
+ playwright: '*'
+ safaridriver: '*'
+ vitest: 3.2.4
+ webdriverio: ^7.0.0 || ^8.0.0 || ^9.0.0
+ peerDependenciesMeta:
+ playwright:
+ optional: true
+ safaridriver:
+ optional: true
+ webdriverio:
+ optional: true
'@vitest/expect@3.2.4':
resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==}
- '@vitest/mocker@3.1.3':
- resolution: {integrity: sha512-PJbLjonJK82uCWHjzgBJZuR7zmAOrSvKk1QBxrennDIgtH4uK0TB1PvYmc0XBCigxxtiAVPfWtAdy4lpz8SQGQ==}
+ '@vitest/mocker@3.2.4':
+ resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==}
peerDependencies:
msw: ^2.4.9
- vite: ^5.0.0 || ^6.0.0
+ vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0
peerDependenciesMeta:
msw:
optional: true
vite:
optional: true
- '@vitest/pretty-format@3.1.3':
- resolution: {integrity: sha512-i6FDiBeJUGLDKADw2Gb01UtUNb12yyXAqC/mmRWuYl+m/U9GS7s8us5ONmGkGpUUo7/iAYzI2ePVfOZTYvUifA==}
-
'@vitest/pretty-format@3.2.4':
resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==}
- '@vitest/runner@3.1.3':
- resolution: {integrity: sha512-Tae+ogtlNfFei5DggOsSUvkIaSuVywujMj6HzR97AHK6XK8i3BuVyIifWAm/sE3a15lF5RH9yQIrbXYuo0IFyA==}
+ '@vitest/runner@3.2.4':
+ resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==}
- '@vitest/snapshot@3.1.3':
- resolution: {integrity: sha512-XVa5OPNTYUsyqG9skuUkFzAeFnEzDp8hQu7kZ0N25B1+6KjGm4hWLtURyBbsIAOekfWQ7Wuz/N/XXzgYO3deWQ==}
-
- '@vitest/spy@3.1.3':
- resolution: {integrity: sha512-x6w+ctOEmEXdWaa6TO4ilb7l9DxPR5bwEb6hILKuxfU1NqWT2mpJD9NJN7t3OTfxmVlOMrvtoFJGdgyzZ605lQ==}
+ '@vitest/snapshot@3.2.4':
+ resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==}
'@vitest/spy@3.2.4':
resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==}
- '@vitest/ui@3.1.3':
- resolution: {integrity: sha512-IipSzX+8DptUdXN/GWq3hq5z18MwnpphYdOMm0WndkRGYELzfq7NDP8dMpZT7JGW1uXFrIGxOW2D0Xi++ulByg==}
- peerDependencies:
- vitest: 3.1.3
-
- '@vitest/utils@3.1.3':
- resolution: {integrity: sha512-2Ltrpht4OmHO9+c/nmHtF09HWiyWdworqnHIwjfvDyWjuwKbdkcS9AnhsDn+8E2RM4x++foD1/tNuLPVvWG1Rg==}
-
'@vitest/utils@3.2.4':
resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
@@ -3628,10 +3605,6 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
- chalk@5.3.0:
- resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==}
- engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
-
check-error@2.1.1:
resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
engines: {node: '>= 16'}
@@ -3997,6 +3970,15 @@ packages:
supports-color:
optional: true
+ debug@4.4.1:
+ resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
decache@4.6.2:
resolution: {integrity: sha512-2LPqkLeu8XWHU8qNCS3kcF6sCcb5zIzvWaAHYSvPfwhdd7mHuah29NssMzrTYyHN4F5oFy2ko9OBYxegtU0FEw==}
@@ -4568,9 +4550,6 @@ packages:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
- fflate@0.8.2:
- resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
-
file-entry-cache@8.0.0:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
@@ -5448,9 +5427,6 @@ packages:
resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==}
engines: {node: '>= 12.0.0'}
- loupe@3.1.3:
- resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==}
-
loupe@3.1.4:
resolution: {integrity: sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==}
@@ -6047,13 +6023,13 @@ packages:
pkg-types@2.1.0:
resolution: {integrity: sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==}
- playwright-core@1.52.0:
- resolution: {integrity: sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==}
+ playwright-core@1.54.1:
+ resolution: {integrity: sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==}
engines: {node: '>=18'}
hasBin: true
- playwright@1.52.0:
- resolution: {integrity: sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==}
+ playwright@1.54.1:
+ resolution: {integrity: sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==}
engines: {node: '>=18'}
hasBin: true
@@ -6936,18 +6912,14 @@ packages:
resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
engines: {node: '>=12.0.0'}
- tinypool@1.0.2:
- resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==}
+ tinypool@1.1.1:
+ resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==}
engines: {node: ^18.0.0 || >=20.0.0}
tinyrainbow@2.0.0:
resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==}
engines: {node: '>=14.0.0'}
- tinyspy@3.0.2:
- resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==}
- engines: {node: '>=14.0.0'}
-
tinyspy@4.0.3:
resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==}
engines: {node: '>=14.0.0'}
@@ -7316,8 +7288,8 @@ packages:
react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
- vite-node@3.1.3:
- resolution: {integrity: sha512-uHV4plJ2IxCl4u1up1FQRrqclylKAogbtBfOTwcuJ28xFi+89PZ57BRh+naIRvH70HPwxy5QHYzg1OrEaC7AbA==}
+ vite-node@3.2.4:
+ resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
@@ -7369,21 +7341,32 @@ packages:
yaml:
optional: true
- vitest-axe@0.1.0:
- resolution: {integrity: sha512-jvtXxeQPg8R/2ANTY8QicA5pvvdRP4F0FsVUAHANJ46YCDASie/cuhlSzu0DGcLmZvGBSBNsNuK3HqfaeknyvA==}
+ vitest-browser-react@1.0.1:
+ resolution: {integrity: sha512-LqiGFCdknrbMoSDWXTCTrPsED3SvdIXIgYOOZyYUNj2dkJusW2eF6NENOlBlxwq+FBQqzNK1X59b+b03pXFpAQ==}
+ engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
peerDependencies:
- vitest: '>=0.16.0'
+ '@types/react': ^18.0.0 || ^19.0.0
+ '@types/react-dom': ^18.0.0 || ^19.0.0
+ '@vitest/browser': ^2.1.0 || ^3.0.0 || ^4.0.0-0
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+ vitest: ^2.1.0 || ^3.0.0 || ^4.0.0-0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
- vitest@3.1.3:
- resolution: {integrity: sha512-188iM4hAHQ0km23TN/adso1q5hhwKqUpv+Sd6p5sOuh6FhQnRNW3IsiIpvxqahtBabsJ2SLZgmGSpcYK4wQYJw==}
+ vitest@3.2.4:
+ resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@types/debug': ^4.1.12
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
- '@vitest/browser': 3.1.3
- '@vitest/ui': 3.1.3
+ '@vitest/browser': 3.2.4
+ '@vitest/ui': 3.2.4
happy-dom: '*'
jsdom: '*'
peerDependenciesMeta:
@@ -7530,6 +7513,18 @@ packages:
utf-8-validate:
optional: true
+ ws@8.18.3:
+ resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
+ engines: {node: '>=10.0.0'}
+ peerDependencies:
+ bufferutil: ^4.0.1
+ utf-8-validate: '>=5.0.2'
+ peerDependenciesMeta:
+ bufferutil:
+ optional: true
+ utf-8-validate:
+ optional: true
+
xml-name-validator@5.0.0:
resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
engines: {node: '>=18'}
@@ -8659,12 +8654,12 @@ snapshots:
'@orpc/shared': 1.5.2
'@orpc/standard-server': 1.5.2
- '@orpc/openapi@1.5.2(crossws@0.3.5)(ws@8.18.1)':
+ '@orpc/openapi@1.5.2(crossws@0.3.5)(ws@8.18.3)':
dependencies:
'@orpc/client': 1.5.2
'@orpc/contract': 1.5.2
'@orpc/openapi-client': 1.5.2
- '@orpc/server': 1.5.2(crossws@0.3.5)(ws@8.18.1)
+ '@orpc/server': 1.5.2(crossws@0.3.5)(ws@8.18.3)
'@orpc/shared': 1.5.2
'@orpc/standard-server': 1.5.2
json-schema-typed: 8.0.1
@@ -8683,7 +8678,7 @@ snapshots:
transitivePeerDependencies:
- '@tanstack/query-core'
- '@orpc/server@1.5.2(crossws@0.3.5)(ws@8.18.1)':
+ '@orpc/server@1.5.2(crossws@0.3.5)(ws@8.18.3)':
dependencies:
'@orpc/client': 1.5.2
'@orpc/contract': 1.5.2
@@ -8695,7 +8690,7 @@ snapshots:
'@orpc/standard-server-peer': 1.5.2
optionalDependencies:
crossws: 0.3.5
- ws: 8.18.1
+ ws: 8.18.3
'@orpc/shared@1.5.2':
dependencies:
@@ -8735,11 +8730,11 @@ snapshots:
'@orpc/shared': 1.5.2
'@tanstack/query-core': 5.81.5
- '@orpc/zod@1.5.2(@orpc/contract@1.5.2)(@orpc/server@1.5.2(crossws@0.3.5)(ws@8.18.1))(crossws@0.3.5)(ws@8.18.1)(zod@3.24.4)':
+ '@orpc/zod@1.5.2(@orpc/contract@1.5.2)(@orpc/server@1.5.2(crossws@0.3.5)(ws@8.18.3))(crossws@0.3.5)(ws@8.18.3)(zod@3.24.4)':
dependencies:
'@orpc/contract': 1.5.2
- '@orpc/openapi': 1.5.2(crossws@0.3.5)(ws@8.18.1)
- '@orpc/server': 1.5.2(crossws@0.3.5)(ws@8.18.1)
+ '@orpc/openapi': 1.5.2(crossws@0.3.5)(ws@8.18.3)
+ '@orpc/server': 1.5.2(crossws@0.3.5)(ws@8.18.3)
'@orpc/shared': 1.5.2
escape-string-regexp: 5.0.0
wildcard-match: 5.1.4
@@ -8849,9 +8844,9 @@ snapshots:
'@pkgjs/parseargs@0.11.0':
optional: true
- '@playwright/test@1.52.0':
+ '@playwright/test@1.54.1':
dependencies:
- playwright: 1.52.0
+ playwright: 1.54.1
'@polka/url@1.0.0-next.28': {}
@@ -10677,7 +10672,7 @@ snapshots:
'@testing-library/dom@10.4.0':
dependencies:
'@babel/code-frame': 7.27.1
- '@babel/runtime': 7.27.0
+ '@babel/runtime': 7.27.6
'@types/aria-query': 5.0.4
aria-query: 5.3.0
chalk: 4.1.2
@@ -10695,16 +10690,6 @@ snapshots:
lodash: 4.17.21
redent: 3.0.0
- '@testing-library/react@16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
- dependencies:
- '@babel/runtime': 7.26.10
- '@testing-library/dom': 10.4.0
- react: 19.1.0
- react-dom: 19.1.0(react@19.1.0)
- optionalDependencies:
- '@types/react': 19.1.3
- '@types/react-dom': 19.1.3(@types/react@19.1.3)
-
'@testing-library/user-event@14.6.1(@testing-library/dom@10.4.0)':
dependencies:
'@testing-library/dom': 10.4.0
@@ -10917,12 +10902,24 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@vitest/expect@3.1.3':
+ '@vitest/browser@3.2.4(playwright@1.54.1)(vite@6.3.5(@types/node@20.14.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.26.0)(tsx@4.19.3)(yaml@2.7.0))(vitest@3.2.4)':
dependencies:
- '@vitest/spy': 3.1.3
- '@vitest/utils': 3.1.3
- chai: 5.2.0
+ '@testing-library/dom': 10.4.0
+ '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.0)
+ '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@20.14.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.26.0)(tsx@4.19.3)(yaml@2.7.0))
+ '@vitest/utils': 3.2.4
+ magic-string: 0.30.17
+ sirv: 3.0.1
tinyrainbow: 2.0.0
+ vitest: 3.2.4(@types/node@20.14.10)(@vitest/browser@3.2.4)(happy-dom@17.4.4)(jiti@2.4.2)(jsdom@24.1.3)(lightningcss@1.30.1)(terser@5.26.0)(tsx@4.19.3)(yaml@2.7.0)
+ ws: 8.18.3
+ optionalDependencies:
+ playwright: 1.54.1
+ transitivePeerDependencies:
+ - bufferutil
+ - msw
+ - utf-8-validate
+ - vite
'@vitest/expect@3.2.4':
dependencies:
@@ -10932,58 +10929,34 @@ snapshots:
chai: 5.2.0
tinyrainbow: 2.0.0
- '@vitest/mocker@3.1.3(vite@6.3.5(@types/node@20.14.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.26.0)(tsx@4.19.3)(yaml@2.7.0))':
+ '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@20.14.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.26.0)(tsx@4.19.3)(yaml@2.7.0))':
dependencies:
- '@vitest/spy': 3.1.3
+ '@vitest/spy': 3.2.4
estree-walker: 3.0.3
magic-string: 0.30.17
optionalDependencies:
vite: 6.3.5(@types/node@20.14.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.26.0)(tsx@4.19.3)(yaml@2.7.0)
- '@vitest/pretty-format@3.1.3':
- dependencies:
- tinyrainbow: 2.0.0
-
'@vitest/pretty-format@3.2.4':
dependencies:
tinyrainbow: 2.0.0
- '@vitest/runner@3.1.3':
+ '@vitest/runner@3.2.4':
dependencies:
- '@vitest/utils': 3.1.3
+ '@vitest/utils': 3.2.4
pathe: 2.0.3
+ strip-literal: 3.0.0
- '@vitest/snapshot@3.1.3':
+ '@vitest/snapshot@3.2.4':
dependencies:
- '@vitest/pretty-format': 3.1.3
+ '@vitest/pretty-format': 3.2.4
magic-string: 0.30.17
pathe: 2.0.3
- '@vitest/spy@3.1.3':
- dependencies:
- tinyspy: 3.0.2
-
'@vitest/spy@3.2.4':
dependencies:
tinyspy: 4.0.3
- '@vitest/ui@3.1.3(vitest@3.1.3)':
- dependencies:
- '@vitest/utils': 3.1.3
- fflate: 0.8.2
- flatted: 3.3.3
- pathe: 2.0.3
- sirv: 3.0.1
- tinyglobby: 0.2.13
- tinyrainbow: 2.0.0
- vitest: 3.1.3(@types/node@20.14.10)(@vitest/ui@3.1.3)(happy-dom@17.4.4)(jiti@2.4.2)(jsdom@24.1.3)(lightningcss@1.30.1)(terser@5.26.0)(tsx@4.19.3)(yaml@2.7.0)
-
- '@vitest/utils@3.1.3':
- dependencies:
- '@vitest/pretty-format': 3.1.3
- loupe: 3.1.3
- tinyrainbow: 2.0.0
-
'@vitest/utils@3.2.4':
dependencies:
'@vitest/pretty-format': 3.2.4
@@ -11409,7 +11382,7 @@ snapshots:
assertion-error: 2.0.1
check-error: 2.1.1
deep-eql: 5.0.2
- loupe: 3.1.3
+ loupe: 3.1.4
pathval: 2.0.0
chalk@2.4.2:
@@ -11428,8 +11401,6 @@ snapshots:
ansi-styles: 4.3.0
supports-color: 7.2.0
- chalk@5.3.0: {}
-
check-error@2.1.1: {}
cheerio-select@2.1.0:
@@ -11778,6 +11749,10 @@ snapshots:
dependencies:
ms: 2.1.3
+ debug@4.4.1:
+ dependencies:
+ ms: 2.1.3
+
decache@4.6.2:
dependencies:
callsite: 1.0.0
@@ -12593,8 +12568,6 @@ snapshots:
node-domexception: 1.0.0
web-streams-polyfill: 3.3.3
- fflate@0.8.2: {}
-
file-entry-cache@8.0.0:
dependencies:
flat-cache: 4.0.1
@@ -13498,8 +13471,6 @@ snapshots:
safe-stable-stringify: 2.5.0
triple-beam: 1.4.1
- loupe@3.1.3: {}
-
loupe@3.1.4: {}
lower-case@2.0.2:
@@ -14146,11 +14117,11 @@ snapshots:
exsolve: 1.0.5
pathe: 2.0.3
- playwright-core@1.52.0: {}
+ playwright-core@1.54.1: {}
- playwright@1.52.0:
+ playwright@1.54.1:
dependencies:
- playwright-core: 1.52.0
+ playwright-core: 1.54.1
optionalDependencies:
fsevents: 2.3.2
@@ -15151,12 +15122,10 @@ snapshots:
fdir: 6.4.6(picomatch@4.0.2)
picomatch: 4.0.2
- tinypool@1.0.2: {}
+ tinypool@1.1.1: {}
tinyrainbow@2.0.0: {}
- tinyspy@3.0.2: {}
-
tinyspy@4.0.3: {}
tmp-promise@3.0.3:
@@ -15498,10 +15467,10 @@ snapshots:
- '@types/react'
- '@types/react-dom'
- vite-node@3.1.3(@types/node@20.14.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.26.0)(tsx@4.19.3)(yaml@2.7.0):
+ vite-node@3.2.4(@types/node@20.14.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.26.0)(tsx@4.19.3)(yaml@2.7.0):
dependencies:
cac: 6.7.14
- debug: 4.4.0
+ debug: 4.4.1
es-module-lexer: 1.7.0
pathe: 2.0.3
vite: 6.3.5(@types/node@20.14.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.26.0)(tsx@4.19.3)(yaml@2.7.0)
@@ -15547,42 +15516,44 @@ snapshots:
tsx: 4.19.3
yaml: 2.7.0
- vitest-axe@0.1.0(vitest@3.1.3):
+ vitest-browser-react@1.0.1(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(@vitest/browser@3.2.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@3.2.4):
dependencies:
- aria-query: 5.3.2
- axe-core: 4.10.3
- chalk: 5.3.0
- dom-accessibility-api: 0.5.16
- lodash-es: 4.17.21
- redent: 3.0.0
- vitest: 3.1.3(@types/node@20.14.10)(@vitest/ui@3.1.3)(happy-dom@17.4.4)(jiti@2.4.2)(jsdom@24.1.3)(lightningcss@1.30.1)(terser@5.26.0)(tsx@4.19.3)(yaml@2.7.0)
+ '@vitest/browser': 3.2.4(playwright@1.54.1)(vite@6.3.5(@types/node@20.14.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.26.0)(tsx@4.19.3)(yaml@2.7.0))(vitest@3.2.4)
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ vitest: 3.2.4(@types/node@20.14.10)(@vitest/browser@3.2.4)(happy-dom@17.4.4)(jiti@2.4.2)(jsdom@24.1.3)(lightningcss@1.30.1)(terser@5.26.0)(tsx@4.19.3)(yaml@2.7.0)
+ optionalDependencies:
+ '@types/react': 19.1.3
+ '@types/react-dom': 19.1.3(@types/react@19.1.3)
- vitest@3.1.3(@types/node@20.14.10)(@vitest/ui@3.1.3)(happy-dom@17.4.4)(jiti@2.4.2)(jsdom@24.1.3)(lightningcss@1.30.1)(terser@5.26.0)(tsx@4.19.3)(yaml@2.7.0):
+ vitest@3.2.4(@types/node@20.14.10)(@vitest/browser@3.2.4)(happy-dom@17.4.4)(jiti@2.4.2)(jsdom@24.1.3)(lightningcss@1.30.1)(terser@5.26.0)(tsx@4.19.3)(yaml@2.7.0):
dependencies:
- '@vitest/expect': 3.1.3
- '@vitest/mocker': 3.1.3(vite@6.3.5(@types/node@20.14.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.26.0)(tsx@4.19.3)(yaml@2.7.0))
- '@vitest/pretty-format': 3.1.3
- '@vitest/runner': 3.1.3
- '@vitest/snapshot': 3.1.3
- '@vitest/spy': 3.1.3
- '@vitest/utils': 3.1.3
+ '@types/chai': 5.2.2
+ '@vitest/expect': 3.2.4
+ '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@20.14.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.26.0)(tsx@4.19.3)(yaml@2.7.0))
+ '@vitest/pretty-format': 3.2.4
+ '@vitest/runner': 3.2.4
+ '@vitest/snapshot': 3.2.4
+ '@vitest/spy': 3.2.4
+ '@vitest/utils': 3.2.4
chai: 5.2.0
- debug: 4.4.0
+ debug: 4.4.1
expect-type: 1.2.1
magic-string: 0.30.17
pathe: 2.0.3
+ picomatch: 4.0.2
std-env: 3.9.0
tinybench: 2.9.0
tinyexec: 0.3.2
- tinyglobby: 0.2.13
- tinypool: 1.0.2
+ tinyglobby: 0.2.14
+ tinypool: 1.1.1
tinyrainbow: 2.0.0
vite: 6.3.5(@types/node@20.14.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.26.0)(tsx@4.19.3)(yaml@2.7.0)
- vite-node: 3.1.3(@types/node@20.14.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.26.0)(tsx@4.19.3)(yaml@2.7.0)
+ vite-node: 3.2.4(@types/node@20.14.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.26.0)(tsx@4.19.3)(yaml@2.7.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 20.14.10
- '@vitest/ui': 3.1.3(vitest@3.1.3)
+ '@vitest/browser': 3.2.4(playwright@1.54.1)(vite@6.3.5(@types/node@20.14.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.26.0)(tsx@4.19.3)(yaml@2.7.0))(vitest@3.2.4)
happy-dom: 17.4.4
jsdom: 24.1.3
transitivePeerDependencies:
@@ -15734,6 +15705,8 @@ snapshots:
ws@8.18.1: {}
+ ws@8.18.3: {}
+
xml-name-validator@5.0.0: {}
xmlbuilder2@3.1.1:
diff --git a/vitest.config.ts b/vitest.config.ts
index 01cd21841..ac33b5948 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -2,16 +2,53 @@ import react from '@vitejs/plugin-react';
import path from 'node:path';
import { defineConfig } from 'vitest/config';
+const resolve = (filePath: string) => path.resolve(__dirname, filePath);
+
export default defineConfig({
plugins: [react()],
test: {
- environment: 'jsdom',
- include: ['app/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
- setupFiles: [path.resolve(__dirname, 'app/tests/setup.ts')],
- },
- resolve: {
- alias: {
- '@': path.resolve(__dirname, './app'),
- },
+ projects: [
+ {
+ test: {
+ name: 'browser',
+ browser: {
+ enabled: true,
+ provider: 'playwright',
+ // https://vitest.dev/guide/browser/playwright
+ instances: [
+ {
+ browser: 'chromium',
+ context: {
+ permissions: ['clipboard-write', 'clipboard-read'],
+ },
+ },
+ ],
+ },
+ include: ['app/**/*.browser.{test,spec}.?(c|m)[jt]s?(x)'],
+ setupFiles: [
+ resolve('app/tests/setup.base.ts'),
+ resolve('app/tests/setup.browser.ts'),
+ ],
+ },
+ resolve: {
+ alias: {
+ '@': resolve('./app'),
+ },
+ },
+ },
+ {
+ test: {
+ name: 'unit',
+ environment: 'node',
+ include: ['app/**/*.unit.{test,spec}.?(c|m)[jt]s?(x)'],
+ setupFiles: [resolve('app/tests/setup.base.ts')],
+ },
+ resolve: {
+ alias: {
+ '@': resolve('./app'),
+ },
+ },
+ },
+ ],
},
});