JiaLiPassion 4374931b0e fix(zone.js): zone.js patch jest should handle done correctly (#36022)
`zone.js` supports jest `test.each()` methods, but it
introduces a bug, which is the `done()` function will not be handled correctly.

```
it('should work with done', done => {
  // done will be undefined.
});
```

The reason is the logic of monkey patching `test` method is different from `jasmine` patch

// jasmine patch
```
return testBody.length === 0
   ? () => testProxyZone.run(testBody, null)
   : done => testProxyZone.run(testBody, null, [done]);
```

// jest patch
```
 return function(...args) {
   return testProxyZone.run(testBody, null, args);
 };
```

the purpose of this change is to handle the following cases.

```
test.each([1, 2])('test.each', (arg1, arg2) => {
  expect(arg1).toBe(1);
  expect(arg2).toBe(2);
});
```

so in jest, it is a little complex, because the `testBody`'s parameter may be bigger than 1, so the
logic in `jasmine`

```
return testBody.length === 0
   ? () => testProxyZone.run(testBody, null)
   : done => testProxyZone.run(testBody, null, [done]);
```
will not work for `test.each` in jest.

So in this PR, I created a dynamic `Function` to return the correct length of paramters (which is required by jest core), to handle
1. normal `test` with or without `done`.
2. each with parameters with or without done.

PR Close #36022
2020-04-07 15:22:16 -07:00

129 lines
4.2 KiB
TypeScript

/**
* @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
*/
'use strict';
Zone.__load_patch('jest', (context: any, Zone: ZoneType) => {
if (typeof jest === 'undefined' || jest['__zone_patch__']) {
return;
}
jest['__zone_patch__'] = true;
if (typeof Zone === 'undefined') {
throw new Error('Missing Zone.js');
}
const ProxyZoneSpec = (Zone as any)['ProxyZoneSpec'];
const SyncTestZoneSpec = (Zone as any)['SyncTestZoneSpec'];
if (!ProxyZoneSpec) {
throw new Error('Missing ProxyZoneSpec');
}
const rootZone = Zone.current;
const syncZone = rootZone.fork(new SyncTestZoneSpec('jest.describe'));
const proxyZone = rootZone.fork(new ProxyZoneSpec());
function wrapDescribeFactoryInZone(originalJestFn: Function) {
return function(this: unknown, ...tableArgs: any[]) {
const originalDescribeFn = originalJestFn.apply(this, tableArgs);
return function(this: unknown, ...args: any[]) {
args[1] = wrapDescribeInZone(args[1]);
return originalDescribeFn.apply(this, args);
};
};
}
function wrapTestFactoryInZone(originalJestFn: Function) {
return function(this: unknown, ...tableArgs: any[]) {
return function(this: unknown, ...args: any[]) {
args[1] = wrapTestInZone(args[1]);
return originalJestFn.apply(this, tableArgs).apply(this, args);
};
};
}
/**
* Gets a function wrapping the body of a jest `describe` block to execute in a
* synchronous-only zone.
*/
function wrapDescribeInZone(describeBody: Function): Function {
return function(this: unknown, ...args: any[]) {
return syncZone.run(describeBody, this, args);
};
}
/**
* Gets a function wrapping the body of a jest `it/beforeEach/afterEach` block to
* execute in a ProxyZone zone.
* This will run in the `proxyZone`.
*/
function wrapTestInZone(testBody: Function): Function {
if (typeof testBody !== 'function') {
return testBody;
}
const wrappedFunc = function() {
return proxyZone.run(testBody, null, arguments as any);
};
// Update the length of wrappedFunc to be the same as the length of the testBody
// So jest core can handle whether the test function has `done()` or not correctly
Object.defineProperty(
wrappedFunc, 'length', {configurable: true, writable: true, enumerable: false});
wrappedFunc.length = testBody.length;
return wrappedFunc;
}
['describe', 'xdescribe', 'fdescribe'].forEach(methodName => {
let originalJestFn: Function = context[methodName];
if (context[Zone.__symbol__(methodName)]) {
return;
}
context[Zone.__symbol__(methodName)] = originalJestFn;
context[methodName] = function(this: unknown, ...args: any[]) {
args[1] = wrapDescribeInZone(args[1]);
return originalJestFn.apply(this, args);
};
context[methodName].each = wrapDescribeFactoryInZone((originalJestFn as any).each);
});
context.describe.only = context.fdescribe;
context.describe.skip = context.xdescribe;
['it', 'xit', 'fit', 'test', 'xtest'].forEach(methodName => {
let originalJestFn: Function = context[methodName];
if (context[Zone.__symbol__(methodName)]) {
return;
}
context[Zone.__symbol__(methodName)] = originalJestFn;
context[methodName] = function(this: unknown, ...args: any[]) {
args[1] = wrapTestInZone(args[1]);
return originalJestFn.apply(this, args);
};
context[methodName].each = wrapTestFactoryInZone((originalJestFn as any).each);
context[methodName].todo = (originalJestFn as any).todo;
});
context.it.only = context.fit;
context.it.skip = context.xit;
context.test.only = context.fit;
context.test.skip = context.xit;
['beforeEach', 'afterEach', 'beforeAll', 'afterAll'].forEach(methodName => {
let originalJestFn: Function = context[methodName];
if (context[Zone.__symbol__(methodName)]) {
return;
}
context[Zone.__symbol__(methodName)] = originalJestFn;
context[methodName] = function(this: unknown, ...args: any[]) {
args[0] = wrapTestInZone(args[0]);
return originalJestFn.apply(this, args);
};
});
});