mirror of
				https://github.com/microsoft/playwright.git
				synced 2025-06-26 21:40:17 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			246 lines
		
	
	
		
			7.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			246 lines
		
	
	
		
			7.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  * 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.
 | |
|  */
 | |
| 
 | |
| const Location = require('./Location.js');
 | |
| const colors = require('colors/safe');
 | |
| const Diff = require('text-diff');
 | |
| const GoldenUtils = require('./GoldenUtils');
 | |
| 
 | |
| class Matchers {
 | |
|   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;
 | |
|   }
 | |
| 
 | |
|   expect(received) {
 | |
|     return new Expect(received, this._matchers);
 | |
|   }
 | |
| };
 | |
| 
 | |
| class MatchError extends Error {
 | |
|   constructor(message, formatter) {
 | |
|     super(message);
 | |
|     this.name = this.constructor.name;
 | |
|     this.formatter = formatter;
 | |
|     this.location = Location.getCallerLocation();
 | |
|     Error.captureStackTrace(this, this.constructor);
 | |
|   }
 | |
| }
 | |
| 
 | |
| module.exports = {Matchers, MatchError};
 | |
| 
 | |
| class Expect {
 | |
|   constructor(received, matchers) {
 | |
|     this.not = {};
 | |
|     this.not.not = this;
 | |
|     for (const matcherName of Object.keys(matchers)) {
 | |
|       const matcher = matchers[matcherName];
 | |
|       this[matcherName] = applyMatcher.bind(null, matcherName, matcher, false /* inverse */, received);
 | |
|       this.not[matcherName] = applyMatcher.bind(null, matcherName, matcher, true /* inverse */, received);
 | |
|     }
 | |
| 
 | |
|     function applyMatcher(matcherName, matcher, inverse, received, ...args) {
 | |
|       const result = matcher.call(null, received, ...args);
 | |
|       const message = `expect.${inverse ? 'not.' : ''}${matcherName} failed` + (result.message ? `: ${result.message}` : '');
 | |
|       if (result.pass === inverse)
 | |
|         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}`,
 | |
|     `Received: ${received}`,
 | |
|     `    Diff: ${highlighted}`,
 | |
|   ];
 | |
|   for (let i = 0; i < Math.min(expected.length, received.length); ++i) {
 | |
|     if (expected[i] !== received[i]) {
 | |
|       const padding = ' '.repeat('    Diff: '.length);
 | |
|       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;
 | |
|     }
 | |
|     return encoded;
 | |
|   };
 | |
| 
 | |
|   const doDecodeLines = (text) => {
 | |
|     let decoded = [];
 | |
|     for (const codepoint of [...text])
 | |
|       decoded.push(decodingMap.get(codepoint));
 | |
|     return decoded;
 | |
|   }
 | |
| 
 | |
|   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);
 | |
|   });
 | |
| 
 | |
|   const flattened = [];
 | |
|   for (const list of highlighted)
 | |
|     flattened.push(...list);
 | |
|   return `Received:\n${flattened.join('\n')}`;
 | |
| }
 | |
| 
 | |
| 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');
 | |
| }
 | |
| 
 | |
| const DefaultMatchers = {
 | |
|   toBe: function(received, expected, message) {
 | |
|     message = message || `${received} == ${expected}`;
 | |
|     return { pass: received === expected, message, formatter: toBeFormatter.bind(null, received, expected) };
 | |
|   },
 | |
| 
 | |
|   toBeFalsy: function(received, message) {
 | |
|     message = message || `${received}`;
 | |
|     return { pass: !received, message };
 | |
|   },
 | |
| 
 | |
|   toBeTruthy: function(received, message) {
 | |
|     message = message || `${received}`;
 | |
|     return { pass: !!received, message };
 | |
|   },
 | |
| 
 | |
|   toBeGreaterThan: function(received, other, message) {
 | |
|     message = message || `${received} > ${other}`;
 | |
|     return { pass: received > other, message };
 | |
|   },
 | |
| 
 | |
|   toBeGreaterThanOrEqual: function(received, other, message) {
 | |
|     message = message || `${received} >= ${other}`;
 | |
|     return { pass: received >= other, message };
 | |
|   },
 | |
| 
 | |
|   toBeLessThan: function(received, other, message) {
 | |
|     message = message || `${received} < ${other}`;
 | |
|     return { pass: received < other, message };
 | |
|   },
 | |
| 
 | |
|   toBeLessThanOrEqual: function(received, other, message) {
 | |
|     message = message || `${received} <= ${other}`;
 | |
|     return { pass: received <= other, message };
 | |
|   },
 | |
| 
 | |
|   toBeNull: function(received, message) {
 | |
|     message = message || `${received} == null`;
 | |
|     return { pass: received === null, message };
 | |
|   },
 | |
| 
 | |
|   toContain: function(received, other, message) {
 | |
|     message = message || `${received} ⊇ ${other}`;
 | |
|     return { pass: received.includes(other), message };
 | |
|   },
 | |
| 
 | |
|   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 };
 | |
|   },
 | |
| 
 | |
|   toBeCloseTo: function(received, other, precision, message) {
 | |
|     return {
 | |
|       pass: Math.abs(received - other) < Math.pow(10, -precision),
 | |
|       message
 | |
|     };
 | |
|   },
 | |
| 
 | |
|   toBeInstanceOf: function(received, other, message) {
 | |
|     message = message || `${received.constructor.name} instanceof ${other.name}`;
 | |
|     return { pass: received instanceof other, message };
 | |
|   },
 | |
| 
 | |
|   toBeGolden: function(received, golden) {
 | |
|     return GoldenUtils.compare(received, golden);
 | |
|   },
 | |
| };
 | |
| 
 | |
| function stringify(value) {
 | |
|   function stabilize(key, object) {
 | |
|     if (typeof object !== 'object' || object === undefined || object === null || Array.isArray(object))
 | |
|       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);
 | |
| }
 | 
