From 9478a816ee1657e87599a1ce78fd42e8e4ee976f Mon Sep 17 00:00:00 2001 From: "JiaLi.Passion" Date: Thu, 10 May 2018 01:00:54 +0900 Subject: [PATCH] feat(test): add jest support to Mocha runner --- lib/mocha/jasmine-bridge/jasmine.expect.ts | 483 +++++++++++------- lib/mocha/jasmine-bridge/jasmine.spy.ts | 16 + lib/mocha/jasmine-bridge/jasmine.ts | 3 +- lib/mocha/jasmine-bridge/jasmine.util.ts | 5 + lib/mocha/jest-bridge/jest-bridge.ts | 9 + lib/mocha/jest-bridge/jest.bdd.ts | 25 + lib/mocha/jest-bridge/jest.expect.ts | 257 ++++++++++ lib/mocha/jest-bridge/jest.spy.ts | 115 +++++ lib/mocha/jest-bridge/jest.ts | 25 + lib/mocha/mocha-patch.ts | 7 + lib/mocha/mocha.ts | 3 +- test/spec/mocha/jest-bridge.spec.ts | 386 ++++++++++++++ .../spec/mocha/mocha-node-test-entry-point.ts | 3 +- 13 files changed, 1136 insertions(+), 201 deletions(-) create mode 100644 lib/mocha/jest-bridge/jest-bridge.ts create mode 100644 lib/mocha/jest-bridge/jest.bdd.ts create mode 100644 lib/mocha/jest-bridge/jest.expect.ts create mode 100644 lib/mocha/jest-bridge/jest.spy.ts create mode 100644 lib/mocha/jest-bridge/jest.ts create mode 100644 test/spec/mocha/jest-bridge.spec.ts diff --git a/lib/mocha/jasmine-bridge/jasmine.expect.ts b/lib/mocha/jasmine-bridge/jasmine.expect.ts index ad5ed825c..71b670c1f 100644 --- a/lib/mocha/jasmine-bridge/jasmine.expect.ts +++ b/lib/mocha/jasmine-bridge/jasmine.expect.ts @@ -5,7 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {addCustomEqualityTester, Any, customEqualityTesters, eq, ObjectContaining} from './jasmine.util'; +import {addCustomEqualityTester, Any, customEqualityTesters, eq, ObjectContaining, toMatch} from './jasmine.util'; export function addJasmineExpect(jasmine: any, global: any) { addExpect(global, jasmine); @@ -28,7 +28,6 @@ function addObjectContaining(jasmine: any) { } function addCustomMatchers(jasmine: any, global: any) { - const util = {equals: eq}; const originalExcept = jasmine['__zone_symbol__expect']; jasmine.addMatchers = function(customMatcher: any) { let customMatchers = jasmine['__zone_symbol__customMatchers']; @@ -36,33 +35,172 @@ function addCustomMatchers(jasmine: any, global: any) { customMatchers = jasmine['__zone_symbol__customMatchers'] = []; } customMatchers.push(customMatcher); - global['expect'] = function(expected: any) { - const expectObj = originalExcept.call(this, expected); - customMatchers.forEach((matcher: any) => { - Object.keys(matcher).forEach(key => { - if (matcher.hasOwnProperty(key)) { - const customExpected = matcher[key](util, customEqualityTesters); - expectObj[key] = function(actual: any) { - return customExpected.compare(expected, actual); - }; - expectObj['not'][key] = function(actual: any) { - return !customExpected.compare(expected, actual); - }; - } - }); - }); - return expectObj; - }; }; } -function addExpect(global: any, jasmine: any) { - global['expect'] = jasmine['__zone_symbol__expect'] = function(expected: any) { - return { - nothing: function() {}, +function buildCustomMatchers(expectObj: any, jasmine: any, expected: any) { + const util = {equals: eq, toMatch: toMatch}; + let customMatchers: any = jasmine['__zone_symbol__customMatchers']; + if (!customMatchers) { + return; + } + customMatchers.forEach((matcher: any) => { + Object.keys(matcher).forEach(key => { + if (matcher.hasOwnProperty(key)) { + const customExpected = matcher[key](util, customEqualityTesters); + expectObj[key] = function(...actuals: any[]) { + if (!customExpected.compare(expected, actuals[0], actuals[1])) { + throw new Error(`${key} failed, expect: ${expected}, actual is: ${actuals[0]}`); + } + }; + expectObj['not'][key] = function(...actuals: any[]) { + if (customExpected.compare(expected, actuals[0], actuals[1])) { + throw new Error(`${key} failed, not expect: ${expected}, actual is: ${actuals[0]}`); + } + }; + } + }); + }); + return expectObj; +} + +function getMatchers(expected: any) { + return { + nothing: function() {}, + toBe: function(actual: any) { + if (expected !== actual) { + throw new Error(`Expected ${expected} to be ${actual}`); + } + }, + toBeCloseTo: function(actual: any, precision: any) { + if (precision !== 0) { + precision = precision || 2; + } + const pow = Math.pow(10, precision + 1); + const delta = Math.abs(expected - actual); + const maxDelta = Math.pow(10, -precision) / 2; + if (Math.round(delta * pow) / pow > maxDelta) { + throw new Error( + `Expected ${expected} to be close to ${actual} with precision ${precision}`); + } + }, + toEqual: function(actual: any) { + if (!eq(expected, actual)) { + throw new Error(`Expected ${expected} to be ${actual}`); + } + }, + toBeGreaterThan: function(actual: number) { + if (expected <= actual) { + throw new Error(`Expected ${expected} to be greater than ${actual}`); + } + }, + toBeGreaterThanOrEqual: function(actual: number) { + if (expected < actual) { + throw new Error(`Expected ${expected} to be greater than or equal ${actual}`); + } + }, + toBeLessThan: function(actual: number) { + if (expected >= actual) { + throw new Error(`Expected ${expected} to be lesser than ${actual}`); + } + }, + toBeLessThanOrEqual: function(actual: number) { + if (expected > actual) { + throw new Error(`Expected ${expected} to be lesser than or equal ${actual}`); + } + }, + toBeDefined: function() { + if (expected === undefined) { + throw new Error(`Expected ${expected} to be defined`); + } + }, + toBeNaN: function() { + if (expected === expected) { + throw new Error(`Expected ${expected} to be NaN`); + } + }, + toBeNegativeInfinity: function() { + if (expected !== Number.NEGATIVE_INFINITY) { + throw new Error(`Expected ${expected} to be -Infinity`); + } + }, + toBeNull: function() { + if (expected !== null) { + throw new Error(`Expected ${expected} to be null`); + } + }, + toBePositiveInfinity: function() { + if (expected !== Number.POSITIVE_INFINITY) { + throw new Error(`Expected ${expected} to be +Infinity`); + } + }, + toBeUndefined: function() { + if (expected !== undefined) { + throw new Error(`Expected ${expected} to be undefined`); + } + }, + toThrow: function() { + try { + expected(); + } catch (error) { + return; + } + + throw new Error(`Expected ${expected} to throw`); + }, + toThrowError: function(errorToBeThrow: any) { + try { + expected(); + } catch (error) { + return; + } + + throw Error(`Expected ${expected} to throw: ${errorToBeThrow}`); + }, + toBeTruthy: function() { + if (!expected) { + throw new Error(`Expected ${expected} to be truthy`); + } + }, + toBeFalsy: function(actual: any) { + if (!!actual) { + throw new Error(`Expected ${actual} to be falsy`); + } + }, + toContain: function(actual: any) { + if (expected.indexOf(actual) === -1) { + throw new Error(`Expected ${expected} to contain ${actual}`); + } + }, + toBeCalled: function() { + if (expected.calls.count() === 0) { + throw new Error(`Expected ${expected} to been called`); + } + }, + toHaveBeenCalled: function() { + if (expected.calls.count() === 0) { + throw new Error(`Expected ${expected} to been called`); + } + }, + toBeCalledWith: function(...params: any[]) { + if (expected.calls.allArgs().filter((args: any) => eq(args, params)).length === 0) { + throw new Error(`Expected ${expected.calls.allArgs()} to been called with ${params}`); + } + }, + toHaveBeenCalledWith: function(...params: any[]) { + if (expected.calls.allArgs().filter((args: any) => eq(args, params)).length === 0) { + throw new Error(`Expected ${expected.calls.allArgs()} to been called with ${params}`); + } + }, + toMatch: function(actual: any) { + if (!toMatch(actual, expected)) { + throw new Error(`Expected ${expected} to match ${actual}`); + } + }, + not: { toBe: function(actual: any) { - if (expected !== actual) { - throw new Error(`Expected ${expected} to be ${actual}`); + if (expected === actual) { + throw new Error(`Expected ${expected} not to be ${actual}`); } }, toBeCloseTo: function(actual: any, precision: any) { @@ -72,232 +210,181 @@ function addExpect(global: any, jasmine: any) { const pow = Math.pow(10, precision + 1); const delta = Math.abs(expected - actual); const maxDelta = Math.pow(10, -precision) / 2; - if (Math.round(delta * pow) / pow > maxDelta) { + if (Math.round(delta * pow) / pow <= maxDelta) { throw new Error( - `Expected ${expected} to be close to ${actual} with precision ${precision}`); + `Expected ${expected} not to be close to ${actual} with precision ${precision}`); } }, toEqual: function(actual: any) { - if (!eq(expected, actual)) { - throw new Error(`Expected ${expected} to be ${actual}`); + if (eq(expected, actual)) { + throw new Error(`Expected ${expected} not to be ${actual}`); + } + }, + toThrow: function() { + try { + expected(); + } catch (error) { + throw new Error(`Expected ${expected} to not throw`); + } + }, + toThrowError: function() { + try { + expected(); + } catch (error) { + throw Error(`Expected ${expected} to not throw error`); } }, toBeGreaterThan: function(actual: number) { - if (expected <= actual) { - throw new Error(`Expected ${expected} to be greater than ${actual}`); + if (expected > actual) { + throw new Error(`Expected ${expected} not to be greater than ${actual}`); } }, toBeGreaterThanOrEqual: function(actual: number) { - if (expected < actual) { - throw new Error(`Expected ${expected} to be greater than or equal ${actual}`); + if (expected >= actual) { + throw new Error(`Expected ${expected} not to be greater than or equal ${actual}`); } }, toBeLessThan: function(actual: number) { - if (expected >= actual) { - throw new Error(`Expected ${expected} to be lesser than ${actual}`); + if (expected < actual) { + throw new Error(`Expected ${expected} not to be lesser than ${actual}`); } }, toBeLessThanOrEqual: function(actual: number) { - if (expected > actual) { - throw new Error(`Expected ${expected} to be lesser than or equal ${actual}`); + if (expected <= actual) { + throw new Error(`Expected ${expected} not to be lesser than or equal ${actual}`); } }, toBeDefined: function() { - if (expected === undefined) { - throw new Error(`Expected ${expected} to be defined`); + if (expected !== undefined) { + throw new Error(`Expected ${expected} not to be defined`); } }, toBeNaN: function() { - if (expected === expected) { - throw new Error(`Expected ${expected} to be NaN`); + if (expected !== expected) { + throw new Error(`Expected ${expected} not to be NaN`); } }, toBeNegativeInfinity: function() { - if (expected !== Number.NEGATIVE_INFINITY) { - throw new Error(`Expected ${expected} to be -Infinity`); + if (expected === Number.NEGATIVE_INFINITY) { + throw new Error(`Expected ${expected} not to be -Infinity`); } }, toBeNull: function() { - if (expected !== null) { - throw new Error(`Expected ${expected} to be null`); + if (expected === null) { + throw new Error(`Expected ${expected} not to be null`); } }, toBePositiveInfinity: function() { - if (expected !== Number.POSITIVE_INFINITY) { - throw new Error(`Expected ${expected} to be +Infinity`); + if (expected === Number.POSITIVE_INFINITY) { + throw new Error(`Expected ${expected} not to be +Infinity`); } }, toBeUndefined: function() { - if (expected !== undefined) { - throw new Error(`Expected ${expected} to be undefined`); - } - }, - toThrow: function() { - try { - expected(); - } catch (error) { - return; + if (expected === undefined) { + throw new Error(`Expected ${expected} not to be undefined`); } - - throw new Error(`Expected ${expected} to throw`); }, - toThrowError: function(errorToBeThrow: any) { - try { - expected(); - } catch (error) { - return; + toBeTruthy: function() { + if (!!expected) { + throw new Error(`Expected ${expected} not to be truthy`); } - - throw Error(`Expected ${expected} to throw: ${errorToBeThrow}`); }, - toBeTruthy: function() { + toBeFalsy: function() { if (!expected) { - throw new Error(`Expected ${expected} to be truthy`); + throw new Error(`Expected ${expected} not to be falsy`); } }, - toBeFalsy: function(actual: any) { - if (!!actual) { - throw new Error(`Expected ${actual} to be falsy`); + toContain: function(actual: any) { + if (expected.indexOf(actual) !== -1) { + throw new Error(`Expected ${expected} not to contain ${actual}`); } }, - toContain: function(actual: any) { - if (expected.indexOf(actual) === -1) { - throw new Error(`Expected ${expected} to contain ${actual}`); + toMatch: function(actual: any) { + if (toMatch(actual, expected)) { + throw new Error(`Expected ${expected} not to match ${actual}`); } }, - toHaveBeenCalled: function() { - if (expected.calls.count() === 0) { - throw new Error(`Expected ${expected} to been called`); + toBeCalled: function() { + if (expected.calls.count() > 0) { + throw new Error(`Expected ${expected} to not been called`); } }, - toHaveBeenCalledWith: function(...params: any[]) { - if (expected.calls.allArgs().filter((args: any) => eq(args, params)).length === 0) { - throw new Error(`Expected ${expected} to been called with ${params}`); + toHaveBeenCalled: function() { + if (expected.calls.count() > 0) { + throw new Error(`Expected ${expected} to not been called`); } }, - toMatch: function(actual: any) { - if (!new RegExp(actual).test(expected)) { - throw new Error(`Expected ${expected} to match ${actual}`); + toBeCalledWith: function(params: any[]) { + if (expected.calls.allArgs().filter((args: any) => eq(args, params)).length > 0) { + throw new Error(`Expected ${expected.calls.allArgs()} to not been called with ${params}`); } }, - not: { - toBe: function(actual: any) { - if (expected === actual) { - throw new Error(`Expected ${expected} not to be ${actual}`); - } - }, - toBeCloseTo: function(actual: any, precision: any) { - if (precision !== 0) { - precision = precision || 2; - } - const pow = Math.pow(10, precision + 1); - const delta = Math.abs(expected - actual); - const maxDelta = Math.pow(10, -precision) / 2; - if (Math.round(delta * pow) / pow <= maxDelta) { - throw new Error( - `Expected ${expected} not to be close to ${actual} with precision ${precision}`); - } - }, - toEqual: function(actual: any) { - if (eq(expected, actual)) { - throw new Error(`Expected ${expected} not to be ${actual}`); - } - }, - toThrow: function() { - try { - expected(); - } catch (error) { - throw new Error(`Expected ${expected} to not throw`); - } - }, - toThrowError: function() { - try { - expected(); - } catch (error) { - throw Error(`Expected ${expected} to not throw error`); - } - }, - toBeGreaterThan: function(actual: number) { - if (expected > actual) { - throw new Error(`Expected ${expected} not to be greater than ${actual}`); - } - }, - toBeGreaterThanOrEqual: function(actual: number) { - if (expected >= actual) { - throw new Error(`Expected ${expected} not to be greater than or equal ${actual}`); - } - }, - toBeLessThan: function(actual: number) { - if (expected < actual) { - throw new Error(`Expected ${expected} not to be lesser than ${actual}`); - } - }, - toBeLessThanOrEqual: function(actual: number) { - if (expected <= actual) { - throw new Error(`Expected ${expected} not to be lesser than or equal ${actual}`); - } - }, - toBeDefined: function() { - if (expected !== undefined) { - throw new Error(`Expected ${expected} not to be defined`); - } - }, - toBeNaN: function() { - if (expected !== expected) { - throw new Error(`Expected ${expected} not to be NaN`); - } - }, - toBeNegativeInfinity: function() { - if (expected === Number.NEGATIVE_INFINITY) { - throw new Error(`Expected ${expected} not to be -Infinity`); - } - }, - toBeNull: function() { - if (expected === null) { - throw new Error(`Expected ${expected} not to be null`); - } - }, - toBePositiveInfinity: function() { - if (expected === Number.POSITIVE_INFINITY) { - throw new Error(`Expected ${expected} not to be +Infinity`); - } - }, - toBeUndefined: function() { - if (expected === undefined) { - throw new Error(`Expected ${expected} not to be undefined`); - } - }, - toBeTruthy: function() { - if (!!expected) { - throw new Error(`Expected ${expected} not to be truthy`); - } - }, - toBeFalsy: function() { - if (!expected) { - throw new Error(`Expected ${expected} not to be falsy`); - } - }, - toContain: function(actual: any) { - if (expected.indexOf(actual) !== -1) { - throw new Error(`Expected ${expected} not to contain ${actual}`); - } - }, - toMatch: function(actual: any) { - if (new RegExp(actual).test(expected)) { - throw new Error(`Expected ${expected} not to match ${actual}`); - } - }, - toHaveBeenCalled: function() { - if (expected.calls.count() > 0) { - throw new Error(`Expected ${expected} to not been called`); - } - }, - toHaveBeenCalledWith: function(params: any[]) { - if (expected.calls.allArgs().filter((args: any) => eq(args, params)).length > 0) { - throw new Error(`Expected ${expected} to not been called with ${params}`); - } + toHaveBeenCalledWith: function(params: any[]) { + if (expected.calls.allArgs().filter((args: any) => eq(args, params)).length > 0) { + throw new Error(`Expected ${expected.calls.allArgs()} to not been called with ${params}`); } } + } + }; +} + +function buildResolveRejects(key: string, matchers: any, expected: any, isNot = false) { + if (matchers.hasOwnProperty(key)) { + const resolveFnFactory = function(isNot = false) { + return function() { + const self = this; + const args = Array.prototype.slice.call(arguments); + return expected.then( + (value: any) => { + const newMatchers: any = getMatchers(value); + return isNot ? newMatchers.not[key].apply(self, args) : + newMatchers[key].apply(self, args); + }, + (error: any) => { + throw error; + }); + } + }; + if (isNot) { + matchers.resolves.not[key] = resolveFnFactory(true); + } else { + matchers.resolves[key] = resolveFnFactory(); + } + const rejectFnFactory = function(isNot = false) { + return function() { + const self = this; + const args = Array.prototype.slice.call(arguments); + return expected.then((value: any) => {}, (error: any) => { + const newMatchers: any = getMatchers(error); + return isNot ? newMatchers.not[key].apply(self, args) : + newMatchers[key].apply(self, args); + }); + } }; + if (isNot) { + matchers.rejects.not[key] = rejectFnFactory(true); + } else { + matchers.rejects[key] = rejectFnFactory(); + } + } +} +function addExpect(global: any, jasmine: any) { + jasmine.__zone_symbol__expect_assertions = 0; + global['expect'] = jasmine['__zone_symbol__expect'] = function(expected: any) { + jasmine.__zone_symbol__expect_assertions++; + const matchers: any = getMatchers(expected); + if (expected && typeof expected.then === 'function') { + // expected maybe a promise + matchers.resolves = {not: {}}; + matchers.rejects = {not: {}}; + Object.keys(matchers).forEach(key => { + buildResolveRejects(key, matchers, expected); + }); + Object.keys(matchers.not).forEach(key => { + buildResolveRejects(key, matchers, expected, true); + }); + } + buildCustomMatchers(matchers, jasmine, expected); + return matchers; }; } \ No newline at end of file diff --git a/lib/mocha/jasmine-bridge/jasmine.spy.ts b/lib/mocha/jasmine-bridge/jasmine.spy.ts index b9095f0fd..9487324c8 100644 --- a/lib/mocha/jasmine-bridge/jasmine.spy.ts +++ b/lib/mocha/jasmine-bridge/jasmine.spy.ts @@ -170,6 +170,19 @@ export function addJasmineSpy(jasmine: any, Mocha: any, global: any) { }; } + updateArgs(newArgs: SpyStrategyOptions) { + if (newArgs.identity) { + this.args.identity = newArgs.identity; + this.baseStrategy.identity = newArgs.identity; + this.and.identity = newArgs.identity; + } + if (newArgs.originalFn) { + this.args.originalFn = newArgs.originalFn; + this.baseStrategy.originalFn = newArgs.originalFn; + this.and.originalFn = newArgs.originalFn; + } + } + exec(spy: any, args: any) { let strategy = this.strategyDict.get(args); if (!strategy) { @@ -234,6 +247,9 @@ export function addJasmineSpy(jasmine: any, Mocha: any, global: any) { wrapper.withArgs = function() { return spyStrategyDispatcher.withArgs.apply(spyStrategyDispatcher, arguments); }; + wrapper.updateArgs = function(newArgs: SpyStrategyOptions) { + spyStrategyDispatcher.updateArgs(newArgs); + }; return wrapper; } diff --git a/lib/mocha/jasmine-bridge/jasmine.ts b/lib/mocha/jasmine-bridge/jasmine.ts index 84e071b3e..65b35d5d7 100644 --- a/lib/mocha/jasmine-bridge/jasmine.ts +++ b/lib/mocha/jasmine-bridge/jasmine.ts @@ -41,9 +41,10 @@ Zone.__load_patch('jasmine2mocha', (global: any) => { configurable: true, enumerable: true, get: function() { - return global.Mocha.__zone_symbol__TIMEOUT; + return jasmine.__zone_symbol__TIMEOUT || 2000; }, set: function(newValue: number) { + jasmine.__zone_symbol__TIMEOUT = newValue; global.Mocha.__zone_symbol__TIMEOUT = newValue; } }); diff --git a/lib/mocha/jasmine-bridge/jasmine.util.ts b/lib/mocha/jasmine-bridge/jasmine.util.ts index 8496577b9..0b9162c45 100644 --- a/lib/mocha/jasmine-bridge/jasmine.util.ts +++ b/lib/mocha/jasmine-bridge/jasmine.util.ts @@ -156,3 +156,8 @@ export function eq(a: any, b: any) { return false; } + +export function toMatch(actual: any, expected: any) { + const regExp = actual instanceof RegExp ? actual : new RegExp(actual); + return regExp.test(expected); +} diff --git a/lib/mocha/jest-bridge/jest-bridge.ts b/lib/mocha/jest-bridge/jest-bridge.ts new file mode 100644 index 000000000..6c5b0251f --- /dev/null +++ b/lib/mocha/jest-bridge/jest-bridge.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import './jest'; diff --git a/lib/mocha/jest-bridge/jest.bdd.ts b/lib/mocha/jest-bridge/jest.bdd.ts new file mode 100644 index 000000000..63dc017fe --- /dev/null +++ b/lib/mocha/jest-bridge/jest.bdd.ts @@ -0,0 +1,25 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export function mappingBDD(jest: any, Mocha: any, global: any) { + const mappings: {jest: string, Mocha: string}[] = [ + // other Jest APIs has already mapping in jasmine2mocha patch + {jest: 'test', Mocha: 'it'} + ]; + mappings.forEach(map => { + if (!global[map.jest]) { + const mocha = map.Mocha; + const chains = mocha.split('.'); + let mochaMethod: any = null; + for (let i = 0; i < chains.length; i++) { + mochaMethod = mochaMethod ? mochaMethod[chains[i]] : global[chains[i]]; + } + global[map.jest] = jest[map.jest] = mochaMethod; + } + }); +} \ No newline at end of file diff --git a/lib/mocha/jest-bridge/jest.expect.ts b/lib/mocha/jest-bridge/jest.expect.ts new file mode 100644 index 000000000..cc72ad8af --- /dev/null +++ b/lib/mocha/jest-bridge/jest.expect.ts @@ -0,0 +1,257 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { Any, eq, toMatch } from '../jasmine-bridge/jasmine.util'; +export function expandExpect(global: any) { + const jasmine = global.jasmine; + const expect: any = global.expect; + + class Anything { } + + expect.anything = function () { + return new Anything(); + }; + + expect.any = function (obj: any) { + return new Any(obj); + }; + + class ArrayContaining { + constructor(public expectedArray: any[]) { } + } + + expect.arrayContaining = function (expectedArray: string[]) { + return new ArrayContaining(expectedArray); + } + + class ObjectContaining { + constructor(public expectedObject: any) { } + } + + expect.objectContaining = function (expectedObject: any) { + return new ObjectContaining(expectedObject); + } + + class StringContaining { + constructor(public expectedString: string) { } + } + + expect.stringContaining = function (expectedString: string) { + return new StringContaining(expectedString); + } + + class StringMatching { + constructor(public expectedMatcher: RegExp | string) { } + } + + expect.stringMatching = function (expectedMatcher: RegExp | string) { + return new StringMatching(expectedMatcher); + } + + const assertions: { test: any, numbers: number }[] = expect.__zone_symbol__assertionsMap = []; + + jasmine.addCustomEqualityTester((a: any, b: any) => { + if (b instanceof Anything) { + if (a === null || a === undefined) { + return false; + } + return true; + } + if (b instanceof Any) { + return b.eq(a); + } + if (b instanceof ArrayContaining && Array.isArray(a)) { + for (let i = 0; i < b.expectedArray.length; i++) { + let found = false; + const bitem = b.expectedArray[i]; + for (let j = 0; j < a.length; j++) { + if (eq(a[j], bitem)) { + found = true; + break; + } + } + if (!found) { + return false; + } + } + return true; + } + if (b instanceof ObjectContaining) { + Object.keys(b.expectedObject).forEach(key => { + if (b.expectedObject.hasOwnProperty(key)) { + if (!eq(a[key], b.expectedObject[key]) || !toMatch(b.expectedObject[key], a[key])) { + return false; + } + } + }); + return true; + } + if (b instanceof StringContaining) { + let astr = a; + if (typeof a !== 'string') { + astr = Object.prototype.toString.call(a); + } + if (!astr) { + return false; + } + return astr.indexOf(b.expectedString) !== -1; + } + if (b instanceof StringMatching) { + let astr = a; + if (typeof a !== 'string') { + astr = Object.prototype.toString.call(a); + } + return toMatch(b.expectedMatcher, astr); + } + }); + + expect.extend = function (extendedMatchers: any) { + const jasmineMatchers: any = {}; + Object.keys(extendedMatchers).forEach(key => { + if (extendedMatchers.hasOwnProperty(key)) { + const matcher = extendedMatchers[key]; + jasmineMatchers[key] = function (util: any, customEqualityTester: any) { + return { + compare: function (actual: any, expected: any) { + const result = matcher(actual, expected); + return result.pass; + } + }; + } + } + }); + jasmine.addMatchers(jasmineMatchers); + }; + + jasmine.addMatchers({ + toHaveBeenCalledTimes: function(util: any, customEqualityTester: any) { + return { + compare: function (actual: any, expected: any) { + return expected.calls.count() === actual; + } + }; + }, + lastCalledWith: function(util: any, customEqualityTester: any) { + return { + compare: function (actual: any, expected: any) { + return util.equals(actual, expected.calls.last().args); + } + }; + }, + toHaveBeenLastCalledWith: function(util: any, customEqualityTester: any) { + return { + compare: function (actual: any, expected: any) { + return util.equals(actual, expected.calls.last().args); + } + }; + }, + toBeInstanceOf: function(util: any, customEqualityTester: any) { + return { + compare: function (actual: any, expected: any) { + return actual instanceof expected; + } + }; + }, + toContainEqual: function(util: any, customEqualityTester: any) { + return { + compare: function (actual: any, expected: any) { + if (!Array.isArray(actual)) { + return false; + } + return actual.filter(a => util.equals(a, expected)).length > 0; + } + }; + }, + toHaveLength: function(util: any, customEqualityTester: any) { + return { + compare: function (actual: any, expected: any) { + return actual.length === expected; + } + }; + }, + toHaveProperty: function(util: any, customEqualityTester: any) { + return { + compare: function (actual: any, expected: any, expectedValue: any) { + const split: string[] = Array.isArray(expected) ? expected : expected.split('.'); + let value = null; + let hasKey = false; + for (let i = 0; i < split.length; i++) { + const prop = split[i]; + const isIndex = typeof prop === 'number'; + if (value) { + hasKey = isIndex ? Array.isArray(value) && (value as any).length > prop : + Object.keys(value).filter(a => util.equals(a, prop)).length > 0; + value = value[prop]; + } else { + hasKey = isIndex ? Array.isArray(actual) && (actual as any).length > prop : + Object.keys(actual).filter(a => util.equals(a, prop)).length > 0; + value = actual[prop]; + } + if (!hasKey) { + return false; + } + } + + if (expectedValue !== undefined) { + return util.equals(expectedValue, value); + } else { + return true; + } + } + }; + }, + toMatchObject: function(util: any, customEqualityTester: any) { + return { + compare: function(actual: any, expected: any) { + Object.keys(expected).forEach(key => { + if (expected.hasOwnProperty(key)) { + if (!util.equals(actual[key], expected[key]) && + !util.toMatch(actual[key], expected[key])) { + return false; + } + } + }); + return true; + } + }; + }, + }); + + expect.assertions = function (numbers: number) { + if (typeof numbers !== 'number') { + return; + } + const currentTest = global.Mocha.__zone_symbol__test; + assertions.push({ test: currentTest, numbers }); + } + + expect.hasAssertions = function () { + const currentTest = global.Mocha.__zone_symbol__test; + assertions.push({ test: currentTest, numbers: 1 }); + } + + if (!global.Mocha.__zone_symbol__afterEach) { + global.Mocha.__zone_symbol__afterEach = []; + } + + global.Mocha.__zone_symbol__afterEach.push((test: any) => { + // check assertions + for (let i = 0; i < assertions.length; i++) { + const ass = assertions[i]; + if (ass.test === test) { + assertions.splice(i, 1); + const actual = jasmine.__zone_symbol__expect_assertions; + jasmine.__zone_symbol__expect_assertions = 0; + if (ass.numbers != actual) { + throw new Error(`Assertions failed, expect should be called ${ + ass.numbers} times, it was actual called ${actual} times.`); + } + return; + } + } + }); +} diff --git a/lib/mocha/jest-bridge/jest.spy.ts b/lib/mocha/jest-bridge/jest.spy.ts new file mode 100644 index 000000000..151c60df3 --- /dev/null +++ b/lib/mocha/jest-bridge/jest.spy.ts @@ -0,0 +1,115 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +export function mappingSpy(jest: any, jasmine: any, global: any) { + function createSpy(spyFactory: (implFn?: Function) => any, implFn?: Function) { + const spy = jasmine.createSpy('jestSpy', implFn); + spy.defaultFn = implFn; + const instances: any[] = []; + const mockFn: any = function MockFn() { + if (this instanceof MockFn) { + instances.push(this); + } else { + let fn = spy.defaultFn; + if (spy.onceFns && spy.onceFns.length > 0) { + fn = spy.onceFns.shift(); + } + const args = Array.prototype.slice.call(arguments); + if (fn) { + return spy.and.callFake(fn).apply(this, args); + } else { + return spy.and.callThrough().apply(this, args); + } + } + }; + mockFn.getMockName = function() { + return spy.mockName || 'jestSpy'; + }; + mockFn.mockName = function(name: string) { + spy.updateArgs({identity: name}); + spy.mockName = name; + return this; + }; + mockFn.mock = {instances}; + Object.defineProperty(mockFn.mock, 'calls', { + configurable: true, + enumerable: true, + get: function() { + return spy.calls.allArgs(); + } + }); + Object.defineProperty(mockFn, 'calls', { + configurable: true, + enumerable: true, + get: function() { + return spy.calls; + } + }); + mockFn.mockClear = function() { + spy.calls.length = 0; + instances.length = 0; + return this; + }; + mockFn.mockReset = function() { + spy.calls.length = 0; + instances.length = 0; + return this; + }; + mockFn.mockImplementation = function(fn: Function) { + spy.defaultFn = fn; + return this; + }; + mockFn.mockImplementationOnce = function(fn: Function) { + if (!spy.onceFns) { + spy.onceFns = []; + } + spy.onceFns.push(fn); + return this; + }; + mockFn.mockReturnThis = function() { + return mockFn.mockImplementation(function() { + return this; + }); + }; + mockFn.mockReturnValue = function(value: any) { + return mockFn.mockImplementation(function() { + return value; + }); + }; + mockFn.mockReturnValueOnce = function(value: any) { + return mockFn.mockImplementationOnce(function() { + return value; + }); + }; + mockFn.mockResolvedValue = function(value: any) { + return mockFn.mockReturnValue(Promise.resolve(value)); + }; + mockFn.mockResolvedValueOnce = function(value: any) { + return mockFn.mockReturnValueOnce(Promise.resolve(value)); + }; + mockFn.mockRejectedValue = function(value: any) { + return mockFn.mockReturnValue(Promise.reject(value)); + }; + mockFn.mockRejectedValueOnce = function(value: any) { + return mockFn.mockReturnValueOnce(Promise.reject(value)); + }; + mockFn.mockRestore = function() { + global.Mocha.clearSpies(global.Mocha.__zone_symbol__current_ctx); + }; + + return mockFn; + } + + jest.fn = function(implFn?: Function) { + return createSpy((implFn?: Function) => jasmine.createSpy('jestSpy', implFn), implFn); + }; + + jest.spyOn = function(obj: any, methodName: string, accessType?: string) { + return accessType ? createSpy(() => global['spyOnProperty'](obj, methodName, accessType)) : + createSpy(() => global['spyOn'](obj, methodName)); + }; +} diff --git a/lib/mocha/jest-bridge/jest.ts b/lib/mocha/jest-bridge/jest.ts new file mode 100644 index 000000000..01569171e --- /dev/null +++ b/lib/mocha/jest-bridge/jest.ts @@ -0,0 +1,25 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {mappingBDD} from './jest.bdd'; +import {expandExpect} from './jest.expect'; +import {mappingSpy} from './jest.spy'; + +Zone.__load_patch('jest2mocha', (global: any) => { + let jest = global['jest']; + if (typeof jest !== 'undefined') { + // jasmine already loaded, just return + return; + } + // create a jasmine global object + jest = global['jest'] = {}; + jest['__zone_symbol__isBridge'] = true; + // BDD mapping + mappingBDD(jest, global.Mocha, global); + expandExpect(global); + mappingSpy(jest, jasmine, global); +}); \ No newline at end of file diff --git a/lib/mocha/mocha-patch.ts b/lib/mocha/mocha-patch.ts index 72cf60878..6dce9627c 100644 --- a/lib/mocha/mocha-patch.ts +++ b/lib/mocha/mocha-patch.ts @@ -85,6 +85,11 @@ Zone.__load_patch('Mocha', (global: any, Zone: ZoneType, api: _ZonePrivate) => { this.afterEach('afterEach clear spies', function() { if (this.test && this.test.ctx && this.test.currentTest) { Mocha.clearSpies(this.test.ctx.currentTest); + if (Mocha.__zone_symbol__afterEach) { + Mocha.__zone_symbol__afterEach.forEach((afterEachCallback: any) => { + afterEachCallback(this.test.ctx.currentTest); + }); + } } }); Mocha.__zone_symbol__suite = this; @@ -106,6 +111,8 @@ Zone.__load_patch('Mocha', (global: any, Zone: ZoneType, api: _ZonePrivate) => { if (test && typeof test.timeout === 'function' && typeof Mocha.__zone_symbol__TIMEOUT === 'number') { test.timeout(Mocha.__zone_symbol__TIMEOUT); + // clear timeout, until user set jasmine.DEFAULT_TIMEOUT_INTERVAL again + Mocha.__zone_symbol__TIMEOUT = null; } } diff --git a/lib/mocha/mocha.ts b/lib/mocha/mocha.ts index 75408c55a..384f1ef59 100644 --- a/lib/mocha/mocha.ts +++ b/lib/mocha/mocha.ts @@ -7,4 +7,5 @@ */ import './mocha-patch'; -import './jasmine-bridge/jasmine-bridge'; \ No newline at end of file +import './jasmine-bridge/jasmine-bridge'; +import './jest-bridge/jest-bridge'; \ No newline at end of file diff --git a/test/spec/mocha/jest-bridge.spec.ts b/test/spec/mocha/jest-bridge.spec.ts new file mode 100644 index 000000000..c2178b62a --- /dev/null +++ b/test/spec/mocha/jest-bridge.spec.ts @@ -0,0 +1,386 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +declare let jest: any; +declare function test(description: string, testFn: () => void): void; +describe('extend', () => { + (expect as any).extend({ + toBeDivisibleBy(received: any, argument: any) { + const pass = received % argument == 0; + if (pass) { + return { + message: () => `expected ${received} not to be divisible by ${argument}`, + pass: true, + }; + } else { + return { + message: () => `expected ${received} to be divisible by ${argument}`, + pass: false, + }; + } + }, + }); + + test('even and odd numbers', () => { + (expect(100) as any).toBeDivisibleBy(2); + (expect(101).not as any).toBeDivisibleBy(2); + }); +}); + +describe('expect', () => { + test('anything test', () => { + expect('test').toEqual((expect as any).anything()); + }); + + test('any(constructor)', () => { + expect('test').toEqual((expect as any).any(String)); + }); + + describe('arrayContaining', () => { + const expected = ['Alice', 'Bob']; + it('matches even if received contains additional elements', () => { + expect(['Alice', 'Bob', 'Eve']).toEqual((expect as any).arrayContaining(expected)); + }); + it('does not match if received does not contain expected elements', () => { + expect(['Bob', 'Eve']).not.toEqual((expect as any).arrayContaining(expected)); + }); + }); + + describe('Beware of a misunderstanding! A sequence of dice rolls', () => { + const expected = [1, 2, 3, 4, 5, 6]; + it('matches even with an unexpected number 7', () => { + expect([4, 1, 6, 7, 3, 5, 2, 5, 4, 6]) + .toEqual( + (expect as any).arrayContaining(expected), + ); + }); + it('does not match without an expected number 2', () => { + expect([4, 1, 6, 7, 3, 5, 7, 5, 4, 6]) + .not.toEqual( + (expect as any).arrayContaining(expected), + ); + }); + }); + + describe('assertions', () => { + test('calls both callbacks', () => { + (expect as any).assertions(2); + function callback1(data: any) { + expect(data).toBeTruthy(); + } + function callback2(data: any) { + expect(data).toBeTruthy(); + } + callback1('test'); + callback2('test'); + }); + + test('calls one callback', () => { + (expect as any).hasAssertions(); + function callback1(data: any) { + expect(data).toBeTruthy(); + } + callback1('test'); + }); + }); + + describe('objectContaining', () => { + test('onPress should object containing with the right thing', () => { + const onPress = {x: 100, y: 200, z: 300}; + (expect(onPress) as any) + .toEqual( + (expect as any).objectContaining({ + x: (expect as any).any(Number), + y: (expect as any).any(Number), + }), + ); + }); + }); + + describe('stringContaining', () => { + test('testStr should contain right string', () => { + expect('test1').toEqual((expect as any).stringContaining('test')); + }); + }); + + describe('stringMatching in arrayContaining', () => { + const expected = [ + (expect as any).stringMatching(/^Alic/), + (expect as any).stringMatching(/^[BR]ob/), + ]; + it('matches even if received contains additional elements', () => { + expect(['Alicia', 'Roberto', 'Evelina']) + .toEqual( + (expect as any).arrayContaining(expected), + ); + }); + it('does not match if received does not contain expected elements', () => { + expect(['Roberto', 'Evelina']) + .not.toEqual( + (expect as any).arrayContaining(expected), + ); + }); + }); + + describe('Promise', () => { + test('resolves to lemon', () => { + // make sure to add a return statement + return (expect(Promise.resolve('lemon')) as any).resolves.toBe('lemon'); + }); + + test('resolves to lemon with await', async () => { + await (expect(Promise.resolve('lemon')) as any).resolves.toBe('lemon'); + await (expect(Promise.resolve('lemon')) as any).resolves.not.toBe('octopus'); + }); + + test('rejects to octopus', () => { + // make sure to add a return statement + return (expect(Promise.reject(new Error('octopus'))) as any) + .rejects.toThrow( + 'octopus', + ); + }); + + test('rejects to octopus', async () => { + await (expect(Promise.reject(new Error('octopus'))) as any).rejects.toThrow('octopus'); + }); + }); + + test('instanceof', () => { + class A {} + (expect(new A()) as any).toBeInstanceOf(A); + (expect(() => {}) as any).toBeInstanceOf(Function); + }); + + test('toContainEqual', () => { + const myBeverage = {delicious: true, sour: false}; + (expect([{delicious: true, sour: false}, {delicious: false, sour: true}]) as any) + .toContainEqual(myBeverage); + }); + + test('toHaveLength', () => { + (expect([1, 2, 3]) as any).toHaveLength(3); + (expect('abc') as any).toHaveLength(3); + (expect('').not as any).toHaveLength(5); + }); + + describe('toMatchObject', () => { + const houseForSale = { + bath: true, + bedrooms: 4, + kitchen: { + amenities: ['oven', 'stove', 'washer'], + area: 20, + wallColor: 'white', + }, + }; + const desiredHouse = { + bath: true, + kitchen: { + amenities: ['oven', 'stove', 'washer'], + wallColor: (expect as any).stringMatching(/white|yellow/), + }, + }; + + test('the house has my desired features', () => { + (expect(houseForSale) as any).toMatchObject(desiredHouse); + }); + }); + + describe('toMatchObject applied to arrays arrays', () => { + test('the number of elements must match exactly', () => { + (expect([{foo: 'bar'}, {baz: 1}]) as any).toMatchObject([{foo: 'bar'}, {baz: 1}]); + }); + + // .arrayContaining "matches a received array which contains elements that + // are *not* in the expected array" + test('.toMatchObject does not allow extra elements', () => { + (expect([{foo: 'bar'}, {baz: 1}]) as any).toMatchObject([{foo: 'bar'}]); + }); + + test('.toMatchObject is called for each elements, so extra object properties are okay', () => { + (expect([{foo: 'bar'}, {baz: 1, extra: 'quux'}]) as any).toMatchObject([ + {foo: 'bar'}, + {baz: 1}, + ]); + }); + }); + + + describe('toHaveProperty', () => { + // Object containing house features to be tested + const houseForSale = { + bath: true, + bedrooms: 4, + kitchen: { + amenities: ['oven', 'stove', 'washer'], + area: 20, + wallColor: 'white', + }, + }; + + test('this house has my desired features', () => { + // Simple Referencing + (expect(houseForSale) as any).toHaveProperty('bath'); + (expect(houseForSale) as any).toHaveProperty('bedrooms', 4); + + (expect(houseForSale).not as any).toHaveProperty('pool'); + + // Deep referencing using dot notation + (expect(houseForSale) as any).toHaveProperty('kitchen.area', 20); + (expect(houseForSale) as any).toHaveProperty('kitchen.amenities', [ + 'oven', + 'stove', + 'washer', + ]); + + (expect(houseForSale).not as any).toHaveProperty('kitchen.open'); + + // Deep referencing using an array containing the keyPath + (expect(houseForSale) as any).toHaveProperty(['kitchen', 'area'], 20); + (expect(houseForSale) as any) + .toHaveProperty( + ['kitchen', 'amenities'], + ['oven', 'stove', 'washer'], + ); + (expect(houseForSale) as any).toHaveProperty(['kitchen', 'amenities', 0], 'oven'); + + (expect(houseForSale).not as any).toHaveProperty(['kitchen', 'open']); + }); + }); + + describe('jest.fn', () => { + test('mock.calls', () => { + const mockFn = jest.fn(); + mockFn('arg1', 'arg2'); + mockFn('arg3', 'arg4'); + expect(mockFn.mock.calls).toEqual([['arg1', 'arg2'], ['arg3', 'arg4']]); + }); + + test('mock.instances', () => { + const mockFn = jest.fn(); + + const a = new mockFn(); + const b = new mockFn(); + + expect(mockFn.mock.instances[0]).toBe(a); // true + expect(mockFn.mock.instances[1]).toBe(b); // true + mockFn.mockClear(); + expect(mockFn.mock.instances.length).toBe(0); + }); + + test('mock.mockImplementation', () => { + const mockFn = jest.fn().mockImplementation((scalar: any) => 42 + scalar); + + const a = mockFn(0); + const b = mockFn(1); + + a === 42; // true + b === 43; // true + + expect(mockFn.mock.calls[0][0]).toBe(0); // true + expect(mockFn.mock.calls[1][0]).toBe(1); // true + }); + + test('mock.mockImplementationOnce', () => { + let myMockFn = jest.fn() + .mockImplementationOnce((cb: any) => cb(null, true)) + .mockImplementationOnce((cb: any) => cb(null, false)); + + const logs: any[] = []; + myMockFn((err: any, val: any) => logs.push(val)); // true + myMockFn((err: any, val: any) => logs.push(val)); // false + expect(logs).toEqual([true, false]); + + myMockFn = jest.fn(() => 'default') + .mockImplementationOnce(() => 'first call') + .mockImplementationOnce(() => 'second call'); + + // 'first call', 'second call', 'default', 'default' + logs.length = 0; + logs.push(myMockFn(), myMockFn(), myMockFn(), myMockFn()); + expect(logs).toEqual(['first call', 'second call', 'default', 'default']); + }); + + test('toHaveBeenCalled', () => { + const mockFn = jest.fn(); + mockFn(); + expect(mockFn).toHaveBeenCalled(); + mockFn(1); + expect(mockFn).toHaveBeenCalledWith(1); + }); + + test('mockReturnThis', () => { + const mockFn = jest.fn(); + mockFn.mockReturnThis(); + expect(mockFn()).toBeUndefined(); + }); + + test('mockReturnValue', () => { + const mockFn = jest.fn(); + mockFn.mockReturnValue(30); + expect(mockFn()).toBe(30); + }); + + test('mockReturnValueOnce', () => { + const myMockFn = jest.fn() + .mockReturnValue('default') + .mockReturnValueOnce('first call') + .mockReturnValueOnce('second call'); + + // 'first call', 'second call', 'default', 'default' + const logs: string[] = []; + logs.push(myMockFn(), myMockFn(), myMockFn(), myMockFn()); + expect(logs).toEqual(['first call', 'second call', 'default', 'default']); + }); + + test('mockResolvedValue', async () => { + const asyncMock = jest.fn().mockResolvedValue(43); + + const result = await asyncMock(); // 43 + expect(result).toBe(43); + }); + + test('mockResolvedValueOnce', async () => { + const asyncMock = jest.fn() + .mockResolvedValue('default') + .mockResolvedValueOnce('first call') + .mockResolvedValueOnce('second call'); + + const logs: string[] = []; + logs.push(await asyncMock()); // first call + logs.push(await asyncMock()); // second call + logs.push(await asyncMock()); // default + logs.push(await asyncMock()); // default + expect(logs).toEqual(['first call', 'second call', 'default', 'default']); + }); + + test('mockRejectedValue', async () => { + const asyncMock = jest.fn().mockRejectedValue(new Error('Async error')); + + try { + await asyncMock(); // throws "Async error" + } catch (err) { + expect(err.message).toEqual('Async error'); + } + }); + + test('mockRejectedValueOnce', async () => { + const asyncMock = jest.fn() + .mockResolvedValueOnce('first call') + .mockRejectedValueOnce(new Error('Async error')); + + try { + const first = await asyncMock(); + expect(first).toEqual('first call'); + await asyncMock(); // throws "Async error" + } catch (err) { + expect(err.message).toEqual('Async error'); + } + }); + }); +}); \ No newline at end of file diff --git a/test/spec/mocha/mocha-node-test-entry-point.ts b/test/spec/mocha/mocha-node-test-entry-point.ts index 7c0174ec0..7dad9ced7 100644 --- a/test/spec/mocha/mocha-node-test-entry-point.ts +++ b/test/spec/mocha/mocha-node-test-entry-point.ts @@ -8,4 +8,5 @@ import '../../../lib/node/rollup-main'; import '../../../lib/mocha/mocha-node-checker'; import '../../../lib/testing/zone-testing'; -import './jasmine-bridge.spec'; \ No newline at end of file +import './jasmine-bridge.spec'; +import './jest-bridge.spec'; \ No newline at end of file