From 2bfeb1d1c7fc1b0cac7968557a427d45a617345d Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 19 Mar 2026 13:53:29 +0100 Subject: [PATCH 1/6] fix: guard disposable and optional `body` --- packages/runner/src/types/tasks.ts | 2 +- packages/spy/src/types.ts | 5 ++++- packages/vitest/src/integrations/vi.ts | 6 ++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/runner/src/types/tasks.ts b/packages/runner/src/types/tasks.ts index db5eb0b61e74..8a1528dbbdac 100644 --- a/packages/runner/src/types/tasks.ts +++ b/packages/runner/src/types/tasks.ts @@ -1262,7 +1262,7 @@ export interface TestAttachment { /** File system path to the attachment */ path?: string /** Inline attachment content as a string or raw binary data */ - body?: string | Uint8Array + body?: string | Uint8Array | undefined } export interface Location { diff --git a/packages/spy/src/types.ts b/packages/spy/src/types.ts index 14b9d4dc3b3d..5dad59635293 100644 --- a/packages/spy/src/types.ts +++ b/packages/spy/src/types.ts @@ -206,7 +206,10 @@ Jest uses the latter for `MockInstance.mockImplementation` etc... and it allows const boolFn: Jest.Mock<() => boolean> = jest.fn<() => true>(() => true) */ /* eslint-disable ts/method-signature-style */ -export interface MockInstance extends Disposable { +export interface MockInstance { + // eslint-disable-next-line ts/ban-ts-comment + // @ts-ignore -- Symbol.dispose might not be in user types + [Symbol.dispose](): void /** * Use it to return the name assigned to the mock with the `.mockName(name)` method. By default, it will return `vi.fn()`. * @see https://vitest.dev/api/mock#getmockname diff --git a/packages/vitest/src/integrations/vi.ts b/packages/vitest/src/integrations/vi.ts index 86a9abb9d42b..43767019f255 100644 --- a/packages/vitest/src/integrations/vi.ts +++ b/packages/vitest/src/integrations/vi.ts @@ -18,6 +18,12 @@ import { waitFor, waitUntil } from './wait' type ESModuleExports = Record +interface Disposable { + // eslint-disable-next-line ts/ban-ts-comment + // @ts-ignore -- Symbol.dispose might not be in user types + [Symbol.dispose]: () => void +} + export interface VitestUtils { /** * Checks if fake timers are enabled. From 6fd361ac62a997e816a8421cd1c39e95657dc9be Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 19 Mar 2026 14:05:42 +0100 Subject: [PATCH 2/6] chore: use optional-types.d.ts trick --- packages/spy/optional-types.d.ts | 6 ++++++ packages/spy/package.json | 6 +++++- packages/spy/src/types.ts | 7 +++---- packages/vitest/optional-types.d.ts | 5 +++++ packages/vitest/src/integrations/vi.ts | 7 +------ 5 files changed, 20 insertions(+), 11 deletions(-) create mode 100644 packages/spy/optional-types.d.ts diff --git a/packages/spy/optional-types.d.ts b/packages/spy/optional-types.d.ts new file mode 100644 index 000000000000..ba94336682fd --- /dev/null +++ b/packages/spy/optional-types.d.ts @@ -0,0 +1,6 @@ +/* eslint-disable ts/ban-ts-comment */ + +export interface Disposable { + // @ts-ignore -- Symbol.dispose might not be in user types + [Symbol.dispose]: () => void +} diff --git a/packages/spy/package.json b/packages/spy/package.json index 472e3197ec23..045d22eaf8bf 100644 --- a/packages/spy/package.json +++ b/packages/spy/package.json @@ -27,13 +27,17 @@ "types": "./dist/index.d.ts", "default": "./dist/index.js" }, + "./optional-types.js": { + "types": "./optional-types.d.ts" + }, "./*": "./*" }, "main": "./dist/index.js", "module": "./dist/index.js", "types": "./dist/index.d.ts", "files": [ - "dist" + "dist", + "optional-types.d.ts" ], "scripts": { "build": "premove dist && rollup -c", diff --git a/packages/spy/src/types.ts b/packages/spy/src/types.ts index 5dad59635293..11120a67b0e9 100644 --- a/packages/spy/src/types.ts +++ b/packages/spy/src/types.ts @@ -1,3 +1,5 @@ +import type { Disposable } from '@vitest/spy/optional-types.js' + export interface MockResultReturn { type: 'return' /** @@ -55,7 +57,7 @@ export type MockProcedureContext = T extend ? InstanceType : ThisParameterType -export interface MockContext { +export interface MockContext extends Disposable { /** * This is an array containing all arguments for each call. One item of the array is the arguments of that call. * @@ -207,9 +209,6 @@ Jest uses the latter for `MockInstance.mockImplementation` etc... and it allows */ /* eslint-disable ts/method-signature-style */ export interface MockInstance { - // eslint-disable-next-line ts/ban-ts-comment - // @ts-ignore -- Symbol.dispose might not be in user types - [Symbol.dispose](): void /** * Use it to return the name assigned to the mock with the `.mockName(name)` method. By default, it will return `vi.fn()`. * @see https://vitest.dev/api/mock#getmockname diff --git a/packages/vitest/optional-types.d.ts b/packages/vitest/optional-types.d.ts index 49cdb4fae820..e53497aa2d05 100644 --- a/packages/vitest/optional-types.d.ts +++ b/packages/vitest/optional-types.d.ts @@ -5,3 +5,8 @@ export type * as jsdomTypes from 'jsdom' // @ts-ignore optional peer dep export type * as happyDomTypes from 'happy-dom' + +export interface Disposable { + // @ts-ignore -- Symbol.dispose might not be in user types + [Symbol.dispose]: () => void +} diff --git a/packages/vitest/src/integrations/vi.ts b/packages/vitest/src/integrations/vi.ts index 43767019f255..f97140da17de 100644 --- a/packages/vitest/src/integrations/vi.ts +++ b/packages/vitest/src/integrations/vi.ts @@ -6,6 +6,7 @@ import type { MaybePartiallyMockedDeep, MockInstance, } from '@vitest/spy' +import type { Disposable } from 'vitest/optional-types.js' import type { RuntimeOptions, SerializedConfig } from '../runtime/config' import type { VitestMocker } from '../runtime/moduleRunner/moduleMocker' import type { MockFactoryWithHelper, MockOptions } from '../types/mocker' @@ -18,12 +19,6 @@ import { waitFor, waitUntil } from './wait' type ESModuleExports = Record -interface Disposable { - // eslint-disable-next-line ts/ban-ts-comment - // @ts-ignore -- Symbol.dispose might not be in user types - [Symbol.dispose]: () => void -} - export interface VitestUtils { /** * Checks if fake timers are enabled. From de5b842e632d8c09c3a4987f423630953704b591 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 19 Mar 2026 14:06:38 +0100 Subject: [PATCH 3/6] chore: external --- packages/spy/rollup.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/spy/rollup.config.js b/packages/spy/rollup.config.js index 98195169d7db..3f9e6e234d31 100644 --- a/packages/spy/rollup.config.js +++ b/packages/spy/rollup.config.js @@ -10,6 +10,7 @@ const external = [ ...builtinModules, ...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {}), + '@vitest/spy/optional-types.js', ] const dtsUtils = createDtsUtils() From 2695d7018cbd335127a93de643018f569f602487 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 19 Mar 2026 14:11:52 +0100 Subject: [PATCH 4/6] fix: the wrong type --- packages/spy/src/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/spy/src/types.ts b/packages/spy/src/types.ts index 11120a67b0e9..dada97940ec0 100644 --- a/packages/spy/src/types.ts +++ b/packages/spy/src/types.ts @@ -57,7 +57,7 @@ export type MockProcedureContext = T extend ? InstanceType : ThisParameterType -export interface MockContext extends Disposable { +export interface MockContext { /** * This is an array containing all arguments for each call. One item of the array is the arguments of that call. * @@ -208,7 +208,7 @@ Jest uses the latter for `MockInstance.mockImplementation` etc... and it allows const boolFn: Jest.Mock<() => boolean> = jest.fn<() => true>(() => true) */ /* eslint-disable ts/method-signature-style */ -export interface MockInstance { +export interface MockInstance extends Disposable { /** * Use it to return the name assigned to the mock with the `.mockName(name)` method. By default, it will return `vi.fn()`. * @see https://vitest.dev/api/mock#getmockname From 5c2606f7239c7a8eb0633524b6d54f9ecc342dd5 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 19 Mar 2026 14:37:13 +0100 Subject: [PATCH 5/6] fix: have a separate file for disposable --- packages/vitest/optional-runtime-types.d.ts | 6 ++++++ packages/vitest/optional-types.d.ts | 5 ----- packages/vitest/package.json | 3 +++ packages/vitest/rollup.config.js | 1 + packages/vitest/src/integrations/vi.ts | 2 +- 5 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 packages/vitest/optional-runtime-types.d.ts diff --git a/packages/vitest/optional-runtime-types.d.ts b/packages/vitest/optional-runtime-types.d.ts new file mode 100644 index 000000000000..ba94336682fd --- /dev/null +++ b/packages/vitest/optional-runtime-types.d.ts @@ -0,0 +1,6 @@ +/* eslint-disable ts/ban-ts-comment */ + +export interface Disposable { + // @ts-ignore -- Symbol.dispose might not be in user types + [Symbol.dispose]: () => void +} diff --git a/packages/vitest/optional-types.d.ts b/packages/vitest/optional-types.d.ts index e53497aa2d05..49cdb4fae820 100644 --- a/packages/vitest/optional-types.d.ts +++ b/packages/vitest/optional-types.d.ts @@ -5,8 +5,3 @@ export type * as jsdomTypes from 'jsdom' // @ts-ignore optional peer dep export type * as happyDomTypes from 'happy-dom' - -export interface Disposable { - // @ts-ignore -- Symbol.dispose might not be in user types - [Symbol.dispose]: () => void -} diff --git a/packages/vitest/package.json b/packages/vitest/package.json index fd6b7d53e42f..0205a64a1cbf 100644 --- a/packages/vitest/package.json +++ b/packages/vitest/package.json @@ -48,6 +48,9 @@ "./optional-types.js": { "types": "./optional-types.d.ts" }, + "./optional-runtime-types.js": { + "types": "./optional-runtime-types.d.ts" + }, "./src/*": "./src/*", "./globals": { "types": "./globals.d.ts" diff --git a/packages/vitest/rollup.config.js b/packages/vitest/rollup.config.js index c0a0cf549d02..1766f7a66a38 100644 --- a/packages/vitest/rollup.config.js +++ b/packages/vitest/rollup.config.js @@ -75,6 +75,7 @@ const external = [ 'node:console', 'node:events', 'inspector', + 'vitest/optional-runtime-types.js', 'vitest/optional-types.js', 'vitest/browser', 'vite/module-runner', diff --git a/packages/vitest/src/integrations/vi.ts b/packages/vitest/src/integrations/vi.ts index f97140da17de..4b1822223f5a 100644 --- a/packages/vitest/src/integrations/vi.ts +++ b/packages/vitest/src/integrations/vi.ts @@ -6,7 +6,7 @@ import type { MaybePartiallyMockedDeep, MockInstance, } from '@vitest/spy' -import type { Disposable } from 'vitest/optional-types.js' +import type { Disposable } from 'vitest/optional-runtime-types.js' import type { RuntimeOptions, SerializedConfig } from '../runtime/config' import type { VitestMocker } from '../runtime/moduleRunner/moduleMocker' import type { MockFactoryWithHelper, MockOptions } from '../types/mocker' From 49baffcb5ea850ae8ff4799dc64c9987ba874a88 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 20 Mar 2026 12:15:41 +0100 Subject: [PATCH 6/6] test: add dts tests --- pnpm-lock.yaml | 20 ++++++++++++++++++- .../package.json | 0 .../src/runner.ts | 3 +++ .../tsconfig.json | 17 ++++++++++++++++ .../exact-optional-property-node/package.json | 12 +++++++++++ .../src/reporter.ts | 2 +- .../tsconfig.json | 1 + test/cli/dts/no-dispose/package.json | 12 +++++++++++ test/cli/dts/no-dispose/src/vitest.ts | 4 ++++ test/cli/dts/no-dispose/tsconfig.json | 17 ++++++++++++++++ 10 files changed, 86 insertions(+), 2 deletions(-) rename test/cli/dts/{exact-optional-property => exact-optional-property-no-node}/package.json (100%) rename test/cli/dts/{exact-optional-property => exact-optional-property-no-node}/src/runner.ts (60%) create mode 100644 test/cli/dts/exact-optional-property-no-node/tsconfig.json create mode 100644 test/cli/dts/exact-optional-property-node/package.json rename test/cli/dts/{exact-optional-property => exact-optional-property-node}/src/reporter.ts (82%) rename test/cli/dts/{exact-optional-property => exact-optional-property-node}/tsconfig.json (95%) create mode 100644 test/cli/dts/no-dispose/package.json create mode 100644 test/cli/dts/no-dispose/src/vitest.ts create mode 100644 test/cli/dts/no-dispose/tsconfig.json diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a907a964e130..1d44e98c9965 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1351,7 +1351,16 @@ importers: specifier: workspace:* version: link:../../../../packages/vitest - test/cli/dts/exact-optional-property: + test/cli/dts/exact-optional-property-no-node: + devDependencies: + '@vitest/runner': + specifier: workspace:* + version: link:../../../../packages/runner + vitest: + specifier: workspace:* + version: link:../../../../packages/vitest + + test/cli/dts/exact-optional-property-node: devDependencies: '@vitest/runner': specifier: workspace:* @@ -1366,6 +1375,15 @@ importers: specifier: workspace:* version: link:../../../../packages/vitest + test/cli/dts/no-dispose: + devDependencies: + '@vitest/runner': + specifier: workspace:* + version: link:../../../../packages/runner + vitest: + specifier: workspace:* + version: link:../../../../packages/vitest + test/config: devDependencies: '@test/test-dep-config': diff --git a/test/cli/dts/exact-optional-property/package.json b/test/cli/dts/exact-optional-property-no-node/package.json similarity index 100% rename from test/cli/dts/exact-optional-property/package.json rename to test/cli/dts/exact-optional-property-no-node/package.json diff --git a/test/cli/dts/exact-optional-property/src/runner.ts b/test/cli/dts/exact-optional-property-no-node/src/runner.ts similarity index 60% rename from test/cli/dts/exact-optional-property/src/runner.ts rename to test/cli/dts/exact-optional-property-no-node/src/runner.ts index da41cb84a9f5..a48f41075b04 100644 --- a/test/cli/dts/exact-optional-property/src/runner.ts +++ b/test/cli/dts/exact-optional-property-no-node/src/runner.ts @@ -1,4 +1,7 @@ import type { VitestRunner } from '@vitest/runner' +import type { FailureScreenshotArtifact } from '@vitest/runner/types' import { VitestTestRunner } from 'vitest/runners' +type _Override = FailureScreenshotArtifact + export class MyRunner extends VitestTestRunner implements VitestRunner {} diff --git a/test/cli/dts/exact-optional-property-no-node/tsconfig.json b/test/cli/dts/exact-optional-property-no-node/tsconfig.json new file mode 100644 index 000000000000..9e1ce6d9410d --- /dev/null +++ b/test/cli/dts/exact-optional-property-no-node/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": ["../_shared/tsconfig.patch.json"], + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "types": [], + "strict": true, + "exactOptionalPropertyTypes": true, + "declaration": true, + "declarationMap": true, + "outDir": "dist", + "sourceMap": true, + "verbatimModuleSyntax": true, + "skipLibCheck": false + } +} diff --git a/test/cli/dts/exact-optional-property-node/package.json b/test/cli/dts/exact-optional-property-node/package.json new file mode 100644 index 000000000000..51c24416b172 --- /dev/null +++ b/test/cli/dts/exact-optional-property-node/package.json @@ -0,0 +1,12 @@ +{ + "name": "@vitest/test-integration-dts-exact-optional-property", + "type": "module", + "private": true, + "scripts": { + "test": "tsc -b" + }, + "devDependencies": { + "@vitest/runner": "workspace:*", + "vitest": "workspace:*" + } +} diff --git a/test/cli/dts/exact-optional-property/src/reporter.ts b/test/cli/dts/exact-optional-property-node/src/reporter.ts similarity index 82% rename from test/cli/dts/exact-optional-property/src/reporter.ts rename to test/cli/dts/exact-optional-property-node/src/reporter.ts index a0b76ae951ec..758bc40a4353 100644 --- a/test/cli/dts/exact-optional-property/src/reporter.ts +++ b/test/cli/dts/exact-optional-property-node/src/reporter.ts @@ -1,5 +1,5 @@ import type { File } from '@vitest/runner' -import { DefaultReporter } from 'vitest/reporters' +import { DefaultReporter } from 'vitest/node' export class MyReporter extends DefaultReporter { override reportTestSummary(files: File[], errors: unknown[], leakCount: number): void { diff --git a/test/cli/dts/exact-optional-property/tsconfig.json b/test/cli/dts/exact-optional-property-node/tsconfig.json similarity index 95% rename from test/cli/dts/exact-optional-property/tsconfig.json rename to test/cli/dts/exact-optional-property-node/tsconfig.json index f41906c5b6a2..004e6a8ea87e 100644 --- a/test/cli/dts/exact-optional-property/tsconfig.json +++ b/test/cli/dts/exact-optional-property-node/tsconfig.json @@ -4,6 +4,7 @@ "target": "ESNext", "module": "ESNext", "moduleResolution": "Bundler", + "types": [], "strict": true, "exactOptionalPropertyTypes": true, "declaration": true, diff --git a/test/cli/dts/no-dispose/package.json b/test/cli/dts/no-dispose/package.json new file mode 100644 index 000000000000..51c24416b172 --- /dev/null +++ b/test/cli/dts/no-dispose/package.json @@ -0,0 +1,12 @@ +{ + "name": "@vitest/test-integration-dts-exact-optional-property", + "type": "module", + "private": true, + "scripts": { + "test": "tsc -b" + }, + "devDependencies": { + "@vitest/runner": "workspace:*", + "vitest": "workspace:*" + } +} diff --git a/test/cli/dts/no-dispose/src/vitest.ts b/test/cli/dts/no-dispose/src/vitest.ts new file mode 100644 index 000000000000..d308614f9ec4 --- /dev/null +++ b/test/cli/dts/no-dispose/src/vitest.ts @@ -0,0 +1,4 @@ +import { vi } from 'vitest' + +const _fn = vi.fn() +const _mock = vi.doMock('./vitest.ts') diff --git a/test/cli/dts/no-dispose/tsconfig.json b/test/cli/dts/no-dispose/tsconfig.json new file mode 100644 index 000000000000..4d6e3973862c --- /dev/null +++ b/test/cli/dts/no-dispose/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": ["../_shared/tsconfig.patch.json"], + "compilerOptions": { + "target": "es2020", + "lib": ["DOM", "ES2020"], + "module": "node16", + "types": [], + "strict": true, + "declaration": true, + "declarationMap": true, + "outDir": "dist", + "sourceMap": true, + "verbatimModuleSyntax": true, + "skipLibCheck": false + }, + "include": ["src/*.ts"] +}