diff --git a/src/html-reporter/html.js b/src/html-reporter/html.js index 2efbedffc..9c5717660 100644 --- a/src/html-reporter/html.js +++ b/src/html-reporter/html.js @@ -1,68 +1,41 @@ -import QUnit from '../core'; import { extend, errorString, escapeText } from '../core/utilities'; import { window, document, navigator, StringMap } from '../globals'; +import { urlParams } from '../urlparams'; import fuzzysort from 'fuzzysort'; -const stats = { - failedTests: [], - defined: 0, - completed: 0 -}; - -(function () { - // Don't load the HTML Reporter on non-browser environments - if (!window || !document) { - return; - } +const hasOwn = Object.prototype.hasOwnProperty; - QUnit.reporters.perf.init(QUnit); - - const config = QUnit.config; - const hiddenTests = []; - let collapseNext = false; - const hasOwn = Object.prototype.hasOwnProperty; - const unfilteredUrl = setUrl({ - filter: undefined, - module: undefined, - moduleId: undefined, - testId: undefined - }); - let dropdownData = null; - - function addEvent (elem, type, fn) { +const DOM = { + on (elem, type, fn) { elem.addEventListener(type, fn, false); - } - - function removeEvent (elem, type, fn) { + }, + off (elem, type, fn) { elem.removeEventListener(type, fn, false); - } - - function addEvents (elems, type, fn) { + }, + onEach (elems, type, fn) { let i = elems.length; while (i--) { - addEvent(elems[i], type, fn); + DOM.on(elems[i], type, fn); } - } - - function hasClass (elem, name) { + }, + // TODO: Use HTMLElement.classList. IE11+, except toggle(x,y), add(x,y), or remove(x,y). + // TODO: Verity that eslint-plugin-compat catches those exceptions. + hasClass (elem, name) { return (' ' + elem.className + ' ').indexOf(' ' + name + ' ') >= 0; - } - - function addClass (elem, name) { - if (!hasClass(elem, name)) { + }, + addClass (elem, name) { + if (!DOM.hasClass(elem, name)) { elem.className += (elem.className ? ' ' : '') + name; } - } - - function toggleClass (elem, name, force) { - if (force || (typeof force === 'undefined' && !hasClass(elem, name))) { - addClass(elem, name); + }, + toggleClass (elem, name, force) { + if (force || (typeof force === 'undefined' && !DOM.hasClass(elem, name))) { + DOM.addClass(elem, name); } else { - removeClass(elem, name); + DOM.removeClass(elem, name); } - } - - function removeClass (elem, name) { + }, + removeClass (elem, name) { let set = ' ' + elem.className + ' '; // Class name may appear multiple times @@ -72,104 +45,248 @@ const stats = { // Trim for prettiness elem.className = set.trim(); + }, + id (name) { + return document.getElementById && document.getElementById(name); } +}; - function id (name) { - return document.getElementById && document.getElementById(name); +function getUrlConfigHtml (config) { + const urlConfig = config.urlConfig; + let urlConfigHtml = ''; + + for (let i = 0; i < urlConfig.length; i++) { + // Options can be either strings or objects with nonempty "id" properties + let val = urlConfig[i]; + if (typeof val === 'string') { + val = { + id: val, + label: val + }; + } + const currentVal = config[val.id]; + + let escaped = escapeText(val.id); + let escapedTooltip = escapeText(val.tooltip); + + if (!val.value || typeof val.value === 'string') { + urlConfigHtml += "'; + } else { + let selection = false; + urlConfigHtml += "'; + } } - function abortTests () { - const abortButton = id('qunit-abort-tests-button'); - if (abortButton) { - abortButton.disabled = true; - abortButton.innerHTML = 'Aborting...'; + return urlConfigHtml; +} + +function getProgressHtml (stats) { + return [ + stats.completed, + ' / ', + stats.defined, + ' tests completed.
' + ].join(''); +} + +function stripHtml (string) { + // Strip tags, html entity and whitespaces + return string + .replace(/<\/?[^>]+(>|$)/g, '') + .replace(/"/g, '') + .replace(/\s+/g, ''); +} + +export default class HtmlReporter { + /** + * @param {QUnit} QUnit + * @param {Object} [options] + * @param {Object} [options.config] For internal usage + */ + static init (QUnit, options) { + // Don't init the HTML Reporter in non-browser environments + if (!window || !document) { + return; } - QUnit.config.queue.length = 0; - return false; + + // TODO: Move to caller (browser runner) + // Wrap window.onerror. We will call the original window.onerror to see if + // the existing handler fully handles the error; if not, we will call the + // QUnit.onError function. + const originalWindowOnError = window.onerror; + // Cover uncaught exceptions + // Returning true will suppress the default browser handler, + // returning false will let it run. + window.onerror = function (message, fileName, lineNumber, columnNumber, errorObj, ...args) { + let ret = false; + if (originalWindowOnError) { + ret = originalWindowOnError.call( + this, + message, + fileName, + lineNumber, + columnNumber, + errorObj, + ...args + ); + } + + // Treat return value as window.onerror itself does, + // Only do our handling if not suppressed. + if (ret !== true) { + // If there is a current test that sets the internal `ignoreGlobalErrors` field + // (such as during `assert.throws()`), then the error is ignored and native + // error reporting is suppressed as well. This is because in browsers, an error + // can sometimes end up in `window.onerror` instead of in the local try/catch. + // This ignoring of errors does not apply to our general onUncaughtException + // method, nor to our `unhandledRejection` handlers, as those are not meant + // to receive an "expected" error during `assert.throws()`. + if (QUnit.config.current && QUnit.config.current.ignoreGlobalErrors) { + return true; + } + + // According to + // https://blog.sentry.io/2016/01/04/client-javascript-reporting-window-onerror, + // most modern browsers support an errorObj argument; use that to + // get a full stack trace if it's available. + const error = errorObj || new Error(message); + if (!error.stack && fileName && lineNumber) { + error.stack = `${fileName}:${lineNumber}`; + } + QUnit.onUncaughtException(error); + } + + return ret; + }; + + // TODO: Move to caller (browser runner) + window.addEventListener('unhandledrejection', function (event) { + QUnit.onUncaughtException(event.reason); + }); + + // TODO: Move to caller (browser runner) + QUnit.reporters.perf.init(QUnit); + + QUnit.on('runEnd', function (runEnd) { + if (QUnit.config.altertitle && document.title) { + // Show ✖ for good, ✔ for bad suite result in title + // use escape sequences in case file gets loaded with non-utf-8 + // charset + document.title = [ + (runEnd.status === 'failed' ? '\u2716' : '\u2714'), + document.title.replace(/^[\u2714\u2716] /i, '') + ].join(' '); + } + + // Scroll back to top to show results + if (QUnit.config.scrolltop && window.scrollTo) { + window.scrollTo(0, 0); + } + }); + + function autostart () { + // Check as late as possible because if projecst set autostart=false, + // they generally do so in their own scripts, after qunit.js. + if (QUnit.config.autostart) { + QUnit.start(); + } + } + + const reporter = new HtmlReporter(QUnit, options); + + // TODO: Move to caller (browser runner) + if (document.readyState === 'complete') { + autostart(); + } else { + DOM.on(window, 'load', autostart); + } + + return reporter; + } + + constructor (QUnit, options = {}) { + this.stats = { + failedTests: [], + defined: 0, + completed: 0 + }; + // This must use a live reference (i.e. not store a copy), because + // users may apply their settings to QUnit.config anywhere between + // loading qunit.js and the last QUnit.begin() listener finishing. + this.config = options.config || QUnit.config; + this.hiddenTests = []; + this.collapseNext = false; + this.unfilteredUrl = this.makeUrl({ + filter: undefined, + module: undefined, + moduleId: undefined, + testId: undefined + }); + this.dropdownData = null; + + QUnit.on('error', this.onError.bind(this)); + QUnit.on('runStart', this.onRunStart.bind(this)); + QUnit.begin(this.onBegin.bind(this)); + QUnit.testStart(this.onTestStart.bind(this)); + QUnit.log(this.onLog.bind(this)); + QUnit.testDone(this.onTestDone.bind(this)); + QUnit.on('runEnd', this.onRunEnd.bind(this)); } - function interceptNavigation (ev) { + // Handle "submit" event from "filter" or "moduleFilter" field. + onFilterSubmit (ev) { // Trim potential accidental whitespace so that QUnit doesn't throw an error about no tests matching the filter. - const filterInputElem = id('qunit-filter-input'); + const filterInputElem = DOM.id('qunit-filter-input'); filterInputElem.value = filterInputElem.value.trim(); - applyUrlParams(); + this.applyUrlParams(); - if (ev && ev.preventDefault) { + if (ev) { ev.preventDefault(); } return false; } - function getUrlConfigHtml () { - const urlConfig = config.urlConfig; - let urlConfigHtml = ''; - - for (let i = 0; i < urlConfig.length; i++) { - // Options can be either strings or objects with nonempty "id" properties - let val = config.urlConfig[i]; - if (typeof val === 'string') { - val = { - id: val, - label: val - }; - } - const currentVal = config[val.id]; - - let escaped = escapeText(val.id); - let escapedTooltip = escapeText(val.tooltip); - - if (!val.value || typeof val.value === 'string') { - urlConfigHtml += "'; - } else { - let selection = false; - urlConfigHtml += "'; - } - } - - return urlConfigHtml; - } - // Handle "click" events on toolbar checkboxes and "change" for select menus. // Updates the URL with the new state of `config.urlConfig` values. - function toolbarChanged () { - const field = this; + onToolbarChanged (ev) { + const field = ev.currentTarget; const params = {}; // Detect if field is a select menu or a checkbox @@ -181,13 +298,15 @@ const stats = { } params[field.name] = value; - let updatedUrl = setUrl(params); + let updatedUrl = this.makeUrl(params); // Check if we can apply the change without a page refresh if (field.name === 'hidepassed' && 'replaceState' in window.history) { + // eslint-disable-next-line no-undef QUnit.urlParams[field.name] = value; - config[field.name] = value || false; - let tests = id('qunit-tests'); + // TODO: Do we really have to write this change to QUnit.config? + this.config[field.name] = value || false; + let tests = DOM.id('qunit-tests'); if (tests) { const length = tests.children.length; const children = tests.children; @@ -200,16 +319,16 @@ const stats = { const classNameHasSkipped = className.indexOf('skipped') > -1; if (classNameHasPass || classNameHasSkipped) { - hiddenTests.push(test); + this.hiddenTests.push(test); } } - for (const hiddenTest of hiddenTests) { + for (const hiddenTest of this.hiddenTests) { tests.removeChild(hiddenTest); } } else { - while (hiddenTests.length) { - tests.appendChild(hiddenTests.shift()); + while (this.hiddenTests.length) { + tests.appendChild(this.hiddenTests.shift()); } } } @@ -219,11 +338,11 @@ const stats = { } } - function setUrl (params) { + makeUrl (params) { let querystring = '?'; const location = window.location; - params = extend(extend({}, QUnit.urlParams), params); + params = extend(extend({}, urlParams), params); for (let key in params) { // Skip inherited or undefined properties @@ -240,16 +359,18 @@ const stats = { } } } + // TODO: Consider changing HTML to use a relative URL here, + // no need for window.location dependency. return location.protocol + '//' + location.host + - location.pathname + querystring.slice(0, -1); + location.pathname + querystring.slice(0, -1); } - function applyUrlParams () { - const filter = id('qunit-filter-input').value; + applyUrlParams () { + const filter = DOM.id('qunit-filter-input').value; - window.location = setUrl({ + window.location = this.makeUrl({ filter: (filter === '') ? undefined : filter, - moduleId: [...dropdownData.selectedMap.keys()], + moduleId: [...this.dropdownData.selectedMap.keys()], // Remove module and testId filter module: undefined, @@ -257,85 +378,51 @@ const stats = { }); } - function toolbarUrlConfigContainer () { - const urlConfigContainer = document.createElement('span'); - - urlConfigContainer.innerHTML = getUrlConfigHtml(); - addClass(urlConfigContainer, 'qunit-url-config'); - - addEvents(urlConfigContainer.getElementsByTagName('input'), 'change', toolbarChanged); - addEvents(urlConfigContainer.getElementsByTagName('select'), 'change', toolbarChanged); - - return urlConfigContainer; - } - - function abortTestsButton () { + abortTestsButton () { const button = document.createElement('button'); button.id = 'qunit-abort-tests-button'; button.innerHTML = 'Abort'; - addEvent(button, 'click', abortTests); + DOM.on(button, 'click', () => { + const abortButton = DOM.id('qunit-abort-tests-button'); + if (abortButton) { + abortButton.disabled = true; + abortButton.innerHTML = 'Aborting...'; + } + this.config.queue.length = 0; + return false; + }); return button; } - function toolbarLooseFilter () { + toolbarLooseFilter () { const filter = document.createElement('form'); - const label = document.createElement('label'); - const input = document.createElement('input'); - const button = document.createElement('button'); - - addClass(filter, 'qunit-filter'); + filter.className = 'qunit-filter'; + const label = document.createElement('label'); label.innerHTML = 'Filter: '; + const input = document.createElement('input'); input.type = 'text'; - input.value = config.filter || ''; + input.value = this.config.filter || ''; input.name = 'filter'; input.id = 'qunit-filter-input'; - button.innerHTML = 'Go'; - label.appendChild(input); + const button = document.createElement('button'); + button.innerHTML = 'Go'; + filter.appendChild(label); filter.appendChild(document.createTextNode(' ')); filter.appendChild(button); - addEvent(filter, 'submit', interceptNavigation); + DOM.on(filter, 'submit', this.onFilterSubmit.bind(this)); return filter; } - function createModuleListItem (moduleId, name, checked) { - return '
  • '; - } - - /** - * @param {Array} Results from fuzzysort - * @return {string} HTML - */ - function moduleListHtml (results) { - let html = ''; - - // Hoist the already selected items, and show them always - // even if not matched by the current search. - dropdownData.selectedMap.forEach((name, moduleId) => { - html += createModuleListItem(moduleId, name, true); - }); - - for (let i = 0; i < results.length; i++) { - const mod = results[i].obj; - if (!dropdownData.selectedMap.has(mod.moduleId)) { - html += createModuleListItem(mod.moduleId, mod.name, false); - } - } - return html; - } - - function toolbarModuleFilter (beginDetails) { + toolbarModuleFilter (beginDetails) { let initialSelected = null; - dropdownData = { + const dropdownData = this.dropdownData = { options: beginDetails.modules.slice(), selectedMap: new StringMap(), isDirty: function () { @@ -344,7 +431,7 @@ const stats = { } }; - if (config.moduleId && config.moduleId.length) { + if (this.config.moduleId && this.config.moduleId.length) { // The module dropdown is seeded with the runtime configuration of the last run. // // We don't reference `config.moduleId` directly after this and keep our own @@ -355,20 +442,49 @@ const stats = { // during rendering. for (let i = 0; i < beginDetails.modules.length; i++) { const mod = beginDetails.modules[i]; - if (config.moduleId.indexOf(mod.moduleId) !== -1) { + if (this.config.moduleId.indexOf(mod.moduleId) !== -1) { dropdownData.selectedMap.set(mod.moduleId, mod.name); } } } initialSelected = new StringMap(dropdownData.selectedMap); + function createModuleListItem (moduleId, name, checked) { + return '
  • '; + } + + /** + * @param {Array} Results from fuzzysort + * @return {string} HTML + */ + function moduleListHtml (results) { + let html = ''; + + // Hoist the already selected items, and show them always + // even if not matched by the current search. + dropdownData.selectedMap.forEach((name, moduleId) => { + html += createModuleListItem(moduleId, name, true); + }); + + for (let i = 0; i < results.length; i++) { + const mod = results[i].obj; + if (!dropdownData.selectedMap.has(mod.moduleId)) { + html += createModuleListItem(mod.moduleId, mod.name, false); + } + } + return html; + } + const moduleSearch = document.createElement('input'); moduleSearch.id = 'qunit-modulefilter-search'; moduleSearch.autocomplete = 'off'; - addEvent(moduleSearch, 'input', searchInput); - addEvent(moduleSearch, 'input', searchFocus); - addEvent(moduleSearch, 'focus', searchFocus); - addEvent(moduleSearch, 'click', searchFocus); + DOM.on(moduleSearch, 'input', searchInput); + DOM.on(moduleSearch, 'input', searchFocus); + DOM.on(moduleSearch, 'focus', searchFocus); + DOM.on(moduleSearch, 'click', searchFocus); const label = document.createElement('label'); label.htmlFor = 'qunit-modulefilter-search'; @@ -380,7 +496,7 @@ const stats = { const applyButton = document.createElement('button'); applyButton.textContent = 'Apply'; applyButton.title = 'Re-run the selected test modules'; - addEvent(applyButton, 'click', applyUrlParams); + DOM.on(applyButton, 'click', this.applyUrlParams); const resetButton = document.createElement('button'); resetButton.textContent = 'Reset'; @@ -391,7 +507,7 @@ const stats = { clearButton.textContent = 'Select none'; clearButton.type = 'button'; clearButton.title = 'Clear the current module selection'; - addEvent(clearButton, 'click', function () { + DOM.on(clearButton, 'click', function () { dropdownData.selectedMap.clear(); selectionChange(); searchInput(); @@ -414,7 +530,7 @@ const stats = { dropDown.style.display = 'none'; dropDown.appendChild(actions); dropDown.appendChild(dropDownList); - addEvent(dropDown, 'change', selectionChange); + DOM.on(dropDown, 'change', selectionChange); searchContainer.appendChild(dropDown); // Set initial moduleSearch.placeholder and clearButton/resetButton. selectionChange(); @@ -424,8 +540,8 @@ const stats = { moduleFilter.appendChild(label); moduleFilter.appendChild(document.createTextNode(' ')); moduleFilter.appendChild(searchContainer); - addEvent(moduleFilter, 'submit', interceptNavigation); - addEvent(moduleFilter, 'reset', function () { + DOM.on(moduleFilter, 'submit', this.onFilterSubmit.bind(this)); + DOM.on(moduleFilter, 'reset', function () { dropdownData.selectedMap = new StringMap(initialSelected); // Set moduleSearch.placeholder and reflect non-dirty state selectionChange(); @@ -444,8 +560,8 @@ const stats = { dropDown.style.display = 'block'; // Hide on Escape keydown or on click outside the container - addEvent(document, 'click', hideHandler); - addEvent(document, 'keydown', hideHandler); + DOM.on(document, 'click', hideHandler); + DOM.on(document, 'keydown', hideHandler); function hideHandler (e) { const inContainer = moduleFilter.contains(e.target); @@ -455,8 +571,8 @@ const stats = { moduleSearch.focus(); } dropDown.style.display = 'none'; - removeEvent(document, 'click', hideHandler); - removeEvent(document, 'keydown', hideHandler); + DOM.off(document, 'click', hideHandler); + DOM.off(document, 'keydown', hideHandler); moduleSearch.value = ''; searchInput(); } @@ -517,7 +633,7 @@ const stats = { } // Update UI state - toggleClass(checkbox.parentNode, 'checked', checkbox.checked); + DOM.toggleClass(checkbox.parentNode, 'checked', checkbox.checked); } const textForm = dropdownData.selectedMap.size @@ -532,15 +648,20 @@ const stats = { return moduleFilter; } - function appendToolbar (beginDetails) { - const toolbar = id('qunit-testrunner-toolbar'); + appendToolbar (beginDetails) { + const toolbar = DOM.id('qunit-testrunner-toolbar'); if (toolbar) { - toolbar.appendChild(toolbarUrlConfigContainer()); + const urlConfigContainer = document.createElement('span'); + urlConfigContainer.innerHTML = getUrlConfigHtml(this.config); + DOM.addClass(urlConfigContainer, 'qunit-url-config'); + DOM.onEach(urlConfigContainer.getElementsByTagName('input'), 'change', this.onToolbarChanged.bind(this)); + DOM.onEach(urlConfigContainer.getElementsByTagName('select'), 'change', this.onToolbarChanged.bind(this)); + toolbar.appendChild(urlConfigContainer); const toolbarFilters = document.createElement('span'); toolbarFilters.id = 'qunit-toolbar-filters'; - toolbarFilters.appendChild(toolbarLooseFilter()); - toolbarFilters.appendChild(toolbarModuleFilter(beginDetails)); + toolbarFilters.appendChild(this.toolbarLooseFilter()); + toolbarFilters.appendChild(this.toolbarModuleFilter(beginDetails)); const clearfix = document.createElement('div'); clearfix.className = 'clearfix'; @@ -550,26 +671,26 @@ const stats = { } } - function appendHeader () { - const header = id('qunit-header'); + appendHeader () { + const header = DOM.id('qunit-header'); if (header) { - header.innerHTML = "" + header.innerHTML + + header.innerHTML = "" + header.innerHTML + ' '; } } - function appendBanner () { - const banner = id('qunit-banner'); + appendBanner () { + const banner = DOM.id('qunit-banner'); if (banner) { banner.className = ''; } } - function appendTestResults () { - const tests = id('qunit-tests'); - let result = id('qunit-testresult'); + appendTestResults () { + const tests = DOM.id('qunit-tests'); + let result = DOM.id('qunit-testresult'); let controls; if (result) { @@ -585,41 +706,42 @@ const stats = { result.innerHTML = '
    Running...
     
    ' + '
    ' + '
    '; - controls = id('qunit-testresult-controls'); + controls = DOM.id('qunit-testresult-controls'); } if (controls) { - controls.appendChild(abortTestsButton()); + controls.appendChild(this.abortTestsButton()); } } - function appendFilteredTest () { - const testId = QUnit.config.testId; + appendFilteredTest () { + const testId = this.config.testId; if (!testId || testId.length <= 0) { return ''; } return "
    Rerunning selected tests: " + escapeText(testId.join(', ')) + " Run all tests
    "; } - function appendUserAgent () { - const userAgent = id('qunit-userAgent'); + appendUserAgent () { + const userAgent = DOM.id('qunit-userAgent'); if (userAgent) { userAgent.innerHTML = ''; userAgent.appendChild( document.createTextNode( + // eslint-disable-next-line no-undef 'QUnit ' + QUnit.version + '; ' + navigator.userAgent ) ); } } - function appendInterface (beginDetails) { - const qunit = id('qunit'); + appendInterface (beginDetails) { + const qunit = DOM.id('qunit'); // For compat with QUnit 1.2, and to support fully custom theme HTML, // we will use any existing elements if no id="qunit" element exists. @@ -638,20 +760,20 @@ const stats = { "

    " + escapeText(document.title) + '

    ' + "

    " + "" + - appendFilteredTest() + + this.appendFilteredTest() + "

    " + "
      "; } - appendHeader(); - appendBanner(); - appendTestResults(); - appendUserAgent(); - appendToolbar(beginDetails); + this.appendHeader(); + this.appendBanner(); + this.appendTestResults(); + this.appendUserAgent(); + this.appendToolbar(beginDetails); } - function appendTest (name, testId, moduleName) { - const tests = id('qunit-tests'); + appendTest (name, testId, moduleName) { + const tests = DOM.id('qunit-tests'); if (!tests) { return; } @@ -666,7 +788,7 @@ const stats = { if (testId !== undefined) { let rerunTrigger = document.createElement('a'); rerunTrigger.innerHTML = 'Rerun'; - rerunTrigger.href = setUrl({ testId: testId }); + rerunTrigger.href = this.makeUrl({ testId: testId }); testBlock.id = 'qunit-test-output-' + testId; testBlock.appendChild(rerunTrigger); @@ -682,12 +804,12 @@ const stats = { return testBlock; } - // HTML Reporter initialization and load - QUnit.on('runStart', function (runStart) { - stats.defined = runStart.testCounts.total; - }); + onRunStart (runStart) { + // HTML Reporter initialization and load + this.stats.defined = runStart.testCounts.total; + } - QUnit.begin(function (beginDetails) { + onBegin (beginDetails) { // Initialize QUnit elements // This is done from begin() instead of runStart, because // urlparams.js uses begin(), which we need to wait for. @@ -695,15 +817,15 @@ const stats = { // add entries to QUnit.config.urlConfig, which may be done // asynchronously. // - appendInterface(beginDetails); - }); + this.appendInterface(beginDetails); + } - function getRerunFailedHtml (failedTests) { + getRerunFailedHtml (failedTests) { if (failedTests.length === 0) { return ''; } - const href = setUrl({ testId: failedTests }); + const href = this.makeUrl({ testId: failedTests }); return [ "
      ", failedTests.length === 1 @@ -713,19 +835,19 @@ const stats = { ].join(''); } - function msToSec (milliseconds) { - if (milliseconds < 1000) { - // Will return e.g. "0.2", "0.03" or "0.004" - return (milliseconds / 1000).toPrecision(1) + ' seconds'; + onRunEnd (runEnd) { + function msToSec (milliseconds) { + if (milliseconds < 1000) { + // Will return e.g. "0.2", "0.03" or "0.004" + return (milliseconds / 1000).toPrecision(1) + ' seconds'; + } + const sec = Math.ceil(milliseconds / 1000); + return sec + (sec === 1 ? ' second' : ' seconds'); } - const sec = Math.ceil(milliseconds / 1000); - return sec + (sec === 1 ? ' second' : ' seconds'); - } - QUnit.on('runEnd', function (runEnd) { - const banner = id('qunit-banner'); - const tests = id('qunit-tests'); - const abortButton = id('qunit-abort-tests-button'); + const banner = DOM.id('qunit-banner'); + const tests = DOM.id('qunit-tests'); + const abortButton = DOM.id('qunit-abort-tests-button'); let html = [ '', runEnd.testCounts.total, ' tests completed in ', msToSec(runEnd.runtime), @@ -734,7 +856,7 @@ const stats = { '', runEnd.testCounts.skipped, ' skipped, ', '', runEnd.testCounts.failed, ' failed, ', 'and ', runEnd.testCounts.todo, ' todo.', - getRerunFailedHtml(stats.failedTests) + this.getRerunFailedHtml(this.stats.failedTests) ].join(''); let test; let assertLi; @@ -766,79 +888,31 @@ const stats = { } if (tests) { - id('qunit-testresult-display').innerHTML = html; + DOM.id('qunit-testresult-display').innerHTML = html; } - - if (config.altertitle && document.title) { - // Show ✖ for good, ✔ for bad suite result in title - // use escape sequences in case file gets loaded with non-utf-8 - // charset - document.title = [ - (runEnd.status === 'failed' ? '\u2716' : '\u2714'), - document.title.replace(/^[\u2714\u2716] /i, '') - ].join(' '); - } - - // Scroll back to top to show results - if (config.scrolltop && window.scrollTo) { - window.scrollTo(0, 0); - } - }); - - function getNameHtml (name, module) { - let nameHtml = ''; - - if (module) { - nameHtml = "" + escapeText(module) + ': '; - } - - nameHtml += "" + escapeText(name) + ''; - - return nameHtml; } - function getProgressHtml (stats) { - return [ - stats.completed, - ' / ', - stats.defined, - ' tests completed.
      ' - ].join(''); - } - - QUnit.testStart(function (details) { - let running, bad; - - appendTest(details.name, details.testId, details.module); + onTestStart (details) { + this.appendTest(details.name, details.testId, details.module); - running = id('qunit-testresult-display'); + let running = DOM.id('qunit-testresult-display'); if (running) { - addClass(running, 'running'); - - bad = QUnit.config.reorder && details.previousFailure; + DOM.addClass(running, 'running'); running.innerHTML = [ - getProgressHtml(stats), - bad + getProgressHtml(this.stats), + details.previousFailure ? 'Rerunning previously failed test:
      ' : 'Running: ', getNameHtml(details.name, details.module), - getRerunFailedHtml(stats.failedTests) + this.getRerunFailedHtml(this.stats.failedTests) ].join(''); } - }); - - function stripHtml (string) { - // Strip tags, html entity and whitespaces - return string - .replace(/<\/?[^>]+(>|$)/g, '') - .replace(/"/g, '') - .replace(/\s+/g, ''); } - QUnit.log(function (details) { - const testItem = id('qunit-test-output-' + details.testId); + onLog (details) { + const testItem = DOM.id('qunit-test-output-' + details.testId); if (!testItem) { return; } @@ -859,11 +933,14 @@ const stats = { // that's a regular assertion for which to render actual/expected and a diff. if (!details.result && hasOwn.call(details, 'expected')) { if (details.negative) { + // eslint-disable-next-line no-undef expected = 'NOT ' + QUnit.dump.parse(details.expected); } else { + // eslint-disable-next-line no-undef expected = QUnit.dump.parse(details.expected); } + // eslint-disable-next-line no-undef actual = QUnit.dump.parse(details.actual); message += "'; } else { message += "
      Expected:
      " +
             escapeText(expected) +
      @@ -880,7 +957,9 @@ const stats = {
                   diff = (diff > 0 ? '+' : '') + diff;
                 }
               } else if (typeof details.actual !== 'boolean' &&
      -              typeof details.expected !== 'boolean') {
      +              typeof details.expected !== 'boolean'
      +        ) {
      +          // eslint-disable-next-line no-undef
                 diff = QUnit.diff(expected, actual);
       
                 // don't show diff if there is zero overlap
      @@ -897,9 +976,9 @@ const stats = {
               expected.indexOf('[object Object]') !== -1) {
               message += "
      Message: " + 'Diff suppressed as the depth of object is more than current max depth (' + - QUnit.config.maxDepth + ').

      Hint: Use QUnit.dump.maxDepth to ' + + this.config.maxDepth + ').

      Hint: Use QUnit.dump.maxDepth to ' + " run with a higher max depth or " + + escapeText(this.makeUrl({ maxDepth: -1 })) + "'>" + 'Rerun without max depth.

      Message: " + @@ -928,16 +1007,16 @@ const stats = { assertLi.className = details.result ? 'pass' : 'fail'; assertLi.innerHTML = message; assertList.appendChild(assertLi); - }); + } - QUnit.testDone(function (details) { - const tests = id('qunit-tests'); - const testItem = id('qunit-test-output-' + details.testId); + onTestDone (details) { + const tests = DOM.id('qunit-tests'); + const testItem = DOM.id('qunit-test-output-' + details.testId); if (!tests || !testItem) { return; } - removeClass(testItem, 'running'); + DOM.removeClass(testItem, 'running'); let status; if (details.failed > 0) { @@ -958,17 +1037,17 @@ const stats = { if (testPassed) { // Collapse the passing tests - addClass(assertList, 'qunit-collapsed'); + DOM.addClass(assertList, 'qunit-collapsed'); } else { - stats.failedTests.push(details.testId); + this.stats.failedTests.push(details.testId); - if (config.collapse) { - if (!collapseNext) { + if (this.config.collapse) { + if (!this.collapseNext) { // Skip collapsing the first failing test - collapseNext = true; + this.collapseNext = true; } else { // Collapse remaining tests - addClass(assertList, 'qunit-collapsed'); + DOM.addClass(assertList, 'qunit-collapsed'); } } } @@ -983,7 +1062,7 @@ const stats = { testTitle.innerHTML += " (" + testCounts + details.assertions.length + ')'; - stats.completed++; + this.stats.completed++; if (details.skipped) { testItem.className = 'skipped'; @@ -992,8 +1071,8 @@ const stats = { skipped.innerHTML = 'skipped'; testItem.insertBefore(skipped, testTitle); } else { - addEvent(testTitle, 'click', function () { - toggleClass(assertList, 'qunit-collapsed'); + DOM.on(testTitle, 'click', function () { + DOM.toggleClass(assertList, 'qunit-collapsed'); }); testItem.className = testPassed ? 'pass' : 'fail'; @@ -1016,26 +1095,26 @@ const stats = { if (details.source) { let sourceName = document.createElement('p'); sourceName.innerHTML = 'Source: ' + escapeText(details.source); - addClass(sourceName, 'qunit-source'); + DOM.addClass(sourceName, 'qunit-source'); if (testPassed) { - addClass(sourceName, 'qunit-collapsed'); + DOM.addClass(sourceName, 'qunit-collapsed'); } - addEvent(testTitle, 'click', function () { - toggleClass(sourceName, 'qunit-collapsed'); + DOM.on(testTitle, 'click', function () { + DOM.toggleClass(sourceName, 'qunit-collapsed'); }); testItem.appendChild(sourceName); } - if (config.hidepassed && (status === 'passed' || details.skipped)) { + if (this.config.hidepassed && (status === 'passed' || details.skipped)) { // use removeChild instead of remove because of support - hiddenTests.push(testItem); + this.hiddenTests.push(testItem); tests.removeChild(testItem); } - }); + } - QUnit.on('error', (error) => { - const testItem = appendTest('global failure'); + onError (error) { + const testItem = this.appendTest('global failure'); if (!testItem) { // HTML Reporter is probably disabled or not yet initialized. return; @@ -1058,73 +1137,17 @@ const stats = { // Make it visible testItem.className = 'fail'; - }); - - function autostart () { - // Check as late as possible because if projecst set autostart=false, - // they generally do so in their own scripts, after qunit.js. - if (config.autostart) { - QUnit.start(); - } } +} - if (document.readyState === 'complete') { - autostart(); - } else { - addEvent(window, 'load', autostart); - } +function getNameHtml (name, module) { + let nameHtml = ''; - // Wrap window.onerror. We will call the original window.onerror to see if - // the existing handler fully handles the error; if not, we will call the - // QUnit.onError function. - const originalWindowOnError = window.onerror; - - // Cover uncaught exceptions - // Returning true will suppress the default browser handler, - // returning false will let it run. - window.onerror = function (message, fileName, lineNumber, columnNumber, errorObj, ...args) { - let ret = false; - if (originalWindowOnError) { - ret = originalWindowOnError.call( - this, - message, - fileName, - lineNumber, - columnNumber, - errorObj, - ...args - ); - } - - // Treat return value as window.onerror itself does, - // Only do our handling if not suppressed. - if (ret !== true) { - // If there is a current test that sets the internal `ignoreGlobalErrors` field - // (such as during `assert.throws()`), then the error is ignored and native - // error reporting is suppressed as well. This is because in browsers, an error - // can sometimes end up in `window.onerror` instead of in the local try/catch. - // This ignoring of errors does not apply to our general onUncaughtException - // method, nor to our `unhandledRejection` handlers, as those are not meant - // to receive an "expected" error during `assert.throws()`. - if (config.current && config.current.ignoreGlobalErrors) { - return true; - } - - // According to - // https://blog.sentry.io/2016/01/04/client-javascript-reporting-window-onerror, - // most modern browsers support an errorObj argument; use that to - // get a full stack trace if it's available. - const error = errorObj || new Error(message); - if (!error.stack && fileName && lineNumber) { - error.stack = `${fileName}:${lineNumber}`; - } - QUnit.onUncaughtException(error); - } + if (module) { + nameHtml = "" + escapeText(module) + ': '; + } - return ret; - }; + nameHtml += "" + escapeText(name) + ''; - window.addEventListener('unhandledrejection', function (event) { - QUnit.onUncaughtException(event.reason); - }); -}()); + return nameHtml; +} diff --git a/src/qunit.js b/src/qunit.js index f4ba54eb2..af2fe1430 100644 --- a/src/qunit.js +++ b/src/qunit.js @@ -1,4 +1,6 @@ -import './core'; +import QUnit from './core'; import './html-runner/fixture'; import './html-runner/urlparams'; -import './html-reporter/html'; + +/* global QUnit */ +QUnit.reporters.html.init(QUnit); diff --git a/src/reporters.js b/src/reporters.js index 862172d5b..02defa222 100644 --- a/src/reporters.js +++ b/src/reporters.js @@ -1,9 +1,11 @@ import ConsoleReporter from './reporters/ConsoleReporter.js'; import PerfReporter from './reporters/PerfReporter.js'; import TapReporter from './reporters/TapReporter.js'; +import HtmlReporer from './html-reporter/html'; export default { console: ConsoleReporter, perf: PerfReporter, - tap: TapReporter + tap: TapReporter, + html: HtmlReporer };