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'), + }, + }, + }, + ], }, });