| // 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); |
| } |