diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 918690cb3638..840ca6bf1bf7 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -120,9 +120,6 @@ jobs:
- name: Test Examples
run: pnpm run test:examples
- - name: Unit Test UI
- run: pnpm run -C packages/ui test:ui
-
- uses: actions/upload-artifact@v6
if: ${{ !cancelled() }}
with:
diff --git a/docs/api/advanced/test-case.md b/docs/api/advanced/test-case.md
index c9601650bc40..b91d14b46152 100644
--- a/docs/api/advanced/test-case.md
+++ b/docs/api/advanced/test-case.md
@@ -143,7 +143,13 @@ test('the validation works correctly', ({ task }) => {
})
```
-If the test did not finish running yet, the meta will be an empty object.
+If the test did not finish running yet, the meta will be an empty object, unless it has static meta:
+
+```ts
+test('the validation works correctly', { meta: { decorated: true } })
+```
+
+Since Vitest 4.1, Vitest inherits [`meta`](/api/advanced/test-suite#meta) property defined on the [suite](/api/advanced/test-suite).
## result
diff --git a/docs/api/advanced/test-suite.md b/docs/api/advanced/test-suite.md
index 56abb67ec125..f8aa837fc62a 100644
--- a/docs/api/advanced/test-suite.md
+++ b/docs/api/advanced/test-suite.md
@@ -198,24 +198,25 @@ Note that errors are serialized into simple objects: `instanceof Error` will alw
function meta(): TaskMeta
```
-Custom [metadata](/api/advanced/metadata) that was attached to the suite during its execution or collection. The meta can be attached by assigning a property to the `suite.meta` object during a test run:
+Custom [metadata](/api/advanced/metadata) that was attached to the suite during its execution or collection. Since Vitest 4.1, the meta can be attached by providing a `meta` object during test collection:
-```ts {7,12}
+```ts {7,10}
import { describe, test, TestRunner } from 'vitest'
-describe('the validation works correctly', () => {
- // assign "decorated" during collection
- const { suite } = TestRunner.getCurrentSuite()
- suite!.meta.decorated = true
-
+describe('the validation works correctly', { meta: { decorated: true } }, () => {
test('some test', ({ task }) => {
// assign "decorated" during test run, it will be available
// only in onTestCaseReady hook
task.suite.meta.decorated = false
+
+ // tests inherit suite's metadata
+ task.meta.decorated === true
})
})
```
+Note that suite metadata will be inherited by tests since Vitest 4.1.
+
:::tip
If metadata was attached during collection (outside of the `test` function), then it will be available in [`onTestModuleCollected`](./reporters#ontestmodulecollected) hook in the custom reporter.
:::
diff --git a/docs/api/browser/commands.md b/docs/api/browser/commands.md
index c53503fde814..d8f08f54a460 100644
--- a/docs/api/browser/commands.md
+++ b/docs/api/browser/commands.md
@@ -17,6 +17,8 @@ By default, Vitest uses `utf-8` encoding but you can override it with options.
::: tip
This API follows [`server.fs`](https://vitejs.dev/config/server-options.html#server-fs-allow) limitations for security reasons.
+
+If [`browser.api.allowWrite`](/config/browser/api) or [`api.allowWrite`](/config/api#api-allowwrite) are disabled, `writeFile` and `removeFile` functions won't do anything.
:::
```ts
diff --git a/docs/api/browser/locators.md b/docs/api/browser/locators.md
index e3cc3fce453e..b7eb6211f9fc 100644
--- a/docs/api/browser/locators.md
+++ b/docs/api/browser/locators.md
@@ -7,7 +7,7 @@ outline: [2, 3]
A locator is a representation of an element or a number of elements. Every locator is defined by a string called a selector. Vitest abstracts this selector by providing convenient methods that generate them behind the scenes.
-The locator API uses a fork of [Playwright's locators](https://playwright.dev/docs/api/class-locator) called [Ivya](https://npmjs.com/ivya). However, Vitest provides this API to every [provider](/config/browser#browser-provider), not just playwright.
+The locator API uses a fork of [Playwright's locators](https://playwright.dev/docs/api/class-locator) called [Ivya](https://npmjs.com/ivya). However, Vitest provides this API to every [provider](/config/browser/provider), not just playwright.
::: tip
This page covers API usage. To better understand locators and their usage, read [Playwright's "Locators" documentation](https://playwright.dev/docs/locators).
diff --git a/docs/api/test.md b/docs/api/test.md
index 84c108bbd2a5..93d6532bb1d3 100644
--- a/docs/api/test.md
+++ b/docs/api/test.md
@@ -170,6 +170,45 @@ it('user returns data from db', { tags: ['db', 'flaky'] }, () => {
})
```
+### meta 4.1.0 {#meta}
+
+- **Type:** `TaskMeta`
+
+Attaches custom [metadata](/api/advanced/metadata) available in reporters.
+
+::: warning
+Vitest merges top-level properties inherited from suites or tags. However, it does not perform a deep merge of nested objects.
+
+```ts
+import { describe, test } from 'vitest'
+
+describe(
+ 'nested meta',
+ {
+ meta: {
+ nested: { object: true, array: false },
+ },
+ },
+ () => {
+ test(
+ 'overrides part of meta',
+ {
+ meta: {
+ nested: { object: false }
+ },
+ },
+ ({ task }) => {
+ // task.meta === { nested: { object: false } }
+ // notice array got lost because "nested" object was overriden
+ }
+ )
+ }
+)
+```
+
+Prefer using non-nested meta, if possible.
+:::
+
### concurrent
- **Type:** `boolean`
diff --git a/docs/config/api.md b/docs/config/api.md
index 2bb16ecf51ea..61d4031187b3 100644
--- a/docs/config/api.md
+++ b/docs/config/api.md
@@ -5,8 +5,28 @@ outline: deep
# api
-- **Type:** `boolean | number`
+- **Type:** `boolean | number | object`
- **Default:** `false`
- **CLI:** `--api`, `--api.port`, `--api.host`, `--api.strictPort`
Listen to port and serve API for [the UI](/guide/ui) or [browser server](/guide/browser/). When set to `true`, the default port is `51204`.
+
+## api.allowWrite 4.1.0 {#api-allowwrite}
+
+- **Type:** `boolean`
+- **Default:** `true` if not exposed to the network, `false` otherwise
+
+Vitest server can save test files or snapshot files via the API. This allows anyone who can connect to the API the ability to run any arbitary code on your machine.
+
+::: danger SECURITY ADVICE
+Vitest does not expose the API to the internet by default and only listens on `localhost`. However if `host` is manually exposed to the network, anyone who connects to it can run arbitrary code on your machine, unless `api.allowWrite` and `api.allowExec` are set to `false`.
+
+If the host is set to anything other than `localhost` or `127.0.0.1`, Vitest will set `api.allowWrite` and `api.allowExec` to `false` by default. This means that any write operations (like changing the code in the UI) will not work. However, if you understand the security implications, you can override them.
+:::
+
+## api.allowExec 4.1.0 {#api-allowexec}
+
+- **Type:** `boolean`
+- **Default:** `true` if not exposed to the network, `false` otherwise
+
+Allows running any test file via the API. See the security advice in [`api.allowWrite`](#api-allowwrite).
diff --git a/docs/config/browser.md b/docs/config/browser.md
deleted file mode 100644
index ff977c09ab39..000000000000
--- a/docs/config/browser.md
+++ /dev/null
@@ -1,626 +0,0 @@
----
-title: Browser Config Reference | Config
-outline: deep
----
-
-# Browser Config Reference
-
-You can change the browser configuration by updating the `test.browser` field in your [config file](/config/). An example of a simple config file:
-
-```ts [vitest.config.ts]
-import { defineConfig } from 'vitest/config'
-import { playwright } from '@vitest/browser-playwright'
-
-export default defineConfig({
- test: {
- browser: {
- enabled: true,
- provider: playwright(),
- instances: [
- {
- browser: 'chromium',
- setupFile: './chromium-setup.js',
- },
- ],
- },
- },
-})
-```
-
-Please, refer to the ["Config Reference"](/config/) article for different config examples.
-
-::: warning
-_All listed options_ on this page are located within a `test` property inside the configuration:
-
-```ts [vitest.config.js]
-export default defineConfig({
- test: {
- browser: {},
- },
-})
-```
-:::
-
-## browser.enabled
-
-- **Type:** `boolean`
-- **Default:** `false`
-- **CLI:** `--browser`, `--browser.enabled=false`
-
-Run all tests inside a browser by default. Note that `--browser` only works if you have at least one [`browser.instances`](#browser-instances) item.
-
-## browser.instances
-
-- **Type:** `BrowserConfig`
-- **Default:** `[]`
-
-Defines multiple browser setups. Every config has to have at least a `browser` field.
-
-You can specify most of the [project options](/config/) (not marked with a icon) and some of the `browser` options like `browser.testerHtmlPath`.
-
-::: warning
-Every browser config inherits options from the root config:
-
-```ts{3,9} [vitest.config.ts]
-export default defineConfig({
- test: {
- setupFile: ['./root-setup-file.js'],
- browser: {
- enabled: true,
- testerHtmlPath: './custom-path.html',
- instances: [
- {
- // will have both setup files: "root" and "browser"
- setupFile: ['./browser-setup-file.js'],
- // implicitly has "testerHtmlPath" from the root config // [!code warning]
- // testerHtmlPath: './custom-path.html', // [!code warning]
- },
- ],
- },
- },
-})
-```
-
-For more examples, refer to the ["Multiple Setups" guide](/guide/browser/multiple-setups).
-:::
-
-List of available `browser` options:
-
-- [`browser.headless`](#browser-headless)
-- [`browser.locators`](#browser-locators)
-- [`browser.viewport`](#browser-viewport)
-- [`browser.testerHtmlPath`](#browser-testerhtmlpath)
-- [`browser.screenshotDirectory`](#browser-screenshotdirectory)
-- [`browser.screenshotFailures`](#browser-screenshotfailures)
-- [`browser.provider`](#browser-provider)
-- [`browser.detailsPanelPosition`](#browser-detailspanelposition)
-
-Under the hood, Vitest transforms these instances into separate [test projects](/api/advanced/test-project) sharing a single Vite server for better caching performance.
-
-## browser.headless
-
-- **Type:** `boolean`
-- **Default:** `process.env.CI`
-- **CLI:** `--browser.headless`, `--browser.headless=false`
-
-Run the browser in a `headless` mode. If you are running Vitest in CI, it will be enabled by default.
-
-## browser.isolate
-
-- **Type:** `boolean`
-- **Default:** the same as [`--isolate`](/config/#isolate)
-- **CLI:** `--browser.isolate`, `--browser.isolate=false`
-
-Run every test in a separate iframe.
-
-::: danger DEPRECATED
-This option is deprecated. Use [`isolate`](/config/#isolate) instead.
-:::
-
-## browser.testerHtmlPath
-
-- **Type:** `string`
-
-A path to the HTML entry point. Can be relative to the root of the project. This file will be processed with [`transformIndexHtml`](https://vite.dev/guide/api-plugin#transformindexhtml) hook.
-
-## browser.api
-
-- **Type:** `number | { port?, strictPort?, host? }`
-- **Default:** `63315`
-- **CLI:** `--browser.api=63315`, `--browser.api.port=1234, --browser.api.host=example.com`
-
-Configure options for Vite server that serves code in the browser. Does not affect [`test.api`](#api) option. By default, Vitest assigns port `63315` to avoid conflicts with the development server, allowing you to run both in parallel.
-
-## browser.provider {#browser-provider}
-
-- **Type:** `BrowserProviderOption`
-- **Default:** `'preview'`
-- **CLI:** `--browser.provider=playwright`
-
-The return value of the provider factory. You can import the factory from `@vitest/browser-` or make your own provider:
-
-```ts{8-10}
-import { playwright } from '@vitest/browser-playwright'
-import { webdriverio } from '@vitest/browser-webdriverio'
-import { preview } from '@vitest/browser-preview'
-
-export default defineConfig({
- test: {
- browser: {
- provider: playwright(),
- provider: webdriverio(),
- provider: preview(), // default
- },
- },
-})
-```
-
-To configure how provider initializes the browser, you can pass down options to the factory function:
-
-```ts{7-13,20-26}
-import { playwright } from '@vitest/browser-playwright'
-
-export default defineConfig({
- test: {
- browser: {
- // shared provider options between all instances
- provider: playwright({
- launchOptions: {
- slowMo: 50,
- channel: 'chrome-beta',
- },
- actionTimeout: 5_000,
- }),
- instances: [
- { browser: 'chromium' },
- {
- browser: 'firefox',
- // overriding options only for a single instance
- // this will NOT merge options with the parent one
- provider: playwright({
- launchOptions: {
- firefoxUserPrefs: {
- 'browser.startup.homepage': 'https://example.com',
- },
- },
- })
- }
- ],
- },
- },
-})
-```
-
-### Custom Provider advanced
-
-::: danger ADVANCED API
-The custom provider API is highly experimental and can change between patches. If you just need to run tests in a browser, use the [`browser.instances`](#browser-instances) option instead.
-:::
-
-```ts
-export interface BrowserProvider {
- name: string
- mocker?: BrowserModuleMocker
- readonly initScripts?: string[]
- /**
- * @experimental opt-in into file parallelisation
- */
- supportsParallelism: boolean
- getCommandsContext: (sessionId: string) => Record
- openPage: (sessionId: string, url: string) => Promise
- getCDPSession?: (sessionId: string) => Promise
- close: () => Awaitable
-}
-```
-
-## browser.ui
-
-- **Type:** `boolean`
-- **Default:** `!isCI`
-- **CLI:** `--browser.ui=false`
-
-Should Vitest UI be injected into the page. By default, injects UI iframe during development.
-
-## browser.detailsPanelPosition
-
-- **Type:** `'right' | 'bottom'`
-- **Default:** `'right'`
-- **CLI:** `--browser.detailsPanelPosition=bottom`, `--browser.detailsPanelPosition=right`
-
-Controls the default position of the details panel in the Vitest UI when running browser tests. See [`browser.detailsPanelPosition`](/config/browser/detailspanelposition) for more details.
-
-## browser.viewport
-
-- **Type:** `{ width, height }`
-- **Default:** `414x896`
-
-Default iframe's viewport.
-
-## browser.locators
-
-Options for built-in [browser locators](/api/browser/locators).
-
-### browser.locators.testIdAttribute
-
-- **Type:** `string`
-- **Default:** `data-testid`
-
-Attribute used to find elements with `getByTestId` locator.
-
-## browser.screenshotDirectory
-
-- **Type:** `string`
-- **Default:** `__screenshots__` in the test file directory
-
-Path to the screenshots directory relative to the `root`.
-
-## browser.screenshotFailures
-
-- **Type:** `boolean`
-- **Default:** `!browser.ui`
-
-Should Vitest take screenshots if the test fails.
-
-## browser.orchestratorScripts
-
-- **Type:** `BrowserScript[]`
-- **Default:** `[]`
-
-Custom scripts that should be injected into the orchestrator HTML before test iframes are initiated. This HTML document only sets up iframes and doesn't actually import your code.
-
-The script `src` and `content` will be processed by Vite plugins. Script should be provided in the following shape:
-
-```ts
-export interface BrowserScript {
- /**
- * If "content" is provided and type is "module", this will be its identifier.
- *
- * If you are using TypeScript, you can add `.ts` extension here for example.
- * @default `injected-${index}.js`
- */
- id?: string
- /**
- * JavaScript content to be injected. This string is processed by Vite plugins if type is "module".
- *
- * You can use `id` to give Vite a hint about the file extension.
- */
- content?: string
- /**
- * Path to the script. This value is resolved by Vite so it can be a node module or a file path.
- */
- src?: string
- /**
- * If the script should be loaded asynchronously.
- */
- async?: boolean
- /**
- * Script type.
- * @default 'module'
- */
- type?: string
-}
-```
-
-## browser.commands
-
-- **Type:** `Record`
-- **Default:** `{ readFile, writeFile, ... }`
-
-Custom [commands](/api/browser/commands) that can be imported during browser tests from `vitest/browser`.
-
-## browser.connectTimeout
-
-- **Type:** `number`
-- **Default:** `60_000`
-
-The timeout in milliseconds. If connection to the browser takes longer, the test suite will fail.
-
-::: info
-This is the time it should take for the browser to establish the WebSocket connection with the Vitest server. In normal circumstances, this timeout should never be reached.
-:::
-
-## browser.trace
-
-- **Type:** `'on' | 'off' | 'on-first-retry' | 'on-all-retries' | 'retain-on-failure' | object`
-- **CLI:** `--browser.trace=on`, `--browser.trace=retain-on-failure`
-- **Default:** `'off'`
-
-Capture a trace of your browser test runs. You can preview traces with [Playwright Trace Viewer](https://trace.playwright.dev/).
-
-This options supports the following values:
-
-- `'on'` - capture trace for all tests. (not recommended as it's performance heavy)
-- `'off'` - do not capture traces.
-- `'on-first-retry'` - capture trace only when retrying the test for the first time.
-- `'on-all-retries'` - capture trace on every retry of the test.
-- `'retain-on-failure'` - capture trace only for tests that fail. This will automatically delete traces for tests that pass.
-- `object` - an object with the following shape:
-
-```ts
-interface TraceOptions {
- mode: 'on' | 'off' | 'on-first-retry' | 'on-all-retries' | 'retain-on-failure'
- /**
- * The directory where all traces will be stored. By default, Vitest
- * stores all traces in `__traces__` folder close to the test file.
- */
- tracesDir?: string
- /**
- * Whether to capture screenshots during tracing. Screenshots are used to build a timeline preview.
- * @default true
- */
- screenshots?: boolean
- /**
- * If this option is true tracing will
- * - capture DOM snapshot on every action
- * - record network activity
- * @default true
- */
- snapshots?: boolean
-}
-```
-
-::: danger WARNING
-This option is supported only by the [**playwright**](/config/browser/playwright) provider.
-:::
-
-## browser.trackUnhandledErrors
-
-- **Type:** `boolean`
-- **Default:** `true`
-
-Enables tracking uncaught errors and exceptions so they can be reported by Vitest.
-
-If you need to hide certain errors, it is recommended to use [`onUnhandledError`](/config/#onunhandlederror) option instead.
-
-Disabling this will completely remove all Vitest error handlers, which can help debugging with the "Pause on exceptions" checkbox turned on.
-
-## browser.expect
-
-- **Type:** `ExpectOptions`
-
-### browser.expect.toMatchScreenshot
-
-Default options for the
-[`toMatchScreenshot` assertion](/api/browser/assertions.html#tomatchscreenshot).
-These options will be applied to all screenshot assertions.
-
-::: tip
-Setting global defaults for screenshot assertions helps maintain consistency
-across your test suite and reduces repetition in individual tests. You can still
-override these defaults at the assertion level when needed for specific test cases.
-:::
-
-```ts
-import { defineConfig } from 'vitest/config'
-
-export default defineConfig({
- test: {
- browser: {
- enabled: true,
- expect: {
- toMatchScreenshot: {
- comparatorName: 'pixelmatch',
- comparatorOptions: {
- threshold: 0.2,
- allowedMismatchedPixels: 100,
- },
- resolveScreenshotPath: ({ arg, browserName, ext, testFileName }) =>
- `custom-screenshots/${testFileName}/${arg}-${browserName}${ext}`,
- },
- },
- },
- },
-})
-```
-
-[All options available in the `toMatchScreenshot` assertion](/api/browser/assertions#options)
-can be configured here. Additionally, two path resolution functions are
-available: `resolveScreenshotPath` and `resolveDiffPath`.
-
-#### browser.expect.toMatchScreenshot.resolveScreenshotPath
-
-- **Type:** `(data: PathResolveData) => string`
-- **Default output:** `` `${root}/${testFileDirectory}/${screenshotDirectory}/${testFileName}/${arg}-${browserName}-${platform}${ext}` ``
-
-A function to customize where reference screenshots are stored. The function
-receives an object with the following properties:
-
-- `arg: string`
-
- Path **without** extension, sanitized and relative to the test file.
-
- This comes from the arguments passed to `toMatchScreenshot`; if called
- without arguments this will be the auto-generated name.
-
- ```ts
- test('calls `onClick`', () => {
- expect(locator).toMatchScreenshot()
- // arg = "calls-onclick-1"
- })
-
- expect(locator).toMatchScreenshot('foo/bar/baz.png')
- // arg = "foo/bar/baz"
-
- expect(locator).toMatchScreenshot('../foo/bar/baz.png')
- // arg = "foo/bar/baz"
- ```
-
-- `ext: string`
-
- Screenshot extension, with leading dot.
-
- This can be set through the arguments passed to `toMatchScreenshot`, but
- the value will fall back to `'.png'` if an unsupported extension is used.
-
-- `browserName: string`
-
- The instance's browser name.
-
-- `platform: NodeJS.Platform`
-
- The value of
- [`process.platform`](https://nodejs.org/docs/v22.16.0/api/process.html#processplatform).
-
-- `screenshotDirectory: string`
-
- The value provided to
- [`browser.screenshotDirectory`](/config/browser/screenshotdirectory),
- if none is provided, its default value.
-
-- `root: string`
-
- Absolute path to the project's [`root`](/config/#root).
-
-- `testFileDirectory: string`
-
- Path to the test file, relative to the project's [`root`](/config/#root).
-
-- `testFileName: string`
-
- The test's filename.
-
-- `testName: string`
-
- The [`test`](/api/test)'s name, including parent
- [`describe`](/api/describe), sanitized.
-
-- `attachmentsDir: string`
-
- The value provided to [`attachmentsDir`](/config/#attachmentsdir), if none is
- provided, its default value.
-
-For example, to group screenshots by browser:
-
-```ts
-resolveScreenshotPath: ({ arg, browserName, ext, root, testFileName }) =>
- `${root}/screenshots/${browserName}/${testFileName}/${arg}${ext}`
-```
-
-#### browser.expect.toMatchScreenshot.resolveDiffPath
-
-- **Type:** `(data: PathResolveData) => string`
-- **Default output:** `` `${root}/${attachmentsDir}/${testFileDirectory}/${testFileName}/${arg}-${browserName}-${platform}${ext}` ``
-
-A function to customize where diff images are stored when screenshot comparisons
-fail. Receives the same data object as
-[`resolveScreenshotPath`](#browser-expect-tomatchscreenshot-resolvescreenshotpath).
-
-For example, to store diffs in a subdirectory of attachments:
-
-```ts
-resolveDiffPath: ({ arg, attachmentsDir, browserName, ext, root, testFileName }) =>
- `${root}/${attachmentsDir}/screenshot-diffs/${testFileName}/${arg}-${browserName}${ext}`
-```
-
-#### browser.expect.toMatchScreenshot.comparators
-
-- **Type:** `Record`
-
-Register custom screenshot comparison algorithms, like [SSIM](https://en.wikipedia.org/wiki/Structural_similarity_index_measure) or other perceptual similarity metrics.
-
-To create a custom comparator, you need to register it in your config. If using TypeScript, declare its options in the `ScreenshotComparatorRegistry` interface.
-
-```ts
-import { defineConfig } from 'vitest/config'
-
-// 1. Declare the comparator's options type
-declare module 'vitest/browser' {
- interface ScreenshotComparatorRegistry {
- myCustomComparator: {
- sensitivity?: number
- ignoreColors?: boolean
- }
- }
-}
-
-// 2. Implement the comparator
-export default defineConfig({
- test: {
- browser: {
- expect: {
- toMatchScreenshot: {
- comparators: {
- myCustomComparator: async (
- reference,
- actual,
- {
- createDiff, // always provided by Vitest
- sensitivity = 0.01,
- ignoreColors = false,
- }
- ) => {
- // ...algorithm implementation
- return { pass, diff, message }
- },
- },
- },
- },
- },
- },
-})
-```
-
-Then use it in your tests:
-
-```ts
-await expect(locator).toMatchScreenshot({
- comparatorName: 'myCustomComparator',
- comparatorOptions: {
- sensitivity: 0.08,
- ignoreColors: true,
- },
-})
-```
-
-**Comparator Function Signature:**
-
-```ts
-type Comparator = (
- reference: {
- metadata: { height: number; width: number }
- data: TypedArray
- },
- actual: {
- metadata: { height: number; width: number }
- data: TypedArray
- },
- options: {
- createDiff: boolean
- } & Options
-) => Promise<{
- pass: boolean
- diff: TypedArray | null
- message: string | null
-}> | {
- pass: boolean
- diff: TypedArray | null
- message: string | null
-}
-```
-
-The `reference` and `actual` images are decoded using the appropriate codec (currently only PNG). The `data` property is a flat `TypedArray` (`Buffer`, `Uint8Array`, or `Uint8ClampedArray`) containing pixel data in RGBA format:
-
-- **4 bytes per pixel**: red, green, blue, alpha (from `0` to `255` each)
-- **Row-major order**: pixels are stored left-to-right, top-to-bottom
-- **Total length**: `width × height × 4` bytes
-- **Alpha channel**: always present. Images without transparency have alpha values set to `255` (fully opaque)
-
-::: tip Performance Considerations
-The `createDiff` option indicates whether a diff image is needed. During [stable screenshot detection](/guide/browser/visual-regression-testing#how-visual-tests-work), Vitest calls comparators with `createDiff: false` to avoid unnecessary work.
-
-**Respect this flag to keep your tests fast**.
-:::
-
-::: warning Handle Missing Options
-The `options` parameter in `toMatchScreenshot()` is optional, so users might not provide all your comparator options. Always make them optional with default values:
-
-```ts
-myCustomComparator: (
- reference,
- actual,
- { createDiff, threshold = 0.1, maxDiff = 100 },
-) => {
- // ...comparison logic
-}
-```
-:::
diff --git a/docs/config/browser/api.md b/docs/config/browser/api.md
index b1491e146473..23e50339a24a 100644
--- a/docs/config/browser/api.md
+++ b/docs/config/browser/api.md
@@ -5,8 +5,24 @@ outline: deep
# browser.api
-- **Type:** `number | { port?, strictPort?, host? }`
+- **Type:** `number | object`
- **Default:** `63315`
- **CLI:** `--browser.api=63315`, `--browser.api.port=1234, --browser.api.host=example.com`
Configure options for Vite server that serves code in the browser. Does not affect [`test.api`](#api) option. By default, Vitest assigns port `63315` to avoid conflicts with the development server, allowing you to run both in parallel.
+
+## api.allowWrite 4.1.0 {#api-allowwrite}
+
+- **Type:** `boolean`
+- **Default:** `true` if not exposed to the network, `false` otherwise
+
+Vitest saves [annotation attachments](/guide/test-annotations), [artifacts](/api/advanced/artifacts) and [snapshots](/guide/snapshot) by receiving a WebSocket connection from the browser. This allows anyone who can connect to the API write any arbitary code on your machine within the root of your project (configured by [`fs.allow`](https://vite.dev/config/server-options#server-fs-allow)).
+
+If browser server is not exposed to the internet (the host is `localhost`), this should not be a problem, so the default value in that case is `true`. If you override the host, Vitest will set `allowWrite` to `false` by default to prevent potentially harmful writes.
+
+## api.allowExec 4.1.0 {#api-allowexec}
+
+- **Type:** `boolean`
+- **Default:** `true` if not exposed to the network, `false` otherwise
+
+Allows running any test file via the UI. This only applies to the interactive elements (and the server code behind them) in the [UI](/guide/ui) that can run the code. If UI is disabled, this has no effect. See [`api.allowExec`](/config/api#api-allowexec) for more information.
diff --git a/docs/config/experimental.md b/docs/config/experimental.md
index 13a01957504c..b7173db81ad9 100644
--- a/docs/config/experimental.md
+++ b/docs/config/experimental.md
@@ -187,17 +187,35 @@ Please leave feedback regarding this feature in a [GitHub Discussion](https://gi
```ts
interface ImportDurationsOptions {
/**
- * Print import breakdown to CLI terminal after tests finish.
+ * When to print import breakdown to CLI terminal.
+ * - false: Never print (default)
+ * - true: Always print
+ * - 'on-warn': Print only when any import exceeds warn threshold
*/
- print?: boolean
+ print?: boolean | 'on-warn'
+ /**
+ * Fail the test run if any import exceeds the danger threshold.
+ * When enabled and threshold exceeded, breakdown is always printed.
+ * @default false
+ */
+ failOnDanger?: boolean
/**
* Maximum number of imports to collect and display.
*/
limit?: number
+ /**
+ * Duration thresholds in milliseconds for coloring and warnings.
+ */
+ thresholds?: {
+ /** Threshold for yellow/warning color. @default 100 */
+ warn?: number
+ /** Threshold for red/danger color and failOnDanger. @default 500 */
+ danger?: number
+ }
}
```
-- **Default:** `{ print: false, limit: 0 }` (`limit` is 10 if `print` or UI is enabled)
+- **Default:** `{ print: false, failOnDanger: false, limit: 0, thresholds: { warn: 100, danger: 500 } }` (`limit` is 10 if `print` or UI is enabled)
Configure import duration collection and display.
@@ -206,26 +224,54 @@ The `print` option controls CLI terminal output. The `limit` option controls how
- Self: the time it took to import the module, excluding static imports;
- Total: the time it took to import the module, including static imports. Note that this does not include `transform` time of the current module.
-
+
+
Note that if the file path is too long, Vitest will truncate it at the start until it fits 45 character limit.
### experimental.importDurations.print {#experimental-importdurationsprint}
+- **Type:** `boolean | 'on-warn'`
+- **Default:** `false`
+
+Controls when to print import breakdown to CLI terminal after tests finish. This only works with [`default`](/guide/reporters#default), [`verbose`](/guide/reporters#verbose), or [`tree`](/guide/reporters#tree) reporters.
+
+- `false`: Never print breakdown
+- `true`: Always print breakdown
+- `'on-warn'`: Print only when any import exceeds the `thresholds.warn` value
+
+### experimental.importDurations.failOnDanger {#experimental-importdurationsfailondanger}
+
- **Type:** `boolean`
- **Default:** `false`
-Print import breakdown to CLI terminal after tests finish. This only works with [`default`](/guide/reporters#default), [`verbose`](/guide/reporters#verbose), or [`tree`](/guide/reporters#tree) reporters.
+Fail the test run if any import exceeds the `thresholds.danger` value. When enabled and the threshold is exceeded, the breakdown is always printed regardless of the `print` setting.
+
+This is useful for enforcing import performance budgets in CI:
+
+```bash
+vitest --experimental.importDurations.failOnDanger
+```
### experimental.importDurations.limit {#experimental-importdurationslimit}
- **Type:** `number`
-- **Default:** `0` (or `10` if `print` or UI is enabled)
+- **Default:** `0` (or `10` if `print`, `failOnDanger`, or UI is enabled)
Maximum number of imports to collect and display in CLI output, [Vitest UI](/guide/ui#import-breakdown), and third-party reporters.
+### experimental.importDurations.thresholds {#experimental-importdurationsthresholds}
+
+- **Type:** `{ warn?: number; danger?: number }`
+- **Default:** `{ warn: 100, danger: 500 }`
+
+Duration thresholds in milliseconds for coloring and warnings:
+
+- `warn`: Threshold for yellow/warning color (default: 100ms)
+- `danger`: Threshold for red/danger color and `failOnDanger` (default: 500ms)
+
::: info
-[Vitest UI](/guide/ui#import-breakdown) shows a breakdown of imports automatically if at least one file took longer than 500 milliseconds to load. You can manually set this option to `false` to disable this.
+[Vitest UI](/guide/ui#import-breakdown) shows a breakdown of imports automatically if at least one file took longer than the `danger` threshold to load.
:::
## experimental.viteModuleRunner 4.1.0 {#experimental-vitemodulerunner}
diff --git a/docs/config/ui.md b/docs/config/ui.md
index e3bac62aa148..76d430a7c747 100644
--- a/docs/config/ui.md
+++ b/docs/config/ui.md
@@ -14,3 +14,7 @@ Enable [Vitest UI](/guide/ui).
::: warning
This features requires a [`@vitest/ui`](https://www.npmjs.com/package/@vitest/ui) package to be installed. If you do not have it already, Vitest will install it when you run the test command for the first time.
:::
+
+::: danger SECURITY ADVICE
+Make sure that your UI server is not exposed to the network. Since Vitest 4.1 setting [`api.host`](/config/api) to anything other than `localhost` will disable the buttons to save the code or run any tests for security reasons, effectively making UI a readonly reporter.
+:::
diff --git a/docs/guide/cli-generated.md b/docs/guide/cli-generated.md
index 9003015fe1a1..5b8acd881de7 100644
--- a/docs/guide/cli-generated.md
+++ b/docs/guide/cli-generated.md
@@ -70,6 +70,20 @@ Specify which IP addresses the server should listen on. Set this to `0.0.0.0` or
Set to true to exit if port is already in use, instead of automatically trying the next available port
+### api.allowExec
+
+- **CLI:** `--api.allowExec`
+- **Config:** [api.allowExec](/config/api#api-allowexec)
+
+Allow API to execute code. (Be careful when enabling this option in untrusted environments)
+
+### api.allowWrite
+
+- **CLI:** `--api.allowWrite`
+- **Config:** [api.allowWrite](/config/api#api-allowwrite)
+
+Allow API to edit files. (Be careful when enabling this option in untrusted environments)
+
### silent
- **CLI:** `--silent [value]`
@@ -332,6 +346,20 @@ Specify which IP addresses the server should listen on. Set this to `0.0.0.0` or
Set to true to exit if port is already in use, instead of automatically trying the next available port
+### browser.api.allowExec
+
+- **CLI:** `--browser.api.allowExec`
+- **Config:** [browser.api.allowExec](/config/browser/api#api-allowexec)
+
+Allow API to execute code. (Be careful when enabling this option in untrusted environments)
+
+### browser.api.allowWrite
+
+- **CLI:** `--browser.api.allowWrite`
+- **Config:** [browser.api.allowWrite](/config/browser/api#api-allowwrite)
+
+Allow API to edit files. (Be careful when enabling this option in untrusted environments)
+
### browser.isolate
- **CLI:** `--browser.isolate`
@@ -847,10 +875,10 @@ Enable caching of modules on the file system between reruns.
### experimental.importDurations.print
-- **CLI:** `--experimental.importDurations.print`
+- **CLI:** `--experimental.importDurations.print `
- **Config:** [experimental.importDurations.print](/config/experimental#experimental-importdurations-print)
-Print import breakdown to CLI terminal after tests finish (default: false).
+When to print import breakdown to CLI terminal. Use `true` to always print, `false` to never print, or `on-warn` to print only when imports exceed the warn threshold (default: false).
### experimental.importDurations.limit
@@ -859,6 +887,27 @@ Print import breakdown to CLI terminal after tests finish (default: false).
Maximum number of imports to collect and display (default: 0, or 10 if print or UI is enabled).
+### experimental.importDurations.failOnDanger
+
+- **CLI:** `--experimental.importDurations.failOnDanger`
+- **Config:** [experimental.importDurations.failOnDanger](/config/experimental#experimental-importdurations-failondanger)
+
+Fail the test run if any import exceeds the danger threshold (default: false).
+
+### experimental.importDurations.thresholds.warn
+
+- **CLI:** `--experimental.importDurations.thresholds.warn `
+- **Config:** [experimental.importDurations.thresholds.warn](/config/experimental#experimental-importdurations-thresholds-warn)
+
+Warning threshold - imports exceeding this are shown in yellow/orange (default: 100).
+
+### experimental.importDurations.thresholds.danger
+
+- **CLI:** `--experimental.importDurations.thresholds.danger `
+- **Config:** [experimental.importDurations.thresholds.danger](/config/experimental#experimental-importdurations-thresholds-danger)
+
+Danger threshold - imports exceeding this are shown in red (default: 500).
+
### experimental.viteModuleRunner
- **CLI:** `--experimental.viteModuleRunner`
diff --git a/docs/guide/ui.md b/docs/guide/ui.md
index 9840f4115976..70e930997a29 100644
--- a/docs/guide/ui.md
+++ b/docs/guide/ui.md
@@ -107,7 +107,7 @@ If the module was inlined, you will see three more windows:
All static imports in the "Source" window show a total time it took to evaluate them by the current module. If the import was already evaluated in the module graph, it will show `0ms` because it is cached by that point.
-If the module took longer than 500 milliseconds to load, the time will be displayed in red. If the module took longer than 100 milliseconds, the time will be displayed in orange.
+If the module took longer than the [`danger` threshold](/config/experimental#experimental-importdurations-thresholds) (default: 500ms) to load, the time will be displayed in red. If the module took longer than the [`warn` threshold](/config/experimental#experimental-importdurations-thresholds) (default: 100ms), the time will be displayed in orange.
You can click on an import source to jump into that module and traverse the graph further (note `./support/assertions/index.ts` below).
@@ -142,6 +142,6 @@ You can click on the module to see the Module Info. If the module is external, i
The breakdown shows a list of modules with self time, total time, and a percentage relative to the time it took to load the whole test file.
-The "Show Import Breakdown" icon will have a red color if there is at least one file that took longer than 500 milliseconds to load, and it will be orange if there is at least one file that took longer than 100 milliseconds.
+The "Show Import Breakdown" icon will have a red color if there is at least one file that took longer than the [`danger` threshold](/config/experimental#experimental-importdurations-thresholds) (default: 500ms) to load, and it will be orange if there is at least one file that took longer than the [`warn` threshold](/config/experimental#experimental-importdurations-thresholds) (default: 100ms).
You can use [`experimental.importDurations.limit`](/config/experimental#experimental-importdurationslimit) to control the number of imports displayed.
diff --git a/docs/package.json b/docs/package.json
index 0d388cf72a5a..30dc74a78a61 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -27,7 +27,7 @@
"@vite-pwa/assets-generator": "^1.0.2",
"@vite-pwa/vitepress": "^1.1.0",
"@vitejs/plugin-vue": "catalog:",
- "@voidzero-dev/vitepress-theme": "^4.3.0",
+ "@voidzero-dev/vitepress-theme": "^4.4.1",
"https-localhost": "^4.7.1",
"tinyglobby": "catalog:",
"unocss": "catalog:",
diff --git a/docs/public/reporter-import-breakdown-light.png b/docs/public/reporter-import-breakdown-light.png
new file mode 100644
index 000000000000..01aaa33ac60d
Binary files /dev/null and b/docs/public/reporter-import-breakdown-light.png differ
diff --git a/docs/public/reporter-import-breakdown.png b/docs/public/reporter-import-breakdown.png
index 5158ef12a4f8..944ba67df934 100644
Binary files a/docs/public/reporter-import-breakdown.png and b/docs/public/reporter-import-breakdown.png differ
diff --git a/examples/fastify/package.json b/examples/fastify/package.json
index 4023c4239a08..6adc8dcf1bf3 100644
--- a/examples/fastify/package.json
+++ b/examples/fastify/package.json
@@ -12,7 +12,7 @@
},
"devDependencies": {
"@vitest/ui": "latest",
- "fastify": "^5.7.1",
+ "fastify": "^5.7.2",
"supertest": "^7.2.2",
"tsx": "^4.21.0",
"vite": "latest",
diff --git a/examples/projects/package.json b/examples/projects/package.json
index 0d3eda33deaf..5cd2ac08995c 100644
--- a/examples/projects/package.json
+++ b/examples/projects/package.json
@@ -12,12 +12,12 @@
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
- "@types/react": "^19.2.9",
+ "@types/react": "^19.2.10",
"@vitejs/plugin-react": "^5.1.2",
"@vitest/ui": "latest",
- "fastify": "^5.7.1",
+ "fastify": "^5.7.2",
"jsdom": "^27.4.0",
- "react": "^19.2.3",
+ "react": "^19.2.4",
"supertest": "^7.2.2",
"tsx": "^4.21.0",
"vite": "latest",
diff --git a/netlify.toml b/netlify.toml
index 041e1799a5bc..e3df27427f4f 100755
--- a/netlify.toml
+++ b/netlify.toml
@@ -25,6 +25,11 @@ from = "/config/file"
to = "/config/"
status = 301
+[[redirects]]
+from = "/config/browser"
+to = "/config/browser/enabled"
+status = 301
+
[[redirects]]
from = "/guide/workspace"
to = "/guide/projects"
diff --git a/package.json b/package.json
index 948746912920..d66e021bde79 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"type": "module",
"version": "4.1.0-beta.2",
"private": true,
- "packageManager": "pnpm@10.28.1",
+ "packageManager": "pnpm@10.28.2",
"description": "Next generation testing framework powered by Vite",
"engines": {
"node": "^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -58,7 +58,7 @@
"magic-string": "^0.30.21",
"pathe": "^2.0.3",
"premove": "^4.0.0",
- "rollup": "^4.56.0",
+ "rollup": "^4.57.0",
"rollup-plugin-dts": "^6.3.0",
"rollup-plugin-license": "^3.6.0",
"tinyglobby": "catalog:",
diff --git a/packages/browser/src/node/commands/fs.ts b/packages/browser/src/node/commands/fs.ts
index d9558106023f..d921f3b111a7 100644
--- a/packages/browser/src/node/commands/fs.ts
+++ b/packages/browser/src/node/commands/fs.ts
@@ -3,12 +3,13 @@ import type { BrowserCommand, TestProject } from 'vitest/node'
import fs, { promises as fsp } from 'node:fs'
import { basename, dirname, resolve } from 'node:path'
import mime from 'mime/lite'
-import { isFileServingAllowed } from 'vitest/node'
+import { isFileLoadingAllowed } from 'vitest/node'
+import { slash } from '../utils'
function assertFileAccess(path: string, project: TestProject) {
if (
- !isFileServingAllowed(path, project.vite)
- && !isFileServingAllowed(path, project.vitest.vite)
+ !isFileLoadingAllowed(project.vite.config, path)
+ && !isFileLoadingAllowed(project.vitest.vite.config, path)
) {
throw new Error(
`Access denied to "${path}". See Vite config documentation for "server.fs": https://vitejs.dev/config/server-options.html#server-fs-strict.`,
@@ -16,11 +17,17 @@ function assertFileAccess(path: string, project: TestProject) {
}
}
+function assertWrite(path: string, project: TestProject) {
+ if (!project.config.browser.api.allowWrite || !project.vitest.config.api.allowWrite) {
+ throw new Error(`Cannot modify file "${path}". File writing is disabled because server is exposed to the internet, see https://vitest.dev/config/browser/api.`)
+ }
+}
+
export const readFile: BrowserCommand<
Parameters
> = async ({ project }, path, options = {}) => {
const filepath = resolve(project.config.root, path)
- assertFileAccess(filepath, project)
+ assertFileAccess(slash(filepath), project)
// never return a Buffer
if (typeof options === 'object' && !options.encoding) {
options.encoding = 'utf-8'
@@ -31,8 +38,9 @@ export const readFile: BrowserCommand<
export const writeFile: BrowserCommand<
Parameters
> = async ({ project }, path, data, options) => {
+ assertWrite(path, project)
const filepath = resolve(project.config.root, path)
- assertFileAccess(filepath, project)
+ assertFileAccess(slash(filepath), project)
const dir = dirname(filepath)
if (!fs.existsSync(dir)) {
await fsp.mkdir(dir, { recursive: true })
@@ -43,14 +51,15 @@ export const writeFile: BrowserCommand<
export const removeFile: BrowserCommand<
Parameters
> = async ({ project }, path) => {
+ assertWrite(path, project)
const filepath = resolve(project.config.root, path)
- assertFileAccess(filepath, project)
+ assertFileAccess(slash(filepath), project)
await fsp.rm(filepath)
}
export const _fileInfo: BrowserCommand<[path: string, encoding: BufferEncoding]> = async ({ project }, path, encoding) => {
const filepath = resolve(project.config.root, path)
- assertFileAccess(filepath, project)
+ assertFileAccess(slash(filepath), project)
const content = await fsp.readFile(filepath, encoding || 'base64')
return {
content,
diff --git a/packages/browser/src/node/rpc.ts b/packages/browser/src/node/rpc.ts
index 200ddb5f9873..1733afd9e26e 100644
--- a/packages/browser/src/node/rpc.ts
+++ b/packages/browser/src/node/rpc.ts
@@ -12,7 +12,7 @@ import { ServerMockResolver } from '@vitest/mocker/node'
import { createBirpc } from 'birpc'
import { parse, stringify } from 'flatted'
import { dirname, join, resolve } from 'pathe'
-import { createDebugger, isFileServingAllowed, isValidApiRequest } from 'vitest/node'
+import { createDebugger, isFileLoadingAllowed, isValidApiRequest } from 'vitest/node'
import { WebSocketServer } from 'ws'
const debug = createDebugger('vitest:browser:api')
@@ -113,13 +113,22 @@ export function setupBrowserRpc(globalServer: ParentBrowserProject, defaultMocke
}
function checkFileAccess(path: string) {
- if (!isFileServingAllowed(path, vite)) {
+ if (!isFileLoadingAllowed(vite.config, path)) {
throw new Error(
`Access denied to "${path}". See Vite config documentation for "server.fs": https://vitejs.dev/config/server-options.html#server-fs-strict.`,
)
}
}
+ function canWrite(project: TestProject) {
+ return (
+ project.config.browser.api.allowWrite
+ && project.vitest.config.browser.api.allowWrite
+ && project.config.api.allowWrite
+ && project.vitest.config.api.allowWrite
+ )
+ }
+
function setupClient(project: TestProject, rpcId: string, ws: WebSocket) {
const mockResolver = new ServerMockResolver(globalServer.vite, {
moduleDirectories: project.config?.deps?.moduleDirectories,
@@ -152,6 +161,23 @@ export function setupBrowserRpc(globalServer: ParentBrowserProject, defaultMocke
}
},
async onTaskArtifactRecord(id, artifact) {
+ if (!canWrite(project)) {
+ if (artifact.type === 'internal:annotation' && artifact.annotation.attachment) {
+ artifact.annotation.attachment = undefined
+ vitest.logger.error(
+ `[vitest] Cannot record annotation attachment because file writing is disabled. See https://vitest.dev/config/browser/api.`,
+ )
+ }
+ // remove attachments if cannot write
+ if (artifact.attachments?.length) {
+ const attachments = artifact.attachments.map(n => n.path).filter(r => !!r).join('", "')
+ artifact.attachments = []
+ vitest.logger.error(
+ `[vitest] Cannot record attachments ("${attachments}") because file writing is disabled, removing attachments from artifact "${artifact.type}". See https://vitest.dev/config/browser/api.`,
+ )
+ }
+ }
+
return vitest._testRun.recordArtifact(id, artifact)
},
async onTaskUpdate(method, packs, events) {
@@ -193,15 +219,27 @@ export function setupBrowserRpc(globalServer: ParentBrowserProject, defaultMocke
},
async saveSnapshotFile(id, content) {
checkFileAccess(id)
+ if (!canWrite(project)) {
+ vitest.logger.error(
+ `[vitest] Cannot save snapshot file "${id}". File writing is disabled because server is exposed to the internet, see https://vitest.dev/config/browser/api.`,
+ )
+ return
+ }
await fs.mkdir(dirname(id), { recursive: true })
- return fs.writeFile(id, content, 'utf-8')
+ await fs.writeFile(id, content, 'utf-8')
},
async removeSnapshotFile(id) {
checkFileAccess(id)
+ if (!canWrite(project)) {
+ vitest.logger.error(
+ `[vitest] Cannot remove snapshot file "${id}". File writing is disabled because server is exposed to the internet, see https://vitest.dev/config/browser/api.`,
+ )
+ return
+ }
if (!existsSync(id)) {
throw new Error(`Snapshot file "${id}" does not exist.`)
}
- return fs.unlink(id)
+ await fs.unlink(id)
},
getBrowserFileSourceMap(id) {
const mod = globalServer.vite.moduleGraph.getModuleById(id)
diff --git a/packages/pretty-format/package.json b/packages/pretty-format/package.json
index bdca82a53356..558242437c9d 100644
--- a/packages/pretty-format/package.json
+++ b/packages/pretty-format/package.json
@@ -38,7 +38,7 @@
},
"devDependencies": {
"@types/react-is": "^19.2.0",
- "react-is": "^19.2.3",
+ "react-is": "^19.2.4",
"react-is-18": "npm:react-is@18.3.1"
}
}
diff --git a/packages/runner/src/suite.ts b/packages/runner/src/suite.ts
index 911479b88f19..f56ee8426f61 100644
--- a/packages/runner/src/suite.ts
+++ b/packages/runner/src/suite.ts
@@ -331,16 +331,32 @@ function createSuiteCollector(
// higher priority should be last, run 1, 2, 3, ... etc
.sort((tag1, tag2) => (tag2.priority ?? POSITIVE_INFINITY) - (tag1.priority ?? POSITIVE_INFINITY))
.reduce((acc, tag) => {
- const { name, description, priority, ...options } = tag
+ const { name, description, priority, meta, ...options } = tag
Object.assign(acc, options)
+ if (meta) {
+ acc.meta = Object.assign(acc.meta ?? Object.create(null), meta)
+ }
return acc
}, {} as TestOptions)
+ const testOwnMeta = options.meta
options = {
...tagsOptions,
...options,
}
const timeout = options.timeout ?? runner.config.testTimeout
+ const parentMeta = currentSuite?.meta
+ const tagMeta = tagsOptions.meta
+ const testMeta = Object.create(null)
+ if (tagMeta) {
+ Object.assign(testMeta, tagMeta)
+ }
+ if (parentMeta) {
+ Object.assign(testMeta, parentMeta)
+ }
+ if (testOwnMeta) {
+ Object.assign(testMeta, testOwnMeta)
+ }
const task: Test = {
id: '',
name,
@@ -365,7 +381,7 @@ function createSuiteCollector(
: options.todo
? 'todo'
: 'run',
- meta: options.meta ?? Object.create(null),
+ meta: testMeta,
annotations: [],
artifacts: [],
tags: testTags,
@@ -513,7 +529,7 @@ function createSuiteCollector(
file: (currentSuite?.file ?? collectorContext.currentSuite?.file)!,
shuffle: suiteOptions?.shuffle,
tasks: [],
- meta: Object.create(null),
+ meta: suiteOptions?.meta ?? Object.create(null),
concurrent: suiteOptions?.concurrent,
tags: unique([...parentTask?.tags || [], ...suiteTags]),
}
@@ -604,9 +620,10 @@ function createSuite() {
const isConcurrentSpecified = options.concurrent || this.concurrent || options.sequential === false
const isSequentialSpecified = options.sequential || this.sequential || options.concurrent === false
+ const { meta: parentMeta, ...parentOptions } = currentSuite?.options || {}
// inherit options from current suite
options = {
- ...currentSuite?.options,
+ ...parentOptions,
...options,
}
@@ -638,6 +655,10 @@ function createSuite() {
options.sequential = isSequential && !isConcurrent
}
+ if (parentMeta) {
+ options.meta = Object.assign(Object.create(null), parentMeta, options.meta)
+ }
+
return createSuiteCollector(
formatName(name),
factory,
diff --git a/packages/runner/src/types/tasks.ts b/packages/runner/src/types/tasks.ts
index 948e358c68ba..2fbffb924a12 100644
--- a/packages/runner/src/types/tasks.ts
+++ b/packages/runner/src/types/tasks.ts
@@ -570,6 +570,10 @@ export interface TestOptions {
tags?: keyof TestTags extends never
? string[] | string
: TestTags[keyof TestTags] | TestTags[keyof TestTags][]
+ /**
+ * Custom test metadata available to reporters.
+ */
+ meta?: Partial
}
export interface TestTags {}
@@ -735,10 +739,6 @@ export interface TaskCustomOptions extends TestOptions {
* Whether the task was produced with `.each()` method.
*/
each?: boolean
- /**
- * Custom metadata for the task that will be assigned to `task.meta`.
- */
- meta?: Record
/**
* Task fixtures.
*/
diff --git a/packages/ui/client/components/ModuleGraphImportBreakdown.vue b/packages/ui/client/components/ModuleGraphImportBreakdown.vue
index b4eaaa9447ba..725eb5fc9e93 100644
--- a/packages/ui/client/components/ModuleGraphImportBreakdown.vue
+++ b/packages/ui/client/components/ModuleGraphImportBreakdown.vue
@@ -25,11 +25,11 @@ const emit = defineEmits<{
const imports = computed(() => {
const file = currentModule.value
const importDurations = file?.importDurations
- if (!importDurations) {
+ const root = config.value.root
+ if (!importDurations || !root) {
return []
}
- const root = config.value.root
const allImports: ImportEntry[] = []
for (const filePath in importDurations) {
const duration = importDurations[filePath]
diff --git a/packages/ui/client/components/ModuleTransformResultView.vue b/packages/ui/client/components/ModuleTransformResultView.vue
index b70d746f97f9..892dc9c7727a 100644
--- a/packages/ui/client/components/ModuleTransformResultView.vue
+++ b/packages/ui/client/components/ModuleTransformResultView.vue
@@ -96,13 +96,17 @@ function onMousedown(editor: Editor, e: MouseEvent) {
function buildShadowImportsHtml(imports: Experimental.UntrackedModuleDefinitionDiagnostic[]) {
const shadowImportsDiv = document.createElement('div')
shadowImportsDiv.classList.add('mb-5')
+ const root = config.value.root
+ if (!root) {
+ return
+ }
imports.forEach(({ resolvedId, totalTime, external }) => {
const importDiv = document.createElement('div')
importDiv.append(document.createTextNode('import '))
const sourceDiv = document.createElement('span')
- const url = relative(config.value.root, resolvedId)
+ const url = relative(root, resolvedId)
sourceDiv.textContent = `"/${url}"`
sourceDiv.className = 'hover:underline decoration-gray cursor-pointer select-none'
importDiv.append(sourceDiv)
@@ -152,6 +156,9 @@ function markImportDurations(codemirror: EditorFromTextArea) {
if (untrackedModules?.length) {
const importDiv = buildShadowImportsHtml(untrackedModules)
+ if (!importDiv) {
+ return
+ }
widgetElements.push(importDiv)
lineWidgets.push(codemirror.addLineWidget(0, importDiv, { above: true }))
}
diff --git a/packages/ui/client/components/Navigation.vue b/packages/ui/client/components/Navigation.vue
index 7c1eb132659f..3bcaf7d2bada 100644
--- a/packages/ui/client/components/Navigation.vue
+++ b/packages/ui/client/components/Navigation.vue
@@ -3,7 +3,7 @@ import type { RunnerTestFile } from 'vitest'
import { Tooltip as VueTooltip } from 'floating-vue'
import { computed, nextTick } from 'vue'
import { isDark, toggleDark } from '~/composables'
-import { client, isReport, runAll, runFiles } from '~/composables/client'
+import { client, config, isReport, runAll, runFiles } from '~/composables/client'
import { explorerTree } from '~/composables/explorer'
import { initialized, shouldShowExpandAll } from '~/composables/explorer/state'
import {
@@ -26,6 +26,10 @@ function updateSnapshot() {
const toggleMode = computed(() => isDark.value ? 'light' : 'dark')
async function onRunAll(files?: RunnerTestFile[]) {
+ if (config.value.api?.allowExec === false) {
+ return
+ }
+
if (coverageEnabled.value) {
disableCoverage.value = true
await nextTick()
@@ -49,6 +53,13 @@ function collapseTests() {
function expandTests() {
explorerTree.expandAllNodes()
}
+
+function getRerunTooltip(filteredFiles: RunnerTestFile[] | undefined) {
+ if (config.value.api?.allowExec === false) {
+ return 'Cannot run tests when `api.allowExec` is `false`. Did you expose UI to the internet?'
+ }
+ return filteredFiles ? (filteredFiles.length === 0 ? 'No test to run (clear filter)' : 'Rerun filtered') : 'Rerun all'
+}
@@ -113,7 +124,7 @@ function expandTests() {
@click="showCoverage()"
/>
{
})
const runButtonTitle = computed(() => {
+ if (config.value.api?.allowExec === false) {
+ return 'Cannot run tests when `api.allowExec` is `false`. Did you expose UI to the internet?'
+ }
return type === 'file'
? 'Run current file'
: type === 'suite'
@@ -237,7 +240,7 @@ const tagsBgGradient = computed(() => {
-->
{
:title="runButtonTitle"
icon="i-carbon:play-filled-alt"
text-green5
+ :disabled="config.api?.allowExec === false"
@click.prevent.stop="onRun(task)"
/>
diff --git a/packages/ui/client/components/views/ViewEditor.vue b/packages/ui/client/components/views/ViewEditor.vue
index 3af52d977a56..022d1226edbe 100644
--- a/packages/ui/client/components/views/ViewEditor.vue
+++ b/packages/ui/client/components/views/ViewEditor.vue
@@ -6,7 +6,7 @@ import { until, useResizeObserver, watchDebounced } from '@vueuse/core'
import { createTooltip, destroyTooltip } from 'floating-vue'
import { computed, nextTick, onBeforeUnmount, ref, shallowRef, watch } from 'vue'
import { getAttachmentUrl, sanitizeFilePath } from '~/composables/attachments'
-import { client, isReport } from '~/composables/client'
+import { client, config, isReport } from '~/composables/client'
import { finished } from '~/composables/client/state'
import { codemirrorRef } from '~/composables/codemirror'
import { openInEditor } from '~/composables/error'
@@ -385,7 +385,7 @@ onBeforeUnmount(clearListeners)
ref="editor"
v-model="code"
h-full
- v-bind="{ lineNumbers: true, readOnly: isReport, saving }"
+ v-bind="{ lineNumbers: true, readOnly: isReport || !config.api?.allowWrite, saving }"
:mode="ext"
data-testid="code-mirror"
@save="onSave"
diff --git a/packages/ui/client/components/views/ViewModuleGraph.vue b/packages/ui/client/components/views/ViewModuleGraph.vue
index 3c5937511fe2..6d21e45b81e7 100644
--- a/packages/ui/client/components/views/ViewModuleGraph.vue
+++ b/packages/ui/client/components/views/ViewModuleGraph.vue
@@ -17,7 +17,7 @@ import {
PositionInitializers,
} from 'd3-graph-controller'
import { computed, onMounted, onUnmounted, ref, shallowRef, toRefs, watch } from 'vue'
-import { isReport } from '~/composables/client'
+import { config, isReport } from '~/composables/client'
import { currentModule } from '~/composables/navigation'
import IconButton from '../IconButton.vue'
import Modal from '../Modal.vue'
@@ -43,14 +43,18 @@ const focusedNode = ref(null)
const filteredGraph = shallowRef(graph.value)
const breakdownIconClass = computed(() => {
let textClass = ''
- const importDurations = currentModule.value?.importDurations || {}
+ const importDurations = currentModule.value?.importDurations
+ const thresholds = config.value.experimental?.importDurations.thresholds
+ if (!importDurations || !thresholds) {
+ return textClass
+ }
for (const moduleId in importDurations) {
const { totalTime } = importDurations[moduleId]
- if (totalTime >= 500) {
+ if (totalTime >= thresholds.danger) {
textClass = 'text-red'
break
}
- else if (totalTime >= 100) {
+ else if (totalTime >= thresholds.warn) {
textClass = 'text-orange'
}
}
diff --git a/packages/ui/client/components/views/ViewReport.vue b/packages/ui/client/components/views/ViewReport.vue
index b6c038ec363a..33cdd436e203 100644
--- a/packages/ui/client/components/views/ViewReport.vue
+++ b/packages/ui/client/components/views/ViewReport.vue
@@ -111,7 +111,7 @@ const {
>
-
+
{
>
-
+
({} as any)
+export const config = shallowRef>({} as any)
export const status = ref('CONNECTING')
export const availableProjects = shallowRef([])
diff --git a/packages/ui/client/composables/client/static.ts b/packages/ui/client/composables/client/static.ts
index 2347ef420865..e538dcf198d9 100644
--- a/packages/ui/client/composables/client/static.ts
+++ b/packages/ui/client/composables/client/static.ts
@@ -64,7 +64,6 @@ export function createStaticClient(): VitestClient {
getExternalResult: asyncNoop,
getTransformResult: asyncNoop,
onDone: noop,
- onTaskUpdate: noop,
writeFile: asyncNoop,
rerun: asyncNoop,
rerunTask: asyncNoop,
diff --git a/packages/ui/client/composables/explorer/state.ts b/packages/ui/client/composables/explorer/state.ts
index 2de095cf338d..0778a8381316 100644
--- a/packages/ui/client/composables/explorer/state.ts
+++ b/packages/ui/client/composables/explorer/state.ts
@@ -67,7 +67,7 @@ function createSafeFilter(
return { matcher: () => true }
}
try {
- return { matcher: createTagsFilter([query], config.value.tags) }
+ return { matcher: createTagsFilter([query], config.value.tags || []) }
}
catch (error: any) {
return { matcher: () => false, error: error.message }
diff --git a/packages/ui/client/composables/module-graph.ts b/packages/ui/client/composables/module-graph.ts
index 3c23f3bcabf6..c34c8524b0e3 100644
--- a/packages/ui/client/composables/module-graph.ts
+++ b/packages/ui/client/composables/module-graph.ts
@@ -65,11 +65,11 @@ export function getModuleGraph(
return defineGraph({})
}
- const externalizedNodes = !config.value.experimental.viteModuleRunner
+ const externalizedNodes = !config.value.experimental?.viteModuleRunner
? defineExternalModuleNodes([...data.inlined, ...data.externalized])
: defineExternalModuleNodes(data.externalized)
const inlinedNodes
- = !config.value.experimental.viteModuleRunner
+ = !config.value.experimental?.viteModuleRunner
? []
: data.inlined.map(module =>
defineInlineModuleNode(module, module === rootPath),
diff --git a/packages/ui/client/composables/navigation.ts b/packages/ui/client/composables/navigation.ts
index ce67c4fffa5e..44e5487d58b4 100644
--- a/packages/ui/client/composables/navigation.ts
+++ b/packages/ui/client/composables/navigation.ts
@@ -17,7 +17,7 @@ export const coverageConfigured = computed(() => coverage.value?.enabled)
export const coverageEnabled = computed(() => {
return (
coverageConfigured.value
- && !!coverage.value.htmlReporter
+ && !!coverage.value?.htmlReporter
)
})
export const mainSizes = useLocalStorage<[left: number, right: number]>(
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 2e54d3789d2b..51aa86c9c875 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -75,10 +75,10 @@
"birpc": "catalog:",
"codemirror": "^5.65.18",
"codemirror-theme-vars": "^0.1.2",
- "d3-graph-controller": "^3.1.6",
+ "d3-graph-controller": "^3.1.7",
"floating-vue": "^5.2.2",
"mime": "^4.1.0",
- "rollup": "^4.56.0",
+ "rollup": "^4.57.0",
"splitpanes": "^4.0.4",
"typescript": "^5.9.3",
"unocss": "catalog:",
@@ -87,7 +87,7 @@
"vitest-browser-vue": "2.0.2",
"vue": "catalog:",
"vue-router": "^4.6.4",
- "vue-tsc": "^3.2.3",
+ "vue-tsc": "^3.2.4",
"vue-virtual-scroller": "2.0.0-beta.8"
}
}
diff --git a/packages/utils/src/serialize.ts b/packages/utils/src/serialize.ts
index 1a7b8ce1a408..b822bca5e687 100644
--- a/packages/utils/src/serialize.ts
+++ b/packages/utils/src/serialize.ts
@@ -120,7 +120,7 @@ export function serializeValue(val: any, seen: WeakMap = new WeakM
obj = Object.getPrototypeOf(obj)
}
if (val instanceof Error) {
- safe(() => val.message = normalizeErrorMessage(val.message))
+ safe(() => clone.message = normalizeErrorMessage(val.message))
}
return clone
}
@@ -136,5 +136,11 @@ function safe(fn: () => void) {
}
function normalizeErrorMessage(message: string) {
- return message.replace(/__(vite_ssr_import|vi_import)_\d+__\./g, '')
+ return message
+ // vite 7+
+ .replace(/\(0\s?,\s?__vite_ssr_import_\d+__.(\w+)\)/g, '$1')
+ // vite <7
+ .replace(/__(vite_ssr_import|vi_import)_\d+__\./g, '')
+ // vitest-browser-* errors will have __vitest___ in their messages
+ .replace(/getByTestId('__vitest_\d+__')/g, 'page')
}
diff --git a/packages/utils/src/source-map.ts b/packages/utils/src/source-map.ts
index 3c2fbd013a2d..efc9a9e322a4 100644
--- a/packages/utils/src/source-map.ts
+++ b/packages/utils/src/source-map.ts
@@ -35,6 +35,7 @@ const stackIgnorePatterns: (string | RegExp)[] = [
/node:\w+/,
/__vitest_test__/,
/__vitest_browser__/,
+ '/@id/__x00__vitest/browser',
/\/deps\/vitest_/,
]
@@ -197,7 +198,10 @@ export function parseSingleV8Stack(raw: string): ParsedStack | null {
if (method) {
method = method
- .replace(/__vite_ssr_import_\d+__\./g, '')
+ // vite 7+
+ .replace(/\(0\s?,\s?__vite_ssr_import_\d+__.(\w+)\)/g, '$1')
+ // vite <7
+ .replace(/__(vite_ssr_import|vi_import)_\d+__\./g, '')
.replace(/(Object\.)?__vite_ssr_export_default__\s?/g, '')
}
diff --git a/packages/vitest/package.json b/packages/vitest/package.json
index a7b9a206a956..92e662d7bb0d 100644
--- a/packages/vitest/package.json
+++ b/packages/vitest/package.json
@@ -193,7 +193,7 @@
},
"devDependencies": {
"@antfu/install-pkg": "^1.1.0",
- "@bomb.sh/tab": "^0.0.11",
+ "@bomb.sh/tab": "^0.0.12",
"@edge-runtime/vm": "^5.0.0",
"@jridgewell/trace-mapping": "catalog:",
"@opentelemetry/api": "^1.9.0",
@@ -212,7 +212,7 @@
"cac": "catalog:",
"empathic": "^2.0.0",
"flatted": "catalog:",
- "happy-dom": "^20.3.7",
+ "happy-dom": "^20.4.0",
"jsdom": "^27.4.0",
"local-pkg": "^1.1.2",
"mime": "^4.1.0",
diff --git a/packages/vitest/src/api/setup.ts b/packages/vitest/src/api/setup.ts
index f4c9a9ced29d..c04c4dd73adb 100644
--- a/packages/vitest/src/api/setup.ts
+++ b/packages/vitest/src/api/setup.ts
@@ -59,9 +59,6 @@ export function setup(ctx: Vitest, _server?: ViteDevServer): void {
function setupClient(ws: WebSocket) {
const rpc = createBirpc(
{
- async onTaskUpdate(packs, events) {
- await ctx._testRun.updated(packs, events)
- },
getFiles() {
return ctx.state.getFiles()
},
@@ -80,12 +77,24 @@ export function setup(ctx: Vitest, _server?: ViteDevServer): void {
`Test file "${id}" was not registered, so it cannot be updated using the API.`,
)
}
+ // silently ignore write attempts if not allowed
+ if (!ctx.config.api.allowWrite) {
+ return
+ }
return fs.writeFile(id, content, 'utf-8')
},
async rerun(files, resetTestNamePattern) {
+ // silently ignore exec attempts if not allowed
+ if (!ctx.config.api.allowExec) {
+ return
+ }
await ctx.rerunFiles(files, undefined, true, resetTestNamePattern)
},
async rerunTask(id) {
+ // silently ignore exec attempts if not allowed
+ if (!ctx.config.api.allowExec) {
+ return
+ }
await ctx.rerunTask(id)
},
getConfig() {
@@ -150,6 +159,11 @@ export function setup(ctx: Vitest, _server?: ViteDevServer): void {
return getModuleGraph(ctx, project, id, browser)
},
async updateSnapshot(file?: File) {
+ // silently ignore exec/write attempts if not allowed
+ // this function both executes the code and write snapshots
+ if (!ctx.config.api.allowExec || !ctx.config.api.allowWrite) {
+ return
+ }
if (!file) {
await ctx.updateSnapshot()
}
diff --git a/packages/vitest/src/api/types.ts b/packages/vitest/src/api/types.ts
index ac97c2d84cb1..231c1008aa33 100644
--- a/packages/vitest/src/api/types.ts
+++ b/packages/vitest/src/api/types.ts
@@ -36,7 +36,6 @@ export interface TransformResultWithSource {
}
export interface WebSocketHandlers {
- onTaskUpdate: (packs: TaskResultPack[], events: TaskEventPack[]) => void
getFiles: () => File[]
getTestFiles: () => Promise
getPaths: () => string[]
diff --git a/packages/vitest/src/node/ast-collect.ts b/packages/vitest/src/node/ast-collect.ts
index 8b29dd1e92e8..562dbab36a6a 100644
--- a/packages/vitest/src/node/ast-collect.ts
+++ b/packages/vitest/src/node/ast-collect.ts
@@ -168,8 +168,10 @@ function astParseFile(filepath: string, code: string) {
}
message = message
- // Vite SSR injects these
- .replace(/__vite_ssr_import_\d+__\./g, '')
+ // vite 7+
+ .replace(/\(0\s?,\s?__vite_ssr_import_\d+__.(\w+)\)/g, '$1')
+ // vite <7
+ .replace(/__(vite_ssr_import|vi_import)_\d+__\./g, '')
// Vitest module mocker injects these
.replace(/__vi_import_\d+__\./g, '')
diff --git a/packages/vitest/src/node/cli/cli-config.ts b/packages/vitest/src/node/cli/cli-config.ts
index f7c2efb49c88..2eb744f295c4 100644
--- a/packages/vitest/src/node/cli/cli-config.ts
+++ b/packages/vitest/src/node/cli/cli-config.ts
@@ -46,6 +46,12 @@ const apiConfig: (port: number) => CLIOptions = (port: number) => ({
description:
'Set to true to exit if port is already in use, instead of automatically trying the next available port',
},
+ allowExec: {
+ description: 'Allow API to execute code. (Be careful when enabling this option in untrusted environments)',
+ },
+ allowWrite: {
+ description: 'Allow API to edit files. (Be careful when enabling this option in untrusted environments)',
+ },
middlewareMode: null,
})
@@ -106,6 +112,12 @@ export const cliOptionsConfig: VitestCLIOptions = {
argument: '[port]',
description: `Specify server port. Note if the port is already being used, Vite will automatically try the next available port so this may not be the actual port the server ends up listening on. If true will be set to ${defaultPort}`,
subcommands: apiConfig(defaultPort),
+ transform(portOrOptions) {
+ if (typeof portOrOptions === 'number') {
+ return { port: portOrOptions }
+ }
+ return portOrOptions
+ },
},
silent: {
description: 'Silent console output from tests. Use `\'passed-only\'` to see logs from failing tests only.',
@@ -827,12 +839,36 @@ export const cliOptionsConfig: VitestCLIOptions = {
},
subcommands: {
print: {
- description: 'Print import breakdown to CLI terminal after tests finish (default: false).',
+ description: 'When to print import breakdown to CLI terminal. Use `true` to always print, `false` to never print, or `on-warn` to print only when imports exceed the warn threshold (default: false).',
+ argument: '',
+ transform(value) {
+ if (value === 'on-warn') {
+ return 'on-warn'
+ }
+ return value
+ },
},
limit: {
description: 'Maximum number of imports to collect and display (default: 0, or 10 if print or UI is enabled).',
argument: '',
},
+ failOnDanger: {
+ description: 'Fail the test run if any import exceeds the danger threshold (default: false).',
+ },
+ thresholds: {
+ description: 'Duration thresholds in milliseconds for coloring and warnings.',
+ argument: '',
+ subcommands: {
+ warn: {
+ description: 'Warning threshold - imports exceeding this are shown in yellow/orange (default: 100).',
+ argument: '',
+ },
+ danger: {
+ description: 'Danger threshold - imports exceeding this are shown in red (default: 500).',
+ argument: '',
+ },
+ },
+ },
},
},
viteModuleRunner: {
diff --git a/packages/vitest/src/node/config/resolveConfig.ts b/packages/vitest/src/node/config/resolveConfig.ts
index c61021cadb3d..14005c02708d 100644
--- a/packages/vitest/src/node/config/resolveConfig.ts
+++ b/packages/vitest/src/node/config/resolveConfig.ts
@@ -1,5 +1,6 @@
import type { ResolvedConfig as ResolvedViteConfig } from 'vite'
import type { Vitest } from '../core'
+import type { Logger } from '../logger'
import type { BenchmarkBuiltinReporters } from '../reporters'
import type { ResolvedBrowserOptions } from '../types/browser'
import type {
@@ -55,9 +56,14 @@ function parseInspector(inspect: string | undefined | boolean | number) {
return { host, port: Number(port) || defaultInspectPort }
}
+/**
+ * @deprecated Internal function
+ */
export function resolveApiServerConfig>(
options: Options,
defaultPort: number,
+ parentApi?: ApiConfig,
+ logger?: Logger,
): ApiConfig | undefined {
let api: ApiConfig | undefined
@@ -97,6 +103,26 @@ export function resolveApiServerConfig {
+ return {
+ allowExec: api?.allowExec,
+ allowWrite: api?.allowWrite,
+ }
+ })(project.isBrowserEnabled() ? config.browser.api : config.api),
// TODO: non serializable function?
diff: config.diff,
retry: config.retry,
diff --git a/packages/vitest/src/node/reporters/base.ts b/packages/vitest/src/node/reporters/base.ts
index 5e85f4043763..207b9db2d3ed 100644
--- a/packages/vitest/src/node/reporters/base.ts
+++ b/packages/vitest/src/node/reporters/base.ts
@@ -607,14 +607,17 @@ export abstract class BaseReporter implements Reporter {
}
}
- if (this.ctx.config.experimental.importDurations.print) {
- this.printImportsBreakdown()
- }
+ this.reportImportDurations()
this.log()
}
- private printImportsBreakdown() {
+ private reportImportDurations() {
+ const { print, failOnDanger, thresholds } = this.ctx.config.experimental.importDurations
+ if (!print && !failOnDanger) {
+ return
+ };
+
const testModules = this.ctx.state.getTestModules()
interface ImportEntry {
@@ -647,6 +650,18 @@ export abstract class BaseReporter implements Reporter {
return
}
+ const dangerImports = allImports.filter(imp => imp.totalTime >= thresholds.danger)
+ const warnImports = allImports.filter(imp => imp.totalTime >= thresholds.warn)
+ const hasDangerImports = dangerImports.length > 0
+ const hasWarnImports = warnImports.length > 0
+
+ // Determine if we should print
+ const shouldFail = failOnDanger && hasDangerImports
+ const shouldPrint = (print === true) || (print === 'on-warn' && hasWarnImports) || shouldFail
+ if (!shouldPrint) {
+ return
+ }
+
const sortedImports = allImports.sort((a, b) => b.totalTime - a.totalTime)
const maxTotalTime = sortedImports[0].totalTime
const limit = this.ctx.config.experimental.importDurations.limit
@@ -657,21 +672,25 @@ export abstract class BaseReporter implements Reporter {
const slowestImport = sortedImports[0]
this.log()
- this.log(c.bold('Import Duration Breakdown') + c.dim(` (ordered by Total Time) (Top ${limit})`))
+ this.log(c.bold('Import Duration Breakdown') + c.dim(` (Top ${limit})`))
+ this.log()
+ this.log(c.dim(`${'Module'.padEnd(50)} ${'Self'.padStart(6)} ${'Total'.padStart(6)}`))
// if there are multiple files, it's highly possible that some of them will import the same large file
// we group them to show the distinction between those files more easily
- // Import Duration Breakdown (ordered by Total Time) (Top 10)
- // .../fields/FieldFile/__tests__/FieldFile.spec.ts self: 7ms total: 1.01s ████████████████████
- // ↳ tests/support/components/index.ts self: 0ms total: 861ms █████████████████░░░
- // ↳ tests/support/components/renderComponent.ts self: 59ms total: 861ms █████████████████░░░
- // ...s__/apps/desktop/form-updater.desktop.spec.ts self: 8ms total: 991ms ████████████████████
- // ...sts__/apps/mobile/form-updater.mobile.spec.ts self: 11ms total: 990ms ████████████████████
- // shared/components/Form/__tests__/Form.spec.ts self: 5ms total: 988ms ████████████████████
- // ↳ tests/support/components/index.ts self: 0ms total: 935ms ███████████████████░
- // ↳ tests/support/components/renderComponent.ts self: 61ms total: 935ms ███████████████████░
- // ...ditor/features/link/__test__/LinkForm.spec.ts self: 7ms total: 972ms ███████████████████░
- // ↳ tests/support/components/renderComponent.ts self: 56ms total: 936ms ███████████████████░
+ // Import Duration Breakdown (Top 10)
+ //
+ // Module Self Total
+ // .../fields/FieldFile/__tests__/FieldFile.spec.ts 7ms 1.01s ████████████████████
+ // ↳ tests/support/components/index.ts 0ms 861ms █████████████████░░░
+ // ↳ tests/support/components/renderComponent.ts 59ms 861ms █████████████████░░░
+ // ...s__/apps/desktop/form-updater.desktop.spec.ts 8ms 991ms ████████████████████
+ // ...sts__/apps/mobile/form-updater.mobile.spec.ts 11ms 990ms ████████████████████
+ // shared/components/Form/__tests__/Form.spec.ts 5ms 988ms ████████████████████
+ // ↳ tests/support/components/index.ts 0ms 935ms ███████████████████░
+ // ↳ tests/support/components/renderComponent.ts 61ms 935ms ███████████████████░
+ // ...ditor/features/link/__test__/LinkForm.spec.ts 7ms 972ms ███████████████████░
+ // ↳ tests/support/components/renderComponent.ts 56ms 936ms ███████████████████░
const groupedImports = Object.entries(
groupBy(topImports, i => i.testModule.id),
@@ -688,7 +707,7 @@ export abstract class BaseReporter implements Reporter {
const pathDisplay = this.ellipsisPath(imp.importedModuleId, imp.external, groupedImports.length > 1 && index > 0)
this.log(
- `${pathDisplay} ${c.dim('self:')} ${this.importDurationTime(imp.selfTime)} ${c.dim('total:')} ${this.importDurationTime(imp.totalTime)} ${bar}`,
+ `${pathDisplay} ${this.importDurationTime(imp.selfTime)} ${this.importDurationTime(imp.totalTime)} ${bar}`,
)
})
}
@@ -697,10 +716,20 @@ export abstract class BaseReporter implements Reporter {
this.log(c.dim('Total imports: ') + allImports.length)
this.log(c.dim('Slowest import (total-time): ') + formatTime(slowestImport.totalTime))
this.log(c.dim('Total import time (self/total): ') + formatTime(totalSelfTime) + c.dim(' / ') + formatTime(totalTotalTime))
+
+ // Fail if danger threshold exceeded
+ if (shouldFail) {
+ this.log()
+ this.ctx.logger.error(
+ `ERROR: ${dangerImports.length} import(s) exceeded the danger threshold of ${thresholds.danger}ms`,
+ )
+ process.exitCode = 1
+ }
}
private importDurationTime(duration: number) {
- const color = duration >= 500 ? c.red : duration >= 100 ? c.yellow : (c: string) => c
+ const { thresholds } = this.ctx.config.experimental.importDurations
+ const color = duration >= thresholds.danger ? c.red : duration >= thresholds.warn ? c.yellow : (c: string) => c
return color(formatTime(duration).padStart(6))
}
diff --git a/packages/vitest/src/node/types/config.ts b/packages/vitest/src/node/types/config.ts
index e65d5b8656dd..49e5caee242a 100644
--- a/packages/vitest/src/node/types/config.ts
+++ b/packages/vitest/src/node/types/config.ts
@@ -44,7 +44,22 @@ export type CSSModuleScopeStrategy = 'stable' | 'scoped' | 'non-scoped'
export type ApiConfig = Pick<
ServerOptions,
'port' | 'strictPort' | 'host' | 'middlewareMode'
->
+> & {
+ /**
+ * Allow any write operations from the API server.
+ *
+ * @default true if `api.host` is exposed to network, false otherwise
+ */
+ allowWrite?: boolean
+ /**
+ * Allow running test files via the API.
+ * If `api.host` is exposed to network and `allowWrite` is true,
+ * anyone connected to the API server can run arbitrary code on your machine.
+ *
+ * @default true if `api.host` is exposed to network, false otherwise
+ */
+ allowExec?: boolean
+}
export interface EnvironmentOptions {
/**
@@ -864,15 +879,39 @@ export interface InlineConfig {
*/
importDurations?: {
/**
- * Print import breakdown to CLI terminal after tests finish.
+ * When to print import breakdown to CLI terminal after tests finish.
+ * - `true`: Always print
+ * - `false`: Never print (default)
+ * - `'on-warn'`: Print only when any import exceeds the warn threshold
* @default false
*/
- print?: boolean
+ print?: boolean | 'on-warn'
/**
* Maximum number of imports to collect and display.
* @default 0 (or 10 if `print` or UI is enabled)
*/
limit?: number
+ /**
+ * Fail the test run if any import exceeds the danger threshold.
+ * When failing, the breakdown is always printed regardless of `print` setting.
+ * @default false
+ */
+ failOnDanger?: boolean
+ /**
+ * Duration thresholds in milliseconds for coloring and warnings.
+ */
+ thresholds?: {
+ /**
+ * Warning threshold - imports exceeding this are shown in yellow/orange.
+ * @default 100
+ */
+ warn?: number
+ /**
+ * Danger threshold - imports exceeding this are shown in red.
+ * @default 500
+ */
+ danger?: number
+ }
}
/**
@@ -1158,8 +1197,13 @@ export interface ResolvedConfig
experimental: Omit['experimental'], 'importDurations'> & {
importDurations: {
- print: boolean
+ print: boolean | 'on-warn'
limit: number
+ failOnDanger: boolean
+ thresholds: {
+ warn: number
+ danger: number
+ }
}
}
}
diff --git a/packages/vitest/src/runtime/config.ts b/packages/vitest/src/runtime/config.ts
index 57a485f3a7eb..18f7dd92fb54 100644
--- a/packages/vitest/src/runtime/config.ts
+++ b/packages/vitest/src/runtime/config.ts
@@ -76,6 +76,10 @@ export interface SerializedConfig {
showDiff?: boolean
truncateThreshold?: number
} | undefined
+ api: {
+ allowExec: boolean | undefined
+ allowWrite: boolean | undefined
+ }
diff: string | SerializedDiffOptions | undefined
retry: SerializableRetry
includeTaskLocation: boolean | undefined
@@ -121,8 +125,13 @@ export interface SerializedConfig {
experimental: {
fsModuleCache: boolean
importDurations: {
- print: boolean
+ print: boolean | 'on-warn'
limit: number
+ failOnDanger: boolean
+ thresholds: {
+ warn: number
+ danger: number
+ }
}
viteModuleRunner: boolean
nodeLoader: boolean
diff --git a/packages/vitest/src/runtime/moduleRunner/moduleEvaluator.ts b/packages/vitest/src/runtime/moduleRunner/moduleEvaluator.ts
index f5e45ea53306..76fb1ed4c1d6 100644
--- a/packages/vitest/src/runtime/moduleRunner/moduleEvaluator.ts
+++ b/packages/vitest/src/runtime/moduleRunner/moduleEvaluator.ts
@@ -315,7 +315,8 @@ export class VitestModuleEvaluator implements ModuleEvaluator {
)})=>{{`
const wrappedCode = `${codeDefinition}${code}\n}}`
const options = {
- filename: module.id,
+ // use original id for auto spy module (vi.mock(..., { spy: true }))
+ filename: module.id.startsWith('mock:') ? module.id.slice(5) : module.id,
lineOffset: 0,
columnOffset: -codeDefinition.length,
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ede38bf7588f..921bbec44b86 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -139,7 +139,7 @@ overrides:
'@vitest/ui': workspace:*
acorn: 8.11.3
mlly: ^1.8.0
- rollup: ^4.56.0
+ rollup: ^4.57.0
vite: 7.1.5
vitest: workspace:*
@@ -175,13 +175,13 @@ importers:
version: 1.58.0
'@rollup/plugin-commonjs':
specifier: ^29.0.0
- version: 29.0.0(rollup@4.56.0)
+ version: 29.0.0(rollup@4.57.0)
'@rollup/plugin-json':
specifier: ^6.1.0
- version: 6.1.0(rollup@4.56.0)
+ version: 6.1.0(rollup@4.57.0)
'@rollup/plugin-node-resolve':
specifier: ^16.0.3
- version: 16.0.3(rollup@4.56.0)
+ version: 16.0.3(rollup@4.57.0)
'@types/node':
specifier: 24.10.9
version: 24.10.9
@@ -222,14 +222,14 @@ importers:
specifier: ^4.0.0
version: 4.0.0
rollup:
- specifier: ^4.56.0
- version: 4.56.0
+ specifier: ^4.57.0
+ version: 4.57.0
rollup-plugin-dts:
specifier: ^6.3.0
- version: 6.3.0(rollup@4.56.0)(typescript@5.9.3)
+ version: 6.3.0(rollup@4.57.0)(typescript@5.9.3)
rollup-plugin-license:
specifier: ^3.6.0
- version: 3.6.0(picomatch@4.0.3)(rollup@4.56.0)
+ version: 3.6.0(picomatch@4.0.3)(rollup@4.57.0)
tinyglobby:
specifier: 'catalog:'
version: 0.2.15
@@ -292,8 +292,8 @@ importers:
specifier: 'catalog:'
version: 6.0.3(vite@7.1.5(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3))
'@voidzero-dev/vitepress-theme':
- specifier: ^4.3.0
- version: 4.3.0(axios@1.13.2)(change-case@5.4.4)(focus-trap@7.6.6)(typescript@5.9.3)(vite@7.1.5(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitepress@2.0.0-alpha.15(@types/node@24.10.9)(axios@1.13.2)(change-case@5.4.4)(jiti@2.6.1)(lightningcss@1.30.2)(oxc-minify@0.108.0)(postcss@8.5.6)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3))
+ specifier: ^4.4.1
+ version: 4.4.1(axios@1.13.4)(change-case@5.4.4)(focus-trap@7.6.6)(typescript@5.9.3)(vite@7.1.5(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitepress@2.0.0-alpha.15(@types/node@24.10.9)(axios@1.13.4)(change-case@5.4.4)(jiti@2.6.1)(lightningcss@1.30.2)(oxc-minify@0.108.0)(postcss@8.5.6)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3))
https-localhost:
specifier: ^4.7.1
version: 4.7.1
@@ -311,7 +311,7 @@ importers:
version: 1.2.0(@vite-pwa/assets-generator@1.0.2)(vite@7.1.5(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(workbox-build@7.1.0(@types/babel__core@7.20.5))(workbox-window@7.4.0)
vitepress:
specifier: 2.0.0-alpha.15
- version: 2.0.0-alpha.15(@types/node@24.10.9)(axios@1.13.2)(change-case@5.4.4)(jiti@2.6.1)(lightningcss@1.30.2)(oxc-minify@0.108.0)(postcss@8.5.6)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
+ version: 2.0.0-alpha.15(@types/node@24.10.9)(axios@1.13.4)(change-case@5.4.4)(jiti@2.6.1)(lightningcss@1.30.2)(oxc-minify@0.108.0)(postcss@8.5.6)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
vitepress-plugin-group-icons:
specifier: ^1.7.1
version: 1.7.1(vite@7.1.5(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
@@ -320,7 +320,7 @@ importers:
version: 1.10.0
vitepress-plugin-tabs:
specifier: ^0.7.3
- version: 0.7.3(vitepress@2.0.0-alpha.15(@types/node@24.10.9)(axios@1.13.2)(change-case@5.4.4)(jiti@2.6.1)(lightningcss@1.30.2)(oxc-minify@0.108.0)(postcss@8.5.6)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3))
+ version: 0.7.3(vitepress@2.0.0-alpha.15(@types/node@24.10.9)(axios@1.13.4)(change-case@5.4.4)(jiti@2.6.1)(lightningcss@1.30.2)(oxc-minify@0.108.0)(postcss@8.5.6)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3))
workbox-window:
specifier: ^7.4.0
version: 7.4.0
@@ -343,8 +343,8 @@ importers:
specifier: workspace:*
version: link:../../packages/ui
fastify:
- specifier: ^5.7.1
- version: 5.7.1
+ specifier: ^5.7.2
+ version: 5.7.2
supertest:
specifier: ^7.2.2
version: 7.2.2
@@ -435,13 +435,13 @@ importers:
version: 6.9.1
'@testing-library/react':
specifier: ^16.3.2
- version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.2.14)(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.2.14)(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@testing-library/user-event':
specifier: ^14.6.1
version: 14.6.1(@testing-library/dom@10.4.1)
'@types/react':
- specifier: ^19.2.9
- version: 19.2.9
+ specifier: ^19.2.10
+ version: 19.2.10
'@vitejs/plugin-react':
specifier: ^5.1.2
version: 5.1.2(vite@7.1.5(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
@@ -449,14 +449,14 @@ importers:
specifier: workspace:*
version: link:../../packages/ui
fastify:
- specifier: ^5.7.1
- version: 5.7.1
+ specifier: ^5.7.2
+ version: 5.7.2
jsdom:
specifier: ^27.4.0
version: 27.4.0
react:
- specifier: ^19.2.3
- version: 19.2.3
+ specifier: ^19.2.4
+ version: 19.2.4
supertest:
specifier: ^7.2.2
version: 7.2.2
@@ -785,8 +785,8 @@ importers:
specifier: ^19.2.0
version: 19.2.0
react-is:
- specifier: ^19.2.3
- version: 19.2.3
+ specifier: ^19.2.4
+ version: 19.2.4
react-is-18:
specifier: npm:react-is@18.3.1
version: react-is@18.3.1
@@ -906,8 +906,8 @@ importers:
specifier: ^0.1.2
version: 0.1.2
d3-graph-controller:
- specifier: ^3.1.6
- version: 3.1.6
+ specifier: ^3.1.7
+ version: 3.1.7
floating-vue:
specifier: ^5.2.2
version: 5.2.2(vue@3.5.27(typescript@5.9.3))
@@ -915,8 +915,8 @@ importers:
specifier: ^4.1.0
version: 4.1.0
rollup:
- specifier: ^4.56.0
- version: 4.56.0
+ specifier: ^4.57.0
+ version: 4.57.0
splitpanes:
specifier: ^4.0.4
version: 4.0.4(vue@3.5.27(typescript@5.9.3))
@@ -942,8 +942,8 @@ importers:
specifier: ^4.6.4
version: 4.6.4(vue@3.5.27(typescript@5.9.3))
vue-tsc:
- specifier: ^3.2.3
- version: 3.2.3(typescript@5.9.3)
+ specifier: ^3.2.4
+ version: 3.2.4(typescript@5.9.3)
vue-virtual-scroller:
specifier: 2.0.0-beta.8
version: 2.0.0-beta.8(vue@3.5.27(typescript@5.9.3))
@@ -1052,8 +1052,8 @@ importers:
specifier: ^1.1.0
version: 1.1.0
'@bomb.sh/tab':
- specifier: ^0.0.11
- version: 0.0.11(cac@6.7.14(patch_hash=a8f0f3517a47ce716ed90c0cfe6ae382ab763b021a664ada2a608477d0621588))(citty@0.1.6)
+ specifier: ^0.0.12
+ version: 0.0.12(cac@6.7.14(patch_hash=a8f0f3517a47ce716ed90c0cfe6ae382ab763b021a664ada2a608477d0621588))(citty@0.1.6)
'@edge-runtime/vm':
specifier: ^5.0.0
version: 5.0.0
@@ -1109,8 +1109,8 @@ importers:
specifier: 'catalog:'
version: 3.3.3
happy-dom:
- specifier: ^20.3.7
- version: 20.3.7
+ specifier: ^20.4.0
+ version: 20.4.0
jsdom:
specifier: ^27.4.0
version: 27.4.0
@@ -1165,8 +1165,8 @@ importers:
test/browser:
devDependencies:
'@types/react':
- specifier: ^19.2.9
- version: 19.2.9
+ specifier: ^19.2.10
+ version: 19.2.10
'@vitejs/plugin-basic-ssl':
specifier: ^2.1.4
version: 2.1.4(vite@7.1.5(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
@@ -1192,11 +1192,11 @@ importers:
specifier: ^1.58.0
version: 1.58.0
react:
- specifier: ^19.2.3
- version: 19.2.3
+ specifier: ^19.2.4
+ version: 19.2.4
react-dom:
- specifier: ^19.2.3
- version: 19.2.3(react@19.2.3)
+ specifier: ^19.2.4
+ version: 19.2.4(react@19.2.4)
url:
specifier: ^0.11.4
version: 0.11.4
@@ -1204,8 +1204,8 @@ importers:
specifier: workspace:*
version: link:../../packages/vitest
vitest-browser-react:
- specifier: ^2.0.4
- version: 2.0.4(@types/react-dom@18.2.14)(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@packages+vitest)
+ specifier: ^2.0.5
+ version: 2.0.5(@types/react-dom@18.2.14)(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@packages+vitest)
ws:
specifier: 'catalog:'
version: 8.19.0
@@ -1274,7 +1274,7 @@ importers:
version: 5.9.3
unplugin-swc:
specifier: ^1.5.9
- version: 1.5.9(@swc/core@1.4.1(@swc/helpers@0.5.17))(rollup@4.56.0)
+ version: 1.5.9(@swc/core@1.4.1(@swc/helpers@0.5.17))(rollup@4.57.0)
vite:
specifier: 7.1.5
version: 7.1.5(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
@@ -1385,10 +1385,10 @@ importers:
version: link:../../packages/web-worker
'@vueuse/integrations':
specifier: ^14.1.0
- version: 14.1.0(axios@1.13.2)(change-case@5.4.4)(focus-trap@7.6.6)(vue@3.5.27(typescript@5.9.3))
+ version: 14.1.0(axios@1.13.4)(change-case@5.4.4)(focus-trap@7.6.6)(vue@3.5.27(typescript@5.9.3))
axios:
- specifier: ^1.13.2
- version: 1.13.2
+ specifier: ^1.13.4
+ version: 1.13.4
immutable:
specifier: 5.1.4
version: 5.1.4
@@ -1399,8 +1399,8 @@ importers:
specifier: ^2.1.1
version: 2.1.1
react:
- specifier: ^19.2.3
- version: 19.2.3
+ specifier: ^19.2.4
+ version: 19.2.4
react-18:
specifier: npm:react@18.3.1
version: react@18.3.1
@@ -1466,7 +1466,7 @@ importers:
version: 2.4.6
happy-dom:
specifier: latest
- version: 20.3.7
+ version: 20.4.0
istanbul-lib-coverage:
specifier: 'catalog:'
version: 3.2.2
@@ -1484,7 +1484,7 @@ importers:
version: 1.97.3
unplugin-swc:
specifier: ^1.5.9
- version: 1.5.9(@swc/core@1.4.1(@swc/helpers@0.5.17))(rollup@4.56.0)
+ version: 1.5.9(@swc/core@1.4.1(@swc/helpers@0.5.17))(rollup@4.57.0)
vite:
specifier: 7.1.5
version: 7.1.5(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
@@ -1553,8 +1553,8 @@ importers:
specifier: ^5.9.3
version: 5.9.3
vue-tsc:
- specifier: ^3.2.3
- version: 3.2.3(typescript@5.9.3)
+ specifier: ^3.2.4
+ version: 3.2.4(typescript@5.9.3)
test/ui:
devDependencies:
@@ -1566,7 +1566,7 @@ importers:
version: link:../../packages/browser-playwright
happy-dom:
specifier: latest
- version: 20.3.7
+ version: 20.4.0
vitest:
specifier: workspace:*
version: link:../../packages/vitest
@@ -2272,8 +2272,8 @@ packages:
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
engines: {node: '>=18'}
- '@bomb.sh/tab@0.0.11':
- resolution: {integrity: sha512-RSqyreeicYBALcMaNxIUJTBknftXsyW45VRq5gKDNwKroh0Re5SDoWwXZaphb+OTEzVdpm/BA8Uq6y0P+AtVYw==}
+ '@bomb.sh/tab@0.0.12':
+ resolution: {integrity: sha512-dYRwg4MqfHR5/BcTy285XOGRhjQFmNpaJBZ0tl2oU+RY595MQ5ApTF6j3OvauPAooHL6cfoOZMySQrOQztT8RQ==}
hasBin: true
peerDependencies:
cac: ^6.7.14
@@ -4202,7 +4202,7 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0
'@types/babel__core': ^7.1.9
- rollup: ^4.56.0
+ rollup: ^4.57.0
peerDependenciesMeta:
'@types/babel__core':
optional: true
@@ -4211,7 +4211,7 @@ packages:
resolution: {integrity: sha512-U2YHaxR2cU/yAiwKJtJRhnyLk7cifnQw0zUpISsocBDoHDJn+HTV74ABqnwr5bEgWUwFZC9oFL6wLe21lHu5eQ==}
engines: {node: '>=16.0.0 || 14 >= 14.17'}
peerDependencies:
- rollup: ^4.56.0
+ rollup: ^4.57.0
peerDependenciesMeta:
rollup:
optional: true
@@ -4220,7 +4220,7 @@ packages:
resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==}
engines: {node: '>=14.0.0'}
peerDependencies:
- rollup: ^4.56.0
+ rollup: ^4.57.0
peerDependenciesMeta:
rollup:
optional: true
@@ -4229,7 +4229,7 @@ packages:
resolution: {integrity: sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==}
engines: {node: '>=14.0.0'}
peerDependencies:
- rollup: ^4.56.0
+ rollup: ^4.57.0
peerDependenciesMeta:
rollup:
optional: true
@@ -4238,7 +4238,7 @@ packages:
resolution: {integrity: sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==}
engines: {node: '>=14.0.0'}
peerDependencies:
- rollup: ^4.56.0
+ rollup: ^4.57.0
peerDependenciesMeta:
rollup:
optional: true
@@ -4246,13 +4246,13 @@ packages:
'@rollup/plugin-replace@2.4.2':
resolution: {integrity: sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==}
peerDependencies:
- rollup: ^4.56.0
+ rollup: ^4.57.0
'@rollup/plugin-terser@0.4.4':
resolution: {integrity: sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==}
engines: {node: '>=14.0.0'}
peerDependencies:
- rollup: ^4.56.0
+ rollup: ^4.57.0
peerDependenciesMeta:
rollup:
optional: true
@@ -4261,13 +4261,13 @@ packages:
resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==}
engines: {node: '>= 8.0.0'}
peerDependencies:
- rollup: ^4.56.0
+ rollup: ^4.57.0
'@rollup/pluginutils@5.1.4':
resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==}
engines: {node: '>=14.0.0'}
peerDependencies:
- rollup: ^4.56.0
+ rollup: ^4.57.0
peerDependenciesMeta:
rollup:
optional: true
@@ -4276,146 +4276,146 @@ packages:
resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==}
engines: {node: '>=14.0.0'}
peerDependencies:
- rollup: ^4.56.0
+ rollup: ^4.57.0
peerDependenciesMeta:
rollup:
optional: true
- '@rollup/rollup-android-arm-eabi@4.56.0':
- resolution: {integrity: sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw==}
+ '@rollup/rollup-android-arm-eabi@4.57.0':
+ resolution: {integrity: sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA==}
cpu: [arm]
os: [android]
- '@rollup/rollup-android-arm64@4.56.0':
- resolution: {integrity: sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q==}
+ '@rollup/rollup-android-arm64@4.57.0':
+ resolution: {integrity: sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg==}
cpu: [arm64]
os: [android]
- '@rollup/rollup-darwin-arm64@4.56.0':
- resolution: {integrity: sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w==}
+ '@rollup/rollup-darwin-arm64@4.57.0':
+ resolution: {integrity: sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg==}
cpu: [arm64]
os: [darwin]
- '@rollup/rollup-darwin-x64@4.56.0':
- resolution: {integrity: sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g==}
+ '@rollup/rollup-darwin-x64@4.57.0':
+ resolution: {integrity: sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA==}
cpu: [x64]
os: [darwin]
- '@rollup/rollup-freebsd-arm64@4.56.0':
- resolution: {integrity: sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ==}
+ '@rollup/rollup-freebsd-arm64@4.57.0':
+ resolution: {integrity: sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g==}
cpu: [arm64]
os: [freebsd]
- '@rollup/rollup-freebsd-x64@4.56.0':
- resolution: {integrity: sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg==}
+ '@rollup/rollup-freebsd-x64@4.57.0':
+ resolution: {integrity: sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA==}
cpu: [x64]
os: [freebsd]
- '@rollup/rollup-linux-arm-gnueabihf@4.56.0':
- resolution: {integrity: sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==}
+ '@rollup/rollup-linux-arm-gnueabihf@4.57.0':
+ resolution: {integrity: sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q==}
cpu: [arm]
os: [linux]
libc: [glibc]
- '@rollup/rollup-linux-arm-musleabihf@4.56.0':
- resolution: {integrity: sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==}
+ '@rollup/rollup-linux-arm-musleabihf@4.57.0':
+ resolution: {integrity: sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA==}
cpu: [arm]
os: [linux]
libc: [musl]
- '@rollup/rollup-linux-arm64-gnu@4.56.0':
- resolution: {integrity: sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==}
+ '@rollup/rollup-linux-arm64-gnu@4.57.0':
+ resolution: {integrity: sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==}
cpu: [arm64]
os: [linux]
libc: [glibc]
- '@rollup/rollup-linux-arm64-musl@4.56.0':
- resolution: {integrity: sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==}
+ '@rollup/rollup-linux-arm64-musl@4.57.0':
+ resolution: {integrity: sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw==}
cpu: [arm64]
os: [linux]
libc: [musl]
- '@rollup/rollup-linux-loong64-gnu@4.56.0':
- resolution: {integrity: sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==}
+ '@rollup/rollup-linux-loong64-gnu@4.57.0':
+ resolution: {integrity: sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q==}
cpu: [loong64]
os: [linux]
libc: [glibc]
- '@rollup/rollup-linux-loong64-musl@4.56.0':
- resolution: {integrity: sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==}
+ '@rollup/rollup-linux-loong64-musl@4.57.0':
+ resolution: {integrity: sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ==}
cpu: [loong64]
os: [linux]
libc: [musl]
- '@rollup/rollup-linux-ppc64-gnu@4.56.0':
- resolution: {integrity: sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==}
+ '@rollup/rollup-linux-ppc64-gnu@4.57.0':
+ resolution: {integrity: sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
- '@rollup/rollup-linux-ppc64-musl@4.56.0':
- resolution: {integrity: sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==}
+ '@rollup/rollup-linux-ppc64-musl@4.57.0':
+ resolution: {integrity: sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA==}
cpu: [ppc64]
os: [linux]
libc: [musl]
- '@rollup/rollup-linux-riscv64-gnu@4.56.0':
- resolution: {integrity: sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==}
+ '@rollup/rollup-linux-riscv64-gnu@4.57.0':
+ resolution: {integrity: sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
- '@rollup/rollup-linux-riscv64-musl@4.56.0':
- resolution: {integrity: sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==}
+ '@rollup/rollup-linux-riscv64-musl@4.57.0':
+ resolution: {integrity: sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ==}
cpu: [riscv64]
os: [linux]
libc: [musl]
- '@rollup/rollup-linux-s390x-gnu@4.56.0':
- resolution: {integrity: sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==}
+ '@rollup/rollup-linux-s390x-gnu@4.57.0':
+ resolution: {integrity: sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg==}
cpu: [s390x]
os: [linux]
libc: [glibc]
- '@rollup/rollup-linux-x64-gnu@4.56.0':
- resolution: {integrity: sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==}
+ '@rollup/rollup-linux-x64-gnu@4.57.0':
+ resolution: {integrity: sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==}
cpu: [x64]
os: [linux]
libc: [glibc]
- '@rollup/rollup-linux-x64-musl@4.56.0':
- resolution: {integrity: sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==}
+ '@rollup/rollup-linux-x64-musl@4.57.0':
+ resolution: {integrity: sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw==}
cpu: [x64]
os: [linux]
libc: [musl]
- '@rollup/rollup-openbsd-x64@4.56.0':
- resolution: {integrity: sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==}
+ '@rollup/rollup-openbsd-x64@4.57.0':
+ resolution: {integrity: sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw==}
cpu: [x64]
os: [openbsd]
- '@rollup/rollup-openharmony-arm64@4.56.0':
- resolution: {integrity: sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ==}
+ '@rollup/rollup-openharmony-arm64@4.57.0':
+ resolution: {integrity: sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA==}
cpu: [arm64]
os: [openharmony]
- '@rollup/rollup-win32-arm64-msvc@4.56.0':
- resolution: {integrity: sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing==}
+ '@rollup/rollup-win32-arm64-msvc@4.57.0':
+ resolution: {integrity: sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ==}
cpu: [arm64]
os: [win32]
- '@rollup/rollup-win32-ia32-msvc@4.56.0':
- resolution: {integrity: sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg==}
+ '@rollup/rollup-win32-ia32-msvc@4.57.0':
+ resolution: {integrity: sha512-3K1lP+3BXY4t4VihLw5MEg6IZD3ojSYzqzBG571W3kNQe4G4CcFpSUQVgurYgib5d+YaCjeFow8QivWp8vuSvA==}
cpu: [ia32]
os: [win32]
- '@rollup/rollup-win32-x64-gnu@4.56.0':
- resolution: {integrity: sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ==}
+ '@rollup/rollup-win32-x64-gnu@4.57.0':
+ resolution: {integrity: sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA==}
cpu: [x64]
os: [win32]
- '@rollup/rollup-win32-x64-msvc@4.56.0':
- resolution: {integrity: sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==}
+ '@rollup/rollup-win32-x64-msvc@4.57.0':
+ resolution: {integrity: sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ==}
cpu: [x64]
os: [win32]
@@ -4830,8 +4830,8 @@ packages:
'@types/react-is@19.2.0':
resolution: {integrity: sha512-NP2xtcjZfORsOa4g2JwdseyEnF+wUCx25fTdG/J/HIY6yKga6+NozRBg2xR2gyh7kKYyd6DXndbq0YbQuTJ7Ew==}
- '@types/react@19.2.9':
- resolution: {integrity: sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==}
+ '@types/react@19.2.10':
+ resolution: {integrity: sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==}
'@types/resolve@1.20.2':
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
@@ -5111,8 +5111,8 @@ packages:
'@vitest/test-fn@file:test/core/deps/dep-fn':
resolution: {directory: test/core/deps/dep-fn, type: directory}
- '@voidzero-dev/vitepress-theme@4.3.0':
- resolution: {integrity: sha512-P4BgHCKfSND8lYD7qKjHEePwO+60bll4HDPJ60Ms7ZU8y6W405eLMXpCJy4t83VIvteiqLRFtB2KFLPB9tEKrA==}
+ '@voidzero-dev/vitepress-theme@4.4.1':
+ resolution: {integrity: sha512-7t8FgLTzVWi2oog0/y4HoQ8PSkBFax+P1TKVsj2by/4PlYQ4GbQ8w4qpaU2ocDMR6lC+PN7ba2dQoBsqK6kTjw==}
peerDependencies:
vitepress: ^2.0.0-alpha.15
vue: ^3.5.0
@@ -5126,15 +5126,9 @@ packages:
'@volar/typescript@2.4.27':
resolution: {integrity: sha512-eWaYCcl/uAPInSK2Lze6IqVWaBu/itVqR5InXcHXFyles4zO++Mglt3oxdgj75BDcv1Knr9Y93nowS8U3wqhxg==}
- '@vue/compiler-core@3.5.26':
- resolution: {integrity: sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==}
-
'@vue/compiler-core@3.5.27':
resolution: {integrity: sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==}
- '@vue/compiler-dom@3.5.26':
- resolution: {integrity: sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==}
-
'@vue/compiler-dom@3.5.27':
resolution: {integrity: sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==}
@@ -5156,12 +5150,12 @@ packages:
'@vue/devtools-shared@8.0.5':
resolution: {integrity: sha512-bRLn6/spxpmgLk+iwOrR29KrYnJjG9DGpHGkDFG82UM21ZpJ39ztUT9OXX3g+usW7/b2z+h46I9ZiYyB07XMXg==}
- '@vue/language-core@3.2.2':
- resolution: {integrity: sha512-5DAuhxsxBN9kbriklh3Q5AMaJhyOCNiQJvCskN9/30XOpdLiqZU9Q+WvjArP17ubdGEyZtBzlIeG5nIjEbNOrQ==}
-
'@vue/language-core@3.2.3':
resolution: {integrity: sha512-VpN/GnYDzGLh44AI6i1OB/WsLXo6vwnl0EWHBelGc4TyC0yEq6azwNaed/+Tgr8anFlSdWYnMEkyHJDPe7ii7A==}
+ '@vue/language-core@3.2.4':
+ resolution: {integrity: sha512-bqBGuSG4KZM45KKTXzGtoCl9cWju5jsaBKaJJe3h5hRAAWpZUuj5G+L+eI01sPIkm4H6setKRlw7E85wLdDNew==}
+
'@vue/reactivity@3.5.27':
resolution: {integrity: sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==}
@@ -5179,9 +5173,6 @@ packages:
'@vue/shared@3.5.24':
resolution: {integrity: sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A==}
- '@vue/shared@3.5.26':
- resolution: {integrity: sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==}
-
'@vue/shared@3.5.27':
resolution: {integrity: sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==}
@@ -5510,8 +5501,8 @@ packages:
avvio@9.1.0:
resolution: {integrity: sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==}
- axios@1.13.2:
- resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==}
+ axios@1.13.4:
+ resolution: {integrity: sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==}
b4a@1.6.4:
resolution: {integrity: sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==}
@@ -5971,8 +5962,8 @@ packages:
resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==}
engines: {node: '>=12'}
- d3-graph-controller@3.1.6:
- resolution: {integrity: sha512-/y6nhkAIpt2jwVcGyIBGfhWnLj4Q7rtrmR5KHX89p6YDY+uziqgEVow8mHsNrYcg7ieU2OsCH+O3DORhCQLq8g==}
+ d3-graph-controller@3.1.7:
+ resolution: {integrity: sha512-gOOYQu+nlr+W1GJ77h/LMJUuLiyX+wx6v+h8spIASpbywWbmzcozZY95BF0B9KNdH/x2iViq98M9RehHRmog5w==}
d3-interpolate@3.0.1:
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
@@ -6609,8 +6600,8 @@ packages:
resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==}
hasBin: true
- fastify@5.7.1:
- resolution: {integrity: sha512-ZW7S4fxlZhE+tYWVokFzjh+i56R+buYKNGhrVl6DtN8sxkyMEzpJnzvO8A/ZZrsg5w6X37u6I4EOQikYS5DXpA==}
+ fastify@5.7.2:
+ resolution: {integrity: sha512-dBJolW+hm6N/yJVf6J5E1BxOBNkuXNl405nrfeR8SpvGWG3aCC2XDHyiFBdow8Win1kj7sjawQc257JlYY6M/A==}
fastq@1.17.1:
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
@@ -6712,10 +6703,6 @@ packages:
resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==}
engines: {node: '>=14'}
- form-data@4.0.4:
- resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
- engines: {node: '>= 6'}
-
form-data@4.0.5:
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
engines: {node: '>= 6'}
@@ -6932,8 +6919,8 @@ packages:
handle-thing@2.0.1:
resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==}
- happy-dom@20.3.7:
- resolution: {integrity: sha512-sb5IzoRl1WJKsUSRe+IloJf3z1iDq5PQ7Yk/ULMsZ5IAQEs9ZL7RsFfiKBXU7nK9QmO+iz0e59EH8r8jexTZ/g==}
+ happy-dom@20.4.0:
+ resolution: {integrity: sha512-RDeQm3dT9n0A5f/TszjUmNCLEuPnMGv3Tv4BmNINebz/h17PA6LMBcxJ5FrcqltNBMh9jA/8ufgDdBYUdBt+eg==}
engines: {node: '>=20.0.0'}
has-bigints@1.1.0:
@@ -8438,10 +8425,10 @@ packages:
rc9@2.1.2:
resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==}
- react-dom@19.2.3:
- resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==}
+ react-dom@19.2.4:
+ resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==}
peerDependencies:
- react: ^19.2.3
+ react: ^19.2.4
react-is@17.0.2:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
@@ -8449,8 +8436,8 @@ packages:
react-is@18.3.1:
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
- react-is@19.2.3:
- resolution: {integrity: sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==}
+ react-is@19.2.4:
+ resolution: {integrity: sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==}
react-refresh@0.18.0:
resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==}
@@ -8460,8 +8447,8 @@ packages:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'}
- react@19.2.3:
- resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==}
+ react@19.2.4:
+ resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
engines: {node: '>=0.10.0'}
readable-stream@2.3.7:
@@ -8622,17 +8609,17 @@ packages:
resolution: {integrity: sha512-d0UrqxYd8KyZ6i3M2Nx7WOMy708qsV/7fTHMHxCMCBOAe3V/U7OMPu5GkX8hC+cmkHhzGnfeYongl1IgiooddA==}
engines: {node: '>=16'}
peerDependencies:
- rollup: ^4.56.0
+ rollup: ^4.57.0
typescript: ^4.5 || ^5.0
rollup-plugin-license@3.6.0:
resolution: {integrity: sha512-1ieLxTCaigI5xokIfszVDRoy6c/Wmlot1fDEnea7Q/WXSR8AqOjYljHDLObAx7nFxHC2mbxT3QnTSPhaic2IYw==}
engines: {node: '>=14.0.0'}
peerDependencies:
- rollup: ^4.56.0
+ rollup: ^4.57.0
- rollup@4.56.0:
- resolution: {integrity: sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==}
+ rollup@4.57.0:
+ resolution: {integrity: sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
@@ -9696,8 +9683,8 @@ packages:
postcss:
optional: true
- vitest-browser-react@2.0.4:
- resolution: {integrity: sha512-FQq2z519Bwp/rANaQXU+ox7M4d0q/bTQkF2pgwRAehE+pqJ6myYOLp+P2Dy2kuk+K4IQJHMyijMCSQ1da/xW8w==}
+ vitest-browser-react@2.0.5:
+ resolution: {integrity: sha512-YODQX8mHTJCyKNVYTWJrLEYrUtw+QfLl78owgvuE7C5ydgmGBq6v5s4jK2w6wdPhIZsN9PpV1rQbmAevWJjO9g==}
peerDependencies:
'@types/react': ^18.0.0 || ^19.0.0
'@types/react-dom': ^18.0.0 || ^19.0.0
@@ -9769,8 +9756,8 @@ packages:
peerDependencies:
vue: ^3.5.0
- vue-tsc@3.2.3:
- resolution: {integrity: sha512-1RdRB7rQXGFMdpo0aXf9spVzWEPGAk7PEb/ejHQwVrcuQA/HsGiixIc3uBQeqY2YjeEEgvr2ShQewBgcN4c1Cw==}
+ vue-tsc@3.2.4:
+ resolution: {integrity: sha512-xj3YCvSLNDKt1iF9OcImWHhmYcihVu9p4b9s4PGR/qp6yhW+tZJaypGxHScRyOrdnHvaOeF+YkZOdKwbgGvp5g==}
hasBin: true
peerDependencies:
typescript: '>=5.0.0'
@@ -10206,7 +10193,7 @@ snapshots:
'@babel/helper-annotate-as-pure@7.27.3':
dependencies:
- '@babel/types': 7.28.5
+ '@babel/types': 7.28.6
'@babel/helper-compilation-targets@7.27.2':
dependencies:
@@ -10232,7 +10219,7 @@ snapshots:
'@babel/helper-optimise-call-expression': 7.27.1
'@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.6)
'@babel/helper-skip-transparent-expression-wrappers': 7.27.1
- '@babel/traverse': 7.28.5
+ '@babel/traverse': 7.28.6
semver: 6.3.1
transitivePeerDependencies:
- supports-color
@@ -10247,7 +10234,7 @@ snapshots:
'@babel/helper-define-polyfill-provider@0.6.5(@babel/core@7.28.6)':
dependencies:
'@babel/core': 7.28.6
- '@babel/helper-compilation-targets': 7.27.2
+ '@babel/helper-compilation-targets': 7.28.6
'@babel/helper-plugin-utils': 7.27.1
debug: 4.4.3
lodash.debounce: 4.0.8
@@ -10259,8 +10246,8 @@ snapshots:
'@babel/helper-member-expression-to-functions@7.28.5':
dependencies:
- '@babel/traverse': 7.28.5
- '@babel/types': 7.28.5
+ '@babel/traverse': 7.28.6
+ '@babel/types': 7.28.6
transitivePeerDependencies:
- supports-color
@@ -10287,15 +10274,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.6)':
- dependencies:
- '@babel/core': 7.28.6
- '@babel/helper-module-imports': 7.27.1
- '@babel/helper-validator-identifier': 7.28.5
- '@babel/traverse': 7.28.5
- transitivePeerDependencies:
- - supports-color
-
'@babel/helper-module-transforms@7.28.6(@babel/core@7.28.6)':
dependencies:
'@babel/core': 7.28.6
@@ -10307,7 +10285,7 @@ snapshots:
'@babel/helper-optimise-call-expression@7.27.1':
dependencies:
- '@babel/types': 7.28.5
+ '@babel/types': 7.28.6
'@babel/helper-plugin-utils@7.27.1': {}
@@ -10316,7 +10294,7 @@ snapshots:
'@babel/core': 7.28.6
'@babel/helper-annotate-as-pure': 7.27.3
'@babel/helper-wrap-function': 7.28.3
- '@babel/traverse': 7.28.5
+ '@babel/traverse': 7.28.6
transitivePeerDependencies:
- supports-color
@@ -10325,14 +10303,14 @@ snapshots:
'@babel/core': 7.28.6
'@babel/helper-member-expression-to-functions': 7.28.5
'@babel/helper-optimise-call-expression': 7.27.1
- '@babel/traverse': 7.28.5
+ '@babel/traverse': 7.28.6
transitivePeerDependencies:
- supports-color
'@babel/helper-skip-transparent-expression-wrappers@7.27.1':
dependencies:
- '@babel/traverse': 7.28.5
- '@babel/types': 7.28.5
+ '@babel/traverse': 7.28.6
+ '@babel/types': 7.28.6
transitivePeerDependencies:
- supports-color
@@ -10346,9 +10324,9 @@ snapshots:
'@babel/helper-wrap-function@7.28.3':
dependencies:
- '@babel/template': 7.27.2
- '@babel/traverse': 7.28.5
- '@babel/types': 7.28.5
+ '@babel/template': 7.28.6
+ '@babel/traverse': 7.28.6
+ '@babel/types': 7.28.6
transitivePeerDependencies:
- supports-color
@@ -10382,7 +10360,7 @@ snapshots:
dependencies:
'@babel/core': 7.28.6
'@babel/helper-plugin-utils': 7.27.1
- '@babel/traverse': 7.28.5
+ '@babel/traverse': 7.28.6
transitivePeerDependencies:
- supports-color
@@ -10409,7 +10387,7 @@ snapshots:
dependencies:
'@babel/core': 7.28.6
'@babel/helper-plugin-utils': 7.27.1
- '@babel/traverse': 7.28.5
+ '@babel/traverse': 7.28.6
transitivePeerDependencies:
- supports-color
@@ -10443,14 +10421,14 @@ snapshots:
'@babel/core': 7.28.6
'@babel/helper-plugin-utils': 7.27.1
'@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.6)
- '@babel/traverse': 7.28.5
+ '@babel/traverse': 7.28.6
transitivePeerDependencies:
- supports-color
'@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.28.6)':
dependencies:
'@babel/core': 7.28.6
- '@babel/helper-module-imports': 7.27.1
+ '@babel/helper-module-imports': 7.28.6
'@babel/helper-plugin-utils': 7.27.1
'@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.6)
transitivePeerDependencies:
@@ -10486,11 +10464,11 @@ snapshots:
dependencies:
'@babel/core': 7.28.6
'@babel/helper-annotate-as-pure': 7.27.3
- '@babel/helper-compilation-targets': 7.27.2
+ '@babel/helper-compilation-targets': 7.28.6
'@babel/helper-globals': 7.28.0
'@babel/helper-plugin-utils': 7.27.1
'@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.6)
- '@babel/traverse': 7.28.5
+ '@babel/traverse': 7.28.6
transitivePeerDependencies:
- supports-color
@@ -10498,13 +10476,13 @@ snapshots:
dependencies:
'@babel/core': 7.28.6
'@babel/helper-plugin-utils': 7.27.1
- '@babel/template': 7.27.2
+ '@babel/template': 7.28.6
'@babel/plugin-transform-destructuring@7.28.5(@babel/core@7.28.6)':
dependencies:
'@babel/core': 7.28.6
'@babel/helper-plugin-utils': 7.27.1
- '@babel/traverse': 7.28.5
+ '@babel/traverse': 7.28.6
transitivePeerDependencies:
- supports-color
@@ -10559,9 +10537,9 @@ snapshots:
'@babel/plugin-transform-function-name@7.27.1(@babel/core@7.28.6)':
dependencies:
'@babel/core': 7.28.6
- '@babel/helper-compilation-targets': 7.27.2
+ '@babel/helper-compilation-targets': 7.28.6
'@babel/helper-plugin-utils': 7.27.1
- '@babel/traverse': 7.28.5
+ '@babel/traverse': 7.28.6
transitivePeerDependencies:
- supports-color
@@ -10588,7 +10566,7 @@ snapshots:
'@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.28.6)':
dependencies:
'@babel/core': 7.28.6
- '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.6)
+ '@babel/helper-module-transforms': 7.28.6(@babel/core@7.28.6)
'@babel/helper-plugin-utils': 7.27.1
transitivePeerDependencies:
- supports-color
@@ -10596,7 +10574,7 @@ snapshots:
'@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.28.6)':
dependencies:
'@babel/core': 7.28.6
- '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.6)
+ '@babel/helper-module-transforms': 7.28.6(@babel/core@7.28.6)
'@babel/helper-plugin-utils': 7.27.1
transitivePeerDependencies:
- supports-color
@@ -10604,17 +10582,17 @@ snapshots:
'@babel/plugin-transform-modules-systemjs@7.28.5(@babel/core@7.28.6)':
dependencies:
'@babel/core': 7.28.6
- '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.6)
+ '@babel/helper-module-transforms': 7.28.6(@babel/core@7.28.6)
'@babel/helper-plugin-utils': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
- '@babel/traverse': 7.28.5
+ '@babel/traverse': 7.28.6
transitivePeerDependencies:
- supports-color
'@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.28.6)':
dependencies:
'@babel/core': 7.28.6
- '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.6)
+ '@babel/helper-module-transforms': 7.28.6(@babel/core@7.28.6)
'@babel/helper-plugin-utils': 7.27.1
transitivePeerDependencies:
- supports-color
@@ -10643,11 +10621,11 @@ snapshots:
'@babel/plugin-transform-object-rest-spread@7.28.4(@babel/core@7.28.6)':
dependencies:
'@babel/core': 7.28.6
- '@babel/helper-compilation-targets': 7.27.2
+ '@babel/helper-compilation-targets': 7.28.6
'@babel/helper-plugin-utils': 7.27.1
'@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.28.6)
'@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.6)
- '@babel/traverse': 7.28.5
+ '@babel/traverse': 7.28.6
transitivePeerDependencies:
- supports-color
@@ -10778,9 +10756,9 @@ snapshots:
'@babel/preset-env@7.28.5(@babel/core@7.28.6)':
dependencies:
- '@babel/compat-data': 7.28.5
+ '@babel/compat-data': 7.28.6
'@babel/core': 7.28.6
- '@babel/helper-compilation-targets': 7.27.2
+ '@babel/helper-compilation-targets': 7.28.6
'@babel/helper-plugin-utils': 7.27.1
'@babel/helper-validator-option': 7.27.1
'@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.28.5(@babel/core@7.28.6)
@@ -10856,7 +10834,7 @@ snapshots:
dependencies:
'@babel/core': 7.28.6
'@babel/helper-plugin-utils': 7.27.1
- '@babel/types': 7.28.5
+ '@babel/types': 7.28.6
esutils: 2.0.3
'@babel/runtime@7.26.0':
@@ -10925,7 +10903,7 @@ snapshots:
'@bcoe/v8-coverage@1.0.2': {}
- '@bomb.sh/tab@0.0.11(cac@6.7.14(patch_hash=a8f0f3517a47ce716ed90c0cfe6ae382ab763b021a664ada2a608477d0621588))(citty@0.1.6)':
+ '@bomb.sh/tab@0.0.12(cac@6.7.14(patch_hash=a8f0f3517a47ce716ed90c0cfe6ae382ab763b021a664ada2a608477d0621588))(citty@0.1.6)':
optionalDependencies:
cac: 6.7.14(patch_hash=a8f0f3517a47ce716ed90c0cfe6ae382ab763b021a664ada2a608477d0621588)
citty: 0.1.6
@@ -12396,20 +12374,20 @@ snapshots:
'@rolldown/pluginutils@1.0.0-beta.53': {}
- '@rollup/plugin-babel@5.3.1(@babel/core@7.28.6)(@types/babel__core@7.20.5)(rollup@4.56.0)':
+ '@rollup/plugin-babel@5.3.1(@babel/core@7.28.6)(@types/babel__core@7.20.5)(rollup@4.57.0)':
dependencies:
'@babel/core': 7.28.6
- '@babel/helper-module-imports': 7.27.1
- '@rollup/pluginutils': 3.1.0(rollup@4.56.0)
- rollup: 4.56.0
+ '@babel/helper-module-imports': 7.28.6
+ '@rollup/pluginutils': 3.1.0(rollup@4.57.0)
+ rollup: 4.57.0
optionalDependencies:
'@types/babel__core': 7.20.5
transitivePeerDependencies:
- supports-color
- '@rollup/plugin-commonjs@29.0.0(rollup@4.56.0)':
+ '@rollup/plugin-commonjs@29.0.0(rollup@4.57.0)':
dependencies:
- '@rollup/pluginutils': 5.3.0(rollup@4.56.0)
+ '@rollup/pluginutils': 5.3.0(rollup@4.57.0)
commondir: 1.0.1
estree-walker: 2.0.2
fdir: 6.5.0(picomatch@4.0.3)
@@ -12417,144 +12395,144 @@ snapshots:
magic-string: 0.30.21
picomatch: 4.0.3
optionalDependencies:
- rollup: 4.56.0
+ rollup: 4.57.0
- '@rollup/plugin-json@6.1.0(rollup@4.56.0)':
+ '@rollup/plugin-json@6.1.0(rollup@4.57.0)':
dependencies:
- '@rollup/pluginutils': 5.1.4(rollup@4.56.0)
+ '@rollup/pluginutils': 5.1.4(rollup@4.57.0)
optionalDependencies:
- rollup: 4.56.0
+ rollup: 4.57.0
- '@rollup/plugin-node-resolve@15.3.1(rollup@4.56.0)':
+ '@rollup/plugin-node-resolve@15.3.1(rollup@4.57.0)':
dependencies:
- '@rollup/pluginutils': 5.3.0(rollup@4.56.0)
+ '@rollup/pluginutils': 5.3.0(rollup@4.57.0)
'@types/resolve': 1.20.2
deepmerge: 4.3.1
is-module: 1.0.0
resolve: 1.22.11
optionalDependencies:
- rollup: 4.56.0
+ rollup: 4.57.0
- '@rollup/plugin-node-resolve@16.0.3(rollup@4.56.0)':
+ '@rollup/plugin-node-resolve@16.0.3(rollup@4.57.0)':
dependencies:
- '@rollup/pluginutils': 5.3.0(rollup@4.56.0)
+ '@rollup/pluginutils': 5.3.0(rollup@4.57.0)
'@types/resolve': 1.20.2
deepmerge: 4.3.1
is-module: 1.0.0
resolve: 1.22.10
optionalDependencies:
- rollup: 4.56.0
+ rollup: 4.57.0
- '@rollup/plugin-replace@2.4.2(rollup@4.56.0)':
+ '@rollup/plugin-replace@2.4.2(rollup@4.57.0)':
dependencies:
- '@rollup/pluginutils': 3.1.0(rollup@4.56.0)
+ '@rollup/pluginutils': 3.1.0(rollup@4.57.0)
magic-string: 0.25.9
- rollup: 4.56.0
+ rollup: 4.57.0
- '@rollup/plugin-terser@0.4.4(rollup@4.56.0)':
+ '@rollup/plugin-terser@0.4.4(rollup@4.57.0)':
dependencies:
serialize-javascript: 6.0.2
smob: 1.5.0
terser: 5.44.1
optionalDependencies:
- rollup: 4.56.0
+ rollup: 4.57.0
- '@rollup/pluginutils@3.1.0(rollup@4.56.0)':
+ '@rollup/pluginutils@3.1.0(rollup@4.57.0)':
dependencies:
'@types/estree': 0.0.39
estree-walker: 1.0.1
picomatch: 2.3.1
- rollup: 4.56.0
+ rollup: 4.57.0
- '@rollup/pluginutils@5.1.4(rollup@4.56.0)':
+ '@rollup/pluginutils@5.1.4(rollup@4.57.0)':
dependencies:
'@types/estree': 1.0.8
estree-walker: 2.0.2
picomatch: 4.0.3
optionalDependencies:
- rollup: 4.56.0
+ rollup: 4.57.0
- '@rollup/pluginutils@5.3.0(rollup@4.56.0)':
+ '@rollup/pluginutils@5.3.0(rollup@4.57.0)':
dependencies:
'@types/estree': 1.0.8
estree-walker: 2.0.2
picomatch: 4.0.3
optionalDependencies:
- rollup: 4.56.0
+ rollup: 4.57.0
- '@rollup/rollup-android-arm-eabi@4.56.0':
+ '@rollup/rollup-android-arm-eabi@4.57.0':
optional: true
- '@rollup/rollup-android-arm64@4.56.0':
+ '@rollup/rollup-android-arm64@4.57.0':
optional: true
- '@rollup/rollup-darwin-arm64@4.56.0':
+ '@rollup/rollup-darwin-arm64@4.57.0':
optional: true
- '@rollup/rollup-darwin-x64@4.56.0':
+ '@rollup/rollup-darwin-x64@4.57.0':
optional: true
- '@rollup/rollup-freebsd-arm64@4.56.0':
+ '@rollup/rollup-freebsd-arm64@4.57.0':
optional: true
- '@rollup/rollup-freebsd-x64@4.56.0':
+ '@rollup/rollup-freebsd-x64@4.57.0':
optional: true
- '@rollup/rollup-linux-arm-gnueabihf@4.56.0':
+ '@rollup/rollup-linux-arm-gnueabihf@4.57.0':
optional: true
- '@rollup/rollup-linux-arm-musleabihf@4.56.0':
+ '@rollup/rollup-linux-arm-musleabihf@4.57.0':
optional: true
- '@rollup/rollup-linux-arm64-gnu@4.56.0':
+ '@rollup/rollup-linux-arm64-gnu@4.57.0':
optional: true
- '@rollup/rollup-linux-arm64-musl@4.56.0':
+ '@rollup/rollup-linux-arm64-musl@4.57.0':
optional: true
- '@rollup/rollup-linux-loong64-gnu@4.56.0':
+ '@rollup/rollup-linux-loong64-gnu@4.57.0':
optional: true
- '@rollup/rollup-linux-loong64-musl@4.56.0':
+ '@rollup/rollup-linux-loong64-musl@4.57.0':
optional: true
- '@rollup/rollup-linux-ppc64-gnu@4.56.0':
+ '@rollup/rollup-linux-ppc64-gnu@4.57.0':
optional: true
- '@rollup/rollup-linux-ppc64-musl@4.56.0':
+ '@rollup/rollup-linux-ppc64-musl@4.57.0':
optional: true
- '@rollup/rollup-linux-riscv64-gnu@4.56.0':
+ '@rollup/rollup-linux-riscv64-gnu@4.57.0':
optional: true
- '@rollup/rollup-linux-riscv64-musl@4.56.0':
+ '@rollup/rollup-linux-riscv64-musl@4.57.0':
optional: true
- '@rollup/rollup-linux-s390x-gnu@4.56.0':
+ '@rollup/rollup-linux-s390x-gnu@4.57.0':
optional: true
- '@rollup/rollup-linux-x64-gnu@4.56.0':
+ '@rollup/rollup-linux-x64-gnu@4.57.0':
optional: true
- '@rollup/rollup-linux-x64-musl@4.56.0':
+ '@rollup/rollup-linux-x64-musl@4.57.0':
optional: true
- '@rollup/rollup-openbsd-x64@4.56.0':
+ '@rollup/rollup-openbsd-x64@4.57.0':
optional: true
- '@rollup/rollup-openharmony-arm64@4.56.0':
+ '@rollup/rollup-openharmony-arm64@4.57.0':
optional: true
- '@rollup/rollup-win32-arm64-msvc@4.56.0':
+ '@rollup/rollup-win32-arm64-msvc@4.57.0':
optional: true
- '@rollup/rollup-win32-ia32-msvc@4.56.0':
+ '@rollup/rollup-win32-ia32-msvc@4.57.0':
optional: true
- '@rollup/rollup-win32-x64-gnu@4.56.0':
+ '@rollup/rollup-win32-x64-gnu@4.57.0':
optional: true
- '@rollup/rollup-win32-x64-msvc@4.56.0':
+ '@rollup/rollup-win32-x64-msvc@4.57.0':
optional: true
'@sec-ant/readable-stream@0.4.1': {}
@@ -12847,14 +12825,14 @@ snapshots:
picocolors: 1.1.1
redent: 3.0.0
- '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.2.14)(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
+ '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.2.14)(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@babel/runtime': 7.28.4
'@testing-library/dom': 10.4.1
- react: 19.2.3
- react-dom: 19.2.3(react@19.2.3)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
optionalDependencies:
- '@types/react': 19.2.9
+ '@types/react': 19.2.10
'@types/react-dom': 18.2.14
'@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)':
@@ -12987,14 +12965,14 @@ snapshots:
'@types/react-dom@18.2.14':
dependencies:
- '@types/react': 19.2.9
+ '@types/react': 19.2.10
optional: true
'@types/react-is@19.2.0':
dependencies:
- '@types/react': 19.2.9
+ '@types/react': 19.2.10
- '@types/react@19.2.9':
+ '@types/react@19.2.10':
dependencies:
csstype: 3.2.3
@@ -13358,22 +13336,22 @@ snapshots:
'@vitest/test-fn@file:test/core/deps/dep-fn': {}
- '@voidzero-dev/vitepress-theme@4.3.0(axios@1.13.2)(change-case@5.4.4)(focus-trap@7.6.6)(typescript@5.9.3)(vite@7.1.5(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitepress@2.0.0-alpha.15(@types/node@24.10.9)(axios@1.13.2)(change-case@5.4.4)(jiti@2.6.1)(lightningcss@1.30.2)(oxc-minify@0.108.0)(postcss@8.5.6)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3))':
+ '@voidzero-dev/vitepress-theme@4.4.1(axios@1.13.4)(change-case@5.4.4)(focus-trap@7.6.6)(typescript@5.9.3)(vite@7.1.5(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitepress@2.0.0-alpha.15(@types/node@24.10.9)(axios@1.13.4)(change-case@5.4.4)(jiti@2.6.1)(lightningcss@1.30.2)(oxc-minify@0.108.0)(postcss@8.5.6)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3))':
dependencies:
'@docsearch/css': 4.3.2
'@docsearch/js': 4.3.2
'@rive-app/canvas-lite': 2.33.3
'@tailwindcss/typography': 0.5.19(tailwindcss@4.1.18)
'@tailwindcss/vite': 4.1.18(vite@7.1.5(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
- '@vue/shared': 3.5.26
+ '@vue/shared': 3.5.27
'@vueuse/core': 14.1.0(vue@3.5.27(typescript@5.9.3))
- '@vueuse/integrations': 14.1.0(axios@1.13.2)(change-case@5.4.4)(focus-trap@7.6.6)(vue@3.5.27(typescript@5.9.3))
+ '@vueuse/integrations': 14.1.0(axios@1.13.4)(change-case@5.4.4)(focus-trap@7.6.6)(vue@3.5.27(typescript@5.9.3))
'@vueuse/shared': 14.1.0(vue@3.5.27(typescript@5.9.3))
mark.js: 8.11.1
minisearch: 7.2.0
reka-ui: 2.7.0(typescript@5.9.3)(vue@3.5.27(typescript@5.9.3))
tailwindcss: 4.1.18
- vitepress: 2.0.0-alpha.15(@types/node@24.10.9)(axios@1.13.2)(change-case@5.4.4)(jiti@2.6.1)(lightningcss@1.30.2)(oxc-minify@0.108.0)(postcss@8.5.6)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
+ vitepress: 2.0.0-alpha.15(@types/node@24.10.9)(axios@1.13.4)(change-case@5.4.4)(jiti@2.6.1)(lightningcss@1.30.2)(oxc-minify@0.108.0)(postcss@8.5.6)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
vue: 3.5.27(typescript@5.9.3)
transitivePeerDependencies:
- '@vue/composition-api'
@@ -13404,14 +13382,6 @@ snapshots:
path-browserify: 1.0.1
vscode-uri: 3.1.0
- '@vue/compiler-core@3.5.26':
- dependencies:
- '@babel/parser': 7.28.5
- '@vue/shared': 3.5.26
- entities: 7.0.0
- estree-walker: 2.0.2
- source-map-js: 1.2.1
-
'@vue/compiler-core@3.5.27':
dependencies:
'@babel/parser': 7.28.5
@@ -13420,11 +13390,6 @@ snapshots:
estree-walker: 2.0.2
source-map-js: 1.2.1
- '@vue/compiler-dom@3.5.26':
- dependencies:
- '@vue/compiler-core': 3.5.26
- '@vue/shared': 3.5.26
-
'@vue/compiler-dom@3.5.27':
dependencies:
'@vue/compiler-core': 3.5.27
@@ -13467,21 +13432,21 @@ snapshots:
dependencies:
rfdc: 1.4.1
- '@vue/language-core@3.2.2':
+ '@vue/language-core@3.2.3':
dependencies:
'@volar/language-core': 2.4.27
- '@vue/compiler-dom': 3.5.26
- '@vue/shared': 3.5.26
+ '@vue/compiler-dom': 3.5.27
+ '@vue/shared': 3.5.27
alien-signals: 3.0.3
muggle-string: 0.4.1
path-browserify: 1.0.1
picomatch: 4.0.3
- '@vue/language-core@3.2.3':
+ '@vue/language-core@3.2.4':
dependencies:
'@volar/language-core': 2.4.27
- '@vue/compiler-dom': 3.5.26
- '@vue/shared': 3.5.26
+ '@vue/compiler-dom': 3.5.27
+ '@vue/shared': 3.5.27
alien-signals: 3.0.3
muggle-string: 0.4.1
path-browserify: 1.0.1
@@ -13511,8 +13476,6 @@ snapshots:
'@vue/shared@3.5.24': {}
- '@vue/shared@3.5.26': {}
-
'@vue/shared@3.5.27': {}
'@vue/test-utils@2.4.6':
@@ -13543,23 +13506,23 @@ snapshots:
'@vueuse/shared': 14.1.0(vue@3.5.27(typescript@5.9.3))
vue: 3.5.27(typescript@5.9.3)
- '@vueuse/integrations@14.0.0(axios@1.13.2)(change-case@5.4.4)(focus-trap@7.6.6)(vue@3.5.27(typescript@5.9.3))':
+ '@vueuse/integrations@14.0.0(axios@1.13.4)(change-case@5.4.4)(focus-trap@7.6.6)(vue@3.5.27(typescript@5.9.3))':
dependencies:
'@vueuse/core': 14.0.0(vue@3.5.27(typescript@5.9.3))
'@vueuse/shared': 14.0.0(vue@3.5.27(typescript@5.9.3))
vue: 3.5.27(typescript@5.9.3)
optionalDependencies:
- axios: 1.13.2
+ axios: 1.13.4
change-case: 5.4.4
focus-trap: 7.6.6
- '@vueuse/integrations@14.1.0(axios@1.13.2)(change-case@5.4.4)(focus-trap@7.6.6)(vue@3.5.27(typescript@5.9.3))':
+ '@vueuse/integrations@14.1.0(axios@1.13.4)(change-case@5.4.4)(focus-trap@7.6.6)(vue@3.5.27(typescript@5.9.3))':
dependencies:
'@vueuse/core': 14.1.0(vue@3.5.27(typescript@5.9.3))
'@vueuse/shared': 14.1.0(vue@3.5.27(typescript@5.9.3))
vue: 3.5.27(typescript@5.9.3)
optionalDependencies:
- axios: 1.13.2
+ axios: 1.13.4
change-case: 5.4.4
focus-trap: 7.6.6
@@ -13802,10 +13765,10 @@ snapshots:
'@fastify/error': 4.2.0
fastq: 1.17.1
- axios@1.13.2:
+ axios@1.13.4:
dependencies:
follow-redirects: 1.15.11
- form-data: 4.0.4
+ form-data: 4.0.5
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
@@ -13814,7 +13777,7 @@ snapshots:
babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.6):
dependencies:
- '@babel/compat-data': 7.28.5
+ '@babel/compat-data': 7.28.6
'@babel/core': 7.28.6
'@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.6)
semver: 6.3.1
@@ -14327,7 +14290,7 @@ snapshots:
d3-quadtree: 3.0.1
d3-timer: 3.0.1
- d3-graph-controller@3.1.6:
+ d3-graph-controller@3.1.7:
dependencies:
'@yeger/debounce': 2.0.17
d3-drag: 3.0.0
@@ -15197,7 +15160,7 @@ snapshots:
dependencies:
strnum: 2.1.1
- fastify@5.7.1:
+ fastify@5.7.2:
dependencies:
'@fastify/ajv-compiler': 4.0.5
'@fastify/error': 4.2.0
@@ -15311,14 +15274,6 @@ snapshots:
cross-spawn: 7.0.6
signal-exit: 4.1.0
- form-data@4.0.4:
- dependencies:
- asynckit: 0.4.0
- combined-stream: 1.0.8
- es-set-tostringtag: 2.1.0
- hasown: 2.0.2
- mime-types: 2.1.35
-
form-data@4.0.5:
dependencies:
asynckit: 0.4.0
@@ -15553,7 +15508,7 @@ snapshots:
handle-thing@2.0.1: {}
- happy-dom@20.3.7:
+ happy-dom@20.4.0:
dependencies:
'@types/node': 24.10.9
'@types/whatwg-mimetype': 3.0.2
@@ -16246,8 +16201,8 @@ snapshots:
magicast@0.3.5:
dependencies:
- '@babel/parser': 7.28.5
- '@babel/types': 7.28.5
+ '@babel/parser': 7.28.6
+ '@babel/types': 7.28.6
source-map-js: 1.2.1
optional: true
@@ -17308,16 +17263,16 @@ snapshots:
defu: 6.1.4
destr: 2.0.5
- react-dom@19.2.3(react@19.2.3):
+ react-dom@19.2.4(react@19.2.4):
dependencies:
- react: 19.2.3
+ react: 19.2.4
scheduler: 0.27.0
react-is@17.0.2: {}
react-is@18.3.1: {}
- react-is@19.2.3: {}
+ react-is@19.2.4: {}
react-refresh@0.18.0: {}
@@ -17325,7 +17280,7 @@ snapshots:
dependencies:
loose-envify: 1.4.0
- react@19.2.3: {}
+ react@19.2.4: {}
readable-stream@2.3.7:
dependencies:
@@ -17523,15 +17478,15 @@ snapshots:
rgb2hex@0.2.5: {}
- rollup-plugin-dts@6.3.0(rollup@4.56.0)(typescript@5.9.3):
+ rollup-plugin-dts@6.3.0(rollup@4.57.0)(typescript@5.9.3):
dependencies:
magic-string: 0.30.21
- rollup: 4.56.0
+ rollup: 4.57.0
typescript: 5.9.3
optionalDependencies:
'@babel/code-frame': 7.27.1
- rollup-plugin-license@3.6.0(picomatch@4.0.3)(rollup@4.56.0):
+ rollup-plugin-license@3.6.0(picomatch@4.0.3)(rollup@4.57.0):
dependencies:
commenting: 1.1.0
fdir: 6.4.4(picomatch@4.0.3)
@@ -17539,41 +17494,41 @@ snapshots:
magic-string: 0.30.21
moment: 2.30.1
package-name-regex: 2.0.6
- rollup: 4.56.0
+ rollup: 4.57.0
spdx-expression-validate: 2.0.0
spdx-satisfies: 5.0.1
transitivePeerDependencies:
- picomatch
- rollup@4.56.0:
+ rollup@4.57.0:
dependencies:
'@types/estree': 1.0.8
optionalDependencies:
- '@rollup/rollup-android-arm-eabi': 4.56.0
- '@rollup/rollup-android-arm64': 4.56.0
- '@rollup/rollup-darwin-arm64': 4.56.0
- '@rollup/rollup-darwin-x64': 4.56.0
- '@rollup/rollup-freebsd-arm64': 4.56.0
- '@rollup/rollup-freebsd-x64': 4.56.0
- '@rollup/rollup-linux-arm-gnueabihf': 4.56.0
- '@rollup/rollup-linux-arm-musleabihf': 4.56.0
- '@rollup/rollup-linux-arm64-gnu': 4.56.0
- '@rollup/rollup-linux-arm64-musl': 4.56.0
- '@rollup/rollup-linux-loong64-gnu': 4.56.0
- '@rollup/rollup-linux-loong64-musl': 4.56.0
- '@rollup/rollup-linux-ppc64-gnu': 4.56.0
- '@rollup/rollup-linux-ppc64-musl': 4.56.0
- '@rollup/rollup-linux-riscv64-gnu': 4.56.0
- '@rollup/rollup-linux-riscv64-musl': 4.56.0
- '@rollup/rollup-linux-s390x-gnu': 4.56.0
- '@rollup/rollup-linux-x64-gnu': 4.56.0
- '@rollup/rollup-linux-x64-musl': 4.56.0
- '@rollup/rollup-openbsd-x64': 4.56.0
- '@rollup/rollup-openharmony-arm64': 4.56.0
- '@rollup/rollup-win32-arm64-msvc': 4.56.0
- '@rollup/rollup-win32-ia32-msvc': 4.56.0
- '@rollup/rollup-win32-x64-gnu': 4.56.0
- '@rollup/rollup-win32-x64-msvc': 4.56.0
+ '@rollup/rollup-android-arm-eabi': 4.57.0
+ '@rollup/rollup-android-arm64': 4.57.0
+ '@rollup/rollup-darwin-arm64': 4.57.0
+ '@rollup/rollup-darwin-x64': 4.57.0
+ '@rollup/rollup-freebsd-arm64': 4.57.0
+ '@rollup/rollup-freebsd-x64': 4.57.0
+ '@rollup/rollup-linux-arm-gnueabihf': 4.57.0
+ '@rollup/rollup-linux-arm-musleabihf': 4.57.0
+ '@rollup/rollup-linux-arm64-gnu': 4.57.0
+ '@rollup/rollup-linux-arm64-musl': 4.57.0
+ '@rollup/rollup-linux-loong64-gnu': 4.57.0
+ '@rollup/rollup-linux-loong64-musl': 4.57.0
+ '@rollup/rollup-linux-ppc64-gnu': 4.57.0
+ '@rollup/rollup-linux-ppc64-musl': 4.57.0
+ '@rollup/rollup-linux-riscv64-gnu': 4.57.0
+ '@rollup/rollup-linux-riscv64-musl': 4.57.0
+ '@rollup/rollup-linux-s390x-gnu': 4.57.0
+ '@rollup/rollup-linux-x64-gnu': 4.57.0
+ '@rollup/rollup-linux-x64-musl': 4.57.0
+ '@rollup/rollup-openbsd-x64': 4.57.0
+ '@rollup/rollup-openharmony-arm64': 4.57.0
+ '@rollup/rollup-win32-arm64-msvc': 4.57.0
+ '@rollup/rollup-win32-ia32-msvc': 4.57.0
+ '@rollup/rollup-win32-x64-gnu': 4.57.0
+ '@rollup/rollup-win32-x64-msvc': 4.57.0
fsevents: 2.3.3
run-applescript@7.0.0: {}
@@ -18354,7 +18309,7 @@ snapshots:
twoslash-vue@0.3.6(typescript@5.9.3):
dependencies:
- '@vue/language-core': 3.2.2
+ '@vue/language-core': 3.2.3
twoslash: 0.3.6(typescript@5.9.3)
twoslash-protocol: 0.3.6
typescript: 5.9.3
@@ -18569,9 +18524,9 @@ snapshots:
oxc-transform: 0.108.0
unplugin: 2.3.11
- unplugin-swc@1.5.9(@swc/core@1.4.1(@swc/helpers@0.5.17))(rollup@4.56.0):
+ unplugin-swc@1.5.9(@swc/core@1.4.1(@swc/helpers@0.5.17))(rollup@4.57.0):
dependencies:
- '@rollup/pluginutils': 5.3.0(rollup@4.56.0)
+ '@rollup/pluginutils': 5.3.0(rollup@4.57.0)
'@swc/core': 1.4.1(@swc/helpers@0.5.17)
load-tsconfig: 0.2.5
unplugin: 2.3.11
@@ -18672,7 +18627,7 @@ snapshots:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
postcss: 8.5.6
- rollup: 4.56.0
+ rollup: 4.57.0
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 24.10.9
@@ -18712,12 +18667,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
- vitepress-plugin-tabs@0.7.3(vitepress@2.0.0-alpha.15(@types/node@24.10.9)(axios@1.13.2)(change-case@5.4.4)(jiti@2.6.1)(lightningcss@1.30.2)(oxc-minify@0.108.0)(postcss@8.5.6)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3)):
+ vitepress-plugin-tabs@0.7.3(vitepress@2.0.0-alpha.15(@types/node@24.10.9)(axios@1.13.4)(change-case@5.4.4)(jiti@2.6.1)(lightningcss@1.30.2)(oxc-minify@0.108.0)(postcss@8.5.6)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3)):
dependencies:
- vitepress: 2.0.0-alpha.15(@types/node@24.10.9)(axios@1.13.2)(change-case@5.4.4)(jiti@2.6.1)(lightningcss@1.30.2)(oxc-minify@0.108.0)(postcss@8.5.6)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
+ vitepress: 2.0.0-alpha.15(@types/node@24.10.9)(axios@1.13.4)(change-case@5.4.4)(jiti@2.6.1)(lightningcss@1.30.2)(oxc-minify@0.108.0)(postcss@8.5.6)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
vue: 3.5.27(typescript@5.9.3)
- vitepress@2.0.0-alpha.15(@types/node@24.10.9)(axios@1.13.2)(change-case@5.4.4)(jiti@2.6.1)(lightningcss@1.30.2)(oxc-minify@0.108.0)(postcss@8.5.6)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2):
+ vitepress@2.0.0-alpha.15(@types/node@24.10.9)(axios@1.13.4)(change-case@5.4.4)(jiti@2.6.1)(lightningcss@1.30.2)(oxc-minify@0.108.0)(postcss@8.5.6)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2):
dependencies:
'@docsearch/css': 4.3.2
'@docsearch/js': 4.3.2
@@ -18730,7 +18685,7 @@ snapshots:
'@vue/devtools-api': 8.0.5
'@vue/shared': 3.5.24
'@vueuse/core': 14.1.0(vue@3.5.27(typescript@5.9.3))
- '@vueuse/integrations': 14.0.0(axios@1.13.2)(change-case@5.4.4)(focus-trap@7.6.6)(vue@3.5.27(typescript@5.9.3))
+ '@vueuse/integrations': 14.0.0(axios@1.13.4)(change-case@5.4.4)(focus-trap@7.6.6)(vue@3.5.27(typescript@5.9.3))
focus-trap: 7.6.6
mark.js: 8.11.1
minisearch: 7.2.0
@@ -18765,13 +18720,13 @@ snapshots:
- universal-cookie
- yaml
- vitest-browser-react@2.0.4(@types/react-dom@18.2.14)(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@packages+vitest):
+ vitest-browser-react@2.0.5(@types/react-dom@18.2.14)(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@packages+vitest):
dependencies:
- react: 19.2.3
- react-dom: 19.2.3(react@19.2.3)
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
vitest: link:packages/vitest
optionalDependencies:
- '@types/react': 19.2.9
+ '@types/react': 19.2.10
'@types/react-dom': 18.2.14
vitest-browser-vue@2.0.2(vitest@packages+vitest)(vue@3.5.27(typescript@5.9.3)):
@@ -18826,10 +18781,10 @@ snapshots:
'@vue/devtools-api': 6.6.4
vue: 3.5.27(typescript@5.9.3)
- vue-tsc@3.2.3(typescript@5.9.3):
+ vue-tsc@3.2.4(typescript@5.9.3):
dependencies:
'@volar/typescript': 2.4.27
- '@vue/language-core': 3.2.3
+ '@vue/language-core': 3.2.4
typescript: 5.9.3
vue-virtual-scroller@2.0.0-beta.8(vue@3.5.27(typescript@5.9.3)):
@@ -19011,10 +18966,10 @@ snapshots:
'@babel/core': 7.28.6
'@babel/preset-env': 7.28.5(@babel/core@7.28.6)
'@babel/runtime': 7.28.4
- '@rollup/plugin-babel': 5.3.1(@babel/core@7.28.6)(@types/babel__core@7.20.5)(rollup@4.56.0)
- '@rollup/plugin-node-resolve': 15.3.1(rollup@4.56.0)
- '@rollup/plugin-replace': 2.4.2(rollup@4.56.0)
- '@rollup/plugin-terser': 0.4.4(rollup@4.56.0)
+ '@rollup/plugin-babel': 5.3.1(@babel/core@7.28.6)(@types/babel__core@7.20.5)(rollup@4.57.0)
+ '@rollup/plugin-node-resolve': 15.3.1(rollup@4.57.0)
+ '@rollup/plugin-replace': 2.4.2(rollup@4.57.0)
+ '@rollup/plugin-terser': 0.4.4(rollup@4.57.0)
'@surma/rollup-plugin-off-main-thread': 2.2.3
ajv: 8.17.1
common-tags: 1.8.2
@@ -19023,7 +18978,7 @@ snapshots:
glob: 7.2.3
lodash: 4.17.21
pretty-bytes: 5.6.0
- rollup: 4.56.0
+ rollup: 4.57.0
source-map: 0.8.0-beta.0
stringify-object: 3.3.0
strip-comments: 2.0.1
diff --git a/test/browser/package.json b/test/browser/package.json
index 8ac2028cbf33..afc300668146 100644
--- a/test/browser/package.json
+++ b/test/browser/package.json
@@ -29,7 +29,7 @@
"test:browser:playwright:html": "PROVIDER=playwright vitest --reporter=html"
},
"devDependencies": {
- "@types/react": "^19.2.9",
+ "@types/react": "^19.2.10",
"@vitejs/plugin-basic-ssl": "^2.1.4",
"@vitest/browser": "workspace:*",
"@vitest/browser-playwright": "workspace:*",
@@ -38,11 +38,11 @@
"@vitest/bundled-lib": "link:./bundled-lib",
"@vitest/cjs-lib": "link:./cjs-lib",
"playwright": "^1.58.0",
- "react": "^19.2.3",
- "react-dom": "^19.2.3",
+ "react": "^19.2.4",
+ "react-dom": "^19.2.4",
"url": "^0.11.4",
"vitest": "workspace:*",
- "vitest-browser-react": "^2.0.4",
+ "vitest-browser-react": "^2.0.5",
"ws": "catalog:"
}
}
diff --git a/test/browser/specs/errors.test.ts b/test/browser/specs/errors.test.ts
index e43fe55a8e84..baa85179a8e8 100644
--- a/test/browser/specs/errors.test.ts
+++ b/test/browser/specs/errors.test.ts
@@ -66,3 +66,73 @@ test('throws an error if test reloads the iframe during a test run', async () =>
`The iframe for "${fs.resolveFile('./iframe-reload.test.ts')}" was reloaded during a test.`,
)
})
+
+test('cannot use fs commands if write is disabled', async () => {
+ const { stderr, fs } = await runInlineBrowserTests({
+ 'fs-commands.test.ts': `
+ import { test, expect, recordArtifact } from 'vitest'
+ import { commands } from 'vitest/browser'
+
+ test.describe('fs security', () => {
+ test('fs writeFile throws an error', async () => {
+ await commands.writeFile('/test-file.txt', 'Hello World')
+ })
+
+ test('fs removeFile throws an error', async () => {
+ await commands.removeFile('/test-file.txt')
+ })
+
+ test('doesnt write attachment to disk', async ({ annotate }) => {
+ await annotate('test-attachment', { data: 'Test Attachment', path: '/test-attachment.txt' })
+ })
+
+ test('cannot record attachments inside artifact', async ({ task }) => {
+ await recordArtifact(task, {
+ attachments: [{ data: 'Artifact Attachment', path: '/artifact-attachment.txt' }],
+ type: 'my-custom',
+ })
+ })
+
+ test('snapshot saves are not saved', () => {
+ expect('snapshot content').toMatchSnapshot()
+ })
+ })
+ `,
+ './__snapshots__/basic.test.js.snap': `// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html`,
+ 'basic.test.js': `
+ import { test } from 'vitest'
+
+ test('basic test', () => {
+ expect(1 + 1).toBe(2)
+ })
+ `,
+ }, {
+ browser: {
+ api: {
+ allowExec: false,
+ allowWrite: false,
+ },
+ },
+ $cliOptions: {
+ update: true,
+ },
+ })
+
+ const errors = stderr.split('\n').filter(line => line.includes('Cannot modify file "/test-file.txt".'))
+ expect(errors).toHaveLength(2 * instances.length)
+
+ expect(stderr).toContain(
+ `Cannot save snapshot file "${fs.resolveFile('./__snapshots__/fs-commands.test.ts.snap')}". File writing is disabled because server is exposed to the internet`,
+ )
+ expect(stderr).toContain(
+ `Cannot remove snapshot file "${fs.resolveFile('./__snapshots__/basic.test.js.snap')}". File writing is disabled because server is exposed to the internet`,
+ )
+
+ // we don't throw an error if cannot write attachment, just warn
+ expect(stderr).toContain(
+ 'Cannot record annotation attachment because file writing is disabled',
+ )
+ expect(stderr).toContain(
+ 'Cannot record attachments ("/artifact-attachment.txt") because file writing is disabled, removing attachments from artifact "my-custom".',
+ )
+})
diff --git a/test/cli/fixtures/basic/basic.test.ts b/test/cli/fixtures/basic/basic.test.ts
new file mode 100644
index 000000000000..eb6b51eae9cb
--- /dev/null
+++ b/test/cli/fixtures/basic/basic.test.ts
@@ -0,0 +1,5 @@
+import { expect, test } from 'vitest';
+
+test('basic test', () => {
+ expect(1 + 1).toBe(2)
+})
diff --git a/test/cli/fixtures/fails/exports-error.test.js b/test/cli/fixtures/fails/exports-error.test.js
new file mode 100644
index 000000000000..6a0217bd94a4
--- /dev/null
+++ b/test/cli/fixtures/fails/exports-error.test.js
@@ -0,0 +1,6 @@
+import { test, vi } from 'vitest'
+
+test('__vite_ssr_import__ is removed in error', () => {
+ // vi is not a function
+ vi()
+})
diff --git a/test/cli/test/__snapshots__/fails.test.ts.snap b/test/cli/test/__snapshots__/fails.test.ts.snap
index fa1a7e84a47e..6afab023cb1f 100644
--- a/test/cli/test/__snapshots__/fails.test.ts.snap
+++ b/test/cli/test/__snapshots__/fails.test.ts.snap
@@ -27,6 +27,8 @@ exports[`should fail expect-soft.test.ts 1`] = `"Error: expect.soft() can only b
exports[`should fail expect-unreachable.test.ts 1`] = `"AssertionError: expected "hi" not to be reached"`;
+exports[`should fail exports-error.test.js 1`] = `"TypeError: vi is not a function"`;
+
exports[`should fail hook-timeout.test.ts 1`] = `"Error: Hook timed out in 10ms."`;
exports[`should fail hooks-called.test.ts 1`] = `
diff --git a/test/cli/test/config/browser-configs.test.ts b/test/cli/test/config/browser-configs.test.ts
index 44504787f4c1..f875818a6805 100644
--- a/test/cli/test/config/browser-configs.test.ts
+++ b/test/cli/test/config/browser-configs.test.ts
@@ -1061,3 +1061,25 @@ test('allows custom transformIndexHtml without custom html file', async () => {
expect(stdout).toContain('✓ |chromium| browser-custom.test.ts')
expect(exitCode).toBe(0)
})
+
+test('show a warning if host is exposed', async () => {
+ const { stderr } = await runVitest({
+ config: false,
+ root: './fixtures/basic',
+ reporters: [
+ {
+ onInit() {
+ throw new Error('stop')
+ },
+ },
+ ],
+ browser: {
+ api: {
+ host: 'custom-host',
+ },
+ },
+ })
+ expect(stderr).toContain(
+ 'API server is exposed to network, disabling write and exec operations by default for security reasons. This can cause some APIs to not work as expected. Set `browser.api.allowExec` manually to hide this warning. See https://vitest.dev/config/browser/api for more details.',
+ )
+})
diff --git a/test/cli/test/fails.test.ts b/test/cli/test/fails.test.ts
index b36cf19362ef..6c1eec0735b4 100644
--- a/test/cli/test/fails.test.ts
+++ b/test/cli/test/fails.test.ts
@@ -7,7 +7,7 @@ import { expect, it } from 'vitest'
import { runInlineTests, runVitest, ts } from '../../test-utils'
const root = resolve(import.meta.dirname, '../fixtures/fails')
-const files = await glob(['**/*.test.ts'], { cwd: root, dot: true, expandDirectories: false })
+const files = await glob(['**/*.test.{ts,js}'], { cwd: root, dot: true, expandDirectories: false })
it.each(files)('should fail %s', async (file) => {
const { stderr } = await runVitest({ root }, [file])
diff --git a/test/cli/test/reporters/import-durations.test.ts b/test/cli/test/reporters/import-durations.test.ts
index f0a56731f4ed..5cce634d25b9 100644
--- a/test/cli/test/reporters/import-durations.test.ts
+++ b/test/cli/test/reporters/import-durations.test.ts
@@ -1,18 +1,42 @@
+import { stripVTControlCharacters } from 'node:util'
import { runVitest } from '#test-utils'
import { resolve } from 'pathe'
import { describe, expect, it } from 'vitest'
+/**
+ * Extract import durations section and normalize variable values for snapshot testing.
+ * Replaces timing values and bar characters with placeholders.
+ */
+function normalizeImportDurationsOutput(stdout: string): string {
+ const plain = stripVTControlCharacters(stdout)
+ const start = plain.indexOf('Import Duration Breakdown')
+ const end = plain.indexOf('Total import time (self/total):')
+ if (start === -1 || end === -1) {
+ return ''
+ }
+ const endOfLine = plain.indexOf('\n', end)
+ const section = plain.slice(start, endOfLine === -1 ? undefined : endOfLine)
+ return section
+ // Normalize time values (e.g., "88ms", "1.01s") to "XXX"
+ // Negative lookahead to avoid matching times in filenames (e.g., "import-durations-50ms.ts")
+ .replace(/\d+(\.\d+)?(ms|s)(?![\w.])/g, 'XXX')
+ // Normalize bar characters to "[BAR]"
+ .replace(/[█░]+/g, '[BAR]')
+ // Normalize multiple spaces (from column padding) to single space
+ .replace(/ {2,}/g, ' ')
+}
+
describe('import durations', () => {
const root = resolve(import.meta.dirname, '..', '..', 'fixtures', 'reporters')
it('should populate importDurations on File with import durations during execution', async () => {
- const { exitCode, ctx } = await runVitest({
+ const { exitCode, ctx, stderr } = await runVitest({
root,
include: ['**/import-durations.test.ts'],
experimental: { importDurations: { limit: 10 } },
})
- expect(exitCode).toBe(0)
+ expect(exitCode, `Expected exit code 0 but got ${exitCode}. stderr: ${stderr}`).toBe(0)
const capturedFiles = ctx!.state.getFiles()
@@ -90,10 +114,19 @@ describe('import durations', () => {
},
})
- expect(stdout).toContain('Import Duration Breakdown')
- expect(stdout).toContain('(ordered by Total Time)')
- expect(stdout).toContain('Total imports:')
- expect(stdout).toContain('(Top 5)')
+ expect(normalizeImportDurationsOutput(stdout)).toMatchInlineSnapshot(`
+ "Import Duration Breakdown (Top 5)
+
+ Module Self Total
+ import-durations.test.ts XXX XXX [BAR]
+ import-durations-50ms.ts XXX XXX [BAR]
+ import-durations-25ms.ts XXX XXX [BAR]
+ ../../../../packages/vitest/dist/index.js XXX XXX [BAR]
+
+ Total imports: 4
+ Slowest import (total-time): XXX
+ Total import time (self/total): XXX / XXX"
+ `)
})
it('should not collect importDurations by default', async () => {
@@ -105,4 +138,66 @@ describe('import durations', () => {
const file = ctx!.state.getFiles()[0]
expect(file.importDurations).toEqual({})
})
+
+ it('should print on-warn only when threshold exceeded', async () => {
+ // With high threshold (500ms), should NOT print (imports are ~75-120ms depending on CI)
+ const { stdout: stdoutHigh } = await runVitest({
+ root,
+ include: ['**/import-durations.test.ts'],
+ experimental: {
+ importDurations: {
+ print: 'on-warn',
+ thresholds: { warn: 500 },
+ },
+ },
+ })
+
+ expect(stdoutHigh).not.toContain('Import Duration Breakdown')
+
+ // With lower threshold (50ms), should print (imports are ~75ms > 50ms)
+ const { stdout: stdoutLow } = await runVitest({
+ root,
+ include: ['**/import-durations.test.ts'],
+ experimental: {
+ importDurations: {
+ print: 'on-warn',
+ thresholds: { warn: 50 },
+ },
+ },
+ })
+
+ expect(stdoutLow).toContain('Import Duration Breakdown')
+ })
+
+ it('should fail when failOnDanger is enabled and threshold exceeded', async () => {
+ // With default danger threshold (500ms), should NOT fail (imports are ~75ms)
+ const { exitCode: exitCodeDefault, stderr: stderrDefault } = await runVitest({
+ root,
+ include: ['**/import-durations.test.ts'],
+ experimental: {
+ importDurations: {
+ failOnDanger: true,
+ },
+ },
+ })
+
+ expect(exitCodeDefault).toBe(0)
+ expect(stderrDefault).not.toContain('exceeded the danger threshold')
+
+ // With lower danger threshold (50ms), should fail (imports are ~75ms > 50ms)
+ const { exitCode: exitCodeLow, stderr: stderrLow, stdout: stdoutLow } = await runVitest({
+ root,
+ include: ['**/import-durations.test.ts'],
+ experimental: {
+ importDurations: {
+ failOnDanger: true,
+ thresholds: { danger: 50 },
+ },
+ },
+ })
+
+ expect(exitCodeLow).toBe(1)
+ expect(stderrLow).toContain('exceeded the danger threshold')
+ expect(stdoutLow).toContain('Import Duration Breakdown')
+ })
})
diff --git a/test/cli/test/test-meta.test.ts b/test/cli/test/test-meta.test.ts
new file mode 100644
index 000000000000..c22b49d8cc96
--- /dev/null
+++ b/test/cli/test/test-meta.test.ts
@@ -0,0 +1,699 @@
+import type { TestCase, TestSuite } from 'vitest/node'
+import { runInlineTests } from '#test-utils'
+import { expect, test } from 'vitest'
+
+test('meta can be defined on test options', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ test('test 1', { meta: { custom: 'value', count: 42 } }, () => {})
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const testCase = testModule.children.at(0) as TestCase
+ expect(testCase.meta()).toMatchInlineSnapshot(`
+ {
+ "count": 42,
+ "custom": "value",
+ }
+ `)
+})
+
+test('meta can be defined on suite options', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ describe('suite', { meta: { suiteKey: 'suiteValue' } }, () => {
+ test('test 1', () => {})
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const testSuite = testModule.children.at(0) as TestSuite
+ expect(testSuite.meta()).toMatchInlineSnapshot(`
+ {
+ "suiteKey": "suiteValue",
+ }
+ `)
+})
+
+test('test inherits meta from parent suite', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ describe('suite', { meta: { inherited: true, level: 'suite' } }, () => {
+ test('test 1', () => {})
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const testSuite = testModule.children.at(0) as TestSuite
+ const testCase = testSuite.children.at(0) as TestCase
+ expect(testCase.meta()).toMatchInlineSnapshot(`
+ {
+ "inherited": true,
+ "level": "suite",
+ }
+ `)
+})
+
+test('test meta overrides inherited suite meta', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ describe('suite', { meta: { shared: 'fromSuite', suiteOnly: true } }, () => {
+ test('test 1', { meta: { shared: 'fromTest', testOnly: 123 } }, () => {})
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const testSuite = testModule.children.at(0) as TestSuite
+ const testCase = testSuite.children.at(0) as TestCase
+ expect(testCase.meta()).toMatchInlineSnapshot(`
+ {
+ "shared": "fromTest",
+ "suiteOnly": true,
+ "testOnly": 123,
+ }
+ `)
+})
+
+test('nested suites inherit meta from parent suites', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ describe('outer', { meta: { outer: true } }, () => {
+ describe('inner', { meta: { inner: true } }, () => {
+ test('test 1', () => {})
+ })
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const outerSuite = testModule.children.at(0) as TestSuite
+ const innerSuite = outerSuite.children.at(0) as TestSuite
+ const testCase = innerSuite.children.at(0) as TestCase
+
+ expect(outerSuite.meta()).toMatchInlineSnapshot(`
+ {
+ "outer": true,
+ }
+ `)
+ expect(innerSuite.meta()).toMatchInlineSnapshot(`
+ {
+ "inner": true,
+ "outer": true,
+ }
+ `)
+ expect(testCase.meta()).toMatchInlineSnapshot(`
+ {
+ "inner": true,
+ "outer": true,
+ }
+ `)
+})
+
+test('deeply nested meta inheritance with overrides', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ describe('level1', { meta: { level: 1, a: 'first' } }, () => {
+ describe('level2', { meta: { level: 2, b: 'second' } }, () => {
+ describe('level3', { meta: { level: 3, a: 'override' } }, () => {
+ test('test 1', { meta: { level: 4 } }, () => {})
+ })
+ })
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const level1 = testModule.children.at(0) as TestSuite
+ const level2 = level1.children.at(0) as TestSuite
+ const level3 = level2.children.at(0) as TestSuite
+ const testCase = level3.children.at(0) as TestCase
+
+ expect(level1.meta()).toMatchInlineSnapshot(`
+ {
+ "a": "first",
+ "level": 1,
+ }
+ `)
+ expect(level2.meta()).toMatchInlineSnapshot(`
+ {
+ "a": "first",
+ "b": "second",
+ "level": 2,
+ }
+ `)
+ expect(level3.meta()).toMatchInlineSnapshot(`
+ {
+ "a": "override",
+ "b": "second",
+ "level": 3,
+ }
+ `)
+ expect(testCase.meta()).toMatchInlineSnapshot(`
+ {
+ "a": "override",
+ "b": "second",
+ "level": 4,
+ }
+ `)
+})
+
+test('meta is accessible from task.meta inside tests', async () => {
+ const { stderr, stdout } = await runInlineTests({
+ 'basic.test.js': `
+ describe('suite', { meta: { suiteKey: 'inherited' } }, () => {
+ test('test 1', { meta: { testKey: 'own' } }, ({ task }) => {
+ console.log('META:', JSON.stringify(task.meta))
+ })
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const metaLine = stdout.split('\n').find(line => line.startsWith('META:'))
+ expect(metaLine).toBeDefined()
+ expect(JSON.parse(metaLine!.slice('META:'.length))).toMatchInlineSnapshot(`
+ {
+ "suiteKey": "inherited",
+ "testKey": "own",
+ }
+ `)
+})
+
+test('sibling tests have independent meta', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ describe('suite', { meta: { shared: 'parent' } }, () => {
+ test('test 1', { meta: { id: 1 } }, () => {})
+ test('test 2', { meta: { id: 2 } }, () => {})
+ test('test 3', () => {})
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const testSuite = testModule.children.at(0) as TestSuite
+ const [test1, test2, test3] = testSuite.children.array() as TestCase[]
+
+ expect(test1.meta()).toMatchInlineSnapshot(`
+ {
+ "id": 1,
+ "shared": "parent",
+ }
+ `)
+ expect(test2.meta()).toMatchInlineSnapshot(`
+ {
+ "id": 2,
+ "shared": "parent",
+ }
+ `)
+ expect(test3.meta()).toMatchInlineSnapshot(`
+ {
+ "shared": "parent",
+ }
+ `)
+})
+
+test('sibling suites have independent meta', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ describe('suite1', { meta: { suite: 1 } }, () => {
+ test('test 1', () => {})
+ })
+ describe('suite2', { meta: { suite: 2 } }, () => {
+ test('test 2', () => {})
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const [suite1, suite2] = testModule.children.array() as TestSuite[]
+ const test1 = suite1.children.at(0) as TestCase
+ const test2 = suite2.children.at(0) as TestCase
+
+ expect(suite1.meta()).toMatchInlineSnapshot(`
+ {
+ "suite": 1,
+ }
+ `)
+ expect(suite2.meta()).toMatchInlineSnapshot(`
+ {
+ "suite": 2,
+ }
+ `)
+ expect(test1.meta()).toMatchInlineSnapshot(`
+ {
+ "suite": 1,
+ }
+ `)
+ expect(test2.meta()).toMatchInlineSnapshot(`
+ {
+ "suite": 2,
+ }
+ `)
+})
+
+test('test without parent suite has empty meta', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ test('test 1', () => {})
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const testCase = testModule.children.at(0) as TestCase
+ expect(testCase.meta()).toMatchInlineSnapshot(`{}`)
+})
+
+test('test.each works with meta', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ describe('suite', { meta: { feature: 'each' } }, () => {
+ test.each([1, 2, 3])('test %i', { meta: { eachTest: true } }, () => {})
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const testSuite = testModule.children.at(0) as TestSuite
+ const tests = testSuite.children.array() as TestCase[]
+
+ expect(tests).toHaveLength(3)
+ for (const test of tests) {
+ expect(test.meta()).toMatchInlineSnapshot(`
+ {
+ "eachTest": true,
+ "feature": "each",
+ }
+ `)
+ }
+})
+
+test('describe.each works with meta', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ describe.each([1, 2])('suite %i', { meta: { dynamic: true } }, () => {
+ test('test', () => {})
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const [suite1, suite2] = testModule.children.array() as TestSuite[]
+
+ expect(suite1.meta()).toMatchInlineSnapshot(`
+ {
+ "dynamic": true,
+ }
+ `)
+ expect(suite2.meta()).toMatchInlineSnapshot(`
+ {
+ "dynamic": true,
+ }
+ `)
+ expect((suite1.children.at(0) as TestCase).meta()).toMatchInlineSnapshot(`
+ {
+ "dynamic": true,
+ }
+ `)
+})
+
+test('concurrent tests have independent meta', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ describe('suite', { meta: { shared: true } }, () => {
+ test.concurrent('test 1', { meta: { id: 1 } }, () => {})
+ test.concurrent('test 2', { meta: { id: 2 } }, () => {})
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const testSuite = testModule.children.at(0) as TestSuite
+ const [test1, test2] = testSuite.children.array() as TestCase[]
+
+ expect(test1.meta()).toMatchInlineSnapshot(`
+ {
+ "id": 1,
+ "shared": true,
+ }
+ `)
+ expect(test2.meta()).toMatchInlineSnapshot(`
+ {
+ "id": 2,
+ "shared": true,
+ }
+ `)
+})
+
+test('meta with complex values', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ test('test 1', {
+ meta: {
+ nested: { a: { b: { c: 1 } } },
+ array: [1, 2, 3],
+ nullValue: null,
+ boolTrue: true,
+ boolFalse: false,
+ num: 42.5,
+ }
+ }, () => {})
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const testCase = testModule.children.at(0) as TestCase
+ expect(testCase.meta()).toMatchInlineSnapshot(`
+ {
+ "array": [
+ 1,
+ 2,
+ 3,
+ ],
+ "boolFalse": false,
+ "boolTrue": true,
+ "nested": {
+ "a": {
+ "b": {
+ "c": 1,
+ },
+ },
+ },
+ "nullValue": null,
+ "num": 42.5,
+ }
+ `)
+})
+
+test('meta works with test modifiers (skip, only, todo)', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ test.skip('skipped test', { meta: { status: 'skipped' } }, () => {})
+ test.todo('todo test', { meta: { status: 'todo' } })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const [skipped, todo] = testModule.children.array() as TestCase[]
+
+ expect(skipped.meta()).toMatchInlineSnapshot(`
+ {
+ "status": "skipped",
+ }
+ `)
+ expect(todo.meta()).toMatchInlineSnapshot(`
+ {
+ "status": "todo",
+ }
+ `)
+})
+
+test('meta works with test.fails', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ test.fails('failing test', { meta: { expectFailure: true } }, () => {
+ throw new Error('Expected error')
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const testCase = testModule.children.at(0) as TestCase
+ expect(testCase.meta()).toMatchInlineSnapshot(`
+ {
+ "expectFailure": true,
+ }
+ `)
+ expect(testCase.result().state).toBe('passed')
+})
+
+test('suite without meta does not inherit to tests', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ describe('suite without meta', () => {
+ test('test with meta', { meta: { ownMeta: true } }, () => {})
+ test('test without meta', () => {})
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const testSuite = testModule.children.at(0) as TestSuite
+ const [withMeta, withoutMeta] = testSuite.children.array() as TestCase[]
+
+ expect(testSuite.meta()).toMatchInlineSnapshot(`{}`)
+ expect(withMeta.meta()).toMatchInlineSnapshot(`
+ {
+ "ownMeta": true,
+ }
+ `)
+ expect(withoutMeta.meta()).toMatchInlineSnapshot(`{}`)
+})
+
+test('meta does not mutate parent when child overrides', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ describe('parent', { meta: { key: 'parent', parentOnly: true } }, () => {
+ describe('child', { meta: { key: 'child', childOnly: true } }, () => {
+ test('test', () => {})
+ })
+ test('sibling test', () => {})
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const parent = testModule.children.at(0) as TestSuite
+ const child = parent.children.at(0) as TestSuite
+ const siblingTest = parent.children.at(1) as TestCase
+
+ expect(parent.meta()).toMatchInlineSnapshot(`
+ {
+ "key": "parent",
+ "parentOnly": true,
+ }
+ `)
+ expect(child.meta()).toMatchInlineSnapshot(`
+ {
+ "childOnly": true,
+ "key": "child",
+ "parentOnly": true,
+ }
+ `)
+ expect(siblingTest.meta()).toMatchInlineSnapshot(`
+ {
+ "key": "parent",
+ "parentOnly": true,
+ }
+ `)
+})
+
+test('meta with test.for', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ describe('suite', { meta: { fromSuite: true } }, () => {
+ test.for([
+ { input: 1, expected: 2 },
+ { input: 2, expected: 4 },
+ ])('test $input', { meta: { forTest: true } }, ({ input, expected }) => {
+ expect(input * 2).toBe(expected)
+ })
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const testSuite = testModule.children.at(0) as TestSuite
+ const tests = testSuite.children.array() as TestCase[]
+
+ expect(tests).toHaveLength(2)
+ for (const test of tests) {
+ expect(test.meta()).toMatchInlineSnapshot(`
+ {
+ "forTest": true,
+ "fromSuite": true,
+ }
+ `)
+ }
+})
+
+test('empty meta object is allowed', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ describe('suite', { meta: {} }, () => {
+ test('test', { meta: {} }, () => {})
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const testSuite = testModule.children.at(0) as TestSuite
+ const testCase = testSuite.children.at(0) as TestCase
+
+ expect(testSuite.meta()).toMatchInlineSnapshot(`{}`)
+ expect(testCase.meta()).toMatchInlineSnapshot(`{}`)
+})
+
+test('meta inheritance across multiple files', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'file1.test.js': `
+ describe('suite in file1', { meta: { file: 1 } }, () => {
+ test('test 1', () => {})
+ })
+ `,
+ 'file2.test.js': `
+ describe('suite in file2', { meta: { file: 2 } }, () => {
+ test('test 2', () => {})
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ },
+ },
+ })
+
+ expect(stderr).toBe('')
+ const testModules = ctx!.state.getTestModules()
+ const file1Module = testModules.find(m => m.moduleId.includes('file1'))!
+ const file2Module = testModules.find(m => m.moduleId.includes('file2'))!
+
+ const suite1 = file1Module.children.at(0) as TestSuite
+ const suite2 = file2Module.children.at(0) as TestSuite
+ const test1 = suite1.children.at(0) as TestCase
+ const test2 = suite2.children.at(0) as TestCase
+
+ expect(test1.meta()).toMatchInlineSnapshot(`
+ {
+ "file": 1,
+ }
+ `)
+ expect(test2.meta()).toMatchInlineSnapshot(`
+ {
+ "file": 2,
+ }
+ `)
+})
diff --git a/test/cli/test/test-tags.test.ts b/test/cli/test/test-tags.test.ts
index 426f81306d58..8da7b33e497b 100644
--- a/test/cli/test/test-tags.test.ts
+++ b/test/cli/test/test-tags.test.ts
@@ -1255,6 +1255,139 @@ test('multiple filter expressions act as AND', async () => {
`)
})
+test('tags can define meta in config', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ test('test 1', { tags: ['unit'] }, () => {})
+ test('test 2', { tags: ['e2e'] }, () => {})
+ test('test 3', { tags: ['unit', 'slow'] }, () => {})
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ tags: [
+ { name: 'unit', meta: { type: 'unit', priority: 1 } },
+ { name: 'e2e', meta: { type: 'e2e', browser: true } },
+ { name: 'slow', meta: { priority: 2, slow: true } },
+ ],
+ },
+ },
+ })
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const [test1, test2, test3] = testModule.children.array() as TestCase[]
+ expect(test1.meta()).toMatchInlineSnapshot(`
+ {
+ "priority": 1,
+ "type": "unit",
+ }
+ `)
+ expect(test2.meta()).toMatchInlineSnapshot(`
+ {
+ "browser": true,
+ "type": "e2e",
+ }
+ `)
+ expect(test3.meta()).toMatchInlineSnapshot(`
+ {
+ "priority": 2,
+ "slow": true,
+ "type": "unit",
+ }
+ `)
+})
+
+test('tag meta is inherited by suite and test meta', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ describe('suite', { tags: ['suite-tag'], meta: { suiteOwn: true } }, () => {
+ test('test', { tags: ['test-tag'], meta: { testOwn: true } }, () => {})
+ })
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ tags: [
+ { name: 'suite-tag', meta: { fromSuiteTag: 'value' } },
+ { name: 'test-tag', meta: { fromTestTag: 'value' } },
+ ],
+ },
+ },
+ })
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const suite = testModule.children.at(0) as TestSuite
+ const testCase = suite.children.at(0) as TestCase
+ // suite has a tag with metadata, but tags are only applied to tests,
+ // so suites don't get tag metadata
+ expect(suite.meta()).toMatchInlineSnapshot(`
+ {
+ "suiteOwn": true,
+ }
+ `)
+ expect(testCase.meta()).toMatchInlineSnapshot(`
+ {
+ "fromSuiteTag": "value",
+ "fromTestTag": "value",
+ "suiteOwn": true,
+ "testOwn": true,
+ }
+ `)
+})
+
+test('test meta overrides tag meta', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ test('test', { tags: ['tagged'], meta: { key: 'fromTest', testOnly: true } }, () => {})
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ tags: [
+ { name: 'tagged', meta: { key: 'fromTag', tagOnly: true } },
+ ],
+ },
+ },
+ })
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const testCase = testModule.children.at(0) as TestCase
+ expect(testCase.meta()).toMatchInlineSnapshot(`
+ {
+ "key": "fromTest",
+ "tagOnly": true,
+ "testOnly": true,
+ }
+ `)
+})
+
+test('multiple tags with meta are merged with priority order', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'basic.test.js': `
+ test('test', { tags: ['low', 'high'] }, () => {})
+ `,
+ 'vitest.config.js': {
+ test: {
+ globals: true,
+ tags: [
+ { name: 'low', priority: 2, meta: { shared: 'low', lowOnly: true } },
+ { name: 'high', priority: 1, meta: { shared: 'high', highOnly: true } },
+ ],
+ },
+ },
+ })
+ expect(stderr).toBe('')
+ const testModule = ctx!.state.getTestModules()[0]
+ const testCase = testModule.children.at(0) as TestCase
+ expect(testCase.meta()).toMatchInlineSnapshot(`
+ {
+ "highOnly": true,
+ "lowOnly": true,
+ "shared": "high",
+ }
+ `)
+})
+
function getTestTree(builder: (fn: (test: TestCase) => any) => any) {
return builder(test => test.options.tags)
}
@@ -1272,3 +1405,20 @@ function removeUndefined>(obj: T): Partial {
}
return result
}
+
+declare module 'vitest' {
+ interface TaskMeta {
+ type?: string
+ priority?: number
+ browser?: boolean
+ slow?: boolean
+ fromSuiteTag?: string
+ fromTestTag?: string
+ suiteOwn?: boolean
+ testOwn?: boolean
+ tagOnly?: boolean
+ shared?: string
+ lowOnly?: boolean
+ highOnly?: boolean
+ }
+}
diff --git a/test/config/test/cli-config.test.ts b/test/config/test/cli-config.test.ts
index 23064dcca089..d0f3c98338b3 100644
--- a/test/config/test/cli-config.test.ts
+++ b/test/config/test/cli-config.test.ts
@@ -53,12 +53,12 @@ it('correctly inherit from the cli', async () => {
retry: 6,
passWithNoTests: true,
bail: 100,
- experimental: expect.objectContaining({
+ experimental: {
importDurations: {
print: true,
limit: 10,
},
- }),
+ },
})
expect(config.testNamePattern?.test('math')).toBe(true)
})
diff --git a/test/config/test/override.test.ts b/test/config/test/override.test.ts
index e1d5e2b05177..ff737f54e575 100644
--- a/test/config/test/override.test.ts
+++ b/test/config/test/override.test.ts
@@ -27,6 +27,8 @@ describe('correctly defines api flag', () => {
})
expect(c.vite.config.server.middlewareMode).toBe(true)
expect(c.config.api).toEqual({
+ allowExec: true,
+ allowWrite: true,
middlewareMode: true,
token: expect.any(String),
})
@@ -42,6 +44,8 @@ describe('correctly defines api flag', () => {
expect(c.vite.config.server.port).toBe(4321)
expect(c.config.api).toEqual({
port: 4321,
+ allowWrite: true,
+ allowExec: true,
token: expect.any(String),
})
})
@@ -51,6 +55,61 @@ describe('correctly defines api flag', () => {
expect(c.config.isolate).toBe(false)
expect(c.config.browser.isolate).toBe(false)
})
+
+ it('allowWrite and allowExec default to true when not exposed to network', async () => {
+ const c = await config({ api: { port: 5555 } }, {})
+ expect(c.api.allowWrite).toBe(true)
+ expect(c.api.allowExec).toBe(true)
+ })
+
+ it('allowWrite and allowExec default to true for localhost', async () => {
+ const c = await config({ api: { port: 5555, host: 'localhost' } }, {})
+ expect(c.api.allowWrite).toBe(true)
+ expect(c.api.allowExec).toBe(true)
+ })
+
+ it('allowWrite and allowExec default to true for 127.0.0.1', async () => {
+ const c = await config({ api: { port: 5555, host: '127.0.0.1' } }, {})
+ expect(c.api.allowWrite).toBe(true)
+ expect(c.api.allowExec).toBe(true)
+ })
+
+ it('allowWrite and allowExec default to false when exposed to network', async () => {
+ const c = await config({ api: { port: 5555, host: '0.0.0.0' } }, {})
+ expect(c.api.allowWrite).toBe(false)
+ expect(c.api.allowExec).toBe(false)
+ })
+
+ it('allowWrite and allowExec can be explicitly overridden when exposed to network', async () => {
+ const c = await config({ api: { port: 5555, host: '0.0.0.0', allowWrite: true, allowExec: true } }, {})
+ expect(c.api.allowWrite).toBe(true)
+ expect(c.api.allowExec).toBe(true)
+ })
+
+ it('allowWrite and allowExec can be explicitly disabled', async () => {
+ const c = await config({ api: { port: 5555, allowWrite: false, allowExec: false } }, {})
+ expect(c.api.allowWrite).toBe(false)
+ expect(c.api.allowExec).toBe(false)
+ })
+
+ it('browser.api inherits allowWrite and allowExec from api', async () => {
+ const c = await config({ api: { port: 5555, allowWrite: false, allowExec: false } }, {})
+ expect(c.browser.api.allowWrite).toBe(false)
+ expect(c.browser.api.allowExec).toBe(false)
+ })
+
+ it('browser.api can override inherited allowWrite and allowExec', async () => {
+ const c = await config({
+ api: { port: 5555, allowWrite: false, allowExec: false },
+ browser: { api: { allowWrite: true, allowExec: true } },
+ }, {
+ browser: {},
+ })
+ expect(c.api.allowWrite).toBe(false)
+ expect(c.api.allowExec).toBe(false)
+ expect(c.browser.api.allowWrite).toBe(true)
+ expect(c.browser.api.allowExec).toBe(true)
+ })
})
describe.each([
diff --git a/test/core/package.json b/test/core/package.json
index 6b98f6887a54..e8107e6723db 100644
--- a/test/core/package.json
+++ b/test/core/package.json
@@ -28,11 +28,11 @@
"@vitest/utils": "workspace:*",
"@vitest/web-worker": "workspace:^",
"@vueuse/integrations": "^14.1.0",
- "axios": "^1.13.2",
+ "axios": "^1.13.4",
"immutable": "5.1.4",
"memfs": "^4.56.10",
"obug": "^2.1.1",
- "react": "^19.2.3",
+ "react": "^19.2.4",
"react-18": "npm:react@18.3.1",
"temporal-polyfill": "~0.3.0",
"tinyrainbow": "catalog:",
diff --git a/test/core/test/custom.test.ts b/test/core/test/custom.test.ts
index 28f08d2c5e5b..b7afeb1f33ad 100644
--- a/test/core/test/custom.test.ts
+++ b/test/core/test/custom.test.ts
@@ -10,6 +10,12 @@ import {
} from 'vitest'
import { Gardener } from '../src/custom/gardener.js'
+declare module 'vitest' {
+ interface TaskMeta {
+ customPropertyToDifferentiateTask?: boolean
+ }
+}
+
// this function will be called, when Vitest collects tasks
const myCustomTask = TestRunner.createChainable(['todo'], function (name: string, fn: () => void) {
TestRunner.getCurrentSuite().task(name, {
diff --git a/test/core/vite.config.ts b/test/core/vite.config.ts
index 92e17c3f36cf..1a597e614393 100644
--- a/test/core/vite.config.ts
+++ b/test/core/vite.config.ts
@@ -65,6 +65,8 @@ export default defineConfig({
test: {
api: {
port: 3023,
+ allowExec: false,
+ allowWrite: false,
},
name: 'core',
includeSource: [
diff --git a/test/coverage-test/fixtures/src/mock-target.ts b/test/coverage-test/fixtures/src/mock-target.ts
new file mode 100644
index 000000000000..bf28b98258e0
--- /dev/null
+++ b/test/coverage-test/fixtures/src/mock-target.ts
@@ -0,0 +1,7 @@
+export function double(value: number): number {
+ return value * 2
+}
+
+export function triple(value: number): number {
+ return value * 3
+}
diff --git a/test/coverage-test/fixtures/test/mock-autospy-fixture.test.ts b/test/coverage-test/fixtures/test/mock-autospy-fixture.test.ts
new file mode 100644
index 000000000000..9a0c310f3331
--- /dev/null
+++ b/test/coverage-test/fixtures/test/mock-autospy-fixture.test.ts
@@ -0,0 +1,10 @@
+import { expect, test, vi } from 'vitest'
+import { double, triple } from '../src/mock-target'
+
+vi.mock(import('../src/mock-target'), { spy: true })
+
+test('autospy calls original and can be spied on', () => {
+ expect(double(5)).toBe(10)
+ expect(double).toHaveBeenCalledWith(5)
+ expect(triple).not.toHaveBeenCalled()
+})
diff --git a/test/coverage-test/fixtures/test/mock-importActual-fixture.test.ts b/test/coverage-test/fixtures/test/mock-importActual-fixture.test.ts
new file mode 100644
index 000000000000..8f00072ba67e
--- /dev/null
+++ b/test/coverage-test/fixtures/test/mock-importActual-fixture.test.ts
@@ -0,0 +1,17 @@
+import { expect, test, vi } from 'vitest'
+import { double, triple } from '../src/mock-target'
+
+// Manual spy approach using importOriginal callback - this collects coverage
+vi.mock(import('../src/mock-target'), async (importOriginal) => {
+ const actual = await importOriginal()
+ return {
+ double: vi.fn(actual.double),
+ triple: vi.fn(actual.triple),
+ }
+})
+
+test('importActual calls original and can be spied on', () => {
+ expect(double(5)).toBe(10)
+ expect(double).toHaveBeenCalledWith(5)
+ expect(triple).not.toHaveBeenCalled()
+})
diff --git a/test/coverage-test/test/mock-autospy.test.ts b/test/coverage-test/test/mock-autospy.test.ts
new file mode 100644
index 000000000000..2c96964dbec7
--- /dev/null
+++ b/test/coverage-test/test/mock-autospy.test.ts
@@ -0,0 +1,40 @@
+import { expect } from 'vitest'
+import { readCoverageMap, runVitest, test } from '../utils'
+
+test('vi.mock({ spy: true }) collects coverage of original module', async () => {
+ await runVitest({
+ include: ['fixtures/test/mock-autospy-fixture.test.ts'],
+ coverage: {
+ reporter: 'json',
+ include: ['fixtures/src/mock-target.ts'],
+ },
+ })
+
+ const coverageMap = await readCoverageMap()
+ expect(coverageMap).toMatchInlineSnapshot(`
+ {
+ "branches": "0/0 (100%)",
+ "functions": "1/2 (50%)",
+ "lines": "1/2 (50%)",
+ "statements": "1/2 (50%)",
+ }
+ `)
+
+ const coverage = coverageMap.fileCoverageFor('/fixtures/src/mock-target.ts')
+ const functionCoverage = Object.keys(coverage.fnMap)
+ .map(index => ({ name: coverage.fnMap[index].name, hits: coverage.f[index] }))
+ .sort((a, b) => a.name.localeCompare(b.name))
+
+ expect(functionCoverage).toMatchInlineSnapshot(`
+ [
+ {
+ "hits": 1,
+ "name": "double",
+ },
+ {
+ "hits": 0,
+ "name": "triple",
+ },
+ ]
+ `)
+})
diff --git a/test/coverage-test/test/mock-importActual.test.ts b/test/coverage-test/test/mock-importActual.test.ts
new file mode 100644
index 000000000000..0ad1d2b7b972
--- /dev/null
+++ b/test/coverage-test/test/mock-importActual.test.ts
@@ -0,0 +1,54 @@
+import { expect } from 'vitest'
+import { isBrowser, isNativeRunner, isV8Provider, readCoverageMap, runVitest, test } from '../utils'
+
+test('vi.importActual() collects coverage of original module', async () => {
+ await runVitest({
+ include: ['fixtures/test/mock-importActual-fixture.test.ts'],
+ coverage: {
+ reporter: 'json',
+ include: ['fixtures/src/mock-target.ts'],
+ },
+ })
+
+ const coverageMap = await readCoverageMap()
+
+ // v8-browser and native runner report 100% due to different coverage collection behavior
+ if ((isBrowser() && isV8Provider()) || isNativeRunner()) {
+ expect(coverageMap).toMatchInlineSnapshot(`
+ {
+ "branches": "0/0 (100%)",
+ "functions": "2/2 (100%)",
+ "lines": "2/2 (100%)",
+ "statements": "2/2 (100%)",
+ }
+ `)
+ }
+ else {
+ expect(coverageMap).toMatchInlineSnapshot(`
+ {
+ "branches": "0/0 (100%)",
+ "functions": "1/2 (50%)",
+ "lines": "1/2 (50%)",
+ "statements": "1/2 (50%)",
+ }
+ `)
+
+ const coverage = coverageMap.fileCoverageFor('/fixtures/src/mock-target.ts')
+ const functionCoverage = Object.keys(coverage.fnMap)
+ .map(index => ({ name: coverage.fnMap[index].name, hits: coverage.f[index] }))
+ .sort((a, b) => a.name.localeCompare(b.name))
+
+ expect(functionCoverage).toMatchInlineSnapshot(`
+ [
+ {
+ "hits": 1,
+ "name": "double",
+ },
+ {
+ "hits": 0,
+ "name": "triple",
+ },
+ ]
+ `)
+ }
+})
diff --git a/test/coverage-test/vitest.config.ts b/test/coverage-test/vitest.config.ts
index f75455f42e3b..2afd3dcfa879 100644
--- a/test/coverage-test/vitest.config.ts
+++ b/test/coverage-test/vitest.config.ts
@@ -90,6 +90,8 @@ export default defineConfig({
'**/query-param-transforms.test.ts',
'**/test/cjs-dependency.test.ts',
'**/test/source-maps.test.ts',
+ '**/test/mock-autospy.test.ts',
+ '**/test/mock-importActual.test.ts',
],
exclude: [FIXTURES],
},
@@ -122,6 +124,8 @@ export default defineConfig({
'**/query-param-transforms.test.ts',
'**/test/cjs-dependency.test.ts',
'**/test/source-maps.test.ts',
+ '**/test/mock-autospy.test.ts',
+ '**/test/mock-importActual.test.ts',
],
exclude: [FIXTURES],
},
diff --git a/test/typescript/package.json b/test/typescript/package.json
index 5ad517fea2e5..a842db3577a1 100644
--- a/test/typescript/package.json
+++ b/test/typescript/package.json
@@ -11,6 +11,6 @@
},
"devDependencies": {
"typescript": "^5.9.3",
- "vue-tsc": "^3.2.3"
+ "vue-tsc": "^3.2.4"
}
}
diff --git a/test/ui/fixtures/snapshot.test.ts b/test/ui/fixtures/snapshot.test.ts
new file mode 100644
index 000000000000..f548b35af28c
--- /dev/null
+++ b/test/ui/fixtures/snapshot.test.ts
@@ -0,0 +1,5 @@
+import { expect, test } from 'vitest';
+
+test('wrong snapshot', () => {
+ expect(1).toMatchInlineSnapshot(`2`)
+})
diff --git a/test/ui/test/html-report.spec.ts b/test/ui/test/html-report.spec.ts
index bbbd6e38c8fc..13980852fcfc 100644
--- a/test/ui/test/html-report.spec.ts
+++ b/test/ui/test/html-report.spec.ts
@@ -66,7 +66,7 @@ test.describe('html report', () => {
await page.goto(pageUrl)
// dashboard
- await expect(page.locator('[aria-labelledby=tests]')).toContainText('15 Pass 1 Fail 16 Total')
+ await expect(page.locator('[aria-labelledby=tests]')).toContainText('15 Pass 2 Fail 17 Total')
// unhandled errors
await expect(page.getByTestId('unhandled-errors')).toContainText(
diff --git a/test/ui/test/ui-security.spec.ts b/test/ui/test/ui-security.spec.ts
new file mode 100644
index 000000000000..c6136ee0e3c1
--- /dev/null
+++ b/test/ui/test/ui-security.spec.ts
@@ -0,0 +1,70 @@
+import type { Vitest } from 'vitest/node'
+import { Writable } from 'node:stream'
+import { expect, test } from '@playwright/test'
+import { startVitest } from 'vitest/node'
+
+const port = 9002
+const pageUrl = `http://localhost:${port}/__vitest__/`
+
+test.describe('ui', () => {
+ let vitest: Vitest | undefined
+
+ test.beforeAll(async () => {
+ // silence Vitest logs
+ const stdout = new Writable({ write: (_, __, callback) => callback() })
+ const stderr = new Writable({ write: (_, __, callback) => callback() })
+ vitest = await startVitest('test', [], {
+ watch: true,
+ ui: true,
+ open: false,
+ api: {
+ port,
+ allowExec: false,
+ allowWrite: false,
+ },
+ reporters: [],
+ }, {}, {
+ stdout,
+ stderr,
+ })
+ expect(vitest).toBeDefined()
+ })
+
+ test.afterAll(async () => {
+ await vitest?.close()
+ })
+
+ test('cannot execute files from the ui', async ({ page }) => {
+ await page.goto(pageUrl)
+
+ await expect(page.getByTestId('btn-run-all')).toBeDisabled()
+
+ const item = page.getByTestId('explorer-item').nth(0)
+ await item.hover()
+ await expect(item.getByTestId('btn-run-test')).toBeDisabled()
+
+ await page.getByPlaceholder('Search...').fill('snapshot')
+
+ const snapshotItem = page.getByTestId('explorer-item').filter({ hasText: 'snapshot.test.ts' })
+ await snapshotItem.hover()
+ await expect(snapshotItem.getByTestId('btn-fix-snapshot')).not.toBeVisible()
+ })
+
+ test('cannot write files', async ({ page }) => {
+ await page.goto(pageUrl)
+
+ const item = page.getByTestId('explorer-item').nth(0)
+ await item.hover()
+ await item.getByTestId('btn-open-details').click()
+
+ await page.getByText('Code').click()
+
+ const editor = page.getByTestId('btn-code')
+ await expect(editor).toBeVisible()
+
+ await editor.click()
+ await page.keyboard.type('\n// some comment')
+
+ await expect(editor).not.toContainText('// some comment')
+ })
+})
diff --git a/test/ui/test/ui.spec.ts b/test/ui/test/ui.spec.ts
index 2ea29aab5511..cd0be8248d13 100644
--- a/test/ui/test/ui.spec.ts
+++ b/test/ui/test/ui.spec.ts
@@ -70,7 +70,7 @@ test.describe('ui', () => {
await page.goto(pageUrl)
// dashboard
- await expect(page.locator('[aria-labelledby=tests]')).toContainText('15 Pass 1 Fail 16 Total')
+ await expect(page.locator('[aria-labelledby=tests]')).toContainText('15 Pass 2 Fail 17 Total')
// unhandled errors
await expect(page.getByTestId('unhandled-errors')).toContainText(
@@ -227,7 +227,7 @@ test.describe('ui', () => {
// match only failing files when fail filter applied
await page.getByPlaceholder('Search...').fill('')
await page.getByText(/^Fail$/, { exact: true }).click()
- await page.getByText('FAIL (1)').click()
+ await page.getByText('FAIL (2)').click()
await expect(page.getByTestId('results-panel').getByText('fixtures/error.test.ts', { exact: true })).toBeVisible()
await expect(page.getByTestId('results-panel').getByText('fixtures/sample.test.ts', { exact: true })).toBeHidden()