Maintenance: Upgrade to Vitest 5 and make the frontend tests 4 times faster.

Vitest is updated to v5 and the pool is switched to `vmForks`, which cuts the
runtime of the frontend suite from ~250s to ~69s.

The new jsdom no longer allows `window.navigation` to be overridden, so the
tests that need it now go through a small `#shared/utils/navigation.ts`
indirection which can be mocked instead.

Co-authored-by: Vladimir Sheremet <vsheremet@cloudflare.com>
This commit is contained in:
Vladimir Sheremet
2026-09-04 10:07:16 +01:00
committed by Benjamin Scharf
parent db07311818
commit 4e489121c5
23 changed files with 172 additions and 204 deletions

1
.gitignore vendored
View File

@@ -107,6 +107,7 @@
# Node
**/node_modules
/.vitest
yarn-error.log
.yarn/cache
/.pnpm-store

View File

@@ -7,6 +7,8 @@ import { mockApplicationConfig } from '#tests/support/mock-applicationConfig.ts'
import { mockUserCurrent } from '#tests/support/mock-userCurrent.ts'
import { waitFor } from '#tests/support/vitest-wrapper.ts'
import { navigateTo } from '#shared/utils/navigation.ts'
import { useBetaUi } from '#desktop/components/BetaUi/composables/useBetaUi.ts'
import { useAppUsageStore } from '#desktop/stores/appUsage.ts'
@@ -18,6 +20,8 @@ vi.mock('#shared/composables/useConfirmation.ts', () => ({
}),
}))
vi.mock('#shared/utils/navigation.ts')
vi.mock(
'#desktop/components/BetaUi/FeedbackDialog/useFeedbackDialog.ts',
async (originalModule) => {
@@ -94,13 +98,6 @@ describe('useNewBetaUi', () => {
setActivePinia(createPinia())
vi.doMock('#shared/utils/pwa.ts')
Object.defineProperty(window, 'location', {
value: {
...window.location,
pathname: '/desktop',
href: '/desktop',
},
})
})
it('sets switchValue and hasFeedbackConsent to null', () => {
@@ -130,11 +127,9 @@ describe('useNewBetaUi', () => {
it('redirects to root URL', () => {
const { toggleBetaUiSwitch } = useBetaUi()
expect(window.location.href).toBe('/desktop')
toggleBetaUiSwitch()
expect(window.location.href).toBe('/')
expect(navigateTo).toHaveBeenCalledWith('/')
})
it('does not re-trigger milestone dialog after switching back to old UI', async () => {

View File

@@ -5,6 +5,7 @@ import { computed, toRef } from 'vue'
import { useConfirmation } from '#shared/composables/useConfirmation.ts'
import { useSessionStore } from '#shared/stores/session.ts'
import { navigateTo } from '#shared/utils/navigation.ts'
import { useAppUsageStore } from '#desktop/stores/appUsage.ts'
@@ -27,7 +28,7 @@ export const initializeBetaUi = () => {
const clearSwitchAndRedirect = (redirectTo: string) => {
clearSwitch()
window.location.href = redirectTo
navigateTo(redirectTo)
}
return { switchValue, clearSwitch, clearSwitchAndRedirect, betaUiSwitchAvailable }

View File

@@ -2,6 +2,8 @@
import { waitFor } from '#tests/support/vitest-wrapper.ts'
import { navigateTo } from '#shared/utils/navigation.ts'
import { MOBILE_SLUG } from '#desktop/composables/responsiveness/useMobileNavigation.ts'
import continueToMobileItem from '../continue-to-mobile.ts'
@@ -23,20 +25,12 @@ vi.mock('#shared/utils/browser.ts', () => ({
},
}))
vi.mock('#shared/utils/navigation.ts')
describe('avatar menu continue-to-mobile plugin', () => {
beforeEach(() => {
setIsMobile(false)
localStorage.removeItem('forceDesktopApp')
Object.defineProperty(window, 'location', {
configurable: true,
writable: true,
value: {
...window.location,
pathname: '/desktop',
href: '/desktop',
},
})
})
it('is hidden by default on non-mobile devices', () => {
@@ -69,8 +63,6 @@ describe('avatar menu continue-to-mobile plugin', () => {
expect(localStorage.getItem('forceDesktopApp')).toBeNull()
})
await waitFor(() => {
expect(window.location.href).toBe(MOBILE_SLUG)
})
await waitFor(() => expect(navigateTo).toHaveBeenCalledWith(MOBILE_SLUG))
})
})

View File

@@ -7,6 +7,8 @@ import { mockApplicationConfig } from '#tests/support/mock-applicationConfig.ts'
import { mockUserCurrent } from '#tests/support/mock-userCurrent.ts'
import { waitForNextTick } from '#tests/support/utils.ts'
import { navigateTo } from '#shared/utils/navigation.ts'
import LayoutPage from '#desktop/components/layout/LayoutPage.vue'
import '#tests/graphql/builders/mocks.ts'
@@ -20,6 +22,8 @@ vi.mock('#shared/server/apollo/client.ts', () => ({
}),
}))
vi.mock('#shared/utils/navigation.ts')
vi.mock(
'#desktop/components/BetaUi/FeedbackDialog/useFeedbackDialog.ts',
async (originalModule) => {
@@ -77,16 +81,6 @@ describe('LayoutPage', () => {
})
describe('Feature: Beta UI Switch', () => {
beforeAll(() => {
Object.defineProperty(window, 'location', {
value: {
...window.location,
pathname: '/desktop',
href: '/desktop',
},
})
})
beforeEach(() => {
mockApplicationConfig({
ui_desktop_beta_switch: true,
@@ -107,11 +101,9 @@ describe('LayoutPage', () => {
expect(toggle).toBeChecked()
expect(window.location.href).toBe('/desktop')
await wrapper.events.click(toggle)
await waitFor(() => expect(window.location.href).toBe('/'))
await waitFor(() => expect(navigateTo).toHaveBeenCalledWith('/'))
})
it('hides the switch if the feature is disabled', async () => {

View File

@@ -3,6 +3,8 @@
import { useLocalStorage } from '@vueuse/core'
import { readonly } from 'vue'
import { navigateTo } from '#shared/utils/navigation.ts'
export const MOBILE_SLUG = '/mobile'
export const useMobileNavigation = () => {
@@ -19,7 +21,7 @@ export const useMobileNavigation = () => {
}
const continueToMobile = () => {
window.location.href = MOBILE_SLUG
navigateTo(MOBILE_SLUG)
}
return {

View File

@@ -7,6 +7,9 @@ import { mockPermissions } from '#tests/support/mock-permissions.ts'
import { mockUserQuery } from '#shared/entities/user/graphql/queries/user.mocks.ts'
import type { User } from '#shared/graphql/types.ts'
import { navigateTo } from '#shared/utils/navigation.ts'
vi.mock('#shared/utils/navigation.ts')
const user: User = {
__typename: 'User',
@@ -59,14 +62,6 @@ describe('User detail view: Delete user', () => {
beforeEach(() => {
mockPermissions(['admin.user'])
mockUserQuery({ user })
Object.defineProperty(window, 'location', {
value: {
...window.location,
pathname: '/desktop',
href: '/desktop',
},
})
})
it('redirects to admin data privacy panel with customer pre-population', async () => {
@@ -81,6 +76,6 @@ describe('User detail view: Delete user', () => {
await view.events.click(within(popover).getByRole('button', { name: 'Delete' }))
expect(window.location.href).toBe('/#system/data_privacy/2')
expect(navigateTo).toHaveBeenCalledWith('/#system/data_privacy/2')
})
})

View File

@@ -1,6 +1,7 @@
// Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
import type { User } from '#shared/graphql/types.ts'
import { navigateTo } from '#shared/utils/navigation.ts'
import { initializeBetaUi } from '#desktop/components/BetaUi/composables/useBetaUi.ts'
import type { DetailViewActionPlugin } from '#desktop/types/actions.ts'
@@ -19,7 +20,7 @@ export default <DetailViewActionPlugin>{
const url = `/#system/data_privacy/${user.internalId}`
if (!switchValue.value) {
window.location.href = url
navigateTo(url)
return
}

View File

@@ -14,8 +14,6 @@ import {
import { getTicketOverviewStorage } from '#mobile/entities/ticket/helpers/ticketOverviewStorage.ts'
const actualLocalStorage = window.localStorage
describe('playing with overviews', () => {
beforeEach(() => {
mockUserCurrent({ id: '666' })
@@ -23,7 +21,7 @@ describe('playing with overviews', () => {
})
afterEach(() => {
window.localStorage = actualLocalStorage
vi.unstubAllGlobals()
const { saveOverviews } = getTicketOverviewStorage()
saveOverviews([])
})

View File

@@ -410,12 +410,7 @@ describe('links handling', () => {
beforeEach(() => {
vi.mocked(isStandalone).mockReturnValue(false)
window.open = open
Object.defineProperty(window, 'location', {
value: {
...window.location,
pathname: '/mobile',
},
})
jsdom.reconfigure({ url: 'http://localhost:3000/mobile' })
})
const clickLink = async (href: string, target = '') => {

View File

@@ -194,6 +194,13 @@ describe('newVideoRowAt', () => {
const renderEditor = (content: string) =>
new Editor({ extensions: [Document, Paragraph, Text], content })
beforeEach(() => vi.useFakeTimers())
afterEach(() => {
vi.runAllTimers()
vi.useRealTimers()
})
it('gives an empty row to the video', () => {
const editor = renderEditor('<p></p>')

View File

@@ -11,9 +11,12 @@ import { useNotifications } from '#shared/components/CommonNotifications/index.t
import { ApplicationBuildChecksumDocument } from '#shared/graphql/queries/applicationBuildChecksum.api.ts'
import { AppMaintenanceDocument } from '#shared/graphql/subscriptions/appMaintenance.api.ts'
import { EnumAppMaintenanceType } from '#shared/graphql/types.ts'
import { reloadPage } from '#shared/utils/navigation.ts'
import useAppMaintenanceCheck from '../useAppMaintenanceCheck.ts'
vi.mock('#shared/utils/navigation.ts')
let subscriptionAppMaintenance: ExtendedIMockSubscription
describe('useAppMaintenanceCheck', () => {
@@ -110,9 +113,6 @@ describe('useAppMaintenanceCheck', () => {
})
it('reacts to force_refresh message by reloading without showing a notification', async () => {
const reloadMock = vi.fn()
vi.stubGlobal('location', { ...window.location, reload: reloadMock })
await subscriptionAppMaintenance.next({
data: {
appMaintenance: {
@@ -121,7 +121,7 @@ describe('useAppMaintenanceCheck', () => {
},
})
expect(reloadMock).toHaveBeenCalledOnce()
expect(reloadPage).toHaveBeenCalledOnce()
expect(useNotifications().notifications.value).toHaveLength(0)
})

View File

@@ -18,6 +18,7 @@ import type {
} from '#shared/graphql/types.ts'
import { EnumAppMaintenanceType } from '#shared/graphql/types.ts'
import { QueryHandler, SubscriptionHandler } from '#shared/server/apollo/handler/index.ts'
import { reloadPage } from '#shared/utils/navigation.ts'
import testFlags from '#shared/utils/testFlags.ts'
import { useQueryPolling } from './useQueryPolling.ts'
@@ -96,7 +97,7 @@ const useAppMaintenanceCheck = (maintenanceOptions: UseAppMaintenanceCheckOption
actionLabel: __('Reload now'),
actionCallback: () => {
maintenanceOptions.onNeedRefresh?.()
window.location.reload()
reloadPage()
},
})
})
@@ -122,7 +123,7 @@ const useAppMaintenanceCheck = (maintenanceOptions: UseAppMaintenanceCheckOption
break
case EnumAppMaintenanceType.ForceRefresh:
maintenanceOptions.onNeedRefresh?.()
window.location.reload()
reloadPage()
return
default:
break
@@ -131,7 +132,7 @@ const useAppMaintenanceCheck = (maintenanceOptions: UseAppMaintenanceCheckOption
notify({
message,
actionLabel: __('Reload now'),
actionCallback: () => window.location.reload(),
actionCallback: reloadPage,
})
})
})

View File

@@ -4,6 +4,7 @@ import { ref } from 'vue'
import { NotificationTypes } from '#shared/components/CommonNotifications/types.ts'
import { useNotifications } from '#shared/components/CommonNotifications/useNotifications.ts'
import UserError from '#shared/errors/UserError.ts'
import { convertToGraphQLId } from '#shared/graphql/utils.ts'
import { useTicketArticleRetryMediaDownload } from '../composables/useTicketArticleRetryMediaDownload.ts'
@@ -34,7 +35,7 @@ describe('useTicketArticleRetryMediaDownload', () => {
})
})
it('rejects on error', async () => {
it('rejects with the user error on error', async () => {
mockTicketArticleRetryMediaDownloadMutation({
ticketArticleRetryMediaDownload: {
success: false,
@@ -46,7 +47,26 @@ describe('useTicketArticleRetryMediaDownload', () => {
},
})
await expect(tryAgain()).rejects.toThrow('')
await expect(tryAgain()).rejects.toSatisfy(
(error: UserError) =>
error instanceof UserError && error.getFirstErrorMessage() === 'Something went wrong',
)
const calls = await waitForTicketArticleRetryMediaDownloadMutationCalls()
expect(calls.at(-1)?.variables).toEqual({
articleId: testArticleId.value,
})
})
it('rejects with a generic failure message on unsuccessful download', async () => {
mockTicketArticleRetryMediaDownloadMutation({
ticketArticleRetryMediaDownload: {
success: false,
},
})
await expect(tryAgain()).rejects.toThrow('Media download failed.')
const calls = await waitForTicketArticleRetryMediaDownloadMutationCalls()

View File

@@ -0,0 +1,7 @@
// Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
export const navigateTo = (url: string) => {
window.location.href = url
}
export const reloadPage = () => window.location.reload()

View File

@@ -18,5 +18,5 @@ interface CustomMatchers<R = unknown> {
declare module 'vitest' {
// oxlint-disable-next-line @typescript-eslint/no-empty-object-type
interface Assertion<T = any> extends CustomMatchers<T> {}
interface Matchers<R, T> extends CustomMatchers<R> {}
}

View File

@@ -1,13 +1,16 @@
// Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
export const mockRouterHooks = () => {
vi.mock('vue-router', async () => {
const module = await vi.importActual<typeof import('vue-router')>('vue-router')
vi.mock('vue-router', async () => {
const module = await vi.importActual<typeof import('vue-router')>('vue-router')
return {
...module,
onBeforeRouteUpdate: vi.fn(),
onBeforeRouteLeave: vi.fn(),
}
})
}
return {
...module,
onBeforeRouteUpdate: vi.fn(),
onBeforeRouteLeave: vi.fn(),
}
})
// this function never did anything, `vi.mock` is always executed as the first statement,
// but vitest 5 now fails with a hard error if it sees `vi.mock` being called outside of the top-level scope
// keeping the function for backwards compatibility, but it is not needed anymore
export const mockRouterHooks = () => undefined

View File

@@ -5,7 +5,7 @@ import { setup as setupA11y, toBeAccessible } from '@sa11y/vitest'
import * as domMatchers from '@testing-library/jest-dom/matchers'
import { toBeDisabled } from '@testing-library/jest-dom/matchers'
import { configure } from '@testing-library/vue'
import { expect, vi } from 'vitest'
import { expect, vi, type MatchersObject } from 'vitest'
import { ServiceWorkerHelper } from '#shared/utils/testSw.ts'
@@ -282,13 +282,13 @@ afterEach((context) => {
})
setupA11y()
// There is a problem that sa11y uses still vitest v3
// @sa11y/vitest still depends on older Vitest APIs, so keep explicit registration
// until it officially supports Vitest 5.
// https://github.com/salesforce/sa11y/blob/master/packages/vitest/package.json
// In vitest v.4 we still need to manually provide the assertion api
expect.extend({ toBeAccessible })
expect.extend(assertions)
expect.extend(domMatchers)
expect.extend(assertions as unknown as MatchersObject)
expect.extend(domMatchers as unknown as MatchersObject)
expect.extend({
// allow aria-disabled in toBeDisabled
@@ -313,19 +313,12 @@ declare module 'vitest' {
skipConsole: boolean
}
interface Assertion<T = any> extends TestingLibraryMatchers<typeof expect.stringContaining, T> {
interface Matchers<R, T> extends TestingLibraryMatchers<R, T> {
/**
* @param options - Allow passing custom rulesets
* @sa11y/preset-rule base, extend, full
*/
toBeAccessible(options?: Parameters<typeof toBeAccessible>[1]): Promise<void>
}
interface AsymmetricMatchersContaining extends TestingLibraryMatchers<any, any> {
/**
* @param options - Allow passing custom rulesets
* @sa11y/preset-rule base, extend, full
*/
toBeAccessible(options?: Parameters<typeof toBeAccessible>[1]): Promise<void>
toBeAccessible(options?: Parameters<typeof toBeAccessible>[1]): R
}
}

View File

@@ -105,7 +105,7 @@
"vite": "^8.2.2",
"vite-plugin-pwa": "^1.3.0",
"vite-plugin-ruby": "^5.2.3",
"vitest": "^4.1.11",
"vitest": "^5.0.0",
"vue-tsc": "^3.3.11"
},
"dependencies": {

161
pnpm-lock.yaml generated
View File

@@ -4,6 +4,9 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
overrides:
'@sa11y/vitest>vitest': ^5.0.0
importers:
.:
@@ -299,7 +302,7 @@ importers:
version: 4.3.3(vite@8.2.2(@types/node@26.2.0)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))
'@testing-library/jest-dom':
specifier: ^7.0.1
version: 7.0.1(@testing-library/dom@10.4.1)(vitest@4.1.11(@types/node@26.2.0)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.2.0)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)))
version: 7.0.1(@testing-library/dom@10.4.1)(vitest@5.0.0(@types/node@26.2.0)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.2.0)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)))
'@testing-library/user-event':
specifier: ^14.6.6
version: 14.6.6(@testing-library/dom@10.4.1)
@@ -445,8 +448,8 @@ importers:
specifier: ^5.2.3
version: 5.2.3(vite@8.2.2(@types/node@26.2.0)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))
vitest:
specifier: ^4.1.11
version: 4.1.11(@types/node@26.2.0)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.2.0)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))
specifier: ^5.0.0
version: 5.0.0(@types/node@26.2.0)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.2.0)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))
vue-tsc:
specifier: ^3.3.11
version: 3.3.11(typescript@6.0.3)
@@ -2288,9 +2291,6 @@ packages:
'@sinonjs/fake-timers@15.4.0':
resolution: {integrity: sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==}
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
'@tailwindcss/node@4.3.3':
resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==}
@@ -2965,11 +2965,8 @@ packages:
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
vue: ^3.2.25
'@vitest/expect@4.1.11':
resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==}
'@vitest/mocker@4.1.11':
resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==}
'@vitest/mocker@5.0.0':
resolution: {integrity: sha512-66PGTMIiVJP3t4a5yxU9qPtf7MdTBs8jmToMvy+HVflB3Yy13WJZTtPePdvU+wjRV02SKK5doLbSA6o9pwOmiA==}
peerDependencies:
msw: ^2.4.9
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
@@ -2982,27 +2979,15 @@ packages:
'@vitest/pretty-format@3.2.7':
resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==}
'@vitest/pretty-format@4.1.11':
resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==}
'@vitest/runner@3.2.7':
resolution: {integrity: sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==}
'@vitest/runner@4.1.11':
resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==}
'@vitest/snapshot@4.1.11':
resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==}
'@vitest/spy@4.1.11':
resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==}
'@vitest/spy@5.0.0':
resolution: {integrity: sha512-uy+luWBAPw9XfthoHi5AkfHUnuPYEESjl0p/r+meoBnU8bxg5GDQ3Ey8MjcJ6sqahkL4PFyrvfMJJBw7LbU06g==}
'@vitest/utils@3.2.7':
resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==}
'@vitest/utils@4.1.11':
resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==}
'@volar/language-core@2.4.28':
resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==}
@@ -3765,8 +3750,8 @@ packages:
es-get-iterator@1.1.3:
resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==}
es-module-lexer@2.3.1:
resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==}
es-module-lexer@2.3.2:
resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==}
es-object-atoms@1.1.2:
resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==}
@@ -4852,6 +4837,9 @@ packages:
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
magic-string@1.2.3:
resolution: {integrity: sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g==}
map-cache@0.2.2:
resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==}
engines: {node: '>=0.10.0'}
@@ -5247,6 +5235,10 @@ packages:
resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==}
engines: {node: '>=12'}
picomatch@4.0.7:
resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
engines: {node: '>=12'}
pinia@4.0.3:
resolution: {integrity: sha512-XMQqpvjgG7LMqVhhFUzKLT4KEbsYbfZZ0CZU9PgdFm1O2VKBmIkykL8+SfNhOaT7sR0wT6BEgKT0YNDYWgmVRA==}
peerDependencies:
@@ -5911,8 +5903,9 @@ packages:
timezone-mock@1.4.3:
resolution: {integrity: sha512-sO5xj1j5bXgKPgUzbM/t0l2O3hZ2F22cczYRo7Kj2Pm8wfbMMw3ohyAunBBP0Vltkd4X3tF0rFu0WAlGvAClGg==}
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
tinybench@6.1.4:
resolution: {integrity: sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==}
engines: {node: '>=20.0.0'}
tinyexec@1.3.0:
resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==}
@@ -5930,10 +5923,6 @@ packages:
resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==}
engines: {node: '>=14.0.0'}
tinyrainbow@3.1.1:
resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==}
engines: {node: '>=14.0.0'}
title-case@3.0.3:
resolution: {integrity: sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==}
@@ -6234,23 +6223,23 @@ packages:
yaml:
optional: true
vitest@4.1.11:
resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==}
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
vitest@5.0.0:
resolution: {integrity: sha512-gpsMNoRhMjMktVxPtstOH4/PJuPyovVaMDr4oDilXaGH1EcqM2OE96SoHT2VIQ6fTGtTjqmHDrEu2X9RQiXf8Q==}
engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@opentelemetry/api': ^1.9.0
'@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
'@vitest/browser-playwright': 4.1.11
'@vitest/browser-preview': 4.1.11
'@vitest/browser-webdriverio': 4.1.11
'@vitest/coverage-istanbul': 4.1.11
'@vitest/coverage-v8': 4.1.11
'@vitest/ui': 4.1.11
'@types/node': ^22.0.0 || >=24.0.0
'@vitest/browser-playwright': 5.0.0
'@vitest/browser-preview': 5.0.0
'@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0
'@vitest/coverage-istanbul': 5.0.0
'@vitest/coverage-v8': 5.0.0
'@vitest/ui': 5.0.0
happy-dom: '*'
jsdom: '*'
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
vite: ^6.4.0 || ^7.0.0 || ^8.0.0
peerDependenciesMeta:
'@edge-runtime/vm':
optional: true
@@ -8383,7 +8372,7 @@ snapshots:
dependencies:
'@types/estree': 1.0.9
estree-walker: 2.0.2
picomatch: 4.0.5
picomatch: 4.0.7
optionalDependencies:
rollup: 4.62.4
@@ -8496,7 +8485,7 @@ snapshots:
'@sa11y/matcher': 8.0.28
'@sa11y/preset-rules': 8.0.28
'@vitest/runner': 3.2.7
vitest: 4.1.11(@types/node@26.2.0)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.2.0)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))
vitest: 5.0.0(@types/node@26.2.0)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.2.0)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))
transitivePeerDependencies:
- '@edge-runtime/vm'
- '@opentelemetry/api'
@@ -8522,8 +8511,6 @@ snapshots:
dependencies:
'@sinonjs/commons': 3.0.1
'@standard-schema/spec@1.1.0': {}
'@tailwindcss/node@4.3.3':
dependencies:
'@jridgewell/remapping': 2.3.5
@@ -8614,7 +8601,7 @@ snapshots:
lz-string: 1.5.0
pretty-format: 27.5.1
'@testing-library/jest-dom@7.0.1(@testing-library/dom@10.4.1)(vitest@4.1.11(@types/node@26.2.0)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.2.0)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)))':
'@testing-library/jest-dom@7.0.1(@testing-library/dom@10.4.1)(vitest@5.0.0(@types/node@26.2.0)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.2.0)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)))':
dependencies:
'@adobe/css-tools': 4.5.0
'@testing-library/dom': 10.4.1
@@ -8624,7 +8611,7 @@ snapshots:
picocolors: 1.1.1
redent: 3.0.0
optionalDependencies:
vitest: 4.1.11(@types/node@26.2.0)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.2.0)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))
vitest: 5.0.0(@types/node@26.2.0)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.2.0)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))
'@testing-library/user-event@14.6.6(@testing-library/dom@10.4.1)':
dependencies:
@@ -9200,20 +9187,12 @@ snapshots:
vite: 8.2.2(@types/node@26.2.0)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)
vue: 3.5.42(typescript@6.0.3)
'@vitest/expect@4.1.11':
'@vitest/mocker@5.0.0(vite@8.2.2(@types/node@26.2.0)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))':
dependencies:
'@standard-schema/spec': 1.1.0
'@types/chai': 5.2.3
'@vitest/spy': 4.1.11
'@vitest/utils': 4.1.11
chai: 6.2.2
tinyrainbow: 3.1.1
'@vitest/mocker@4.1.11(vite@8.2.2(@types/node@26.2.0)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))':
dependencies:
'@vitest/spy': 4.1.11
'@jridgewell/trace-mapping': 0.3.31
'@vitest/spy': 5.0.0
estree-walker: 3.0.3
magic-string: 0.30.21
magic-string: 1.2.3
optionalDependencies:
vite: 8.2.2(@types/node@26.2.0)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)
@@ -9221,29 +9200,13 @@ snapshots:
dependencies:
tinyrainbow: 2.0.0
'@vitest/pretty-format@4.1.11':
dependencies:
tinyrainbow: 3.1.1
'@vitest/runner@3.2.7':
dependencies:
'@vitest/utils': 3.2.7
pathe: 2.0.3
strip-literal: 3.1.0
'@vitest/runner@4.1.11':
dependencies:
'@vitest/utils': 4.1.11
pathe: 2.0.3
'@vitest/snapshot@4.1.11':
dependencies:
'@vitest/pretty-format': 4.1.11
'@vitest/utils': 4.1.11
magic-string: 0.30.21
pathe: 2.0.3
'@vitest/spy@4.1.11': {}
'@vitest/spy@5.0.0': {}
'@vitest/utils@3.2.7':
dependencies:
@@ -9251,12 +9214,6 @@ snapshots:
loupe: 3.2.1
tinyrainbow: 2.0.0
'@vitest/utils@4.1.11':
dependencies:
'@vitest/pretty-format': 4.1.11
convert-source-map: 2.0.0
tinyrainbow: 3.1.1
'@volar/language-core@2.4.28':
dependencies:
'@volar/source-map': 2.4.28
@@ -10155,7 +10112,7 @@ snapshots:
isarray: 2.0.5
stop-iteration-iterator: 1.1.0
es-module-lexer@2.3.1: {}
es-module-lexer@2.3.2: {}
es-object-atoms@1.1.2:
dependencies:
@@ -11225,6 +11182,10 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
magic-string@1.2.3:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
map-cache@0.2.2: {}
markdown-it@14.3.0:
@@ -11735,6 +11696,8 @@ snapshots:
picomatch@4.0.5: {}
picomatch@4.0.7: {}
pinia@4.0.3(@vue/devtools-api@8.2.1)(typescript@6.0.3)(vue@3.5.42(typescript@6.0.3)):
dependencies:
'@vue/devtools-api': 8.2.1
@@ -12510,7 +12473,7 @@ snapshots:
timezone-mock@1.4.3: {}
tinybench@2.9.0: {}
tinybench@6.1.4: {}
tinyexec@1.3.0: {}
@@ -12523,8 +12486,6 @@ snapshots:
tinyrainbow@2.0.0: {}
tinyrainbow@3.1.1: {}
title-case@3.0.3:
dependencies:
tslib: 2.8.1
@@ -12787,26 +12748,20 @@ snapshots:
terser: 5.50.0
yaml: 2.9.0
vitest@4.1.11(@types/node@26.2.0)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.2.0)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)):
vitest@5.0.0(@types/node@26.2.0)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.2.0)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)):
dependencies:
'@vitest/expect': 4.1.11
'@vitest/mocker': 4.1.11(vite@8.2.2(@types/node@26.2.0)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))
'@vitest/pretty-format': 4.1.11
'@vitest/runner': 4.1.11
'@vitest/snapshot': 4.1.11
'@vitest/spy': 4.1.11
'@vitest/utils': 4.1.11
es-module-lexer: 2.3.1
'@types/chai': 5.2.3
'@vitest/mocker': 5.0.0(vite@8.2.2(@types/node@26.2.0)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0))
chai: 6.2.2
es-module-lexer: 2.3.2
expect-type: 1.4.0
magic-string: 0.30.21
magic-string: 1.2.3
obug: 2.1.4
pathe: 2.0.3
picomatch: 4.0.5
picomatch: 4.0.7
std-env: 4.2.0
tinybench: 2.9.0
tinybench: 6.1.4
tinyexec: 1.3.0
tinyglobby: 0.2.17
tinyrainbow: 3.1.1
vite: 8.2.2(@types/node@26.2.0)(jiti@2.7.0)(terser@5.50.0)(yaml@2.9.0)
why-is-node-running: 2.3.0
optionalDependencies:

View File

@@ -4,4 +4,10 @@ allowBuilds:
esbuild: true
unrs-resolver: true
vue-demi: true
minimumReleaseAgeExclude:
- '@vitest/mocker@5.0.0'
- '@vitest/spy@5.0.0'
- vitest@5.0.0
overrides:
'@sa11y/vitest>vitest': ^5.0.0
verifyDepsBeforeRun: false

View File

@@ -22,7 +22,7 @@
"#tests/*": ["./app/frontend/tests/*"],
"#cy/*": ["./.dev/cypress/*"],
},
"types": ["vitest/globals", "vite/client", "vite-plugin-pwa/client"],
"types": ["vitest/globals", "vitest/jsdom", "vite/client", "vite-plugin-pwa/client"],
"esModuleInterop": true,
"experimentalDecorators": true,
"skipLibCheck": true,

View File

@@ -77,6 +77,11 @@ export default defineConfig(async ({ mode, command }) => {
)
}
if (isTesting) {
// make sure timezone is always correct - in CLI and in the extension
process.env.TZ = 'utc'
}
let https = false
// vite-ruby controlls this variable, it's either "true" or "false"
@@ -192,6 +197,7 @@ export default defineConfig(async ({ mode, command }) => {
root: './app/frontend',
setupFiles: ['./tests/vitest.setup.ts'],
environment: 'jsdom',
pool: 'vmForks',
env: {
SA11Y_RULESET: 'full',
},
@@ -199,6 +205,12 @@ export default defineConfig(async ({ mode, command }) => {
css: false,
testTimeout: isEnvBooleanSet(process.env.CI) ? 30_000 : 5_000,
unstubGlobals: true,
// Persistent cache between reruns, invalidates if any dependency is updated.
// Vitest doesn't cache files with `import.meta.glob` inside, so it might be a good
// idea to put them all inside as few files as possible just for a small perf boost.
// The perf difference is noticeable when running a single/few tests, but has
// negligible impact when running the whole suite due to parallelisation.
fsModuleCache: true,
// Node v25+ enables experimental webstorage by default (stability: release candidate).
// Without --localstorage-file, Node provides localStorage as an empty object (no methods).
// This conflicts with jsdom's full localStorage implementation needed for tests.
@@ -227,14 +239,6 @@ export default defineConfig(async ({ mode, command }) => {
replacement: join(dirname(require.resolve('date-fns/package.json')), 'index.cjs'),
},
],
experimental: {
// persistent cache between reruns, invalidates if any dependency is updated
// vitest doesn't cache files with `import.meta.glob` inside, so it might be a good
// idea to put them all inside as few files as possible just for a small perf boost
// the perf difference is noticible when running a single/a few tests, but has
// negligible impact when running the whole suite due to parallelisation
fsModuleCache: true,
},
},
plugins,
}