Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -748,7 +748,8 @@ jobs:
cp dist/bin/packages/zone.js/npm_package/bundles/zone-mix.umd.js ./packages/zone.js/test/extra/ &&
cp dist/bin/packages/zone.js/npm_package/bundles/zone-patch-electron.umd.js ./packages/zone.js/test/extra/ &&
yarn --cwd packages/zone.js electrontest
- run: yarn --cwd packages/zone.js jesttest
- run: yarn --cwd packages/zone.js jest:test
- run: yarn --cwd packages/zone.js jest:nodetest
- run: yarn --cwd packages/zone.js/test/typings install --frozen-lockfile --non-interactive
- run: yarn --cwd packages/zone.js/test/typings test

Expand Down
184 changes: 175 additions & 9 deletions packages/zone.js/lib/jest/jest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,13 @@

'use strict';

Zone.__load_patch('jest', (context: any, Zone: ZoneType) => {
Zone.__load_patch('jest', (context: any, Zone: ZoneType, api: _ZonePrivate) => {
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'];

Expand All @@ -29,7 +24,8 @@ Zone.__load_patch('jest', (context: any, Zone: ZoneType) => {

const rootZone = Zone.current;
const syncZone = rootZone.fork(new SyncTestZoneSpec('jest.describe'));
const proxyZone = rootZone.fork(new ProxyZoneSpec());
const proxyZoneSpec = new ProxyZoneSpec();
const proxyZone = rootZone.fork(proxyZoneSpec);

function wrapDescribeFactoryInZone(originalJestFn: Function) {
return function(this: unknown, ...tableArgs: any[]) {
Expand Down Expand Up @@ -65,11 +61,20 @@ Zone.__load_patch('jest', (context: any, Zone: ZoneType) => {
* execute in a ProxyZone zone.
* This will run in the `proxyZone`.
*/
function wrapTestInZone(testBody: Function): Function {
function wrapTestInZone(testBody: Function, isTestFunc = false): Function {
if (typeof testBody !== 'function') {
return testBody;
}
const wrappedFunc = function() {
if ((Zone as any)[api.symbol('useFakeTimersCalled')] === true && testBody &&
!(testBody as any).isFakeAsync) {
// jest.useFakeTimers is called, run into fakeAsyncTest automatically.
const fakeAsyncModule = (Zone as any)[Zone.__symbol__('fakeAsyncTest')];
if (fakeAsyncModule && typeof fakeAsyncModule.fakeAsync === 'function') {
testBody = fakeAsyncModule.fakeAsync(testBody);
}
}
proxyZoneSpec.isTestFunc = isTestFunc;
return proxyZone.run(testBody, null, arguments as any);
};
// Update the length of wrappedFunc to be the same as the length of the testBody
Expand Down Expand Up @@ -102,7 +107,7 @@ Zone.__load_patch('jest', (context: any, Zone: ZoneType) => {
}
context[Zone.__symbol__(methodName)] = originalJestFn;
context[methodName] = function(this: unknown, ...args: any[]) {
args[1] = wrapTestInZone(args[1]);
args[1] = wrapTestInZone(args[1], true);
return originalJestFn.apply(this, args);
};
context[methodName].each = wrapTestFactoryInZone((originalJestFn as any).each);
Expand All @@ -125,4 +130,165 @@ Zone.__load_patch('jest', (context: any, Zone: ZoneType) => {
return originalJestFn.apply(this, args);
};
});

(Zone as any).patchJestObject = function patchJestObject(Timer: any, isModern = false) {
// check whether currently the test is inside fakeAsync()
function isPatchingFakeTimer() {
const fakeAsyncZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec');
return !!fakeAsyncZoneSpec;
}

// check whether the current function is inside `test/it` or other methods
// such as `describe/beforeEach`
function isInTestFunc() {
const proxyZoneSpec = Zone.current.get('ProxyZoneSpec');
return proxyZoneSpec && proxyZoneSpec.isTestFunc;
}

if (Timer[api.symbol('fakeTimers')]) {
return;
}

Timer[api.symbol('fakeTimers')] = true;
// patch jest fakeTimer internal method to make sure no console.warn print out
api.patchMethod(Timer, '_checkFakeTimers', delegate => {
return function(self: any, args: any[]) {
if (isPatchingFakeTimer()) {
return true;
} else {
return delegate.apply(self, args);
}
}
});

// patch useFakeTimers(), set useFakeTimersCalled flag, and make test auto run into fakeAsync
api.patchMethod(Timer, 'useFakeTimers', delegate => {
return function(self: any, args: any[]) {
(Zone as any)[api.symbol('useFakeTimersCalled')] = true;
if (isModern || isInTestFunc()) {
return delegate.apply(self, args);
}
return self;
}
});

// patch useRealTimers(), unset useFakeTimers flag
api.patchMethod(Timer, 'useRealTimers', delegate => {
return function(self: any, args: any[]) {
(Zone as any)[api.symbol('useFakeTimersCalled')] = false;
if (isModern || isInTestFunc()) {
return delegate.apply(self, args);
}
return self;
}
});

// patch setSystemTime(), call setCurrentRealTime() in the fakeAsyncTest
api.patchMethod(Timer, 'setSystemTime', delegate => {
return function(self: any, args: any[]) {
const fakeAsyncZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec');
if (fakeAsyncZoneSpec && isPatchingFakeTimer()) {
fakeAsyncZoneSpec.setCurrentRealTime(args[0]);
} else {
return delegate.apply(self, args);
}
}
});

// patch getSystemTime(), call getCurrentRealTime() in the fakeAsyncTest
api.patchMethod(Timer, 'getRealSystemTime', delegate => {
return function(self: any, args: any[]) {
const fakeAsyncZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec');
if (fakeAsyncZoneSpec && isPatchingFakeTimer()) {
return fakeAsyncZoneSpec.getCurrentRealTime(args[0]);
} else {
return delegate.apply(self, args);
}
}
});

// patch runAllTicks(), run all microTasks inside fakeAsync
api.patchMethod(Timer, 'runAllTicks', delegate => {
return function(self: any, args: any[]) {
const fakeAsyncZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec');
if (fakeAsyncZoneSpec) {
fakeAsyncZoneSpec.flushMicrotasks();
} else {
return delegate.apply(self, args);
}
}
});

// patch runAllTimers(), run all macroTasks inside fakeAsync
api.patchMethod(Timer, 'runAllTimers', delegate => {
return function(self: any, args: any[]) {
const fakeAsyncZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec');
if (fakeAsyncZoneSpec) {
fakeAsyncZoneSpec.flush(100, true);
} else {
return delegate.apply(self, args);
}
}
});

// patch advanceTimersByTime(), call tick() in the fakeAsyncTest
api.patchMethod(Timer, 'advanceTimersByTime', delegate => {
return function(self: any, args: any[]) {
const fakeAsyncZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec');
if (fakeAsyncZoneSpec) {
fakeAsyncZoneSpec.tick(args[0]);
} else {
return delegate.apply(self, args);
}
}
});

// patch runOnlyPendingTimers(), call flushOnlyPendingTimers() in the fakeAsyncTest
api.patchMethod(Timer, 'runOnlyPendingTimers', delegate => {
return function(self: any, args: any[]) {
const fakeAsyncZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec');
if (fakeAsyncZoneSpec) {
fakeAsyncZoneSpec.flushOnlyPendingTimers();
} else {
return delegate.apply(self, args);
}
}
});

// patch advanceTimersToNextTimer(), call tickToNext() in the fakeAsyncTest
api.patchMethod(Timer, 'advanceTimersToNextTimer', delegate => {
return function(self: any, args: any[]) {
const fakeAsyncZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec');
if (fakeAsyncZoneSpec) {
fakeAsyncZoneSpec.tickToNext(args[0]);
} else {
return delegate.apply(self, args);
}
}
});

// patch clearAllTimers(), call removeAllTimers() in the fakeAsyncTest
api.patchMethod(Timer, 'clearAllTimers', delegate => {
return function(self: any, args: any[]) {
const fakeAsyncZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec');
if (fakeAsyncZoneSpec) {
fakeAsyncZoneSpec.removeAllTimers();
} else {
return delegate.apply(self, args);
}
}
});

// patch getTimerCount(), call getTimerCount() in the fakeAsyncTest
api.patchMethod(Timer, 'getTimerCount', delegate => {
return function(self: any, args: any[]) {
const fakeAsyncZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec');
if (fakeAsyncZoneSpec) {
return fakeAsyncZoneSpec.getTimerCount();
} else {
return delegate.apply(self, args);
}
}
});
}
});
4 changes: 3 additions & 1 deletion packages/zone.js/lib/testing/fake-async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Zone.__load_patch('fakeasync', (global: any, Zone: ZoneType, api: _ZonePrivate)
*/
function fakeAsync(fn: Function): (...args: any[]) => any {
// Not using an arrow function to preserve context passed from call site
return function(this: unknown, ...args: any[]) {
const fakeAsyncFn: any = function(this: unknown, ...args: any[]) {
const proxyZoneSpec = ProxyZoneSpec.assertPresent();
if (Zone.current.get('FakeAsyncTestZoneSpec')) {
throw new Error('fakeAsync() calls can not be nested');
Expand Down Expand Up @@ -93,6 +93,8 @@ Zone.__load_patch('fakeasync', (global: any, Zone: ZoneType, api: _ZonePrivate)
resetFakeAsyncZone();
}
};
(fakeAsyncFn as any).isFakeAsync = true;
return fakeAsyncFn;
}

function _getFakeAsyncZoneSpec(): any {
Expand Down
76 changes: 76 additions & 0 deletions packages/zone.js/lib/zone-spec/fake-async-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ class Scheduler {
this._currentRealTime = realTime;
}

getRealSystemTime() {
return OriginalDate.now();
}

scheduleFunction(cb: Function, delay: number, options?: {
args?: any[],
isPeriodic?: boolean,
Expand Down Expand Up @@ -145,6 +149,27 @@ class Scheduler {
}
}

removeAll(): void {
this._schedulerQueue = [];
}

getTimerCount(): number {
return this._schedulerQueue.length;
}

tickToNext(step: number = 1, doTick?: (elapsed: number) => void, tickOptions?: {
processNewMacroTasksSynchronously: boolean
}) {
if (this._schedulerQueue.length < step) {
return;
}
// Find the last task currently queued in the scheduler queue and tick
// till that time.
const startTime = this._currentTime;
const targetTask = this._schedulerQueue[step - 1];
this.tick(targetTask.endTime - startTime, doTick, tickOptions);
}

tick(millis: number = 0, doTick?: (elapsed: number) => void, tickOptions?: {
processNewMacroTasksSynchronously: boolean
}): void {
Expand Down Expand Up @@ -212,6 +237,18 @@ class Scheduler {
}
}

flushOnlyPendingTimers(doTick?: (elapsed: number) => void): number {
if (this._schedulerQueue.length === 0) {
return 0;
}
// Find the last task currently queued in the scheduler queue and tick
// till that time.
const startTime = this._currentTime;
const lastTask = this._schedulerQueue[this._schedulerQueue.length - 1];
this.tick(lastTask.endTime - startTime, doTick, {processNewMacroTasksSynchronously: false});
return this._currentTime - startTime;
}

flush(limit = 20, flushPeriodic = false, doTick?: (elapsed: number) => void): number {
if (flushPeriodic) {
return this.flushPeriodic(doTick);
Expand Down Expand Up @@ -401,6 +438,10 @@ class FakeAsyncTestZoneSpec implements ZoneSpec {
this._scheduler.setCurrentRealTime(realTime);
}

getRealSystemTime() {
return this._scheduler.getRealSystemTime();
}

static patchDate() {
if (!!global[Zone.__symbol__('disableDatePatching')]) {
// we don't want to patch global Date
Expand Down Expand Up @@ -450,6 +491,20 @@ class FakeAsyncTestZoneSpec implements ZoneSpec {
FakeAsyncTestZoneSpec.resetDate();
}

tickToNext(steps: number = 1, doTick?: (elapsed: number) => void, tickOptions: {
processNewMacroTasksSynchronously: boolean
} = {processNewMacroTasksSynchronously: true}): void {
if (steps <= 0) {
return;
}
FakeAsyncTestZoneSpec.assertInZone();
this.flushMicrotasks();
this._scheduler.tickToNext(steps, doTick, tickOptions);
if (this._lastError !== null) {
this._resetLastErrorAndThrow();
}
}

tick(millis: number = 0, doTick?: (elapsed: number) => void, tickOptions: {
processNewMacroTasksSynchronously: boolean
} = {processNewMacroTasksSynchronously: true}): void {
Expand Down Expand Up @@ -486,6 +541,27 @@ class FakeAsyncTestZoneSpec implements ZoneSpec {
return elapsed;
}

flushOnlyPendingTimers(doTick?: (elapsed: number) => void): number {
FakeAsyncTestZoneSpec.assertInZone();
this.flushMicrotasks();
const elapsed = this._scheduler.flushOnlyPendingTimers(doTick);
if (this._lastError !== null) {
this._resetLastErrorAndThrow();
}
return elapsed;
}

removeAllTimers() {
FakeAsyncTestZoneSpec.assertInZone();
this._scheduler.removeAll();
this.pendingPeriodicTimers = [];
this.pendingTimers = [];
}

getTimerCount() {
return this._scheduler.getTimerCount() + this._microtasks.length;
}

// ZoneSpec implementation below.

name: string;
Expand Down
5 changes: 3 additions & 2 deletions packages/zone.js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,16 @@
"devDependencies": {
"@types/node": "^10.9.4",
"domino": "2.1.2",
"jest": "^25.1.0",
"jest": "^26.4",
"mocha": "^3.1.2",
"mock-require": "3.0.3",
"promises-aplus-tests": "^2.1.2",
"typescript": "4.0.2"
},
"scripts": {
"electrontest": "cd test/extra && node electron.js",
"jesttest": "jest --config ./test/jest/jest.config.js ./test/jest/jest.spec.js",
"jest:test": "jest --config ./test/jest/jest.config.js ./test/jest/jest.spec.js",
"jest:nodetest": "jest --config ./test/jest/jest.node.config.js ./test/jest/jest.spec.js",
"promisetest": "tsc -p . && node ./test/promise/promise-test.js",
"promisefinallytest": "tsc -p . && mocha ./test/promise/promise.finally.spec.js"
},
Expand Down
6 changes: 6 additions & 0 deletions packages/zone.js/test/jest/jest-zone-patch-fake-timer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const exportFakeTimersToSandboxGlobal = function(jestEnv) {
jestEnv.global.legacyFakeTimers = jestEnv.fakeTimers;
jestEnv.global.modernFakeTimers = jestEnv.fakeTimersModern;
};

module.exports = exportFakeTimersToSandboxGlobal;
Loading