/** @odoo-module */ import { Component, onWillRender, useEffect, useRef, useState, xml } from "@odoo/owl"; import { Test } from "../core/test"; import { formatTime } from "../hoot_utils"; import { getTitle, setTitle } from "../mock/window"; import { getColors } from "./hoot_colors"; import { HootTestPath } from "./hoot_test_path"; /** * @typedef {{ * }} HootStatusPanelProps */ //----------------------------------------------------------------------------- // Global //----------------------------------------------------------------------------- const { Object: { values: $values }, Math: { ceil: $ceil, floor: $floor, max: $max, min: $min, random: $random }, clearInterval, document, performance, setInterval, } = globalThis; /** @type {Performance["now"]} */ const $now = performance.now.bind(performance); //----------------------------------------------------------------------------- // Internal //----------------------------------------------------------------------------- /** * @param {number} min * @param {number} max */ const randInt = (min, max) => $floor($random() * (max - min + 1)) + min; /** * @param {string} content */ const spawnIncentive = (content) => { const incentive = document.createElement("div"); const params = [ `--_content: '${content}'`, `--_fly-duration: ${randInt(2_000, 3_000)}`, `--_size: ${randInt(32, 48)}`, `--_wiggle-duration: ${randInt(800, 2_000)}`, `--_wiggle-range: ${randInt(5, 30)}`, `--_x: ${randInt(0, 100)}`, `--_y: ${randInt(100, 150)}`, ]; incentive.setAttribute("class", `incentive fixed`); incentive.setAttribute("style", params.join(";")); /** @param {AnimationEvent} ev */ const onEnd = (ev) => ev.animationName === "animation-incentive-travel" && incentive.remove(); incentive.addEventListener("animationend", onEnd); incentive.addEventListener("animationcancel", onEnd); document.querySelector("hoot-container").shadowRoot.appendChild(incentive); }; /** * @param {boolean} failed */ const updateTitle = (failed) => { const toAdd = failed ? TITLE_PREFIX.fail : TITLE_PREFIX.pass; let title = getTitle(); if (title.startsWith(toAdd)) { return; } for (const prefix of $values(TITLE_PREFIX)) { if (title.startsWith(prefix)) { title = title.slice(prefix.length); break; } } setTitle(`${toAdd} ${title}`); }; const TIMER_PRECISION = 100; // in ms const TITLE_PREFIX = { fail: "✖", pass: "✔", }; //----------------------------------------------------------------------------- // Exports //----------------------------------------------------------------------------- /** @extends {Component} */ export class HootStatusPanel extends Component { static components = { HootTestPath }; static props = {}; static template = xml`
Ready
/
`; formatTime = formatTime; setup() { const startTimer = () => { stopTimer(); currentTestStart = $now(); intervalId = setInterval(() => { this.state.timer = $floor(($now() - currentTestStart) / TIMER_PRECISION) * TIMER_PRECISION; }, TIMER_PRECISION); }; const stopTimer = () => { if (intervalId) { clearInterval(intervalId); intervalId = 0; } this.state.timer = 0; }; const { runner, ui } = this.env; this.canvasRef = useRef("progress-canvas"); this.runnerReporting = useState(runner.reporting); this.runnerState = useState(runner.state); this.state = useState({ className: "", timer: null, }); this.uiState = useState(ui); this.progressBarIndex = 0; let currentTestStart; let intervalId = 0; runner.beforeAll(() => { this.state.debug = runner.debug; }); runner.afterAll(() => { if (!runner.config.headless) { stopTimer(); } updateTitle(this.runnerReporting.failed > 0); if (runner.config.fun) { for (let i = 0; i < this.runnerReporting.failed; i++) { spawnIncentive("😭"); } for (let i = 0; i < this.runnerReporting.passed; i++) { spawnIncentive("🦉"); } } }); if (!runner.config.headless) { runner.beforeEach(startTimer); runner.afterPostTest(stopTimer); } useEffect( (el) => { if (el) { [el.width, el.height] = [el.clientWidth, el.clientHeight]; el.getContext("2d").clearRect(0, 0, el.width, el.height); } }, () => [this.canvasRef.el] ); onWillRender(() => this.updateProgressBar()); } /** * @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; } } getLastPage() { const { resultsPerPage, totalResults } = this.uiState; return $max($floor((totalResults - 1) / resultsPerPage), 0); } nextPage() { this.uiState.resultsPage = $min(this.uiState.resultsPage + 1, this.getLastPage()); } previousPage() { this.uiState.resultsPage = $max(this.uiState.resultsPage - 1, 0); } sortResults() { this.uiState.resultsPage = 0; if (!this.uiState.sortResults) { this.uiState.sortResults = "desc"; } else if (this.uiState.sortResults === "desc") { this.uiState.sortResults = "asc"; } else { this.uiState.sortResults = false; } } updateProgressBar() { const canvas = this.canvasRef.el; if (!canvas) { return; } const ctx = canvas.getContext("2d"); const { width, height } = canvas; const { done, tests } = this.runnerState; const doneList = [...done]; const cellSize = width / tests.length; const colors = getColors(); while (this.progressBarIndex < done.size) { const test = doneList[this.progressBarIndex]; const x = $floor(this.progressBarIndex * cellSize); switch (test.status) { case Test.ABORTED: ctx.fillStyle = colors.abort; break; case Test.FAILED: ctx.fillStyle = colors.fail; break; case Test.PASSED: ctx.fillStyle = test.config.todo ? colors.todo : colors.pass; break; case Test.SKIPPED: ctx.fillStyle = colors.skip; break; } ctx.fillRect(x, 0, $ceil(cellSize), height); this.progressBarIndex++; } } }