-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
snapshot.js
290 lines (246 loc) · 7.91 KB
/
snapshot.js
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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
/**
* This file contains helper methods to create expected snapshot structures
* of both instance and ES6 exports.
*
* The files are located here and not under /test or /tools so it's transpiled
* into ES5 code under /lib and can be used straight by node.js
*/
import assert from 'assert'
import * as allIsFunctions from './is.js'
import { create } from '../core/create.js'
import { endsWith } from './string.js'
export const validateTypeOf = allIsFunctions.typeOf
export function validateBundle (expectedBundleStructure, bundle) {
const originalWarn = console.warn
console.warn = function (...args) {
if (args.join(' ').includes('is moved to') && args.join(' ').includes('Please use the new location instead')) {
// Ignore warnings like:
// Warning: math.type.isNumber is moved to math.isNumber in v6.0.0. Please use the new location instead.
return
}
originalWarn.apply(console, args)
}
try {
const issues = []
// see whether all expected functions and objects are there
traverse(expectedBundleStructure, (expectedType, path) => {
const actualValue = get(bundle, path)
const actualType = validateTypeOf(actualValue)
const message = (actualType === 'undefined')
? 'Missing entry in bundle. ' +
`Path: ${JSON.stringify(path)}, expected type: ${expectedType}, actual type: ${actualType}`
: 'Unexpected entry type in bundle. ' +
`Path: ${JSON.stringify(path)}, expected type: ${expectedType}, actual type: ${actualType}`
if (actualType !== expectedType) {
issues.push({ actualType, expectedType, message })
console.warn(message)
}
})
// see whether there are any functions or objects that shouldn't be there
traverse(bundle, (actualValue, path) => {
const actualType = validateTypeOf(actualValue)
const expectedType = get(expectedBundleStructure, path) || 'undefined'
// FIXME: ugly to have these special cases
if (path.join('.').includes('docs.')) {
// ignore the contents of docs
return
}
if (path.join('.').includes('all.')) {
// ignore the contents of all dependencies
return
}
const message = (expectedType === 'undefined')
? 'Unknown entry in bundle. ' +
'Is there a new function added which is missing in this snapshot test? ' +
`Path: ${JSON.stringify(path)}, expected type: ${expectedType}, actual type: ${actualType}`
: 'Unexpected entry type in bundle. ' +
`Path: ${JSON.stringify(path)}, expected type: ${expectedType}, actual type: ${actualType}`
if (actualType !== expectedType) {
issues.push({ actualType, expectedType, message })
console.warn(message)
}
})
// assert on the first issue (if any)
if (issues.length > 0) {
const { actualType, expectedType, message } = issues[0]
console.warn(`${issues.length} bundle issues found`)
assert.strictEqual(actualType, expectedType, message)
}
} finally {
console.warn = originalWarn
}
}
/**
* Based on an object with factory functions, create the expected
* structures for ES6 export and a mathjs instance.
* @param {Object} factories
* @return {{expectedInstanceStructure: Object, expectedES6Structure: Object}}
*/
export function createSnapshotFromFactories (factories) {
const math = create(factories)
const allFactoryFunctions = {}
const allFunctionsConstantsClasses = {}
const allFunctionsConstants = {}
const allTransformFunctions = {}
const allDependencyCollections = {}
const allClasses = {}
const allNodeClasses = {}
Object.keys(factories).forEach(factoryName => {
const factory = factories[factoryName]
const name = factory.fn
const isTransformFunction = factory.meta && factory.meta.isTransformFunction
const isClass = !isLowerCase(name[0]) && (validateTypeOf(math[name]) === 'function')
const dependenciesName = factory.fn +
(isTransformFunction ? 'Transform' : '') +
'Dependencies'
allFactoryFunctions[factoryName] = 'function'
allFunctionsConstantsClasses[name] = validateTypeOf(math[name])
allDependencyCollections[dependenciesName] = 'Object'
if (isTransformFunction) {
allTransformFunctions[name] = 'function'
}
if (isClass) {
if (endsWith(name, 'Node')) {
allNodeClasses[name] = 'function'
} else {
allClasses[name] = 'function'
}
} else {
allFunctionsConstants[name] = validateTypeOf(math[name])
}
})
let embeddedDocs = {}
Object.keys(factories).forEach(factoryName => {
const factory = factories[factoryName]
const name = factory.fn
if (isLowerCase(factory.fn[0])) { // ignore class names starting with upper case
embeddedDocs[name] = 'Object'
}
})
embeddedDocs = exclude(embeddedDocs, [
'equalScalar',
'apply',
'addScalar',
'subtractScalar',
'multiplyScalar',
'print',
'divideScalar',
'parse',
'compile',
'parser',
'chain',
'reviver',
'replacer'
])
const allTypeChecks = {}
Object.keys(allIsFunctions).forEach(name => {
if (name.indexOf('is') === 0) {
allTypeChecks[name] = 'function'
}
})
const allErrorClasses = {
ArgumentsError: 'function',
DimensionError: 'function',
IndexError: 'function'
}
const expectedInstanceStructure = {
...allFunctionsConstantsClasses,
on: 'function',
off: 'function',
once: 'function',
emit: 'function',
import: 'function',
config: 'function',
create: 'function',
factory: 'function',
...allTypeChecks,
...allErrorClasses,
expression: {
transform: {
...allTransformFunctions
},
mathWithTransform: {
// note that we don't have classes here,
// only functions and constants are allowed in the editor
...exclude(allFunctionsConstants, [
'chain'
]),
config: 'function'
}
}
}
const expectedES6Structure = {
// functions
...exclude(allFunctionsConstantsClasses, [
'E',
'false',
'Infinity',
'NaN',
'null',
'PI',
'true'
]),
create: 'function',
config: 'function',
factory: 'function',
_true: 'boolean',
_false: 'boolean',
_null: 'null',
_Infinity: 'number',
_NaN: 'number',
...allTypeChecks,
...allErrorClasses,
...allDependencyCollections,
...allFactoryFunctions,
docs: embeddedDocs
}
return {
expectedInstanceStructure,
expectedES6Structure
}
}
function traverse (obj, callback = (value, path) => {}, path = []) {
// FIXME: ugly to have these special cases
if (path.length > 0 && path[0].includes('Dependencies')) {
// special case for objects holding a collection of dependencies
callback(obj, path)
} else if (validateTypeOf(obj) === 'Array') {
obj.map((item, index) => traverse(item, callback, path.concat(index)))
} else if (validateTypeOf(obj) === 'Object') {
Object.keys(obj).forEach(key => {
// FIXME: ugly to have these special cases
// ignore special case of deprecated docs
if (key === 'docs' && path.join('.') === 'expression') {
return
}
traverse(obj[key], callback, path.concat(key))
})
} else {
callback(obj, path)
}
}
function get (object, path) {
let child = object
for (let i = 0; i < path.length; i++) {
const key = path[i]
child = child ? child[key] : undefined
}
return child
}
/**
* Create a copy of the provided `object` and delete
* all properties listed in `excludedProperties`
* @param {Object} object
* @param {string[]} excludedProperties
* @return {Object}
*/
function exclude (object, excludedProperties) {
const strippedObject = Object.assign({}, object)
excludedProperties.forEach(excludedProperty => {
delete strippedObject[excludedProperty]
})
return strippedObject
}
function isLowerCase (text) {
return typeof text === 'string' && text.toLowerCase() === text
}