From 5f6ef41cdd44dbacba99aacc9f3eb46eacdbdc3d Mon Sep 17 00:00:00 2001 From: "JiaLi.Passion" Date: Wed, 20 Jun 2018 12:55:48 +0900 Subject: [PATCH] feat(error): fix #975, can config how to load blacklist zone stack frames --- .travis.yml | 4 + MODULE.md | 51 +++++++ file-size-limit.json | 2 +- gulpfile.js | 12 ++ karma-base.conf.js | 2 + karma-build.conf.js | 3 +- lib/browser/define-property.ts | 2 +- lib/common/error-rewrite.ts | 240 ++++++++++++++++++++++----------- lib/common/utils.ts | 4 +- lib/node/node_util.ts | 2 +- package.json | 2 +- sauce-selenium3.conf.js | 2 +- sauce.conf.js | 2 +- test/browser/WebSocket.spec.ts | 2 +- test/common/Error.spec.ts | 65 +++++++-- test/main.ts | 28 ++-- test/node/fs.spec.ts | 61 +++++---- test/node/timer.spec.ts | 31 +++-- test/node_entry_point.ts | 1 + test/node_error_entry_point.ts | 41 ++++++ test/webdriver/test.sauce.js | 6 +- yarn.lock | 6 +- 22 files changed, 408 insertions(+), 161 deletions(-) create mode 100644 test/node_error_entry_point.ts diff --git a/.travis.yml b/.travis.yml index 802a256f9..2e51e3b18 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,9 +39,13 @@ script: - node_modules/.bin/karma start karma-build-sauce-mocha.conf.js --single-run - node_modules/.bin/karma start karma-dist-sauce-selenium3-jasmine.conf.js --single-run - node_modules/.bin/karma start karma-build-sauce-selenium3-mocha.conf.js --single-run + - node_modules/.bin/karma start karma-dist-sauce-jasmine3.conf.js --single-run --errorpolicy=disable + - node_modules/.bin/karma start karma-dist-sauce-jasmine3.conf.js --single-run --errorpolicy=lazy - node_modules/.bin/gulp test/node - node_modules/.bin/gulp test/node -no-patch-clock - node_modules/.bin/gulp test/bluebird + - node_modules/.bin/gulp test/node/disableerror + - node_modules/.bin/gulp test/node/lazyerror - node simple-server.js 2>&1> server.log& - node ./test/webdriver/test.sauce.js - yarn add jasmine@3.0.0 jasmine-core@3.0.0 mocha@5.0.1 diff --git a/MODULE.md b/MODULE.md index 377f66fa9..51519a03e 100644 --- a/MODULE.md +++ b/MODULE.md @@ -76,6 +76,57 @@ you can do like this. ``` +- Error + +By default, `zone.js/dist/zone-error` will not be loaded for performance concern. +This package will provide following functionality. + + 1. Error inherit: handle `extend Error` issue. + ``` + class MyError extends Error {} + const myError = new MyError(); + console.log('is MyError instanceof Error', (myError instanceof Error)); + ``` + + without `zone-error` patch, the example above will output `false`, with the patch, the reuslt will be `true`. + + 2. BlacklistZoneStackFrames: remove zone.js stack from `stackTrace`, and add `zone` information. Without this patch, a lot of `zone.js` invocation stack will be shown + in stack frames. + + ``` + at zone.run (polyfill.bundle.js: 3424) + at zoneDelegate.invokeTask (polyfill.bundle.js: 3424) + at zoneDelegate.runTask (polyfill.bundle.js: 3424) + at zone.drainMicroTaskQueue (polyfill.bundle.js: 3424) + at a.b.c (vendor.bundle.js: 12345 ) + at d.e.f (main.bundle.js: 23456) + ``` + + with this patch, those zone frames will be removed, + and the zone information `/` will be added + + ``` + at a.b.c (vendor.bundle.js: 12345 ) + at d.e.f (main.bundle.js: 23456 ) + ``` + + The second feature will slow down the `Error` performance, so `zone.js` provide a flag to let you be able to control the behavior. + The flag is `__Zone_Error_BlacklistedStackFrames_policy`. And the available options is: + + 1. default: this is the default one, if you load `zone.js/dist/zone-error` without + setting the flag, `default` will be used, and `BlackListStackFrames` will be available + when `new Error()`, you can get a `error.stack` which is `zone stack free`. But this + will slow down `new Error()` a little bit. + + 2. disable: this will disable `BlackListZoneStackFrame` feature, and if you load + `zone.js/dist/zone-error`, you will only get a `wrapped Error` which can handle + `Error inherit` issue. + + 3. lazy: this is a feature to let you be able to get `BlackListZoneStackFrame` feature, + but not impact performance. But as a trade off, you can't get the `zone free stack + frames` by access `error.stack`. You can only get it by access `error.zoneAwareStack`. + + - Angular(2+) Angular uses zone.js to manage async operations and decide when to perform change detection. Thus, in Angular, diff --git a/file-size-limit.json b/file-size-limit.json index 2c03611e5..26e5195d5 100644 --- a/file-size-limit.json +++ b/file-size-limit.json @@ -3,7 +3,7 @@ { "path": "dist/zone.min.js", "checkTarget": true, - "limit": 40000 + "limit": 40144 } ] } diff --git a/gulpfile.js b/gulpfile.js index 385bcdde7..64235017b 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -444,6 +444,18 @@ gulp.task('test/bluebird', ['compile-node'], function(cb) { nodeTest(specFiles, cb); }); +gulp.task('test/node/disableerror', ['compile-node'], function(cb) { + process.env.errorpolicy = 'disable'; + var specFiles = ['build/test/node_error_entry_point.js']; + nodeTest(specFiles, cb); +}); + +gulp.task('test/node/lazyerror', ['compile-node'], function(cb) { + process.env.errorpolicy = 'lazy'; + var specFiles = ['build/test/node_error_entry_point.js']; + nodeTest(specFiles, cb); +}); + // Check the coding standards and programming errors gulp.task('lint', () => { const tslint = require('gulp-tslint'); diff --git a/karma-base.conf.js b/karma-base.conf.js index d31a4310f..32f447cff 100644 --- a/karma-base.conf.js +++ b/karma-base.conf.js @@ -9,6 +9,7 @@ module.exports = function(config) { config.set({ basePath: '', + client: {errorpolicy: config.errorpolicy}, files: [ 'node_modules/systemjs/dist/system-polyfills.js', 'node_modules/systemjs/dist/system.src.js', 'node_modules/whatwg-fetch/fetch.js', @@ -41,6 +42,7 @@ module.exports = function(config) { browsers: ['Chrome'], captureTimeout: 60000, + retryLimit: 4, autoWatch: true, singleRun: false diff --git a/karma-build.conf.js b/karma-build.conf.js index 87c87a5ea..aa2d3113c 100644 --- a/karma-build.conf.js +++ b/karma-build.conf.js @@ -6,12 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ -module.exports = function (config) { +module.exports = function(config) { require('./karma-base.conf.js')(config); config.files.push('build/test/wtf_mock.js'); config.files.push('build/test/test_fake_polyfill.js'); config.files.push('build/lib/zone.js'); config.files.push('build/lib/common/promise.js'); - config.files.push('build/lib/common/error-rewrite.js'); config.files.push('build/test/main.js'); }; diff --git a/lib/browser/define-property.ts b/lib/browser/define-property.ts index 872e86ea9..872631148 100644 --- a/lib/browser/define-property.ts +++ b/lib/browser/define-property.ts @@ -19,7 +19,7 @@ const _create = Object.create; const unconfigurablesKey = zoneSymbol('unconfigurables'); export function propertyPatch() { - Object.defineProperty = function(obj, prop, desc) { + Object.defineProperty = function(obj: any, prop: string, desc: any) { if (isUnconfigurable(obj, prop)) { throw new TypeError('Cannot assign to read only property \'' + prop + '\' of ' + obj); } diff --git a/lib/common/error-rewrite.ts b/lib/common/error-rewrite.ts index dc9a48918..1f63473d0 100644 --- a/lib/common/error-rewrite.ts +++ b/lib/common/error-rewrite.ts @@ -46,10 +46,71 @@ Zone.__load_patch('Error', (global: any, Zone: ZoneType, api: _ZonePrivate) => { // We must find the frame where Error was created, otherwise we assume we don't understand stack let zoneAwareFrame1: string; let zoneAwareFrame2: string; + let zoneAwareFrame1WithoutNew: string; + let zoneAwareFrame2WithoutNew: string; + let zoneAwareFrame3WithoutNew: string; global['Error'] = ZoneAwareError; const stackRewrite = 'stackRewrite'; + type BlackListedStackFramesPolicy = 'default'|'disable'|'lazy'; + const blackListedStackFramesPolicy: BlackListedStackFramesPolicy = + global['__Zone_Error_BlacklistedStackFrames_policy'] || 'default'; + + interface ZoneFrameName { + zoneName: string; + parent?: ZoneFrameName; + } + + function buildZoneFrameNames(zoneFrame: _ZoneFrame) { + let zoneFrameName: ZoneFrameName = {zoneName: zoneFrame.zone.name}; + let result = zoneFrameName; + while (zoneFrame.parent) { + zoneFrame = zoneFrame.parent; + const parentZoneFrameName = {zoneName: zoneFrame.zone.name}; + zoneFrameName.parent = parentZoneFrameName; + zoneFrameName = parentZoneFrameName; + } + return result; + } + + function buildZoneAwareStackFrames( + originalStack: string, zoneFrame: _ZoneFrame|ZoneFrameName|null, isZoneFrame = true) { + let frames: string[] = originalStack.split('\n'); + let i = 0; + // Find the first frame + while (!(frames[i] === zoneAwareFrame1 || frames[i] === zoneAwareFrame2 || + frames[i] === zoneAwareFrame1WithoutNew || frames[i] === zoneAwareFrame2WithoutNew || + frames[i] === zoneAwareFrame3WithoutNew) && + i < frames.length) { + i++; + } + for (; i < frames.length && zoneFrame; i++) { + let frame = frames[i]; + if (frame.trim()) { + switch (blackListedStackFrames[frame]) { + case FrameType.blackList: + frames.splice(i, 1); + i--; + break; + case FrameType.transition: + if (zoneFrame.parent) { + // This is the special frame where zone changed. Print and process it accordingly + zoneFrame = zoneFrame.parent; + } else { + zoneFrame = null; + } + frames.splice(i, 1); + i--; + break; + default: + frames[i] += isZoneFrame ? ` [${(zoneFrame as _ZoneFrame).zone.name}]` : + ` [${(zoneFrame as ZoneFrameName).zoneName}]`; + } + } + } + return frames.join('\n'); + } /** * This is ZoneAwareError which processes the stack frame and cleans up extra frames as well as * adds zone information to it. @@ -62,42 +123,17 @@ Zone.__load_patch('Error', (global: any, Zone: ZoneType, api: _ZonePrivate) => { // Process the stack trace and rewrite the frames. if ((ZoneAwareError as any)[stackRewrite] && originalStack) { - let frames: string[] = originalStack.split('\n'); - let zoneFrame: _ZoneFrame|null = api.currentZoneFrame(); - let i = 0; - // Find the first frame - while (!(frames[i] === zoneAwareFrame1 || frames[i] === zoneAwareFrame2) && - i < frames.length) { - i++; - } - for (; i < frames.length && zoneFrame; i++) { - let frame = frames[i]; - if (frame.trim()) { - switch (blackListedStackFrames[frame]) { - case FrameType.blackList: - frames.splice(i, 1); - i--; - break; - case FrameType.transition: - if (zoneFrame.parent) { - // This is the special frame where zone changed. Print and process it accordingly - zoneFrame = zoneFrame.parent; - } else { - zoneFrame = null; - } - frames.splice(i, 1); - i--; - break; - default: - frames[i] += ` [${zoneFrame.zone.name}]`; - } + let zoneFrame = api.currentZoneFrame(); + if (blackListedStackFramesPolicy === 'lazy') { + // don't handle stack trace now + (error as any)[api.symbol('zoneFrameNames')] = buildZoneFrameNames(zoneFrame); + } else if (blackListedStackFramesPolicy === 'default') { + try { + error.stack = error.zoneAwareStack = buildZoneAwareStackFrames(originalStack, zoneFrame); + } catch (e) { + // ignore as some browsers don't allow overriding of stack } } - try { - error.stack = error.zoneAwareStack = frames.join('\n'); - } catch (e) { - // ignore as some browsers don't allow overriding of stack - } } if (this instanceof NativeError && this.constructor != NativeError) { @@ -123,6 +159,29 @@ Zone.__load_patch('Error', (global: any, Zone: ZoneType, api: _ZonePrivate) => { (ZoneAwareError as any)[blacklistedStackFramesSymbol] = blackListedStackFrames; (ZoneAwareError as any)[stackRewrite] = false; + const zoneAwareStackSymbol = api.symbol('zoneAwareStack'); + + // try to define zoneAwareStack property when blackListed + // policy is delay + if (blackListedStackFramesPolicy === 'lazy') { + Object.defineProperty(ZoneAwareError.prototype, 'zoneAwareStack', { + configurable: true, + enumerable: true, + get: function() { + if (!this[zoneAwareStackSymbol]) { + this[zoneAwareStackSymbol] = buildZoneAwareStackFrames( + this.originalStack, this[api.symbol('zoneFrameNames')], false); + } + return this[zoneAwareStackSymbol]; + }, + set: function(newStack: string) { + this.originalStack = newStack; + this[zoneAwareStackSymbol] = buildZoneAwareStackFrames( + this.originalStack, this[api.symbol('zoneFrameNames')], false); + } + }); + } + // those properties need special handling const specialPropertyNames = ['stackTraceLimit', 'captureStackTrace', 'prepareStackTrace']; // those properties of NativeError should be set to ZoneAwareError @@ -194,65 +253,71 @@ Zone.__load_patch('Error', (global: any, Zone: ZoneType, api: _ZonePrivate) => { } }); + if (blackListedStackFramesPolicy === 'disable') { + // don't need to run detectZone to populate + // blacklisted stack frames + return; + } // Now we need to populate the `blacklistedStackFrames` as well as find the // run/runGuarded/runTask frames. This is done by creating a detect zone and then threading // the execution through all of the above methods so that we can look at the stack trace and // find the frames of interest. - const ZONE_AWARE_ERROR = 'ZoneAwareError'; - const ERROR_DOT = 'Error.'; - const EMPTY = ''; - const RUN_GUARDED = 'runGuarded'; - const RUN_TASK = 'runTask'; - const RUN = 'run'; - const BRACKETS = '('; - const AT = '@'; let detectZone: Zone = Zone.current.fork({ name: 'detect', - onHandleError: function(parentZD: ZoneDelegate, current: Zone, target: Zone, error: any): - boolean { - if (error.originalStack && Error === ZoneAwareError) { - let frames = error.originalStack.split(/\n/); - let runFrame = false, runGuardedFrame = false, runTaskFrame = false; - while (frames.length) { - let frame = frames.shift(); - // On safari it is possible to have stack frame with no line number. - // This check makes sure that we don't filter frames on name only (must have - // line number) - if (/:\d+:\d+/.test(frame)) { - // Get rid of the path so that we don't accidentally find function name in path. - // In chrome the separator is `(` and `@` in FF and safari - // Chrome: at Zone.run (zone.js:100) - // Chrome: at Zone.run (http://localhost:9876/base/build/lib/zone.js:100:24) - // FireFox: Zone.prototype.run@http://localhost:9876/base/build/lib/zone.js:101:24 - // Safari: run@http://localhost:9876/base/build/lib/zone.js:101:24 - let fnName: string = frame.split(BRACKETS)[0].split(AT)[0]; - let frameType = FrameType.transition; - if (fnName.indexOf(ZONE_AWARE_ERROR) !== -1) { - zoneAwareFrame1 = frame; - zoneAwareFrame2 = frame.replace(ERROR_DOT, EMPTY); - blackListedStackFrames[zoneAwareFrame2] = FrameType.blackList; - } - if (fnName.indexOf(RUN_GUARDED) !== -1) { - runGuardedFrame = true; - } else if (fnName.indexOf(RUN_TASK) !== -1) { - runTaskFrame = true; - } else if (fnName.indexOf(RUN) !== -1) { - runFrame = true; - } else { - frameType = FrameType.blackList; - } - blackListedStackFrames[frame] = frameType; - // Once we find all of the frames we can stop looking. - if (runFrame && runGuardedFrame && runTaskFrame) { - (ZoneAwareError as any)[stackRewrite] = true; - break; + onHandleError: function( + parentZD: ZoneDelegate, current: Zone, target: Zone, error: any): boolean { + if (error.originalStack && Error === ZoneAwareError) { + let frames = error.originalStack.split(/\n/); + let runFrame = false, runGuardedFrame = false, runTaskFrame = false; + while (frames.length) { + let frame = frames.shift(); + // On safari it is possible to have stack frame with no line number. + // This check makes sure that we don't filter frames on name only (must have + // line number or exact equals to `ZoneAwareError`) + if (/:\d+:\d+/.test(frame) || frame === 'ZoneAwareError') { + // Get rid of the path so that we don't accidentally find function name in path. + // In chrome the separator is `(` and `@` in FF and safari + // Chrome: at Zone.run (zone.js:100) + // Chrome: at Zone.run (http://localhost:9876/base/build/lib/zone.js:100:24) + // FireFox: Zone.prototype.run@http://localhost:9876/base/build/lib/zone.js:101:24 + // Safari: run@http://localhost:9876/base/build/lib/zone.js:101:24 + let fnName: string = frame.split('(')[0].split('@')[0]; + let frameType = FrameType.transition; + if (fnName.indexOf('ZoneAwareError') !== -1) { + if (fnName.indexOf('new ZoneAwareError') !== -1) { + zoneAwareFrame1 = frame; + zoneAwareFrame2 = frame.replace('new ZoneAwareError', 'new Error.ZoneAwareError'); + } else { + zoneAwareFrame1WithoutNew = frame; + zoneAwareFrame2WithoutNew = frame.replace('Error.', ''); + if (frame.indexOf('Error.ZoneAwareError') === -1) { + zoneAwareFrame3WithoutNew = + frame.replace('ZoneAwareError', 'Error.ZoneAwareError'); } } + blackListedStackFrames[zoneAwareFrame2] = FrameType.blackList; + } + if (fnName.indexOf('runGuarded') !== -1) { + runGuardedFrame = true; + } else if (fnName.indexOf('runTask') !== -1) { + runTaskFrame = true; + } else if (fnName.indexOf('run') !== -1) { + runFrame = true; + } else { + frameType = FrameType.blackList; + } + blackListedStackFrames[frame] = frameType; + // Once we find all of the frames we can stop looking. + if (runFrame && runGuardedFrame && runTaskFrame) { + (ZoneAwareError as any)[stackRewrite] = true; + break; } } - return false; } + } + return false; + } }) as Zone; // carefully constructor a stack frame which contains all of the frames of interest which // need to be detected and blacklisted. @@ -293,7 +358,17 @@ Zone.__load_patch('Error', (global: any, Zone: ZoneType, api: _ZonePrivate) => { childDetectZone.scheduleMicroTask( blacklistedStackFramesSymbol, () => { - throw new (ZoneAwareError as any)(ZoneAwareError, NativeError); + throw new Error(); + }, + undefined, + (t: Task) => { + (t as any)._transitionTo = fakeTransitionTo; + t.invoke(); + }); + childDetectZone.scheduleMicroTask( + blacklistedStackFramesSymbol, + () => { + throw Error(); }, undefined, (t: Task) => { @@ -316,5 +391,6 @@ Zone.__load_patch('Error', (global: any, Zone: ZoneType, api: _ZonePrivate) => { () => {}); }); }); + Error.stackTraceLimit = originalStackTraceLimit; }); diff --git a/lib/common/utils.ts b/lib/common/utils.ts index e7bbed307..0165da1c0 100644 --- a/lib/common/utils.ts +++ b/lib/common/utils.ts @@ -342,10 +342,10 @@ export function copySymbolProperties(src: any, dest: any) { symbols.forEach((symbol: any) => { const desc = Object.getOwnPropertyDescriptor(src, symbol); Object.defineProperty(dest, symbol, { - get: function () { + get: function() { return src[symbol]; }, - set: function (value: any) { + set: function(value: any) { if (desc && (!desc.writable || typeof desc.set !== 'function')) { // if src[symbol] is not writable or not have a setter, just return return; diff --git a/lib/node/node_util.ts b/lib/node/node_util.ts index d37aa4aa0..7ea994851 100644 --- a/lib/node/node_util.ts +++ b/lib/node/node_util.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {setShouldCopySymbolProperties, patchOnProperties, patchMethod, bindArguments} from '../common/utils'; +import {bindArguments, patchMethod, patchOnProperties, setShouldCopySymbolProperties} from '../common/utils'; Zone.__load_patch('node_util', (global: any, Zone: ZoneType, api: _ZonePrivate) => { api.patchOnProperties = patchOnProperties; diff --git a/package.json b/package.json index edcf64c43..533547247 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "dependencies": {}, "devDependencies": { "@types/jasmine": "2.2.33", - "@types/node": "^8.10.17", + "@types/node": "^9.x", "@types/systemjs": "^0.19.30", "assert": "^1.4.1", "bluebird": "^3.5.1", diff --git a/sauce-selenium3.conf.js b/sauce-selenium3.conf.js index d06a50258..0141e8808 100644 --- a/sauce-selenium3.conf.js +++ b/sauce-selenium3.conf.js @@ -8,7 +8,7 @@ module.exports = function(config) { 'SL_CHROME60': {base: 'SauceLabs', browserName: 'Chrome', platform: 'Windows 10', version: '60.0'}, 'SL_SAFARI11': - {base: 'SauceLabs', browserName: 'safari', platform: 'macOS 10.13', version: '11.0'}, + {base: 'SauceLabs', browserName: 'safari', platform: 'macOS 10.13', version: '11.1'}, }; config.set({ diff --git a/sauce.conf.js b/sauce.conf.js index 8a1c8a53a..c3ad894fc 100644 --- a/sauce.conf.js +++ b/sauce.conf.js @@ -36,7 +36,7 @@ module.exports = function(config, ignoredLaunchers) { version: '8.4' },*/ 'SL_IOS9': {base: 'SauceLabs', browserName: 'iphone', platform: 'OS X 10.10', version: '9.3'}, - 'SL_IOS10': {base: 'SauceLabs', browserName: 'iphone', platform: 'OS X 10.10', version: '10.2'}, + 'SL_IOS10': {base: 'SauceLabs', browserName: 'iphone', platform: 'OS X 10.10', version: '10.3'}, 'SL_IE9': { base: 'SauceLabs', browserName: 'internet explorer', diff --git a/test/browser/WebSocket.spec.ts b/test/browser/WebSocket.spec.ts index 82c51c321..86441f2fe 100644 --- a/test/browser/WebSocket.spec.ts +++ b/test/browser/WebSocket.spec.ts @@ -47,7 +47,7 @@ if (!window['saucelabs']) { expect(e.data).toBe('pass'); done(); }; - }); + }, 10000); it('should work with addEventListener', function(done) { testZone.run(function() { diff --git a/test/common/Error.spec.ts b/test/common/Error.spec.ts index 540faf82f..3ed1a5713 100644 --- a/test/common/Error.spec.ts +++ b/test/common/Error.spec.ts @@ -78,7 +78,15 @@ class TestMessageError extends WrappedError { describe('ZoneAwareError', () => { // If the environment does not supports stack rewrites, then these tests will fail // and there is no point in running them. - if (!(Error as any)['stackRewrite']) return; + const _global: any = typeof window !== 'undefined' ? window : global; + let config: any; + if (typeof __karma__ !== 'undefined') { + config = __karma__ && (__karma__ as any).config; + } else if (typeof process !== 'undefined') { + config = process.env; + } + const policy = (config && config['errorpolicy']) || 'default'; + if (!(Error as any)['stackRewrite'] && policy !== 'disable') return; it('should keep error prototype chain correctly', () => { class MyError extends Error {} @@ -180,15 +188,18 @@ describe('ZoneAwareError', () => { }); it('should show zone names in stack frames and remove extra frames', () => { + if (policy === 'disable' || !(Error as any)['stackRewrite']) { + return; + } const rootZone = Zone.root; const innerZone = rootZone.fork({name: 'InnerZone'}); rootZone.run(testFn); function testFn() { - let outside: Error; - let inside: Error; - let outsideWithoutNew: Error; - let insideWithoutNew: Error; + let outside: any; + let inside: any; + let outsideWithoutNew: any; + let insideWithoutNew: any; try { throw new Error('Outside'); } catch (e) { @@ -212,6 +223,13 @@ describe('ZoneAwareError', () => { } }); + if (policy === 'lazy') { + outside.stack = outside.zoneAwareStack; + outsideWithoutNew.stack = outsideWithoutNew.zoneAwareStack; + inside.stack = inside.zoneAwareStack; + insideWithoutNew.stack = insideWithoutNew.zoneAwareStack; + } + expect(outside.stack).toEqual(outside.zoneAwareStack); expect(outsideWithoutNew.stack).toEqual(outsideWithoutNew.zoneAwareStack); expect(inside!.stack).toEqual(inside!.zoneAwareStack); @@ -251,7 +269,6 @@ describe('ZoneAwareError', () => { if (/Error /.test(insideWithoutNewFrames[0])) { insideWithoutNewFrames.shift(); } - expect(outsideFrames[0]).toMatch(/testFn.*[]/); expect(insideFrames[0]).toMatch(/insideRun.*[InnerZone]]/); @@ -267,13 +284,31 @@ describe('ZoneAwareError', () => { const zoneAwareFrames = [ 'Zone.run', 'Zone.runGuarded', 'Zone.scheduleEventTask', 'Zone.scheduleMicroTask', 'Zone.scheduleMacroTask', 'Zone.runTask', 'ZoneDelegate.scheduleTask', - 'ZoneDelegate.invokeTask', 'zoneAwareAddListener' + 'ZoneDelegate.invokeTask', 'zoneAwareAddListener', 'Zone.prototype.run', + 'Zone.prototype.runGuarded', 'Zone.prototype.scheduleEventTask', + 'Zone.prototype.scheduleMicroTask', 'Zone.prototype.scheduleMacroTask', + 'Zone.prototype.runTask', 'ZoneDelegate.prototype.scheduleTask', + 'ZoneDelegate.prototype.invokeTask', 'ZoneTask.invokeTask' ]; - function assertStackDoesNotContainZoneFrames(err: Error) { - const frames = err.stack!.split('\n'); - for (let i = 0; i < frames.length; i++) { - expect(zoneAwareFrames.filter(f => frames[i].indexOf(f) !== -1)).toEqual([]); + function assertStackDoesNotContainZoneFrames(err: any) { + const frames = policy === 'lazy' ? err.zoneAwareStack.split('\n') : err.stack.split('\n'); + if (policy === 'disable') { + let hasZoneStack = false; + for (let i = 0; i < frames.length; i++) { + if (hasZoneStack) { + break; + } + hasZoneStack = zoneAwareFrames.filter(f => frames[i].indexOf(f) !== -1).length > 0; + } + if (!hasZoneStack) { + console.log('stack', err.originalStack); + } + expect(hasZoneStack).toBe(true); + } else { + for (let i = 0; i < frames.length; i++) { + expect(zoneAwareFrames.filter(f => frames[i].indexOf(f) !== -1)).toEqual([]); + } } }; @@ -317,7 +352,9 @@ describe('ZoneAwareError', () => { it('Error with new which cause by promise rejection should not have zone frames visible', (done) => { const p = new Promise((resolve, reject) => { - reject(new Error('test error')); + setTimeout(() => { + reject(new Error('test error')); + }); }); p.catch(err => { assertStackDoesNotContainZoneFrames(err); @@ -328,7 +365,9 @@ describe('ZoneAwareError', () => { it('Error without new which cause by promise rejection should not have zone frames visible', (done) => { const p = new Promise((resolve, reject) => { - reject(Error('test error')); + setTimeout(() => { + reject(Error('test error')); + }); }); p.catch(err => { assertStackDoesNotContainZoneFrames(err); diff --git a/test/main.ts b/test/main.ts index 69514966d..ccfedf5a8 100644 --- a/test/main.ts +++ b/test/main.ts @@ -13,6 +13,14 @@ declare const __karma__: { }; __karma__.loaded = function() {}; + +if (typeof __karma__ !== 'undefined') { + (window as any)['__Zone_Error_BlacklistedStackFrames_policy'] = + (__karma__ as any).config.errorpolicy; +} else if (typeof process !== 'undefined') { + (window as any)['__Zone_Error_BlacklistedStackFrames_policy'] = process.env.errorpolicy; +} + (window as any).global = window; System.config({ defaultJSExtensions: true, @@ -29,7 +37,7 @@ if ((window as any)[(Zone as any).__symbol__('setTimeout')]) { // this means that Zone has not patched the browser yet, which means we must be running in // build mode and need to load the browser patch. browserPatchedPromise = System.import('/base/build/test/browser-zone-setup').then(() => { - let testFrameworkPatch = typeof(window as any).Mocha !== 'undefined' ? + let testFrameworkPatch = typeof (window as any).Mocha !== 'undefined' ? '/base/build/lib/mocha/mocha' : '/base/build/lib/jasmine/jasmine'; return System.import(testFrameworkPatch); @@ -42,13 +50,15 @@ browserPatchedPromise.then(() => { '/base/build/test/test-env-setup-jasmine'; // Setup test environment System.import(testFrameworkPatch).then(() => { - System.import('/base/build/test/browser_entry_point') - .then( - () => { - __karma__.start(); - }, - (error) => { - console.error(error.stack || error); - }); + System.import('/base/build/lib/common/error-rewrite').then(() => { + System.import('/base/build/test/browser_entry_point') + .then( + () => { + __karma__.start(); + }, + (error) => { + console.error(error.stack || error); + }); + }); }); }); diff --git a/test/node/fs.spec.ts b/test/node/fs.spec.ts index 1ba819dd7..55cd02b24 100644 --- a/test/node/fs.spec.ts +++ b/test/node/fs.spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {exists, read, unlink, unwatchFile, watch, write, watchFile, writeFile, openSync, fstatSync, closeSync, unlinkSync} from 'fs'; +import {closeSync, exists, fstatSync, openSync, read, unlink, unlinkSync, unwatchFile, watch, watchFile, write, writeFile} from 'fs'; import * as util from 'util'; describe('nodejs file system', () => { @@ -94,12 +94,15 @@ describe('nodejs file system', () => { describe('util.promisify', () => { it('fs.exists should work with util.promisify', (done: DoneFn) => { const promisifyExists = util.promisify(exists); - promisifyExists(__filename).then(r => { - expect(r).toBe(true); - done(); - }, err => { - fail(`should not be here with error: ${err}`); - }); + promisifyExists(__filename) + .then( + r => { + expect(r).toBe(true); + done(); + }, + err => { + fail(`should not be here with error: ${err}`); + }); }); it('fs.read should work with util.promisify', (done: DoneFn) => { @@ -111,15 +114,17 @@ describe('util.promisify', () => { const buffer = new Buffer(bufferSize); let bytesRead = 0; // fd, buffer, offset, length, position, callback - promisifyRead(fd, buffer, bytesRead, chunkSize, bytesRead).then( - (value) => { - expect(value.bytesRead).toBe(chunkSize); - closeSync(fd); - done(); - }, err => { - closeSync(fd); - fail(`should not be here with error: ${error}.`); - }); + promisifyRead(fd, buffer, bytesRead, chunkSize, bytesRead) + .then( + (value) => { + expect(value.bytesRead).toBe(chunkSize); + closeSync(fd); + done(); + }, + err => { + closeSync(fd); + fail(`should not be here with error: ${error}.`); + }); }); it('fs.write should work with util.promisify', (done: DoneFn) => { @@ -133,16 +138,18 @@ describe('util.promisify', () => { buffer[i] = 0; } // fd, buffer, offset, length, position, callback - promisifyWrite(fd, buffer, 0, chunkSize, 0).then( - (value) => { - expect(value.bytesWritten).toBe(chunkSize); - closeSync(fd); - unlinkSync(dest); - done(); - }, err => { - closeSync(fd); - unlinkSync(dest); - fail(`should not be here with error: ${error}.`); - }); + promisifyWrite(fd, buffer, 0, chunkSize, 0) + .then( + (value) => { + expect(value.bytesWritten).toBe(chunkSize); + closeSync(fd); + unlinkSync(dest); + done(); + }, + err => { + closeSync(fd); + unlinkSync(dest); + fail(`should not be here with error: ${error}.`); + }); }); }); \ No newline at end of file diff --git a/test/node/timer.spec.ts b/test/node/timer.spec.ts index 082c0af32..eff071905 100644 --- a/test/node/timer.spec.ts +++ b/test/node/timer.spec.ts @@ -5,26 +5,31 @@ * 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 { promisify } from 'util'; +import {promisify} from 'util'; describe('node timer', () => { it('util.promisify should work with setTimeout', (done: DoneFn) => { const setTimeoutPromise = promisify(setTimeout); - setTimeoutPromise(50, 'value').then(value => { - expect(value).toEqual('value'); - done(); - }, error => { - fail(`should not be here with error: ${error}.`); - }); + setTimeoutPromise(50, 'value') + .then( + value => { + expect(value).toEqual('value'); + done(); + }, + error => { + fail(`should not be here with error: ${error}.`); + }); }); it('util.promisify should work with setImmediate', (done: DoneFn) => { const setImmediatePromise = promisify(setImmediate); - setImmediatePromise('value').then(value => { - expect(value).toEqual('value'); - done(); - }, error => { - fail(`should not be here with error: ${error}.`); - }); + setImmediatePromise('value').then( + value => { + expect(value).toEqual('value'); + done(); + }, + error => { + fail(`should not be here with error: ${error}.`); + }); }); }); \ No newline at end of file diff --git a/test/node_entry_point.ts b/test/node_entry_point.ts index a98cb3a7c..7d7c4ee79 100644 --- a/test/node_entry_point.ts +++ b/test/node_entry_point.ts @@ -20,6 +20,7 @@ import '../lib/zone-spec/task-tracking'; import '../lib/zone-spec/wtf'; import '../lib/rxjs/rxjs'; import '../lib/rxjs/rxjs-fake-async'; + // Setup test environment import '../lib/jasmine/jasmine'; import './test-env-setup-jasmine'; diff --git a/test/node_error_entry_point.ts b/test/node_error_entry_point.ts new file mode 100644 index 000000000..f70682ed9 --- /dev/null +++ b/test/node_error_entry_point.ts @@ -0,0 +1,41 @@ +/** + * @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 + */ + +// Must be loaded before zone loads, so that zone can detect WTF. +import './wtf_mock'; +import './test_fake_polyfill'; + +// Setup tests for Zone without microtask support +import '../lib/zone'; +import '../lib/common/promise'; +import '../lib/common/to-string'; + +if (typeof __karma__ !== 'undefined') { + (global as any)['__Zone_Error_BlacklistedStackFrames_policy'] = + (__karma__ as any).config.errorpolicy; +} else if (typeof process !== 'undefined') { + (global as any)['__Zone_Error_BlacklistedStackFrames_policy'] = process.env.errorpolicy; +} + +import '../lib/common/error-rewrite'; +import '../lib/node/node'; +import '../lib/zone-spec/async-test'; +import '../lib/zone-spec/fake-async-test'; +import '../lib/zone-spec/long-stack-trace'; +import '../lib/zone-spec/proxy'; +import '../lib/zone-spec/sync-test'; +import '../lib/zone-spec/task-tracking'; +import '../lib/zone-spec/wtf'; +import '../lib/rxjs/rxjs'; + +import '../lib/testing/promise-testing'; +// Setup test environment +import './test-env-setup-jasmine'; + +// List all tests here: +import './common/Error.spec'; diff --git a/test/webdriver/test.sauce.js b/test/webdriver/test.sauce.js index 13c7e80d1..922f91028 100644 --- a/test/webdriver/test.sauce.js +++ b/test/webdriver/test.sauce.js @@ -16,10 +16,10 @@ const desiredCapabilities = { safari8: {browserName: 'safari', platform: 'OS X 10.10', version: '8.0'}, safari9: {browserName: 'safari', platform: 'OS X 10.11', version: '9.0'}, safari10: {browserName: 'safari', platform: 'OS X 10.11', version: '10.0'}, - safari11: {browserName: 'safari', platform: 'macOS 10.13', version: '11.0'}, + safari11: {browserName: 'safari', platform: 'macOS 10.13', version: '11.1'}, /*ios84: {browserName: 'iphone', platform: 'OS X 10.10', version: '8.4'},*/ ios93: {browserName: 'iphone', platform: 'OS X 10.10', version: '9.3'}, - ios10: {browserName: 'iphone', platform: 'OS X 10.10', version: '10.2'}, + ios10: {browserName: 'iphone', platform: 'OS X 10.10', version: '10.3'}, ios11: {browserName: 'iphone', platform: 'OS X 10.12', version: '11.2'}, /* ie9: { @@ -134,4 +134,4 @@ Promise.all(tasks).then(() => { } else { exit(0); } -}); \ No newline at end of file +}); diff --git a/yarn.lock b/yarn.lock index a5ef50c82..4047760fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6,9 +6,9 @@ version "2.2.33" resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-2.2.33.tgz#4715cfd2ca7fbd632fc7f1784f13e637bed028c5" -"@types/node@^8.10.17": - version "8.10.17" - resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.17.tgz#d48cf10f0dc6dcf59f827f5a3fc7a4a6004318d3" +"@types/node@^9.x": + version "9.6.22" + resolved "https://registry.yarnpkg.com/@types/node/-/node-9.6.22.tgz#05b55093faaadedea7a4b3f76e9a61346a6dd209" "@types/systemjs@^0.19.30": version "0.19.33"