1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 | import assert from 'assert'
|
9 | import * as allIsFunctions from './is'
|
10 | import { create } from '../core/create'
|
11 | import { endsWith } from './string'
|
12 |
|
13 | export function validateBundle (expectedBundleStructure, bundle) {
|
14 | const originalWarn = console.warn
|
15 |
|
16 | console.warn = function (...args) {
|
17 | if (args.join(' ').indexOf('is moved to') !== -1 && args.join(' ').indexOf('Please use the new location instead') !== -1) {
|
18 |
|
19 |
|
20 | return
|
21 | }
|
22 |
|
23 | originalWarn.apply(console, args)
|
24 | }
|
25 |
|
26 | try {
|
27 | const issues = []
|
28 |
|
29 |
|
30 | traverse(expectedBundleStructure, (expectedType, path) => {
|
31 | const actualValue = get(bundle, path)
|
32 | const actualType = validateTypeOf(actualValue)
|
33 |
|
34 | const message = (actualType === 'undefined')
|
35 | ? 'Missing entry in bundle. ' +
|
36 | `Path: ${JSON.stringify(path)}, expected type: ${expectedType}, actual type: ${actualType}`
|
37 | : 'Unexpected entry type in bundle. ' +
|
38 | `Path: ${JSON.stringify(path)}, expected type: ${expectedType}, actual type: ${actualType}`
|
39 |
|
40 | if (actualType !== expectedType) {
|
41 | issues.push({ actualType, expectedType, message })
|
42 |
|
43 | console.warn(message)
|
44 | }
|
45 | })
|
46 |
|
47 | // see whether there are any functions or objects that shouldn't be there
|
48 | traverse(bundle, (actualValue, path) => {
|
49 | const actualType = validateTypeOf(actualValue)
|
50 | const expectedType = get(expectedBundleStructure, path) || 'undefined'
|
51 |
|
52 | // FIXME: ugly to have these special cases
|
53 | if (path.join('.').indexOf('docs.') !== -1) {
|
54 | // ignore the contents of docs
|
55 | return
|
56 | }
|
57 | if (path.join('.').indexOf('all.') !== -1) {
|
58 | // ignore the contents of all dependencies
|
59 | return
|
60 | }
|
61 |
|
62 | const message = (expectedType === 'undefined')
|
63 | ? 'Unknown entry in bundle. ' +
|
64 | 'Is there a new function added which is missing in this snapshot test? ' +
|
65 | `Path: ${JSON.stringify(path)}, expected type: ${expectedType}, actual type: ${actualType}`
|
66 | : 'Unexpected entry type in bundle. ' +
|
67 | `Path: ${JSON.stringify(path)}, expected type: ${expectedType}, actual type: ${actualType}`
|
68 |
|
69 | if (actualType !== expectedType) {
|
70 | issues.push({ actualType, expectedType, message })
|
71 |
|
72 | console.warn(message)
|
73 | }
|
74 | })
|
75 |
|
76 | // assert on the first issue (if any)
|
77 | if (issues.length > 0) {
|
78 | const { actualType, expectedType, message } = issues[0]
|
79 |
|
80 | console.warn(`${issues.length} bundle issues found`)
|
81 |
|
82 | assert.strictEqual(actualType, expectedType, message)
|
83 | }
|
84 | } finally {
|
85 | console.warn = originalWarn
|
86 | }
|
87 | }
|
88 |
|
89 | /**
|
90 | * Based on an object with factory functions, create the expected
|
91 | * structures for ES6 export and a mathjs instance.
|
92 | * @param {Object} factories
|
93 | * @return {{expectedInstanceStructure: Object, expectedES6Structure: Object}}
|
94 | */
|
95 | export function createSnapshotFromFactories (factories) {
|
96 | const math = create(factories)
|
97 |
|
98 | const allFactoryFunctions = {}
|
99 | const allFunctionsConstantsClasses = {}
|
100 | const allFunctionsConstants = {}
|
101 | const allTransformFunctions = {}
|
102 | const allDependencyCollections = {}
|
103 | const allClasses = {}
|
104 | const allNodeClasses = {}
|
105 |
|
106 | Object.keys(factories).forEach(factoryName => {
|
107 | const factory = factories[factoryName]
|
108 | const name = factory.fn
|
109 | const isTransformFunction = factory.meta && factory.meta.isTransformFunction
|
110 | const isClass = !isLowerCase(name[0]) && (validateTypeOf(math[name]) === 'Function')
|
111 | const dependenciesName = factory.fn +
|
112 | (isTransformFunction ? 'Transform' : '') +
|
113 | 'Dependencies'
|
114 |
|
115 | allFactoryFunctions[factoryName] = 'Function'
|
116 | allFunctionsConstantsClasses[name] = validateTypeOf(math[name])
|
117 | allDependencyCollections[dependenciesName] = 'Object'
|
118 |
|
119 | if (isTransformFunction) {
|
120 | allTransformFunctions[name] = 'Function'
|
121 | }
|
122 |
|
123 | if (isClass) {
|
124 | if (endsWith(name, 'Node')) {
|
125 | allNodeClasses[name] = 'Function'
|
126 | } else {
|
127 | allClasses[name] = 'Function'
|
128 | }
|
129 | } else {
|
130 | allFunctionsConstants[name] = validateTypeOf(math[name])
|
131 | }
|
132 | })
|
133 |
|
134 | let embeddedDocs = {}
|
135 | Object.keys(factories).forEach(factoryName => {
|
136 | const factory = factories[factoryName]
|
137 | const name = factory.fn
|
138 |
|
139 | if (isLowerCase(factory.fn[0])) { // ignore class names starting with upper case
|
140 | embeddedDocs[name] = 'Object'
|
141 | }
|
142 | })
|
143 | embeddedDocs = exclude(embeddedDocs, [
|
144 | 'equalScalar',
|
145 | 'apply',
|
146 | 'addScalar',
|
147 | 'multiplyScalar',
|
148 | 'eye',
|
149 | 'print',
|
150 | 'divideScalar',
|
151 | 'parse',
|
152 | 'compile',
|
153 | 'parser',
|
154 | 'chain',
|
155 | 'reviver'
|
156 | ])
|
157 |
|
158 | const allTypeChecks = {}
|
159 | Object.keys(allIsFunctions).forEach(name => {
|
160 | if (name.indexOf('is') === 0) {
|
161 | allTypeChecks[name] = 'Function'
|
162 | }
|
163 | })
|
164 |
|
165 | const allErrorClasses = {
|
166 | ArgumentsError: 'Function',
|
167 | DimensionError: 'Function',
|
168 | IndexError: 'Function'
|
169 | }
|
170 |
|
171 | const expectedInstanceStructure = {
|
172 | ...allFunctionsConstantsClasses,
|
173 |
|
174 | on: 'Function',
|
175 | off: 'Function',
|
176 | once: 'Function',
|
177 | emit: 'Function',
|
178 | import: 'Function',
|
179 | var: 'Function',
|
180 | eval: 'Function',
|
181 | typeof: 'Function',
|
182 | config: 'Function',
|
183 | create: 'Function',
|
184 | factory: 'Function',
|
185 |
|
186 | ...allTypeChecks,
|
187 | ...allErrorClasses,
|
188 |
|
189 | expression: {
|
190 | transform: {
|
191 | ...allTransformFunctions
|
192 | },
|
193 | mathWithTransform: {
|
194 | // note that we don't have classes here,
|
195 | // only functions and constants are allowed in the editor
|
196 | ...exclude(allFunctionsConstants, [
|
197 | 'chain'
|
198 | ]),
|
199 | config: 'Function'
|
200 | },
|
201 | // deprecated stuff:
|
202 | // docs: embeddedDocs,
|
203 | node: {
|
204 | ...allNodeClasses
|
205 | },
|
206 | parse: 'Function',
|
207 | Parser: 'Function'
|
208 | },
|
209 |
|
210 | // deprecated stuff:
|
211 | type: {
|
212 | ...allTypeChecks,
|
213 | ...allClasses
|
214 | },
|
215 | json: {
|
216 | reviver: 'Function'
|
217 | },
|
218 | error: {
|
219 | ...allErrorClasses
|
220 | }
|
221 | }
|
222 |
|
223 | const expectedES6Structure = {
|
224 | // functions
|
225 | ...exclude(allFunctionsConstantsClasses, [
|
226 | 'typeof',
|
227 | 'eval',
|
228 | 'var',
|
229 | 'E',
|
230 | 'false',
|
231 | 'Infinity',
|
232 | 'NaN',
|
233 | 'null',
|
234 | 'PI',
|
235 | 'true'
|
236 | ]),
|
237 | create: 'Function',
|
238 | config: 'Function',
|
239 | factory: 'Function',
|
240 | deprecatedEval: 'Function',
|
241 | deprecatedImport: 'Function',
|
242 | deprecatedVar: 'Function',
|
243 | deprecatedTypeof: 'Function',
|
244 | _true: 'boolean',
|
245 | _false: 'boolean',
|
246 | _null: 'null',
|
247 | _Infinity: 'number',
|
248 | _NaN: 'number',
|
249 |
|
250 | ...allTypeChecks,
|
251 | ...allErrorClasses,
|
252 | ...allDependencyCollections,
|
253 | ...allFactoryFunctions,
|
254 |
|
255 | docs: embeddedDocs,
|
256 |
|
257 | // deprecated stuff:
|
258 | expression: {
|
259 | node: {
|
260 | ...allNodeClasses
|
261 | },
|
262 | parse: 'Function',
|
263 | Parser: 'Function'
|
264 | },
|
265 | type: {
|
266 | ...allTypeChecks,
|
267 | ...allClasses
|
268 | },
|
269 | json: {
|
270 | reviver: 'Function'
|
271 | },
|
272 | error: {
|
273 | ...allErrorClasses
|
274 | }
|
275 | }
|
276 |
|
277 | return {
|
278 | expectedInstanceStructure,
|
279 | expectedES6Structure
|
280 | }
|
281 | }
|
282 |
|
283 | export function validateTypeOf (x) {
|
284 | if (x && x.type === 'Unit') {
|
285 | return 'Unit'
|
286 | }
|
287 |
|
288 | if (x && x.type === 'Complex') {
|
289 | return 'Complex'
|
290 | }
|
291 |
|
292 | if (Array.isArray(x)) {
|
293 | return 'Array'
|
294 | }
|
295 |
|
296 | if (x === null) {
|
297 | return 'null'
|
298 | }
|
299 |
|
300 | if (typeof x === 'function') {
|
301 | return 'Function'
|
302 | }
|
303 |
|
304 | if (typeof x === 'object') {
|
305 | return 'Object'
|
306 | }
|
307 |
|
308 | return typeof x
|
309 | }
|
310 |
|
311 | function traverse (obj, callback = (value, path) => {}, path = []) {
|
312 | // FIXME: ugly to have these special cases
|
313 | if (path.length > 0 && path[0].indexOf('Dependencies') !== -1) {
|
314 | // special case for objects holding a collection of dependencies
|
315 | callback(obj, path)
|
316 | } else if (validateTypeOf(obj) === 'Array') {
|
317 | obj.map((item, index) => traverse(item, callback, path.concat(index)))
|
318 | } else if (validateTypeOf(obj) === 'Object') {
|
319 | Object.keys(obj).forEach(key => {
|
320 | // FIXME: ugly to have these special cases
|
321 | // ignore special case of deprecated docs
|
322 | if (key === 'docs' && path.join('.') === 'expression') {
|
323 | return
|
324 | }
|
325 |
|
326 | traverse(obj[key], callback, path.concat(key))
|
327 | })
|
328 | } else {
|
329 | callback(obj, path)
|
330 | }
|
331 | }
|
332 |
|
333 | function get (object, path) {
|
334 | let child = object
|
335 |
|
336 | for (let i = 0; i < path.length; i++) {
|
337 | const key = path[i]
|
338 | child = child ? child[key] : undefined
|
339 | }
|
340 |
|
341 | return child
|
342 | }
|
343 |
|
344 | /**
|
345 | * Create a copy of the provided `object` and delete
|
346 | * all properties listed in `excludedProperties`
|
347 | * @param {Object} object
|
348 | * @param {string[]} excludedProperties
|
349 | * @return {Object}
|
350 | */
|
351 | function exclude (object, excludedProperties) {
|
352 | const strippedObject = Object.assign({}, object)
|
353 |
|
354 | excludedProperties.forEach(excludedProperty => {
|
355 | delete strippedObject[excludedProperty]
|
356 | })
|
357 |
|
358 | return strippedObject
|
359 | }
|
360 |
|
361 | function isLowerCase (text) {
|
362 | return typeof text === 'string' && text.toLowerCase() === text
|
363 | }
|
364 |
|
\ | No newline at end of file |