/
fixture.ts
141 lines (120 loc) · 3.62 KB
/
fixture.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
import type { TestContext } from './types'
export interface FixtureItem {
prop: string
value: any
index: number
/**
* Indicates whether the fixture is a function
*/
isFn: boolean
/**
* The dependencies(fixtures) of current fixture function.
*/
deps?: FixtureItem[]
}
export function mergeContextFixtures(fixtures: Record<string, any>, context: { fixtures?: FixtureItem[] } = {}) {
const fixtureArray: FixtureItem[] = Object.entries(fixtures)
.map(([prop, value], index) => {
const isFn = typeof value === 'function'
return {
prop,
value,
index,
isFn,
}
})
if (Array.isArray(context.fixtures))
context.fixtures = context.fixtures.concat(fixtureArray)
else
context.fixtures = fixtureArray
// Update dependencies of fixture functions
fixtureArray.forEach((fixture) => {
if (fixture.isFn) {
const usedProps = getUsedProps(fixture.value)
if (usedProps.length)
fixture.deps = context.fixtures!.filter(({ index, prop }) => index !== fixture.index && usedProps.includes(prop))
}
})
return context
}
export function withFixtures(fn: Function, fixtures: FixtureItem[], context: TestContext & Record<string, any>) {
if (!fixtures.length)
return () => fn(context)
const usedProps = getUsedProps(fn)
if (!usedProps.length)
return () => fn(context)
const usedFixtures = fixtures.filter(({ prop }) => usedProps.includes(prop))
const pendingFixtures = resolveDeps(usedFixtures)
let cursor = 0
async function use(fixtureValue: any) {
const { prop } = pendingFixtures[cursor++]
context[prop] = fixtureValue
if (cursor < pendingFixtures.length)
await next()
else await fn(context)
}
async function next() {
const { value } = pendingFixtures[cursor]
typeof value === 'function' ? await value(context, use) : await use(value)
}
return () => next()
}
function resolveDeps(fixtures: FixtureItem[], depSet = new Set<FixtureItem>(), pendingFixtures: FixtureItem[] = []) {
fixtures.forEach((fixture) => {
if (pendingFixtures.includes(fixture))
return
if (!fixture.isFn || !fixture.deps) {
pendingFixtures.push(fixture)
return
}
if (depSet.has(fixture))
throw new Error('circular fixture dependency')
depSet.add(fixture)
resolveDeps(fixture.deps, depSet, pendingFixtures)
pendingFixtures.push(fixture)
depSet.clear()
})
return pendingFixtures
}
function getUsedProps(fn: Function) {
const match = fn.toString().match(/[^(]*\(([^)]*)/)
if (!match)
return []
const args = splitByComma(match[1])
if (!args.length)
return []
const first = args[0]
if (!(first.startsWith('{') && first.endsWith('}')))
throw new Error('the first argument must use object destructuring pattern')
const _first = first.slice(1, -1).replace(/\s/g, '')
const props = splitByComma(_first).map((prop) => {
return prop.replace(/\:.*|\=.*/g, '')
})
const last = props.at(-1)
if (last && last.startsWith('...'))
throw new Error('Rest parameters are not supported')
return props
}
function splitByComma(s: string) {
const result = []
const stack = []
let start = 0
for (let i = 0; i < s.length; i++) {
if (s[i] === '{' || s[i] === '[') {
stack.push(s[i] === '{' ? '}' : ']')
}
else if (s[i] === stack[stack.length - 1]) {
stack.pop()
}
else if (!stack.length && s[i] === ',') {
const token = s.substring(start, i).trim()
if (token)
result.push(token)
start = i + 1
}
}
const lastToken = s.substring(start).trim()
if (lastToken)
result.push(lastToken)
return result
}