From daac33cdc84c6a882ec04c3009e6a230153716b0 Mon Sep 17 00:00:00 2001 From: JiaLiPassion Date: Sat, 1 Feb 2020 00:08:40 +0900 Subject: [PATCH] feat: add basic jest support (#35080) PR Close #35080 --- .circleci/config.yml | 1 + packages/zone.js/lib/jasmine/jasmine.ts | 7 +- packages/zone.js/lib/jest/jest.ts | 124 +++++++++++++++++++ packages/zone.js/lib/testing/zone-testing.ts | 3 +- packages/zone.js/package.json | 4 +- packages/zone.js/test/jest/jest-zone.js | 2 + packages/zone.js/test/jest/jest.config.js | 3 + packages/zone.js/test/jest/jest.spec.js | 39 ++++++ 8 files changed, 180 insertions(+), 3 deletions(-) create mode 100644 packages/zone.js/lib/jest/jest.ts create mode 100644 packages/zone.js/test/jest/jest-zone.js create mode 100644 packages/zone.js/test/jest/jest.config.js create mode 100644 packages/zone.js/test/jest/jest.spec.js diff --git a/.circleci/config.yml b/.circleci/config.yml index 9c71ec2b71..317d3df312 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -742,6 +742,7 @@ jobs: cp dist/bin/packages/zone.js/npm_package/dist/zone-mix.js ./packages/zone.js/test/extra/ && cp dist/bin/packages/zone.js/npm_package/dist/zone-patch-electron.js ./packages/zone.js/test/extra/ && yarn --cwd packages/zone.js electrontest + - run: yarn --cwd packages/zone.js jesttest # Windows jobs # Docs: https://circleci.com/docs/2.0/hello-world-windows/ diff --git a/packages/zone.js/lib/jasmine/jasmine.ts b/packages/zone.js/lib/jasmine/jasmine.ts index e5c766b687..9d2c18f2e9 100644 --- a/packages/zone.js/lib/jasmine/jasmine.ts +++ b/packages/zone.js/lib/jasmine/jasmine.ts @@ -9,6 +9,7 @@ /// 'use strict'; +declare let jest: any; ((_global: any) => { const __extends = function(d: any, b: any) { for (const p in b) @@ -19,6 +20,11 @@ // Patch jasmine's describe/it/beforeEach/afterEach functions so test code always runs // in a testZone (ProxyZone). (See: angular/zone.js#91 & angular/angular#10503) if (!Zone) throw new Error('Missing: zone.js'); + if (typeof jest !== 'undefined') { + // return if jasmine is a light implementation inside jest + // in this case, we are running inside jest not jasmine + return; + } if (typeof jasmine == 'undefined') throw new Error('Missing: jasmine.js'); if ((jasmine as any)['__zone_patch__']) throw new Error(`'jasmine' has already been patched with 'Zone'.`); @@ -285,7 +291,6 @@ // This is the zone which will be used for running individual tests. // It will be a proxy zone, so that the tests function can retroactively install // different zones. - // Example: // - In beforeEach() do childZone = Zone.current.fork(...); // - In it() try to do fakeAsync(). The issue is that because the beforeEach forked the // zone outside of fakeAsync it will be able to escape the fakeAsync rules. diff --git a/packages/zone.js/lib/jest/jest.ts b/packages/zone.js/lib/jest/jest.ts new file mode 100644 index 0000000000..28e989c0d4 --- /dev/null +++ b/packages/zone.js/lib/jest/jest.ts @@ -0,0 +1,124 @@ +/** + * @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[]) { + const testFn = originalJestFn.apply(this, tableArgs); + return function(this: unknown, ...args: any[]) { + args[1] = wrapTestInZone(args[1]); + return testFn.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 `testProxyZone`. + */ + function wrapTestInZone(testBody: Function): Function { + if (typeof testBody !== 'function') { + return testBody; + } + // The `done` callback is only passed through if the function expects at least one argument. + // Note we have to make a function with correct number of arguments, otherwise jest will + // think that all functions are sync or async. + return function(this: unknown, ...args: any[]) { return proxyZone.run(testBody, this, args); }; + } + + ['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); + }; + }); +}); diff --git a/packages/zone.js/lib/testing/zone-testing.ts b/packages/zone.js/lib/testing/zone-testing.ts index c5ebd1ad3b..e8154dba82 100644 --- a/packages/zone.js/lib/testing/zone-testing.ts +++ b/packages/zone.js/lib/testing/zone-testing.ts @@ -11,6 +11,7 @@ import '../zone-spec/long-stack-trace'; import '../zone-spec/proxy'; import '../zone-spec/sync-test'; import '../jasmine/jasmine'; +import '../jest/jest'; import './async-testing'; import './fake-async'; -import './promise-testing'; \ No newline at end of file +import './promise-testing'; diff --git a/packages/zone.js/package.json b/packages/zone.js/package.json index e867922584..c47ada0cdd 100644 --- a/packages/zone.js/package.json +++ b/packages/zone.js/package.json @@ -17,6 +17,7 @@ "devDependencies": { "@types/node": "^10.9.4", "domino": "2.1.2", + "jest": "^25.1.0", "mocha": "^3.1.2", "mock-require": "3.0.3", "promises-aplus-tests": "^2.1.2", @@ -25,7 +26,8 @@ "scripts": { "promisetest": "tsc -p . && node ./promise-test.js", "promisefinallytest": "tsc -p . && mocha promise.finally.spec.js", - "electrontest": "cd test/extra && node electron.js" + "electrontest": "cd test/extra && node electron.js", + "jesttest": "jest --config ./test/jest/jest.config.js ./test/jest/jest.spec.js" }, "repository": { "type": "git", diff --git a/packages/zone.js/test/jest/jest-zone.js b/packages/zone.js/test/jest/jest-zone.js new file mode 100644 index 0000000000..0aac44f296 --- /dev/null +++ b/packages/zone.js/test/jest/jest-zone.js @@ -0,0 +1,2 @@ +require('../../../../dist/bin/packages/zone.js/npm_package/dist/zone'); +require('../../../../dist/bin/packages/zone.js/npm_package/dist/zone-testing'); diff --git a/packages/zone.js/test/jest/jest.config.js b/packages/zone.js/test/jest/jest.config.js new file mode 100644 index 0000000000..c798061d36 --- /dev/null +++ b/packages/zone.js/test/jest/jest.config.js @@ -0,0 +1,3 @@ +module.exports = { + setupFilesAfterEnv: ['./jest-zone.js'] +}; diff --git a/packages/zone.js/test/jest/jest.spec.js b/packages/zone.js/test/jest/jest.spec.js new file mode 100644 index 0000000000..13e3841279 --- /dev/null +++ b/packages/zone.js/test/jest/jest.spec.js @@ -0,0 +1,39 @@ +function assertInsideProxyZone() { + expect(Zone.current.name).toEqual('ProxyZone'); +} +function assertInsideSyncDescribeZone() { + expect(Zone.current.name).toEqual('syncTestZone for jest.describe'); +} +describe('describe', () => { + assertInsideSyncDescribeZone(); + beforeEach(() => { assertInsideProxyZone(); }); + beforeAll(() => { assertInsideProxyZone(); }); + afterEach(() => { assertInsideProxyZone(); }); + afterAll(() => { assertInsideProxyZone(); }); +}); +describe.each([[1, 2]])('describe.each', (arg1, arg2) => { + assertInsideSyncDescribeZone(); + expect(arg1).toBe(1); + expect(arg2).toBe(2); +}); +describe('test', () => { + it('it', () => { assertInsideProxyZone(); }); + it.each([[1, 2]])('it.each', (arg1, arg2) => { + assertInsideProxyZone(); + expect(arg1).toBe(1); + expect(arg2).toBe(2); + }); + test('test', () => { assertInsideProxyZone(); }); + test.each([[]])('test.each', () => { assertInsideProxyZone(); }); +}); + +it('it', () => { assertInsideProxyZone(); }); +it.each([[1, 2]])('it.each', (arg1, arg2) => { + assertInsideProxyZone(); + expect(arg1).toBe(1); + expect(arg2).toBe(2); +}); +test('test', () => { assertInsideProxyZone(); }); +test.each([[]])('test.each', () => { assertInsideProxyZone(); }); + +test.todo('todo');