From d0e1bd96fb2ba50097a3c1594f484c643ee85bf2 Mon Sep 17 00:00:00 2001 From: Steve Gravrock Date: Fri, 1 Jul 2022 16:58:38 -0700 Subject: [PATCH 1/5] README updates * Removed redundancy * Added a link to the FAQ * Removed obsolete support channels --- README.md | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index e1e27d09..1b47eb81 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,12 @@ [![Build Status](https://circleci.com/gh/jasmine/jasmine.svg?style=shield)](https://circleci.com/gh/jasmine/jasmine) [![Open Source Helpers](https://www.codetriage.com/jasmine/jasmine/badges/users.svg)](https://www.codetriage.com/jasmine/jasmine) -[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fjasmine%2Fjasmine.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fjasmine%2Fjasmine?ref=badge_shield) # A JavaScript Testing Framework Jasmine is a Behavior Driven Development testing framework for JavaScript. It does not rely on browsers, DOM, or any JavaScript framework. Thus it's suited for websites, [Node.js](http://nodejs.org) projects, or anywhere that JavaScript can run. -Documentation & guides live here: [http://jasmine.github.io](http://jasmine.github.io/) -For a quick start guide of Jasmine, see the beginning of [http://jasmine.github.io/edge/introduction.html](http://jasmine.github.io/edge/introduction.html). - -Upgrading from Jasmine 3.x? Check out the 4.0 release notes for a list of -what's new (including breaking changes). You can also read the [upgrade guide](https://jasmine.github.io/tutorials/upgrading_to_Jasmine_4.0). +Upgrading from Jasmine 3.x? Check out the [upgrade guide](https://jasmine.github.io/tutorials/upgrading_to_Jasmine_4.0). ## Contributing @@ -28,7 +23,7 @@ for details. See the [documentation site](https://jasmine.github.io/pages/docs_home.html), particularly the [Your First Suite tutorial](https://jasmine.github.io/tutorials/your_first_suite) -for information on writing specs. +for information on writing specs, and [the FAQ](https://jasmine.github.io/pages/faq.html). ## Supported environments @@ -47,15 +42,7 @@ For evergreen browsers, each version of Jasmine is tested against the version of at the time of release. Other browsers, as well as older & newer versions of some supported browsers, are likely to work. However, Jasmine isn't tested against them and they aren't actively supported. -See the [release notes](https://github.com/jasmine/jasmine/tree/main/release_notes) -for the supported environments for each Jasmine release. - -## Support - -* Search past discussions: [http://groups.google.com/group/jasmine-js](http://groups.google.com/group/jasmine-js). -* Send an email to the list: [jasmine-js@googlegroups.com](mailto:jasmine-js@googlegroups.com). -* View the project backlog at Pivotal Tracker: [http://www.pivotaltracker.com/projects/10606](http://www.pivotaltracker.com/projects/10606). -* Follow us on Twitter: [@JasmineBDD](http://twitter.com/JasmineBDD). +To find out what environments work with a particular Jasmine release, see the [release notes](https://github.com/jasmine/jasmine/tree/main/release_notes). ## Maintainers @@ -71,8 +58,4 @@ for the supported environments for each Jasmine release. * [Christian Williams](mailto:antixian666@gmail.com) * Sheel Choksi -Copyright (c) 2008-2022 Jasmine Maintainers. This software is licensed under the MIT License. - - -## License -[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fjasmine%2Fjasmine.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fjasmine%2Fjasmine?ref=badge_large) +Copyright (c) 2008-2022 Jasmine Maintainers. This software is licensed under the [MIT License](https://github.com/jasmine/jasmine/blob/main/MIT.LICENSE). From 140c12e8fc6c525a64c31921e0463d3d1d894dc2 Mon Sep 17 00:00:00 2001 From: Steve Gravrock Date: Sat, 23 Jul 2022 10:16:47 -0700 Subject: [PATCH 2/5] Added Firefox 102 (current ESR) to CI --- scripts/run-all-browsers | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/run-all-browsers b/scripts/run-all-browsers index d4fc6cce..6c3302c6 100755 --- a/scripts/run-all-browsers +++ b/scripts/run-all-browsers @@ -25,6 +25,7 @@ passfile=`mktemp -t jasmine-results.XXXXXX` || exit 1 failfile=`mktemp -t jasmine-results.XXXXXX` || exit 1 run_browser chrome latest run_browser firefox latest +run_browser firefox 102 run_browser firefox 91 run_browser safari 15 run_browser safari 14 From e022e6199c77482b1e2c9b683590f791909b6216 Mon Sep 17 00:00:00 2001 From: Steve Gravrock Date: Sat, 23 Jul 2022 10:25:45 -0700 Subject: [PATCH 3/5] Bump version to 4.3.0 --- RELEASE.md | 14 ++++++------- lib/jasmine-core/jasmine.js | 2 +- package.json | 2 +- release_notes/4.3.0.md | 40 +++++++++++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 9 deletions(-) create mode 100644 release_notes/4.3.0.md diff --git a/RELEASE.md b/RELEASE.md index 00c5838e..347c6b3a 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -35,11 +35,10 @@ When ready to release - specs are all green and the stories are done: ### Commit and push core changes -1. Run the browser tests using `scripts/run-all-browsers`. -2. Commit release notes and version changes (jasmine.js, package.json) -3. Push -4. Tag the release and push the tag. -5. Wait for Circle CI to go green +1. Commit release notes and version changes (jasmine.js, package.json) +2. Push +3. Tag the release and push the tag. +4. Wait for Circle CI to go green ### Build standalone distribution @@ -53,7 +52,9 @@ When ready to release - specs are all green and the stories are done: ### Release the docs -Probably only need to do this when releasing a minor version, and not a patch version. +Probably only need to do this when releasing a minor version, and not a patch +version. See [the README file in the docs repo](https://github.com/jasmine/jasmine.github.io/blob/master/README.md) +for instructions. 1. `rake update_edge_jasmine` 1. `npm run jsdoc` @@ -68,7 +69,6 @@ Probably only need to do this when releasing a minor version, and not a patch ve 1. In `package.json`, update both the package version and the jasmine-core dependency version 1. Commit and push. 1. Wait for Circle CI to go green again. -1. Run the tests on Windows locally. 1. `grunt release `. (Note: This will publish the package by running `npm publish`.) ### Finally diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index 1201ec00..6a429e53 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -10429,5 +10429,5 @@ getJasmineRequireObj().UserContext = function(j$) { }; getJasmineRequireObj().version = function() { - return '4.2.0'; + return '4.3.0'; }; diff --git a/package.json b/package.json index fdfb6c5b..fd4eb305 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "jasmine-core", "license": "MIT", - "version": "4.2.0", + "version": "4.3.0", "repository": { "type": "git", "url": "https://github.com/jasmine/jasmine.git" diff --git a/release_notes/4.3.0.md b/release_notes/4.3.0.md new file mode 100644 index 00000000..261b32f5 --- /dev/null +++ b/release_notes/4.3.0.md @@ -0,0 +1,40 @@ +# Jasmine 4.3.0 Release Notes + +## New Features + +* Added [`jasmine.spyOnGlobalErrorsAsync`](https://jasmine.github.io/api/4.3/jasmine.html#.spyOnGlobalErrorsAsync), + to better support testing code that's + expected to produce unhandled exceptions or unhandled promise rejections + * Fixes [#1843](https://github.com/jasmine/jasmine/issues/1843) + * Fixes [#1453](https://github.com/jasmine/jasmine/issues/1453) + +## Documentation updates + +* Updated the README to reduce redundancy and update support links + +## Internal improvements + +* Split `Env` into several smaller classes +* Replaced uses of `var` with `const`/`let` +* Replaced most uses of `self = this` with arrow fns +* Removed obsolete and unused utility fns +* Separated reporter- and runable-specific queue runner configuration +* Added more test coverage for default spy strategies +* Converted integration specs to `async`/`await` + +## Supported environments + +jasmine-core 4.3.0 has been tested in the following environments. + +| Environment | Supported versions | +|-------------------|--------------------| +| Node | 12.17+, 14, 16, 18 | +| Safari | 14-15 | +| Chrome | 103 | +| Firefox | 91, 102 | +| Edge | 103 | + + +------ + +_Release Notes generated with _[Anchorman](http://github.com/infews/anchorman)_ From 79c6bbc1891d1f61e7950f10ce6511529f068003 Mon Sep 17 00:00:00 2001 From: Steve Gravrock Date: Sat, 13 Aug 2022 08:54:32 -0700 Subject: [PATCH 4/5] clearStack optimizations * Avoid setTimeout in Node, because we don't need the overhead there. * Still call setTimeout in browsers to prevent the tab from being killed. * Use queueMicrotask in Safari, because it's dramatically faster than MessageChannel there. * Continue to use MessageChannel in other supported browsers becuase it's somewhat faster than queueMicrotask there. * Don't use setImmediate any more because there's a faster alternative in all supported envs. In jasmine-core's own test suite, this yields a roughly 50-70% speedup in Node, ~20% in Edge, and 75-90%(!) in Safari. --- lib/jasmine-core/jasmine.js | 72 ++++--- spec/core/ClearStackSpec.js | 326 ++++++++++++++++++------------- spec/core/integration/EnvSpec.js | 39 +++- src/core/ClearStack.js | 72 ++++--- 4 files changed, 322 insertions(+), 187 deletions(-) diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index 6a429e53..534da5df 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -2789,8 +2789,32 @@ getJasmineRequireObj().CallTracker = function(j$) { getJasmineRequireObj().clearStack = function(j$) { const maxInlineCallCount = 10; - function messageChannelImpl(global, setTimeout) { - const channel = new global.MessageChannel(); + function browserQueueMicrotaskImpl(global) { + const { setTimeout, queueMicrotask } = global; + let currentCallCount = 0; + return function clearStack(fn) { + currentCallCount++; + + if (currentCallCount < maxInlineCallCount) { + queueMicrotask(fn); + } else { + currentCallCount = 0; + setTimeout(fn); + } + }; + } + + function nodeQueueMicrotaskImpl(global) { + const { queueMicrotask } = global; + + return function(fn) { + queueMicrotask(fn); + }; + } + + function messageChannelImpl(global) { + const { MessageChannel, setTimeout } = global; + const channel = new MessageChannel(); let head = {}; let tail = head; @@ -2801,7 +2825,7 @@ getJasmineRequireObj().clearStack = function(j$) { delete head.task; if (taskRunning) { - global.setTimeout(task, 0); + setTimeout(task, 0); } else { try { taskRunning = true; @@ -2827,29 +2851,31 @@ getJasmineRequireObj().clearStack = function(j$) { } function getClearStack(global) { - let currentCallCount = 0; - const realSetTimeout = global.setTimeout; - const setTimeoutImpl = function clearStack(fn) { - Function.prototype.apply.apply(realSetTimeout, [global, [fn, 0]]); - }; + const NODE_JS = + global.process && + global.process.versions && + typeof global.process.versions.node === 'string'; - if (j$.isFunction_(global.setImmediate)) { - const realSetImmediate = global.setImmediate; - return function(fn) { - currentCallCount++; + const SAFARI = + global.navigator && + /^((?!chrome|android).)*safari/i.test(global.navigator.userAgent); - if (currentCallCount < maxInlineCallCount) { - realSetImmediate(fn); - } else { - currentCallCount = 0; - - setTimeoutImpl(fn); - } - }; - } else if (!j$.util.isUndefined(global.MessageChannel)) { - return messageChannelImpl(global, setTimeoutImpl); + if (NODE_JS) { + // Unlike browsers, Node doesn't require us to do a periodic setTimeout + // so we avoid the overhead. + return nodeQueueMicrotaskImpl(global); + } else if ( + SAFARI || + j$.util.isUndefined(global.MessageChannel) /* tests */ + ) { + // queueMicrotask is dramatically faster than MessageChannel in Safari. + // Some of our own integration tests provide a mock queueMicrotask in all + // environments because it's simpler to mock than MessageChannel. + return browserQueueMicrotaskImpl(global); } else { - return setTimeoutImpl; + // MessageChannel is faster than queueMicrotask in supported browsers + // other than Safari. + return messageChannelImpl(global); } } diff --git a/spec/core/ClearStackSpec.js b/spec/core/ClearStackSpec.js index 1cfaf6ad..626969f5 100644 --- a/spec/core/ClearStackSpec.js +++ b/spec/core/ClearStackSpec.js @@ -9,160 +9,216 @@ describe('ClearStack', function() { }); }); - it('uses setImmediate when available', function() { - const setImmediate = jasmine - .createSpy('setImmediate') - .and.callFake(function(fn) { - fn(); - }), - global = { setImmediate: setImmediate }, - clearStack = jasmineUnderTest.getClearStack(global); - let called = false; + describe('in Safari', function() { + usesQueueMicrotaskWithSetTimeout(function() { + return { + navigator: { + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.0.8 (KHTML, like Gecko) Version/15.1 Safari/605.0.8' + }, + // queueMicrotask should be used even though MessageChannel is present + MessageChannel: fakeMessageChannel + }; + }); + }); - clearStack(function() { - called = true; + describe('in browsers other than Safari', function() { + usesMessageChannel(function() { + return { + navigator: { + // Chrome's user agent string contains "Safari" so it's a good test case + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36' + } + }; }); - expect(called).toBe(true); - expect(setImmediate).toHaveBeenCalled(); + describe('when MessageChannel is unavailable', function() { + usesQueueMicrotaskWithSetTimeout(function() { + return { + navigator: { + userAgent: 'CERN-LineMode/2.15 libwww/2.17b3', + MessageChannel: undefined + } + }; + }); + }); }); - it('uses setTimeout instead of setImmediate every 10 calls to make sure we release the CPU', function() { - const setImmediate = jasmine.createSpy('setImmediate'), - setTimeout = jasmine.createSpy('setTimeout'), - global = { setImmediate: setImmediate, setTimeout: setTimeout }, - clearStack = jasmineUnderTest.getClearStack(global); - - clearStack(function() {}); - clearStack(function() {}); - clearStack(function() {}); - clearStack(function() {}); - clearStack(function() {}); - clearStack(function() {}); - clearStack(function() {}); - clearStack(function() {}); - clearStack(function() {}); - - expect(setImmediate).toHaveBeenCalled(); - expect(setTimeout).not.toHaveBeenCalled(); - - clearStack(function() {}); - expect(setImmediate.calls.count()).toEqual(9); - expect(setTimeout.calls.count()).toEqual(1); - - clearStack(function() {}); - expect(setImmediate.calls.count()).toEqual(10); - expect(setTimeout.calls.count()).toEqual(1); - }); - - it('uses MessageChannels when available', function() { - const fakeChannel = { - port1: {}, - port2: { - postMessage: function() { - fakeChannel.port1.onmessage(); + describe('in Node', function() { + usesQueueMicrotaskWithoutSetTimeout(function() { + return { + process: { + versions: { + node: '3.1415927' } } - }, - global = { - MessageChannel: function() { - return fakeChannel; - } - }, - clearStack = jasmineUnderTest.getClearStack(global); - let called = false; + }; + }); + }); - clearStack(function() { - called = true; + function usesMessageChannel(makeGlobal) { + it('uses MessageChannel', function() { + const global = { + ...makeGlobal(), + MessageChannel: fakeMessageChannel + }; + const clearStack = jasmineUnderTest.getClearStack(global); + let called = false; + + clearStack(function() { + called = true; + }); + + expect(called).toBe(true); }); - expect(called).toBe(true); - }); - - it('uses setTimeout instead of MessageChannel every 10 calls to make sure we release the CPU', function() { - const fakeChannel = { - port1: {}, - port2: { - postMessage: jasmine - .createSpy('postMessage') - .and.callFake(function() { - fakeChannel.port1.onmessage(); - }) - } - }, - setTimeout = jasmine.createSpy('setTimeout'), - global = { + it('uses setTimeout instead of MessageChannel every 10 calls to make sure we release the CPU', function() { + const fakeChannel = fakeMessageChannel(); + spyOn(fakeChannel.port2, 'postMessage'); + const setTimeout = jasmine.createSpy('setTimeout'); + const global = { + ...makeGlobal(), + setTimeout, MessageChannel: function() { return fakeChannel; - }, - setTimeout: setTimeout - }, - clearStack = jasmineUnderTest.getClearStack(global); - - clearStack(function() {}); - clearStack(function() {}); - clearStack(function() {}); - clearStack(function() {}); - clearStack(function() {}); - clearStack(function() {}); - clearStack(function() {}); - clearStack(function() {}); - clearStack(function() {}); - - expect(fakeChannel.port2.postMessage).toHaveBeenCalled(); - expect(setTimeout).not.toHaveBeenCalled(); - - clearStack(function() {}); - expect(fakeChannel.port2.postMessage.calls.count()).toEqual(9); - expect(setTimeout.calls.count()).toEqual(1); - - clearStack(function() {}); - expect(fakeChannel.port2.postMessage.calls.count()).toEqual(10); - expect(setTimeout.calls.count()).toEqual(1); - }); - - it('calls setTimeout when onmessage is called recursively', function() { - const fakeChannel = { - port1: {}, - port2: { - postMessage: function() { - fakeChannel.port1.onmessage(); - } } - }, - setTimeout = jasmine.createSpy('setTimeout'), - global = { - MessageChannel: function() { - return fakeChannel; - }, - setTimeout: setTimeout - }, - clearStack = jasmineUnderTest.getClearStack(global), - fn = jasmine.createSpy('second clearStack function'); + }; + const clearStack = jasmineUnderTest.getClearStack(global); - clearStack(function() { - clearStack(fn); + for (let i = 0; i < 9; i++) { + clearStack(function() {}); + } + + expect(fakeChannel.port2.postMessage).toHaveBeenCalled(); + expect(setTimeout).not.toHaveBeenCalled(); + + clearStack(function() {}); + expect(fakeChannel.port2.postMessage).toHaveBeenCalledTimes(9); + expect(setTimeout).toHaveBeenCalledTimes(1); + + clearStack(function() {}); + expect(fakeChannel.port2.postMessage).toHaveBeenCalledTimes(10); + expect(setTimeout).toHaveBeenCalledTimes(1); }); - expect(fn).not.toHaveBeenCalled(); - expect(setTimeout).toHaveBeenCalledWith(fn, 0); - }); + it('calls setTimeout when onmessage is called recursively', function() { + const setTimeout = jasmine.createSpy('setTimeout'); + const global = { + ...makeGlobal(), + setTimeout, + MessageChannel: fakeMessageChannel + }; + const clearStack = jasmineUnderTest.getClearStack(global); + const fn = jasmine.createSpy('second clearStack function'); - it('falls back to setTimeout', function() { - const setTimeout = jasmine - .createSpy('setTimeout') - .and.callFake(function(fn) { + clearStack(function() { + clearStack(fn); + }); + + expect(fn).not.toHaveBeenCalled(); + expect(setTimeout).toHaveBeenCalledWith(fn, 0); + }); + } + + function usesQueueMicrotaskWithSetTimeout(makeGlobal) { + it('uses queueMicrotask', function() { + const global = { + ...makeGlobal(), + queueMicrotask: function(fn) { fn(); - }), - global = { setTimeout: setTimeout }, - clearStack = jasmineUnderTest.getClearStack(global); - let called = false; + } + }; + const clearStack = jasmineUnderTest.getClearStack(global); + let called = false; - clearStack(function() { - called = true; + clearStack(function() { + called = true; + }); + + expect(called).toBe(true); }); - expect(called).toBe(true); - expect(setTimeout).toHaveBeenCalledWith(jasmine.any(Function), 0); - }); + it('uses setTimeout instead of queueMicrotask every 10 calls to make sure we release the CPU', function() { + const queueMicrotask = jasmine.createSpy('queueMicrotask'); + const setTimeout = jasmine.createSpy('setTimeout'); + const global = { + ...makeGlobal(), + queueMicrotask, + setTimeout + }; + const clearStack = jasmineUnderTest.getClearStack(global); + + for (let i = 0; i < 9; i++) { + clearStack(function() {}); + } + + expect(queueMicrotask).toHaveBeenCalled(); + expect(setTimeout).not.toHaveBeenCalled(); + + clearStack(function() {}); + expect(queueMicrotask).toHaveBeenCalledTimes(9); + expect(setTimeout).toHaveBeenCalledTimes(1); + + clearStack(function() {}); + expect(queueMicrotask).toHaveBeenCalledTimes(10); + expect(setTimeout).toHaveBeenCalledTimes(1); + }); + } + + function usesQueueMicrotaskWithoutSetTimeout(makeGlobal) { + it('uses queueMicrotask', function() { + const global = { + ...makeGlobal(), + queueMicrotask: function(fn) { + fn(); + } + }; + const clearStack = jasmineUnderTest.getClearStack(global); + let called = false; + + clearStack(function() { + called = true; + }); + + expect(called).toBe(true); + }); + + it('does not use setTimeout', function() { + const queueMicrotask = jasmine.createSpy('queueMicrotask'); + const setTimeout = jasmine.createSpy('setTimeout'); + const global = { + ...makeGlobal(), + queueMicrotask, + setTimeout + }; + const clearStack = jasmineUnderTest.getClearStack(global); + + clearStack(function() {}); + clearStack(function() {}); + clearStack(function() {}); + clearStack(function() {}); + clearStack(function() {}); + clearStack(function() {}); + clearStack(function() {}); + clearStack(function() {}); + clearStack(function() {}); + clearStack(function() {}); + + expect(queueMicrotask).toHaveBeenCalledTimes(10); + expect(setTimeout).not.toHaveBeenCalled(); + }); + } + + function fakeMessageChannel() { + const channel = { + port1: {}, + port2: { + postMessage: function() { + channel.port1.onmessage(); + } + } + }; + return channel; + } }); diff --git a/spec/core/integration/EnvSpec.js b/spec/core/integration/EnvSpec.js index 01533038..92f27ad4 100644 --- a/spec/core/integration/EnvSpec.js +++ b/spec/core/integration/EnvSpec.js @@ -436,6 +436,9 @@ describe('Env integration', function() { }, clearTimeout: function(fn, delay) { clearTimeout(fn, delay); + }, + queueMicrotask: function(fn) { + queueMicrotask(fn); } }; spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); @@ -473,6 +476,9 @@ describe('Env integration', function() { }, clearTimeout: function(fn) { clearTimeout(fn); + }, + queueMicrotask: function(fn) { + queueMicrotask(fn); } }; spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); @@ -529,6 +535,9 @@ describe('Env integration', function() { }, clearTimeout: function(fn, delay) { clearTimeout(fn, delay); + }, + queueMicrotask: function(fn) { + queueMicrotask(fn); } }; spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); @@ -545,8 +554,8 @@ describe('Env integration', function() { env.it('fails', function(specDone) { setTimeout(function() { specDone(); - setTimeout(function() { - setTimeout(function() { + queueMicrotask(function() { + queueMicrotask(function() { global.onerror('fail'); }); }); @@ -578,6 +587,9 @@ describe('Env integration', function() { }, clearTimeout: function(fn, delay) { clearTimeout(fn, delay); + }, + queueMicrotask: function(fn) { + queueMicrotask(fn); } }; spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); @@ -638,6 +650,9 @@ describe('Env integration', function() { }, clearTimeout: function(fn, delay) { clearTimeout(fn, delay); + }, + queueMicrotask: function(fn) { + queueMicrotask(fn); } }; spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); @@ -677,6 +692,9 @@ describe('Env integration', function() { }, clearTimeout: function(fn) { clearTimeout(fn); + }, + queueMicrotask: function(fn) { + queueMicrotask(fn); } }; spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); @@ -1427,8 +1445,8 @@ describe('Env integration', function() { global: { setTimeout: globalSetTimeout, clearTimeout: clearTimeout, - setImmediate: function(cb) { - return setTimeout(cb, 0); + queueMicrotask: function(fn) { + queueMicrotask(fn); } } }); @@ -1501,8 +1519,8 @@ describe('Env integration', function() { clearTimeout: clearTimeout, setInterval: setInterval, clearInterval: clearInterval, - setImmediate: function(cb) { - return realSetTimeout(cb, 0); + queueMicrotask: function(fn) { + queueMicrotask(fn); } } }); @@ -2625,6 +2643,9 @@ describe('Env integration', function() { clearTimeout: function(fn, delay) { clearTimeout(fn, delay); }, + queueMicrotask: function(fn) { + queueMicrotask(fn); + }, onerror: function() {} }; spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); @@ -2680,6 +2701,9 @@ describe('Env integration', function() { clearTimeout: function(fn, delay) { clearTimeout(fn, delay); }, + queueMicrotask: function(fn) { + queueMicrotask(fn); + }, onerror: originalOnerror }; spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); @@ -2869,6 +2893,9 @@ describe('Env integration', function() { }, clearTimeout: function(fn, delay) { return clearTimeout(fn, delay); + }, + queueMicrotask: function(fn) { + queueMicrotask(fn); } }; spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); diff --git a/src/core/ClearStack.js b/src/core/ClearStack.js index 3d864244..ced9605a 100644 --- a/src/core/ClearStack.js +++ b/src/core/ClearStack.js @@ -1,8 +1,32 @@ getJasmineRequireObj().clearStack = function(j$) { const maxInlineCallCount = 10; - function messageChannelImpl(global, setTimeout) { - const channel = new global.MessageChannel(); + function browserQueueMicrotaskImpl(global) { + const { setTimeout, queueMicrotask } = global; + let currentCallCount = 0; + return function clearStack(fn) { + currentCallCount++; + + if (currentCallCount < maxInlineCallCount) { + queueMicrotask(fn); + } else { + currentCallCount = 0; + setTimeout(fn); + } + }; + } + + function nodeQueueMicrotaskImpl(global) { + const { queueMicrotask } = global; + + return function(fn) { + queueMicrotask(fn); + }; + } + + function messageChannelImpl(global) { + const { MessageChannel, setTimeout } = global; + const channel = new MessageChannel(); let head = {}; let tail = head; @@ -13,7 +37,7 @@ getJasmineRequireObj().clearStack = function(j$) { delete head.task; if (taskRunning) { - global.setTimeout(task, 0); + setTimeout(task, 0); } else { try { taskRunning = true; @@ -39,29 +63,31 @@ getJasmineRequireObj().clearStack = function(j$) { } function getClearStack(global) { - let currentCallCount = 0; - const realSetTimeout = global.setTimeout; - const setTimeoutImpl = function clearStack(fn) { - Function.prototype.apply.apply(realSetTimeout, [global, [fn, 0]]); - }; + const NODE_JS = + global.process && + global.process.versions && + typeof global.process.versions.node === 'string'; - if (j$.isFunction_(global.setImmediate)) { - const realSetImmediate = global.setImmediate; - return function(fn) { - currentCallCount++; + const SAFARI = + global.navigator && + /^((?!chrome|android).)*safari/i.test(global.navigator.userAgent); - if (currentCallCount < maxInlineCallCount) { - realSetImmediate(fn); - } else { - currentCallCount = 0; - - setTimeoutImpl(fn); - } - }; - } else if (!j$.util.isUndefined(global.MessageChannel)) { - return messageChannelImpl(global, setTimeoutImpl); + if (NODE_JS) { + // Unlike browsers, Node doesn't require us to do a periodic setTimeout + // so we avoid the overhead. + return nodeQueueMicrotaskImpl(global); + } else if ( + SAFARI || + j$.util.isUndefined(global.MessageChannel) /* tests */ + ) { + // queueMicrotask is dramatically faster than MessageChannel in Safari. + // Some of our own integration tests provide a mock queueMicrotask in all + // environments because it's simpler to mock than MessageChannel. + return browserQueueMicrotaskImpl(global); } else { - return setTimeoutImpl; + // MessageChannel is faster than queueMicrotask in supported browsers + // other than Safari. + return messageChannelImpl(global); } } From f934e6d8167024b5adca90cb45630409f2eb1462 Mon Sep 17 00:00:00 2001 From: Steve Gravrock Date: Sat, 20 Aug 2022 10:27:44 -0700 Subject: [PATCH 5/5] Assume that addEventListener/removeEventListener are present in browsers Jasmine 3.0 dropped support for the last browser that didn't support the standard event handler methods (IE 9). --- lib/jasmine-core/jasmine.js | 17 +++----- spec/core/GlobalErrorsSpec.js | 72 ++++++++++++++++++-------------- spec/core/integration/EnvSpec.js | 16 +++++++ src/core/GlobalErrors.js | 17 +++----- 4 files changed, 66 insertions(+), 56 deletions(-) diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index 534da5df..166ffdb6 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -4081,21 +4081,14 @@ getJasmineRequireObj().GlobalErrors = function(j$) { } }; - if (global.addEventListener) { - global.addEventListener( - 'unhandledrejection', - browserRejectionHandler - ); - } + global.addEventListener('unhandledrejection', browserRejectionHandler); this.uninstall = function uninstall() { global.onerror = originalHandler; - if (global.removeEventListener) { - global.removeEventListener( - 'unhandledrejection', - browserRejectionHandler - ); - } + global.removeEventListener( + 'unhandledrejection', + browserRejectionHandler + ); }; } }; diff --git a/spec/core/GlobalErrorsSpec.js b/spec/core/GlobalErrorsSpec.js index 8c8c9e12..dfb714ad 100644 --- a/spec/core/GlobalErrorsSpec.js +++ b/spec/core/GlobalErrorsSpec.js @@ -1,8 +1,8 @@ describe('GlobalErrors', function() { it('calls the added handler on error', function() { - const fakeGlobal = { onerror: null }, - handler = jasmine.createSpy('errorHandler'), - errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); + const fakeGlobal = minimalBrowserGlobal(); + const handler = jasmine.createSpy('errorHandler'); + const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); errors.install(); errors.pushListener(handler); @@ -13,10 +13,10 @@ describe('GlobalErrors', function() { }); it('enables external interception of error by overriding global.onerror', function() { - const fakeGlobal = { onerror: null }, - handler = jasmine.createSpy('errorHandler'), - hijackHandler = jasmine.createSpy('hijackErrorHandler'), - errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); + const fakeGlobal = minimalBrowserGlobal(); + const handler = jasmine.createSpy('errorHandler'); + const hijackHandler = jasmine.createSpy('hijackErrorHandler'); + const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); errors.install(); errors.pushListener(handler); @@ -30,10 +30,10 @@ describe('GlobalErrors', function() { }); it('calls the global error handler with all parameters', function() { - const fakeGlobal = { onerror: null }, - handler = jasmine.createSpy('errorHandler'), - errors = new jasmineUnderTest.GlobalErrors(fakeGlobal), - fooError = new Error('foo'); + const fakeGlobal = minimalBrowserGlobal(); + const handler = jasmine.createSpy('errorHandler'); + const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); + const fooError = new Error('foo'); errors.install(); errors.pushListener(handler); @@ -50,10 +50,10 @@ describe('GlobalErrors', function() { }); it('only calls the most recent handler', function() { - const fakeGlobal = { onerror: null }, - handler1 = jasmine.createSpy('errorHandler1'), - handler2 = jasmine.createSpy('errorHandler2'), - errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); + const fakeGlobal = minimalBrowserGlobal(); + const handler1 = jasmine.createSpy('errorHandler1'); + const handler2 = jasmine.createSpy('errorHandler2'); + const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); errors.install(); errors.pushListener(handler1); @@ -66,10 +66,10 @@ describe('GlobalErrors', function() { }); it('calls previous handlers when one is removed', function() { - const fakeGlobal = { onerror: null }, - handler1 = jasmine.createSpy('errorHandler1'), - handler2 = jasmine.createSpy('errorHandler2'), - errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); + const fakeGlobal = minimalBrowserGlobal(); + const handler1 = jasmine.createSpy('errorHandler1'); + const handler2 = jasmine.createSpy('errorHandler2'); + const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); errors.install(); errors.pushListener(handler1); @@ -91,9 +91,12 @@ describe('GlobalErrors', function() { }); it('uninstalls itself, putting back a previous callback', function() { - const originalCallback = jasmine.createSpy('error'), - fakeGlobal = { onerror: originalCallback }, - errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); + const originalCallback = jasmine.createSpy('error'); + const fakeGlobal = { + ...minimalBrowserGlobal(), + onerror: originalCallback + }; + const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); expect(fakeGlobal.onerror).toBe(originalCallback); @@ -107,9 +110,9 @@ describe('GlobalErrors', function() { }); it('rethrows the original error when there is no handler', function() { - const fakeGlobal = {}, - errors = new jasmineUnderTest.GlobalErrors(fakeGlobal), - originalError = new Error('nope'); + const fakeGlobal = minimalBrowserGlobal(); + const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); + const originalError = new Error('nope'); errors.install(); @@ -407,7 +410,7 @@ describe('GlobalErrors', function() { describe('#setOverrideListener', function() { it('overrides the existing handlers in browsers until removed', function() { - const fakeGlobal = { onerror: null }; + const fakeGlobal = minimalBrowserGlobal(); const handler0 = jasmine.createSpy('handler0'); const handler1 = jasmine.createSpy('handler1'); const overrideHandler = jasmine.createSpy('overrideHandler'); @@ -529,8 +532,7 @@ describe('GlobalErrors', function() { }); it('throws if there is already an override handler', function() { - const fakeGlobal = { onerror: null }; - const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); + const errors = new jasmineUnderTest.GlobalErrors(minimalBrowserGlobal()); errors.setOverrideListener(() => {}, () => {}); expect(function() { @@ -541,9 +543,8 @@ describe('GlobalErrors', function() { describe('#removeOverrideListener', function() { it("calls the handler's onRemove callback", function() { - const fakeGlobal = { onerror: null }; const onRemove = jasmine.createSpy('onRemove'); - const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); + const errors = new jasmineUnderTest.GlobalErrors(minimalBrowserGlobal()); errors.setOverrideListener(() => {}, onRemove); errors.removeOverrideListener(); @@ -552,10 +553,17 @@ describe('GlobalErrors', function() { }); it('does not throw if there is no handler', function() { - const fakeGlobal = { onerror: null }; - const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal); + const errors = new jasmineUnderTest.GlobalErrors(minimalBrowserGlobal()); expect(() => errors.removeOverrideListener()).not.toThrow(); }); }); + + function minimalBrowserGlobal() { + return { + addEventListener() {}, + removeEventListener() {}, + onerror: null + }; + } }); diff --git a/spec/core/integration/EnvSpec.js b/spec/core/integration/EnvSpec.js index 92f27ad4..b61322d2 100644 --- a/spec/core/integration/EnvSpec.js +++ b/spec/core/integration/EnvSpec.js @@ -431,6 +431,7 @@ describe('Env integration', function() { describe('Handling async errors', function() { it('routes async errors to a running spec', async function() { const global = { + ...browserEventMethods(), setTimeout: function(fn, delay) { return setTimeout(fn, delay); }, @@ -471,6 +472,7 @@ describe('Env integration', function() { describe('When the running spec has reported specDone', function() { it('routes async errors to an ancestor suite', async function() { const global = { + ...browserEventMethods(), setTimeout: function(fn, delay) { return setTimeout(fn, delay); }, @@ -530,6 +532,7 @@ describe('Env integration', function() { it('routes async errors to a running suite', async function() { const global = { + ...browserEventMethods(), setTimeout: function(fn, delay) { return setTimeout(fn, delay); }, @@ -582,6 +585,7 @@ describe('Env integration', function() { describe('When the running suite has reported suiteDone', function() { it('routes async errors to an ancestor suite', async function() { const global = { + ...browserEventMethods(), setTimeout: function(fn, delay) { return setTimeout(fn, delay); }, @@ -645,6 +649,7 @@ describe('Env integration', function() { describe('When the env has started reporting jasmineDone', function() { it('logs the error to the console', async function() { const global = { + ...browserEventMethods(), setTimeout: function(fn, delay) { return setTimeout(fn, delay); }, @@ -687,6 +692,7 @@ describe('Env integration', function() { it('routes all errors that occur during stack clearing somewhere', async function() { const global = { + ...browserEventMethods(), setTimeout: function(fn, delay) { return setTimeout(fn, delay); }, @@ -2637,6 +2643,7 @@ describe('Env integration', function() { it('reports errors that occur during loading', async function() { const global = { + ...browserEventMethods(), setTimeout: function(fn, delay) { return setTimeout(fn, delay); }, @@ -2695,6 +2702,7 @@ describe('Env integration', function() { it('does not install a global error handler during loading', async function() { const originalOnerror = jasmine.createSpy('original onerror'); const global = { + ...browserEventMethods(), setTimeout: function(fn, delay) { return setTimeout(fn, delay); }, @@ -2888,6 +2896,7 @@ describe('Env integration', function() { describe('When there are load errors', function() { it('is "failed"', async function() { const global = { + ...browserEventMethods(), setTimeout: function(fn, delay) { return setTimeout(fn, delay); }, @@ -3933,4 +3942,11 @@ describe('Env integration', function() { ); }); }); + + function browserEventMethods() { + return { + addEventListener() {}, + removeEventListener() {} + }; + } }); diff --git a/src/core/GlobalErrors.js b/src/core/GlobalErrors.js index bd121681..11b9697f 100644 --- a/src/core/GlobalErrors.js +++ b/src/core/GlobalErrors.js @@ -109,21 +109,14 @@ getJasmineRequireObj().GlobalErrors = function(j$) { } }; - if (global.addEventListener) { - global.addEventListener( - 'unhandledrejection', - browserRejectionHandler - ); - } + global.addEventListener('unhandledrejection', browserRejectionHandler); this.uninstall = function uninstall() { global.onerror = originalHandler; - if (global.removeEventListener) { - global.removeEventListener( - 'unhandledrejection', - browserRejectionHandler - ); - } + global.removeEventListener( + 'unhandledrejection', + browserRejectionHandler + ); }; } };