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>
246 lines
8.4 KiB
JavaScript
246 lines
8.4 KiB
JavaScript
// Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
|
|
|
/// <reference types="vitest/config" />
|
|
|
|
import { createRequire } from 'module'
|
|
import { readFileSync } from 'node:fs'
|
|
import { resolve, dirname, join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { homedir } from 'os'
|
|
|
|
import tailwindcss from '@tailwindcss/vite'
|
|
import VuePlugin from '@vitejs/plugin-vue'
|
|
import { defineConfig } from 'vite'
|
|
import { VitePWA } from 'vite-plugin-pwa'
|
|
|
|
import { discoverAddonWeaveRules } from './app/frontend/build/addonWeave/discoverRules.mjs'
|
|
import addonWeavePlugin from './app/frontend/build/addonWeave/plugin.mjs'
|
|
import svgIconsPlugin from './app/frontend/build/iconsPlugin.mjs'
|
|
import { discoverSkinMarkerClashes } from './app/frontend/build/skinMarker/discoverClashes.mjs'
|
|
import skinMarkerPlugin from './app/frontend/build/skinMarker/plugin.mjs'
|
|
|
|
const dir = dirname(fileURLToPath(import.meta.url))
|
|
|
|
const SSL_PATH = resolve(homedir(), '.local/state/localhost.rb')
|
|
|
|
const isEnvBooleanSet = (value) => ['true', '1'].includes(value)
|
|
|
|
export default defineConfig(async ({ mode, command }) => {
|
|
const isTesting = ['test', 'cypress'].includes(mode)
|
|
const isBuild = command === 'build'
|
|
|
|
const require = createRequire(import.meta.url)
|
|
|
|
// Build-time addon weave: discover every installed addon's *.weave.mjs manifest
|
|
// and rewrite the targeted core SFC source before the Vue compiler runs.
|
|
const addonWeaveRules = await discoverAddonWeaveRules()
|
|
|
|
const plugins = [
|
|
// enforce: 'pre' → both run before VuePlugin compiles the SFC.
|
|
addonWeavePlugin(addonWeaveRules),
|
|
// Auto-stamp data-zammad-target on native SFC roots — the stable custom-CSS
|
|
// hook. Skipped under test, where it would perturb component DOM snapshots.
|
|
...(isTesting || isEnvBooleanSet(process.env.VITE_TEST_MODE)
|
|
? []
|
|
: [skinMarkerPlugin(discoverSkinMarkerClashes())]),
|
|
tailwindcss(),
|
|
VuePlugin({
|
|
template: {
|
|
compilerOptions: {
|
|
nodeTransforms:
|
|
isTesting || isEnvBooleanSet(process.env.VITE_TEST_MODE)
|
|
? []
|
|
: [require('./app/frontend/build/transforms/transformTestId.js')],
|
|
},
|
|
},
|
|
}),
|
|
svgIconsPlugin(),
|
|
]
|
|
|
|
if (!isTesting || isBuild) {
|
|
// Ruby plugin is not needed inside of the vitest context and has some side effects.
|
|
const { default: RubyPlugin } = require('vite-plugin-ruby')
|
|
plugins.push(RubyPlugin())
|
|
|
|
plugins.push(
|
|
...VitePWA({
|
|
disable: isTesting || isEnvBooleanSet(process.env.VITE_TEST_MODE),
|
|
// should be generated on ruby side
|
|
manifest: false,
|
|
registerType: 'prompt',
|
|
srcDir: 'apps/mobile/sw',
|
|
filename: 'sw.ts',
|
|
includeManifestIcons: false,
|
|
injectRegister: null,
|
|
strategies: 'injectManifest',
|
|
}),
|
|
)
|
|
}
|
|
|
|
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"
|
|
if (isEnvBooleanSet(process.env.VITE_RUBY_HTTPS)) {
|
|
const SSL_CERT = readFileSync(resolve(SSL_PATH, 'localhost.crt'))
|
|
const SSL_KEY = readFileSync(resolve(SSL_PATH, 'localhost.key'))
|
|
|
|
https = {
|
|
cert: SSL_CERT,
|
|
key: SSL_KEY,
|
|
}
|
|
}
|
|
|
|
let publicDir
|
|
|
|
if (!isBuild) {
|
|
publicDir = resolve(dir, 'public')
|
|
}
|
|
|
|
return {
|
|
publicDir,
|
|
build: {
|
|
rolldownOptions: {
|
|
output: {
|
|
manualChunks(id) {
|
|
const manualChunksMap = {
|
|
lodash: ['lodash-es'],
|
|
vue: ['vue', 'vue-router', 'pinia'],
|
|
datepicker: ['@vuepic/vue-datepicker'],
|
|
linkifyjs: ['linkifyjs', 'linkify-string'],
|
|
graphql: [
|
|
'graphql',
|
|
// 🚨 'graphql-ruby-client',
|
|
// Important: don't include the package root here, it pulls in the Node-only `sync` entry
|
|
// which imports fs/path/crypto/http/... and triggers Vite "externalized for browser" warnings.
|
|
'graphql-ruby-client/subscriptions/ActionCableLink',
|
|
'graphql-tag',
|
|
'@apollo/client',
|
|
'@vue/apollo-composable',
|
|
'@rails/actioncable',
|
|
],
|
|
formkit: [
|
|
'@formkit/core',
|
|
'@formkit/dev',
|
|
// '@formkit/drag-and-drop', # is not used in mobile
|
|
'@formkit/i18n',
|
|
'@formkit/inputs',
|
|
'@formkit/rules',
|
|
'@formkit/tailwindcss',
|
|
'@formkit/themes',
|
|
'@formkit/utils',
|
|
'@formkit/validation',
|
|
'@formkit/vue',
|
|
],
|
|
}
|
|
|
|
if (!id.includes('node_modules')) return undefined
|
|
|
|
for (const [chunkName, packages] of Object.entries(manualChunksMap)) {
|
|
if (
|
|
packages.some(
|
|
(pkg) =>
|
|
id.includes(`node_modules/${pkg}/`) ||
|
|
id.includes(`node_modules/.pnpm/${pkg.replace('/', '+').replace('@', '')}`),
|
|
)
|
|
) {
|
|
return chunkName
|
|
}
|
|
}
|
|
|
|
return undefined
|
|
},
|
|
},
|
|
},
|
|
},
|
|
resolve: {
|
|
preserveSymlinks: isEnvBooleanSet(process.env.PRESERVE_SYMLINKS),
|
|
alias: {
|
|
'^vue-easy-lightbox$': 'vue-easy-lightbox/dist/external-css/vue-easy-lightbox.esm.min.js',
|
|
// In non-test builds, alias fake-timers to a tiny shim so Vite doesn't resolve Node core deps
|
|
...(isTesting
|
|
? {}
|
|
: {
|
|
'@sinonjs/fake-timers': resolve(dir, 'app/frontend/build/shims/fake-timers.ts'),
|
|
}),
|
|
},
|
|
},
|
|
server: {
|
|
https,
|
|
// The HMR websocket cannot ride the Rails asset proxy (no upgrade support) — point
|
|
// the client directly at the Vite dev server port instead of the page origin.
|
|
hmr: {
|
|
host: process.env.VITE_RUBY_HOST || 'localhost',
|
|
clientPort: Number(process.env.VITE_RUBY_PORT || 3036),
|
|
},
|
|
watch: {
|
|
ignored: isTesting
|
|
? []
|
|
: [
|
|
'**/*.spec.*',
|
|
'**/__tests__/**/*',
|
|
(path) => !path.includes('app/frontend') || path.includes('frontend/tests'),
|
|
],
|
|
},
|
|
},
|
|
define: {
|
|
VITE_TEST_MODE:
|
|
isEnvBooleanSet(process.env.VITEST) || isEnvBooleanSet(process.env.VITE_TEST_MODE),
|
|
},
|
|
test: {
|
|
globals: true,
|
|
// narrowing down test folder speeds up fast-glob in Vitest
|
|
root: './app/frontend',
|
|
setupFiles: ['./tests/vitest.setup.ts'],
|
|
environment: 'jsdom',
|
|
pool: 'vmForks',
|
|
env: {
|
|
SA11Y_RULESET: 'full',
|
|
},
|
|
clearMocks: true,
|
|
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.
|
|
...(parseInt(process.versions.node, 10) >= 25
|
|
? { execArgv: ['--no-experimental-webstorage'] }
|
|
: {}),
|
|
onConsoleLog(log) {
|
|
if (
|
|
log.includes('Not implemented: navigation') ||
|
|
log.includes('<Suspense> is an experimental feature')
|
|
)
|
|
return false
|
|
},
|
|
// perf improvements
|
|
server: {
|
|
deps: {
|
|
// vue-datepicker imports from date-fns and we don't want it to import ES version
|
|
inline: ['@vuepic/vue-datepicker'],
|
|
},
|
|
},
|
|
alias: [
|
|
// ESM version of date-fns exports a lot of extra files which takes 500ms to load on M4 Mac
|
|
// CJS version takes ~175ms which is also a lot, but not as much
|
|
{
|
|
find: /^date-fns$/,
|
|
replacement: join(dirname(require.resolve('date-fns/package.json')), 'index.cjs'),
|
|
},
|
|
],
|
|
},
|
|
plugins,
|
|
}
|
|
})
|