Compare commits

...

7 Commits

Author SHA1 Message Date
Steve Gravrock 5cc739d879 Bump version to 5.0.0-alpha.1 2023-04-08 13:02:21 -07:00
Steve Gravrock 1e7f07259e API docs for parallel support things that jasmine-npm uses 2023-04-08 12:37:40 -07:00
Steve Gravrock c36a5cfd96 Parallel: Cleaner interface for reporter dispatching
This gets jasmine-npm out of having to deal with QueueRunner, GlobalErrors,
and ReportDispatcher directly.
2023-04-08 11:41:15 -07:00
Steve Gravrock 299fd1f770 Removed unnecessary TODO 2023-03-25 11:21:04 -07:00
Steve Gravrock 656427d328 Parallel: Disallow calls to Env#config from spec and helper files
Such configuration changes only affect one worker, which is almost certainly
not the intent.
2023-03-25 11:16:54 -07:00
Steve Gravrock 621522fdd4 Updated Glob 2023-03-21 22:35:15 -07:00
Steve Gravrock 6e3589bf52 Updated most dev dependencies 2023-03-21 22:33:24 -07:00
13 changed files with 660 additions and 223 deletions
+206 -103
View File
@@ -89,7 +89,9 @@ var getJasmineRequireObj = (function(jasmineGlobal) {
j$.CompleteOnFirstErrorSkipPolicy = jRequire.CompleteOnFirstErrorSkipPolicy( j$.CompleteOnFirstErrorSkipPolicy = jRequire.CompleteOnFirstErrorSkipPolicy(
j$ j$
); );
j$.reporterEvents = jRequire.reporterEvents(j$);
j$.ReportDispatcher = jRequire.ReportDispatcher(j$); j$.ReportDispatcher = jRequire.ReportDispatcher(j$);
j$.ParallelReportDispatcher = jRequire.ParallelReportDispatcher(j$);
j$.RunableResources = jRequire.RunableResources(j$); j$.RunableResources = jRequire.RunableResources(j$);
j$.Runner = jRequire.Runner(j$); j$.Runner = jRequire.Runner(j$);
j$.Spec = jRequire.Spec(j$); j$.Spec = jRequire.Spec(j$);
@@ -1309,6 +1311,12 @@ getJasmineRequireObj().Env = function(j$) {
* @function * @function
*/ */
this.configure = function(configuration) { this.configure = function(configuration) {
if (parallelLoadingState) {
throw new Error(
'Jasmine cannot be configured via Env in parallel mode'
);
}
const booleanProps = [ const booleanProps = [
'random', 'random',
'failSpecWithNoExpectations', 'failSpecWithNoExpectations',
@@ -1549,72 +1557,7 @@ getJasmineRequireObj().Env = function(j$) {
* @see custom_reporter * @see custom_reporter
*/ */
reporter = new j$.ReportDispatcher( reporter = new j$.ReportDispatcher(
[ j$.reporterEvents,
/**
* `jasmineStarted` is called after all of the specs have been loaded, but just before execution starts.
* @function
* @name Reporter#jasmineStarted
* @param {JasmineStartedInfo} suiteInfo Information about the full Jasmine suite that is being run
* @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on.
* @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion.
* @see async
*/
'jasmineStarted',
/**
* When the entire suite has finished execution `jasmineDone` is called
* @function
* @name Reporter#jasmineDone
* @param {JasmineDoneInfo} suiteInfo Information about the full Jasmine suite that just finished running.
* @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on.
* @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion.
* @see async
*/
'jasmineDone',
/**
* `suiteStarted` is invoked when a `describe` starts to run
* @function
* @name Reporter#suiteStarted
* @param {SuiteResult} result Information about the individual {@link describe} being run
* @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on.
* @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion.
* @see async
*/
'suiteStarted',
/**
* `suiteDone` is invoked when all of the child specs and suites for a given suite have been run
*
* While jasmine doesn't require any specific functions, not defining a `suiteDone` will make it impossible for a reporter to know when a suite has failures in an `afterAll`.
* @function
* @name Reporter#suiteDone
* @param {SuiteResult} result
* @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on.
* @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion.
* @see async
*/
'suiteDone',
/**
* `specStarted` is invoked when an `it` starts to run (including associated `beforeEach` functions)
* @function
* @name Reporter#specStarted
* @param {SpecResult} result Information about the individual {@link it} being run
* @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on.
* @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion.
* @see async
*/
'specStarted',
/**
* `specDone` is invoked when an `it` and its associated `beforeEach` and `afterEach` functions have been run.
*
* While jasmine doesn't require any specific functions, not defining a `specDone` will make it impossible for a reporter to know when a spec has failed.
* @function
* @name Reporter#specDone
* @param {SpecResult} result
* @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on.
* @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion.
* @see async
*/
'specDone'
],
function(options) { function(options) {
options.SkipPolicy = j$.NeverSkipPolicy; options.SkipPolicy = j$.NeverSkipPolicy;
return queueRunnerFactory(options); return queueRunnerFactory(options);
@@ -1638,7 +1581,6 @@ getJasmineRequireObj().Env = function(j$) {
}; };
this.parallelReset = function() { this.parallelReset = function() {
// TODO: ensure that autoCleanClosures was false
suiteBuilder.parallelReset(); suiteBuilder.parallelReset();
runner.parallelReset(); runner.parallelReset();
}; };
@@ -7167,6 +7109,100 @@ getJasmineRequireObj().NeverSkipPolicy = function(j$) {
return NeverSkipPolicy; return NeverSkipPolicy;
}; };
getJasmineRequireObj().ParallelReportDispatcher = function(j$) {
/**
* @class ParallelReportDispatcher
* @implements Reporter
* @classdesc A report dispatcher packaged for convenient use from outside jasmine-core.
*
* This is intended to help packages like `jasmine` (the Jasmine runner for
* Node.js) do their own report dispatching in order to support parallel
* execution. If you aren't implementing a runner package that supports
* parallel execution, this class probably isn't what you're looking for.
*
* Warning: Do not use ParallelReportDispatcher in the same process that
* Jasmine specs run in. Doing so will break Jasmine's error handling.
* @param onError {function} Function called when an unhandled exception, unhandled promise rejection, or explicit reporter failure occurs
*/
function ParallelReportDispatcher(onError, deps = {}) {
const ReportDispatcher = deps.ReportDispatcher || j$.ReportDispatcher;
const QueueRunner = deps.QueueRunner || j$.QueueRunner;
const globalErrors = deps.globalErrors || new j$.GlobalErrors();
const dispatcher = ReportDispatcher(
j$.reporterEvents,
function(queueRunnerOptions) {
queueRunnerOptions = {
...queueRunnerOptions,
globalErrors,
timeout: { setTimeout, clearTimeout },
fail: function(error) {
// A callback-style async reporter called either done.fail()
// or done(anError).
if (!error) {
error = new Error('A reporter called done.fail()');
}
onError(error);
},
onException: function(error) {
// A reporter method threw an exception or returned a rejected
// promise, or there was an unhandled exception or unhandled promise
// rejection while an asynchronous reporter method was running.
onError(error);
}
};
new QueueRunner(queueRunnerOptions).execute();
},
function(error) {
// A reporter called done() more than once.
onError(error);
}
);
const self = {
/**
* Adds a reporter to the list of reporters that events will be dispatched to.
* @function
* @name ParallelReportDispatcher#addReporter
* @param {Reporter} reporterToAdd The reporter to be added.
* @see custom_reporter
*/
addReporter: dispatcher.addReporter.bind(dispatcher),
/**
* Clears all registered reporters.
* @function
* @name ParallelReportDispatcher#clearReporters
*/
clearReporters: dispatcher.clearReporters.bind(dispatcher),
/**
* Installs a global error handler. After this method is called, any
* unhandled exceptions or unhandled promise rejections will be passed to
* the onError callback that was passed to the constructor.
* @function
* @name ParallelReportDispatcher#installGlobalErrors
*/
installGlobalErrors: globalErrors.install.bind(globalErrors),
/**
* Uninstalls the global error handler.
* @function
* @name ParallelReportDispatcher#uninstallGlobalErrors
*/
uninstallGlobalErrors: function() {
// late-bind uninstall because it doesn't exist until install is called
globalErrors.uninstall(globalErrors);
}
};
for (const eventName of j$.reporterEvents) {
self[eventName] = dispatcher[eventName].bind(dispatcher);
}
return self;
}
return ParallelReportDispatcher;
};
getJasmineRequireObj().makePrettyPrinter = function(j$) { getJasmineRequireObj().makePrettyPrinter = function(j$) {
class SinglePrettyPrintRun { class SinglePrettyPrintRun {
constructor(customObjectFormatters, pp) { constructor(customObjectFormatters, pp) {
@@ -7528,15 +7564,6 @@ getJasmineRequireObj().makePrettyPrinter = function(j$) {
}; };
getJasmineRequireObj().QueueRunner = function(j$) { getJasmineRequireObj().QueueRunner = function(j$) {
/*
QueueRunner isn't part of the public interface, but it is used by
jasmine-npm. -core and -npm version in lockstep at the major and minor
levels but version independently at the patch level. Any changes that
would break -npm should be done in a major or minor release, never a
patch release, and accompanied by a change to -npm that's released in
the same version.
*/
let nextid = 1; let nextid = 1;
function StopExecutionError() {} function StopExecutionError() {}
@@ -7793,17 +7820,21 @@ getJasmineRequireObj().QueueRunner = function(j$) {
// on the stack at this point. // on the stack at this point.
if (j$.isAsyncFunction_(fn)) { if (j$.isAsyncFunction_(fn)) {
this.onException( this.onException(
'An asynchronous before/it/after ' + new Error(
'function was defined with the async keyword but also took a ' + 'An asynchronous before/it/after ' +
'done callback. Either remove the done callback (recommended) or ' + 'function was defined with the async keyword but also took a ' +
'remove the async keyword.' 'done callback. Either remove the done callback (recommended) or ' +
'remove the async keyword.'
)
); );
} else { } else {
this.onException( this.onException(
'An asynchronous before/it/after ' + new Error(
'function took a done callback but also returned a promise. ' + 'An asynchronous before/it/after ' +
'Either remove the done callback (recommended) or change the ' + 'function took a done callback but also returned a promise. ' +
'function to not return a promise.' 'Either remove the done callback (recommended) or change the ' +
'function to not return a promise.'
)
); );
} }
} }
@@ -7823,15 +7854,6 @@ getJasmineRequireObj().QueueRunner = function(j$) {
}; };
getJasmineRequireObj().ReportDispatcher = function(j$) { getJasmineRequireObj().ReportDispatcher = function(j$) {
/*
ReportDispatcher isn't part of the public interface, but it is used by
jasmine-npm. -core and -npm version in lockstep at the major and minor
levels but version independently at the patch level. Any changes that
would break -npm should be done in a major or minor release, never a
patch release, and accompanied by a change to -npm that's released in
the same version.
*/
function ReportDispatcher(methods, queueRunnerFactory, onLateError) { function ReportDispatcher(methods, queueRunnerFactory, onLateError) {
const dispatchedMethods = methods || []; const dispatchedMethods = methods || [];
@@ -7912,6 +7934,77 @@ getJasmineRequireObj().ReportDispatcher = function(j$) {
return ReportDispatcher; return ReportDispatcher;
}; };
getJasmineRequireObj().reporterEvents = function() {
const events = [
/**
* `jasmineStarted` is called after all of the specs have been loaded, but just before execution starts.
* @function
* @name Reporter#jasmineStarted
* @param {JasmineStartedInfo} suiteInfo Information about the full Jasmine suite that is being run
* @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on.
* @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion.
* @see async
*/
'jasmineStarted',
/**
* When the entire suite has finished execution `jasmineDone` is called
* @function
* @name Reporter#jasmineDone
* @param {JasmineDoneInfo} suiteInfo Information about the full Jasmine suite that just finished running.
* @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on.
* @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion.
* @see async
*/
'jasmineDone',
/**
* `suiteStarted` is invoked when a `describe` starts to run
* @function
* @name Reporter#suiteStarted
* @param {SuiteResult} result Information about the individual {@link describe} being run
* @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on.
* @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion.
* @see async
*/
'suiteStarted',
/**
* `suiteDone` is invoked when all of the child specs and suites for a given suite have been run
*
* While jasmine doesn't require any specific functions, not defining a `suiteDone` will make it impossible for a reporter to know when a suite has failures in an `afterAll`.
* @function
* @name Reporter#suiteDone
* @param {SuiteResult} result
* @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on.
* @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion.
* @see async
*/
'suiteDone',
/**
* `specStarted` is invoked when an `it` starts to run (including associated `beforeEach` functions)
* @function
* @name Reporter#specStarted
* @param {SpecResult} result Information about the individual {@link it} being run
* @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on.
* @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion.
* @see async
*/
'specStarted',
/**
* `specDone` is invoked when an `it` and its associated `beforeEach` and `afterEach` functions have been run.
*
* While jasmine doesn't require any specific functions, not defining a `specDone` will make it impossible for a reporter to know when a spec has failed.
* @function
* @name Reporter#specDone
* @param {SpecResult} result
* @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on.
* @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion.
* @see async
*/
'specDone'
];
Object.freeze(events);
return events;
};
getJasmineRequireObj().interface = function(jasmine, env) { getJasmineRequireObj().interface = function(jasmine, env) {
const jasmineInterface = { const jasmineInterface = {
/** /**
@@ -10293,31 +10386,41 @@ getJasmineRequireObj().SuiteBuilder = function(j$) {
}; };
getJasmineRequireObj().Timer = function() { getJasmineRequireObj().Timer = function() {
/*
Timer isn't part of the public interface, but it is used by
jasmine-npm. -core and -npm version in lockstep at the major and minor
levels but version independently at the patch level. Any changes that
would break -npm should be done in a major or minor release, never a
patch release, and accompanied by a change to -npm that's released in
the same version.
*/
const defaultNow = (function(Date) { const defaultNow = (function(Date) {
return function() { return function() {
return new Date().getTime(); return new Date().getTime();
}; };
})(Date); })(Date);
/**
* @class Timer
* @classdesc Tracks elapsed time
* @example
* const timer = new jasmine.Timer();
* timer.start();
* const elapsed = timer.elapsed()
*/
function Timer(options) { function Timer(options) {
options = options || {}; options = options || {};
const now = options.now || defaultNow; const now = options.now || defaultNow;
let startTime; let startTime;
/**
* Starts the timer.
* @function
* @name Timer#start
*/
this.start = function() { this.start = function() {
startTime = now(); startTime = now();
}; };
/**
* Determines the time since the timer was started.
* @function
* @name Timer#elapsed
* @returns {number} Elapsed time in milliseconds, or NaN if the timer has not been started
*/
this.elapsed = function() { this.elapsed = function() {
return now() - startTime; return now() - startTime;
}; };
@@ -10610,5 +10713,5 @@ getJasmineRequireObj().UserContext = function(j$) {
}; };
getJasmineRequireObj().version = function() { getJasmineRequireObj().version = function() {
return '5.0.0-alpha.0'; return '5.0.0-alpha.1';
}; };
+7 -7
View File
@@ -1,7 +1,7 @@
{ {
"name": "jasmine-core", "name": "jasmine-core",
"license": "MIT", "license": "MIT",
"version": "5.0.0-alpha.0", "version": "5.0.0-alpha.1",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/jasmine/jasmine.git" "url": "https://github.com/jasmine/jasmine.git"
@@ -34,10 +34,10 @@
"package.json" "package.json"
], ],
"devDependencies": { "devDependencies": {
"eslint": "^7.32.0", "eslint": "^8.36.0",
"eslint-plugin-compat": ">=4.0.0 <4.1.0", "eslint-plugin-compat": "^4.0.0",
"glob": "^7.2.0", "glob": "^9.3.1",
"grunt": ">=1.0.4 <1.6.0", "grunt": "^1.0.4",
"grunt-cli": "^1.3.2", "grunt-cli": "^1.3.2",
"grunt-contrib-compress": "^2.0.0", "grunt-contrib-compress": "^2.0.0",
"grunt-contrib-concat": "^2.0.0", "grunt-contrib-concat": "^2.0.0",
@@ -45,10 +45,10 @@
"grunt-sass": "^3.0.2", "grunt-sass": "^3.0.2",
"jasmine": "github:jasmine/jasmine-npm#5.0", "jasmine": "github:jasmine/jasmine-npm#5.0",
"jasmine-browser-runner": "^1.0.0", "jasmine-browser-runner": "^1.0.0",
"jsdom": "^19.0.0", "jsdom": "^21.1.1",
"load-grunt-tasks": "^5.1.0", "load-grunt-tasks": "^5.1.0",
"prettier": "1.17.1", "prettier": "1.17.1",
"sass": "1.58.3", "sass": "^1.58.3",
"shelljs": "^0.8.3", "shelljs": "^0.8.3",
"temp": "^0.9.0" "temp": "^0.9.0"
}, },
+39
View File
@@ -0,0 +1,39 @@
# Jasmine Core 5.0.0-alpha.1 Release Notes
## Summary
This release provides improved support for parallel execution via the `jasmine`
package. Please see its release notes and the
[parallel documentation](https://jasmine.github.io/tutorials/running_specs_in_parallel)
for more information.
## New features and bug fixes
* Parallel: Cleaner interface for reporter dispatching
* When Env#config is called from spec and helper files in parallel mode, throw
an error rather than behaving unpredictably.
## Documentation improvements
* API reference docs for parallel support APIs that jasmine-npm uses
## Internal improvements
* Updated dev dependencies
## Supported environments
jasmine-core 5.0.0-alpha.1 has been tested in the following environments.
| Environment | Supported versions |
|-------------------|--------------------|
| Node | 16.14+, 18 |
| Safari | 15-16 |
| Chrome | 112 |
| Firefox | 102, 111 |
| Edge | 111 |
------
_Release Notes generated with _[Anchorman](http://github.com/infews/anchorman)_
+16 -2
View File
@@ -758,16 +758,16 @@ describe('Env', function() {
describe('In parallel mode', function() { describe('In parallel mode', function() {
it('rejects if random is set to false', async function() { it('rejects if random is set to false', async function() {
env.setParallelLoadingState('specs');
env.configure({ random: false }); env.configure({ random: false });
env.setParallelLoadingState('specs');
await expectAsync(env.execute()).toBeRejectedWithError( await expectAsync(env.execute()).toBeRejectedWithError(
'Randomization cannot be disabled in parallel mode' 'Randomization cannot be disabled in parallel mode'
); );
}); });
it('rejects if seed is set', async function() { it('rejects if seed is set', async function() {
env.setParallelLoadingState('specs');
env.configure({ seed: 1234 }); env.configure({ seed: 1234 });
env.setParallelLoadingState('specs');
await expectAsync(env.execute()).toBeRejectedWithError( await expectAsync(env.execute()).toBeRejectedWithError(
'Random seed cannot be set in parallel mode' 'Random seed cannot be set in parallel mode'
); );
@@ -817,4 +817,18 @@ describe('Env', function() {
}).toThrowError('Reporters cannot be removed via Env in parallel mode'); }).toThrowError('Reporters cannot be removed via Env in parallel mode');
}); });
}); });
describe('#configure', function() {
it('throws when called in parallel mode', function() {
env.setParallelLoadingState('helpers');
expect(function() {
env.configure({});
}).toThrowError('Jasmine cannot be configured via Env in parallel mode');
env.setParallelLoadingState('specs');
expect(function() {
env.configure({});
}).toThrowError('Jasmine cannot be configured via Env in parallel mode');
});
});
}); });
+176
View File
@@ -0,0 +1,176 @@
describe('ParallelReportDispatcher', function() {
it('dispatches the standard reporter events', async function() {
const subject = new jasmineUnderTest.ParallelReportDispatcher(() => {}, {
globalErrors: mockGlobalErrors()
});
const events = [
'jasmineStarted',
'jasmineDone',
'suiteStarted',
'suiteDone',
'specStarted',
'specDone'
];
const reporter = jasmine.createSpyObj('reporter', events);
subject.addReporter(reporter);
for (const eventName of events) {
const payload = { payloadFor: eventName };
await subject[eventName](payload);
expect(reporter[eventName]).toHaveBeenCalledWith(payload);
}
});
it('installs and uninstalls the global error handler', function() {
const globalErrors = mockGlobalErrors();
const subject = new jasmineUnderTest.ParallelReportDispatcher(() => {}, {
globalErrors
});
subject.installGlobalErrors();
expect(globalErrors.install).toHaveBeenCalled();
subject.uninstallGlobalErrors();
expect(globalErrors.uninstall).toHaveBeenCalled();
});
it('handles global errors from async reporters', async function() {
const globalErrors = mockGlobalErrors();
const onError = jasmine.createSpy('onError');
const subject = new jasmineUnderTest.ParallelReportDispatcher(onError, {
globalErrors
});
const reporter = jasmine.createSpyObj('reporter', [
'jasmineStarted',
'jasmineDone'
]);
let resolveStarted;
reporter.jasmineStarted.and.callFake(function() {
return new Promise(function(res) {
resolveStarted = res;
});
});
subject.addReporter(reporter);
const promise = subject.jasmineStarted({});
expect(globalErrors.pushListener).toHaveBeenCalled();
expect(globalErrors.popListener).not.toHaveBeenCalled();
const error = new Error('nope');
globalErrors.pushListener.calls.argsFor(0)[0](error);
expect(onError).toHaveBeenCalledWith(error);
resolveStarted();
await promise;
expect(globalErrors.popListener).toHaveBeenCalled();
});
it('handles done(error) from callback-style async reporters', function() {
const globalErrors = mockGlobalErrors();
const onError = jasmine.createSpy('onError');
const subject = new jasmineUnderTest.ParallelReportDispatcher(onError, {
globalErrors
});
const reporter = jasmine.createSpyObj('reporter', [
'jasmineStarted',
'jasmineDone'
]);
let callback;
reporter.jasmineStarted = function(event, cb) {
callback = cb;
};
subject.addReporter(reporter);
subject.jasmineStarted({});
expect(callback).toBeInstanceOf(Function);
const error = new Error('nope');
callback(error);
expect(onError).toHaveBeenCalledWith(error);
});
it('handles done.fail() from callback-style async reporters', function() {
const globalErrors = mockGlobalErrors();
const onError = jasmine.createSpy('onError');
const subject = new jasmineUnderTest.ParallelReportDispatcher(onError, {
globalErrors
});
const reporter = jasmine.createSpyObj('reporter', [
'jasmineStarted',
'jasmineDone'
]);
let callback;
reporter.jasmineStarted = function(event, cb) {
callback = cb;
};
subject.addReporter(reporter);
subject.jasmineStarted({});
expect(callback).toBeInstanceOf(Function);
const error = new Error('nope');
callback.fail(error);
expect(onError).toHaveBeenCalledWith(error);
onError.calls.reset();
callback.fail();
expect(onError).toHaveBeenCalledWith(
new Error('A reporter called done.fail()')
);
});
it('handles errors due to mixed async style in reporters', async function() {
const globalErrors = mockGlobalErrors();
const onError = jasmine.createSpy('onError');
const subject = new jasmineUnderTest.ParallelReportDispatcher(onError, {
globalErrors
});
subject.addReporter({
async jasmineStarted(event, done) {
done();
}
});
await subject.jasmineStarted({});
expect(onError).toHaveBeenCalledWith(
new Error(
'An asynchronous before/it/after function took a done callback but also returned a promise. Either remove the done callback (recommended) or change the function to not return a promise.'
)
);
});
it('handles errors due to multiple done calls in reporters', async function() {
const globalErrors = mockGlobalErrors();
const onError = jasmine.createSpy('onError');
const subject = new jasmineUnderTest.ParallelReportDispatcher(onError, {
globalErrors
});
subject.addReporter({
jasmineStarted(event, done) {
done();
done();
}
});
await subject.jasmineStarted({});
expect(onError).toHaveBeenCalledWith(
new Error(
"An asynchronous reporter callback called its 'done' callback more than once."
)
);
});
function mockGlobalErrors() {
const globalErrors = jasmine.createSpyObj('globalErrors', [
'install',
'pushListener',
'popListener'
]);
globalErrors.install.and.callFake(function() {
globalErrors.uninstall = jasmine.createSpy('globalErrors.uninstall');
});
return globalErrors;
}
});
+13 -9
View File
@@ -595,11 +595,13 @@ describe('QueueRunner', function() {
queueRunner.execute(); queueRunner.execute();
expect(onException).toHaveBeenCalledWith( expect(onException).toHaveBeenCalledWith(
'An asynchronous ' + new Error(
'before/it/after function took a done callback but also returned a ' + 'An asynchronous ' +
'promise. ' + 'before/it/after function took a done callback but also returned a ' +
'Either remove the done callback (recommended) or change the function ' + 'promise. ' +
'to not return a promise.' 'Either remove the done callback (recommended) or change the function ' +
'to not return a promise.'
)
); );
}); });
@@ -615,10 +617,12 @@ describe('QueueRunner', function() {
queueRunner.execute(); queueRunner.execute();
expect(onException).toHaveBeenCalledWith( expect(onException).toHaveBeenCalledWith(
'An asynchronous ' + new Error(
'before/it/after function was defined with the async keyword but ' + 'An asynchronous ' +
'also took a done callback. Either remove the done callback ' + 'before/it/after function was defined with the async keyword but ' +
'(recommended) or remove the async keyword.' 'also took a done callback. Either remove the done callback ' +
'(recommended) or remove the async keyword.'
)
); );
}); });
}); });
+7 -67
View File
@@ -174,6 +174,12 @@ getJasmineRequireObj().Env = function(j$) {
* @function * @function
*/ */
this.configure = function(configuration) { this.configure = function(configuration) {
if (parallelLoadingState) {
throw new Error(
'Jasmine cannot be configured via Env in parallel mode'
);
}
const booleanProps = [ const booleanProps = [
'random', 'random',
'failSpecWithNoExpectations', 'failSpecWithNoExpectations',
@@ -414,72 +420,7 @@ getJasmineRequireObj().Env = function(j$) {
* @see custom_reporter * @see custom_reporter
*/ */
reporter = new j$.ReportDispatcher( reporter = new j$.ReportDispatcher(
[ j$.reporterEvents,
/**
* `jasmineStarted` is called after all of the specs have been loaded, but just before execution starts.
* @function
* @name Reporter#jasmineStarted
* @param {JasmineStartedInfo} suiteInfo Information about the full Jasmine suite that is being run
* @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on.
* @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion.
* @see async
*/
'jasmineStarted',
/**
* When the entire suite has finished execution `jasmineDone` is called
* @function
* @name Reporter#jasmineDone
* @param {JasmineDoneInfo} suiteInfo Information about the full Jasmine suite that just finished running.
* @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on.
* @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion.
* @see async
*/
'jasmineDone',
/**
* `suiteStarted` is invoked when a `describe` starts to run
* @function
* @name Reporter#suiteStarted
* @param {SuiteResult} result Information about the individual {@link describe} being run
* @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on.
* @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion.
* @see async
*/
'suiteStarted',
/**
* `suiteDone` is invoked when all of the child specs and suites for a given suite have been run
*
* While jasmine doesn't require any specific functions, not defining a `suiteDone` will make it impossible for a reporter to know when a suite has failures in an `afterAll`.
* @function
* @name Reporter#suiteDone
* @param {SuiteResult} result
* @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on.
* @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion.
* @see async
*/
'suiteDone',
/**
* `specStarted` is invoked when an `it` starts to run (including associated `beforeEach` functions)
* @function
* @name Reporter#specStarted
* @param {SpecResult} result Information about the individual {@link it} being run
* @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on.
* @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion.
* @see async
*/
'specStarted',
/**
* `specDone` is invoked when an `it` and its associated `beforeEach` and `afterEach` functions have been run.
*
* While jasmine doesn't require any specific functions, not defining a `specDone` will make it impossible for a reporter to know when a spec has failed.
* @function
* @name Reporter#specDone
* @param {SpecResult} result
* @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on.
* @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion.
* @see async
*/
'specDone'
],
function(options) { function(options) {
options.SkipPolicy = j$.NeverSkipPolicy; options.SkipPolicy = j$.NeverSkipPolicy;
return queueRunnerFactory(options); return queueRunnerFactory(options);
@@ -503,7 +444,6 @@ getJasmineRequireObj().Env = function(j$) {
}; };
this.parallelReset = function() { this.parallelReset = function() {
// TODO: ensure that autoCleanClosures was false
suiteBuilder.parallelReset(); suiteBuilder.parallelReset();
runner.parallelReset(); runner.parallelReset();
}; };
+93
View File
@@ -0,0 +1,93 @@
getJasmineRequireObj().ParallelReportDispatcher = function(j$) {
/**
* @class ParallelReportDispatcher
* @implements Reporter
* @classdesc A report dispatcher packaged for convenient use from outside jasmine-core.
*
* This is intended to help packages like `jasmine` (the Jasmine runner for
* Node.js) do their own report dispatching in order to support parallel
* execution. If you aren't implementing a runner package that supports
* parallel execution, this class probably isn't what you're looking for.
*
* Warning: Do not use ParallelReportDispatcher in the same process that
* Jasmine specs run in. Doing so will break Jasmine's error handling.
* @param onError {function} Function called when an unhandled exception, unhandled promise rejection, or explicit reporter failure occurs
*/
function ParallelReportDispatcher(onError, deps = {}) {
const ReportDispatcher = deps.ReportDispatcher || j$.ReportDispatcher;
const QueueRunner = deps.QueueRunner || j$.QueueRunner;
const globalErrors = deps.globalErrors || new j$.GlobalErrors();
const dispatcher = ReportDispatcher(
j$.reporterEvents,
function(queueRunnerOptions) {
queueRunnerOptions = {
...queueRunnerOptions,
globalErrors,
timeout: { setTimeout, clearTimeout },
fail: function(error) {
// A callback-style async reporter called either done.fail()
// or done(anError).
if (!error) {
error = new Error('A reporter called done.fail()');
}
onError(error);
},
onException: function(error) {
// A reporter method threw an exception or returned a rejected
// promise, or there was an unhandled exception or unhandled promise
// rejection while an asynchronous reporter method was running.
onError(error);
}
};
new QueueRunner(queueRunnerOptions).execute();
},
function(error) {
// A reporter called done() more than once.
onError(error);
}
);
const self = {
/**
* Adds a reporter to the list of reporters that events will be dispatched to.
* @function
* @name ParallelReportDispatcher#addReporter
* @param {Reporter} reporterToAdd The reporter to be added.
* @see custom_reporter
*/
addReporter: dispatcher.addReporter.bind(dispatcher),
/**
* Clears all registered reporters.
* @function
* @name ParallelReportDispatcher#clearReporters
*/
clearReporters: dispatcher.clearReporters.bind(dispatcher),
/**
* Installs a global error handler. After this method is called, any
* unhandled exceptions or unhandled promise rejections will be passed to
* the onError callback that was passed to the constructor.
* @function
* @name ParallelReportDispatcher#installGlobalErrors
*/
installGlobalErrors: globalErrors.install.bind(globalErrors),
/**
* Uninstalls the global error handler.
* @function
* @name ParallelReportDispatcher#uninstallGlobalErrors
*/
uninstallGlobalErrors: function() {
// late-bind uninstall because it doesn't exist until install is called
globalErrors.uninstall(globalErrors);
}
};
for (const eventName of j$.reporterEvents) {
self[eventName] = dispatcher[eventName].bind(dispatcher);
}
return self;
}
return ParallelReportDispatcher;
};
+12 -17
View File
@@ -1,13 +1,4 @@
getJasmineRequireObj().QueueRunner = function(j$) { getJasmineRequireObj().QueueRunner = function(j$) {
/*
QueueRunner isn't part of the public interface, but it is used by
jasmine-npm. -core and -npm version in lockstep at the major and minor
levels but version independently at the patch level. Any changes that
would break -npm should be done in a major or minor release, never a
patch release, and accompanied by a change to -npm that's released in
the same version.
*/
let nextid = 1; let nextid = 1;
function StopExecutionError() {} function StopExecutionError() {}
@@ -264,17 +255,21 @@ getJasmineRequireObj().QueueRunner = function(j$) {
// on the stack at this point. // on the stack at this point.
if (j$.isAsyncFunction_(fn)) { if (j$.isAsyncFunction_(fn)) {
this.onException( this.onException(
'An asynchronous before/it/after ' + new Error(
'function was defined with the async keyword but also took a ' + 'An asynchronous before/it/after ' +
'done callback. Either remove the done callback (recommended) or ' + 'function was defined with the async keyword but also took a ' +
'remove the async keyword.' 'done callback. Either remove the done callback (recommended) or ' +
'remove the async keyword.'
)
); );
} else { } else {
this.onException( this.onException(
'An asynchronous before/it/after ' + new Error(
'function took a done callback but also returned a promise. ' + 'An asynchronous before/it/after ' +
'Either remove the done callback (recommended) or change the ' + 'function took a done callback but also returned a promise. ' +
'function to not return a promise.' 'Either remove the done callback (recommended) or change the ' +
'function to not return a promise.'
)
); );
} }
} }
-9
View File
@@ -1,13 +1,4 @@
getJasmineRequireObj().ReportDispatcher = function(j$) { getJasmineRequireObj().ReportDispatcher = function(j$) {
/*
ReportDispatcher isn't part of the public interface, but it is used by
jasmine-npm. -core and -npm version in lockstep at the major and minor
levels but version independently at the patch level. Any changes that
would break -npm should be done in a major or minor release, never a
patch release, and accompanied by a change to -npm that's released in
the same version.
*/
function ReportDispatcher(methods, queueRunnerFactory, onLateError) { function ReportDispatcher(methods, queueRunnerFactory, onLateError) {
const dispatchedMethods = methods || []; const dispatchedMethods = methods || [];
+19 -9
View File
@@ -1,29 +1,39 @@
getJasmineRequireObj().Timer = function() { getJasmineRequireObj().Timer = function() {
/*
Timer isn't part of the public interface, but it is used by
jasmine-npm. -core and -npm version in lockstep at the major and minor
levels but version independently at the patch level. Any changes that
would break -npm should be done in a major or minor release, never a
patch release, and accompanied by a change to -npm that's released in
the same version.
*/
const defaultNow = (function(Date) { const defaultNow = (function(Date) {
return function() { return function() {
return new Date().getTime(); return new Date().getTime();
}; };
})(Date); })(Date);
/**
* @class Timer
* @classdesc Tracks elapsed time
* @example
* const timer = new jasmine.Timer();
* timer.start();
* const elapsed = timer.elapsed()
*/
function Timer(options) { function Timer(options) {
options = options || {}; options = options || {};
const now = options.now || defaultNow; const now = options.now || defaultNow;
let startTime; let startTime;
/**
* Starts the timer.
* @function
* @name Timer#start
*/
this.start = function() { this.start = function() {
startTime = now(); startTime = now();
}; };
/**
* Determines the time since the timer was started.
* @function
* @name Timer#elapsed
* @returns {number} Elapsed time in milliseconds, or NaN if the timer has not been started
*/
this.elapsed = function() { this.elapsed = function() {
return now() - startTime; return now() - startTime;
}; };
+70
View File
@@ -0,0 +1,70 @@
getJasmineRequireObj().reporterEvents = function() {
const events = [
/**
* `jasmineStarted` is called after all of the specs have been loaded, but just before execution starts.
* @function
* @name Reporter#jasmineStarted
* @param {JasmineStartedInfo} suiteInfo Information about the full Jasmine suite that is being run
* @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on.
* @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion.
* @see async
*/
'jasmineStarted',
/**
* When the entire suite has finished execution `jasmineDone` is called
* @function
* @name Reporter#jasmineDone
* @param {JasmineDoneInfo} suiteInfo Information about the full Jasmine suite that just finished running.
* @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on.
* @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion.
* @see async
*/
'jasmineDone',
/**
* `suiteStarted` is invoked when a `describe` starts to run
* @function
* @name Reporter#suiteStarted
* @param {SuiteResult} result Information about the individual {@link describe} being run
* @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on.
* @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion.
* @see async
*/
'suiteStarted',
/**
* `suiteDone` is invoked when all of the child specs and suites for a given suite have been run
*
* While jasmine doesn't require any specific functions, not defining a `suiteDone` will make it impossible for a reporter to know when a suite has failures in an `afterAll`.
* @function
* @name Reporter#suiteDone
* @param {SuiteResult} result
* @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on.
* @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion.
* @see async
*/
'suiteDone',
/**
* `specStarted` is invoked when an `it` starts to run (including associated `beforeEach` functions)
* @function
* @name Reporter#specStarted
* @param {SpecResult} result Information about the individual {@link it} being run
* @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on.
* @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion.
* @see async
*/
'specStarted',
/**
* `specDone` is invoked when an `it` and its associated `beforeEach` and `afterEach` functions have been run.
*
* While jasmine doesn't require any specific functions, not defining a `specDone` will make it impossible for a reporter to know when a spec has failed.
* @function
* @name Reporter#specDone
* @param {SpecResult} result
* @param {Function} [done] Used to specify to Jasmine that this callback is asynchronous and Jasmine should wait until it has been called before moving on.
* @returns {} Optionally return a Promise instead of using `done` to cause Jasmine to wait for completion.
* @see async
*/
'specDone'
];
Object.freeze(events);
return events;
};
+2
View File
@@ -67,7 +67,9 @@ var getJasmineRequireObj = (function(jasmineGlobal) {
j$.CompleteOnFirstErrorSkipPolicy = jRequire.CompleteOnFirstErrorSkipPolicy( j$.CompleteOnFirstErrorSkipPolicy = jRequire.CompleteOnFirstErrorSkipPolicy(
j$ j$
); );
j$.reporterEvents = jRequire.reporterEvents(j$);
j$.ReportDispatcher = jRequire.ReportDispatcher(j$); j$.ReportDispatcher = jRequire.ReportDispatcher(j$);
j$.ParallelReportDispatcher = jRequire.ParallelReportDispatcher(j$);
j$.RunableResources = jRequire.RunableResources(j$); j$.RunableResources = jRequire.RunableResources(j$);
j$.Runner = jRequire.Runner(j$); j$.Runner = jRequire.Runner(j$);
j$.Spec = jRequire.Spec(j$); j$.Spec = jRequire.Spec(j$);