// Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only import { RunnerStatus, TestStatus } from './batchedtestrunner.js' class AttentionType { static None = 1; static Bad = 2; static Good = 3; static Warning = 4; static Info = 5; static Ignore = 6; }; export class IncidentType { // See QAbstractTestLogger::IncidentTypes (and keep in sync with it): static Pass = 'pass'; static Fail = 'fail'; static Skip = 'skip'; static XFail = 'xfail'; static XPass = 'xpass'; static BlacklistedPass = 'bpass'; static BlacklistedFail = 'bfail'; static BlacklistedXPass = 'bxpass'; static BlacklistedXFail = 'bxfail'; // The following is not mapped from QAbstractTestLogger::IncidentTypes and is used internally: static None = 'none'; static values() { return Object.getOwnPropertyNames(IncidentType) .filter( propertyName => ['length', 'prototype', 'values', 'name'].indexOf(propertyName) === -1) .map(propertyName => IncidentType[propertyName]); } } class OutputArea { #outputDiv; constructor() { this.#outputDiv = document.createElement('div'); this.#outputDiv.classList.add('output-area'); this.#outputDiv.classList.add('light-background'); document.querySelector('body').appendChild(this.#outputDiv); } addOutput(text, attentionType) { const newContentWrapper = document.createElement('span'); newContentWrapper.className = 'output-line'; newContentWrapper.innerText = text; switch (attentionType) { case AttentionType.Bad: newContentWrapper.classList.add('bad'); break; case AttentionType.Good: newContentWrapper.classList.add('good'); break; case AttentionType.Warning: newContentWrapper.classList.add('warning'); break case AttentionType.Info: newContentWrapper.classList.add('info'); break; case AttentionType.Ignore: newContentWrapper.classList.add('ignore'); break; default: break; } this.#outputDiv.appendChild(newContentWrapper); } } class Counter { #count = 0; #decriptionElement; #counterElement; constructor(parentElement, incidentType) { this.#decriptionElement = document.createElement('span'); this.#decriptionElement.classList.add(incidentType); this.#decriptionElement.classList.add('zero'); this.#decriptionElement.innerText = Counter.#humanReadableIncidentName(incidentType); parentElement.appendChild(this.#decriptionElement); this.#counterElement = document.createElement('span'); this.#counterElement.classList.add(incidentType); this.#counterElement.classList.add('zero'); parentElement.appendChild(this.#counterElement); } increment() { if (!this.#count++) { this.#decriptionElement.classList.remove('zero'); this.#counterElement.classList.remove('zero'); } this.#counterElement.innerText = this.#count; } static #humanReadableIncidentName(incidentName) { switch (incidentName) { case IncidentType.Pass: return 'Passed'; case IncidentType.Fail: return 'Failed'; case IncidentType.Skip: return 'Skipped'; case IncidentType.XFail: return 'Known failure'; case IncidentType.XPass: return 'Unexpectedly passed'; case IncidentType.BlacklistedPass: return 'Blacklisted passed'; case IncidentType.BlacklistedFail: return 'Blacklisted failed'; case IncidentType.BlacklistedXPass: return 'Blacklisted unexpectedly passed'; case IncidentType.BlacklistedXFail: return 'Blacklisted unexpectedly failed'; case IncidentType.None: throw new Error('Incident of the None type cannot be displayed'); } } } class Counters { #contentsDiv; #counters; constructor(parentElement) { this.#contentsDiv = document.createElement('div'); this.#contentsDiv.className = 'counter-box'; parentElement.appendChild(this.#contentsDiv); const centerDiv = document.createElement('div'); this.#contentsDiv.appendChild(centerDiv); this.#counters = new Map(IncidentType.values() .filter(incidentType => incidentType !== IncidentType.None) .map(incidentType => [incidentType, new Counter(centerDiv, incidentType)])); } incrementIncidentCounter(incidentType) { this.#counters.get(incidentType).increment(); } } export class UI { #contentsDiv; #counters; #outputArea; constructor(parentElement, hasCounters) { this.#contentsDiv = document.createElement('div'); parentElement.appendChild(this.#contentsDiv); if (hasCounters) this.#counters = new Counters(this.#contentsDiv); this.#outputArea = new OutputArea(this.#contentsDiv); } get counters() { return this.#counters; } get outputArea() { return this.#outputArea; } htmlElement() { return this.#contentsDiv; } } class OutputScanner { static #supportedIncidentTypes = IncidentType.values().filter( incidentType => incidentType !== IncidentType.None); static get supportedIncidentTypes() { return this.#supportedIncidentTypes; } #regex; constructor(regex) { this.#regex = regex; } classifyOutputLine(line) { const match = this.#regex.exec(line); if (!match) return IncidentType.None; match.splice(0, 1); // Find the index of the first non-empty matching group and recover an incident type for it. return OutputScanner.supportedIncidentTypes[match.findIndex(element => !!element)]; } } class XmlOutputScanner extends OutputScanner { constructor() { // Scan for any line with an incident of type from supportedIncidentTypes. The matching // group at offset n will contain the type. The match type can be preceded by any number of // whitespace characters to factor in the indentation. super(new RegExp(`^\\s* `(${TextOutputScanner.#incidentNameMap.get(incidentType)})`).join('|')}\\s`)); } } export class ScannerFactory { static createScannerForFormat(format) { switch (format) { case 'txt': return new TextOutputScanner(); case 'xml': return new XmlOutputScanner(); default: return null; } } } export class VisualOutputProducer { #batchedTestRunner; #outputArea; #counters; #outputScanner; #processedLines; constructor(outputArea, counters, outputScanner, batchedTestRunner) { this.#outputArea = outputArea; this.#counters = counters; this.#outputScanner = outputScanner; this.#batchedTestRunner = batchedTestRunner; this.#processedLines = 0; } run() { this.#batchedTestRunner.onStatusChanged.addEventListener( status => this.#onRunnerStatusChanged(status)); this.#batchedTestRunner.onTestStatusChanged.addEventListener( (test, status) => this.#onTestStatusChanged(test, status)); this.#batchedTestRunner.onTestOutputChanged.addEventListener( (test, output) => this.#onTestOutputChanged(test, output)); const currentTest = [...this.#batchedTestRunner.results.entries()].find( entry => entry[1].status === TestStatus.Running)?.[0]; const output = this.#batchedTestRunner.results.get(currentTest)?.output; if (output) this.#onTestOutputChanged(testName, output); this.#onRunnerStatusChanged(this.#batchedTestRunner.status); } async #onRunnerStatusChanged(status) { if (RunnerStatus.Running === status) return; this.#outputArea.addOutput( `Runner exited with status: ${status}`, status === RunnerStatus.Passed ? AttentionType.Good : AttentionType.Bad); if (RunnerStatus.Error === status) this.#outputArea.addOutput(`The error was: ${this.#batchedTestRunner.errorDetails}`); } async #onTestOutputChanged(_, output) { const notSent = output.slice(this.#processedLines); for (const out of notSent) { const incidentType = this.#outputScanner?.classifyOutputLine(out); if (incidentType !== IncidentType.None) this.#counters.incrementIncidentCounter(incidentType); this.#outputArea.addOutput( out, (() => { switch (incidentType) { case IncidentType.Fail: case IncidentType.XPass: return AttentionType.Bad; case IncidentType.Pass: return AttentionType.Good; case IncidentType.XFail: return AttentionType.Warning; case IncidentType.Skip: return AttentionType.Info; case IncidentType.BlacklistedFail: case IncidentType.BlacklistedPass: case IncidentType.BlacklistedXFail: case IncidentType.BlacklistedXPass: return AttentionType.Ignore; case IncidentType.None: return AttentionType.None; } })()); } this.#processedLines = output.length; } async #onTestStatusChanged(_, status) { if (status === TestStatus.Running) this.#processedLines = 0; await new Promise(resolve => window.setTimeout(resolve, 500)); } }