| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  | /** | 
					
						
							|  |  |  |  * Copyright 2017 Google Inc. All rights reserved. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * Licensed under the Apache License, Version 2.0 (the "License"); | 
					
						
							|  |  |  |  * you may not use this file except in compliance with the License. | 
					
						
							|  |  |  |  * You may obtain a copy of the License at | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  *     http://www.apache.org/licenses/LICENSE-2.0
 | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * Unless required by applicable law or agreed to in writing, software | 
					
						
							|  |  |  |  * distributed under the License is distributed on an "AS IS" BASIS, | 
					
						
							|  |  |  |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
					
						
							|  |  |  |  * See the License for the specific language governing permissions and | 
					
						
							|  |  |  |  * limitations under the License. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-29 21:38:30 -07:00
										 |  |  | const Location = require('./Location.js'); | 
					
						
							| 
									
										
										
										
											2020-02-20 22:55:39 -08:00
										 |  |  | const colors = require('colors/safe'); | 
					
						
							|  |  |  | const Diff = require('text-diff'); | 
					
						
							| 
									
										
										
										
											2020-04-06 18:01:56 -07:00
										 |  |  | const GoldenUtils = require('./GoldenUtils'); | 
					
						
							| 
									
										
										
										
											2020-02-20 22:55:39 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  | class Matchers { | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |   constructor(customMatchers = {}) { | 
					
						
							|  |  |  |     this._matchers = {}; | 
					
						
							|  |  |  |     Object.assign(this._matchers, DefaultMatchers); | 
					
						
							|  |  |  |     Object.assign(this._matchers, customMatchers); | 
					
						
							|  |  |  |     this.expect = this.expect.bind(this); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   addMatcher(name, matcher) { | 
					
						
							|  |  |  |     this._matchers[name] = matcher; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-20 22:55:39 -08:00
										 |  |  |   expect(received) { | 
					
						
							|  |  |  |     return new Expect(received, this._matchers); | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |   } | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-20 22:55:39 -08:00
										 |  |  | class MatchError extends Error { | 
					
						
							|  |  |  |   constructor(message, formatter) { | 
					
						
							|  |  |  |     super(message); | 
					
						
							|  |  |  |     this.name = this.constructor.name; | 
					
						
							|  |  |  |     this.formatter = formatter; | 
					
						
							| 
									
										
										
										
											2020-04-06 17:47:17 -07:00
										 |  |  |     this.location = Location.getCallerLocation(); | 
					
						
							| 
									
										
										
										
											2020-02-20 22:55:39 -08:00
										 |  |  |     Error.captureStackTrace(this, this.constructor); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | module.exports = {Matchers, MatchError}; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  | class Expect { | 
					
						
							| 
									
										
										
										
											2020-02-20 22:55:39 -08:00
										 |  |  |   constructor(received, matchers) { | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |     this.not = {}; | 
					
						
							|  |  |  |     this.not.not = this; | 
					
						
							|  |  |  |     for (const matcherName of Object.keys(matchers)) { | 
					
						
							|  |  |  |       const matcher = matchers[matcherName]; | 
					
						
							| 
									
										
										
										
											2020-02-20 22:55:39 -08:00
										 |  |  |       this[matcherName] = applyMatcher.bind(null, matcherName, matcher, false /* inverse */, received); | 
					
						
							|  |  |  |       this.not[matcherName] = applyMatcher.bind(null, matcherName, matcher, true /* inverse */, received); | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-20 22:55:39 -08:00
										 |  |  |     function applyMatcher(matcherName, matcher, inverse, received, ...args) { | 
					
						
							|  |  |  |       const result = matcher.call(null, received, ...args); | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |       const message = `expect.${inverse ? 'not.' : ''}${matcherName} failed` + (result.message ? `: ${result.message}` : ''); | 
					
						
							|  |  |  |       if (result.pass === inverse) | 
					
						
							| 
									
										
										
										
											2020-02-20 22:55:39 -08:00
										 |  |  |         throw new MatchError(message, result.formatter || defaultFormatter.bind(null, received)); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function defaultFormatter(received) { | 
					
						
							|  |  |  |   return `Received: ${colors.red(JSON.stringify(received))}`; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function stringFormatter(received, expected) { | 
					
						
							|  |  |  |   const diff = new Diff(); | 
					
						
							|  |  |  |   const result = diff.main(expected, received); | 
					
						
							|  |  |  |   diff.cleanupSemantic(result); | 
					
						
							|  |  |  |   const highlighted = result.map(([type, text]) => { | 
					
						
							|  |  |  |     if (type === -1) | 
					
						
							|  |  |  |       return colors.bgRed(text); | 
					
						
							|  |  |  |     if (type === 1) | 
					
						
							|  |  |  |       return colors.bgGreen.black(text); | 
					
						
							|  |  |  |     return text; | 
					
						
							|  |  |  |   }).join(''); | 
					
						
							|  |  |  |   const output = [ | 
					
						
							|  |  |  |     `Expected: ${expected}`, | 
					
						
							| 
									
										
										
										
											2020-03-07 17:29:41 -08:00
										 |  |  |     `Received: ${received}`, | 
					
						
							|  |  |  |     `    Diff: ${highlighted}`, | 
					
						
							| 
									
										
										
										
											2020-02-20 22:55:39 -08:00
										 |  |  |   ]; | 
					
						
							|  |  |  |   for (let i = 0; i < Math.min(expected.length, received.length); ++i) { | 
					
						
							|  |  |  |     if (expected[i] !== received[i]) { | 
					
						
							| 
									
										
										
										
											2020-03-07 17:29:41 -08:00
										 |  |  |       const padding = ' '.repeat('    Diff: '.length); | 
					
						
							| 
									
										
										
										
											2020-02-20 22:55:39 -08:00
										 |  |  |       const firstDiffCharacter = '~'.repeat(i) + '^'; | 
					
						
							|  |  |  |       output.push(colors.red(padding + firstDiffCharacter)); | 
					
						
							|  |  |  |       break; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   return output.join('\n'); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function objectFormatter(received, expected) { | 
					
						
							|  |  |  |   const encodingMap = new Map(); | 
					
						
							|  |  |  |   const decodingMap = new Map(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const doEncodeLines = (lines) => { | 
					
						
							|  |  |  |     let encoded = ''; | 
					
						
							|  |  |  |     for (const line of lines) { | 
					
						
							|  |  |  |       let code = encodingMap.get(line); | 
					
						
							|  |  |  |       if (!code) { | 
					
						
							|  |  |  |         code = String.fromCodePoint(encodingMap.size); | 
					
						
							|  |  |  |         encodingMap.set(line, code); | 
					
						
							|  |  |  |         decodingMap.set(code, line); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       encoded += code; | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2020-02-20 22:55:39 -08:00
										 |  |  |     return encoded; | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const doDecodeLines = (text) => { | 
					
						
							|  |  |  |     let decoded = []; | 
					
						
							|  |  |  |     for (const codepoint of [...text]) | 
					
						
							|  |  |  |       decoded.push(decodingMap.get(codepoint)); | 
					
						
							|  |  |  |     return decoded; | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2020-02-20 22:55:39 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |   let receivedEncoded = doEncodeLines(received.split('\n')); | 
					
						
							|  |  |  |   let expectedEncoded = doEncodeLines(expected.split('\n')); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const diff = new Diff(); | 
					
						
							|  |  |  |   const result = diff.main(expectedEncoded, receivedEncoded); | 
					
						
							|  |  |  |   diff.cleanupSemantic(result); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const highlighted = result.map(([type, text]) => { | 
					
						
							|  |  |  |     const lines = doDecodeLines(text); | 
					
						
							|  |  |  |     if (type === -1) | 
					
						
							|  |  |  |       return lines.map(line => '-   ' + colors.bgRed(line)); | 
					
						
							|  |  |  |     if (type === 1) | 
					
						
							|  |  |  |       return lines.map(line => '+   ' + colors.bgGreen.black(line)); | 
					
						
							|  |  |  |     return lines.map(line => '    ' + line); | 
					
						
							| 
									
										
										
										
											2020-04-07 22:56:21 -07:00
										 |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const flattened = []; | 
					
						
							|  |  |  |   for (const list of highlighted) | 
					
						
							|  |  |  |     flattened.push(...list); | 
					
						
							|  |  |  |   return `Received:\n${flattened.join('\n')}`; | 
					
						
							| 
									
										
										
										
											2020-02-20 22:55:39 -08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function toBeFormatter(received, expected) { | 
					
						
							|  |  |  |   if (typeof expected === 'string' && typeof received === 'string') { | 
					
						
							|  |  |  |     return stringFormatter(JSON.stringify(received), JSON.stringify(expected)); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   return [ | 
					
						
							|  |  |  |     `Expected: ${JSON.stringify(expected)}`, | 
					
						
							|  |  |  |     `Received: ${colors.red(JSON.stringify(received))}`, | 
					
						
							|  |  |  |   ].join('\n'); | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const DefaultMatchers = { | 
					
						
							| 
									
										
										
										
											2020-02-20 22:55:39 -08:00
										 |  |  |   toBe: function(received, expected, message) { | 
					
						
							|  |  |  |     message = message || `${received} == ${expected}`; | 
					
						
							|  |  |  |     return { pass: received === expected, message, formatter: toBeFormatter.bind(null, received, expected) }; | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |   }, | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-20 22:55:39 -08:00
										 |  |  |   toBeFalsy: function(received, message) { | 
					
						
							|  |  |  |     message = message || `${received}`; | 
					
						
							|  |  |  |     return { pass: !received, message }; | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |   }, | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-20 22:55:39 -08:00
										 |  |  |   toBeTruthy: function(received, message) { | 
					
						
							|  |  |  |     message = message || `${received}`; | 
					
						
							|  |  |  |     return { pass: !!received, message }; | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |   }, | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-20 22:55:39 -08:00
										 |  |  |   toBeGreaterThan: function(received, other, message) { | 
					
						
							|  |  |  |     message = message || `${received} > ${other}`; | 
					
						
							|  |  |  |     return { pass: received > other, message }; | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |   }, | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-20 22:55:39 -08:00
										 |  |  |   toBeGreaterThanOrEqual: function(received, other, message) { | 
					
						
							|  |  |  |     message = message || `${received} >= ${other}`; | 
					
						
							|  |  |  |     return { pass: received >= other, message }; | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |   }, | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-20 22:55:39 -08:00
										 |  |  |   toBeLessThan: function(received, other, message) { | 
					
						
							|  |  |  |     message = message || `${received} < ${other}`; | 
					
						
							|  |  |  |     return { pass: received < other, message }; | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |   }, | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-20 22:55:39 -08:00
										 |  |  |   toBeLessThanOrEqual: function(received, other, message) { | 
					
						
							|  |  |  |     message = message || `${received} <= ${other}`; | 
					
						
							|  |  |  |     return { pass: received <= other, message }; | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |   }, | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-20 22:55:39 -08:00
										 |  |  |   toBeNull: function(received, message) { | 
					
						
							|  |  |  |     message = message || `${received} == null`; | 
					
						
							|  |  |  |     return { pass: received === null, message }; | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |   }, | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-20 22:55:39 -08:00
										 |  |  |   toContain: function(received, other, message) { | 
					
						
							|  |  |  |     message = message || `${received} ⊇ ${other}`; | 
					
						
							|  |  |  |     return { pass: received.includes(other), message }; | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |   }, | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-20 22:55:39 -08:00
										 |  |  |   toEqual: function(received, other, message) { | 
					
						
							|  |  |  |     let receivedJson = stringify(received); | 
					
						
							|  |  |  |     let otherJson = stringify(other); | 
					
						
							|  |  |  |     let formatter = objectFormatter.bind(null, receivedJson, otherJson); | 
					
						
							|  |  |  |     if (receivedJson.length < 40 && otherJson.length < 40) { | 
					
						
							|  |  |  |       receivedJson = receivedJson.split('\n').map(line => line.trim()).join(' '); | 
					
						
							|  |  |  |       otherJson = otherJson.split('\n').map(line => line.trim()).join(' '); | 
					
						
							|  |  |  |       formatter = stringFormatter.bind(null, receivedJson, otherJson); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     message = message || `\n${receivedJson} ≈ ${otherJson}`; | 
					
						
							|  |  |  |     return { pass: receivedJson === otherJson, message, formatter }; | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |   }, | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-20 22:55:39 -08:00
										 |  |  |   toBeCloseTo: function(received, other, precision, message) { | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |     return { | 
					
						
							| 
									
										
										
										
											2020-02-20 22:55:39 -08:00
										 |  |  |       pass: Math.abs(received - other) < Math.pow(10, -precision), | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |       message | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |   }, | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-20 22:55:39 -08:00
										 |  |  |   toBeInstanceOf: function(received, other, message) { | 
					
						
							|  |  |  |     message = message || `${received.constructor.name} instanceof ${other.name}`; | 
					
						
							|  |  |  |     return { pass: received instanceof other, message }; | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |   }, | 
					
						
							| 
									
										
										
										
											2020-04-13 14:12:44 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |   toBeGolden: function(received, golden) { | 
					
						
							|  |  |  |     return GoldenUtils.compare(received, golden); | 
					
						
							|  |  |  |   }, | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function stringify(value) { | 
					
						
							|  |  |  |   function stabilize(key, object) { | 
					
						
							| 
									
										
										
										
											2020-02-20 22:55:39 -08:00
										 |  |  |     if (typeof object !== 'object' || object === undefined || object === null || Array.isArray(object)) | 
					
						
							| 
									
										
										
										
											2019-11-18 18:18:28 -08:00
										 |  |  |       return object; | 
					
						
							|  |  |  |     const result = {}; | 
					
						
							|  |  |  |     for (const key of Object.keys(object).sort()) | 
					
						
							|  |  |  |       result[key] = object[key]; | 
					
						
							|  |  |  |     return result; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return JSON.stringify(stabilize(null, value), stabilize, 2); | 
					
						
							|  |  |  | } |