diff --git a/docs/guide/mocking.md b/docs/guide/mocking.md index b8d9bb3bee68..fb841dc839ed 100644 --- a/docs/guide/mocking.md +++ b/docs/guide/mocking.md @@ -383,11 +383,11 @@ module.exports = fs.promises ::: ```ts -// hello-world.js +// read-hello-world.js import { readFileSync } from 'node:fs' export function readHelloWorld(path) { - return readFileSync('./hello-world.txt') + return readFileSync(path) } ``` @@ -395,7 +395,7 @@ export function readHelloWorld(path) { // hello-world.test.js import { beforeEach, expect, it, vi } from 'vitest' import { fs, vol } from 'memfs' -import { readHelloWorld } from './hello-world.js' +import { readHelloWorld } from './read-hello-world.js' // tell vitest to use fs mock from __mocks__ folder // this can be done in a setup file if fs should always be mocked @@ -408,7 +408,7 @@ beforeEach(() => { }) it('should return correct text', () => { - const path = './hello-world.txt' + const path = '/hello-world.txt' fs.writeFileSync(path, 'hello world') const text = readHelloWorld(path) @@ -423,7 +423,7 @@ it('can return a value multiple times', () => { './dir2/hw.txt': 'hello dir2', }, // default cwd - '/tmp' + '/tmp', ) expect(readHelloWorld('/tmp/dir1/hw.txt')).toBe('hello dir1') diff --git a/packages/vitest/src/runtime/mocker.ts b/packages/vitest/src/runtime/mocker.ts index 75dd253f9f23..dd87c9b1ef96 100644 --- a/packages/vitest/src/runtime/mocker.ts +++ b/packages/vitest/src/runtime/mocker.ts @@ -188,7 +188,7 @@ export class VitestMocker { return { id, fsPath, - external, + external: external ? this.normalizePath(external) : external, } } @@ -315,14 +315,28 @@ export class VitestMocker { const files = readdirSync(mockFolder) const baseOriginal = basename(path) - for (const file of files) { - const baseFile = basename(file, extname(file)) - if (baseFile === baseOriginal) { - return resolve(mockFolder, file) + function findFile(files: string[], baseOriginal: string): string | null { + for (const file of files) { + const baseFile = basename(file, extname(file)) + if (baseFile === baseOriginal) { + const path = resolve(mockFolder, file) + // if the same name, return the file + if (fs.statSync(path).isFile()) { + return path + } + else { + // find folder/index.{js,ts} + const indexFile = findFile(readdirSync(path), 'index') + if (indexFile) { + return indexFile + } + } + } } + return null } - return null + return findFile(files, baseOriginal) } const dir = dirname(path) @@ -517,7 +531,7 @@ export class VitestMocker { const mocks = this.mockMap.get(suitefile) || {} const resolves = this.resolveCache.get(suitefile) || {} - mocks[id] = factory || this.resolveMockPath(path, external) + mocks[id] = factory || this.resolveMockPath(id, external) resolves[id] = originalId this.mockMap.set(suitefile, mocks) @@ -546,7 +560,7 @@ export class VitestMocker { let mock = this.getDependencyMock(normalizedId) if (mock === undefined) { - mock = this.resolveMockPath(fsPath, external) + mock = this.resolveMockPath(normalizedId, external) } if (mock === null) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2aacb66e3b45..0c72c8d0cc36 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1173,6 +1173,9 @@ importers: immutable: specifier: 5.0.0-beta.5 version: 5.0.0-beta.5 + memfs: + specifier: ^4.8.2 + version: 4.8.2 strip-ansi: specifier: ^7.1.0 version: 7.1.0 @@ -1434,12 +1437,18 @@ packages: peerDependencies: '@algolia/client-search': '>= 4.9.1 < 6' algoliasearch: '>= 4.9.1 < 6' + peerDependenciesMeta: + '@algolia/client-search': + optional: true '@algolia/autocomplete-shared@1.9.3': resolution: {integrity: sha512-Wnm9E4Ye6Rl6sTTqjoymD+l8DjSTHsHboVRYrKgEt8Q7UHm9nYbqhN/i0fhUYA3OAEH7WA8x3jfpnmJm3rKvaQ==} peerDependencies: '@algolia/client-search': '>= 4.9.1 < 6' algoliasearch: '>= 4.9.1 < 6' + peerDependenciesMeta: + '@algolia/client-search': + optional: true '@algolia/cache-browser-local-storage@4.20.0': resolution: {integrity: sha512-uujahcBt4DxduBTvYdwO3sBfHuJvJokiC3BP1+O70fglmE1ShkH8lpXqZBac1rrU3FnNYSUs4pL9lBdTKeRPOQ==} @@ -6785,6 +6794,10 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} + memfs@4.8.2: + resolution: {integrity: sha512-j4WKth315edViMBGkHW6NTF0QBjsTrcRDmYNcGsPq+ozMEyCCCIlX2d2mJ5wuh6iHvJ3FevUrr48v58YRqVdYg==} + engines: {node: '>= 4.0.0'} + merge-anything@5.1.7: resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==} engines: {node: '>=12.13'} @@ -9240,13 +9253,15 @@ snapshots: '@algolia/autocomplete-preset-algolia@1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)': dependencies: '@algolia/autocomplete-shared': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0) - '@algolia/client-search': 4.20.0 algoliasearch: 4.20.0 + optionalDependencies: + '@algolia/client-search': 4.20.0 '@algolia/autocomplete-shared@1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)': dependencies: - '@algolia/client-search': 4.20.0 algoliasearch: 4.20.0 + optionalDependencies: + '@algolia/client-search': 4.20.0 '@algolia/cache-browser-local-storage@4.20.0': dependencies: @@ -15602,6 +15617,10 @@ snapshots: media-typer@0.3.0: {} + memfs@4.8.2: + dependencies: + tslib: 2.6.2 + merge-anything@5.1.7: dependencies: is-what: 4.1.8 diff --git a/test/core/__mocks__/fs.cjs b/test/core/__mocks__/fs.cjs new file mode 100644 index 000000000000..1d1562604b44 --- /dev/null +++ b/test/core/__mocks__/fs.cjs @@ -0,0 +1,6 @@ +// we can also use `import`, but then +// every export should be explicitly defined + +const { fs } = require('memfs') + +module.exports = fs diff --git a/test/core/__mocks__/fs/promises.cjs b/test/core/__mocks__/fs/promises.cjs new file mode 100644 index 000000000000..9fa31bcf52b0 --- /dev/null +++ b/test/core/__mocks__/fs/promises.cjs @@ -0,0 +1,6 @@ +// we can also use `import`, but then +// every export should be explicitly defined + +const { fs } = require('memfs') + +module.exports = fs.promises diff --git a/test/core/package.json b/test/core/package.json index 2439a810a807..6a04ff535722 100644 --- a/test/core/package.json +++ b/test/core/package.json @@ -23,6 +23,7 @@ "axios": "^0.26.1", "debug": "^4.3.4", "immutable": "5.0.0-beta.5", + "memfs": "^4.8.2", "strip-ansi": "^7.1.0", "sweetalert2": "^11.6.16", "tinyrainbow": "^1.2.0", diff --git a/test/core/src/read-hello-world.ts b/test/core/src/read-hello-world.ts new file mode 100644 index 000000000000..5eeb77427b69 --- /dev/null +++ b/test/core/src/read-hello-world.ts @@ -0,0 +1,6 @@ +// hello-world.js +import { readFileSync } from 'node:fs' + +export function readHelloWorld(path: string) { + return readFileSync(path, 'utf-8') +} diff --git a/test/core/test/file-path.test.ts b/test/core/test/file-path.test.ts index e0fb611f6160..022835826b5a 100644 --- a/test/core/test/file-path.test.ts +++ b/test/core/test/file-path.test.ts @@ -2,7 +2,11 @@ import { existsSync } from 'node:fs' import { describe, expect, it, vi } from 'vitest' import { isWindows, slash, toFilePath } from '../../../packages/vite-node/src/utils' -vi.mock('fs') +vi.mock('fs', () => { + return { + existsSync: vi.fn(), + } +}) describe('current url', () => { it('__filename is equal to import.meta.url', () => { diff --git a/test/core/test/mock-fs.test.ts b/test/core/test/mock-fs.test.ts new file mode 100644 index 000000000000..4d834133ea93 --- /dev/null +++ b/test/core/test/mock-fs.test.ts @@ -0,0 +1,37 @@ +// hello-world.test.js +import { beforeEach, expect, it, vi } from 'vitest' +import { fs, vol } from 'memfs' +import { readHelloWorld } from '../src/read-hello-world' + +// tell vitest to use fs mock from __mocks__ folder +// this can be done in a setup file if fs should always be mocked +vi.mock('node:fs') +vi.mock('node:fs/promises') + +beforeEach(() => { + // reset the state of in-memory fs + vol.reset() +}) + +it('should return correct text', () => { + const path = '/hello-world.txt' + fs.writeFileSync(path, 'hello world') + + const text = readHelloWorld(path) + expect(text).toBe('hello world') +}) + +it('can return a value multiple times', () => { + // you can use vol.fromJSON to define several files + vol.fromJSON( + { + './dir1/hw.txt': 'hello dir1', + './dir2/hw.txt': 'hello dir2', + }, + // default cwd + '/tmp', + ) + + expect(readHelloWorld('/tmp/dir1/hw.txt')).toBe('hello dir1') + expect(readHelloWorld('/tmp/dir2/hw.txt')).toBe('hello dir2') +})