Compare commits

...

27 Commits

Author SHA1 Message Date
Steve Gravrock
788eba34b6 Bump version to 6.0.0-alpha.2
Some checks failed
Test in latest available Safari / build (push) Has been cancelled
2025-11-15 10:22:27 -08:00
Steve Gravrock
1f31b4b0f6 Increase font size in HTML reporters 2025-11-15 09:06:19 -08:00
Steve Gravrock
00a8a11904 Merge branch 'slow-reporter' into 6.0 2025-11-15 09:01:44 -08:00
Steve Gravrock
3899d83fb6 HtmlReporterV2: show median and mean spec run time 2025-11-15 09:01:06 -08:00
Steve Gravrock
8f13684a01 Add a slowest specs list to HTMLReporterV2 2025-11-15 08:57:25 -08:00
Steve Gravrock
bdf63f2402 Remove code to support browsers that don't have MessageChannel
Jasmine hasn't actually run in any such browsers since 2.x.
2025-11-12 21:59:17 -08:00
Steve Gravrock
9c2ffae2f9 Add experimental safariYieldStrategy: "time" config option
This greatly improves speed, at least in jasmine-core's own tests.
2025-11-12 21:08:59 -08:00
Steve Gravrock
7b2807b321 Convert clearStack from a function to an object 2025-11-11 18:54:25 -08:00
Steve Gravrock
e930622548 Merge branch 'main' into 6.0 2025-11-11 09:14:14 -08:00
Steve Gravrock
56e2832ebe Add manual and cron triggers to Safari build 2025-11-11 09:13:34 -08:00
Steve Gravrock
d31d33aeb3 Introduce a tab bar
This will make it easier to add a third tab to HtmlReporterV2.
2025-11-09 09:58:57 -08:00
Steve Gravrock
e4c69e960e Add 'use strict' to AllOf.js 2025-11-04 05:52:45 -08:00
Steve Gravrock
a8431f33bd Merge branch '5.99' into 6.0 2025-11-03 17:22:11 -08:00
Steve Gravrock
4995c967ac Merge branch 'main' into 5.99 2025-11-03 17:14:24 -08:00
Steve Gravrock
9a9d3994da Add Safari 26 to supported browsers 2025-11-03 07:37:11 -08:00
Steve Gravrock
ff9feb29d3 Configurable spec/suite filename detection
* Adds extraItStackFrames and extraDescribeStackFrames config properties.
* Un-deprecates the filename properties of reporter events.
* Fixes #2065.
2025-11-01 14:17:01 -07:00
Steve Gravrock
fee7e6e64e Merge branch 'jonahd-g-main'
* Adds jasmine.allOf asymmetric equality tester
* Merges #2087 from @jonahd-g
* Fixes #2083
2025-11-01 09:01:53 -07:00
Steve Gravrock
18d4d38655 Fix version number in 5.12.1 release notes 2025-10-29 19:55:00 -07:00
Steve Gravrock
53e9bc68d2 Bump version to 5.12.1 2025-10-29 19:53:34 -07:00
Steve Gravrock
2be50e1b87 Merge branch 'bonkevin-fix-custom-matcher'
* Fixes custom matchers in top-level specs
* Merges #2088 from @bonkevin
2025-10-29 19:44:06 -07:00
bonkevin
27a1257b6d fix: unavailable custom matchers on top-it 2025-10-29 13:04:10 -04:00
Jonah Bron
75658e0566 jasmine.allOf AsymmetricEqualityTester
New asymmetric equality tester that accepts a variable number of arguments, and will pass if all of them evaluate as being equal to the input value.
Includes unit tests
2025-10-27 10:10:16 -07:00
Steve Gravrock
85322d1877 Re-add support for partial spec name filtering
No UI for this but users can construct URLs manually.
Fixes #2085.
2025-10-24 17:26:49 -07:00
Steve Gravrock
6667a42301 Docs: Fix HtmlReporterV2 ctor example 2025-10-22 16:36:58 -07:00
Steve Gravrock
020dffd504 Don't spy on getGlobal 2025-10-19 10:08:05 -07:00
Steve Gravrock
4201fd848f Require spec/suite property keys to be strings, not just anything that's cloneable and serializable
This matches the jsdoc.
2025-10-19 09:15:12 -07:00
Steve Gravrock
9a67c4e24d Copy 6.0.0-alpha.1 release notes from branch 2025-10-18 13:05:29 -07:00
50 changed files with 2317 additions and 727 deletions

View File

@@ -1,5 +1,5 @@
# Run tests against supported Node versions, and (except for pull requests)
# against supported browsers.
# against supported browsers that are available on Saucelabs.
version: 2.1
@@ -89,7 +89,7 @@ jobs:
export SAUCE_TUNNEL_NAME=$CIRCLE_WORKFLOW_JOB_ID
scripts/start-sauce-connect
set +o errexit
scripts/run-all-browsers
scripts/run-sauce-browsers
exitcode=$?
set -o errexit
scripts/stop-sauce-connect

23
.github/workflows/safari.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: Test in latest available Safari
on:
push:
pull_request:
workflow_dispatch:
schedule:
- cron: '0 0 * * *'
jobs:
build:
runs-on: macos-latest
steps:
- name: Report Safari version
run: osascript -e 'get version of application "Safari"'
- uses: actions/checkout@v4
- name: Use Node.js 22.x
uses: actions/setup-node@v4
with:
node-version: 22.x
- run: npm install
- run: npm run build
- run: JASMINE_BROWSER=safari npm run ci

View File

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

View File

@@ -28,9 +28,18 @@ should also rev to that version.
When ready to release - specs are all green and the stories are done:
1. Update the release notes in `release_notes` - use the Anchorman gem to generate the markdown file and edit accordingly. Include a list of supported environments.
1. Update the version in `package.json`
1. Run `npm run build`.
1. Update the release notes in `release_notes` - use the Anchorman gem to
generate the Markdown file and edit accordingly. Include a list of supported
environments. Get that information from these places:
* For Node, see .circleci/config.yml or the README.
* For Firefox ESR and Safari <=17, see scripts/run-sauce-browsers or the README.
* For evergreen browsers, trigger a Circle CI run and check the
[Saucelabs dashboard](https://app.saucelabs.com/dashboard/tests?ownerId=90a771d55857492da3bd5251a2d92457&ownerType=user&ownerName=jasmine-js&start=last7days)
once it's finished.
* For Safari >17, trigger the [Safari action](https://github.com/jasmine/jasmine/actions/workflows/safari.yml)
and get the version from the output.
2. Update the version in `package.json`
3. Run `npm run build`.
### Commit and push core changes

View File

@@ -58,6 +58,12 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
env.addReporter(jsApiReporter);
env.addReporter(htmlReporter);
/**
* Configures Jasmine based on the current set of query parameters. This
* supports all parameters set by the HTML reporter as well as
* spec=partialPath, which filters out specs whose paths don't contain the
* parameter.
*/
env.configure(urls.configFromCurrentUrl());
/**

View File

@@ -35,6 +35,8 @@ jasmineRequire.html = function(j$) {
j$.private.SymbolsView = jasmineRequire.SymbolsView(j$);
j$.private.SummaryTreeView = jasmineRequire.SummaryTreeView(j$);
j$.private.FailuresView = jasmineRequire.FailuresView(j$);
j$.private.PerformanceView = jasmineRequire.PerformanceView(j$);
j$.private.TabBar = jasmineRequire.TabBar(j$);
j$.HtmlReporter = jasmineRequire.HtmlReporter(j$);
j$.HtmlReporterV2Urls = jasmineRequire.HtmlReporterV2Urls(j$);
j$.HtmlReporterV2 = jasmineRequire.HtmlReporterV2(j$);
@@ -187,16 +189,52 @@ jasmineRequire.HtmlReporter = function(j$) {
results.appendChild(summary.rootEl);
if (this.#stateBuilder.anyNonTopSuiteFailures) {
this.#alerts.addFailureToggle(
() => this.#setMenuModeTo('jasmine-failure-list'),
() => this.#setMenuModeTo('jasmine-spec-list')
);
this.#addFailureToggle();
this.#setMenuModeTo('jasmine-failure-list');
this.#failures.show();
}
}
#addFailureToggle() {
const onClickFailures = () => this.#setMenuModeTo('jasmine-failure-list');
const onClickSpecList = () => this.#setMenuModeTo('jasmine-spec-list');
const failuresLink = createDom(
'a',
{ className: 'jasmine-failures-menu', href: '#' },
'Failures'
);
let specListLink = createDom(
'a',
{ className: 'jasmine-spec-list-menu', href: '#' },
'Spec List'
);
failuresLink.onclick = function() {
onClickFailures();
return false;
};
specListLink.onclick = function() {
onClickSpecList();
return false;
};
this.#alerts.addBar(
createDom(
'span',
{ className: 'jasmine-menu jasmine-bar jasmine-spec-list' },
[createDom('span', {}, 'Spec List | '), failuresLink]
)
);
this.#alerts.addBar(
createDom(
'span',
{ className: 'jasmine-menu jasmine-bar jasmine-failure-list' },
[specListLink, createDom('span', {}, ' | Failures ')]
)
);
}
#find(selector) {
return this.#getContainer().querySelector(
'.jasmine_html-reporter ' + selector
@@ -434,6 +472,7 @@ jasmineRequire.AlertsView = function(j$) {
);
}
// TODO: remove this once HtmlReporterV2 doesn't use it
addFailureToggle(onClickFailures, onClickSpecList) {
const failuresLink = createDom(
'a',
@@ -456,14 +495,20 @@ jasmineRequire.AlertsView = function(j$) {
return false;
};
this.#createAndAdd('jasmine-menu jasmine-bar jasmine-spec-list', [
createDom('span', {}, 'Spec List | '),
failuresLink
]);
this.#createAndAdd('jasmine-menu jasmine-bar jasmine-failure-list', [
specListLink,
createDom('span', {}, ' | Failures ')
]);
this.rootEl.appendChild(
createDom(
'span',
{ className: 'jasmine-menu jasmine-bar jasmine-spec-list' },
[createDom('span', {}, 'Spec List | '), failuresLink]
)
);
this.rootEl.appendChild(
createDom(
'span',
{ className: 'jasmine-menu jasmine-bar jasmine-failure-list' },
[specListLink, createDom('span', {}, ' | Failures ')]
)
);
}
addGlobalFailure(failure) {
@@ -946,6 +991,10 @@ jasmineRequire.HtmlReporterV2 = function(j$) {
const { createDom, noExpectations } = j$.private.htmlReporterUtils;
const specListTabId = 'jasmine-specListTab';
const failuresTabId = 'jasmine-failuresTab';
const perfTabId = 'jasmine-perfTab';
/**
* @class HtmlReporterV2
* @classdesc Displays results and allows re-running individual specs and suites.
@@ -958,7 +1007,7 @@ jasmineRequire.HtmlReporterV2 = function(j$) {
* const reporter = new jasmine.HtmlReporterV2({
* env,
* urls,
* container: document.body
* getContainer: () => document.body
* });
*/
class HtmlReporterV2 {
@@ -974,6 +1023,7 @@ jasmineRequire.HtmlReporterV2 = function(j$) {
// Sub-views
#alerts;
#statusBar;
#tabBar;
#progress;
#banner;
#failures;
@@ -1011,6 +1061,25 @@ jasmineRequire.HtmlReporterV2 = function(j$) {
this.#statusBar = new j$.private.OverallStatusBar(this.#urlBuilder);
this.#statusBar.showRunning();
this.#alerts.addBar(this.#statusBar.rootEl);
this.#tabBar = new j$.private.TabBar(
[
{ id: specListTabId, label: 'Spec List' },
{ id: failuresTabId, label: 'Failures' },
{ id: perfTabId, label: 'Performance' }
],
tabId => {
if (tabId === specListTabId) {
this.#setMenuModeTo('jasmine-spec-list');
} else if (tabId === failuresTabId) {
this.#setMenuModeTo('jasmine-failure-list');
} else {
this.#setMenuModeTo('jasmine-performance');
}
}
);
this.#alerts.addBar(this.#tabBar.rootEl);
this.#progress = new ProgressView();
this.#banner = new j$.private.Banner(
this.#queryString.navigateWithNewParam.bind(this.#queryString),
@@ -1101,15 +1170,17 @@ jasmineRequire.HtmlReporterV2 = function(j$) {
);
summary.addResults(this.#stateBuilder.topResults);
results.appendChild(summary.rootEl);
const perf = new j$.private.PerformanceView();
perf.addResults(this.#stateBuilder.topResults);
results.appendChild(perf.rootEl);
this.#tabBar.showTab(specListTabId);
this.#tabBar.showTab(perfTabId);
if (this.#stateBuilder.anyNonTopSuiteFailures) {
this.#alerts.addFailureToggle(
() => this.#setMenuModeTo('jasmine-failure-list'),
() => this.#setMenuModeTo('jasmine-spec-list')
);
this.#setMenuModeTo('jasmine-failure-list');
this.#failures.show();
this.#tabBar.showTab(failuresTabId);
this.#tabBar.selectTab(failuresTabId);
} else {
this.#tabBar.selectTab(specListTabId);
}
}
@@ -1148,7 +1219,6 @@ jasmineRequire.HtmlReporterV2 = function(j$) {
this.rootEl.value = this.rootEl.value + 1;
if (result.status === 'failed') {
// TODO: also a non-color indicator
this.rootEl.classList.add('failed');
}
}
@@ -1233,7 +1303,10 @@ jasmineRequire.HtmlReporterV2Urls = function(j$) {
}
/**
* Creates a {@link Configuration} from the current page's URL.
* Creates a {@link Configuration} from the current page's URL. Supported
* query string parameters include all those set by {@link HtmlReporterV2}
* as well as spec=partialPath, which filters out specs whose paths don't
* contain partialPath.
* @returns {Configuration}
* @example
* const urls = new jasmine.HtmlReporterV2Urls();
@@ -1259,9 +1332,10 @@ jasmineRequire.HtmlReporterV2Urls = function(j$) {
}
const specFilter = new j$.private.HtmlSpecFilterV2({
filterString: () => {
return this.queryString.getParam('path');
}
filterParams: () => ({
path: this.queryString.getParam('path'),
spec: this.queryString.getParam('spec')
})
});
config.specFilter = function(spec) {
@@ -1281,35 +1355,35 @@ jasmineRequire.HtmlReporterV2Urls = function(j$) {
jasmineRequire.HtmlSpecFilterV2 = function() {
class HtmlSpecFilterV2 {
#getFilterString;
#getFilterParams;
constructor(options) {
this.#getFilterString = options.filterString;
this.#getFilterParams = options.filterParams;
}
/**
* Determines whether the spec with the specified name should be executed.
* @name HtmlSpecFilterV2#matches
* @function
* @param {Spec} spec
* @returns {boolean}
*/
matches(spec) {
const filterString = this.#getFilterString();
const params = this.#getFilterParams();
if (!filterString) {
return true;
if (params.path) {
return this.#matchesPath(spec, JSON.parse(params.path));
} else if (params.spec) {
// Like legacy HtmlSpecFilter, retained because it's convenient for
// hand-constructing filter URLs
return spec.getFullName().includes(params.spec);
}
const filterPath = JSON.parse(this.#getFilterString());
return true;
}
#matchesPath(spec, path) {
const specPath = spec.getPath();
if (filterPath.length > specPath.length) {
if (path.length > specPath.length) {
return false;
}
for (let i = 0; i < filterPath.length; i++) {
if (specPath[i] !== filterPath[i]) {
for (let i = 0; i < path.length; i++) {
if (specPath[i] !== path[i]) {
return false;
}
}
@@ -1430,6 +1504,105 @@ jasmineRequire.OverallStatusBar = function(j$) {
return OverallStatusBar;
};
jasmineRequire.PerformanceView = function(j$) {
const createDom = j$.private.htmlReporterUtils.createDom;
const MAX_SLOW_SPECS = 20;
class PerformanceView {
#summary;
#tbody;
constructor() {
this.#tbody = document.createElement('tbody');
this.#summary = document.createElement('div');
this.rootEl = createDom(
'div',
{ className: 'jasmine-performance-view' },
createDom('h2', {}, 'Performance'),
this.#summary,
createDom('h3', {}, 'Slowest Specs'),
createDom(
'table',
{},
createDom(
'thead',
{},
createDom(
'tr',
{},
createDom('th', {}, 'Duration'),
createDom('th', {}, 'Spec Name')
)
),
this.#tbody
)
);
}
addResults(resultsTree) {
const specResults = [];
getSpecResults(resultsTree, specResults);
if (specResults.length === 0) {
return;
}
specResults.sort(function(a, b) {
if (a.duration < b.duration) {
return 1;
} else if (a.duration > b.duration) {
return -1;
} else {
return 0;
}
});
this.#populateSumary(specResults);
this.#populateTable(specResults);
}
#populateSumary(specResults) {
const total = specResults.map(r => r.duration).reduce((a, b) => a + b, 0);
const mean = total / specResults.length;
const median = specResults[Math.floor(specResults.length / 2)].duration;
this.#summary.appendChild(
document.createTextNode(`Mean spec run time: ${mean.toFixed(0)}ms`)
);
this.#summary.appendChild(document.createElement('br'));
this.#summary.appendChild(
document.createTextNode(`Median spec run time: ${median}ms`)
);
}
#populateTable(specResults) {
specResults = specResults.slice(0, MAX_SLOW_SPECS);
for (const r of specResults) {
this.#tbody.appendChild(
createDom(
'tr',
{},
createDom('td', {}, `${r.duration}ms`),
createDom('td', {}, r.fullName)
)
);
}
}
}
function getSpecResults(resultsTree, dest) {
for (const node of resultsTree.children) {
if (node.type === 'suite') {
getSpecResults(node, dest);
} else if (node.result.status !== 'excluded') {
dest.push(node.result);
}
}
}
return PerformanceView;
};
jasmineRequire.ResultsStateBuilder = function(j$) {
'use strict';
@@ -1661,3 +1834,81 @@ jasmineRequire.SymbolsView = function(j$) {
return SymbolsView;
};
jasmineRequire.TabBar = function(j$) {
const createDom = j$.private.htmlReporterUtils.createDom;
class TabBar {
#tabs;
#onSelectTab;
// tabSpecs should be an array of {id, label}.
// All tabs are initially not visible and not selected.
constructor(tabSpecs, onSelectTab) {
this.#onSelectTab = onSelectTab;
this.#tabs = [];
this.#tabs = tabSpecs.map(ts => new Tab(ts, () => this.selectTab(ts.id)));
this.rootEl = createDom(
'span',
{ className: 'jasmine-menu jasmine-bar' },
this.#tabs.map(t => t.rootEl)
);
}
showTab(id) {
for (const tab of this.#tabs) {
if (tab.rootEl.id === id) {
tab.setVisibility(true);
}
}
}
selectTab(id) {
for (const tab of this.#tabs) {
tab.setSelected(tab.rootEl.id === id);
}
this.#onSelectTab(id);
}
}
class Tab {
#spec;
#onClick;
constructor(spec, onClick) {
this.#spec = spec;
this.#onClick = onClick;
this.rootEl = createDom(
'span',
{ id: spec.id, className: 'jasmine-tab jasmine-hidden' },
this.#createLink()
);
}
setVisibility(visible) {
this.rootEl.classList.toggle('jasmine-hidden', !visible);
}
setSelected(selected) {
if (selected) {
this.rootEl.textContent = this.#spec.label;
} else {
this.rootEl.textContent = '';
this.rootEl.appendChild(this.#createLink());
}
}
#createLink() {
const link = createDom('a', { href: '#' }, this.#spec.label);
link.addEventListener('click', e => {
e.preventDefault();
this.#onClick();
});
return link;
}
}
return TabBar;
};

View File

@@ -8,7 +8,7 @@ body {
background-color: #eee;
padding: 5px;
margin: -8px;
font-size: 11px;
font-size: 12px;
font-family: Monaco, "Lucida Console", monospace;
line-height: 14px;
color: #333;
@@ -63,7 +63,7 @@ body {
float: right;
line-height: 28px;
padding-right: 9px;
font-size: 11px;
font-size: 12px;
}
.jasmine_html-reporter .jasmine-symbol-summary {
overflow: hidden;
@@ -198,11 +198,17 @@ body {
color: white;
}
.jasmine_html-reporter.jasmine-spec-list .jasmine-bar.jasmine-menu.jasmine-failure-list,
.jasmine_html-reporter.jasmine-spec-list .jasmine-results .jasmine-failures {
.jasmine_html-reporter.jasmine-spec-list .jasmine-results .jasmine-failures,
.jasmine_html-reporter.jasmine-spec-list .jasmine-performance-view {
display: none;
}
.jasmine_html-reporter.jasmine-failure-list .jasmine-bar.jasmine-menu.jasmine-spec-list,
.jasmine_html-reporter.jasmine-failure-list .jasmine-summary {
.jasmine_html-reporter.jasmine-failure-list .jasmine-summary,
.jasmine_html-reporter.jasmine-failure-list .jasmine-performance-view {
display: none;
}
.jasmine_html-reporter.jasmine-performance .jasmine-results .jasmine-failures,
.jasmine_html-reporter.jasmine-performance .jasmine-summary {
display: none;
}
.jasmine_html-reporter .jasmine-results {
@@ -323,4 +329,23 @@ body {
}
.jasmine_html-reporter .jasmine-debug-log .jasmine-debug-log-msg {
white-space: pre;
}
.jasmine-hidden {
display: none;
}
.jasmine-tab + .jasmine-tab:before {
content: " | ";
}
.jasmine-performance-view h2, .jasmine-performance-view h3 {
margin-top: 1em;
margin-bottom: 1em;
}
.jasmine-performance-view table {
border-spacing: 5px;
}
.jasmine-performance-view th, .jasmine-performance-view td {
text-align: left;
}

View File

@@ -50,11 +50,12 @@ var getJasmineRequireObj = (function() {
j$.private.util = jRequire.util(j$);
j$.private.errors = jRequire.errors();
j$.private.formatErrorMsg = jRequire.formatErrorMsg(j$);
j$.private.AllOf = jRequire.AllOf(j$);
j$.private.Any = jRequire.Any(j$);
j$.private.Anything = jRequire.Anything(j$);
j$.private.CallTracker = jRequire.CallTracker(j$);
j$.private.MockDate = jRequire.MockDate(j$);
j$.private.getClearStack = jRequire.clearStack(j$);
j$.private.getStackClearer = jRequire.StackClearer(j$);
j$.private.Clock = jRequire.Clock();
j$.private.DelayedFunctionScheduler = jRequire.DelayedFunctionScheduler(j$);
j$.private.Deprecator = jRequire.Deprecator(j$);
@@ -407,6 +408,19 @@ getJasmineRequireObj().base = function(j$, jasmineGlobal) {
);
};
/**
* Get an {@link AsymmetricEqualityTester} that will succeed if the actual
* value being compared matches every provided equality tester.
* @name asymmetricEqualityTesters.allOf
* @emittedName jasmine.allOf
* @since 5.13.0
* @function
* @param {...*} arguments - The asymmetric equality checkers to compare.
*/
j$.allOf = function() {
return new j$.AllOf(...arguments);
};
/**
* Get an {@link AsymmetricEqualityTester} that will succeed if the actual
* value being compared is an instance of the specified class/constructor.
@@ -849,8 +863,11 @@ getJasmineRequireObj().Spec = function(j$) {
// Key and value will eventually be cloned during reporting. The error
// thrown at that point if they aren't cloneable isn't very helpful.
// Throw a better one now.
j$.private.util.assertReporterCloneable(key, 'Key');
if (!j$.private.isString(key)) {
throw new Error('Key must be a string');
}
j$.private.util.assertReporterCloneable(value, 'Value');
this.#executionState.properties = this.#executionState.properties || {};
this.#executionState.properties[key] = value;
}
@@ -930,12 +947,11 @@ getJasmineRequireObj().Spec = function(j$) {
* @property {String} description - The description passed to the {@link it} that created this spec.
* @property {String} fullName - The full description including all ancestors of this spec.
* @property {String|null} parentSuiteId - The ID of the suite containing this spec, or null if this spec is not in a describe().
* @property {String} filename - Deprecated. The name of the file the spec was defined in.
* @property {String} filename - The name of the file the spec was defined in.
* Note: The value may be incorrect if zone.js is installed or
* `it`/`fit`/`xit` have been replaced with versions that don't maintain the
* same call stack height as the originals. This property may be removed in
* a future version unless there is enough user interest in keeping it.
* See {@link https://github.com/jasmine/jasmine/issues/2065}.
* same call stack height as the originals. You can fix that by setting
* {@link Configuration#extraItStackFrames}.
* @property {ExpectationResult[]} failedExpectations - The list of expectations that failed during execution of this spec.
* @property {ExpectationResult[]} passedExpectations - The list of expectations that passed during execution of this spec.
* @property {ExpectationResult[]} deprecationWarnings - The list of deprecation warnings that occurred during execution this spec.
@@ -1117,7 +1133,20 @@ getJasmineRequireObj().Spec = function(j$) {
* @returns {Array.<string>}
* @since 5.7.0
*/
getPath: this.getPath.bind(this)
getPath: this.getPath.bind(this),
/**
* The name of the file the spec was defined in.
* Note: The value may be incorrect if zone.js is installed or
* `it`/`fit`/`xit` have been replaced with versions that don't maintain the
* same call stack height as the originals. You can fix that by setting
* {@link Configuration#extraItStackFrames}.
* @name Spec#filename
* @readonly
* @type {string}
* @since 5.13.0
*/
filename: this.filename
};
}
@@ -1196,6 +1225,8 @@ getJasmineRequireObj().Order = function() {
getJasmineRequireObj().Env = function(j$) {
'use strict';
const DEFAULT_IT_DESCRIBE_STACK_DEPTH = 3;
/**
* @class Env
* @since 2.0.0
@@ -1213,7 +1244,7 @@ getJasmineRequireObj().Env = function(j$) {
const realSetTimeout = global.setTimeout;
const realClearTimeout = global.clearTimeout;
const clearStack = j$.private.getClearStack(global);
const stackClearer = j$.private.getStackClearer(global);
this.clock = new j$.private.Clock(
global,
function() {
@@ -1223,7 +1254,7 @@ getJasmineRequireObj().Env = function(j$) {
);
const globalErrors = new GlobalErrors(
undefined,
global,
// Configuration is late-bound because GlobalErrors needs to be constructed
// before it's set to detect load-time errors in browsers
() => this.configuration()
@@ -1292,6 +1323,7 @@ getJasmineRequireObj().Env = function(j$) {
config.update(changes);
deprecator.verboseDeprecations(config.verboseDeprecations);
stackClearer.setSafariYieldStrategy(config.safariYieldStrategy);
};
/**
@@ -1494,7 +1526,7 @@ getJasmineRequireObj().Env = function(j$) {
};
function runQueue(options) {
options.clearStack = options.clearStack || clearStack;
options.clearStack = options.clearStack || stackClearer;
options.timeout = {
setTimeout: realSetTimeout,
clearTimeout: realClearTimeout
@@ -1786,14 +1818,14 @@ getJasmineRequireObj().Env = function(j$) {
this.describe = function(description, definitionFn) {
ensureIsNotNested('describe');
const filename = callerCallerFilename();
const filename = indirectCallerFilename(describeStackDepth());
return suiteBuilder.describe(description, definitionFn, filename)
.metadata;
};
this.xdescribe = function(description, definitionFn) {
ensureIsNotNested('xdescribe');
const filename = callerCallerFilename();
const filename = indirectCallerFilename(describeStackDepth());
return suiteBuilder.xdescribe(description, definitionFn, filename)
.metadata;
};
@@ -1801,30 +1833,38 @@ getJasmineRequireObj().Env = function(j$) {
this.fdescribe = function(description, definitionFn) {
ensureIsNotNested('fdescribe');
ensureNonParallel('fdescribe');
const filename = callerCallerFilename();
const filename = indirectCallerFilename(describeStackDepth());
return suiteBuilder.fdescribe(description, definitionFn, filename)
.metadata;
};
this.it = function(description, fn, timeout) {
ensureIsNotNested('it');
const filename = callerCallerFilename();
const filename = indirectCallerFilename(itStackDepth());
return suiteBuilder.it(description, fn, timeout, filename).metadata;
};
this.xit = function(description, fn, timeout) {
ensureIsNotNested('xit');
const filename = callerCallerFilename();
const filename = indirectCallerFilename(itStackDepth());
return suiteBuilder.xit(description, fn, timeout, filename).metadata;
};
this.fit = function(description, fn, timeout) {
ensureIsNotNested('fit');
ensureNonParallel('fit');
const filename = callerCallerFilename();
const filename = indirectCallerFilename(itStackDepth());
return suiteBuilder.fit(description, fn, timeout, filename).metadata;
};
function itStackDepth() {
return DEFAULT_IT_DESCRIBE_STACK_DEPTH + config.extraItStackFrames;
}
function describeStackDepth() {
return DEFAULT_IT_DESCRIBE_STACK_DEPTH + config.extraDescribeStackFrames;
}
/**
* Get a user-defined property as part of the properties field of {@link SpecDoneEvent}
* @name Env#getSpecProperty
@@ -2010,11 +2050,12 @@ getJasmineRequireObj().Env = function(j$) {
};
}
function callerCallerFilename() {
function indirectCallerFilename(depth) {
const frames = new j$.private.StackTrace(new Error()).frames;
// frames[3] should always exist except in Jasmine's own tests, which bypass
// the global it/describe layer, but don't crash if it doesn't.
return frames[3] && frames[3].file;
// The specified frame should always exist except in Jasmine's own tests,
// which bypass the global it/describe layer, but could be absent in case
// of misconfiguration. Don't crash if it's absent.
return frames[depth] && frames[depth].file;
}
return Env;
@@ -2152,6 +2193,36 @@ getJasmineRequireObj().JsApiReporter = function(j$) {
return JsApiReporter;
};
getJasmineRequireObj().AllOf = function(j$) {
'use strict';
function AllOf() {
const expectedValues = Array.from(arguments);
if (expectedValues.length === 0) {
throw new TypeError(
'jasmine.allOf() expects at least one argument to be passed.'
);
}
this.expectedValues = expectedValues;
}
AllOf.prototype.asymmetricMatch = function(other, matchersUtil) {
for (const expectedValue of this.expectedValues) {
if (!matchersUtil.equals(other, expectedValue)) {
return false;
}
}
return true;
};
AllOf.prototype.jasmineToString = function(pp) {
return '<jasmine.allOf(' + pp(this.expectedValues) + ')>';
};
return AllOf;
};
getJasmineRequireObj().Any = function(j$) {
'use strict';
@@ -2849,131 +2920,6 @@ getJasmineRequireObj().CallTracker = function(j$) {
return CallTracker;
};
getJasmineRequireObj().clearStack = function(j$) {
'use strict';
const maxInlineCallCount = 10;
function browserQueueMicrotaskImpl(global) {
const unclampedSetTimeout = getUnclampedSetTimeout(global);
const { queueMicrotask } = global;
let currentCallCount = 0;
return function clearStack(fn) {
currentCallCount++;
if (currentCallCount < maxInlineCallCount) {
queueMicrotask(fn);
} else {
currentCallCount = 0;
unclampedSetTimeout(fn);
}
};
}
function nodeQueueMicrotaskImpl(global) {
const { queueMicrotask } = global;
return function(fn) {
queueMicrotask(fn);
};
}
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 (!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 = {};
let tail = head;
let taskRunning = false;
channel.port1.onmessage = function() {
head = head.next;
const task = head.task;
delete head.task;
if (taskRunning) {
setTimeout(task, 0);
} else {
try {
taskRunning = true;
task();
} finally {
taskRunning = false;
}
}
};
return function postMessage(fn) {
tail = tail.next = { task: fn };
channel.port2.postMessage(0);
};
}
function getClearStack(global) {
const NODE_JS =
global.process &&
global.process.versions &&
typeof global.process.versions.node === 'string';
// 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)|(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_OR_WIN_WEBKIT || !global.MessageChannel /* tests */) {
// 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);
} else {
// MessageChannel is faster than queueMicrotask in supported browsers
// other than Safari.
return messageChannelImpl(global);
}
}
return getClearStack;
};
getJasmineRequireObj().Clock = function() {
'use strict';
@@ -3507,7 +3453,45 @@ getJasmineRequireObj().Configuration = function(j$) {
* @type Boolean
* @default false
*/
detectLateRejectionHandling: false
detectLateRejectionHandling: false,
/**
* The number of extra stack frames inserted by a wrapper around {@link it}
* or by some other local modification. Jasmine uses this to determine the
* filename for {@link SpecStartedEvent} and {@link SpecDoneEvent}.
* @name Configuration#extraItStackFrames
* @since 5.13.0
* @type number
* @default 0
*/
extraItStackFrames: 0,
/**
* The number of extra stack frames inserted by a wrapper around
* {@link describe} or by some other local modification. Jasmine uses this
* to determine the filename for {@link SpecStartedEvent} and
* {@link SpecDoneEvent}.
* @name Configuration#extraDescribeStackFrames
* @since 5.13.0
* @type number
* @default 0
*/
extraDescribeStackFrames: 0,
/**
* The strategy to use in Safari and similar browsers to determine how often
* to yield control by calling setTimeout. If set to "count", the default,
* the frequency of setTimeout calls is based on the number of relevant
* function calls. If set to "time", the frequency of setTimeout calls is
* based on elapsed time. Using "time" may provide a significant performance
* improvement, but as of 6.0 it hasn't been tested with a wide variety of
* workloads and should be considered experimental.
* @name Configuration#safariYieldStrategy
* @since 6.0.0
* @type 'count' | 'time'
* @default 'count'
*/
safariYieldStrategy: 'count'
};
Object.freeze(defaultConfig);
@@ -3558,6 +3542,28 @@ getJasmineRequireObj().Configuration = function(j$) {
if (typeof changes.seed !== 'undefined') {
this.#values.seed = changes.seed;
}
// 0 is a valid value for both of these, so a truthiness check wouldn't work
if (typeof changes.extraItStackFrames !== 'undefined') {
this.#values.extraItStackFrames = changes.extraItStackFrames;
}
if (typeof changes.extraDescribeStackFrames !== 'undefined') {
this.#values.extraDescribeStackFrames =
changes.extraDescribeStackFrames;
}
if (typeof changes.safariYieldStrategy !== 'undefined') {
const v = changes.safariYieldStrategy;
if (v === 'count' || v === 'time') {
this.#values.safariYieldStrategy = v;
} else {
throw new Error(
"Invalid safariYieldStrategy value. Valid values are 'count' and 'time'."
);
}
}
}
}
@@ -8560,11 +8566,11 @@ getJasmineRequireObj().QueueRunner = function(j$) {
}
this.onComplete = attrs.onComplete || emptyFn;
this.clearStack =
attrs.clearStack ||
function(fn) {
this.clearStack = attrs.clearStack || {
clearStack(fn) {
fn();
};
}
};
this.onException = attrs.onException || emptyFn;
this.onMultipleDone = attrs.onMultipleDone || fallbackOnMultipleDone;
this.userContext = attrs.userContext || new j$.private.UserContext();
@@ -8744,7 +8750,7 @@ getJasmineRequireObj().QueueRunner = function(j$) {
}
}
this.clearStack(() => {
this.clearStack.clearStack(() => {
this.globalErrors.popListener(this.handleFinalError);
if (this.errored_) {
@@ -8902,6 +8908,7 @@ getJasmineRequireObj().reporterEvents = function(j$) {
* {@link ReporterCapabilities} will apply.
* @name Reporter#reporterCapabilities
* @type ReporterCapabilities | undefined
* @optional
* @since 5.0
*/
/**
@@ -8968,6 +8975,7 @@ getJasmineRequireObj().reporterEvents = function(j$) {
/**
* `specStarted` is invoked when an `it` starts to run (including associated `beforeEach` functions)
* @function
* @optional
* @name Reporter#specStarted
* @param {SpecStartedEvent} 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.
@@ -10678,6 +10686,164 @@ getJasmineRequireObj().SpyStrategy = function(j$) {
return SpyStrategy;
};
getJasmineRequireObj().StackClearer = function(j$) {
'use strict';
const maxInlineCallCount = 10;
// 25ms gives a good balance of speed and UI responsiveness when running
// jasmine-core's own tests in Safari 18. The exact value isn't critical.
const safariYieldIntervalMs = 25;
function browserQueueMicrotaskImpl(global) {
const unclampedSetTimeout = getUnclampedSetTimeout(global);
const { queueMicrotask } = global;
let yieldStrategy = 'count';
let currentCallCount = 0; // for count strategy
let nextSetTimeoutTime; // for time strategy
return {
clearStack(fn) {
currentCallCount++;
let shouldSetTimeout;
if (yieldStrategy === 'time') {
const now = new Date().getTime();
shouldSetTimeout = now >= nextSetTimeoutTime;
if (shouldSetTimeout) {
nextSetTimeoutTime = now + safariYieldIntervalMs;
}
} else {
shouldSetTimeout = currentCallCount >= maxInlineCallCount;
if (shouldSetTimeout) {
currentCallCount = 0;
}
}
if (shouldSetTimeout) {
unclampedSetTimeout(fn);
} else {
queueMicrotask(fn);
}
},
setSafariYieldStrategy(strategy) {
yieldStrategy = strategy;
if (yieldStrategy === 'time') {
nextSetTimeoutTime = new Date().getTime() + safariYieldIntervalMs;
}
}
};
}
function nodeQueueMicrotaskImpl(global) {
const { queueMicrotask } = global;
return {
clearStack(fn) {
queueMicrotask(fn);
},
setSafariYieldStrategy() {}
};
}
function messageChannelImpl(global) {
const { setTimeout } = global;
const postMessage = getPostMessage(global);
let currentCallCount = 0;
return {
clearStack(fn) {
currentCallCount++;
if (currentCallCount < maxInlineCallCount) {
postMessage(fn);
} else {
currentCallCount = 0;
setTimeout(fn);
}
},
setSafariYieldStrategy() {}
};
}
function getUnclampedSetTimeout(global) {
const { setTimeout } = global;
if (!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 = {};
let tail = head;
let taskRunning = false;
channel.port1.onmessage = function() {
head = head.next;
const task = head.task;
delete head.task;
if (taskRunning) {
setTimeout(task, 0);
} else {
try {
taskRunning = true;
task();
} finally {
taskRunning = false;
}
}
};
return function postMessage(fn) {
tail = tail.next = { task: fn };
channel.port2.postMessage(0);
};
}
function getStackClearer(global) {
const NODE_JS =
global.process &&
global.process.versions &&
typeof global.process.versions.node === 'string';
// 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)|(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_OR_WIN_WEBKIT) {
// 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.
return browserQueueMicrotaskImpl(global);
} else {
// MessageChannel is faster than queueMicrotask in supported browsers
// other than Safari.
return messageChannelImpl(global);
}
}
return getStackClearer;
};
getJasmineRequireObj().StackTrace = function(j$) {
'use strict';
@@ -10851,8 +11017,11 @@ getJasmineRequireObj().Suite = function(j$) {
// Key and value will eventually be cloned during reporting. The error
// thrown at that point if they aren't cloneable isn't very helpful.
// Throw a better one now.
j$.private.util.assertReporterCloneable(key, 'Key');
if (!j$.private.isString(key)) {
throw new Error('Key must be a string');
}
j$.private.util.assertReporterCloneable(value, 'Value');
this.#result.properties = this.#result.properties || {};
this.#result.properties[key] = value;
}
@@ -10960,12 +11129,11 @@ getJasmineRequireObj().Suite = function(j$) {
* @property {String} description - The description text passed to the {@link describe} that made this suite.
* @property {String} fullName - The full description including all ancestors of this suite.
* @property {String|null} parentSuiteId - The ID of the suite containing this suite, or null if this is not in another describe().
* @property {String} filename - Deprecated. The name of the file the suite was defined in.
* @property {String} filename - The name of the file the suite was defined in.
* Note: The value may be incorrect if zone.js is installed or
* `describe`/`fdescribe`/`xdescribe` have been replaced with versions that
* don't maintain the same call stack height as the originals. This property
* may be removed in a future version unless there is enough user interest
* in keeping it. See {@link https://github.com/jasmine/jasmine/issues/2065}.
* don't maintain the same call stack height as the originals. You can fix
* that by setting {@link Configuration#extraDescribeStackFrames}.
* @property {ExpectationResult[]} failedExpectations - The list of expectations that failed in an {@link afterAll} for this suite.
* @property {ExpectationResult[]} deprecationWarnings - The list of deprecation warnings that occurred on this suite.
* @property {String} status - Once the suite has completed, this string represents the pass/fail status of this suite.
@@ -11173,6 +11341,19 @@ getJasmineRequireObj().Suite = function(j$) {
* @since 2.0.0
*/
this.description = suite.description;
/**
* The name of the file the suite was defined in.
* Note: The value may be incorrect if zone.js is installed or
* `describe`/`fdescribe`/`xdescribe` have been replaced with versions
* that don't maintain the same call stack height as the originals. You
* can fix that by setting {@link Configuration#extraItStackFrames}.
* @name Suite#filename
* @readonly
* @type {string}
* @since 5.13.0
*/
this.filename = suite.filename;
}
/**
@@ -11852,7 +12033,10 @@ getJasmineRequireObj().TreeRunner = function(j$) {
_executeSpec(spec, specOverallDone) {
const onStart = next => {
this.#currentRunableTracker.setCurrentSpec(spec);
this.#runableResources.initForRunable(spec.id, spec.parentSuiteId);
this.#runableResources.initForRunable(
spec.id,
spec.parentSuiteId || this.#executionTree.topSuite.id
);
this.#reportDispatcher.specStarted(spec.startedEvent()).then(next);
};
const resultCallback = (result, next) => {
@@ -12111,5 +12295,5 @@ getJasmineRequireObj().UserContext = function(j$) {
};
getJasmineRequireObj().version = function() {
return '6.0.0-alpha.1';
return '6.0.0-alpha.2';
};

View File

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

29
release_notes/5.12.1.md Normal file
View File

@@ -0,0 +1,29 @@
Jasmine Core 5.12.1 Release Notes
## Bug fixes
* Fix custom matchers in top-level specs
* Merges [#2088](https://github.com/jasmine/jasmine/pull/2088) from @bonkevin
## Supported environments
This version has been tested in the following environments.
| Environment | Supported versions |
|-------------|--------------------------------|
| Node | 18.20.5**, 20, 22, 24 |
| Safari | 16**, 17** |
| Chrome | 141* |
| Firefox | 102**, 115**, 128**, 140, 144* |
| Edge | 141* |
\* 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

@@ -0,0 +1,90 @@
# Jasmine Core 6.0.0-alpha.2 Release Notes
This is a pre-release, intended to offer a preview of upcoming changes and to
solicit feedback.
## A Note About Pre-Release Compatibility
There may be additional breaking changes in future 6.0 pre-releases or in the
final 6.0 release. That's allowed by the semver specification, but users are
sometimes unpleasantly surprised by it.
NPM's implementation of carat version ranges assumes that subsequent
pre-releases and final releases are fully compatible with earlier pre-releases.
If your package.json contains `"jasmine-core": "^6.0.0-alpha.2`,
NPM might install any later 6.x version even though there is no guarantee of
compatibility. If that isn't ok, you should specify an exact pre-release version:
`"jasmine-core": "6.0.0-alpha.2`.
## Changes to supported environments
Safari 26 is now supported on a best-effort basis.†
Due to the limited availability of Safari 18 and later on free CI services,
Safari support in future jasmine-core versions will be limited to:
* Best-effort support for the latest Safari version available on GitHub Actions,
which may change at any time.
* Best-effort support for Safari 16 and 17 for as long as it remains practical.
## Bug Fixes
* Fix custom matchers in top-level specs††
* Merges [#2088](https://github.com/jasmine/jasmine/pull/2088) from @bonkevin
## New features
* Larger body font size in HTML reporters
* New Performance tab in HtmlReporterV2 shows metrics and a list of the slowest
specs.
* Experimental `safariYieldStrategy: "time"` config option, which may make
Jasmine run significantly faster in Safari and similar browsers. So far, this
option has not been tested on a wide variety of workloads. Feedback is
appreciated.
* New `extraItStackFrames` and `extraDescribeStackFrames` config options to fix
the filename properties of reporter events in configurations that wrap
`it`/`describe`, such as zone.js.†
* `jasmine.allOf asymmetric` equality tester†
* Merges [#2087](https://github.com/jasmine/jasmine/pull/2087) from @jonahd-g
* Fixes [#2083](https://github.com/jasmine/jasmine/issues/2083)
* Re-add support for partial spec name filtering via `spec` query parameter
* Fixes [#2085](https://github.com/jasmine/jasmine/issues/2085).
* Require spec/suite property keys to be strings, not just anything that's
cloneable and serializable. This matches the existing API reference
documentation.
## Documentation improvements
* Fix HtmlReporterV2 ctor example
## Internal Improvements
* Remove code to support browsers that don't have MessageChannel. Jasmine hasn't
run in any such browsers since 2.x.
† Also likely to be included in a future 5.x release.<br>
†† Also released in 5.12.1.
## Supported environments
This version has been tested in the following environments.
| Environment | Supported versions |
|-------------------|--------------------------------|
| Node | 20, 22, 24 |
| Safari | 16**, 17**, 26** |
| Chrome | 142* |
| Firefox | 102**, 115**, 128**, 140, 145* |
| Edge | 142* |
\* 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

@@ -1,5 +1,10 @@
#!/bin/sh
# Run tests in supported browsers that are available on Saucelabs.
# Note: The latest Safari version is tested via GitHub Actions because Saucelabs
# only makes it available to paid enterprise accounts. See
# .github/workflows/safari.yml.
run_browser() {
browser=$1
version=$2

View File

@@ -10,7 +10,14 @@ describe('Configuration', function() {
'detectLateRejectionHandling',
'verboseDeprecations'
];
const allKeys = [...standardBooleanKeys, 'seed', 'specFilter'];
const allKeys = [
...standardBooleanKeys,
'seed',
'specFilter',
'extraItStackFrames',
'extraDescribeStackFrames',
'safariYieldStrategy'
];
Object.freeze(standardBooleanKeys);
Object.freeze(allKeys);
@@ -28,6 +35,9 @@ describe('Configuration', function() {
expect(subject.forbidDuplicateNames).toEqual(true);
expect(subject.verboseDeprecations).toEqual(false);
expect(subject.detectLateRejectionHandling).toEqual(false);
expect(subject.extraItStackFrames).toEqual(0);
expect(subject.extraDescribeStackFrames).toEqual(0);
expect(subject.safariYieldStrategy).toEqual('count');
});
describe('copy()', function() {
@@ -109,5 +119,48 @@ describe('Configuration', function() {
subject.update({ seed: null });
expect(subject.seed).toBeNull();
});
it('sets extraItStackFrames when not undefined', function() {
const subject = new privateUnderTest.Configuration();
subject.update({ extraItStackFrames: undefined });
expect(subject.extraItStackFrames).toEqual(0);
subject.update({ extraItStackFrames: 100000 });
expect(subject.extraItStackFrames).toEqual(100000);
});
it('sets extraDescribeStackFrames when not undefined', function() {
const subject = new privateUnderTest.Configuration();
subject.update({ extraDescribeStackFrames: undefined });
expect(subject.extraDescribeStackFrames).toEqual(0);
subject.update({ extraDescribeStackFrames: 100000 });
expect(subject.extraDescribeStackFrames).toEqual(100000);
});
it('sets safariYieldStrategy when valid', function() {
const subject = new privateUnderTest.Configuration();
subject.update({ safariYieldStrategy: undefined });
expect(subject.safariYieldStrategy).toEqual('count');
subject.update({ safariYieldStrategy: 'time' });
expect(subject.safariYieldStrategy).toEqual('time');
subject.update({ safariYieldStrategy: 'count' });
expect(subject.safariYieldStrategy).toEqual('count');
});
it('rejcts invalid safariYieldStrategy values', function() {
const subject = new privateUnderTest.Configuration();
expect(function() {
subject.update({ safariYieldStrategy: 'thyme' });
}).toThrowError(
"Invalid safariYieldStrategy value. Valid values are 'count' and 'time'."
);
});
});
});

View File

@@ -1,4 +1,3 @@
// TODO: Fix these unit tests!
describe('Env', function() {
let env;
beforeEach(function() {
@@ -95,7 +94,7 @@ describe('Env', function() {
});
});
it('accepts its own current configureation', function() {
it('accepts its own current configuration', function() {
env.configure(env.configuration());
});
@@ -198,6 +197,29 @@ describe('Env', function() {
expect(innerSuite.parentSuite).toBe(suite);
expect(spec.getFullName()).toEqual('outer suite inner suite a spec');
});
it('sets the caller filename correctly when extraDescribeStackFrames is not set', function() {
// IIFE is used to match the stack depth when global describe() is called
const suite = (function() {
return env[methodName]('a suite', function() {
env.it('a spec');
});
})();
expect(suite.filename).toMatch(/EnvSpec\.js$/);
});
it('sets the caller filename correctly when extraDescribeStackFrames is set', function() {
env.configure({ extraDescribeStackFrames: 2 });
// IIFE is used to match the stack depth when global describe() is called
const suite = (function() {
return specHelpers.callerFilenameShim(function() {
return env[methodName]('a suite', function() {
env.it('a spec');
});
});
})();
expect(suite.filename).toMatch(/EnvSpec\.js$/);
});
}
describe('#describe', function() {
@@ -300,6 +322,25 @@ describe('Env', function() {
.not.toEqual('');
expect(spec.pend).toBeFalsy();
});
it('sets the caller filename correctly when extraItStackFrames is not set', function() {
// IIFE is used to match the stack depth when global it() is called
const spec = (function() {
return env[methodName]('a spec', function() {});
})();
expect(spec.filename).toMatch(/EnvSpec\.js$/);
});
it('sets the caller filename correctly when extraItStackFrames is set', function() {
env.configure({ extraItStackFrames: 2 });
// IIFE is used to match the stack depth when global it() is called
const spec = (function() {
return specHelpers.callerFilenameShim(function() {
return env[methodName]('a spec', function() {});
});
})();
expect(spec.filename).toMatch(/EnvSpec\.js$/);
});
}
describe('#it', function() {

View File

@@ -486,7 +486,7 @@ describe('QueueRunner', function() {
errorListeners.pop();
}
},
clearStack = jasmine.createSpy('clearStack'),
clearStack = jasmine.createSpyObj('clearStack', ['clearStack']),
onException = jasmine.createSpy('onException'),
queueRunner = new privateUnderTest.QueueRunner({
queueableFns: [queueableFn],
@@ -498,10 +498,10 @@ describe('QueueRunner', function() {
queueRunner.execute();
jasmine.clock().tick();
expect(clearStack).toHaveBeenCalled();
expect(clearStack.clearStack).toHaveBeenCalled();
expect(errorListeners.length).toEqual(1);
errorListeners[0](error);
clearStack.calls.argsFor(0)[0]();
clearStack.clearStack.calls.argsFor(0)[0]();
expect(onException).toHaveBeenCalledWith(error);
});
});
@@ -908,22 +908,22 @@ describe('QueueRunner', function() {
},
afterFn = { fn: jasmine.createSpy('afterFn') },
completeCallback = jasmine.createSpy('completeCallback'),
clearStack = jasmine.createSpy('clearStack'),
clearStack = jasmine.createSpyObj('clearStack', ['clearStack']),
queueRunner = new privateUnderTest.QueueRunner({
queueableFns: [asyncFn, afterFn],
clearStack: clearStack,
onComplete: completeCallback
});
clearStack.and.callFake(function(fn) {
clearStack.clearStack.and.callFake(function(fn) {
fn();
});
queueRunner.execute();
jasmine.clock().tick();
expect(afterFn.fn).toHaveBeenCalled();
expect(clearStack).toHaveBeenCalled();
clearStack.calls.argsFor(0)[0]();
expect(clearStack.clearStack).toHaveBeenCalled();
clearStack.clearStack.calls.argsFor(0)[0]();
expect(completeCallback).toHaveBeenCalled();
});
});

View File

@@ -103,26 +103,14 @@ describe('Spec', function() {
});
});
it('throws if the key is not structured-cloneable', function() {
it('throws if the key is not a string', function() {
const spec = new privateUnderTest.Spec({
queueableFn: { fn: () => {} }
});
expect(function() {
spec.setSpecProperty(new Promise(() => {}), '');
}).toThrowError("Key can't be cloned");
});
it('throws if the key is not JSON-serializable', function() {
const spec = new privateUnderTest.Spec({
queueableFn: { fn: () => {} }
});
expect(function() {
const k = {};
k.self = k;
spec.setSpecProperty(k, '');
}).toThrowError("Key can't be cloned");
spec.setSpecProperty({}, '');
}).toThrowError('Key must be a string');
});
it('throws if the value is not structured-cloneable', function() {

View File

@@ -1,6 +1,6 @@
describe('ClearStack', function() {
describe('StackClearer', function() {
it('works in an integrationy way', function(done) {
const clearStack = privateUnderTest.getClearStack(
const { clearStack } = privateUnderTest.getStackClearer(
jasmineUnderTest.getGlobal()
);
@@ -36,7 +36,7 @@ describe('ClearStack', function() {
queueMicrotask
};
const clearStack = privateUnderTest.getClearStack(global);
const { clearStack } = privateUnderTest.getStackClearer(global);
for (let i = 0; i < 9; i++) {
clearStack(function() {});
@@ -73,17 +73,6 @@ describe('ClearStack', function() {
}
};
});
describe('when MessageChannel is unavailable', function() {
usesQueueMicrotaskWithSetTimeout(function() {
return {
navigator: {
userAgent: 'CERN-LineMode/2.15 libwww/2.17b3',
MessageChannel: undefined
}
};
});
});
});
describe('in Node', function() {
@@ -104,7 +93,7 @@ describe('ClearStack', function() {
...makeGlobal(),
MessageChannel: fakeMessageChannel
};
const clearStack = privateUnderTest.getClearStack(global);
const { clearStack } = privateUnderTest.getStackClearer(global);
let called = false;
clearStack(function() {
@@ -125,7 +114,7 @@ describe('ClearStack', function() {
return fakeChannel;
}
};
const clearStack = privateUnderTest.getClearStack(global);
const { clearStack } = privateUnderTest.getStackClearer(global);
for (let i = 0; i < 9; i++) {
clearStack(function() {});
@@ -150,7 +139,7 @@ describe('ClearStack', function() {
setTimeout,
MessageChannel: fakeMessageChannel
};
const clearStack = privateUnderTest.getClearStack(global);
const { clearStack } = privateUnderTest.getStackClearer(global);
const fn = jasmine.createSpy('second clearStack function');
clearStack(function() {
@@ -170,7 +159,7 @@ describe('ClearStack', function() {
fn();
}
};
const clearStack = privateUnderTest.getClearStack(global);
const { clearStack } = privateUnderTest.getStackClearer(global);
let called = false;
clearStack(function() {
@@ -180,30 +169,82 @@ describe('ClearStack', function() {
expect(called).toBe(true);
});
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 = privateUnderTest.getClearStack(global);
function hasSetTimeoutBehavior(configure) {
it('uses setTimeout instead of queueMicrotask every 10 calls', function() {
const queueMicrotask = jasmine.createSpy('queueMicrotask');
const setTimeout = jasmine.createSpy('setTimeout');
const global = {
...makeGlobal(),
queueMicrotask,
setTimeout
};
const stackClearer = privateUnderTest.getStackClearer(global);
for (let i = 0; i < 9; i++) {
clearStack(function() {});
}
if (configure) {
configure(stackClearer);
}
expect(queueMicrotask).toHaveBeenCalled();
expect(setTimeout).not.toHaveBeenCalled();
for (let i = 0; i < 9; i++) {
stackClearer.clearStack(function() {});
}
clearStack(function() {});
expect(queueMicrotask).toHaveBeenCalledTimes(9);
expect(setTimeout).toHaveBeenCalledTimes(1);
expect(queueMicrotask).toHaveBeenCalled();
expect(setTimeout).not.toHaveBeenCalled();
clearStack(function() {});
expect(queueMicrotask).toHaveBeenCalledTimes(10);
expect(setTimeout).toHaveBeenCalledTimes(1);
stackClearer.clearStack(function() {});
expect(queueMicrotask).toHaveBeenCalledTimes(9);
expect(setTimeout).toHaveBeenCalledTimes(1);
stackClearer.clearStack(function() {});
expect(queueMicrotask).toHaveBeenCalledTimes(10);
expect(setTimeout).toHaveBeenCalledTimes(1);
});
}
hasSetTimeoutBehavior();
describe('With yield strategy explicitly set to count', function() {
hasSetTimeoutBehavior(function(stackClearer) {
stackClearer.setSafariYieldStrategy('count');
});
});
describe('With yield strategy set to time', function() {
beforeEach(function() {
jasmine.clock().install();
jasmine.clock().mockDate();
});
afterEach(function() {
jasmine.clock().uninstall();
});
it('uses setTimeout instead of queueMicrotask every 25 milliseconds', function() {
const queueMicrotask = jasmine.createSpy('queueMicrotask');
const setTimeout = jasmine.createSpy('setTimeout');
const global = {
...makeGlobal(),
queueMicrotask,
setTimeout
};
const stackClearer = privateUnderTest.getStackClearer(global);
stackClearer.setSafariYieldStrategy('time');
// 10+ counts should not trigger a setTimeout if they happen fast enough
jasmine.clock().tick(24);
for (let i = 0; i < 11; i++) {
stackClearer.clearStack(function() {});
}
expect(queueMicrotask).toHaveBeenCalled();
expect(setTimeout).not.toHaveBeenCalled();
queueMicrotask.calls.reset();
jasmine.clock().tick(1);
stackClearer.clearStack(function() {});
expect(queueMicrotask).not.toHaveBeenCalled();
expect(setTimeout).toHaveBeenCalledTimes(1);
});
});
}
@@ -215,7 +256,7 @@ describe('ClearStack', function() {
fn();
}
};
const clearStack = privateUnderTest.getClearStack(global);
const { clearStack } = privateUnderTest.getStackClearer(global);
let called = false;
clearStack(function() {
@@ -233,7 +274,7 @@ describe('ClearStack', function() {
queueMicrotask,
setTimeout
};
const clearStack = privateUnderTest.getClearStack(global);
const { clearStack } = privateUnderTest.getStackClearer(global);
clearStack(function() {});
clearStack(function() {});

View File

@@ -409,12 +409,12 @@ describe('Suite', function() {
});
describe('#setSuiteProperty', function() {
it('throws if the key is not structured-cloneable', function() {
it('throws if the key is not a string', function() {
const suite = new privateUnderTest.Suite({});
expect(function() {
suite.setSuiteProperty(new Promise(() => {}), '');
}).toThrowError("Key can't be cloned");
suite.setSuiteProperty({}, '');
}).toThrowError('Key must be a string');
});
it('throws if the value is not structured-cloneable', function() {
@@ -424,6 +424,16 @@ describe('Suite', function() {
suite.setSuiteProperty('k', new Promise(() => {}));
}).toThrowError("Value can't be cloned");
});
it('throws if the value is not JSON-serializable', function() {
const suite = new privateUnderTest.Suite({});
expect(function() {
const v = {};
v.self = v;
suite.setSuiteProperty('k', v);
}).toThrowError("Value can't be cloned");
});
});
describe('#startedEvent', function() {

View File

@@ -0,0 +1,63 @@
describe('AllOf', function() {
it('matches a single value', function() {
const matchersUtil = new privateUnderTest.MatchersUtil();
const allOf = new privateUnderTest.AllOf('foo');
expect(allOf.asymmetricMatch('foo', matchersUtil)).toBeTrue();
});
it('matches a single matcher', function() {
const matchersUtil = new privateUnderTest.MatchersUtil();
const allOf = new privateUnderTest.AllOf(
new privateUnderTest.StringContaining('oo')
);
expect(allOf.asymmetricMatch('foo', matchersUtil)).toBeTrue();
});
it('matches multiple matchers', function() {
const matchersUtil = new privateUnderTest.MatchersUtil();
const allOf = new privateUnderTest.AllOf(
new privateUnderTest.StringContaining('o'),
new privateUnderTest.StringContaining('f')
);
expect(allOf.asymmetricMatch('foo', matchersUtil)).toBeTrue();
});
it('does not match when value does not match', function() {
const matchersUtil = new privateUnderTest.MatchersUtil();
const allOf = new privateUnderTest.AllOf('bar');
expect(allOf.asymmetricMatch('foo', matchersUtil)).toBeFalse();
});
it('does not match when any matchers fail', function() {
const matchersUtil = new privateUnderTest.MatchersUtil();
const allOf = new privateUnderTest.AllOf(
new privateUnderTest.StringContaining('o'),
new privateUnderTest.StringContaining('x')
);
expect(allOf.asymmetricMatch('foo', matchersUtil)).toBeFalse();
});
it('jasmineToStrings itself', function() {
const matcher = new privateUnderTest.AllOf('o');
const pp = jasmine.createSpy('pp').and.returnValue('sample');
expect(matcher.jasmineToString(pp)).toEqual('<jasmine.allOf(sample)>');
expect(pp).toHaveBeenCalledWith(['o']);
});
describe('when called without an argument', function() {
it('tells the user to pass a constructor argument', function() {
expect(function() {
new privateUnderTest.AllOf();
}).toThrowError(
TypeError,
'jasmine.allOf() expects at least one argument to be passed.'
);
});
});
});

View File

@@ -1114,8 +1114,17 @@ describe('Env integration', function() {
global: {
setTimeout: globalSetTimeout,
clearTimeout: clearTimeout,
addEventListener() {},
removeEventListener() {},
queueMicrotask: function(fn) {
queueMicrotask(fn);
},
// Enough Node globals to make getStackClearer() return the microtask
// implementation, which is the easiest to mock
process: {
versions: {
node: ''
}
}
}
});
@@ -1179,7 +1188,7 @@ describe('Env integration', function() {
global: {
setTimeout: function(cb, t) {
const stack = new Error().stack;
if (stack.indexOf('ClearStack') >= 0) {
if (stack.indexOf('clearStack') >= 0) {
return realSetTimeout(cb, t);
} else {
return setTimeout(cb, t);
@@ -1188,8 +1197,17 @@ describe('Env integration', function() {
clearTimeout: clearTimeout,
setInterval: setInterval,
clearInterval: clearInterval,
addEventListener() {},
removeEventListener() {},
queueMicrotask: function(fn) {
queueMicrotask(fn);
},
// Enough Node globals to make getStackClearer() return the microtask
// implementation, which is the easiest to mock
process: {
versions: {
node: ''
}
}
}
});
@@ -2274,6 +2292,34 @@ describe('Env integration', function() {
await env.execute();
});
it('Custom matchers set in top-level beforeAll should be available to all specs and suites', async function() {
const matchers = {
toFoo: function() {}
};
env.beforeAll(function() {
env.addMatchers(matchers);
});
env.describe('suite - top-level', function() {
env.it('has access to the custom matcher', function() {
expect(env.expect().toFoo).toBeDefined();
});
env.describe('suite - nested', function() {
env.it('has access to the custom matcher', function() {
expect(env.expect().toFoo).toBeDefined();
});
});
});
env.it('spec - top-level - has access to the custom matcher', function() {
expect(env.expect().toFoo).toBeDefined();
});
await env.execute();
});
it('throws an exception if you try to create a spy outside of a runnable', async function() {
const obj = { fn: function() {} };
let exception;
@@ -2775,12 +2821,18 @@ describe('Env integration', function() {
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
},
// Enough Node globals to make getStackClearer() return the microtask
// implementation, which is the easiest to mock
process: {
versions: {
node: ''
}
}
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
env.cleanup_();
env = new privateUnderTest.Env();
env = new privateUnderTest.Env({ global });
const reporter = jasmine.createSpyObj('reporter', [
'jasmineDone',
'suiteDone',
@@ -3228,13 +3280,11 @@ describe('Env integration', function() {
});
it('is resolved after the stack is cleared', function(done) {
const realClearStack = privateUnderTest.getClearStack(
jasmineUnderTest.getGlobal()
),
clearStackSpy = jasmine
.createSpy('clearStack')
.and.callFake(realClearStack);
spyOn(privateUnderTest, 'getClearStack').and.returnValue(clearStackSpy);
const stackClearer = privateUnderTest.getStackClearer(
jasmineUnderTest.getGlobal()
);
spyOn(stackClearer, 'clearStack').and.callThrough();
spyOn(privateUnderTest, 'getStackClearer').and.returnValue(stackClearer);
// Create a new env that has the clearStack defined above
env.cleanup_();
@@ -3245,10 +3295,10 @@ describe('Env integration', function() {
});
env.execute(null).then(function() {
expect(clearStackSpy).toHaveBeenCalled(); // (many times)
clearStackSpy.calls.reset();
expect(stackClearer.clearStack).toHaveBeenCalled(); // (many times)
stackClearer.clearStack.calls.reset();
setTimeout(function() {
expect(clearStackSpy).not.toHaveBeenCalled();
expect(stackClearer.clearStack).not.toHaveBeenCalled();
done();
});
});
@@ -3337,13 +3387,11 @@ describe('Env integration', function() {
});
it('is called after the stack is cleared', async function() {
const realClearStack = privateUnderTest.getClearStack(
jasmineUnderTest.getGlobal()
),
clearStackSpy = jasmine
.createSpy('clearStack')
.and.callFake(realClearStack);
spyOn(privateUnderTest, 'getClearStack').and.returnValue(clearStackSpy);
const stackClearer = privateUnderTest.getStackClearer(
jasmineUnderTest.getGlobal()
);
spyOn(stackClearer, 'clearStack').and.callThrough();
spyOn(privateUnderTest, 'getStackClearer').and.returnValue(stackClearer);
// Create a new env that has the clearStack defined above
env.cleanup_();
@@ -3355,12 +3403,12 @@ describe('Env integration', function() {
await env.execute();
expect(clearStackSpy).toHaveBeenCalled(); // (many times)
clearStackSpy.calls.reset();
expect(stackClearer.clearStack).toHaveBeenCalled(); // (many times)
stackClearer.clearStack.calls.reset();
await new Promise(resolve => setTimeout(resolve));
expect(clearStackSpy).not.toHaveBeenCalled();
expect(stackClearer.clearStack).not.toHaveBeenCalled();
});
it('is called after QueueRunner timeouts are cleared', async function() {

View File

@@ -11,8 +11,8 @@ describe('Global error handling (integration)', function() {
env.cleanup_();
});
it('reports errors that occur during loading', async function() {
const global = {
function mockGlobal() {
return {
...browserEventMethods(),
setTimeout: function(fn, delay) {
return setTimeout(fn, delay);
@@ -23,12 +23,21 @@ describe('Global error handling (integration)', function() {
queueMicrotask: function(fn) {
queueMicrotask(fn);
},
onerror: function() {}
onerror: function() {},
// Enough Node globals to make getStackClearer() return the microtask
// implementation, which is the easiest to mock
process: {
versions: {
node: ''
}
}
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
}
it('reports errors that occur during loading', async function() {
const global = mockGlobal();
env.cleanup_();
env = new privateUnderTest.Env();
env = new privateUnderTest.Env({ global });
const reporter = jasmine.createSpyObj('reporter', [
'jasmineDone',
'suiteDone',
@@ -70,21 +79,9 @@ describe('Global error handling (integration)', function() {
describe('If suppressLoadErrors: true was passed', function() {
it('does not install a global error handler during loading', async function() {
const global = mockGlobal();
const originalOnerror = jasmine.createSpy('original onerror');
const global = {
...browserEventMethods(),
setTimeout: function(fn, delay) {
return setTimeout(fn, delay);
},
clearTimeout: function(fn, delay) {
clearTimeout(fn, delay);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
},
onerror: originalOnerror
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
global.onerror = originalOnerror;
const globalErrors = new privateUnderTest.GlobalErrors(global);
const onerror = jasmine.createSpy('onerror');
globalErrors.pushListener(onerror);
@@ -92,6 +89,7 @@ describe('Global error handling (integration)', function() {
env.cleanup_();
env = new privateUnderTest.Env({
suppressLoadErrors: true,
global,
GlobalErrors: function() {
return globalErrors;
}
@@ -115,21 +113,9 @@ describe('Global error handling (integration)', function() {
describe('Handling unhandled exceptions', function() {
it('routes unhandled exceptions to the running spec', async function() {
const global = {
...browserEventMethods(),
setTimeout: function(fn, delay) {
return setTimeout(fn, delay);
},
clearTimeout: function(fn, delay) {
clearTimeout(fn, delay);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
}
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
const global = mockGlobal();
env.cleanup_();
env = new privateUnderTest.Env();
env = new privateUnderTest.Env({ global });
const reporter = jasmine.createSpyObj('fakeReporter', [
'specDone',
'suiteDone'
@@ -156,24 +142,12 @@ describe('Global error handling (integration)', function() {
describe('When the most recently running spec has reported specDone', function() {
it('routes unhandled exceptions to an ancestor suite', async function() {
const global = {
...browserEventMethods(),
setTimeout: function(fn, delay) {
return setTimeout(fn, delay);
},
clearTimeout: function(fn) {
clearTimeout(fn);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
}
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
const realClearStack = privateUnderTest.getClearStack(global);
const global = mockGlobal();
const stackClearer = privateUnderTest.getStackClearer(global);
const realClearStack = stackClearer.clearStack;
const clearStackCallbacks = {};
let clearStackCallCount = 0;
spyOn(privateUnderTest, 'getClearStack').and.returnValue(function(fn) {
spyOn(stackClearer, 'clearStack').and.callFake(function(fn) {
clearStackCallCount++;
if (clearStackCallbacks[clearStackCallCount]) {
@@ -182,9 +156,12 @@ describe('Global error handling (integration)', function() {
realClearStack(fn);
});
spyOn(privateUnderTest, 'getStackClearer').and.returnValue(
stackClearer
);
env.cleanup_();
env = new privateUnderTest.Env();
env = new privateUnderTest.Env({ global });
let suiteErrors = [];
env.addReporter({
@@ -220,21 +197,9 @@ describe('Global error handling (integration)', function() {
});
it('routes unhandled exceptions to the running suite', async function() {
const global = {
...browserEventMethods(),
setTimeout: function(fn, delay) {
return setTimeout(fn, delay);
},
clearTimeout: function(fn, delay) {
clearTimeout(fn, delay);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
}
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
const global = mockGlobal();
env.cleanup_();
env = new privateUnderTest.Env();
env = new privateUnderTest.Env({ global });
const reporter = jasmine.createSpyObj('fakeReporter', [
'specDone',
'suiteDone'
@@ -273,24 +238,12 @@ describe('Global error handling (integration)', function() {
describe('When the most recently suite has reported suiteDone', function() {
it('routes unhandled exceptions to an ancestor suite', async function() {
const global = {
...browserEventMethods(),
setTimeout: function(fn, delay) {
return setTimeout(fn, delay);
},
clearTimeout: function(fn, delay) {
clearTimeout(fn, delay);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
}
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
const realClearStack = privateUnderTest.getClearStack(global);
const global = mockGlobal();
const stackClearer = privateUnderTest.getStackClearer(global);
const realClearStack = stackClearer.clearStack;
const clearStackCallbacks = {};
let clearStackCallCount = 0;
spyOn(privateUnderTest, 'getClearStack').and.returnValue(function(fn) {
spyOn(stackClearer, 'clearStack').and.callFake(function(fn) {
clearStackCallCount++;
if (clearStackCallbacks[clearStackCallCount]) {
@@ -299,9 +252,12 @@ describe('Global error handling (integration)', function() {
realClearStack(fn);
});
spyOn(privateUnderTest, 'getStackClearer').and.returnValue(
stackClearer
);
env.cleanup_();
env = new privateUnderTest.Env();
env = new privateUnderTest.Env({ global });
let suiteErrors = [];
env.addReporter({
@@ -341,21 +297,9 @@ describe('Global error handling (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);
},
clearTimeout: function(fn, delay) {
clearTimeout(fn, delay);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
}
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
const global = mockGlobal();
env.cleanup_();
env = new privateUnderTest.Env();
env = new privateUnderTest.Env({ global });
spyOn(console, 'error');
@@ -384,26 +328,14 @@ describe('Global error handling (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);
},
clearTimeout: function(fn) {
clearTimeout(fn);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
}
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
const realClearStack = privateUnderTest.getClearStack(global);
const global = mockGlobal();
const stackClearer = privateUnderTest.getStackClearer(global);
const realClearStack = stackClearer.clearStack;
let clearStackCallCount = 0;
let jasmineDone = false;
const expectedErrors = [];
const expectedErrorsAfterJasmineDone = [];
spyOn(privateUnderTest, 'getClearStack').and.returnValue(function(fn) {
spyOn(stackClearer, 'clearStack').and.callFake(function(fn) {
clearStackCallCount++;
const msg = `Error in clearStack #${clearStackCallCount}`;
@@ -416,10 +348,11 @@ describe('Global error handling (integration)', function() {
dispatchErrorEvent(global, 'error', { error: msg });
realClearStack(fn);
});
spyOn(privateUnderTest, 'getStackClearer').and.returnValue(stackClearer);
spyOn(console, 'error');
env.cleanup_();
env = new privateUnderTest.Env();
env = new privateUnderTest.Env({ global });
const receivedErrors = [];
function logErrors(event) {
@@ -459,21 +392,9 @@ describe('Global error handling (integration)', function() {
describe('Handling unhandled promise rejections', function() {
it('routes unhandled promise rejections to the running spec', async function() {
const global = {
...browserEventMethods(),
setTimeout: function(fn, delay) {
return setTimeout(fn, delay);
},
clearTimeout: function(fn, delay) {
clearTimeout(fn, delay);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
}
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
const global = mockGlobal();
env.cleanup_();
env = new privateUnderTest.Env();
env = new privateUnderTest.Env({ global });
const reporter = jasmine.createSpyObj('fakeReporter', [
'specDone',
'suiteDone'
@@ -502,24 +423,12 @@ describe('Global error handling (integration)', function() {
describe('When the most recently running spec has reported specDone', function() {
it('routes unhandled promise rejections to an ancestor suite', async function() {
const global = {
...browserEventMethods(),
setTimeout: function(fn, delay) {
return setTimeout(fn, delay);
},
clearTimeout: function(fn) {
clearTimeout(fn);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
}
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
const realClearStack = privateUnderTest.getClearStack(global);
const global = mockGlobal();
const stackClearer = privateUnderTest.getStackClearer(global);
const realClearStack = stackClearer.clearStack;
const clearStackCallbacks = {};
let clearStackCallCount = 0;
spyOn(privateUnderTest, 'getClearStack').and.returnValue(function(fn) {
spyOn(stackClearer, 'clearStack').and.callFake(function(fn) {
clearStackCallCount++;
if (clearStackCallbacks[clearStackCallCount]) {
@@ -528,9 +437,12 @@ describe('Global error handling (integration)', function() {
realClearStack(fn);
});
spyOn(privateUnderTest, 'getStackClearer').and.returnValue(
stackClearer
);
env.cleanup_();
env = new privateUnderTest.Env();
env = new privateUnderTest.Env({ global });
let suiteErrors = [];
env.addReporter({
@@ -566,21 +478,9 @@ describe('Global error handling (integration)', function() {
});
it('routes unhandled promise rejections to the running suite', async function() {
const global = {
...browserEventMethods(),
setTimeout: function(fn, delay) {
return setTimeout(fn, delay);
},
clearTimeout: function(fn, delay) {
clearTimeout(fn, delay);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
}
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
const global = mockGlobal();
env.cleanup_();
env = new privateUnderTest.Env();
env = new privateUnderTest.Env({ global });
const reporter = jasmine.createSpyObj('fakeReporter', [
'specDone',
'suiteDone'
@@ -621,24 +521,12 @@ describe('Global error handling (integration)', function() {
describe('When the most recently suite has reported suiteDone', function() {
it('routes unhandled promise rejections to an ancestor suite', async function() {
const global = {
...browserEventMethods(),
setTimeout: function(fn, delay) {
return setTimeout(fn, delay);
},
clearTimeout: function(fn, delay) {
clearTimeout(fn, delay);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
}
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
const realClearStack = privateUnderTest.getClearStack(global);
const global = mockGlobal();
const stackClearer = privateUnderTest.getStackClearer(global);
const realClearStack = stackClearer.clearStack;
const clearStackCallbacks = {};
let clearStackCallCount = 0;
spyOn(privateUnderTest, 'getClearStack').and.returnValue(function(fn) {
spyOn(stackClearer, 'clearStack').and.callFake(function(fn) {
clearStackCallCount++;
if (clearStackCallbacks[clearStackCallCount]) {
@@ -647,9 +535,12 @@ describe('Global error handling (integration)', function() {
realClearStack(fn);
});
spyOn(privateUnderTest, 'getStackClearer').and.returnValue(
stackClearer
);
env.cleanup_();
env = new privateUnderTest.Env();
env = new privateUnderTest.Env({ global });
let suiteErrors = [];
env.addReporter({
@@ -689,21 +580,9 @@ describe('Global error handling (integration)', function() {
describe('When the env has started reporting jasmineDone', function() {
it('logs the rejection to the console', async function() {
const global = {
...browserEventMethods(),
setTimeout: function(fn, delay) {
return setTimeout(fn, delay);
},
clearTimeout: function(fn, delay) {
clearTimeout(fn, delay);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
}
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
const global = mockGlobal();
env.cleanup_();
env = new privateUnderTest.Env();
env = new privateUnderTest.Env({ global });
spyOn(console, 'error');
@@ -734,26 +613,14 @@ describe('Global error handling (integration)', function() {
});
it('routes all unhandled promise rejections that occur during stack clearing somewhere', async function() {
const global = {
...browserEventMethods(),
setTimeout: function(fn, delay) {
return setTimeout(fn, delay);
},
clearTimeout: function(fn) {
clearTimeout(fn);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
}
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
const realClearStack = privateUnderTest.getClearStack(global);
const global = mockGlobal();
const stackClearer = privateUnderTest.getStackClearer(global);
const realClearStack = stackClearer.clearStack;
let clearStackCallCount = 0;
let jasmineDone = false;
const expectedErrors = [];
const expectedErrorsAfterJasmineDone = [];
spyOn(privateUnderTest, 'getClearStack').and.returnValue(function(fn) {
spyOn(stackClearer, 'clearStack').and.callFake(function(fn) {
clearStackCallCount++;
const reason = `Error in clearStack #${clearStackCallCount}`;
const expectedMsg = `Unhandled promise rejection: ${reason} thrown`;
@@ -767,10 +634,11 @@ describe('Global error handling (integration)', function() {
dispatchErrorEvent(global, 'unhandledrejection', { reason });
realClearStack(fn);
});
spyOn(privateUnderTest, 'getStackClearer').and.returnValue(stackClearer);
spyOn(console, 'error');
env.cleanup_();
env = new privateUnderTest.Env();
env = new privateUnderTest.Env({ global });
const receivedErrors = [];
function logErrors(event) {
@@ -819,21 +687,9 @@ describe('Global error handling (integration)', function() {
let global, reporter;
beforeEach(function() {
global = {
...browserEventMethods(),
setTimeout: function(fn, delay) {
return setTimeout(fn, delay);
},
clearTimeout: function(fn, delay) {
clearTimeout(fn, delay);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
}
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
global = mockGlobal();
env.cleanup_();
env = new privateUnderTest.Env();
env = new privateUnderTest.Env({ global });
env.configure({ detectLateRejectionHandling: true });
reporter = jasmine.createSpyObj('fakeReporter', [
@@ -966,21 +822,8 @@ describe('Global error handling (integration)', function() {
describe("When the unhandled rejection event doesn't have a promise", function() {
it('reports the rejection', async function() {
const global = {
...browserEventMethods(),
setTimeout: function(fn, delay) {
return setTimeout(fn, delay);
},
clearTimeout: function(fn, delay) {
clearTimeout(fn, delay);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
}
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
env.cleanup_();
env = new privateUnderTest.Env();
const global = mockGlobal();
env = new privateUnderTest.Env({ global });
env.configure({ detectLateRejectionHandling: true });
const reporter = jasmine.createSpyObj('fakeReporter', [
'specDone',
@@ -1012,21 +855,9 @@ describe('Global error handling (integration)', function() {
});
it('works when the suite is run multiple times', async function() {
const global = {
...browserEventMethods(),
setTimeout: function(fn, delay) {
return setTimeout(fn, delay);
},
clearTimeout: function(fn, delay) {
clearTimeout(fn, delay);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
}
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
const global = mockGlobal();
env.cleanup_();
env = new privateUnderTest.Env();
env = new privateUnderTest.Env({ global });
env.configure({ autoCleanClosures: false });
const reporter = jasmine.createSpyObj('fakeReporter', ['specDone']);

View File

@@ -30,6 +30,7 @@ describe('The jasmine namespace', function() {
'version',
// Asymmetric equality testers
'allOf',
'any',
'anything',
'arrayContaining',

View File

@@ -0,0 +1,5 @@
(function() {
specHelpers.callerFilenameShim = function(fn) {
return fn();
};
})();

View File

@@ -167,18 +167,18 @@ describe('HtmlReporterV2', function() {
'.jasmine-alert .jasmine-bar'
);
expect(alertBars.length).toEqual(4);
expect(alertBars[1].innerHTML).toMatch(
expect(alertBars.length).toEqual(5);
expect(alertBars[2].innerHTML).toMatch(
/spec deprecation.*\(in spec: a spec with a deprecation\)/
);
expect(alertBars[1].getAttribute('class')).toEqual(
expect(alertBars[2].getAttribute('class')).toEqual(
'jasmine-bar jasmine-warning'
);
expect(alertBars[2].innerHTML).toMatch(
expect(alertBars[3].innerHTML).toMatch(
/suite deprecation.*\(in suite: a suite with a deprecation\)/
);
expect(alertBars[3].innerHTML).toMatch(/global deprecation/);
expect(alertBars[3].innerHTML).not.toMatch(/in /);
expect(alertBars[4].innerHTML).toMatch(/global deprecation/);
expect(alertBars[4].innerHTML).not.toMatch(/in /);
});
it('displays expandable stack traces', function() {
@@ -259,6 +259,214 @@ describe('HtmlReporterV2', function() {
});
});
describe('The tab bar', function() {
function checkHidden(tabs, expected) {
const actual = Array.from(tabs).map(t =>
t.classList.contains('jasmine-hidden')
);
expect(actual)
.withContext('tab hiddenness')
.toEqual(expected);
}
describe('while Jasmine is running', function() {
it('hides all tabs', function() {
const reporter = setup();
reporter.initialize();
reporter.jasmineStarted({ totalSpecsDefined: 0 });
const tabs = container.querySelectorAll('.jasmine-tab');
expect(tabs.length).toEqual(3);
expect(tabs[0].textContent).toEqual('Spec List');
expect(tabs[1].textContent).toEqual('Failures');
expect(tabs[2].textContent).toEqual('Performance');
checkHidden(tabs, [true, true, true]);
// Results, even failures, should not show any tabs
reporter.specDone({
id: 1,
description: 'a failing spec',
fullName: 'a failing spec',
status: 'failed',
failedExpectations: [{}],
passedExpectations: []
});
checkHidden(tabs, [true, true, true]);
});
});
describe('when Jasmine is done', function() {
function hasSpecOrSuiteFailureBehavior(reportEvents) {
let reporter;
beforeEach(function() {
reporter = setup();
reporter.initialize();
reportEvents(reporter);
});
it('shows all three tabs', function() {
const tabs = container.querySelectorAll('.jasmine-tab');
checkHidden(tabs, [false, false, false]);
});
it('selects the Failures tab', function() {
const reporterNode = container.querySelector(
'.jasmine_html-reporter'
);
expect(reporterNode).toHaveClass('jasmine-failure-list');
});
it('switches between failure details and the spec summary', function() {
const tabs = container.querySelectorAll('.jasmine-tab');
let specListLink = () => tabs[0].querySelector('a');
let failuresLink = () => tabs[1].querySelector('a');
const reporterNode = container.querySelector(
'.jasmine_html-reporter'
);
expect(specListLink().textContent).toEqual('Spec List');
expect(failuresLink())
.withContext('failures link')
.toBeFalsy();
specListLink().click();
expect(reporterNode).toHaveClass('jasmine-spec-list');
expect(reporterNode).not.toHaveClass('jasmine-failure-list');
expect(specListLink())
.withContext('spec list link')
.toBeFalsy();
expect(failuresLink().textContent).toEqual('Failures');
failuresLink().click();
expect(reporterNode.getAttribute('class')).toMatch(
'jasmine-failure-list'
);
expect(failuresLink())
.withContext('failures link')
.toBeFalsy();
expect(specListLink().textContent).toEqual('Spec List');
expect(reporterNode).toHaveClass('jasmine-failure-list');
expect(reporterNode).not.toHaveClass('jasmine-spec-list');
});
}
function hasSpecAndSuiteSuccessBehavior(reportEvents) {
let reporter;
beforeEach(function() {
reporter = setup();
reporter.initialize();
reportEvents(reporter);
});
it('shows the Spec List and Performance tabs', function() {
const tabs = container.querySelectorAll('.jasmine-tab');
checkHidden(tabs, [false, true, false]);
});
it('shows the spec list view', function() {
const reporterNode = container.querySelector(
'.jasmine_html-reporter'
);
expect(reporterNode).toHaveClass('jasmine-spec-list');
expect(reporterNode).not.toHaveClass('jasmine-failure-list');
});
}
describe('with spec failures', function() {
hasSpecOrSuiteFailureBehavior(function(reporter) {
reporter.jasmineStarted({ totalSpecsDefined: 0 });
reporter.specDone({
id: 1,
description: 'a failing spec',
fullName: 'a failing spec',
status: 'failed',
failedExpectations: [{}],
passedExpectations: []
});
reporter.specDone({
id: 2,
description: 'a passing spec',
fullName: 'a passing spec',
status: 'passed',
failedExpectations: [],
passedExpectations: []
});
reporter.jasmineDone({});
});
});
describe('with suite failures', function() {
hasSpecOrSuiteFailureBehavior(function(reporter) {
reporter.jasmineStarted({ totalSpecsDefined: 0 });
reporter.specDone({
id: 1,
description: 'a failing spec',
fullName: 'a failing spec',
status: 'failed',
failedExpectations: [{}],
passedExpectations: []
});
reporter.specDone({
id: 2,
description: 'a passing spec',
fullName: 'a passing spec',
status: 'passed',
failedExpectations: [],
passedExpectations: []
});
reporter.jasmineDone({});
});
});
describe('without any failures', function() {
hasSpecAndSuiteSuccessBehavior(function(reporter) {
reporter.jasmineStarted({ totalSpecsDefined: 0 });
reporter.specDone({
id: 1,
description: 'a passing spec',
fullName: 'a passing spec',
status: 'passed',
failedExpectations: [],
passedExpectations: []
});
reporter.suiteDone({ id: 1 });
reporter.jasmineDone({});
});
});
describe('with only top suite failures', function() {
// Top suite failures are displayed in their own alert bars, so they
// don't cause the failures tab to be shown.
hasSpecAndSuiteSuccessBehavior(function(reporter) {
reporter.jasmineStarted({ totalSpecsDefined: 0 });
reporter.jasmineDone({
failedExpectations: [{}]
});
});
});
it('shows the slow spec view when the Performance tab is clicked', function() {
const reporter = setup();
reporter.initialize();
reporter.jasmineStarted({ totalSpecsDefined: 0 });
reporter.specDone({
duration: 1.2,
failedExpectations: [],
passedExpectations: []
});
reporter.jasmineDone({});
const tabs = container.querySelectorAll('.jasmine-tab');
let perfLink = tabs[2].querySelector('a');
const reporterNode = container.querySelector('.jasmine_html-reporter');
expect(perfLink.textContent).toEqual('Performance');
perfLink.click();
expect(reporterNode).toHaveClass('jasmine-performance');
expect(reporterNode.innerHTML).toContain('<h2>Performance</h2>');
expect(reporterNode.innerHTML).toContain('<td>1.2ms</td>');
});
});
});
describe('when Jasmine is done', function() {
it('adds a warning to the link title of specs that have no expectations', function() {
const reporter = setup();
@@ -449,21 +657,18 @@ describe('HtmlReporterV2', function() {
]
});
const alertBars = container.querySelectorAll(
'.jasmine-alert .jasmine-bar'
const errorBars = container.querySelectorAll(
'.jasmine-alert .jasmine-bar.jasmine-errored'
);
expect(alertBars.length).toEqual(3);
expect(alertBars[1].getAttribute('class')).toEqual(
'jasmine-bar jasmine-errored'
);
expect(alertBars[1].innerHTML).toMatch(
expect(errorBars.length).toEqual(2);
expect(errorBars[0].innerHTML).toMatch(
/AfterAll Global After All Failure/
);
expect(alertBars[2].innerHTML).toMatch(
expect(errorBars[1].innerHTML).toMatch(
/Error during loading: Your JS is borken/
);
expect(alertBars[2].innerHTML).not.toMatch(/line/);
expect(errorBars[1].innerHTML).not.toMatch(/line/);
});
it('does not display the "AfterAll" prefix for other error types', function() {
@@ -482,16 +687,16 @@ describe('HtmlReporterV2', function() {
]
});
const alertBars = container.querySelectorAll(
'.jasmine-alert .jasmine-bar'
const errorBars = container.querySelectorAll(
'.jasmine-alert .jasmine-bar.jasmine-errored'
);
expect(alertBars.length).toEqual(4);
expect(alertBars[1].textContent).toContain('load error');
expect(alertBars[2].textContent).toContain('lateExpectation error');
expect(alertBars[3].textContent).toContain('lateError error');
expect(errorBars.length).toEqual(3);
expect(errorBars[0].textContent).toContain('load error');
expect(errorBars[1].textContent).toContain('lateExpectation error');
expect(errorBars[2].textContent).toContain('lateError error');
for (let bar of alertBars) {
for (let bar of errorBars) {
expect(bar.textContent).not.toContain('AfterAll');
}
});
@@ -512,12 +717,12 @@ describe('HtmlReporterV2', function() {
]
});
const alertBars = container.querySelectorAll(
'.jasmine-alert .jasmine-bar'
const alertBar = container.querySelector(
'.jasmine-alert .jasmine-bar.jasmine-errored'
);
expect(alertBars.length).toEqual(2);
expect(alertBars[1].innerHTML).toMatch(
expect(alertBar).toBeTruthy();
expect(alertBar.innerHTML).toMatch(
/Error during loading: Your JS is borken in some\/file.js line 42/
);
});
@@ -758,12 +963,12 @@ describe('HtmlReporterV2', function() {
});
it('reports the specs counts', function() {
const alertBars = container.querySelectorAll(
'.jasmine-alert .jasmine-bar'
const resultBar = container.querySelector(
'.jasmine-alert .jasmine-bar.jasmine-overall-result'
);
expect(alertBars.length).toEqual(1);
expect(alertBars[0].innerHTML).toMatch(/2 specs, 0 failures/);
expect(resultBar).toBeTruthy();
expect(resultBar.innerHTML).toMatch(/2 specs, 0 failures/);
});
it('reports no failure details', function() {
@@ -1072,23 +1277,6 @@ describe('HtmlReporterV2', function() {
)}`
);
});
it('allows switching between failure details and the spec summary', function() {
const menuBar = container.querySelectorAll('.jasmine-bar')[1];
expect(menuBar.getAttribute('class')).not.toMatch(/hidden/);
const link = menuBar.querySelector('a');
expect(link.innerHTML).toEqual('Failures');
expect(link.getAttribute('href')).toEqual('#');
});
it("sets the reporter to 'Failures List' mode", function() {
const reporterNode = container.querySelector('.jasmine_html-reporter');
expect(reporterNode.getAttribute('class')).toMatch(
'jasmine-failure-list'
);
});
});
it('counts failures that are reported in the jasmineDone event', function() {

View File

@@ -1,48 +1,69 @@
describe('HtmlSpecFilterV2', function() {
it('matches everything when no string is provided', function() {
const specFilter = new privateUnderTest.HtmlSpecFilterV2({
filterString() {
return '';
}
});
describe('When both query parameters are falsy', function() {
it('matches everything', function() {
const specFilter = new privateUnderTest.HtmlSpecFilterV2({
filterParams() {
return { path: '', spec: '' };
}
});
expect(specFilter.matches({})).toBeTrue();
expect(specFilter.matches({})).toBeTrue();
});
});
it('matches a spec with the exact same path', function() {
const specFilter = new privateUnderTest.HtmlSpecFilterV2({
filterString() {
return '["a","b","c"]';
}
describe('When the path parameter is truthy', function() {
it('matches a spec with the exact same path', function() {
const specFilter = new privateUnderTest.HtmlSpecFilterV2({
filterParams() {
return { path: '["a","b","c"]', spec: '' };
}
});
expect(specFilter.matches(stubSpec(['a', 'b', 'c']))).toBeTrue();
});
expect(specFilter.matches(stubSpec(['a', 'b', 'c']))).toBeTrue();
});
it('matches a spec whose path has the filter path as a prefix', function() {
const specFilter = new privateUnderTest.HtmlSpecFilterV2({
filterParams() {
return { path: '["a","b"]', spec: '' };
}
});
it('matches a spec whose path has the filter path as a prefix', function() {
const specFilter = new privateUnderTest.HtmlSpecFilterV2({
filterString() {
return '["a","b"]';
}
expect(specFilter.matches(stubSpec(['a', 'b', 'c']))).toBeTrue();
});
expect(specFilter.matches(stubSpec(['a', 'b', 'c']))).toBeTrue();
});
it('does not match a spec with a different path', function() {
const specFilter = new privateUnderTest.HtmlSpecFilterV2({
filterParams() {
return { path: '["a","b","c"]', spec: '' };
}
});
it('does not match a spec with a different path', function() {
const specFilter = new privateUnderTest.HtmlSpecFilterV2({
filterString() {
return '["a","b","c"]';
}
expect(specFilter.matches(stubSpec(['a', 'd', 'c']))).toBeFalse();
});
expect(specFilter.matches(stubSpec(['a', 'd', 'c']))).toBeFalse();
});
function stubSpec(path) {
describe('When the path parameter is falsy and the spec parameter is truthy', function() {
it('matches specs with full names containing the parameter value', function() {
const specFilter = new privateUnderTest.HtmlSpecFilterV2({
filterParams() {
return { path: '', spec: 'bar' };
}
});
expect(specFilter.matches(stubSpec('', 'foo bar baz'))).toBeTrue();
expect(specFilter.matches(stubSpec('', 'foo baz'))).toBeFalse();
expect(specFilter.matches(stubSpec('', 'sandbars'))).toBeTrue();
});
});
function stubSpec(path, fullName) {
return {
getPath() {
return path;
},
getFullName() {
return fullName;
}
};
}

View File

@@ -0,0 +1,107 @@
'use strict';
describe('PerformanceView', function() {
it('shows specs ordered by execution time', function() {
const stateBuilder = new privateUnderTest.ResultsStateBuilder();
stateBuilder.suiteStarted({});
stateBuilder.specDone({
fullName: 'spec A',
duration: 2
});
stateBuilder.suiteDone({});
stateBuilder.specDone({
fullName: 'spec B',
duration: 1
});
stateBuilder.specDone({
fullName: 'spec C',
duration: 3
});
const subject = new privateUnderTest.PerformanceView();
subject.addResults(stateBuilder.topResults);
const rows = Array.from(subject.rootEl.querySelectorAll('tbody tr'));
const durations = rows.map(r => r.querySelectorAll('td')[0].textContent);
const names = rows.map(r => r.querySelectorAll('td')[1].textContent);
expect(names).toEqual(['spec C', 'spec A', 'spec B']);
expect(durations).toEqual(['3ms', '2ms', '1ms']);
});
it('shows at most 20 specs', function() {
const stateBuilder = new privateUnderTest.ResultsStateBuilder();
const subject = new privateUnderTest.PerformanceView();
for (let i = 0; i < 21; i++) {
stateBuilder.specDone({
fullName: `spec ${i}`,
duration: i
});
}
subject.addResults(stateBuilder.topResults);
expect(subject.rootEl.querySelectorAll('tbody tr').length).toEqual(20);
expect(subject.textContent).not.toContain('spec 0');
});
it('shows mean and median run times for an odd number of specs', function() {
const stateBuilder = new privateUnderTest.ResultsStateBuilder();
const subject = new privateUnderTest.PerformanceView();
stateBuilder.specDone({ duration: 1 });
stateBuilder.specDone({ duration: 2 });
stateBuilder.specDone({ duration: 5 });
subject.addResults(stateBuilder.topResults);
expect(subject.rootEl.textContent).toContain('Mean spec run time: 3ms');
expect(subject.rootEl.textContent).toContain('Median spec run time: 2ms');
});
it('shows mean and median run times for an even number of specs', function() {
const stateBuilder = new privateUnderTest.ResultsStateBuilder();
const subject = new privateUnderTest.PerformanceView();
stateBuilder.specDone({ duration: 1 });
stateBuilder.specDone({ duration: 3 });
stateBuilder.specDone({ duration: 10 });
stateBuilder.specDone({ duration: 2 });
subject.addResults(stateBuilder.topResults);
expect(subject.rootEl.textContent).toContain('Mean spec run time: 4ms');
expect(subject.rootEl.textContent).toContain('Median spec run time: 2ms');
});
it('copes with 0 specs', function() {
const stateBuilder = new privateUnderTest.ResultsStateBuilder();
const subject = new privateUnderTest.PerformanceView();
expect(function() {
subject.addResults(stateBuilder.topResults);
}).not.toThrow();
});
it('filters out excluded specs', function() {
const stateBuilder = new privateUnderTest.ResultsStateBuilder();
stateBuilder.specDone({
fullName: 'spec A',
duration: 2
});
stateBuilder.specDone({
fullName: 'spec B',
duration: 1,
status: 'excluded'
});
stateBuilder.specDone({
fullName: 'spec C',
duration: 3
});
const subject = new privateUnderTest.PerformanceView();
subject.addResults(stateBuilder.topResults);
const rows = Array.from(subject.rootEl.querySelectorAll('tbody tr'));
const names = rows.map(r => r.querySelectorAll('td')[1].textContent);
expect(names).toEqual(['spec C', 'spec A']);
expect(subject.rootEl.textContent).toContain('Mean spec run time: 3ms');
expect(subject.rootEl.textContent).toContain('Median spec run time: 2ms');
});
});

97
spec/html/TabBarSpec.js Normal file
View File

@@ -0,0 +1,97 @@
describe('TabBar', function() {
it('initially renders but hides the tabs', function() {
const subject = new privateUnderTest.TabBar([
{ id: 'tab1', label: 'tab 1' }
]);
const tabs = subject.rootEl.querySelectorAll('.jasmine-tab');
expect(tabs.length).toEqual(1);
expect(tabs[0].id).toEqual('tab1');
expect(tabs[0]).toHaveClass('jasmine-hidden');
const link = tabs[0].querySelector('a');
expect(link).toBeTruthy();
expect(link.textContent).toEqual('tab 1');
});
it('does not initially call the onSelect callback', function() {
const onSelect = jasmine.createSpy('onSelect');
new privateUnderTest.TabBar([{ id: 'tab1', label: '' }], onSelect);
expect(onSelect).not.toHaveBeenCalled();
});
describe('#showTab', function() {
it('shows the specified tab', function() {
const subject = new privateUnderTest.TabBar([
{ id: 'tab1' },
{ id: 'tab2' }
]);
subject.showTab('tab2');
const tabs = subject.rootEl.querySelectorAll('.jasmine-tab');
expect(tabs[0]).toHaveClass('jasmine-hidden');
expect(tabs[1]).not.toHaveClass('jasmine-hidden');
});
it('does not hide previously shown tabs', function() {
const subject = new privateUnderTest.TabBar([
{ id: 'tab1' },
{ id: 'tab2' }
]);
subject.showTab('tab1');
subject.showTab('tab2');
const tabs = subject.rootEl.querySelectorAll('.jasmine-tab');
expect(tabs[0]).not.toHaveClass('jasmine-hidden');
});
});
describe("When a tab's link is clicked", function() {
it("calls the onSelect callback with the tab's id", function() {
const onSelect = jasmine.createSpy('onSelect');
const subject = new privateUnderTest.TabBar(
[{ id: 'tab1', label: '' }],
onSelect
);
subject.rootEl.querySelector('.jasmine-tab a').click();
expect(onSelect).toHaveBeenCalledWith('tab1');
});
it('shows links on all non-selected tabs only', function() {
const subject = new privateUnderTest.TabBar(
[
{ id: 'tab1', label: 'tab 1' },
{ id: 'tab2', label: 'tab 2' },
{ id: 'tab3', label: 'tab 3' }
],
() => {}
);
subject.rootEl.querySelectorAll('.jasmine-tab a')[1].click();
let tabs = subject.rootEl.querySelectorAll('.jasmine-tab');
expect(tabs[0].querySelector('a'))
.withContext('tab 1')
.toBeTruthy();
expect(tabs[1].querySelector('a'))
.withContext('tab 1')
.toBeFalsy();
expect(tabs[2].querySelector('a'))
.withContext('tab 1')
.toBeTruthy();
subject.rootEl.querySelectorAll('.jasmine-tab a')[0].click();
tabs = subject.rootEl.querySelectorAll('.jasmine-tab');
expect(tabs[0].querySelector('a'))
.withContext('tab 1')
.toBeFalsy();
expect(tabs[1].querySelector('a'))
.withContext('tab 1')
.toBeTruthy();
expect(tabs[2].querySelector('a'))
.withContext('tab 1')
.toBeTruthy();
});
});
});

View File

@@ -23,11 +23,13 @@ module.exports = {
'helpers/BrowserFlags.js',
'helpers/domHelpers.js',
'helpers/integrationMatchers.js',
'helpers/callerFilenameShim.js',
'helpers/defineJasmineUnderTest.js',
'helpers/resetEnv.js'
],
env: {
forbidDuplicateNames: true
forbidDuplicateNames: true,
safariYieldStrategy: 'time'
},
random: true,
browser: {

View File

@@ -8,6 +8,7 @@
"helpers/init.js",
"helpers/domHelpers.js",
"helpers/integrationMatchers.js",
"helpers/callerFilenameShim.js",
"helpers/overrideConsoleLogForCircleCi.js",
"helpers/nodeDefineJasmineUnderTest.js",
"helpers/resetEnv.js"

View File

@@ -34,6 +34,12 @@
*/
env.addReporter(jsApiReporter);
env.addReporter(htmlReporter);
/**
* Configures Jasmine based on the current set of query parameters. This
* supports all parameters set by the HTML reporter as well as
* spec=partialPath, which filters out specs whose paths don't contain the
* parameter.
*/
env.configure(urls.configFromCurrentUrl());
/**

View File

@@ -128,7 +128,45 @@ getJasmineRequireObj().Configuration = function(j$) {
* @type Boolean
* @default false
*/
detectLateRejectionHandling: false
detectLateRejectionHandling: false,
/**
* The number of extra stack frames inserted by a wrapper around {@link it}
* or by some other local modification. Jasmine uses this to determine the
* filename for {@link SpecStartedEvent} and {@link SpecDoneEvent}.
* @name Configuration#extraItStackFrames
* @since 5.13.0
* @type number
* @default 0
*/
extraItStackFrames: 0,
/**
* The number of extra stack frames inserted by a wrapper around
* {@link describe} or by some other local modification. Jasmine uses this
* to determine the filename for {@link SpecStartedEvent} and
* {@link SpecDoneEvent}.
* @name Configuration#extraDescribeStackFrames
* @since 5.13.0
* @type number
* @default 0
*/
extraDescribeStackFrames: 0,
/**
* The strategy to use in Safari and similar browsers to determine how often
* to yield control by calling setTimeout. If set to "count", the default,
* the frequency of setTimeout calls is based on the number of relevant
* function calls. If set to "time", the frequency of setTimeout calls is
* based on elapsed time. Using "time" may provide a significant performance
* improvement, but as of 6.0 it hasn't been tested with a wide variety of
* workloads and should be considered experimental.
* @name Configuration#safariYieldStrategy
* @since 6.0.0
* @type 'count' | 'time'
* @default 'count'
*/
safariYieldStrategy: 'count'
};
Object.freeze(defaultConfig);
@@ -179,6 +217,28 @@ getJasmineRequireObj().Configuration = function(j$) {
if (typeof changes.seed !== 'undefined') {
this.#values.seed = changes.seed;
}
// 0 is a valid value for both of these, so a truthiness check wouldn't work
if (typeof changes.extraItStackFrames !== 'undefined') {
this.#values.extraItStackFrames = changes.extraItStackFrames;
}
if (typeof changes.extraDescribeStackFrames !== 'undefined') {
this.#values.extraDescribeStackFrames =
changes.extraDescribeStackFrames;
}
if (typeof changes.safariYieldStrategy !== 'undefined') {
const v = changes.safariYieldStrategy;
if (v === 'count' || v === 'time') {
this.#values.safariYieldStrategy = v;
} else {
throw new Error(
"Invalid safariYieldStrategy value. Valid values are 'count' and 'time'."
);
}
}
}
}

View File

@@ -1,6 +1,8 @@
getJasmineRequireObj().Env = function(j$) {
'use strict';
const DEFAULT_IT_DESCRIBE_STACK_DEPTH = 3;
/**
* @class Env
* @since 2.0.0
@@ -18,7 +20,7 @@ getJasmineRequireObj().Env = function(j$) {
const realSetTimeout = global.setTimeout;
const realClearTimeout = global.clearTimeout;
const clearStack = j$.private.getClearStack(global);
const stackClearer = j$.private.getStackClearer(global);
this.clock = new j$.private.Clock(
global,
function() {
@@ -28,7 +30,7 @@ getJasmineRequireObj().Env = function(j$) {
);
const globalErrors = new GlobalErrors(
undefined,
global,
// Configuration is late-bound because GlobalErrors needs to be constructed
// before it's set to detect load-time errors in browsers
() => this.configuration()
@@ -97,6 +99,7 @@ getJasmineRequireObj().Env = function(j$) {
config.update(changes);
deprecator.verboseDeprecations(config.verboseDeprecations);
stackClearer.setSafariYieldStrategy(config.safariYieldStrategy);
};
/**
@@ -299,7 +302,7 @@ getJasmineRequireObj().Env = function(j$) {
};
function runQueue(options) {
options.clearStack = options.clearStack || clearStack;
options.clearStack = options.clearStack || stackClearer;
options.timeout = {
setTimeout: realSetTimeout,
clearTimeout: realClearTimeout
@@ -591,14 +594,14 @@ getJasmineRequireObj().Env = function(j$) {
this.describe = function(description, definitionFn) {
ensureIsNotNested('describe');
const filename = callerCallerFilename();
const filename = indirectCallerFilename(describeStackDepth());
return suiteBuilder.describe(description, definitionFn, filename)
.metadata;
};
this.xdescribe = function(description, definitionFn) {
ensureIsNotNested('xdescribe');
const filename = callerCallerFilename();
const filename = indirectCallerFilename(describeStackDepth());
return suiteBuilder.xdescribe(description, definitionFn, filename)
.metadata;
};
@@ -606,30 +609,38 @@ getJasmineRequireObj().Env = function(j$) {
this.fdescribe = function(description, definitionFn) {
ensureIsNotNested('fdescribe');
ensureNonParallel('fdescribe');
const filename = callerCallerFilename();
const filename = indirectCallerFilename(describeStackDepth());
return suiteBuilder.fdescribe(description, definitionFn, filename)
.metadata;
};
this.it = function(description, fn, timeout) {
ensureIsNotNested('it');
const filename = callerCallerFilename();
const filename = indirectCallerFilename(itStackDepth());
return suiteBuilder.it(description, fn, timeout, filename).metadata;
};
this.xit = function(description, fn, timeout) {
ensureIsNotNested('xit');
const filename = callerCallerFilename();
const filename = indirectCallerFilename(itStackDepth());
return suiteBuilder.xit(description, fn, timeout, filename).metadata;
};
this.fit = function(description, fn, timeout) {
ensureIsNotNested('fit');
ensureNonParallel('fit');
const filename = callerCallerFilename();
const filename = indirectCallerFilename(itStackDepth());
return suiteBuilder.fit(description, fn, timeout, filename).metadata;
};
function itStackDepth() {
return DEFAULT_IT_DESCRIBE_STACK_DEPTH + config.extraItStackFrames;
}
function describeStackDepth() {
return DEFAULT_IT_DESCRIBE_STACK_DEPTH + config.extraDescribeStackFrames;
}
/**
* Get a user-defined property as part of the properties field of {@link SpecDoneEvent}
* @name Env#getSpecProperty
@@ -815,11 +826,12 @@ getJasmineRequireObj().Env = function(j$) {
};
}
function callerCallerFilename() {
function indirectCallerFilename(depth) {
const frames = new j$.private.StackTrace(new Error()).frames;
// frames[3] should always exist except in Jasmine's own tests, which bypass
// the global it/describe layer, but don't crash if it doesn't.
return frames[3] && frames[3].file;
// The specified frame should always exist except in Jasmine's own tests,
// which bypass the global it/describe layer, but could be absent in case
// of misconfiguration. Don't crash if it's absent.
return frames[depth] && frames[depth].file;
}
return Env;

View File

@@ -51,11 +51,11 @@ getJasmineRequireObj().QueueRunner = function(j$) {
}
this.onComplete = attrs.onComplete || emptyFn;
this.clearStack =
attrs.clearStack ||
function(fn) {
this.clearStack = attrs.clearStack || {
clearStack(fn) {
fn();
};
}
};
this.onException = attrs.onException || emptyFn;
this.onMultipleDone = attrs.onMultipleDone || fallbackOnMultipleDone;
this.userContext = attrs.userContext || new j$.private.UserContext();
@@ -235,7 +235,7 @@ getJasmineRequireObj().QueueRunner = function(j$) {
}
}
this.clearStack(() => {
this.clearStack.clearStack(() => {
this.globalErrors.popListener(this.handleFinalError);
if (this.errored_) {

View File

@@ -72,8 +72,11 @@ getJasmineRequireObj().Spec = function(j$) {
// Key and value will eventually be cloned during reporting. The error
// thrown at that point if they aren't cloneable isn't very helpful.
// Throw a better one now.
j$.private.util.assertReporterCloneable(key, 'Key');
if (!j$.private.isString(key)) {
throw new Error('Key must be a string');
}
j$.private.util.assertReporterCloneable(value, 'Value');
this.#executionState.properties = this.#executionState.properties || {};
this.#executionState.properties[key] = value;
}
@@ -153,12 +156,11 @@ getJasmineRequireObj().Spec = function(j$) {
* @property {String} description - The description passed to the {@link it} that created this spec.
* @property {String} fullName - The full description including all ancestors of this spec.
* @property {String|null} parentSuiteId - The ID of the suite containing this spec, or null if this spec is not in a describe().
* @property {String} filename - Deprecated. The name of the file the spec was defined in.
* @property {String} filename - The name of the file the spec was defined in.
* Note: The value may be incorrect if zone.js is installed or
* `it`/`fit`/`xit` have been replaced with versions that don't maintain the
* same call stack height as the originals. This property may be removed in
* a future version unless there is enough user interest in keeping it.
* See {@link https://github.com/jasmine/jasmine/issues/2065}.
* same call stack height as the originals. You can fix that by setting
* {@link Configuration#extraItStackFrames}.
* @property {ExpectationResult[]} failedExpectations - The list of expectations that failed during execution of this spec.
* @property {ExpectationResult[]} passedExpectations - The list of expectations that passed during execution of this spec.
* @property {ExpectationResult[]} deprecationWarnings - The list of deprecation warnings that occurred during execution this spec.
@@ -340,7 +342,20 @@ getJasmineRequireObj().Spec = function(j$) {
* @returns {Array.<string>}
* @since 5.7.0
*/
getPath: this.getPath.bind(this)
getPath: this.getPath.bind(this),
/**
* The name of the file the spec was defined in.
* Note: The value may be incorrect if zone.js is installed or
* `it`/`fit`/`xit` have been replaced with versions that don't maintain the
* same call stack height as the originals. You can fix that by setting
* {@link Configuration#extraItStackFrames}.
* @name Spec#filename
* @readonly
* @type {string}
* @since 5.13.0
*/
filename: this.filename
};
}

View File

@@ -1,20 +1,48 @@
getJasmineRequireObj().clearStack = function(j$) {
getJasmineRequireObj().StackClearer = function(j$) {
'use strict';
const maxInlineCallCount = 10;
// 25ms gives a good balance of speed and UI responsiveness when running
// jasmine-core's own tests in Safari 18. The exact value isn't critical.
const safariYieldIntervalMs = 25;
function browserQueueMicrotaskImpl(global) {
const unclampedSetTimeout = getUnclampedSetTimeout(global);
const { queueMicrotask } = global;
let currentCallCount = 0;
return function clearStack(fn) {
currentCallCount++;
let yieldStrategy = 'count';
let currentCallCount = 0; // for count strategy
let nextSetTimeoutTime; // for time strategy
if (currentCallCount < maxInlineCallCount) {
queueMicrotask(fn);
} else {
currentCallCount = 0;
unclampedSetTimeout(fn);
return {
clearStack(fn) {
currentCallCount++;
let shouldSetTimeout;
if (yieldStrategy === 'time') {
const now = new Date().getTime();
shouldSetTimeout = now >= nextSetTimeoutTime;
if (shouldSetTimeout) {
nextSetTimeoutTime = now + safariYieldIntervalMs;
}
} else {
shouldSetTimeout = currentCallCount >= maxInlineCallCount;
if (shouldSetTimeout) {
currentCallCount = 0;
}
}
if (shouldSetTimeout) {
unclampedSetTimeout(fn);
} else {
queueMicrotask(fn);
}
},
setSafariYieldStrategy(strategy) {
yieldStrategy = strategy;
if (yieldStrategy === 'time') {
nextSetTimeoutTime = new Date().getTime() + safariYieldIntervalMs;
}
}
};
}
@@ -22,8 +50,11 @@ getJasmineRequireObj().clearStack = function(j$) {
function nodeQueueMicrotaskImpl(global) {
const { queueMicrotask } = global;
return function(fn) {
queueMicrotask(fn);
return {
clearStack(fn) {
queueMicrotask(fn);
},
setSafariYieldStrategy() {}
};
}
@@ -32,15 +63,19 @@ getJasmineRequireObj().clearStack = function(j$) {
const postMessage = getPostMessage(global);
let currentCallCount = 0;
return function clearStack(fn) {
currentCallCount++;
if (currentCallCount < maxInlineCallCount) {
postMessage(fn);
} else {
currentCallCount = 0;
setTimeout(fn);
}
return {
clearStack(fn) {
currentCallCount++;
if (currentCallCount < maxInlineCallCount) {
postMessage(fn);
} else {
currentCallCount = 0;
setTimeout(fn);
}
},
setSafariYieldStrategy() {}
};
}
@@ -88,7 +123,7 @@ getJasmineRequireObj().clearStack = function(j$) {
};
}
function getClearStack(global) {
function getStackClearer(global) {
const NODE_JS =
global.process &&
global.process.versions &&
@@ -106,12 +141,10 @@ getJasmineRequireObj().clearStack = function(j$) {
// Unlike browsers, Node doesn't require us to do a periodic setTimeout
// so we avoid the overhead.
return nodeQueueMicrotaskImpl(global);
} else if (SAFARI_OR_WIN_WEBKIT || !global.MessageChannel /* tests */) {
} else if (SAFARI_OR_WIN_WEBKIT) {
// 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);
} else {
// MessageChannel is faster than queueMicrotask in supported browsers
@@ -120,5 +153,5 @@ getJasmineRequireObj().clearStack = function(j$) {
}
}
return getClearStack;
return getStackClearer;
};

View File

@@ -37,8 +37,11 @@ getJasmineRequireObj().Suite = function(j$) {
// Key and value will eventually be cloned during reporting. The error
// thrown at that point if they aren't cloneable isn't very helpful.
// Throw a better one now.
j$.private.util.assertReporterCloneable(key, 'Key');
if (!j$.private.isString(key)) {
throw new Error('Key must be a string');
}
j$.private.util.assertReporterCloneable(value, 'Value');
this.#result.properties = this.#result.properties || {};
this.#result.properties[key] = value;
}
@@ -146,12 +149,11 @@ getJasmineRequireObj().Suite = function(j$) {
* @property {String} description - The description text passed to the {@link describe} that made this suite.
* @property {String} fullName - The full description including all ancestors of this suite.
* @property {String|null} parentSuiteId - The ID of the suite containing this suite, or null if this is not in another describe().
* @property {String} filename - Deprecated. The name of the file the suite was defined in.
* @property {String} filename - The name of the file the suite was defined in.
* Note: The value may be incorrect if zone.js is installed or
* `describe`/`fdescribe`/`xdescribe` have been replaced with versions that
* don't maintain the same call stack height as the originals. This property
* may be removed in a future version unless there is enough user interest
* in keeping it. See {@link https://github.com/jasmine/jasmine/issues/2065}.
* don't maintain the same call stack height as the originals. You can fix
* that by setting {@link Configuration#extraDescribeStackFrames}.
* @property {ExpectationResult[]} failedExpectations - The list of expectations that failed in an {@link afterAll} for this suite.
* @property {ExpectationResult[]} deprecationWarnings - The list of deprecation warnings that occurred on this suite.
* @property {String} status - Once the suite has completed, this string represents the pass/fail status of this suite.
@@ -359,6 +361,19 @@ getJasmineRequireObj().Suite = function(j$) {
* @since 2.0.0
*/
this.description = suite.description;
/**
* The name of the file the suite was defined in.
* Note: The value may be incorrect if zone.js is installed or
* `describe`/`fdescribe`/`xdescribe` have been replaced with versions
* that don't maintain the same call stack height as the originals. You
* can fix that by setting {@link Configuration#extraItStackFrames}.
* @name Suite#filename
* @readonly
* @type {string}
* @since 5.13.0
*/
this.filename = suite.filename;
}
/**

View File

@@ -49,7 +49,10 @@ getJasmineRequireObj().TreeRunner = function(j$) {
_executeSpec(spec, specOverallDone) {
const onStart = next => {
this.#currentRunableTracker.setCurrentSpec(spec);
this.#runableResources.initForRunable(spec.id, spec.parentSuiteId);
this.#runableResources.initForRunable(
spec.id,
spec.parentSuiteId || this.#executionTree.topSuite.id
);
this.#reportDispatcher.specStarted(spec.startedEvent()).then(next);
};
const resultCallback = (result, next) => {

View File

@@ -0,0 +1,29 @@
getJasmineRequireObj().AllOf = function(j$) {
'use strict';
function AllOf() {
const expectedValues = Array.from(arguments);
if (expectedValues.length === 0) {
throw new TypeError(
'jasmine.allOf() expects at least one argument to be passed.'
);
}
this.expectedValues = expectedValues;
}
AllOf.prototype.asymmetricMatch = function(other, matchersUtil) {
for (const expectedValue of this.expectedValues) {
if (!matchersUtil.equals(other, expectedValue)) {
return false;
}
}
return true;
};
AllOf.prototype.jasmineToString = function(pp) {
return '<jasmine.allOf(' + pp(this.expectedValues) + ')>';
};
return AllOf;
};

View File

@@ -229,6 +229,19 @@ getJasmineRequireObj().base = function(j$, jasmineGlobal) {
);
};
/**
* Get an {@link AsymmetricEqualityTester} that will succeed if the actual
* value being compared matches every provided equality tester.
* @name asymmetricEqualityTesters.allOf
* @emittedName jasmine.allOf
* @since 5.13.0
* @function
* @param {...*} arguments - The asymmetric equality checkers to compare.
*/
j$.allOf = function() {
return new j$.AllOf(...arguments);
};
/**
* Get an {@link AsymmetricEqualityTester} that will succeed if the actual
* value being compared is an instance of the specified class/constructor.

View File

@@ -7,6 +7,7 @@ getJasmineRequireObj().reporterEvents = function(j$) {
* {@link ReporterCapabilities} will apply.
* @name Reporter#reporterCapabilities
* @type ReporterCapabilities | undefined
* @optional
* @since 5.0
*/
/**
@@ -73,6 +74,7 @@ getJasmineRequireObj().reporterEvents = function(j$) {
/**
* `specStarted` is invoked when an `it` starts to run (including associated `beforeEach` functions)
* @function
* @optional
* @name Reporter#specStarted
* @param {SpecStartedEvent} 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.

View File

@@ -26,11 +26,12 @@ var getJasmineRequireObj = (function() {
j$.private.util = jRequire.util(j$);
j$.private.errors = jRequire.errors();
j$.private.formatErrorMsg = jRequire.formatErrorMsg(j$);
j$.private.AllOf = jRequire.AllOf(j$);
j$.private.Any = jRequire.Any(j$);
j$.private.Anything = jRequire.Anything(j$);
j$.private.CallTracker = jRequire.CallTracker(j$);
j$.private.MockDate = jRequire.MockDate(j$);
j$.private.getClearStack = jRequire.clearStack(j$);
j$.private.getStackClearer = jRequire.StackClearer(j$);
j$.private.Clock = jRequire.Clock();
j$.private.DelayedFunctionScheduler = jRequire.DelayedFunctionScheduler(j$);
j$.private.Deprecator = jRequire.Deprecator(j$);

View File

@@ -24,6 +24,7 @@ jasmineRequire.AlertsView = function(j$) {
);
}
// TODO: remove this once HtmlReporterV2 doesn't use it
addFailureToggle(onClickFailures, onClickSpecList) {
const failuresLink = createDom(
'a',
@@ -46,14 +47,20 @@ jasmineRequire.AlertsView = function(j$) {
return false;
};
this.#createAndAdd('jasmine-menu jasmine-bar jasmine-spec-list', [
createDom('span', {}, 'Spec List | '),
failuresLink
]);
this.#createAndAdd('jasmine-menu jasmine-bar jasmine-failure-list', [
specListLink,
createDom('span', {}, ' | Failures ')
]);
this.rootEl.appendChild(
createDom(
'span',
{ className: 'jasmine-menu jasmine-bar jasmine-spec-list' },
[createDom('span', {}, 'Spec List | '), failuresLink]
)
);
this.rootEl.appendChild(
createDom(
'span',
{ className: 'jasmine-menu jasmine-bar jasmine-failure-list' },
[specListLink, createDom('span', {}, ' | Failures ')]
)
);
}
addGlobalFailure(failure) {

View File

@@ -142,16 +142,52 @@ jasmineRequire.HtmlReporter = function(j$) {
results.appendChild(summary.rootEl);
if (this.#stateBuilder.anyNonTopSuiteFailures) {
this.#alerts.addFailureToggle(
() => this.#setMenuModeTo('jasmine-failure-list'),
() => this.#setMenuModeTo('jasmine-spec-list')
);
this.#addFailureToggle();
this.#setMenuModeTo('jasmine-failure-list');
this.#failures.show();
}
}
#addFailureToggle() {
const onClickFailures = () => this.#setMenuModeTo('jasmine-failure-list');
const onClickSpecList = () => this.#setMenuModeTo('jasmine-spec-list');
const failuresLink = createDom(
'a',
{ className: 'jasmine-failures-menu', href: '#' },
'Failures'
);
let specListLink = createDom(
'a',
{ className: 'jasmine-spec-list-menu', href: '#' },
'Spec List'
);
failuresLink.onclick = function() {
onClickFailures();
return false;
};
specListLink.onclick = function() {
onClickSpecList();
return false;
};
this.#alerts.addBar(
createDom(
'span',
{ className: 'jasmine-menu jasmine-bar jasmine-spec-list' },
[createDom('span', {}, 'Spec List | '), failuresLink]
)
);
this.#alerts.addBar(
createDom(
'span',
{ className: 'jasmine-menu jasmine-bar jasmine-failure-list' },
[specListLink, createDom('span', {}, ' | Failures ')]
)
);
}
#find(selector) {
return this.#getContainer().querySelector(
'.jasmine_html-reporter ' + selector

View File

@@ -3,6 +3,10 @@ jasmineRequire.HtmlReporterV2 = function(j$) {
const { createDom, noExpectations } = j$.private.htmlReporterUtils;
const specListTabId = 'jasmine-specListTab';
const failuresTabId = 'jasmine-failuresTab';
const perfTabId = 'jasmine-perfTab';
/**
* @class HtmlReporterV2
* @classdesc Displays results and allows re-running individual specs and suites.
@@ -15,7 +19,7 @@ jasmineRequire.HtmlReporterV2 = function(j$) {
* const reporter = new jasmine.HtmlReporterV2({
* env,
* urls,
* container: document.body
* getContainer: () => document.body
* });
*/
class HtmlReporterV2 {
@@ -31,6 +35,7 @@ jasmineRequire.HtmlReporterV2 = function(j$) {
// Sub-views
#alerts;
#statusBar;
#tabBar;
#progress;
#banner;
#failures;
@@ -68,6 +73,25 @@ jasmineRequire.HtmlReporterV2 = function(j$) {
this.#statusBar = new j$.private.OverallStatusBar(this.#urlBuilder);
this.#statusBar.showRunning();
this.#alerts.addBar(this.#statusBar.rootEl);
this.#tabBar = new j$.private.TabBar(
[
{ id: specListTabId, label: 'Spec List' },
{ id: failuresTabId, label: 'Failures' },
{ id: perfTabId, label: 'Performance' }
],
tabId => {
if (tabId === specListTabId) {
this.#setMenuModeTo('jasmine-spec-list');
} else if (tabId === failuresTabId) {
this.#setMenuModeTo('jasmine-failure-list');
} else {
this.#setMenuModeTo('jasmine-performance');
}
}
);
this.#alerts.addBar(this.#tabBar.rootEl);
this.#progress = new ProgressView();
this.#banner = new j$.private.Banner(
this.#queryString.navigateWithNewParam.bind(this.#queryString),
@@ -158,15 +182,17 @@ jasmineRequire.HtmlReporterV2 = function(j$) {
);
summary.addResults(this.#stateBuilder.topResults);
results.appendChild(summary.rootEl);
const perf = new j$.private.PerformanceView();
perf.addResults(this.#stateBuilder.topResults);
results.appendChild(perf.rootEl);
this.#tabBar.showTab(specListTabId);
this.#tabBar.showTab(perfTabId);
if (this.#stateBuilder.anyNonTopSuiteFailures) {
this.#alerts.addFailureToggle(
() => this.#setMenuModeTo('jasmine-failure-list'),
() => this.#setMenuModeTo('jasmine-spec-list')
);
this.#setMenuModeTo('jasmine-failure-list');
this.#failures.show();
this.#tabBar.showTab(failuresTabId);
this.#tabBar.selectTab(failuresTabId);
} else {
this.#tabBar.selectTab(specListTabId);
}
}
@@ -205,7 +231,6 @@ jasmineRequire.HtmlReporterV2 = function(j$) {
this.rootEl.value = this.rootEl.value + 1;
if (result.status === 'failed') {
// TODO: also a non-color indicator
this.rootEl.classList.add('failed');
}
}

View File

@@ -21,7 +21,10 @@ jasmineRequire.HtmlReporterV2Urls = function(j$) {
}
/**
* Creates a {@link Configuration} from the current page's URL.
* Creates a {@link Configuration} from the current page's URL. Supported
* query string parameters include all those set by {@link HtmlReporterV2}
* as well as spec=partialPath, which filters out specs whose paths don't
* contain partialPath.
* @returns {Configuration}
* @example
* const urls = new jasmine.HtmlReporterV2Urls();
@@ -47,9 +50,10 @@ jasmineRequire.HtmlReporterV2Urls = function(j$) {
}
const specFilter = new j$.private.HtmlSpecFilterV2({
filterString: () => {
return this.queryString.getParam('path');
}
filterParams: () => ({
path: this.queryString.getParam('path'),
spec: this.queryString.getParam('spec')
})
});
config.specFilter = function(spec) {

View File

@@ -1,34 +1,34 @@
jasmineRequire.HtmlSpecFilterV2 = function() {
class HtmlSpecFilterV2 {
#getFilterString;
#getFilterParams;
constructor(options) {
this.#getFilterString = options.filterString;
this.#getFilterParams = options.filterParams;
}
/**
* Determines whether the spec with the specified name should be executed.
* @name HtmlSpecFilterV2#matches
* @function
* @param {Spec} spec
* @returns {boolean}
*/
matches(spec) {
const filterString = this.#getFilterString();
const params = this.#getFilterParams();
if (!filterString) {
return true;
if (params.path) {
return this.#matchesPath(spec, JSON.parse(params.path));
} else if (params.spec) {
// Like legacy HtmlSpecFilter, retained because it's convenient for
// hand-constructing filter URLs
return spec.getFullName().includes(params.spec);
}
const filterPath = JSON.parse(this.#getFilterString());
return true;
}
#matchesPath(spec, path) {
const specPath = spec.getPath();
if (filterPath.length > specPath.length) {
if (path.length > specPath.length) {
return false;
}
for (let i = 0; i < filterPath.length; i++) {
if (specPath[i] !== filterPath[i]) {
for (let i = 0; i < path.length; i++) {
if (specPath[i] !== path[i]) {
return false;
}
}

View File

@@ -0,0 +1,98 @@
jasmineRequire.PerformanceView = function(j$) {
const createDom = j$.private.htmlReporterUtils.createDom;
const MAX_SLOW_SPECS = 20;
class PerformanceView {
#summary;
#tbody;
constructor() {
this.#tbody = document.createElement('tbody');
this.#summary = document.createElement('div');
this.rootEl = createDom(
'div',
{ className: 'jasmine-performance-view' },
createDom('h2', {}, 'Performance'),
this.#summary,
createDom('h3', {}, 'Slowest Specs'),
createDom(
'table',
{},
createDom(
'thead',
{},
createDom(
'tr',
{},
createDom('th', {}, 'Duration'),
createDom('th', {}, 'Spec Name')
)
),
this.#tbody
)
);
}
addResults(resultsTree) {
const specResults = [];
getSpecResults(resultsTree, specResults);
if (specResults.length === 0) {
return;
}
specResults.sort(function(a, b) {
if (a.duration < b.duration) {
return 1;
} else if (a.duration > b.duration) {
return -1;
} else {
return 0;
}
});
this.#populateSumary(specResults);
this.#populateTable(specResults);
}
#populateSumary(specResults) {
const total = specResults.map(r => r.duration).reduce((a, b) => a + b, 0);
const mean = total / specResults.length;
const median = specResults[Math.floor(specResults.length / 2)].duration;
this.#summary.appendChild(
document.createTextNode(`Mean spec run time: ${mean.toFixed(0)}ms`)
);
this.#summary.appendChild(document.createElement('br'));
this.#summary.appendChild(
document.createTextNode(`Median spec run time: ${median}ms`)
);
}
#populateTable(specResults) {
specResults = specResults.slice(0, MAX_SLOW_SPECS);
for (const r of specResults) {
this.#tbody.appendChild(
createDom(
'tr',
{},
createDom('td', {}, `${r.duration}ms`),
createDom('td', {}, r.fullName)
)
);
}
}
}
function getSpecResults(resultsTree, dest) {
for (const node of resultsTree.children) {
if (node.type === 'suite') {
getSpecResults(node, dest);
} else if (node.result.status !== 'excluded') {
dest.push(node.result);
}
}
}
return PerformanceView;
};

77
src/html/TabBar.js Normal file
View File

@@ -0,0 +1,77 @@
jasmineRequire.TabBar = function(j$) {
const createDom = j$.private.htmlReporterUtils.createDom;
class TabBar {
#tabs;
#onSelectTab;
// tabSpecs should be an array of {id, label}.
// All tabs are initially not visible and not selected.
constructor(tabSpecs, onSelectTab) {
this.#onSelectTab = onSelectTab;
this.#tabs = [];
this.#tabs = tabSpecs.map(ts => new Tab(ts, () => this.selectTab(ts.id)));
this.rootEl = createDom(
'span',
{ className: 'jasmine-menu jasmine-bar' },
this.#tabs.map(t => t.rootEl)
);
}
showTab(id) {
for (const tab of this.#tabs) {
if (tab.rootEl.id === id) {
tab.setVisibility(true);
}
}
}
selectTab(id) {
for (const tab of this.#tabs) {
tab.setSelected(tab.rootEl.id === id);
}
this.#onSelectTab(id);
}
}
class Tab {
#spec;
#onClick;
constructor(spec, onClick) {
this.#spec = spec;
this.#onClick = onClick;
this.rootEl = createDom(
'span',
{ id: spec.id, className: 'jasmine-tab jasmine-hidden' },
this.#createLink()
);
}
setVisibility(visible) {
this.rootEl.classList.toggle('jasmine-hidden', !visible);
}
setSelected(selected) {
if (selected) {
this.rootEl.textContent = this.#spec.label;
} else {
this.rootEl.textContent = '';
this.rootEl.appendChild(this.#createLink());
}
}
#createLink() {
const link = createDom('a', { href: '#' }, this.#spec.label);
link.addEventListener('click', e => {
e.preventDefault();
this.#onClick();
});
return link;
}
}
return TabBar;
};

View File

@@ -23,7 +23,7 @@ $failing-mark: "\d7";
$pending-mark: "*";
$space: "\0020";
$font-size: 11px;
$font-size: 12px;
$large-font-size: 14px;
body {
@@ -280,15 +280,25 @@ body {
}
// simplify toggle control between the two menu bars
// TODO: clean this up once HtmlReporter is removed
&.jasmine-spec-list {
.jasmine-bar.jasmine-menu.jasmine-failure-list,
.jasmine-results .jasmine-failures {
.jasmine-results .jasmine-failures,
.jasmine-performance-view {
display: none;
}
}
&.jasmine-failure-list {
.jasmine-bar.jasmine-menu.jasmine-spec-list,
.jasmine-summary,
.jasmine-performance-view {
display: none;
}
}
&.jasmine-performance {
.jasmine-results .jasmine-failures,
.jasmine-summary {
display: none;
}
@@ -464,3 +474,26 @@ body {
}
}
}
.jasmine-hidden {
display: none;
}
.jasmine-tab + .jasmine-tab:before {
content: ' | ';
}
.jasmine-performance-view {
h2, h3 {
margin-top: 1em;
margin-bottom: 1em;
}
table {
border-spacing: 5px;
}
th, td {
text-align: left;
}
}

View File

@@ -11,6 +11,8 @@ jasmineRequire.html = function(j$) {
j$.private.SymbolsView = jasmineRequire.SymbolsView(j$);
j$.private.SummaryTreeView = jasmineRequire.SummaryTreeView(j$);
j$.private.FailuresView = jasmineRequire.FailuresView(j$);
j$.private.PerformanceView = jasmineRequire.PerformanceView(j$);
j$.private.TabBar = jasmineRequire.TabBar(j$);
j$.HtmlReporter = jasmineRequire.HtmlReporter(j$);
j$.HtmlReporterV2Urls = jasmineRequire.HtmlReporterV2Urls(j$);
j$.HtmlReporterV2 = jasmineRequire.HtmlReporterV2(j$);