blob: 0f941aebaad70945ca9761ff36459abacd7e5058 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {sanitize} from './sanitizer.js';
import {DATA_SYMBOL} from './checks-fetcher.js';
/**
* Called when the check-result-expanded element is added to the page.
*
* @param {element} element: the check-result-expanded element.
*/
export async function installChecksResult(element) {
if (!element?.result?.message) return;
// Remove message div with raw message string.
const host = element.parentNode.host;
const messageElement = host.querySelector('.message');
if (messageElement) {
host.removeChild(messageElement);
}
const data = element.result[DATA_SYMBOL];
if (!data) {
formatInnerHTML(element, `<p>${element.result.message}</p>`);
return;
}
// TODO(gavinmak): Add a loading indicator.
// Replace all fetchable <text-artifacts/>.
const {variantData} = data;
const htmlPromises = [];
for (let i = 0; i < variantData.length; i++) {
const {result, resultArtifacts} = variantData[i];
const {summaryHtml} = result;
// White space and new lines are respected in checks-result. Pushing
// elements to messages separately avoids bad formatting from this.
//
// Strings are pushed with promises into htmlPromises to avoid iterating
// through variantData more than once. Pushing 'string' is equivalent to
// pushing Promise.resolve('string') when using Promise.allSettled below.
htmlPromises.push(
`<li><h3>Run #${i+1}: ${result.status}</h3>`,
replaceTextArtifacts(summaryHtml, resultArtifacts),
'</li>',
);
}
// Use <ul> instead of <ol> since results are labeled 'Run #x'.
// TODO(gavinmak): Display a rejected promise's reason separately.
const html = await Promise.allSettled(htmlPromises);
formatInnerHTML(
element,`<ul>${html.map(r => r.value || r.reason).join('')}</ul>`);
}
/**
* Replaces all <text-artifact/> elements in html with their fetched text
* artifacts. Only elements whose artifact-ids are in artifacts are replaced.
*
* Note: Because message is parsed by parseFromString, any self-closing
* <text-artifact /> tags without a fetchable artifact-id will be replaced by an
* opening and closing <text-artifact></text-artifact> tags.
*
* TODO(gavinmak): Support invocation-level artifacts.
* TODO(gavinmak): Refactor this method and sanitizer to accept an
* already-parsed node.
*
* @param {String} html: the CheckResult HTML message.
* @param {Array} artifacts: the CheckResult's list of artifacts.
*/
export async function replaceTextArtifacts(html, artifacts) {
if (!artifacts.length) {
return html;
}
const urlMap = new Map(artifacts.map(a => [a.artifactId, a.fetchUrl]));
const parser = new DOMParser();
const parsed = parser.parseFromString(html, 'text/html');
const textArtifacts = parsed.body.querySelectorAll('text-artifact');
await Promise.all([...textArtifacts].map(async e => {
const url = urlMap.get(e.getAttribute('artifact-id'));
if (!url) { return; }
const res = await fetch(url);
const text = await res.text();
// TODO(gavinmak): Use outerHTML to handle cases where text is wrapped in
// HTML.
e.replaceWith(text);
}));
return parsed.body.innerHTML;
}
/**
* Sets element's innerHTML to the sanitized html and adds style to the element.
*
* @param {element} element: the check-result-expanded element.
* @param {String} html: the CheckResult HTML message.
*/
function formatInnerHTML(element, html) {
element.style.wordBreak = 'break-all';
element.style.whiteSpace = 'pre-wrap';
element.style.fontFamily = 'monospace';
element.innerHTML = sanitize(html);
}