Compare commits

..

16 Commits

Author SHA1 Message Date
Steve Gravrock
5cd7d47f72 Bump version to 5.3.0 2024-09-07 13:18:21 -07:00
Steve Gravrock
d0fe5c4712 Require curly braces around loop and conditonal bodies 2024-09-02 11:30:36 -07:00
Steve Gravrock
f602c4911c Merge branch 'dave-unclamp-safari' of https://github.com/dcsaszar/jasmine
* Significantly improves performance in Safari
* Merges #2040 from @dcsaszar
* Fixes #2008
2024-09-02 11:22:56 -07:00
Steve Gravrock
7aaf7eaf30 API reference for reporter capabilities 2024-09-02 10:53:56 -07:00
David Császár
35f16e8125 Add test for unclamping setTimeout in Safari 2024-09-01 10:22:16 +02:00
David Császár
acc777c267 Unclamp setTimeout in Safari
Fixes #2008

wrapping setTimeout in postMessage is a trade-off between
* slowdown due to postMessage (slow on Safari)
* speedup due to no setTimeout clamping (can be severe on Safari)
2024-09-01 10:21:57 +02:00
David Császár
1c74356691 Add unclampedSetTimeout helper 2024-09-01 09:54:55 +02:00
David Császár
bbebea0fa5 Refactor: extract postMessage 2024-09-01 09:31:52 +02:00
Steve Gravrock
66eb27b0af Throw if spying has no effect
This provides a useful diagnostic in cases where assigning to a property
is a no-op, like localStorage in Firefox and Safari 17.

See #2036 and #2007.
2024-08-17 09:07:18 -07:00
Steve Gravrock
7a63c06a65 Merge branch 'webkit-performance' of https://github.com/m-akinc/jasmine
* Merges #2034 from @m-akinc
2024-08-10 07:26:00 -07:00
Mert Akinc
554dfd4923 Update comments as requested 2024-08-05 12:27:11 -05:00
Mert Akinc
36a6e2aa1d Merge branch 'main' into webkit-performance 2024-08-05 12:19:53 -05:00
Steve Gravrock
3c4b73f136 Fixed globbing in own test suite when running on Windows outside of c:\Users
The previous code used path.join() to construct glob input. That should't
work, but it did as long as the working directory was under c:\Users.
2024-08-03 18:06:20 -07:00
Mert Akinc
bc3ed74336 Formatting fix 2024-07-26 17:54:04 -05:00
Mert Akinc
97b6f33cc2 Add test, update rexex pattern and constant name 2024-07-26 17:45:23 -05:00
Mert Akinc
a9889ddb31 Improve performance on Playwright Windows WebKit 2024-07-25 09:54:32 -05:00
14 changed files with 296 additions and 52 deletions

View File

@@ -11,6 +11,7 @@
"ecmaVersion": 2018
},
"rules": {
"curly": "error",
"quotes": [
"error",
"single",

View File

@@ -2430,7 +2430,9 @@ getJasmineRequireObj().MapContaining = function(j$) {
}
MapContaining.prototype.asymmetricMatch = function(other, matchersUtil) {
if (!j$.isMap(other)) return false;
if (!j$.isMap(other)) {
return false;
}
for (const [key, value] of this.sample) {
// for each key/value pair in `sample`
@@ -2569,7 +2571,9 @@ getJasmineRequireObj().SetContaining = function(j$) {
}
SetContaining.prototype.asymmetricMatch = function(other, matchersUtil) {
if (!j$.isSet(other)) return false;
if (!j$.isSet(other)) {
return false;
}
for (const item of this.sample) {
// for each item in `sample` there should be at least one matching item in `other`
@@ -2876,7 +2880,8 @@ getJasmineRequireObj().clearStack = function(j$) {
const maxInlineCallCount = 10;
function browserQueueMicrotaskImpl(global) {
const { setTimeout, queueMicrotask } = global;
const unclampedSetTimeout = getUnclampedSetTimeout(global);
const { queueMicrotask } = global;
let currentCallCount = 0;
return function clearStack(fn) {
currentCallCount++;
@@ -2885,7 +2890,7 @@ getJasmineRequireObj().clearStack = function(j$) {
queueMicrotask(fn);
} else {
currentCallCount = 0;
setTimeout(fn);
unclampedSetTimeout(fn);
}
};
}
@@ -2899,6 +2904,37 @@ getJasmineRequireObj().clearStack = function(j$) {
}
function messageChannelImpl(global) {
const { setTimeout } = global;
const postMessage = getPostMessage(global);
let currentCallCount = 0;
return function clearStack(fn) {
currentCallCount++;
if (currentCallCount < maxInlineCallCount) {
postMessage(fn);
} else {
currentCallCount = 0;
setTimeout(fn);
}
};
}
function getUnclampedSetTimeout(global) {
const { setTimeout } = global;
if (j$.util.isUndefined(global.MessageChannel)) {
return setTimeout;
}
const postMessage = getPostMessage(global);
return function unclampedSetTimeout(fn) {
postMessage(function() {
setTimeout(fn);
});
};
}
function getPostMessage(global) {
const { MessageChannel, setTimeout } = global;
const channel = new MessageChannel();
let head = {};
@@ -2922,17 +2958,9 @@ getJasmineRequireObj().clearStack = function(j$) {
}
};
let currentCallCount = 0;
return function clearStack(fn) {
currentCallCount++;
if (currentCallCount < maxInlineCallCount) {
tail = tail.next = { task: fn };
channel.port2.postMessage(0);
} else {
currentCallCount = 0;
setTimeout(fn);
}
return function postMessage(fn) {
tail = tail.next = { task: fn };
channel.port2.postMessage(0);
};
}
@@ -2942,20 +2970,25 @@ getJasmineRequireObj().clearStack = function(j$) {
global.process.versions &&
typeof global.process.versions.node === 'string';
const SAFARI =
// Windows builds of WebKit have a fairly generic user agent string when no application name is provided:
// e.g. "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/605.1.15 (KHTML, like Gecko)"
const SAFARI_OR_WIN_WEBKIT =
global.navigator &&
/^((?!chrome|android).)*safari/i.test(global.navigator.userAgent);
/(^((?!chrome|android).)*safari)|(Win64; x64\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\)$)/i.test(
global.navigator.userAgent
);
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 ||
SAFARI_OR_WIN_WEBKIT ||
j$.util.isUndefined(global.MessageChannel) /* tests */
) {
// queueMicrotask is dramatically faster than MessageChannel in Safari,
// at least through version 16.
// queueMicrotask is dramatically faster than MessageChannel in Safari
// and other WebKit-based browsers, such as the one distributed by Playwright
// to test Safari-like behavior on Windows.
// Some of our own integration tests provide a mock queueMicrotask in all
// environments because it's simpler to mock than MessageChannel.
return browserQueueMicrotaskImpl(global);
@@ -6650,7 +6683,10 @@ getJasmineRequireObj().toHaveSpyInteractions = function(j$) {
let hasSpy = false;
const calledSpies = [];
for (const spy of Object.values(actual)) {
if (!j$.isSpy(spy)) continue;
if (!j$.isSpy(spy)) {
continue;
}
hasSpy = true;
if (spy.calls.any()) {
@@ -8051,6 +8087,32 @@ getJasmineRequireObj().ReportDispatcher = function(j$) {
};
getJasmineRequireObj().reporterEvents = function() {
/**
* Used to tell Jasmine what optional or uncommonly implemented features
* the reporter supports. If not specified, the defaults described in
* {@link ReporterCapabilities} will apply.
* @name Reporter#reporterCapabilities
* @type ReporterCapabilities | undefined
* @since 5.0
*/
/**
* Used to tell Jasmine what optional or uncommonly implemented features
* the reporter supports.
* @interface ReporterCapabilities
* @see Reporter#reporterCapabilities
* @since 5.0
*/
/**
* Indicates whether the reporter supports parallel execution. Jasmine will
* not allow parallel execution unless all reporters that are in use set this
* capability to true.
* @name ReporterCapabilities#parallel
* @type boolean | undefined
* @default false
* @see running_specs_in_parallel
* @since 5.0
*/
const events = [
/**
* `jasmineStarted` is called after all of the specs have been loaded, but just before execution starts.
@@ -9449,6 +9511,16 @@ getJasmineRequireObj().SpyRegistry = function(j$) {
obj[methodName] = spiedMethod;
// Check if setting the property actually worked. Some objects, such as
// localStorage in Firefox and later Safari versions, have no-op setters.
if (obj[methodName] !== spiedMethod) {
throw new Error(
j$.formatErrorMsg('<spyOn>')(
`Can't spy on ${methodName} because assigning to it had no effect`
)
);
}
return spiedMethod;
};
@@ -10910,5 +10982,5 @@ getJasmineRequireObj().UserContext = function(j$) {
};
getJasmineRequireObj().version = function() {
return '5.2.0';
return '5.3.0';
};

View File

@@ -1,7 +1,7 @@
{
"name": "jasmine-core",
"license": "MIT",
"version": "5.2.0",
"version": "5.3.0",
"repository": {
"type": "git",
"url": "https://github.com/jasmine/jasmine.git"

35
release_notes/5.3.0.md Normal file
View File

@@ -0,0 +1,35 @@
# Jasmine Core 5.3.0 Release Notes
## Changes
* Improved performance in Safari
* Merges [#2040](https://github.com/jasmine/jasmine/pull/2040) from @dcsaszar
* Fixes [#2008](https://github.com/jasmine/jasmine/issues/2008)
* Improved performance in Playwright Webkit on Windows
* Merges [#2034](https://github.com/jasmine/jasmine/pull/2034) from @m-akinc
* Throw if spying has no effect, as when spying on localStorage methods in Firefox and Safari 17
* See [#2036](https://github.com/jasmine/jasmine/issues/2036) and [#2007](https://github.com/jasmine/jasmine/issues/2007)
## Documentation improvements
* Added API reference for reporter capabilities
## Supported environments
This version has been tested in the following environments.
| Environment | Supported versions |
|-------------------|--------------------|
| Node | 18, 20, 22 |
| Safari | 15-17 |
| Chrome | 128 |
| Firefox | 102, 115, 130 |
| Edge | 128 |
------
_Release Notes generated with _[Anchorman](http://github.com/infews/anchorman)_

View File

@@ -20,6 +20,47 @@ describe('ClearStack', function() {
MessageChannel: fakeMessageChannel
};
});
it('uses MessageChannel to reduce setTimeout clamping', function() {
const fakeChannel = fakeMessageChannel();
spyOn(fakeChannel.port2, 'postMessage');
const queueMicrotask = jasmine.createSpy('queueMicrotask');
const global = {
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'
},
MessageChannel: function() {
return fakeChannel;
},
queueMicrotask
};
const clearStack = jasmineUnderTest.getClearStack(global);
for (let i = 0; i < 9; i++) {
clearStack(function() {});
}
expect(fakeChannel.port2.postMessage).not.toHaveBeenCalled();
clearStack(function() {});
expect(fakeChannel.port2.postMessage).toHaveBeenCalledTimes(1);
});
});
describe("in WebKit (Playwright's build for Windows)", function() {
usesQueueMicrotaskWithSetTimeout(function() {
return {
navigator: {
userAgent:
'Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/605.1.15 (KHTML, like Gecko)'
},
// queueMicrotask should be used even though MessageChannel is present
MessageChannel: fakeMessageChannel
};
});
});
describe('in browsers other than Safari', function() {

View File

@@ -94,6 +94,30 @@ describe('SpyRegistry', function() {
}).not.toThrowError(/is not declared writable or has no setter/);
});
it('throws if assigning to the property is a no-op', function() {
const scope = {};
function original() {
return 1;
}
Object.defineProperty(scope, 'myFunc', {
get() {
return original;
},
set() {}
});
const spyRegistry = new jasmineUnderTest.SpyRegistry({
createSpy: createSpy
});
expect(function() {
spyRegistry.spyOn(scope, 'myFunc');
}).toThrowError(
"<spyOn> : Can't spy on myFunc because assigning to it had no effect"
);
});
it('overrides the method on the object and returns the spy', function() {
const originalFunctionWasCalled = false,
spyRegistry = new jasmineUnderTest.SpyRegistry({

View File

@@ -1315,8 +1315,9 @@ describe('Env integration', function() {
'works with constructors when using callThrough spy strategy',
function() {
function MyClass(foo) {
if (!(this instanceof MyClass))
if (!(this instanceof MyClass)) {
throw new Error('You must use the new keyword.');
}
this.foo = foo;
}
const subject = { MyClass: MyClass };

View File

@@ -12,14 +12,12 @@
};
function getSourceFiles() {
const src_files = ['core/**/*.js', 'version.js'].map(function(file) {
return path.join(__dirname, '../../', 'src/', file);
});
const globs = ['../../src/core/**/*.js', '../../src/version.js'];
const srcFiles = globs.flatMap(g => glob.sync(g, { cwd: __dirname }));
const files = src_files.flatMap(g => glob.sync(g));
files.forEach(function(resolvedFile) {
require(resolvedFile);
});
for (const file of srcFiles) {
require(file);
}
}
getSourceFiles();

View File

@@ -2,7 +2,8 @@ getJasmineRequireObj().clearStack = function(j$) {
const maxInlineCallCount = 10;
function browserQueueMicrotaskImpl(global) {
const { setTimeout, queueMicrotask } = global;
const unclampedSetTimeout = getUnclampedSetTimeout(global);
const { queueMicrotask } = global;
let currentCallCount = 0;
return function clearStack(fn) {
currentCallCount++;
@@ -11,7 +12,7 @@ getJasmineRequireObj().clearStack = function(j$) {
queueMicrotask(fn);
} else {
currentCallCount = 0;
setTimeout(fn);
unclampedSetTimeout(fn);
}
};
}
@@ -25,6 +26,37 @@ getJasmineRequireObj().clearStack = function(j$) {
}
function messageChannelImpl(global) {
const { setTimeout } = global;
const postMessage = getPostMessage(global);
let currentCallCount = 0;
return function clearStack(fn) {
currentCallCount++;
if (currentCallCount < maxInlineCallCount) {
postMessage(fn);
} else {
currentCallCount = 0;
setTimeout(fn);
}
};
}
function getUnclampedSetTimeout(global) {
const { setTimeout } = global;
if (j$.util.isUndefined(global.MessageChannel)) {
return setTimeout;
}
const postMessage = getPostMessage(global);
return function unclampedSetTimeout(fn) {
postMessage(function() {
setTimeout(fn);
});
};
}
function getPostMessage(global) {
const { MessageChannel, setTimeout } = global;
const channel = new MessageChannel();
let head = {};
@@ -48,17 +80,9 @@ getJasmineRequireObj().clearStack = function(j$) {
}
};
let currentCallCount = 0;
return function clearStack(fn) {
currentCallCount++;
if (currentCallCount < maxInlineCallCount) {
tail = tail.next = { task: fn };
channel.port2.postMessage(0);
} else {
currentCallCount = 0;
setTimeout(fn);
}
return function postMessage(fn) {
tail = tail.next = { task: fn };
channel.port2.postMessage(0);
};
}
@@ -68,20 +92,25 @@ getJasmineRequireObj().clearStack = function(j$) {
global.process.versions &&
typeof global.process.versions.node === 'string';
const SAFARI =
// Windows builds of WebKit have a fairly generic user agent string when no application name is provided:
// e.g. "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/605.1.15 (KHTML, like Gecko)"
const SAFARI_OR_WIN_WEBKIT =
global.navigator &&
/^((?!chrome|android).)*safari/i.test(global.navigator.userAgent);
/(^((?!chrome|android).)*safari)|(Win64; x64\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\)$)/i.test(
global.navigator.userAgent
);
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 ||
SAFARI_OR_WIN_WEBKIT ||
j$.util.isUndefined(global.MessageChannel) /* tests */
) {
// queueMicrotask is dramatically faster than MessageChannel in Safari,
// at least through version 16.
// queueMicrotask is dramatically faster than MessageChannel in Safari
// and other WebKit-based browsers, such as the one distributed by Playwright
// to test Safari-like behavior on Windows.
// Some of our own integration tests provide a mock queueMicrotask in all
// environments because it's simpler to mock than MessageChannel.
return browserQueueMicrotaskImpl(global);

View File

@@ -84,6 +84,16 @@ getJasmineRequireObj().SpyRegistry = function(j$) {
obj[methodName] = spiedMethod;
// Check if setting the property actually worked. Some objects, such as
// localStorage in Firefox and later Safari versions, have no-op setters.
if (obj[methodName] !== spiedMethod) {
throw new Error(
j$.formatErrorMsg('<spyOn>')(
`Can't spy on ${methodName} because assigning to it had no effect`
)
);
}
return spiedMethod;
};

View File

@@ -11,7 +11,9 @@ getJasmineRequireObj().MapContaining = function(j$) {
}
MapContaining.prototype.asymmetricMatch = function(other, matchersUtil) {
if (!j$.isMap(other)) return false;
if (!j$.isMap(other)) {
return false;
}
for (const [key, value] of this.sample) {
// for each key/value pair in `sample`

View File

@@ -11,7 +11,9 @@ getJasmineRequireObj().SetContaining = function(j$) {
}
SetContaining.prototype.asymmetricMatch = function(other, matchersUtil) {
if (!j$.isSet(other)) return false;
if (!j$.isSet(other)) {
return false;
}
for (const item of this.sample) {
// for each item in `sample` there should be at least one matching item in `other`

View File

@@ -32,7 +32,10 @@ getJasmineRequireObj().toHaveSpyInteractions = function(j$) {
let hasSpy = false;
const calledSpies = [];
for (const spy of Object.values(actual)) {
if (!j$.isSpy(spy)) continue;
if (!j$.isSpy(spy)) {
continue;
}
hasSpy = true;
if (spy.calls.any()) {

View File

@@ -1,4 +1,30 @@
getJasmineRequireObj().reporterEvents = function() {
/**
* Used to tell Jasmine what optional or uncommonly implemented features
* the reporter supports. If not specified, the defaults described in
* {@link ReporterCapabilities} will apply.
* @name Reporter#reporterCapabilities
* @type ReporterCapabilities | undefined
* @since 5.0
*/
/**
* Used to tell Jasmine what optional or uncommonly implemented features
* the reporter supports.
* @interface ReporterCapabilities
* @see Reporter#reporterCapabilities
* @since 5.0
*/
/**
* Indicates whether the reporter supports parallel execution. Jasmine will
* not allow parallel execution unless all reporters that are in use set this
* capability to true.
* @name ReporterCapabilities#parallel
* @type boolean | undefined
* @default false
* @see running_specs_in_parallel
* @since 5.0
*/
const events = [
/**
* `jasmineStarted` is called after all of the specs have been loaded, but just before execution starts.