Compare commits

..

14 Commits

Author SHA1 Message Date
Steve Gravrock
f4be08b657 Bump version to 5.8.0 2025-06-06 17:34:09 -07:00
Steve Gravrock
50ef882a1a Merge branch 'gh1886-spy-args-deep-clone' of https://github.com/evanwalsh/jasmine
Merges #2062 from @evanwaslh
Fixes #1886
2025-06-05 06:54:37 -07:00
Steve Gravrock
c1cd5c6291 Use custom object formatters in spy strategy mismatch errors 2025-06-05 05:46:29 -07:00
Steve Gravrock
63ed2b3948 Include function names in pretty printer output
This helps make matcher errors and spy strategy mismatch errors easier
to understand in cases where the difference involves expecting one
function but getting a different one.
2025-06-04 18:37:44 -07:00
Steve Gravrock
0183acc682 Fix diff building when only one side has a custom object formatter
Fixes #2061
2025-06-04 18:04:40 -07:00
Steve Gravrock
e15819c0dd Test aginast Node 24 2025-05-27 17:32:39 -07:00
Evan Walsh
f694194b2b Allow passing a function to saveArgumentsByValue to customize how arguments are saved
For instance, pass `structuredClone` to do a deep clone.

Fixes https://github.com/jasmine/jasmine/issues/1886
2025-05-27 15:43:21 -04:00
Steve Gravrock
94c00886a6 Merge branch 'setimmedate' of https://github.com/atscott/jasmine
Merges #2058 from @atscott
2025-05-03 10:00:41 -07:00
Steve Gravrock
f5915d7963 Bump version to 5.7.1 2025-05-01 19:31:30 -07:00
Steve Gravrock
15587f3ce3 Merge branch 'autotickuninstall' of https://github.com/atscott/jasmine
Merges #2057 from @atscott
2025-05-01 16:46:30 -07:00
Andrew Scott
3ecddc2555 fixup! fix(Clock): Ensure that uninstalling the clock also stops auto tick 2025-05-01 10:25:24 -07:00
Andrew Scott
6a7c0e6368 perf(clock): use setImmediate for autoTick macrotask in Node
When called within an I/O cycle, `setImmediate` is generally faster because it
is designed to execute immediately after the current I/O event completes,
whereas `setTimeout(0)` gets placed in the timers queue and might be subject to delays.

> The main advantage to using setImmediate() over setTimeout() is setImmediate()
> will always be executed before any timers if scheduled within an I/O cycle,
> independently of how many timers are present.

* https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick#setimmediate-vs-settimeout
* https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick#poll
* https://nodejs.org/en/learn/asynchronous-work/understanding-setimmediate
2025-04-30 14:10:39 -07:00
Andrew Scott
84daa0f5dc fix(Clock): Ensure that uninstalling the clock also stops auto tick
The autotick feature mistakenly does not account for the clock being a
singleton and the re-installation of the clock causes the auto ticking
exit conditions to become true again, before it has a chance to break.
2025-04-30 13:38:10 -07:00
Steve Gravrock
c6b3e947e9 Fixed errors in 5.7.0 release notes 2025-04-29 07:28:36 -07:00
18 changed files with 322 additions and 32 deletions

View File

@@ -4,6 +4,10 @@
version: 2.1
executors:
node24:
docker:
- image: cimg/node:24.0.0
working_directory: ~/workspace
node22:
docker:
- image: cimg/node:22.0.0
@@ -100,6 +104,9 @@ workflows:
push:
jobs:
- build:
executor: node24
name: build_node_24
- build:
executor: node22
name: build_node_22
@@ -125,10 +132,10 @@ workflows:
requires:
- build_node_18
- test_parallel:
executor: node18
name: test_parallel_node_18
executor: node24
name: test_parallel_node_24
requires:
- build_node_18
- build_node_24
- test_parallel:
executor: node22
name: test_parallel_node_22
@@ -139,6 +146,11 @@ workflows:
name: test_parallel_node_20
requires:
- build_node_20
- test_parallel:
executor: node18
name: test_parallel_node_18
requires:
- build_node_18
- test_browsers:
requires:
- build_node_18

View File

@@ -29,7 +29,7 @@ Microsoft Edge) as well as Node.
| Environment | Supported versions |
|-------------------|----------------------------|
| Node | 18, 20, 22 |
| Node | 18, 20, 22, 24 |
| Safari | 15*, 16*, 17* |
| Chrome | Evergreen |
| Firefox | Evergreen, 102*, 115*, 128 |

View File

@@ -2803,7 +2803,7 @@ getJasmineRequireObj().CallTracker = function(j$) {
this.track = function(context) {
if (opts.cloneArgs) {
context.args = j$.util.cloneArgs(context.args);
context.args = opts.argsCloner(context.args);
}
calls.push(context);
};
@@ -2911,13 +2911,15 @@ getJasmineRequireObj().CallTracker = function(j$) {
};
/**
* Set this spy to do a shallow clone of arguments passed to each invocation.
* Set this spy to do a clone of arguments passed to each invocation.
* @name Spy#calls#saveArgumentsByValue
* @since 2.5.0
* @param {Function} [argsCloner] A function to use to clone the arguments. Defaults to a shallow cloning function.
* @function
*/
this.saveArgumentsByValue = function() {
this.saveArgumentsByValue = function(argsCloner = j$.util.cloneArgs) {
opts.cloneArgs = true;
opts.argsCloner = argsCloner;
};
this.unverifiedCount = function() {
@@ -3125,6 +3127,10 @@ getJasmineRequireObj().Clock = function() {
* @function
*/
this.uninstall = function() {
// Ensure auto ticking loop is aborted when clock is uninstalled
if (tickMode.mode === 'auto') {
tickMode = { mode: 'manual', counter: tickMode.counter + 1 };
}
delayedFunctionScheduler = null;
mockDate.uninstall();
replace(global, realTimingFunctions);
@@ -3278,6 +3284,12 @@ callbacks to execute _before_ running the next one.
//
// @return {!Promise<undefined>}
async function newMacrotask() {
if (NODE_JS) {
// setImmediate is generally faster than setTimeout in Node
// https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick#setimmediate-vs-settimeout
return new Promise(resolve => void setImmediate(resolve));
}
// MessageChannel ensures that setTimeout is not throttled to 4ms.
// https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#reasons_for_delays_longer_than_specified
// https://stackblitz.com/edit/stackblitz-starters-qtlpcc
@@ -4871,7 +4883,10 @@ getJasmineRequireObj().DiffBuilder = function(j$) {
);
if (useCustom) {
messages.push(wrapPrettyPrinted(actualCustom, expectedCustom, path));
const prettyActual = actualCustom || this.prettyPrinter_(actual);
const prettyExpected =
expectedCustom || this.prettyPrinter_(expected);
messages.push(wrapPrettyPrinted(prettyActual, prettyExpected, path));
return false; // don't recurse further
}
@@ -7748,7 +7763,11 @@ getJasmineRequireObj().makePrettyPrinter = function(j$) {
} else if (value instanceof RegExp) {
this.emitScalar(value.toString());
} else if (typeof value === 'function') {
this.emitScalar('Function');
if (value.name) {
this.emitScalar(`Function '${value.name}'`);
} else {
this.emitScalar('Function');
}
} else if (j$.isDomNode(value)) {
if (value.tagName) {
this.emitDomElement(value);
@@ -9664,7 +9683,7 @@ getJasmineRequireObj().Spy = function(j$) {
"Spy '" +
strategyArgs.name +
"' received a call with arguments " +
j$.basicPrettyPrinter_(Array.prototype.slice.call(args)) +
matchersUtil.pp(Array.prototype.slice.call(args)) +
' but all configured strategies specify other arguments.'
);
} else {
@@ -11391,5 +11410,5 @@ getJasmineRequireObj().UserContext = function(j$) {
};
getJasmineRequireObj().version = function() {
return '5.7.0';
return '5.8.0';
};

View File

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

View File

@@ -6,7 +6,6 @@
to automatically tick the clock asynchronously
* Merges #2042 from @atscott and @stephenfarrar
* Fixes #1725
* Fixes #1932
* Expose [spec path](https://jasmine.github.io/api/5.7/Spec.html#getPath) as an
array of names in addition to the existing concatenated name
@@ -54,10 +53,9 @@ This version has been tested in the following environments.
\* Evergreen browser. Each version of Jasmine is tested against the latest
version available at release time.<br>
\** Environments that are past end of life are supported on a best-effort basis.
They may be dropped in a future minor release of Jasmine if continued support
becomes impractical.
\** Supported on a best-effort basis. Support for these versions may be dropped
if it becomes impractical, and bugs affecting only these versions may not be
treated as release blockers.
------

28
release_notes/5.7.1.md Normal file
View File

@@ -0,0 +1,28 @@
# Jasmine Core 5.7.1 Release Notes
## Bug fixes
* Ensure that uninstalling the clock also stops auto tick
* Merges #2057 from @atscott
## Supported environments
This version has been tested in the following environments.
| Environment | Supported versions |
|-------------------|-------------------------|
| Node | 18**, 20, 22 |
| Safari | 15**, 16**, 17** |
| Chrome | 136* |
| Firefox | 102**, 115**, 128, 138* |
| Edge | 135* |
\* Evergreen browser. Each version of Jasmine is tested against the latest
version available at release time.<br>
\** Supported on a best-effort basis. Support for these versions may be dropped
if it becomes impractical, and bugs affecting only these versions may not be
treated as release blockers.
------
_Release Notes generated with _[Anchorman](http://github.com/infews/anchorman)_

44
release_notes/5.8.0.md Normal file
View File

@@ -0,0 +1,44 @@
# Jasmine Core 5.8.0 Release Notes
## New Features
* Allow passing a function to `saveArgumentsByValue` to customize how arguments
are saved
* Merges [#2062](https://github.com/jasmine/jasmine/pull/2062) from @evanwaslh
* Fixes [#1886](https://github.com/jasmine/jasmine/issues/1886)
* Use custom object formatters in spy strategy mismatch errors
* Include function names in pretty printer output
* Improve performance of autoTick in Node
* Merges [#2058](https://github.com/jasmine/jasmine/pull/2058) from @atscott
## Bug Fixes
* Fix diff building when only one side has a custom object formatter
* Fixes [#2061](https://github.com/jasmine/jasmine/issues/2061)
## Documentation improvements
* Added Node 24 to supported environments
## Supported environments
This version has been tested in the following environments.
| Environment | Supported versions |
|-------------------|-------------------------|
| Node | 18**, 20, 22, 24 |
| Safari | 15**, 16**, 17** |
| Chrome | 137* |
| Firefox | 102**, 115**, 128, 139* |
| Edge | 137* |
\* Evergreen browser. Each version of Jasmine is tested against the latest
version available at release time.<br>
\** Supported on a best-effort basis. Support for these versions may be dropped
if it becomes impractical, and bugs affecting only these versions may not be
treated as release blockers.
------
_Release Notes generated with _[Anchorman](http://github.com/infews/anchorman)_

View File

@@ -134,6 +134,42 @@ describe('CallTracker', function() {
expect(callTracker.mostRecent().args[1]).toEqual(arrayArg);
});
it('allows object arguments to be deep cloned', function() {
const callTracker = new jasmineUnderTest.CallTracker();
callTracker.saveArgumentsByValue(args => JSON.parse(JSON.stringify(args)));
const objectArg = { foo: { bar: { baz: ['qux'] } } },
arrayArg = ['foo', 'bar'];
callTracker.track({
object: {},
args: [objectArg, arrayArg, false, undefined, null, NaN, '', 0, 1.0]
});
objectArg.foo.bar.baz.push('quux');
expect(callTracker.mostRecent().args[0]).not.toBe(objectArg);
expect(callTracker.mostRecent().args[0]).not.toEqual(objectArg);
expect(callTracker.mostRecent().args[0]).toEqual({
foo: { bar: { baz: ['qux'] } }
});
expect(callTracker.mostRecent().args[1]).not.toBe(arrayArg);
expect(callTracker.mostRecent().args[1]).toEqual(arrayArg);
});
it('can take any function to transform arguments when saving by value', function() {
const callTracker = new jasmineUnderTest.CallTracker();
callTracker.saveArgumentsByValue(JSON.stringify);
const objectArg = { foo: { bar: { baz: ['qux'] } } },
arrayArg = ['foo', 'bar'],
args = [objectArg, arrayArg, false, undefined, null, NaN, '', 0, 1.0];
callTracker.track({ object: {}, args });
expect(callTracker.mostRecent().args).toEqual(JSON.stringify(args));
});
it('saves primitive arguments by value', function() {
const callTracker = new jasmineUnderTest.CallTracker(),
args = [undefined, null, false, '', /\s/, 0, 1.2, NaN];

View File

@@ -699,22 +699,39 @@ describe('Clock (acceptance)', function() {
tick: function() {},
uninstall: function() {}
};
// window setTimeout to window to make firefox happy
const _setTimeout =
typeof window !== 'undefined' ? setTimeout.bind(window) : setTimeout;
// passing a fake global allows us to preserve the real timing functions for use in tests
const _global = { setTimeout: _setTimeout, setInterval: setInterval };
clock = new jasmineUnderTest.Clock(
// We use the real window for global or firefox is displeased when we try to call a real setTimeout on an object "that doesn't implement window".
typeof window !== 'undefined' ? window : { setTimeout: setTimeout },
_global,
function() {
return delayedFunctionScheduler;
},
mockDate
);
clock.install();
clock.autoTick();
clock.install().autoTick();
});
afterEach(() => {
clock.uninstall();
});
it('flushes microtask queue between macrotasks', async () => {
const log = [];
await new Promise(r => clock.setTimeout(r, 10)).then(() => {
log.push(1);
Promise.resolve().then(() => log.push(2));
Promise.resolve().then(() => log.push(3));
});
await new Promise(r => clock.setTimeout(r, 10)).then(() => {
log.push(4);
Promise.resolve().then(() => log.push(5));
});
expect(log).toEqual([1, 2, 3, 4, 5]);
});
it('can run setTimeouts/setIntervals asynchronously', function() {
const recurring = jasmine.createSpy('recurring'),
fn1 = jasmine.createSpy('fn1'),
@@ -776,6 +793,26 @@ describe('Clock (acceptance)', function() {
});
});
it('aborts auto ticking when uninstalled, even if installed again synchonrously', async () => {
clock.uninstall();
clock.install();
let resolved = false;
const promise = new Promise(resolve => {
clock.setTimeout(resolve, 1);
}).then(() => {
resolved = true;
});
// wait some real time and verify that the clock did not flush the timer above automatically
await new Promise(resolve => setTimeout(resolve, 2));
expect(resolved).toBe(false);
// enabling auto tick again will flush the timer
clock.autoTick();
await expectAsync(promise).toBeResolved();
});
it('speeds up the execution of the timers in all browsers', async () => {
const startTimeMs = performance.now() / 1000;
await new Promise(resolve => clock.setTimeout(resolve, 5000));

View File

@@ -164,7 +164,7 @@ describe('PrettyPrinter', function() {
"Object({ foo: 'bar', baz: 3, nullValue: null, undefinedValue: undefined })"
);
expect(pp({ foo: function() {}, bar: [1, 2, 3] })).toEqual(
'Object({ foo: Function, bar: [ 1, 2, 3 ] })'
"Object({ foo: Function 'foo', bar: [ 1, 2, 3 ] })"
);
});
@@ -450,7 +450,7 @@ describe('PrettyPrinter', function() {
};
expect(pp(objFromOtherContext)).toEqual(
"Object({ foo: 'bar', toString: Function })"
"Object({ foo: 'bar', toString: Function 'toString' })"
);
});
@@ -477,6 +477,17 @@ describe('PrettyPrinter', function() {
expect(pp(a)).toEqual('<anonymous>({ })');
});
it('stringifies functions with names', function() {
const pp = jasmineUnderTest.makePrettyPrinter();
expect(pp(foo)).toEqual("Function 'foo'");
function foo() {}
});
it('stringifies functions without names', function() {
const pp = jasmineUnderTest.makePrettyPrinter();
expect(pp(function() {})).toEqual('Function');
});
it('should handle objects with null prototype', function() {
const pp = jasmineUnderTest.makePrettyPrinter();
const obj = Object.create(null);

View File

@@ -3855,6 +3855,35 @@ describe('Env integration', function() {
expect(failedExpectations).toEqual([]);
});
it('uses custom object formatters in spy strategy argument mismatch errors', async function() {
env.it('a spec', function() {
env.addCustomObjectFormatter(function(value) {
if (typeof value === 'string') {
return 'custom:' + value;
}
});
const spy = env
.createSpy('foo')
.withArgs('x')
.and.returnValue('');
spy('y');
});
let failedExpectations;
env.addReporter({
specDone: r => (failedExpectations = r.failedExpectations)
});
await env.execute();
expect(failedExpectations).toEqual([
jasmine.objectContaining({
message: jasmine.stringContaining(
'received a call with arguments [ custom:y ]'
)
})
]);
});
describe('#spyOnGlobalErrorsAsync', function() {
const leftInstalledMessage =
'Global error spy was not uninstalled. ' +

View File

@@ -161,6 +161,63 @@ describe('DiffBuilder', function() {
expect(diffBuilder.getMessage()).toEqual(expectedMsg);
});
it('handles cases where only the expected has a custom object formatter', function() {
const formatter = function(x) {
if (typeof x === 'number') {
return '[number:' + x + ']';
}
};
const prettyPrinter = jasmineUnderTest.makePrettyPrinter([formatter]);
const diffBuilder = new jasmineUnderTest.DiffBuilder({
prettyPrinter: prettyPrinter
});
diffBuilder.setRoots('five', 4);
diffBuilder.recordMismatch();
expect(diffBuilder.getMessage()).toEqual(
"Expected 'five' to equal [number:4]."
);
});
it('handles cases where only the actual has a custom object formatter', function() {
const formatter = function(x) {
if (typeof x === 'number') {
return '[number:' + x + ']';
}
};
const prettyPrinter = jasmineUnderTest.makePrettyPrinter([formatter]);
const diffBuilder = new jasmineUnderTest.DiffBuilder({
prettyPrinter: prettyPrinter
});
diffBuilder.setRoots(5, 'four');
diffBuilder.recordMismatch();
expect(diffBuilder.getMessage()).toEqual(
"Expected [number:5] to equal 'four'."
);
});
it('handles complex cases where only one side has a custom object formatter', function() {
const formatter = function(x) {
if (typeof x === 'number') {
return '[number:' + x + ']';
}
};
const prettyPrinter = jasmineUnderTest.makePrettyPrinter([formatter]);
const diffBuilder = new jasmineUnderTest.DiffBuilder({
prettyPrinter: prettyPrinter
});
diffBuilder.setRoots(5, { foo: 'bar', fnord: { graults: ['wombat'] } });
diffBuilder.recordMismatch();
expect(diffBuilder.getMessage()).toEqual(
"Expected [number:5] to equal Object({ foo: 'bar', fnord: Object({ graults: [ 'wombat' ] }) })."
);
});
it('builds diffs involving asymmetric equality testers that implement valuesForDiff_ at the root', function() {
const prettyPrinter = jasmineUnderTest.makePrettyPrinter([]),
diffBuilder = new jasmineUnderTest.DiffBuilder({

View File

@@ -458,9 +458,9 @@ describe('toEqual', function() {
});
it('reports mismatches between Functions', function() {
const actual = { x: function() {} },
expected = { x: function() {} },
message = 'Expected $.x = Function to equal Function.';
const actual = { x: function() {} };
const expected = { x: function() {} };
const message = "Expected $.x = Function 'x' to equal Function 'x'.";
expect(compareEquals(actual, expected).message).toEqual(message);
});

View File

@@ -9,7 +9,7 @@ getJasmineRequireObj().CallTracker = function(j$) {
this.track = function(context) {
if (opts.cloneArgs) {
context.args = j$.util.cloneArgs(context.args);
context.args = opts.argsCloner(context.args);
}
calls.push(context);
};
@@ -117,13 +117,15 @@ getJasmineRequireObj().CallTracker = function(j$) {
};
/**
* Set this spy to do a shallow clone of arguments passed to each invocation.
* Set this spy to do a clone of arguments passed to each invocation.
* @name Spy#calls#saveArgumentsByValue
* @since 2.5.0
* @param {Function} [argsCloner] A function to use to clone the arguments. Defaults to a shallow cloning function.
* @function
*/
this.saveArgumentsByValue = function() {
this.saveArgumentsByValue = function(argsCloner = j$.util.cloneArgs) {
opts.cloneArgs = true;
opts.argsCloner = argsCloner;
};
this.unverifiedCount = function() {

View File

@@ -69,6 +69,10 @@ getJasmineRequireObj().Clock = function() {
* @function
*/
this.uninstall = function() {
// Ensure auto ticking loop is aborted when clock is uninstalled
if (tickMode.mode === 'auto') {
tickMode = { mode: 'manual', counter: tickMode.counter + 1 };
}
delayedFunctionScheduler = null;
mockDate.uninstall();
replace(global, realTimingFunctions);
@@ -222,6 +226,12 @@ callbacks to execute _before_ running the next one.
//
// @return {!Promise<undefined>}
async function newMacrotask() {
if (NODE_JS) {
// setImmediate is generally faster than setTimeout in Node
// https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick#setimmediate-vs-settimeout
return new Promise(resolve => void setImmediate(resolve));
}
// MessageChannel ensures that setTimeout is not throttled to 4ms.
// https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#reasons_for_delays_longer_than_specified
// https://stackblitz.com/edit/stackblitz-starters-qtlpcc

View File

@@ -35,7 +35,11 @@ getJasmineRequireObj().makePrettyPrinter = function(j$) {
} else if (value instanceof RegExp) {
this.emitScalar(value.toString());
} else if (typeof value === 'function') {
this.emitScalar('Function');
if (value.name) {
this.emitScalar(`Function '${value.name}'`);
} else {
this.emitScalar('Function');
}
} else if (j$.isDomNode(value)) {
if (value.tagName) {
this.emitDomElement(value);

View File

@@ -161,7 +161,7 @@ getJasmineRequireObj().Spy = function(j$) {
"Spy '" +
strategyArgs.name +
"' received a call with arguments " +
j$.basicPrettyPrinter_(Array.prototype.slice.call(args)) +
matchersUtil.pp(Array.prototype.slice.call(args)) +
' but all configured strategies specify other arguments.'
);
} else {

View File

@@ -37,7 +37,10 @@ getJasmineRequireObj().DiffBuilder = function(j$) {
);
if (useCustom) {
messages.push(wrapPrettyPrinted(actualCustom, expectedCustom, path));
const prettyActual = actualCustom || this.prettyPrinter_(actual);
const prettyExpected =
expectedCustom || this.prettyPrinter_(expected);
messages.push(wrapPrettyPrinted(prettyActual, prettyExpected, path));
return false; // don't recurse further
}