diff --git a/.gitignore b/.gitignore index a684fea2c..29669c1b7 100644 --- a/.gitignore +++ b/.gitignore @@ -79,4 +79,3 @@ dist # IDE .idea -.vscode diff --git a/.vscode/settings.json b/.vscode/settings.json index c14110ca5..4ca507008 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,6 @@ { - "cSpell.words": ["Listr"], + "cSpell.words": [ + "unmock" + ], "editor.formatOnSave": false } diff --git a/package.json b/package.json index 05bfdec53..669eee625 100644 --- a/package.json +++ b/package.json @@ -43,11 +43,11 @@ "rollup-plugin-dts": "^4.0.1", "rollup-plugin-esbuild": "^4.7.2", "rollup-plugin-license": "^2.6.0", - "tinyspy": "^0.2.1", + "tinyspy": "^0.2.4", "typescript": "^4.5.4", "vite": "^2.7.4", "vitepress": "^0.20.9", "vitest": "workspace:*", "vue": "3.2.26" } -} +} \ No newline at end of file diff --git a/packages/vitest/LICENSE.md b/packages/vitest/LICENSE.md index dc6affcbc..ee5a2c066 100644 --- a/packages/vitest/LICENSE.md +++ b/packages/vitest/LICENSE.md @@ -1001,6 +1001,21 @@ Repository: sindresorhus/log-update --------------------------------------- +## magic-string +License: MIT +By: Rich Harris +Repository: https://github.com/rich-harris/magic-string + +> Copyright 2018 Rich Harris +> +> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +--------------------------------------- + ## merge-stream License: MIT By: Stephen Sugden @@ -1699,6 +1714,35 @@ Repository: 7rulnik/source-map --------------------------------------- +## sourcemap-codec +License: MIT +By: Rich Harris +Repository: https://github.com/Rich-Harris/sourcemap-codec + +> The MIT License +> +> Copyright (c) 2015 Rich Harris +> +> Permission is hereby granted, free of charge, to any person obtaining a copy +> of this software and associated documentation files (the "Software"), to deal +> in the Software without restriction, including without limitation the rights +> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +> copies of the Software, and to permit persons to whom the Software is +> furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in +> all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +> THE SOFTWARE. + +--------------------------------------- + ## string-width License: MIT By: Sindre Sorhus diff --git a/packages/vitest/package.json b/packages/vitest/package.json index 6fafeb1a9..42271ac0a 100644 --- a/packages/vitest/package.json +++ b/packages/vitest/package.json @@ -79,6 +79,7 @@ "happy-dom": "^2.25.0", "jsdom": "^19.0.0", "log-update": "^5.0.0", + "magic-string": "^0.25.7", "micromatch": "^4.0.4", "mlly": "^0.3.16", "nanoid": "^3.1.30", @@ -113,4 +114,4 @@ "engines": { "node": ">=14.14.0" } -} +} \ No newline at end of file diff --git a/packages/vitest/src/integrations/jest-mock.ts b/packages/vitest/src/integrations/jest-mock.ts index e97a166fc..1c8fab56a 100644 --- a/packages/vitest/src/integrations/jest-mock.ts +++ b/packages/vitest/src/integrations/jest-mock.ts @@ -48,6 +48,60 @@ export interface JestMockCompatFn ext (...args: TArgs): TReturns } +export type MockableFunction = (...args: Array) => any +export type MethodKeysOf = { + [K in keyof T]: T[K] extends MockableFunction ? K : never; +}[keyof T] +export type PropertyKeysOf = { + [K in keyof T]: T[K] extends MockableFunction ? never : K; +}[keyof T] + +export type ArgumentsOf = T extends (...args: infer A) => any ? A : never + +export type ConstructorArgumentsOf = T extends new (...args: infer A) => any + ? A + : never +export type MaybeMockedConstructor = T extends new ( + ...args: Array +) => infer R + ? JestMockCompatFn, R> + : T +export type MockedFunction = MockWithArgs & { + [K in keyof T]: T[K]; +} +export type MockedFunctionDeep = MockWithArgs & +MockedObjectDeep +export type MockedObject = MaybeMockedConstructor & { + [K in MethodKeysOf]: T[K] extends MockableFunction + ? MockedFunction + : T[K]; +} & {[K in PropertyKeysOf]: T[K]} +export type MockedObjectDeep = MaybeMockedConstructor & { + [K in MethodKeysOf]: T[K] extends MockableFunction + ? MockedFunctionDeep + : T[K]; +} & {[K in PropertyKeysOf]: MaybeMockedDeep} + +export type MaybeMockedDeep = T extends MockableFunction + ? MockedFunctionDeep + : T extends object + ? MockedObjectDeep + : T + +export type MaybeMocked = T extends MockableFunction + ? MockedFunction + : T extends object + ? MockedObject + : T + +export interface MockWithArgs + extends JestMockCompatFn, ReturnType> { + new (...args: ConstructorArgumentsOf): T + (...args: ArgumentsOf): ReturnType +} + +export const spies = new Set() + export function spyOn( obj: T, method: K, @@ -116,14 +170,14 @@ function enhanceSpy( stub.mockReset = () => { stub.reset() + implementation = () => undefined as unknown as TReturns + onceImplementations = [] return stub } stub.mockRestore = () => { + stub.mockReset() implementation = undefined - onceImplementations = [] - stub.reset() - ;(stub as unknown as SpyImpl).restore() return stub } @@ -156,7 +210,7 @@ function enhanceSpy( stub.mockImplementation(() => Promise.reject(val)) stub.mockRejectedValueOnce = (val: unknown) => - stub.mockImplementation(() => Promise.reject(val)) + stub.mockImplementationOnce(() => Promise.reject(val)) util.addProperty(stub, 'mock', () => mockContext) @@ -166,6 +220,8 @@ function enhanceSpy( return impl.apply(this, args) }) + spies.add(stub) + return stub as any } diff --git a/packages/vitest/src/integrations/utils.ts b/packages/vitest/src/integrations/utils.ts index d97a1cf7e..66b19d247 100644 --- a/packages/vitest/src/integrations/utils.ts +++ b/packages/vitest/src/integrations/utils.ts @@ -1,6 +1,8 @@ -import { spies } from 'tinyspy' +/* eslint-disable @typescript-eslint/no-unused-vars */ + import { FakeTimers } from './timers' -import { fn, spyOn } from './jest-mock' +import type { MaybeMocked, MaybeMockedDeep } from './jest-mock' +import { fn, spies, spyOn } from './jest-mock' class VitestUtils { private _timers: FakeTimers @@ -43,7 +45,25 @@ class VitestUtils { spyOn = spyOn fn = fn - mock = (path: string) => path + + // just hints for transformer to rewrite imports + public mock(path: string) {} + public unmock(path: string) {} + + public async importActual(path: string): Promise { + return {} as T + } + + public async importMock(path: string): Promise { + return {} as T + } + + // the typings test helper + public mocked(item: T, deep?: false): MaybeMocked; + public mocked(item: T, deep: true): MaybeMockedDeep; + public mocked(item: T, _deep = false): MaybeMocked | MaybeMockedDeep { + return item as any + } public isMockFunction(fn: any) { return typeof fn === 'function' @@ -52,21 +72,24 @@ class VitestUtils { } public clearAllMocks() { - spies.forEach((spy) => { - spy.reset() - }) + // @ts-expect-error clearing module mocks + __vitest__clearMocks__({ clearMocks: true }) + spies.forEach(spy => spy.mockClear()) + return this } public resetAllMocks() { - spies.forEach((spy) => { - spy.reset() - }) + // @ts-expect-error reseting module mocks + __vitest__clearMocks__({ mockReset: true }) + spies.forEach(spy => spy.mockReset()) + return this } public restoreAllMocks() { - spies.forEach((spy) => { - spy.restore() - }) + // @ts-expect-error restoring module mocks + __vitest__clearMocks__({ restoreMocks: true }) + spies.forEach(spy => spy.mockRestore()) + return this } } diff --git a/packages/vitest/src/node/config.ts b/packages/vitest/src/node/config.ts index 4c7b6c0cc..666ad756d 100644 --- a/packages/vitest/src/node/config.ts +++ b/packages/vitest/src/node/config.ts @@ -23,6 +23,10 @@ export function resolveConfig( resolved.threads = resolved.threads ?? true resolved.interpretDefault = resolved.interpretDefault ?? true + resolved.clearMocks = resolved.clearMocks ?? false + resolved.restoreMocks = resolved.restoreMocks ?? false + resolved.mockReset = resolved.mockReset ?? false + resolved.include = resolved.include ?? defaultInclude resolved.exclude = resolved.exclude ?? defaultExclude diff --git a/packages/vitest/src/node/execute.ts b/packages/vitest/src/node/execute.ts index 70500ae2e..66d2a44a4 100644 --- a/packages/vitest/src/node/execute.ts +++ b/packages/vitest/src/node/execute.ts @@ -1,13 +1,21 @@ import { builtinModules, createRequire } from 'module' import { fileURLToPath, pathToFileURL } from 'url' +import { existsSync, readdirSync } from 'fs' import vm from 'vm' -import { dirname, resolve } from 'pathe' +import { basename, dirname, resolve } from 'pathe' import { isValidNodeImport } from 'mlly' import type { ModuleCache } from '../types' -import { slash } from '../utils' +import { mergeSlashes, slash } from '../utils' +import { spies, spyOn } from '../integrations/jest-mock' export type FetchFunction = (id: string) => Promise +interface SuiteMocks { + [suitePath: string]: { + [originalPath: string]: string | null + } +} + export interface ExecuteOptions { root: string files: string[] @@ -16,6 +24,7 @@ export interface ExecuteOptions { inline: (string | RegExp)[] external: (string | RegExp)[] moduleCache: Map + mockMap: SuiteMocks } const defaultInline = [ @@ -68,8 +77,43 @@ export async function interpretedImport(path: string, interpretDefault: boolean) return mod } +function resolveMockPath(mockPath: string, root: string, nmName: string | null) { + // it's a node_module alias + // all mocks should be inside /__mocks__ + if (nmName) { + const mockFolder = resolve(root, '__mocks__') + const files = readdirSync(mockFolder) + + for (const file of files) { + const [basename] = file.split('.') + if (basename === nmName) + return resolve(mockFolder, file).replace(root, '') + } + + return null + } + + const dir = dirname(mockPath) + const baseId = basename(mockPath) + const fullPath = resolve(dir, '__mocks__', baseId) + return existsSync(fullPath) ? fullPath.replace(root, '') : null +} + +// TODO https://jestjs.io/docs/jest-object#jestcreatemockfrommodulemodulename +function mockObject(obj: any) { + const newObj = { ...obj } + // eslint-disable-next-line no-restricted-syntax + for (const k in obj) { + newObj[k] = obj[k] + + if (typeof obj[k] === 'function' && !obj[k].__isSpy) + spyOn(newObj, k) + } + return newObj +} + export async function executeInViteNode(options: ExecuteOptions) { - const { moduleCache, root, files, fetch } = options + const { moduleCache, root, files, fetch, mockMap } = options const externalCache = new Map() builtinModules.forEach(m => externalCache.set(m, true)) @@ -79,9 +123,57 @@ export async function executeInViteNode(options: ExecuteOptions) { result.push(await cachedRequest(`/@fs/${slash(resolve(file))}`, [])) return result + function getSuiteFilepath() { + return process.__vitest_worker__?.filepath + } + + function getActualPath(path: string, nmName: string) { + return nmName ? mergeSlashes(`/@fs/${path}`) : path.replace(root, '') + } + + function unmockPath(path: string, nmName: string) { + const suitefile = getSuiteFilepath() + + if (suitefile) { + const fsPath = getActualPath(path, nmName) + mockMap[suitefile] ??= {} + delete mockMap[suitefile][fsPath] + } + } + + function mockPath(path: string, nmName: string) { + const suitefile = getSuiteFilepath() + + if (suitefile) { + const mockPath = resolveMockPath(path, root, nmName) + const fsPath = getActualPath(path, nmName) + mockMap[suitefile] ??= {} + mockMap[suitefile][fsPath] = mockPath + } + } + + function clearMocks({ clearMocks, mockReset, restoreMocks }: { clearMocks: boolean; mockReset: boolean; restoreMocks: boolean}) { + if (!clearMocks && !mockReset && !restoreMocks) + return + + spies.forEach((s) => { + if (restoreMocks) + s.mockRestore() + else if (mockReset) + s.mockReset() + else if (clearMocks) + s.mockClear() + }) + } + async function directRequest(id: string, fsPath: string, callstack: string[]) { callstack = [...callstack, id] - const request = async(dep: string) => { + const suite = getSuiteFilepath() + const request = async(dep: string, canMock = true) => { + const mocks = mockMap[suite || ''] || {} + const mock = mocks[dep] + if (mock && canMock) + dep = mock if (callstack.includes(dep)) { const cacheKey = toFilePath(dep, root) if (!moduleCache.get(cacheKey)?.exports) @@ -114,6 +206,20 @@ export async function executeInViteNode(options: ExecuteOptions) { return exports.default }, } + + const importActual = (path: string, nmName: string) => { + return request(getActualPath(path, nmName), false) + } + + const importMock = async(path: string, nmName: string) => { + const mockPath = resolveMockPath(path, root, nmName) + if (mockPath === null) { + const exports = await request(getActualPath(path, nmName), false) + return mockObject(exports) + } + return request(mockPath, true) + } + const context = { // esm transformed by Vite __vite_ssr_import__: request, @@ -121,6 +227,16 @@ export async function executeInViteNode(options: ExecuteOptions) { __vite_ssr_exports__: exports, __vite_ssr_exportAll__: (obj: any) => exportAll(exports, obj), __vite_ssr_import_meta__: { url }, + + // vitest.mock API + __vitest__mock__: mockPath, + __vitest__unmock__: unmockPath, + __vitest__importActual__: importActual, + __vitest__importMock__: importMock, + // spies from 'jest-mock' are different inside suites and execute, + // so wee need to call this twice - inside suite and here + __vitest__clearMocks__: clearMocks, + // cjs compact require: createRequire(url), exports, @@ -135,6 +251,12 @@ export async function executeInViteNode(options: ExecuteOptions) { }) await fn(...Object.values(context)) + const mocks = suite ? mockMap[suite] : null + if (mocks) { + if (mocks[id] === null) + exportAll(exports, mockObject(exports)) + } + return exports } diff --git a/packages/vitest/src/node/index.ts b/packages/vitest/src/node/index.ts index 7aecfc1dc..101c4c227 100644 --- a/packages/vitest/src/node/index.ts +++ b/packages/vitest/src/node/index.ts @@ -10,6 +10,7 @@ import { SnapshotManager } from '../integrations/snapshot/manager' import { configFiles, defaultPort } from '../constants' import { hasFailed, noop, slash, toArray } from '../utils' import { ConsoleReporter } from '../reporters/console' +import { MocksPlugin } from '../plugins/mock' import type { WorkerPool } from './pool' import { StateManager } from './state' import { resolveConfig } from './config' @@ -286,6 +287,7 @@ export async function createVitest(options: UserConfig, viteOverrides: ViteUserC server.middlewares.use((await import('../api/middleware')).default(ctx)) }, } as VitePlugin, + MocksPlugin(), ], server: { open: options.open, diff --git a/packages/vitest/src/plugins/mock.ts b/packages/vitest/src/plugins/mock.ts new file mode 100644 index 000000000..f39d0e547 --- /dev/null +++ b/packages/vitest/src/plugins/mock.ts @@ -0,0 +1,49 @@ +import type { Plugin } from 'vite' +import MagicString from 'magic-string' + +const mockRegexp = /\b(?:vitest|vi)\s*.\s*(mock|unmock|importActual|importMock)\(["`'\s](.*[@\w_-]+)["`'\s]\);?/mg + +export const MocksPlugin = (): Plugin => { + return { + name: 'vitest:mock-plugin', + enforce: 'post', + async transform(code, id) { + let m: MagicString | undefined + const matchAll = Array.from(code.matchAll(mockRegexp)) + + if (!matchAll.length) + return + + for (const match of matchAll) { + const [line, method, modulePath] = match + const filepath = await this.resolve(modulePath, id) + if (filepath) { + m ??= new MagicString(code) + const start = match.index || 0 + const end = start + line.length + + let nodeModule = 'null' + if (filepath.id.includes('/node_modules/')) + nodeModule = `"${modulePath}"` + + const overwrite = `__vitest__${method}__("${filepath.id}", ${nodeModule});` + + if (method === 'mock') { + m.prepend(`${overwrite}\n\n`) + m.remove(start, end) + } + else { + m.overwrite(start, end, overwrite) + } + } + } + + if (m) { + return { + code: m.toString(), + map: m.generateMap(), + } + } + }, + } +} diff --git a/packages/vitest/src/runtime/entry.ts b/packages/vitest/src/runtime/entry.ts index e96348a2e..770c2fc67 100644 --- a/packages/vitest/src/runtime/entry.ts +++ b/packages/vitest/src/runtime/entry.ts @@ -1,5 +1,6 @@ import { promises as fs } from 'fs' import type { BuiltinEnvironment, ResolvedConfig } from '../types' +import { vi } from '../integrations/utils' import { setupGlobalEnv, withEnv } from './setup' import { startTests } from './run' @@ -14,8 +15,14 @@ export async function run(files: string[], config: ResolvedConfig): Promise { await startTests([file], config) }) + + process.__vitest_worker__.filepath = undefined } } diff --git a/packages/vitest/src/runtime/run.ts b/packages/vitest/src/runtime/run.ts index 7e2d95fa5..ccf46a066 100644 --- a/packages/vitest/src/runtime/run.ts +++ b/packages/vitest/src/runtime/run.ts @@ -2,7 +2,7 @@ import { performance } from 'perf_hooks' import type { HookListener } from 'vitest' import type { ResolvedConfig, Suite, SuiteHooks, Task, Test } from '../types' import { getSnapshotClient } from '../integrations/snapshot/chai' -import { hasFailed, hasTests, partitionSuiteChildren } from '../utils' +import { clearModuleMocks, hasFailed, hasTests, partitionSuiteChildren } from '../utils' import { getFn, getHooks } from './map' import { rpc, send } from './rpc' import { collectTests } from './collect' @@ -32,6 +32,8 @@ export async function runTest(test: Test) { } updateTask(test) + clearModuleMocks() + getSnapshotClient().setTest(test) process.__vitest_worker__.current = test diff --git a/packages/vitest/src/runtime/suite.ts b/packages/vitest/src/runtime/suite.ts index 1726db224..b3c7bae79 100644 --- a/packages/vitest/src/runtime/suite.ts +++ b/packages/vitest/src/runtime/suite.ts @@ -148,6 +148,7 @@ declare global { rpc: RpcCall send: RpcSend current?: Test + filepath?: string moduleCache: Map } } diff --git a/packages/vitest/src/runtime/worker.ts b/packages/vitest/src/runtime/worker.ts index e8d3c2dc9..9250b058b 100644 --- a/packages/vitest/src/runtime/worker.ts +++ b/packages/vitest/src/runtime/worker.ts @@ -10,6 +10,7 @@ let _viteNode: { collect: (files: string[], config: ResolvedConfig) => Promise } const moduleCache: Map = new Map() +const mockMap = {} async function startViteNode(ctx: WorkerContext) { if (_viteNode) @@ -40,6 +41,7 @@ async function startViteNode(ctx: WorkerContext) { external: config.depsExternal, interpretDefault: config.interpretDefault, moduleCache, + mockMap, }))[0] _viteNode = { run, collect } diff --git a/packages/vitest/src/types/config.ts b/packages/vitest/src/types/config.ts index 26d9cabc3..09d1ed607 100644 --- a/packages/vitest/src/types/config.ts +++ b/packages/vitest/src/types/config.ts @@ -154,6 +154,24 @@ export interface InlineConfig { * @default false */ api?: boolean | number + + /** + * Will call `.mockClear()` on all spies before each test + * @default false + */ + clearMocks?: boolean + + /** + * Will call `.mockReset()` on all spies before each test + * @default false + */ + mockReset?: boolean + + /** + * Will call `.mockRestore()` on all spies before each test + * @default false + */ + restoreMocks?: boolean } export interface UserConfig extends InlineConfig { diff --git a/packages/vitest/src/utils.ts b/packages/vitest/src/utils.ts index e2dd979aa..fadae908b 100644 --- a/packages/vitest/src/utils.ts +++ b/packages/vitest/src/utils.ts @@ -1,6 +1,7 @@ import c from 'picocolors' import { isPackageExists } from 'local-pkg' import { resolve } from 'pathe' +import { vi } from './integrations/utils' import type { Arrayable, Nullable, Suite, Task, Test } from './types' /** @@ -23,6 +24,10 @@ export function slash(str: string) { return str.replace(/\\/g, '/') } +export function mergeSlashes(str: string) { + return str.replace(/\/\//g, '/') +} + export const noop = () => {} /** @@ -129,4 +134,16 @@ export async function ensurePackageInstalled( return false } +export function clearModuleMocks() { + const { clearMocks, mockReset, restoreMocks } = process.__vitest_worker__.config + + // since each function calls another, we can just call one + if (restoreMocks) + vi.restoreAllMocks() + else if (mockReset) + vi.resetAllMocks() + else if (clearMocks) + vi.clearAllMocks() +} + export { resolve as resolvePath } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 26d4927fd..9f5d4f202 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,7 +24,7 @@ importers: rollup-plugin-dts: ^4.0.1 rollup-plugin-esbuild: ^4.7.2 rollup-plugin-license: ^2.6.0 - tinyspy: ^0.2.1 + tinyspy: ^0.2.4 typescript: ^4.5.4 vite: ^2.7.4 vitepress: ^0.20.9 @@ -51,7 +51,7 @@ importers: rollup-plugin-dts: 4.0.1_typescript@4.5.4 rollup-plugin-esbuild: 4.7.2 rollup-plugin-license: 2.6.0 - tinyspy: 0.2.1 + tinyspy: 0.2.4 typescript: 4.5.4 vite: 2.7.4 vitepress: 0.20.9 @@ -136,6 +136,7 @@ importers: jsdom: ^19.0.0 local-pkg: ^0.4.0 log-update: ^5.0.0 + magic-string: ^0.25.7 micromatch: ^4.0.4 mlly: ^0.3.16 nanoid: ^3.1.30 @@ -177,6 +178,7 @@ importers: happy-dom: 2.25.0 jsdom: 19.0.0 log-update: 5.0.0 + magic-string: 0.25.7 micromatch: 4.0.4 mlly: 0.3.16 nanoid: 3.1.30 @@ -234,6 +236,19 @@ importers: vite: 2.7.3 vitest: link:../../packages/vitest + test/mocks: + specifiers: + axios: ^0.24.0 + tinyspy: ^0.2.4 + vite: ^2.7.2 + vitest: workspace:* + dependencies: + axios: 0.24.0 + tinyspy: 0.2.4 + devDependencies: + vite: 2.7.3 + vitest: link:../../packages/vitest + test/puppeteer: specifiers: puppeteer: ^13.0.0 @@ -1351,27 +1366,6 @@ packages: react: 17.0.2 dev: false - /@mui/styled-engine/5.2.4_922a85da57e3646a57465b7970b0de85: - resolution: {integrity: sha512-tlilkVsAR+MeWIPILzj1uGjiy5tKONZgblXY49LECUNF7u7aTDszqmv0hRG+1IAZjNts+ox8XAlldPNfj+OKvA==} - engines: {node: '>=12.0.0'} - peerDependencies: - '@emotion/react': ^11.4.1 - '@emotion/styled': ^11.3.0 - react: ^17.0.2 - peerDependenciesMeta: - '@emotion/react': - optional: true - '@emotion/styled': - optional: true - dependencies: - '@babel/runtime': 7.16.3 - '@emotion/cache': 11.7.1 - '@emotion/react': 11.7.1_react@17.0.2 - '@emotion/styled': 11.6.0_79c1490562c3c65ef55eb132a37e39b4 - prop-types: 15.7.2 - react: 17.0.2 - dev: false - /@mui/styled-engine/5.2.5_922a85da57e3646a57465b7970b0de85: resolution: {integrity: sha512-vNEB2SXCetXKLeMZ49SZLWeiX8uQeLI/jk/dzqnYdGJx8Eq98hCfTucDfJwt2RUhSYGH/3BET2pZqT3w8aOTzQ==} engines: {node: '>=12.0.0'} @@ -1413,7 +1407,7 @@ packages: '@emotion/react': 11.7.1_react@17.0.2 '@emotion/styled': 11.6.0_79c1490562c3c65ef55eb132a37e39b4 '@mui/private-theming': 5.2.3_react@17.0.2 - '@mui/styled-engine': 5.2.4_922a85da57e3646a57465b7970b0de85 + '@mui/styled-engine': 5.2.5_922a85da57e3646a57465b7970b0de85 '@mui/types': 7.1.0 '@mui/utils': 5.2.3_react@17.0.2 clsx: 1.1.1 @@ -2583,6 +2577,14 @@ packages: hasBin: true dev: true + /axios/0.24.0: + resolution: {integrity: sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==} + dependencies: + follow-redirects: 1.14.6 + transitivePeerDependencies: + - debug + dev: false + /babel-plugin-macros/2.8.0: resolution: {integrity: sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==} dependencies: @@ -4217,6 +4219,16 @@ packages: resolution: {integrity: sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==} dev: true + /follow-redirects/1.14.6: + resolution: {integrity: sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + /foreground-child/2.0.0: resolution: {integrity: sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==} engines: {node: '>=8.0.0'} @@ -6555,15 +6567,9 @@ packages: engines: {node: '>=14.0.0'} dev: false - /tinyspy/0.2.1: - resolution: {integrity: sha512-5iI2IXXrXVm2vipLylJpQjBGUoA992ir3ZFnRb/pPYZX8AHXr4a3XevUO1+611Utiqr0/EYnxhGU3Fux5vwfJQ==} - engines: {node: '>=14.0.0'} - dev: true - /tinyspy/0.2.4: resolution: {integrity: sha512-A6C0zYMhY2cECXJwJfgesG1jbEjz/d9Enuzx5cC1qiSrGp6eZDMtO8Dxl0Nrn47wR9abEtGr2ckQi52OoJ7eiQ==} engines: {node: '>=14.0.0'} - dev: false /to-fast-properties/2.0.0: resolution: {integrity: sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=} diff --git a/test/core/src/__mocks__/submodule.ts b/test/core/src/__mocks__/submodule.ts new file mode 100644 index 000000000..ab86b0314 --- /dev/null +++ b/test/core/src/__mocks__/submodule.ts @@ -0,0 +1 @@ +export const two = 1 + 1 + 1 diff --git a/test/core/src/mockedA.ts b/test/core/src/mockedA.ts new file mode 100644 index 000000000..5846afbb3 --- /dev/null +++ b/test/core/src/mockedA.ts @@ -0,0 +1,3 @@ +export function mockedA() { + return 'A' +} diff --git a/test/core/src/mockedB.ts b/test/core/src/mockedB.ts new file mode 100644 index 000000000..9c9b90b20 --- /dev/null +++ b/test/core/src/mockedB.ts @@ -0,0 +1,5 @@ +import { mockedA } from './mockedA' + +export function mockedB() { + return mockedA() +} diff --git a/test/core/test/mocked-no-mocks-same.test.ts b/test/core/test/mocked-no-mocks-same.test.ts new file mode 100644 index 000000000..2db34c1b1 --- /dev/null +++ b/test/core/test/mocked-no-mocks-same.test.ts @@ -0,0 +1,10 @@ +import { expect, test, vi } from 'vitest' +import { mockedA } from '../src/mockedA' + +vi.mock('../src/mockedA.ts') + +test('testing mocking module without __mocks__ - suites dont conflict', () => { + mockedA() + + expect(mockedA).toHaveBeenCalledTimes(1) +}) diff --git a/test/core/test/mocked-no-mocks.test.ts b/test/core/test/mocked-no-mocks.test.ts new file mode 100644 index 000000000..e27fd68d8 --- /dev/null +++ b/test/core/test/mocked-no-mocks.test.ts @@ -0,0 +1,18 @@ +import { expect, test, vi } from 'vitest' +import { mockedA } from '../src/mockedA' +import { mockedB } from '../src/mockedB' + +vi.mock('../src/mockedA.ts') +vi.mock('../src/mockedB.ts') + +test('testing mocking module without __mocks__', () => { + mockedA() + expect(mockedA).toHaveBeenCalledTimes(1) +}) + +test('mocking several modules work', () => { + mockedB() + + expect(mockedA).toHaveBeenCalledTimes(2) + expect(mockedB).toHaveBeenCalledTimes(1) +}) diff --git a/test/core/test/mocked.test.ts b/test/core/test/mocked.test.ts new file mode 100644 index 000000000..7b834c2ff --- /dev/null +++ b/test/core/test/mocked.test.ts @@ -0,0 +1,19 @@ +import { assert, expect, test, vi, vitest } from 'vitest' +import { two } from '../src/submodule' +import * as mocked from '../src/mockedA' +import { mockedB } from '../src/mockedB' + +vitest.mock('../src/submodule') + +test('submodule is mocked to return "two" as 3', () => { + assert.equal(3, two) +}) + +test('can mock esm', () => { + const spy = vi.spyOn(mocked, 'mockedA') + + mockedB() + + expect(spy).toHaveBeenCalled() + expect(spy).toHaveReturnedWith('A') +}) diff --git a/test/mocks/__mocks__/axios.ts b/test/mocks/__mocks__/axios.ts new file mode 100644 index 000000000..2a7fc7eda --- /dev/null +++ b/test/mocks/__mocks__/axios.ts @@ -0,0 +1,5 @@ +import { vi } from 'vitest' + +export default { + get: vi.fn(), +} diff --git a/test/mocks/package.json b/test/mocks/package.json new file mode 100644 index 000000000..2174c6923 --- /dev/null +++ b/test/mocks/package.json @@ -0,0 +1,21 @@ +{ + "name": "@vitest/test-lit", + "private": true, + "files": [ + "dist", + "types" + ], + "scripts": { + "coverage": "vitest --coverage", + "dev": "vite", + "test": "vitest" + }, + "dependencies": { + "axios": "^0.24.0", + "tinyspy": "^0.2.4" + }, + "devDependencies": { + "vite": "^2.7.2", + "vitest": "workspace:*" + } +} \ No newline at end of file diff --git a/test/mocks/src/example.ts b/test/mocks/src/example.ts new file mode 100644 index 000000000..0e55cec2c --- /dev/null +++ b/test/mocks/src/example.ts @@ -0,0 +1,27 @@ +export function square(a: number, b: number) { + return a * b +} +export async function asyncSquare(a: number, b: number) { + const result = (await a) * b + return result +} +export const someClasss = new (class Bar { + public array: number[] + constructor() { + this.array = [1, 2, 3] + } + + foo() {} +})() +export const object = { + baz: 'foo', + bar: { + fiz: 1, + buzz: [1, 2, 3], + }, +} +export const array = [1, 2, 3] +export const number = 123 +export const string = 'baz' +export const boolean = true +export const symbol = Symbol.for('a.b.c') diff --git a/test/mocks/test/axios-not-mocked.test.ts b/test/mocks/test/axios-not-mocked.test.ts new file mode 100644 index 000000000..a7e35e037 --- /dev/null +++ b/test/mocks/test/axios-not-mocked.test.ts @@ -0,0 +1,13 @@ +import axios from 'axios' + +test('mocked axios', async() => { + const { default: ax } = await vi.importMock('axios') + + await ax.get('string') + + expect(ax.get).toHaveBeenCalledWith('string') +}) + +test('actual axios is not mocked', async() => { + expect(vi.isMockFunction(axios.get)).toBe(false) +}) diff --git a/test/mocks/test/axios.test.ts b/test/mocks/test/axios.test.ts new file mode 100644 index 000000000..fe2cec6f2 --- /dev/null +++ b/test/mocks/test/axios.test.ts @@ -0,0 +1,16 @@ +import axios from 'axios' + +vi.mock('axios') + +test('mocked axios', async() => { + await axios.get('string') + + expect(axios.get).toHaveBeenCalledWith('string') + expect(axios.post).toBeUndefined() +}) + +test('can get actual axios', async() => { + const ax = await vi.importActual('axios') + + expect(vi.isMockFunction(ax.get)).toBe(false) +}) diff --git a/test/mocks/test/example-mock.test.ts b/test/mocks/test/example-mock.test.ts new file mode 100644 index 000000000..69f5f0d80 --- /dev/null +++ b/test/mocks/test/example-mock.test.ts @@ -0,0 +1,37 @@ +import type * as exampleModule from '../src/example' + +// TODO +test.skip('all mocked are valid', async() => { + const example = await vi.importMock('../src/example') + + // creates a new mocked function with no formal arguments. + // expect(example.square.name).toEqual('square') + // expect(example.square.length).toEqual(0) + + // async functions get the same treatment as standard synchronous functions. + // expect(example.asyncSquare.name).toEqual('asyncSquare') + // expect(example.asyncSquare.length).toEqual(0) + + // creates a new class with the same interface, member functions and properties are mocked. + // expect(example.someClasss.constructor.name).toEqual('Bar') + // expect(example.someClasss.foo.name).toEqual('foo') + // expect(example.someClasss.array.length).toEqual(0) + + // creates a deeply cloned version of the original object. + expect(example.object).toEqual({ + baz: 'foo', + bar: { + fiz: 1, + buzz: [], + }, + }) + + // creates a new empty array, ignoring the original array. + expect(example.array.length).toEqual(0) + + // creates a new property with the same primitive value as the original property. + expect(example.number).toEqual(123) + expect(example.string).toEqual('baz') + expect(example.boolean).toEqual(true) + expect(example.symbol).toEqual(Symbol.for('a.b.c')) +}) diff --git a/test/mocks/test/tinyspy.test.ts b/test/mocks/test/tinyspy.test.ts new file mode 100644 index 000000000..e7c724650 --- /dev/null +++ b/test/mocks/test/tinyspy.test.ts @@ -0,0 +1,8 @@ +import type * as tinyspyModule from 'tinyspy' + +test('tinyspy is not mocked with __mocks__, but automatically mocked', async() => { + const tinyspy = await vi.importMock('tinyspy') + + expect(vi.isMockFunction(tinyspy.spyOn)).toBe(true) + expect(vi.isMockFunction(tinyspy.spy)).toBe(true) +}) diff --git a/test/mocks/vite.config.ts b/test/mocks/vite.config.ts new file mode 100644 index 000000000..7bca79c61 --- /dev/null +++ b/test/mocks/vite.config.ts @@ -0,0 +1,11 @@ +/// + +import { defineConfig } from 'vite' + +// https://vitejs.dev/config/ +export default defineConfig({ + test: { + global: true, + environment: 'node', + }, +})