frontend/src/index.js (266 lines of code) (raw):

import { REV_LATEST, DOM_READY, main, show, hide, message, getPathCoverage, getHistory, getZeroCoverageData, buildNavbar, render, getSource, getFilters, } from "./common.js"; import { buildRoute, monitorOptions, readRoute, updateRoute } from "./route.js"; import { zeroCoverageDisplay, zeroCoverageMenu, } from "./zero_coverage_report.js"; import "normalize.css/normalize.css"; import "./style.scss"; import Prism from "prismjs"; import Chartist from "chartist"; import "chartist/dist/chartist.css"; const VIEW_ZERO_COVERAGE = "zero"; const VIEW_DIRECTORY = "directory"; const VIEW_FILE = "file"; function browserMenu(revision, filters, route) { const context = { revision, platforms: filters.platforms.map((p) => { return { name: p, selected: p === route.platform, }; }), suites: filters.suites.map((s) => { return { name: s, selected: s === route.suite, }; }), }; render("menu_browser", context, "menu"); } async function graphHistory(history, path) { if (history === null) { message("warning", `No history data for ${path}`); return; } const dateStr = function (timestamp) { const date = new Date(timestamp); return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()}`; }; const data = { series: [ { name: "History", data: history.map((push) => { return { x: push.date * 1000, y: push.coverage, }; }), }, ], }; const config = { // Display dates on a linear scale axisX: { type: Chartist.FixedScaleAxis, divisor: 20, labelInterpolationFnc: dateStr, }, // Fix display bug when points are too close lineSmooth: Chartist.Interpolation.cardinal({ tension: 1, }), }; const elt = show("history").querySelector(".ct-chart"); const chart = new Chartist.Line(elt, data, config); chart.on("draw", function (evt) { if (evt.type === "point") { // Load revision from graph when a point is clicked const revision = history[evt.index].changeset; evt.element._node.onclick = function () { updateRoute({ revision }); }; // Display revision from graph when a point is overed evt.element._node.onmouseover = function () { const ctx = { revision: revision.substring(0, 12), date: dateStr(evt.value.x), }; render("history_point", ctx, "history_details"); }; } }); } async function showDirectory(dir, revision, files) { const context = { navbar: buildNavbar(dir, revision), files: files.map((file) => { file.route = buildRoute({ path: file.path, view: file.type, }); // Calc decimal range to make a nice coloration file.coveragePercent = Math.floor(file.coveragePercent); file.range = parseInt(file.coveragePercent / 10) * 10; return file; }), revision: revision || REV_LATEST, file_name() { // Build filename relative to current dir if (dir) { // Remove extra / only when present const offset = dir[dir.length - 1] === "/" ? 0 : 1; return this.path.substring(dir.length + offset); } return this.path; }, }; render("file_browser", context, "output"); } async function showFile(source, file, revision, selectedLine) { selectedLine = selectedLine !== undefined ? parseInt(selectedLine) : -1; let language; if (file.path.endsWith("cpp") || file.path.endsWith("h")) { language = "cpp"; } else if (file.path.endsWith("c")) { language = "c"; } else if ( file.path.endsWith("js") || file.path.endsWith("jsm") || file.path.endsWith("mjs") || file.path.endsWith("jsx") ) { language = "javascript"; } else if (file.path.endsWith("css")) { language = "css"; } else if (file.path.endsWith("py")) { language = "python"; } else if (file.path.endsWith("java")) { language = "java"; } const context = { navbar: buildNavbar(file.path, revision), language, lines: source.map((line, nb) => { const coverage = file.coverage[nb]; let cssClass = ""; let hits = null; if (coverage !== undefined && coverage >= 0) { cssClass = coverage > 0 ? "covered" : "uncovered"; // Build a nicer coverage string for counts if (coverage >= 1000000) { hits = { nb: parseInt(coverage / 1000000), unit: "M", }; } else if (coverage >= 1000) { hits = { nb: parseInt(coverage / 1000), unit: "k", }; } else if (coverage > 0) { hits = { nb: coverage, unit: "", }; } } // To make line numbers start from 1 nb = nb + 1; // Override css class when selected if (nb === selectedLine) { cssClass = "selected"; } return { nb, hits, coverage, line: line || " ", css_class: cssClass, route: buildRoute({ line: nb }), }; }), }; hide("message"); hide("history"); const output = render("file_coverage", context, "output"); // Scroll to line if (selectedLine > 0) { const line = output.querySelector("#l" + selectedLine); line.scrollIntoView({ behavior: "smooth", block: "center", }); } // Highlight source code once displayed Prism.highlightAll(output); } async function load() { const route = readRoute(); // Reset display, dom-safe hide("history"); hide("output"); message( "loading", "Loading coverage data for " + (route.path || "mozilla-central") + " @ " + (route.revision || REV_LATEST), ); // Load only zero coverage for that specific view if (route.view === VIEW_ZERO_COVERAGE) { const zeroCoverage = await getZeroCoverageData(); return { view: VIEW_ZERO_COVERAGE, path: route.path, zeroCoverage, route, }; } // Default to directory view on home if (!route.view) { route.view = VIEW_DIRECTORY; } try { const viewContent = route.view === VIEW_DIRECTORY ? getHistory(route.path, route.platform, route.suite) : getSource(route.path, route.revision); const [coverage, filters, viewData] = await Promise.all([ getPathCoverage(route.path, route.revision, route.platform, route.suite), getFilters(), viewContent, ]); return { view: route.view, path: route.path, revision: route.revision, route, coverage, filters, viewData, }; } catch (err) { console.warn("Failed to load coverage", err); await DOM_READY; // We want to always display this message message("error", "Failed to load coverage: " + err.message); throw err; } } export async function display(data) { if (data.view === VIEW_ZERO_COVERAGE) { await zeroCoverageMenu(data.route); await zeroCoverageDisplay(data.zeroCoverage, data.path); } else if (data.view === VIEW_DIRECTORY) { hide("message"); browserMenu(data.revision, data.filters, data.route); await graphHistory(data.viewData, data.path); await showDirectory(data.path, data.revision, data.coverage.children); } else if (data.view === VIEW_FILE) { browserMenu(data.revision, data.filters, data.route); await showFile( data.viewData, data.coverage, data.revision, data.route.line, ); } else { message("error", "Invalid view : " + data.view); } // Always monitor options on newly rendered output monitorOptions(data); } main(load, display);