From d689d280f0cb6a569a8e69c64f2eae9c9dae8fac Mon Sep 17 00:00:00 2001 From: Timo Tijhof Date: Thu, 4 Jul 2024 04:21:49 +0100 Subject: [PATCH] HTML Reporter: QUnit 3.0 theme structure and layout Notable design changes: * Edge to edge design. Remove body margin (browser default), remove rounded corners. * Progress bar. The qunit-banner was previously blank until it turns green/red. Test execution is now animated with a blue progress bar. * Adopt system-ui fonts, remove any text shadows. * Sticky header reduced to toolbar. Re-implemented using `position: sticky`, to create a normal page-level scrollbar instead of a scrollable area. The rest of the page naturally flows under it. This fixes various layout quirks, with custom elements no longer squished or pushed outside the viewport. This is similar to how things rendered prior to QUnit 2.14. The sticky part now excludes the page title and user agent, making more optimal use of vertical space. * Move "Abort" button to the left. This previously floated in a somewhat-random position on the right. The toolbar now occupies the same height during the "running" and "completed" state, avoiding a visible layout shift. * Remove 2px offset hack from checkboxes to make them vertically aligned. Other changes: * `#qunit-userAgent` is now DIV rather than H2, placed above #qunit-testrunner-toolbar instead of below it. * `#qunit-toolbar` now parents `#qunit-banner`, `#qunit-testresult`, and `#qunit-filteredTest`. * `#qunit-header` now parents `H1` and `#qunit-userAgent`. * The `#qunit-modulefilter-actions` buttons are now ordered in a more natural way in the DOM. Previously, the order was reversed, with the default theme using float: right to reverse them. To achieve right-aligned buttons like before, one could set `text-align: right` on `.qunit-modulefilter-actions`. * The `.qunit-skipped-label` and `.qunit-todo-label` elements, which are palced before TODO/SKIP test titles, now include a space between the label and the test title. This means the word "skipped" and the first word of the test title no longer get presented semantically as one word with intra-word styling (e.g. to a screen reader, or when copying selected text). This also removes the need for margin-right hack in the theme. Fixed bug: * Assertion counts the parentheses of `#qunit-test` items were double-bolded, and on failing tests the numbers were tripple-bolded. In browsers/fonts that support "bolder" (e.g. Firefox) this resulted in an ever-wider font. Changed `` to `` given they are already inside a ``. Closes https://github.com/qunitjs/qunit/pull/1774. --- docs/browser.md | 14 +- src/qunit.css | 370 ++++++++++++++++------------------ src/reporters/HtmlReporter.js | 347 ++++++++++++++----------------- test/main/HtmlReporter.js | 4 +- 4 files changed, 327 insertions(+), 408 deletions(-) diff --git a/docs/browser.md b/docs/browser.md index f86bc002b..d9eeda6d3 100644 --- a/docs/browser.md +++ b/docs/browser.md @@ -202,11 +202,15 @@ QUnit 1.x, 2.x: ```html
-

-
-
-

-
+
+

+
+
+
+
+
+
+
    ``` diff --git a/src/qunit.css b/src/qunit.css index 5ba1d43ff..ac292a3c4 100644 --- a/src/qunit.css +++ b/src/qunit.css @@ -7,81 +7,66 @@ * https://jquery.org/license */ -/** Font Family and Sizes */ +/** + * Resets + */ -#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult { - font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; +body { + margin: 0; + padding: 0; } -#qunit-testrunner-toolbar, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } -#qunit-tests { font-size: smaller; } - - -/** Resets */ - -#qunit-tests, #qunit-header, #qunit-banner, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { +[id=qunit] * { margin: 0; padding: 0; } +#qunit { + font-family: system-ui, sans-serif; + font-size: 13px; + line-height: 1.2; + background: #FFF; + color: #333; +} + /* Style our buttons in a simple way, uninfluenced by the styles the tested app might load. Don't affect buttons in #qunit-fixture! https://github.com/qunitjs/qunit/pull/1395 https://github.com/qunitjs/qunit/issues/1437 */ -#qunit-testrunner-toolbar button, -#qunit-testresult button { - all: unset; /* best effort, modern browsers only */ - font: inherit; - color: initial; - border: initial; - background-color: buttonface; - padding: 0 4px; +[id=qunit] button { + all: revert; /* best effort, modern browsers only */ + cursor: pointer; } - - -/** Fixed headers with scrollable tests */ - -@supports (display: flex) or (display: -webkit-box) { - @media (min-height: 500px) { - #qunit { - position: fixed; - left: 0; - right: 0; - top: 0; - bottom: 0; - padding: 8px; - display: -webkit-box; - display: flex; - flex-direction: column; - } - - #qunit-tests { - overflow: scroll; - } - - #qunit-banner { - flex: 5px 0 0; - } - } +[id=qunit] button:disabled { + cursor: default; } - -/** Header (excluding toolbar) */ +/** + * Header + */ #qunit-header { - padding: 0.5em 0 0.5em 1em; + padding: 13px; color: #C2CCD1; background-color: #0D3349; - font-size: 1.5em; - line-height: 1em; - font-weight: 400; + display: flex; + flex-flow: row wrap; + justify-content: space-between; + align-items: flex-end; + gap: 13px; +} - border-radius: 5px 5px 0 0; +#qunit-header h1 { + flex-grow: 1; + font-size: 24px; + line-height: 1; + font-weight: 400; } #qunit-header a { + display: block; text-decoration: none; color: inherit; } @@ -92,135 +77,140 @@ } #qunit-banner { + --qunit-progress: 0%; + height: 5px; + background-image: linear-gradient(#0769AD, #0769AD); + background-repeat: no-repeat; + background-size: var(--qunit-progress); + transition: background-size 100ms ease-out; +} +#qunit-banner.qunit-pass { + background-image: none; + background-color: #C6E746; +} +#qunit-banner.qunit-fail { + background-image: none; + background-color: #EE5757; } -#qunit-filteredTest { - padding: 0.5em 1em 0.5em 1em; - color: #366097; - background-color: #F4FF77; +#qunit-toolbar { + background-color: #EEE; + border-bottom: 1px solid #FFF; } -#qunit-userAgent { - padding: 0.5em 1em 0.5em 1em; - color: #FFF; - background-color: #2B81AF; - text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; +#qunit-testresult { + clear: both; + border-top: 1px solid #FFF; } +#qunit-toolbar-urlconfig, +#qunit-toolbar-filters, +#qunit-testresult, +#qunit-filteredTest { + padding: 7px 13px; +} -/** Toolbar */ +#qunit-filteredTest { + border-top: 1px solid #FFF; + background-color: #fbdbfb; +} -#qunit-testrunner-toolbar { - padding: 0.5em 1em 0.5em 1em; - color: #5E740B; - background-color: #EEE; +/** Fixed toolbar, with scrollable test results */ +@supports (position: sticky) { + @media (min-height: 400px) { + #qunit-toolbar { + position: sticky; + top: 0px; + box-shadow: rgba(13, 51, 73, 0.3) 0px 5px 10px -5px; + } + } } -#qunit-testrunner-toolbar .clearfix { - height: 0; - clear: both; +/** + * Toolbar elements + */ + +#qunit-toolbar label { + display: inline-block; + margin-right: 6px; + line-height: 2.1; } -#qunit-testrunner-toolbar label { +#qunit-toolbar form { display: inline-block; } -#qunit-testrunner-toolbar input[type=checkbox], -#qunit-testrunner-toolbar input[type=radio] { +#qunit-toolbar input[type=checkbox], +#qunit-toolbar input[type=radio] { margin: 3px; - vertical-align: -2px; } -#qunit-testrunner-toolbar input[type=text] { +#qunit-toolbar input[type=text] { box-sizing: border-box; - height: 1.6em; + height: 30px; + padding: 0 6px; } -#qunit-testrunner-toolbar button, -#qunit-testresult button { - border-radius: .25em; - border: 1px solid #AAA; - background-color: #F8F8F8; - color: #222; - line-height: 1.6; - cursor: pointer; -} -#qunit-testrunner-toolbar button:hover, -#qunit-testresult button:hover { - border-color: #AAA; - background-color: #FFF; - color: #444; -} -#qunit-testrunner-toolbar button:active, -#qunit-testresult button:active { - border-color: #777; - background-color: #CCC; - color: #000; -} -#qunit-testrunner-toolbar button:focus, -#qunit-testresult button:focus { - border-color: #2F68DA; - /* emulate 2px border without a layout shift */ - box-shadow: inset 0 0 0 1px #2F68DA -} -#qunit-testrunner-toolbar button:disabled, -#qunit-testresult button:disabled { - border-color: #CCC; - background-color: #CCC; - color: #FFF; - cursor: default; -} +/** + * Toolbar layout + */ -#qunit-toolbar-filters { - float: right; - /* aligning right avoids overflows and inefficient use of space - around the dropdown menu on narrow viewports */ - text-align: right; +#qunit-testrunner-toolbar { + display: flex; + flex-flow: row wrap; + justify-content: space-between; } +#qunit-toolbar-filters { + flex-grow: 1; + justify-content: flex-end; -.qunit-url-config, -.qunit-filter, -#qunit-modulefilter { - display: inline-block; - line-height: 2.1em; - text-align: left; + display: flex; + flex-flow: row wrap; + gap: 13px; } - -.qunit-filter, #qunit-modulefilter { - position: relative; - margin-left: 1em; + flex-grow: 1; + max-width: 450px; } - -.qunit-url-config label { - margin-right: 0.5em; +#qunit-modulefilter label { + margin-right: 0; + display: flex; + flex-flow: row wrap; } - #qunit-modulefilter-search { - box-sizing: border-box; - min-width: 400px; - min-width: min(400px, 80vw); + width: 100%; } +@media (max-width: 1000px) { + #qunit-toolbar-filters { + justify-content: flex-start; + } +} + +/** + * Module selector + */ + #qunit-modulefilter-search-container { + /* grow within `#qunit-modulefilter label` */ + flex-grow: 1; position: relative; } #qunit-modulefilter-search-container:after { position: absolute; - right: 0.3em; + right: 6px; bottom: 0; - line-height: 100%; - content: "\25bc"; - color: black; + height: 30px; /* align with input[type=text] */ + width: 16px; + content: ""; + background: url('data:image/svg+xml,') center no-repeat; } #qunit-modulefilter-dropdown { /* align with #qunit-modulefilter-search */ box-sizing: border-box; - min-width: 400px; - min-width: min(400px, 80vw); - max-width: 80vw; + width: 100%; position: absolute; right: 0; top: 100%; @@ -236,37 +226,25 @@ color: #0D3349; background-color: #F5F5F5; z-index: 99; + max-height: min(80vh, 700px); + overflow-y: auto; } #qunit-modulefilter-actions { display: block; overflow: auto; /* align with #qunit-modulefilter-dropdown-list */ - font: smaller/1.5em sans-serif; -} -@media (min-width: 350px) { - #qunit-modulefilter-actions { - position: absolute; - right: 0; - } + line-height: 1.5; } -#qunit-modulefilter-dropdown #qunit-modulefilter-actions > * { - box-sizing: border-box; +#qunit-modulefilter-actions button { max-height: 2.8em; - display: block; - padding: 0.4em; -} - -#qunit-modulefilter-dropdown #qunit-modulefilter-actions > button { - float: right; margin: 0.25em; } #qunit-modulefilter-dropdown-list { margin: 0; padding: 0; - font: smaller/1.5em sans-serif; } #qunit-modulefilter-dropdown-list li { @@ -289,8 +267,29 @@ color: #444; } +/** Test Result */ + +#qunit-testresult { + /* left #qunit-testresult-display, right #qunit-testresult-controls */ + display: flex; + flex-flow: row wrap; + align-items: center; + gap: 13px; +} +#qunit-testresult a { + color: #2F68DA; +} +#qunit-testresult .module-name { + font-weight: 700; +} +#qunit-testresult-controls { + max-width: 10%; +} +#qunit-testresult-controls:empty { + display: none; +} -/** Tests: Pass/Fail */ +/** Test output */ #qunit-tests { list-style-position: inside; @@ -342,8 +341,6 @@ padding: 0.5em; background-color: #FFF; - - border-radius: 5px; } .qunit-source { @@ -387,11 +384,17 @@ text-decoration: none; } -/*** Test Counts */ +/** Test output: Counts */ -#qunit-tests b.counts { color: #0D3349; } -#qunit-tests b.passed { color: #5E740B; } -#qunit-tests b.failed { color: #710909; } +#qunit-tests .counts { + color: #0D3349; +} +#qunit-tests .passed { + color: #5E740B; +} +#qunit-tests .failed { + color: #710909; +} #qunit-tests li li { padding: 5px; @@ -400,8 +403,7 @@ list-style-position: inside; } -/*** Passing Styles */ - +/** Test output: Passing */ #qunit-tests .pass { color: #2F68DA; @@ -421,9 +423,8 @@ #qunit-tests .pass .test-actual, #qunit-tests .pass .test-expected { color: #999; } -#qunit-banner.qunit-pass { background-color: #C6E746; } -/*** Failing Styles */ +/** Test output: Failing */ #qunit-tests .fail { color: #000; @@ -437,19 +438,15 @@ white-space: pre; } -#qunit-tests > li:last-child { - border-radius: 0 0 5px 5px; -} - #qunit-tests .fail .test-actual { color: #EE5757; } #qunit-tests .fail .test-expected { color: #008000; } -#qunit-banner.qunit-fail { background-color: #EE5757; } +/** Test output: Aborted */ -/*** Aborted tests */ #qunit-tests .aborted { color: #000; background-color: orange; } -/*** Skipped tests */ + +/** Test output: Skipped */ #qunit-tests .skipped { background-color: #EBECE9; @@ -463,42 +460,13 @@ color: #366097; line-height: 1.8em; padding: 0 0.5em; - margin: -0.4em 0.4em -0.4em 0; + margin: -0.4em 0 -0.4em 0; } #qunit-tests .qunit-todo-label { background-color: #EEE; } -/** Result */ - -#qunit-testresult { - color: #366097; - background-color: #E2F0F7; - - border-bottom: 1px solid #FFF; -} -#qunit-testresult a { - color: #2F68DA; -} -#qunit-testresult .clearfix { - height: 0; - clear: both; -} -#qunit-testresult .module-name { - font-weight: 700; -} -#qunit-testresult-display { - padding: 0.5em 1em 0.5em 1em; - width: 85%; - float:left; -} -#qunit-testresult-controls { - padding: 0.5em 1em 0.5em 1em; - width: 10%; - float:left; -} - /** Fixture */ #qunit-fixture { diff --git a/src/reporters/HtmlReporter.js b/src/reporters/HtmlReporter.js index ccf1e09e7..db9e6ce54 100644 --- a/src/reporters/HtmlReporter.js +++ b/src/reporters/HtmlReporter.js @@ -70,34 +70,34 @@ function getUrlConfigHtml (config) { let escapedTooltip = escapeText(val.tooltip); if (!val.value || typeof val.value === 'string') { - urlConfigHtml += "'; + urlConfigHtml += ''; } else { let selection = false; - urlConfigHtml += "'; } @@ -115,15 +115,6 @@ function getUrlConfigHtml (config) { return urlConfigHtml; } -function getProgressHtml (stats) { - return [ - stats.completed, - ' / ', - stats.defined, - ' tests completed.
    ' - ].join(''); -} - function stripHtml (string) { // Strip tags, html entity and whitespaces return string @@ -196,6 +187,9 @@ export default class HtmlReporter { this.dropdownData = null; this.element = options.element || undefined; + this.elementBanner = null; + this.elementDisplay = null; + this.elementTests = null; // TODO: Consider rendering the UI early when possible for improved UX. // NOTE: Only listen for "error" and "runStart" now. @@ -251,30 +245,28 @@ export default class HtmlReporter { // Set either true or undefined, which will now take precedence over // the original urlParams in makeUrl() this.hidepassed = value; - let tests = this.element.querySelector('#qunit-tests'); - if (tests) { - const length = tests.children.length; - const children = tests.children; - - if (field.checked) { - for (let i = 0; i < length; i++) { - const test = children[i]; - const className = test ? test.className : ''; - const classNameHasPass = className.indexOf('pass') > -1; - const classNameHasSkipped = className.indexOf('skipped') > -1; - - if (classNameHasPass || classNameHasSkipped) { - this.hiddenTests.push(test); - } + const tests = this.elementTests; + const length = tests.children.length; + const children = tests.children; + + if (field.checked) { + for (let i = 0; i < length; i++) { + const test = children[i]; + const className = test ? test.className : ''; + const classNameHasPass = className.indexOf('pass') > -1; + const classNameHasSkipped = className.indexOf('skipped') > -1; + + if (classNameHasPass || classNameHasSkipped) { + this.hiddenTests.push(test); } + } - for (const hiddenTest of this.hiddenTests) { - tests.removeChild(hiddenTest); - } - } else { - while (this.hiddenTests.length) { - tests.appendChild(this.hiddenTests.shift()); - } + for (const hiddenTest of this.hiddenTests) { + tests.removeChild(hiddenTest); + } + } else { + while (this.hiddenTests.length) { + tests.appendChild(this.hiddenTests.shift()); } } window.history.replaceState(null, '', updatedUrl); @@ -330,13 +322,10 @@ export default class HtmlReporter { abortTestsButton () { const button = document.createElement('button'); button.id = 'qunit-abort-tests-button'; - button.innerHTML = 'Abort'; + button.textContent = 'Abort'; DOM.on(button, 'click', () => { - const abortButton = this.element.querySelector('#qunit-abort-tests-button'); - if (abortButton) { - abortButton.disabled = true; - abortButton.innerHTML = 'Aborting...'; - } + button.disabled = true; + button.textContent = 'Aborting...'; this.abort(); return false; }); @@ -348,7 +337,7 @@ export default class HtmlReporter { filter.className = 'qunit-filter'; const label = document.createElement('label'); - label.innerHTML = 'Filter: '; + label.textContent = 'Filter: '; const input = document.createElement('input'); input.type = 'text'; @@ -359,7 +348,7 @@ export default class HtmlReporter { label.appendChild(input); const button = document.createElement('button'); - button.innerHTML = 'Go'; + button.textContent = 'Go'; filter.appendChild(label); filter.appendChild(document.createTextNode(' ')); @@ -427,19 +416,23 @@ export default class HtmlReporter { return html; } + const label = document.createElement('label'); + label.htmlFor = 'qunit-modulefilter-search'; + label.textContent = 'Module:\u00A0 '; + + const searchContainer = document.createElement('span'); + searchContainer.id = 'qunit-modulefilter-search-container'; + label.appendChild(searchContainer); + const moduleSearch = document.createElement('input'); + // Set type=text explicitly for ease of styling + moduleSearch.setAttribute('type', 'text'); moduleSearch.id = 'qunit-modulefilter-search'; moduleSearch.autocomplete = 'off'; 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'; - label.textContent = 'Module:'; - const searchContainer = document.createElement('span'); - searchContainer.id = 'qunit-modulefilter-search-container'; searchContainer.appendChild(moduleSearch); const applyButton = document.createElement('button'); @@ -487,8 +480,6 @@ export default class HtmlReporter { const moduleFilter = document.createElement('form'); moduleFilter.id = 'qunit-modulefilter'; moduleFilter.appendChild(label); - moduleFilter.appendChild(document.createTextNode(' ')); - moduleFilter.appendChild(searchContainer); DOM.on(moduleFilter, 'submit', this.onFilterSubmit.bind(this)); DOM.on(moduleFilter, 'reset', function () { dropdownData.selectedMap = new StringMap(initialSelected); @@ -539,13 +530,13 @@ export default class HtmlReporter { // module names, indicating how the interface works. This also makes // for a quicker interaction in the common case of small projects. // Don't mandate typing just to get the menu. - results = dropdownData.options.slice(0, 20).map(obj => { + results = dropdownData.options.slice(0, 100).map(obj => { // Fake empty results. https://github.com/farzher/fuzzysort/issues/41 return { obj: obj }; }); } else { results = fuzzysort.go(searchText, dropdownData.options, { - limit: 20, + limit: 100, key: 'name', allowTypo: true }); @@ -597,59 +588,28 @@ export default class HtmlReporter { return moduleFilter; } - appendToolbar (beginDetails) { - const toolbar = this.element.querySelector('#qunit-testrunner-toolbar'); - if (toolbar) { + appendToolbarControls (beginDetails) { + const toolbarControls = this.element.querySelector('#qunit-testrunner-toolbar'); + if (toolbarControls) { const urlConfigContainer = document.createElement('span'); + urlConfigContainer.id = 'qunit-toolbar-urlconfig'; + urlConfigContainer.className = 'qunit-url-config'; 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(this.toolbarLooseFilter()); toolbarFilters.appendChild(this.toolbarModuleFilter(beginDetails)); - const clearfix = document.createElement('div'); - clearfix.className = 'clearfix'; - - toolbar.appendChild(toolbarFilters); - toolbar.appendChild(clearfix); - } - } - - appendHeader () { - const header = this.element.querySelector('#qunit-header'); - - if (header) { - header.innerHTML = "" + header.innerHTML + - ' '; + toolbarControls.appendChild(urlConfigContainer); + toolbarControls.appendChild(toolbarFilters); } } - appendTestResults () { - const tests = this.element.querySelector('#qunit-tests'); - let result = this.element.querySelector('#qunit-testresult'); - let controls; - - if (result) { - result.parentNode.removeChild(result); - } - - if (tests) { - tests.innerHTML = ''; - result = document.createElement('div'); - result.id = 'qunit-testresult'; - result.className = 'result'; - tests.parentNode.insertBefore(result, tests); - result.innerHTML = '
    Running...
     
    ' + - '
    ' + - '
    '; - controls = this.element.querySelector('#qunit-testresult-controls'); - } - + appendTestResultControls () { + const controls = this.element.querySelector('#qunit-testresult-controls'); if (controls) { controls.appendChild(this.abortTestsButton()); } @@ -660,52 +620,41 @@ export default class HtmlReporter { if (!testId || testId.length <= 0) { return ''; } - return "
    Rerunning selected tests: " + + return '
    Rerunning selected tests: ' + escapeText(testId.join(', ')) + - " Run all tests
    "; - } - - appendUserAgent () { - const userAgent = this.element.querySelector('#qunit-userAgent'); - - if (userAgent) { - userAgent.innerHTML = ''; - userAgent.appendChild( - document.createTextNode( - 'QUnit ' + version + '; ' + navigator.userAgent - ) - ); - } + '">Run all tests
    '; } appendInterface (beginDetails) { - const qunit = this.element; - - qunit.setAttribute('role', 'main'); - // Since QUnit 1.3, these are created automatically. - qunit.innerHTML = - "

    " + escapeText(document.title) + '

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

    " + - "
      "; - - this.appendHeader(); - this.appendTestResults(); - this.appendUserAgent(); - this.appendToolbar(beginDetails); + this.element.setAttribute('role', 'main'); + this.element.innerHTML = + '
      ' + + '

      ' + escapeText(document.title) + '

      ' + + '
      ' + escapeText('QUnit ' + version + '; ' + navigator.userAgent) + '
      ' + + '
      ' + + '' + + '
        '; + + this.elementBanner = this.element.querySelector('#qunit-banner'); + this.elementDisplay = this.element.querySelector('#qunit-testresult-display'); + this.elementTests = this.element.querySelector('#qunit-tests'); + + this.appendToolbarControls(beginDetails); + this.appendTestResultControls(); } appendTest (name, testId, moduleName) { - const tests = this.element.querySelector('#qunit-tests'); - if (!tests) { - return; - } - let title = document.createElement('strong'); title.innerHTML = getNameHtml(name, moduleName); @@ -715,7 +664,7 @@ export default class HtmlReporter { // No ID or rerun link for "global failure" blocks if (testId !== undefined) { let rerunTrigger = document.createElement('a'); - rerunTrigger.innerHTML = 'Rerun'; + rerunTrigger.textContent = 'Rerun'; rerunTrigger.href = this.makeUrl({ testId: testId }); testBlock.id = 'qunit-test-output-' + testId; @@ -727,7 +676,7 @@ export default class HtmlReporter { testBlock.appendChild(assertList); - tests.appendChild(testBlock); + this.elementTests.appendChild(testBlock); return testBlock; } @@ -764,7 +713,7 @@ export default class HtmlReporter { const href = this.makeUrl({ testId: failedTests }); return [ - "
        ", + '
        ', failedTests.length === 1 ? 'Rerun 1 failed test' : 'Rerun ' + failedTests.length + ' failed tests', @@ -782,8 +731,6 @@ export default class HtmlReporter { return sec + (sec === 1 ? ' second' : ' seconds'); } - const banner = this.element.querySelector('#qunit-banner'); - const tests = this.element.querySelector('#qunit-tests'); const abortButton = this.element.querySelector('#qunit-abort-tests-button'); let html = [ '', runEnd.testCounts.total, ' tests completed in ', @@ -803,48 +750,47 @@ export default class HtmlReporter { if (abortButton && abortButton.disabled) { html = 'Tests aborted after ' + msToSec(runEnd.runtime) + '.'; - for (let i = 0; i < tests.children.length; i++) { - test = tests.children[i]; + for (let i = 0; i < this.elementTests.children.length; i++) { + test = this.elementTests.children[i]; if (test.className === '' || test.className === 'running') { test.className = 'aborted'; assertList = test.getElementsByTagName('ol')[0]; assertLi = document.createElement('li'); assertLi.className = 'fail'; - assertLi.innerHTML = 'Test aborted.'; + assertLi.textContent = 'Test aborted.'; assertList.appendChild(assertLi); } } } - if (banner && (!abortButton || abortButton.disabled === false)) { - banner.className = runEnd.status === 'failed' ? 'qunit-fail' : 'qunit-pass'; + if (!abortButton || abortButton.disabled === false) { + this.elementBanner.className = runEnd.status === 'failed' ? 'qunit-fail' : 'qunit-pass'; } if (abortButton) { abortButton.parentNode.removeChild(abortButton); } - if (tests) { - this.element.querySelector('#qunit-testresult-display').innerHTML = html; - } + this.elementDisplay.innerHTML = html; } onTestStart (details) { this.appendTest(details.name, details.testId, details.module); - let running = this.element.querySelector('#qunit-testresult-display'); - - if (running) { - DOM.addClass(running, 'running'); + this.elementDisplay.className = 'running'; + this.elementDisplay.innerHTML = [ + details.previousFailure + ? 'Rerunning previously failed test:
        ' + : `Running test ${this.stats.completed} of ${this.stats.defined}:
        `, + getNameHtml(details.name, details.module), + this.getRerunFailedHtml(this.stats.failedTests) + ].join(''); - running.innerHTML = [ - getProgressHtml(this.stats), - details.previousFailure - ? 'Rerunning previously failed test:
        ' - : 'Running: ', - getNameHtml(details.name, details.module), - this.getRerunFailedHtml(this.stats.failedTests) - ].join(''); + if (this.elementBanner.style.setProperty) { + this.elementBanner.style.setProperty( + '--qunit-progress', + Math.ceil(((this.stats.completed + 1) / this.stats.defined) * 100) + '%' + ); } } @@ -855,8 +801,8 @@ export default class HtmlReporter { } let message = escapeText(details.message) || (details.result ? 'okay' : 'failed'); - message = "" + message + ''; - message += "@ " + details.runtime + ' ms'; + message = '' + message + ''; + message += '@ ' + details.runtime + ' ms'; let expected; let actual; @@ -874,12 +820,12 @@ export default class HtmlReporter { } actual = dump.parse(details.actual); - message += "' + + '
        Expected:
        " +
        +      message += '';
         
               if (actual !== expected) {
        -        message += "';
         
                 let showDiff = false;
        @@ -902,26 +848,26 @@ export default class HtmlReporter {
                 }
         
                 if (showDiff) {
        -          message += "';
                 }
               } else if (expected.indexOf('[object Array]') !== -1 ||
                 expected.indexOf('[object Object]') !== -1) {
        -        message += "';
        +        message += '';
               } else {
        -        message += "';
        +        message += '';
               }
         
               if (details.source) {
        -        message += "';
        +        message += '';
               }
         
               message += '
        Expected:
        ' +
               escapeText(expected) +
               '
        Result:
        " +
        +        message += '
        Result:
        ' +
                   escapeText(actual) + '
        Diff:
        " +
        +          message += '
        Diff:
        ' +
                     diffHtml + '
        Message: " + - 'Diff suppressed as the depth of object is more than current max depth (' + - this.config.maxDepth + ').

        Hint: Use QUnit.dump.maxDepth to ' + - " run with a higher max depth or " + - 'Rerun without max depth.

        Message: ' + + 'Diff suppressed as the depth of object is more than current max depth (' + + this.config.maxDepth + ').

        Hint: Use QUnit.dump.maxDepth to ' + + ' run with a higher max depth or " + + 'Rerun without max depth.

        Message: " + - 'Diff suppressed as the expected and actual results have an equivalent' + - ' serialization
        Message: ' + + 'Diff suppressed as the expected and actual results have an equivalent' + + ' serialization
        Source:
        " +
        -        escapeText(details.source) + '
        Source:
        ' +
        +          escapeText(details.source) + '
        '; @@ -929,9 +875,9 @@ export default class HtmlReporter { // This occurs when pushFailure is called and we have an extracted stack trace } else if (!details.result && details.source) { message += '' + - "' + - '
        Source:
        " +
        -      escapeText(details.source) + '
        '; + '
        Source:
        ' +
        +        escapeText(details.source) + '
        '; } let assertList = testItem.getElementsByTagName('ol')[0]; @@ -943,9 +889,8 @@ export default class HtmlReporter { } onTestDone (details) { - const tests = this.element.querySelector('#qunit-tests'); - const testItem = this.element.querySelector('#qunit-test-output-' + details.testId); - if (!tests || !testItem) { + const testItem = this.elementTests.querySelector('#qunit-test-output-' + details.testId); + if (!testItem) { return; } @@ -988,10 +933,10 @@ export default class HtmlReporter { // The testItem.firstChild is the test name let testTitle = testItem.firstChild; let badGoodCounts = bad - ? "" + bad + ', ' + "" + good + ', ' + ? '' + bad + ', ' + '' + good + ', ' : ''; - testTitle.innerHTML += " (" + badGoodCounts + details.total + ')'; + testTitle.innerHTML += ' (' + badGoodCounts + details.total + ')'; this.stats.completed++; @@ -999,8 +944,9 @@ export default class HtmlReporter { testItem.className = 'skipped'; let skipped = document.createElement('em'); skipped.className = 'qunit-skipped-label'; - skipped.innerHTML = 'skipped'; + skipped.textContent = 'skipped'; testItem.insertBefore(skipped, testTitle); + testItem.insertBefore(document.createTextNode(' '), testTitle); } else { DOM.on(testTitle, 'click', function () { DOM.toggleClass(assertList, 'qunit-collapsed'); @@ -1011,14 +957,15 @@ export default class HtmlReporter { if (details.todo) { const todoLabel = document.createElement('em'); todoLabel.className = 'qunit-todo-label'; - todoLabel.innerHTML = 'todo'; + todoLabel.textContent = 'todo'; testItem.className += ' todo'; testItem.insertBefore(todoLabel, testTitle); + testItem.insertBefore(document.createTextNode(' '), testTitle); } let time = document.createElement('span'); time.className = 'runtime'; - time.innerHTML = details.runtime + ' ms'; + time.textContent = details.runtime + ' ms'; testItem.insertBefore(time, assertList); } @@ -1041,12 +988,12 @@ export default class HtmlReporter { // use removeChild instead of remove because of support this.hiddenTests.push(testItem); - tests.removeChild(testItem); + this.elementTests.removeChild(testItem); } } onError (error) { - const testItem = this.element && this.appendTest('global failure'); + const testItem = this.elementTests && this.appendTest('global failure'); if (!testItem) { // HTML Reporter is probably disabled or not yet initialized. // This kind of early error will be visible in the browser console @@ -1058,10 +1005,10 @@ export default class HtmlReporter { // Render similar to a failed assertion (see above QUnit.log callback) let message = escapeText(errorString(error)); - message = "" + message + ''; + message = '' + message + ''; if (error && error.stack) { message += '' + - "' + '
        Source:
        " +
        +        '
        Source:
        ' +
                 escapeText(error.stack) + '
        '; } @@ -1080,10 +1027,10 @@ function getNameHtml (name, module) { let nameHtml = ''; if (module) { - nameHtml = "" + escapeText(module) + ': '; + nameHtml = '' + escapeText(module) + ': '; } - nameHtml += "" + escapeText(name) + ''; + nameHtml += '' + escapeText(name) + ''; return nameHtml; } diff --git a/test/main/HtmlReporter.js b/test/main/HtmlReporter.js index d5aa50d75..29f57beb7 100644 --- a/test/main/HtmlReporter.js +++ b/test/main/HtmlReporter.js @@ -107,7 +107,7 @@ QUnit.test('testresult-display [begin]', function (assert) { var testresult = element.querySelector('#qunit-testresult'); assert.equal(testresult.className, 'result', 'testresult class'); - assert.equal(testresult.textContent, 'Running...\u00A0Abort', 'testresult text'); + assert.equal(testresult.textContent, 'AbortRunning...\u00A0', 'testresult text'); var display = element.querySelector('#qunit-testresult-display'); assert.equal(display.className, '', 'display class'); @@ -129,7 +129,7 @@ QUnit.test('testresult-display [testStart]', function (assert) { var display = element.querySelector('#qunit-testresult-display'); assert.equal(display.className, 'running', 'display class'); - assert.equal(display.textContent, '1 / 4 tests completed.Running: B', 'display text'); + assert.equal(display.textContent, 'Running test 1 of 4: B', 'display text'); var testOutput = element.querySelector('#qunit-test-output-00A'); assert.equal(