Skip to content

Commit b8140fc

Browse files
zmullettZac Mullettsheremet-va
authoredJan 4, 2024
fix(browser): resolved failure to find arbitrarily-named snapshot files when using expect(...).toMatchFileSnapshot() matcher. (#4839)
Co-authored-by: Zac Mullett <zac@Zacs-Air.modem> Co-authored-by: Vladimir <sleuths.slews0s@icloud.com>
1 parent 345a25d commit b8140fc

File tree

10 files changed

+61
-15
lines changed

10 files changed

+61
-15
lines changed
 

‎packages/snapshot/src/manager.ts

-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import type { SnapshotResult, SnapshotStateOptions, SnapshotSummary } from './ty
33

44
export class SnapshotManager {
55
summary: SnapshotSummary = undefined!
6-
resolvedPaths = new Set<string>()
76
extension = '.snap'
87

98
constructor(public options: Omit<SnapshotStateOptions, 'snapshotEnvironment'>) {
@@ -30,7 +29,6 @@ export class SnapshotManager {
3029
})
3130

3231
const path = resolver(testPath, this.extension)
33-
this.resolvedPaths.add(path)
3432
return path
3533
}
3634

‎packages/vitest/src/api/setup.ts

+17-8
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,27 @@ import { createBirpc } from 'birpc'
66
import { parse, stringify } from 'flatted'
77
import type { WebSocket } from 'ws'
88
import { WebSocketServer } from 'ws'
9+
import { isFileServingAllowed } from 'vite'
910
import type { ViteDevServer } from 'vite'
1011
import type { StackTraceParserOptions } from '@vitest/utils/source-map'
1112
import { API_PATH } from '../constants'
1213
import type { Vitest } from '../node'
1314
import type { File, ModuleGraphData, Reporter, TaskResultPack, UserConsoleLog } from '../types'
14-
import { getModuleGraph, isPrimitive } from '../utils'
15+
import { getModuleGraph, isPrimitive, stringifyReplace } from '../utils'
1516
import type { WorkspaceProject } from '../node/workspace'
1617
import { parseErrorStacktrace } from '../utils/source-map'
1718
import type { TransformResultWithSource, WebSocketEvents, WebSocketHandlers } from './types'
1819

19-
export function setup(vitestOrWorkspace: Vitest | WorkspaceProject, server?: ViteDevServer) {
20+
export function setup(vitestOrWorkspace: Vitest | WorkspaceProject, _server?: ViteDevServer) {
2021
const ctx = 'ctx' in vitestOrWorkspace ? vitestOrWorkspace.ctx : vitestOrWorkspace
2122

2223
const wss = new WebSocketServer({ noServer: true })
2324

2425
const clients = new Map<WebSocket, BirpcReturn<WebSocketEvents, WebSocketHandlers>>()
2526

26-
;(server || ctx.server).httpServer?.on('upgrade', (request, socket, head) => {
27+
const server = _server || ctx.server
28+
29+
server.httpServer?.on('upgrade', (request, socket, head) => {
2730
if (!request.url)
2831
return
2932

@@ -37,6 +40,11 @@ export function setup(vitestOrWorkspace: Vitest | WorkspaceProject, server?: Vit
3740
})
3841
})
3942

43+
function checkFileAccess(path: string) {
44+
if (!isFileServingAllowed(path, server))
45+
throw new Error(`Access denied to "${path}". See Vite config documentation for "server.fs": https://vitejs.dev/config/server-options.html#server-fs-strict.`)
46+
}
47+
4048
function setupClient(ws: WebSocket) {
4149
const rpc = createBirpc<WebSocketEvents, WebSocketHandlers>(
4250
{
@@ -73,7 +81,8 @@ export function setup(vitestOrWorkspace: Vitest | WorkspaceProject, server?: Vit
7381
return ctx.snapshot.resolveRawPath(testPath, rawPath)
7482
},
7583
async readSnapshotFile(snapshotPath) {
76-
if (!ctx.snapshot.resolvedPaths.has(snapshotPath) || !existsSync(snapshotPath))
84+
checkFileAccess(snapshotPath)
85+
if (!existsSync(snapshotPath))
7786
return null
7887
return fs.readFile(snapshotPath, 'utf-8')
7988
},
@@ -88,13 +97,13 @@ export function setup(vitestOrWorkspace: Vitest | WorkspaceProject, server?: Vit
8897
return fs.writeFile(id, content, 'utf-8')
8998
},
9099
async saveSnapshotFile(id, content) {
91-
if (!ctx.snapshot.resolvedPaths.has(id))
92-
throw new Error(`Snapshot file "${id}" does not exist.`)
100+
checkFileAccess(id)
93101
await fs.mkdir(dirname(id), { recursive: true })
94102
return fs.writeFile(id, content, 'utf-8')
95103
},
96104
async removeSnapshotFile(id) {
97-
if (!ctx.snapshot.resolvedPaths.has(id) || !existsSync(id))
105+
checkFileAccess(id)
106+
if (!existsSync(id))
98107
throw new Error(`Snapshot file "${id}" does not exist.`)
99108
return fs.unlink(id)
100109
},
@@ -140,7 +149,7 @@ export function setup(vitestOrWorkspace: Vitest | WorkspaceProject, server?: Vit
140149
post: msg => ws.send(msg),
141150
on: fn => ws.on('message', fn),
142151
eventNames: ['onUserConsoleLog', 'onFinished', 'onCollected', 'onCancel'],
143-
serialize: stringify,
152+
serialize: (data: any) => stringify(data, stringifyReplace),
144153
deserialize: parse,
145154
},
146155
)

‎packages/vitest/src/utils/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export * from './global'
99
export * from './timers'
1010
export * from './env'
1111
export * from './modules'
12+
export * from './serialization'
1213

1314
export const isWindows = isNode && process.platform === 'win32'
1415
export function getRunMode() {
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Serialization support utils.
2+
3+
function cloneByOwnProperties(value: any) {
4+
// Clones the value's properties into a new Object. The simpler approach of
5+
// Object.assign() won't work in the case that properties are not enumerable.
6+
return Object.getOwnPropertyNames(value)
7+
.reduce((clone, prop) => ({
8+
...clone,
9+
[prop]: value[prop],
10+
}), {})
11+
}
12+
13+
/**
14+
* Replacer function for serialization methods such as JS.stringify() or
15+
* flatted.stringify().
16+
*/
17+
export function stringifyReplace(key: string, value: any) {
18+
if (value instanceof Error)
19+
return cloneByOwnProperties(value)
20+
else
21+
return value
22+
}

‎test/benchmark/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"type": "module",
44
"private": true,
55
"scripts": {
6-
"test": "node --test specs/ && echo '1'",
6+
"test": "node --test specs/* && echo '1'",
77
"bench:json": "vitest bench --reporter=json",
88
"bench": "vitest bench"
99
},

‎test/browser/specs/runner.test.mjs

+6-2
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ const passedTests = getPassed(browserResultJson.testResults)
2828
const failedTests = getFailed(browserResultJson.testResults)
2929

3030
await test('tests are actually running', async () => {
31-
assert.ok(browserResultJson.testResults.length === 9, 'Not all the tests have been run')
31+
assert.ok(browserResultJson.testResults.length === 10, 'Not all the tests have been run')
3232
assert.ok(passedTests.length === 8, 'Some tests failed')
33-
assert.ok(failedTests.length === 1, 'Some tests have passed but should fail')
33+
assert.ok(failedTests.length === 2, 'Some tests have passed but should fail')
3434

3535
assert.doesNotMatch(stderr, /Unhandled Error/, 'doesn\'t have any unhandled errors')
3636
})
@@ -80,3 +80,7 @@ await test('popup apis should log a warning', () => {
8080
assert.ok(stderr.includes('Vitest encountered a \`confirm\("test"\)\`'), 'prints warning for confirm')
8181
assert.ok(stderr.includes('Vitest encountered a \`prompt\("test"\)\`'), 'prints warning for prompt')
8282
})
83+
84+
await test('snapshot inaccessible file debuggability', () => {
85+
assert.ok(stdout.includes('Access denied to "/inaccesible/path".'), 'file security enforcement explained')
86+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
my snapshot content
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

3-
exports[`file snapshot 1`] = `1`;
3+
exports[`snapshot 1`] = `1`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { expect, test } from 'vitest'
2+
3+
test('file snapshot', async () => {
4+
await expect('inaccessible snapshot content')
5+
.toMatchFileSnapshot('/inaccesible/path')
6+
})

‎test/browser/test/snapshot.test.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ test('inline snapshot', () => {
44
expect(1).toMatchInlineSnapshot('1')
55
})
66

7-
test('file snapshot', () => {
7+
test('snapshot', () => {
88
expect(1).toMatchSnapshot()
99
})
10+
11+
test('file snapshot', async () => {
12+
await expect('my snapshot content')
13+
.toMatchFileSnapshot('./__snapshots__/custom/my_snapshot')
14+
})

0 commit comments

Comments
 (0)
Please sign in to comment.