/** @odoo-module */ import { Component, onWillRender, useState, xml } from "@odoo/owl"; import { parseRegExp } from "../../hoot-dom/hoot_dom_utils"; import { Test } from "../core/test"; import { EXCLUDE_PREFIX } from "../core/url"; import { formatTime, getFuzzyScore, normalize } from "../hoot_utils"; import { HootLogCounters } from "./hoot_log_counters"; import { HootJobButtons } from "./hoot_job_buttons"; import { HootTechnicalValue } from "./hoot_technical_value"; import { HootTestPath } from "./hoot_test_path"; import { HootTestResult } from "./hoot_test_result"; /** * @typedef {{ * }} HootReportingProps * * @typedef {import("../core/test").Test} Test */ //----------------------------------------------------------------------------- // Global //----------------------------------------------------------------------------- const { Boolean, RegExp } = globalThis; //----------------------------------------------------------------------------- // Internal //----------------------------------------------------------------------------- /** * @param {keyof import("../core/runner").Runner["state"]} varName * @param {string} colorClassName */ const issueTemplate = (varName, colorClassName) => /* xml */ `

Global (x) : stack trace available in the console

`; const sortByDurationAscending = (a, b) => a.duration - b.duration; const sortByDurationDescending = (a, b) => b.duration - a.duration; const COLORS = { failed: "text-fail", passed: "text-pass", skipped: "text-skip", todo: "text-todo", }; //----------------------------------------------------------------------------- // Exports //----------------------------------------------------------------------------- /** @extends {Component} */ export class HootReporting extends Component { static components = { HootLogCounters, HootJobButtons, HootTechnicalValue, HootTestPath, HootTestResult, }; static props = {}; static template = xml`
${issueTemplate("globalErrors", "fail")} ${issueTemplate("globalWarnings", "abort")}
skipped aborted after
No tests found matching in suite .

/ tests completed

  • tests passed
  • tests failed
  • tests skipped
  • tests to do
`; Test = Test; formatTime = formatTime; setup() { const { runner, ui } = this.env; this.config = useState(runner.config); this.runnerReporting = useState(runner.reporting); this.runnerState = useState(runner.state); this.state = useState({ /** @type {string[]} */ openGroups: [], /** @type {string[]} */ openTests: [], }); this.uiState = useState(ui); const { showdetail } = this.config; let didShowDetail = false; runner.afterPostTest((test) => { if ( showdetail && !(showdetail === "first-fail" && didShowDetail) && [Test.FAILED, Test.ABORTED].includes(test.status) ) { didShowDetail = true; this.state.openTests.push(test.id); } }); onWillRender(() => { this.filteredResults = this.computeFilteredResults(); this.uiState.totalResults = this.filteredResults.length; }); } computeFilteredResults() { const { selectedSuiteId, sortResults, statusFilter } = this.uiState; const queryFilter = this.getQueryFilter(); const results = []; for (const test of this.runnerState.done) { let matchFilter = false; switch (statusFilter) { case "failed": { matchFilter = !test.config.skip && test.results.some((r) => !r.pass); break; } case "passed": { matchFilter = !test.config.todo && !test.config.skip && test.results.every((r) => r.pass); break; } case "skipped": { matchFilter = test.config.skip; break; } case "todo": { matchFilter = test.config.todo; break; } default: { matchFilter = Boolean(selectedSuiteId) || test.results.some((r) => !r.pass); break; } } if (matchFilter && selectedSuiteId) { matchFilter = test.path.some((suite) => suite.id === selectedSuiteId); } if (matchFilter && queryFilter) { matchFilter = queryFilter(test.key); } if (!matchFilter) { continue; } results.push({ duration: test.lastResults?.duration, status: test.status, id: `test#${test.id}`, test: test, }); } if (!sortResults) { return results; } return results.sort( sortResults === "asc" ? sortByDurationAscending : sortByDurationDescending ); } /** * @param {typeof this.uiState.statusFilter} status */ filterResults(status) { this.uiState.resultsPage = 0; if (this.uiState.statusFilter === status) { this.uiState.statusFilter = null; } else { this.uiState.statusFilter = status; } } getEmptyMessage() { const { selectedSuiteId, statusFilter } = this.uiState; if (!statusFilter && !selectedSuiteId) { return null; } return { statusFilter, statusFilterClassName: COLORS[statusFilter], filter: this.config.filter, selectedSuiteName: selectedSuiteId && this.env.runner.suites.get(selectedSuiteId).name, }; } getQueryFilter() { const { filter } = this.config; if (!filter) { return null; } const nFilter = parseRegExp(normalize(filter), { safe: true }); if (nFilter instanceof RegExp) { return (key) => nFilter.test(key); } const isExcluding = nFilter.startsWith(EXCLUDE_PREFIX); const pattern = isExcluding ? nFilter.slice(EXCLUDE_PREFIX.length) : nFilter; return (key) => getFuzzyScore(pattern, key) > 0; } /** * @param {PointerEvent} ev * @param {string} id */ toggleGroup(ev, id) { const index = this.state.openGroups.indexOf(id); if (ev.altKey) { if (index in this.state.openGroups) { this.state.openGroups = []; } else { this.state.openGroups = this.filteredResults .filter((r) => r.suite) .map((r) => r.suite.id); } } else { if (index in this.state.openGroups) { this.state.openGroups.splice(index, 1); } else { this.state.openGroups.push(id); } } } }