From 1551120536f6f572a3bb5656db566f0a1bb217d8 Mon Sep 17 00:00:00 2001 From: Timo Tijhof Date: Sun, 14 Jul 2024 01:00:16 +0100 Subject: [PATCH] HTML Reporter: Restore "running" class on test item == Background In QUnit 1.16.0, as part of https://github.com/qunitjs/qunit/issues/677, commit 168b048c68 changed the testStart() callback to no longer always create `qunit-test-output-TESTID` with class "running", but instead eagerly create them ahead of time via a QUnit.begin() handler (without the "running" class), and then during testStart() set the class to "running", or lazily create the item then. This introduced a bug where, late-defined tests lack the "running" class. This bug then later spread to all items as we now treat all tests as late-defined. In QUnit 2.8.0, as part of https://github.com/qunitjs/qunit/pull/1323, this was attempted to be restored, but it added it to a different element, namely `#qunit-testresult-display` which from what I could find in our history, never previously carried this class. This might've been done by acciddent, but in any case, let's keep that. == What * Restore this to simplify the CSS, and makes it more developer-friendly to theme QUnit by not having to list all statuses, and/or having to use a direct-child selector like `#qunit-tests > li` to reliably select test items (and not other lists like the assertion list). * For back-compat, keep the running class on `qunit-testresult-display` but move it to onBegin() since there is no reason to set this again and again on every test since it never changes. * Document these bits in the recently created "Theme API" section. --- docs/browser.md | 6 +++ src/qunit.css | 99 ++++++++++++++++------------------- src/reporters/HtmlReporter.js | 68 ++++++++++++------------ test/main/HtmlReporter.js | 2 +- 4 files changed, 88 insertions(+), 87 deletions(-) diff --git a/docs/browser.md b/docs/browser.md index 8754f8c5a..414b1d2c5 100644 --- a/docs/browser.md +++ b/docs/browser.md @@ -235,6 +235,12 @@ The following selectors are considered stable and supported: | `#qunit-modulefilter-dropdown` | Module selector, dropdown menu. | `#qunit-modulefilter-actions`

`#qunit-modulefilter-actions button` | Module selector, top area of dropdown menu with "Reset" and "Apply" buttons. | `#qunit-modulefilter .clickable`

`#qunit-modulefilter .clickable.checked` | Module selector, options in the dropdown menu. +| `#qunit-tests` | List of test items. +| `#qunit-tests > li`

QUnit 3:
`.qunit-test` | Test item. +| `#qunit-tests > li:not(.skipped):not(.pass):not(.fail)`

QUnit 3:
`.qunit-test.running`| Currently running test. +| `#qunit-tests > li.pass`
`#qunit-tests > li.fail`
`#qunit-tests > li.skipped`

QUnit 3:
`.qunit-test.pass`
`.qunit-test.fail`
`.qunit-test.skipped` | Test by one of the three mutually-exclusive outcomes (pass, fail, skipped). +| `#qunit-tests > li.todo`

QUnit 3:
`.qunit-test.todo` | Test was marked as "todo". It will also have the `pass` or `fail` class. +| `.qunit-assert-list` | List of one assertions under a given test. {:class="table-style-api"} ### HTML API diff --git a/src/qunit.css b/src/qunit.css index 4e20ab4db..0c49909b8 100644 --- a/src/qunit.css +++ b/src/qunit.css @@ -295,44 +295,32 @@ body { list-style-position: inside; } -#qunit-tests li { +.qunit-test { padding: 0.4em 1em 0.4em 1em; border-bottom: 1px solid #FFF; list-style-position: inside; } -#qunit-tests > li { +.qunit-test.running { display: none; } -#qunit-tests li.running, -#qunit-tests li.pass, -#qunit-tests li.fail, -#qunit-tests li.skipped, -#qunit-tests li.aborted { - display: list-item; -} - -#qunit-tests li .qunit-test-name { +.qunit-test:not(.skipped) .qunit-test-name { cursor: pointer; } -#qunit-tests li.skipped .qunit-test-name { - cursor: default; -} - -#qunit-tests li a { +.qunit-test a { padding: 0.5em; color: inherit; text-decoration: underline; user-select: none; } -#qunit-tests li a:hover, -#qunit-tests li a:focus { +.qunit-test a:hover, +.qunit-test a:focus { color: #0D3349; } -#qunit-tests li .runtime { +.qunit-test .runtime { float: right; font-size: smaller; user-select: none; @@ -345,42 +333,49 @@ body { background-color: #FFF; } +.qunit-collapsed { + display: none; +} + .qunit-source { margin: 0.6em 0 0.3em; } -.qunit-collapsed { - display: none; +.qunit-assert-list li { + padding: 5px; + background-color: #FFF; + border-bottom: none; + list-style-position: inside; } -#qunit-tests table { +.qunit-assert-list table { border-collapse: collapse; margin-top: 0.2em; } -#qunit-tests th { +.qunit-assert-list th { text-align: right; vertical-align: top; padding: 0 0.5em 0 0; } -#qunit-tests td { +.qunit-assert-listt td { vertical-align: top; } -#qunit-tests pre { +.qunit-assert-list pre { margin: 0; white-space: pre-wrap; word-wrap: break-word; } -#qunit-tests del { +.qunit-assert-list del { color: #374E0C; background-color: #E0F2BE; text-decoration: none; } -#qunit-tests ins { +.qunit-assert-list ins { color: #500; background-color: #FFCACA; text-decoration: none; @@ -388,74 +383,72 @@ body { /** Test output: Counts */ -#qunit-tests .counts { +.qunit-test .counts { color: #0D3349; } -#qunit-tests .passed { +.qunit-test .passed { color: #5E740B; } -#qunit-tests .failed { +.qunit-test .failed { color: #710909; } -#qunit-tests li li { - padding: 5px; - background-color: #FFF; - border-bottom: none; - list-style-position: inside; -} - /** Test output: Passing */ -#qunit-tests .pass { +.qunit-test.pass { color: #2F68DA; background-color: #E2F0F7; } -#qunit-tests .pass .test-name { +.qunit-test.pass .test-name { color: #366097; } -#qunit-tests li li.pass { +.qunit-assert-list .pass { color: #3C510C; background-color: #FFF; border-left: 10px solid #C6E746; } -#qunit-tests .pass .test-actual, -#qunit-tests .pass .test-expected { color: #999; } +.qunit-test.pass .test-actual, +.qunit-test.pass .test-expected { color: #999; } /** Test output: Failing */ -#qunit-tests .fail { +.qunit-test.fail { color: #000; background-color: #EE5757; } -#qunit-tests li li.fail { +.qunit-assert-list .fail { color: #710909; background-color: #FFF; border-left: 10px solid #EE5757; white-space: pre; } - -#qunit-tests .fail .test-actual { color: #EE5757; } -#qunit-tests .fail .test-expected { color: #008000; } - +.qunit-assert-list .fail .test-actual { + color: #EE5757; +} +.qunit-assert-list .fail .test-expected { + color: #008000; +} /** Test output: Aborted */ -#qunit-tests .aborted { color: #000; background-color: orange; } +.qunit-test.aborted { + color: #000; + background-color: orange; +} /** Test output: Skipped */ -#qunit-tests .skipped { +.qunit-test.skipped { background-color: #EBECE9; } -#qunit-tests .qunit-todo-label, -#qunit-tests .qunit-skipped-label { +.qunit-test .qunit-todo-label, +.qunit-test .qunit-skipped-label { background-color: #F4FF77; display: inline-block; font-style: normal; @@ -465,7 +458,7 @@ body { margin: -0.4em 0 -0.4em 0; } -#qunit-tests .qunit-todo-label { +.qunit-test .qunit-todo-label { background-color: #EEE; } diff --git a/src/reporters/HtmlReporter.js b/src/reporters/HtmlReporter.js index 8f4b3e5de..fb5ad0310 100644 --- a/src/reporters/HtmlReporter.js +++ b/src/reporters/HtmlReporter.js @@ -660,6 +660,7 @@ export default class HtmlReporter { title.innerHTML = getNameHtml(name, moduleName); let testBlock = document.createElement('li'); + testBlock.className = 'qunit-test running'; testBlock.appendChild(title); // No ID or rerun link for "global failure" blocks @@ -674,7 +675,6 @@ export default class HtmlReporter { let assertList = document.createElement('ol'); assertList.className = 'qunit-assert-list'; - testBlock.appendChild(assertList); this.elementTests.appendChild(testBlock); @@ -705,6 +705,7 @@ export default class HtmlReporter { // https://github.com/qunitjs/qunit/issues/1657 onBegin (beginDetails) { this.appendInterface(beginDetails); + this.elementDisplay.className = 'running'; } getRerunFailedHtml (failedTests) { @@ -750,7 +751,7 @@ export default class HtmlReporter { for (let i = 0; i < this.elementTests.children.length; i++) { const test = this.elementTests.children[i]; - if (test.className === '' || test.className === 'running') { + if (DOM.hasClass(test.className, 'running')) { test.className = 'aborted'; const assertList = test.getElementsByTagName('ol')[0]; const assertLi = document.createElement('li'); @@ -775,7 +776,6 @@ export default class HtmlReporter { onTestStart (details) { this.appendTest(details.name, details.testId, details.module); - this.elementDisplay.className = 'running'; this.elementDisplay.innerHTML = [ details.previousFailure ? 'Rerunning previously failed test:
' @@ -892,71 +892,74 @@ export default class HtmlReporter { return; } - DOM.removeClass(testItem, 'running'); - + // This test passed if it has no unexpected failed assertions + // TODO: Add "status" from TestReport#getStatus() to testDone() and use that. let status; - if (details.failed > 0) { - status = 'failed'; - } else if (details.todo) { - status = 'todo'; + if (details.skipped) { + status = 'skipped'; } else { - status = details.skipped ? 'skipped' : 'passed'; + const passed = (details.failed > 0 ? details.todo : !details.todo); + status = !passed ? 'failed' : (details.todo ? 'todo' : 'passed'); } + const testPassed = status !== 'failed'; - let assertList = testItem.getElementsByTagName('ol')[0]; - - let good = details.passed; - let bad = details.failed; + this.stats.completed++; + if (!testPassed) { + this.stats.failedTests.push(details.testId); + } - // This test passed if it has no unexpected failed assertions - const testPassed = details.failed > 0 ? details.todo : !details.todo; + // The testItem.firstChild is the test name + let testTitle = testItem.firstChild; + let assertList = testItem.getElementsByTagName('ol')[0]; + // Collapse passing tests by default if (testPassed) { - // Collapse the passing tests DOM.addClass(assertList, 'qunit-collapsed'); } else { - this.stats.failedTests.push(details.testId); - if (this.config.collapse) { if (!this.collapseNext) { // Skip collapsing the first failing test this.collapseNext = true; } else { - // Collapse remaining tests + // Collapse subsequent failing tests DOM.addClass(assertList, 'qunit-collapsed'); } } } + if (status !== 'skipped') { + DOM.on(testTitle, 'click', function () { + DOM.toggleClass(assertList, 'qunit-collapsed'); + }); + } - // The testItem.firstChild is the test name - let testTitle = testItem.firstChild; + let good = details.passed; + let bad = details.failed; let badGoodCounts = bad ? '' + bad + ', ' + '' + good + ', ' : ''; testTitle.innerHTML += ' (' + badGoodCounts + details.total + ')'; - this.stats.completed++; + DOM.removeClass(testItem, 'running'); + + if (status === 'skipped') { + DOM.addClass(testItem, 'skipped'); - if (details.skipped) { - testItem.className = 'skipped'; let skipped = document.createElement('em'); skipped.className = 'qunit-skipped-label'; skipped.textContent = 'skipped'; testItem.insertBefore(skipped, testTitle); testItem.insertBefore(document.createTextNode(' '), testTitle); } else { - DOM.on(testTitle, 'click', function () { - DOM.toggleClass(assertList, 'qunit-collapsed'); - }); - - testItem.className = testPassed ? 'pass' : 'fail'; + DOM.addClass(testItem, testPassed ? 'pass' : 'fail'); if (details.todo) { + // Add label both for status=todo (passing) and for status=failed on a todo test. + DOM.addClass(testItem, 'todo'); + const todoLabel = document.createElement('em'); todoLabel.className = 'qunit-todo-label'; todoLabel.textContent = 'todo'; - testItem.className += ' todo'; testItem.insertBefore(todoLabel, testTitle); testItem.insertBefore(document.createTextNode(' '), testTitle); } @@ -983,9 +986,8 @@ export default class HtmlReporter { const hidepassed = (this.hidepassed !== null ? this.hidepassed : this.config.hidepassed); if (hidepassed && (status === 'passed' || details.skipped)) { - // use removeChild instead of remove because of support this.hiddenTests.push(testItem); - + // use removeChild() instead of remove() for wider browser support this.elementTests.removeChild(testItem); } } diff --git a/test/main/HtmlReporter.js b/test/main/HtmlReporter.js index 4238d4d42..54911de94 100644 --- a/test/main/HtmlReporter.js +++ b/test/main/HtmlReporter.js @@ -110,7 +110,7 @@ QUnit.test('testresult-display [begin]', function (assert) { assert.equal(testresult.textContent, 'AbortRunning...\u00A0', 'testresult text'); var display = element.querySelector('#qunit-testresult-display'); - assert.equal(display.className, '', 'display class'); + assert.equal(display.className, 'running', 'display class'); assert.equal(display.textContent, 'Running...\u00A0', 'display text'); });