blob: 3565673842acf7e98d4e7c85136e1c4f68ec25f1 [file] [log] [blame]
<!doctype html>
<meta charset="UTF-8">
<title>Layout Tests</title>
<!--
Displays web_tests results.
-->
<style>
body {
font-family: sans-serif;
min-height: 120vh;
}
button {
margin-top: 4px;
}
input {
vertical-align: middle;
margin-top: 0;
margin-bottom: 0;
}
p, h2, h3, h4 {
margin: 8px 0 4px 0;
}
#right-toolbar {
position: absolute;
top: 8px;
right: 0;
font-size: smaller;
}
.good {
background-color: rgba(0, 255, 0, 0.1);
}
.bad {
background-color: rgba(255, 0, 0, 0.1);
}
#help {
font-family: sans-serif;
box-sizing: border-box;
position: fixed;
width: 96vw;
height: 96vh;
top: 2vh;
left: 2vw;
border: 5px solid black;
background-color: white;
padding: 16px;
box-shadow: 0 0 20px;
overflow: auto;
z-index: 1;
}
#summary > p {
margin: 0.2em 0 0 0;
}
#dashboard {
user-select: none;
}
#report {
font-family: monospace;
}
#none {
color: green;
margin-left: 2em;
font-size: x-large;
font-weight: bold;
font-style: italic;
}
.fix-width {
display: inline-block;
width: 7em;
text-align: right;
margin-right: 1em;
}
.hidden {
display: none;
}
.warn {
color: red;
}
.h-expect {
margin-left: 1.25em;
}
.expect {
line-height: 200%;
cursor: zoom-in;
}
.expect:hover, .expect:focus {
background-color: #F4F4F4;
}
.expect:focus > .details {
visibility: visible;
}
.details {
box-sizing: border-box;
visibility: hidden;
display: inline-block;
position: relative;
top: 0.2em;
width: 1em;
height: 1em;
border-top: 0.5em solid transparent;
border-bottom: 0.5em solid transparent;
border-right: none;
border-left: 0.5em solid gray;
margin-right: .25em;
cursor: pointer;
}
.details.open {
visibility: visible !important;
top: 0.5em;
border-left: 0.5em solid transparent;
border-right: 0.5em solid transparent;
border-top: 0.5em solid gray;
border-bottom: none;
}
.result-frame {
border: 1px solid gray;
border-top: 1px solid transparent;
margin-left: 2.25em;
margin-right: 2.25em;
margin-top: 4px;
margin-bottom: 16px;
}
.result-menu {
list-style-type: none;
margin: 0;
padding: 0;
}
.result-menu li {
display: inline-block;
min-width: 100px;
font-size: larger;
border: 1px dotted gray;
border-bottom: 1px solid transparent;
margin-right: 8px;
}
.result-output iframe {
width: 100%;
height: 50vh;
max-height: 800px;
border: 0px solid gray;
resize: both;
overflow: auto;
}
#filters {
margin-top: 8px;
}
#filters label {
font-family: sans-serif;
font-size: smaller;
}
.flag {
display: inline-block;
vertical-align:middle;
width: 1.2em;
height: 1.2em;
border: 1px solid #DDD;
cursor: default;
user-select: none;
margin-right: 8px;
}
.flag.flagged::after {
content: "⚑";
user-select: none;
font-size: x-large;
position: relative;
top: -0.1em;
left: 0.1em;
line-height: 100%;
color: gray;
}
#copied {
color: #4F8A10;
margin-left: 5px;
}
#flag-toolbar {
display: inline-block;
}
#flag-toolbar.hidden {
display: none;
}
</style>
<body>
<h3>Test run summary <span id="builder_name"></span> <span class="flag_name"></span></h3>
<div id="right-toolbar">
<a id="help_button" href="javascript:GUI.toggleVisibility('help')">help</a>
<div style="">go back to <a href="legacy-results.html">legacy-results.html</a></div>
</div>
<div id="help" class="hidden hide-on-esc">
<button style="position:fixed;right:40px;" onclick="GUI.toggleVisibility('help')">Close</button>
<h3>Keyboard navigation</h3>
<ul>
<li><b>Space</b> Show full results of the next test. This is the easiest way to navigate, just hit spacebar (and shift space to go back).
<li><b>Tab</b> to select the next test.
<li><b>Enter</b> to see test details. This will automatically close other details.
<li><b>ctrl A</b> to select text of all tests for easy copying.
<li><b>f</b> to flag/unflag a test.
<li><b>?</b> to blink error region of a failed image test.
<li><b>p</b> prints full record of currently selected test to console.
</ul>
<p>Modifiers:</p>
<ul>
<li><b>Shift</b> hold shift key to keep other details open.
<li><b>Meta</b> meta means all. Set/unset all flags or open/close all results (max 100).
</ul>
<p>This page lets you query and display test results.</p>
<h3>Querying Results</h3>
<p>Select the results you are interested in using "Query" buttons.
Narrow down the results further with "Filter" checkboxes.</p>
<p>"Did not pass" query will exclude PASS and WONTFIX tests.</p>
<h3>Displaying results.</h3>
<p>You can view the list of matched tests in several result formats.
Select format that works best for what you are trying to accomplish
using "format" popup. Following formats are available:</p>
<h4>1. Plain text</h4>
<pre>fast/forms/<a href="https://cs.chromium.org/chromium/src/third_party/blink/web_tests/fast/forms/validation-bubble-appearance-rtl-ui.html?q=validation-bubble-appearance-rtl-ui.html&dr">validation-bubble-appearance-rtl-ui.html</a></pre>
<p>Plain text shows the test path.</p>
<h4>2. TestExpectations</a></h4>
<pre><a href="#">crbug.com/bug</a> layout/test/path/<a href="#">test.html</a> [ Status ]</pre>
<p>TestExpectationsshow lines as they'd appear in <a
href="https://chromium.googlesource.com/chromium/src/+/master/docs/testing/web_test_expectations.md">TestExpectations</a> file.</p>
<p>The interesting part here is [ Status ]. Inside TestExpectations file, [ Status ]
can have multiple values, representing all expected results. For example:</p>
<pre>[ Failure Slow Timeout Crash Pass ]</pre>
<p>Result lines include existing expected values, and make a guess about what the new
test expectation line should look like by merging together expected and actual
results. The actual result will be shown in bold. For example:</p>
<pre>TestResult(PASS) + TestExpectation(Failure) => [ Pass ]
TestResult(CRASH) + TextExpectation(Failure) => [ Failure <b>Crash</b> ]</pre>
<p>If you are doing a lot of TestExpectation edits, the hope is that this will make
your job as easy as copy and paste.</p>
<h4>3. Crash site</h4>
<pre>
### Crash site: Internals.cpp(3455)
editing/pasteboard/<a href="https://cs.chromium.org/chromium/src/third_party/blink/web_tests/editing/pasteboard/copy-paste-white-space.html?q=copy-paste-white-space.html&dr">copy-paste-white-space.html</a></pre>
<p>Crash site groups "Crash" tests with similar stack traces together. For best results, use it while filtering only crashes.</p>
<h4>4. Text mismatch</h4>
<pre>
### Text mismatch failure: general text mismatch
accessibility/dimensions-include-descendants.html
### Text mismatch failure: newlines only
accessibility/aria-controls-with-tabs.html
### Text mismatch failure: spaces and tabs only
accessibility/aria-describedby-on-input.html
</pre>
<p>Text mistmatch groups "Text failure" tests together.
</p>
<h4>5. Rebaseline</h4>
<p>Generates a bash script to rebaseline based on new results. @xiaochengh knows the details on how to use it, and @kojii is
our one user.</p>
<h3>Viewing results of a single test</h3>
<h4>Image results</h4>
<p>Click on images to zoom in. Select image viewing mode from the popup.</p>
<p>When viewing images for the first time, red flash highlights enclosing
rectangle that was colored red in the diff. Diff flash and color eyedropper
will not be available on file:// urls because of CSP.</p>
<h4>Text results</h4>
<p>Different panels show the expected, actual text results and differences.</p>
<p>For repaint tests, the "repaint" panel visualizes the repaint rectangles.</p>
<h3>Flagging</h3>
<p>Tests can be flagged by clicking on that square box on the right hand side. View all flagged tests with "Flagged" filter. "F" is the keyboard shortcut.
<h3>Bugs</h3>
<p>If you are unhappy with results, please file a bug, or fix it <a href="https://cs.chromium.org/chromium/src/third_party/blink/web_tests/fast/harness/results.html">here</a>.</p>
<p><code>window.localStorage.setItem("testLocationOverride", "file://path/to/your/web_tests")</code> is a secret preference to redirect test links to a custom url.</p>
</div>
<div id="summary">
<p><span class="fix-width">Passed</span><span id="summary_passed"></span></p>
<p><span class="fix-width">Regressions</span><span id="summary_regressions"></span></p>
<p><span class="fix-width">Total</span><span id="summary_total"></span></p>
<p><span class="fix-width">Counts</span><span id="summary_details"></span></p>
</div>
<hr>
<div id="dashboard">
<div>
<span class="fix-width">Query</span>
<button id="button_regressions" onclick="javascript:Query.query('Regressions', Filters.regression, true)">
Regressions
<span id="count_regressions"></span>
</button>
<button onclick="javascript:Query.query('Unexpected passes', Filters.unexpectedPass, true)">
Unexpected Pass
<span id="count_unexpected_pass"></span>
</button>
<button onclick="javascript:Query.query('Did not pass', Filters.notpass, true)">
Did not pass
<span id="count_testexpectations"></span>
</button>
<button onclick="javascript:Query.query('All', Filters.all, true)">
All
<span id="count_all"></span>
</button>
<button onclick="javascript:Query.query('Flaky', Filters.flaky, true)">
Flaky
<span id="count_flaky"></span>
</button>
<button onclick="javascript:Query.query('Unexpected Flaky', Filters.unexpectedFlaky, true)">
Unexpected flaky
<span id="count_unexpected_flaky"></span>
</button>
<button onclick="javascript:Query.query('Flagged', Filters.flagged, true)">
Flagged
</button>
<div id="flag-toolbar" class="hidden">
<button onclick="javascript:Query.query('Flag failures', Filters.flagFailure, true)">
<span class="flag_name"></span> failures
<span id="count_flagfailure"></span>
</button>
<button onclick="javascript:Query.query('Flag failures', Filters.flagPass, true)">
<span class="flag_name"></span> passes
<span id="count_flagpass"></span>
</button>
</div>
</div>
<div id="filters">
<span class="fix-width">Filters</span>
<input id="text-filter" onchange="Query.filterChanged()" type="text" placeholder="test name/bug#, hit enter">
<label id="CRASH"><input type="checkbox">Crash <span></span></label>
<label id="TIMEOUT"><input type="checkbox">Timeout <span></span></label>
<label id="TEXT"><input type="checkbox">Text failure <span></span></label>
<label id="HARNESS"><input type="checkbox">Harness failure <span></span></label>
<label id="IMAGE"><input type="checkbox">Image failure <span></span></label>
<label id="IMAGE_TEXT"><input type="checkbox">Image+text failure <span></span></label>
<label id="AUDIO"><input type="checkbox">Audio failure <span></span></label>
<label id="SKIP"><input type="checkbox">Skipped <span></span></label>
<label id="PASS"><input type="checkbox">Pass <span></span></label>
<label id="WONTFIX"><input type="checkbox">WontFix <span></span></label>
<label id="MISSING"><input type="checkbox">Missing <span></span></label>
<label id="LEAK"><input type="checkbox">Leak <span></span></label>
</div>
</div>
<div id="report_header" style="margin-top:8px">
<span class="fix-width">Tests shown</span><span id="report_count"></span>
<span id="report_title" style="font-weight:bold"></span>
in format:
<select id="report_format" onchange="Query.generateReport()">
<option value="plain" selected>Plain text</option>
<option value="expectation">TestExpectations</option>
<option value="crashsite">Crash site</option>
<option value="textmismatch">Text mismatch</option>
<option value="rebaseline">Rebaseline script</option>
</select>
<span style="margin-left: 20px">
<button id="copy_report" title="Copy the shown/flagged tests to clipboard in the current format"
onclick="GUI.copyResult(false)" disabled>Copy report</button>
<button id="copy_single_line" title="Copy the shown/flagged tests to clipboard in a single line for use in command lines."
onclick="GUI.copyResult(true)" disabled>Copy single line</button>
<label id="flagged_only" class="hidden"><input id="flagged_only_checkbox" type="checkbox" checked>Flagged only</label>
<span id="copied" class="hidden">Copied.</span>
</span>
</div>
<hr id="progress" align="left">
<div id="report" style="margin-top:8px"></div>
<script>
"use strict";
// Results loaded from full_results_jsonp.js.
let globalResults = {};
let globalTestMap = new Map(); // id => test
const TestResultInformation = {
"CRASH": { index: 1, text: "Crash", isFailure: true, isSuccess: false },
"FAIL": { index: 2, text: "Failure", isFailure: true, isSuccess: false },
"TEXT": { index: 3, text: "Failure", isFailure: true, isSuccess: false },
"IMAGE": { index: 4, text: "Failure", isFailure: true, isSuccess: false },
"IMAGE+TEXT": { index: 5, text: "Failure", isFailure: true, isSuccess: false },
"AUDIO": { index: 6, text: "Failure", isFailure: true, isSuccess: false },
"TIMEOUT": { index: 7, text: "Timeout", isFailure: true, isSuccess: false },
"LEAK": { index: 8, text: "LEAK", isFailure: true, isSuccess: false },
"SLOW": { index: 9, text: "Slow", isFailure: false, isSuccess: true },
"SKIP": { index: 10, text: "Skip", isFailure: false, isSuccess: false },
"MISSING": { index: 11, text: "Missing", isFailure: false, isSuccess: false },
"WONTFIX": { index: 12, text: "WontFix", isFailure: false, isSuccess: false },
"PASS": { index: 13, text: "Pass", isFailure: false, isSuccess: true },
"NOTRUN": { index: 14, text: "NOTRUN", isFailure: false, isSuccess: true }
};
// Sorted from worst to best.
const TestResultComparator = function (a, b) {
if (TestResultInformation[a].index > TestResultInformation[b].index)
return 1;
else if (TestResultInformation[a].index == TestResultInformation[b].index)
return 0;
else
return -1;
};
// Traversal traverses all the tests.
// Use Traversal.traverse(filter, action) to perform action on selected tests.
class Traversal {
constructor(testRoot, textFilter) {
this.root = testRoot;
this.reset();
}
traverse(filter, action) {
console.time("traverse");
action = action || function() {};
this._helper(this.root, "", filter, action);
console.timeEnd("traverse");
}
reset() {
this.testCount = 0;
this.filteredCount = 0;
this.lastDir = "";
this.html = [];
return this;
}
_helper(node, path, filter, action) {
if (GUI.isTest(node)) {
this.testCount++;
if (filter(node, path)) {
this.filteredCount++;
action(node, path, this);
}
} else {
for (let p of node.keys())
this._helper(node.get(p), path + "/" + p, filter, action);
}
}
} // class Traversal
const PathParserGlobals = {
layout_tests_dir: null,
chromium_revision: null
};
class PathParser {
constructor(path) {
this.path = path;
let href;
[href, this.dir, this.file] = path.match("/(.*/)?(.*)");
this.dir = this.dir || "";
try {
[, this.raw_basename, this.raw_extension] = this.file.match(/(.+)\.(.+)/);
} catch {
this.raw_basename = "ERROR";
this.raw_extension = "ERROR";
}
// blink/tools/blinkpy/web_tests/port/base.py output_filename()
let index = this.raw_extension.indexOf("?");
if (index > -1) {
let query_string = this.raw_extension.slice(index + 1);
query_string = query_string.replace(/[~#%&*{}\:<>?\/|"]/g, "_");
this.basename = this.raw_basename + "_" + query_string;
this.extension = this.raw_extension.slice(0, index);
} else {
[this.basename, this.extension] = [this.raw_basename, this.raw_extension];
}
href = href.replace(/^\/virtual\/[^\/]*/, ""); // virtual suites
href = href.replace(/\?.*$/, ""); // query strings (variants)
this.testHref = this.testBaseHref() + href;
}
static initGlobals(fullResults) {
for (let p in PathParserGlobals)
PathParserGlobals[p] = fullResults[p];
}
resultLink(resultName) {
return this.dir + this.basename + resultName;
}
repaintOverlayLink() {
return this.resultLink("-overlay.html?") + encodeURIComponent(this.testHref);
}
testBaseHref() {
if (window.localStorage.getItem("testLocationOverride")) {
// Experimental preference.
// Use "window.localStorage.setItem("testLocationOverride", "file://path/to/your/web_tests")
return window.localStorage.getItem("testLocationOverride");
} else if (PathParserGlobals.layout_tests_dir) {
return PathParserGlobals.layout_tests_dir;
} else if (location.toString().indexOf('file://') == 0) {
// tests were run locally.
return "../../../third_party/blink/web_tests";
} else if (PathParserGlobals.chromium_revision) {
// Existing crrev list is incorrect: https://crbug.com/750347
let correctedRevision = PathParserGlobals.chromium_revision.replace("refs/heads/master@{#", "").replace("}", "");
return "https://crrev.com/" + correctedRevision + "/third_party/blink/web_tests";
} else {
return "https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/web_tests";
}
}
} // class PathParser
// Report deals with displaying a single test.
const Report = {
getDefaultPrinter: () => {
let val = document.querySelector("#report_format").value;
window.localStorage.setItem("reportFormat", val);
switch(val) {
case "expectation":
return {print: Report.printExpectation, render: Report.renderResultList};
case "crashsite":
return {print: Report.printCrashSite, render: Report.renderGroupCrashSite};
case "textmismatch":
return {print: Report.printTextMismatch, render: Report.renderGroupTextMismatch};
case "rebaseline":
return {print: Report.printRebaseline, render: Report.renderResultList};
case "plain":
default:
return {print: Report.printPlainTest, render: Report.renderResultList};
}
},
printFlag: (test) => {
return `<div class="flag ${test.flagged ? "flagged" : ""}"
title="Hold Meta key to flag/unflag all tests."></div>`;
},
printPlainTest: (test, path, traversal) => {
let pathParser = new PathParser(path);
let html = `
<div class='expect' tabindex='0' data-id='${test.expectId}'>
<div class='details'></div>${Report.printFlag(test)}
${pathParser.dir}<a target='test' tabindex='-1' href='${pathParser.testHref}'>${pathParser.file}</a>
</div>`;
traversal.html.push(html);
},
printExpectation: (test, path, traversal) => {
// TestExpectations file format is documented at:
// https://chromium.googlesource.com/chromium/src/+/master/docs/testing/web_test_expectations.md
let pathParser = new PathParser(path);
// Print directory header if this test's directory is different from the last.
if (pathParser.dir != traversal.lastDir) {
traversal.html.push("<br>");
traversal.html.push("<div class='h-expect'>### " + pathParser.dir + "</div>");
traversal.lastDir = pathParser.dir;
}
let statusMap = new Map(test.expectedMap);
if (statusMap.has("PASS") && statusMap.size == 1)
statusMap.delete("PASS");
for (let s of test.actualMap.keys()) {
let result = s;
if (result == "TEXT" || result == "IMAGE" || result == "IMAGE+TEXT")
result = "FAIL";
if (result == "SKIP" && test.expectedMap.has("WONTFIX"))
result = "WONTFIX";
statusMap.set(result, "bold");
}
let status = "";
for (let key of statusMap.keys()) {
if (statusMap.get(key) == "bold")
status += ` <b>${TestResultInformation[key].text}</b>`;
else
status += ` ${TestResultInformation[key].text}`;
}
let bug = test.actualMap.has("PASS") ? "" : "<span class='warn'>NEEDBUG</span>";
if (test.bugs && test.bugs.length > 0) {
bug = "";
for (let b of test.bugs) {
bug += `<a target='crbug' tabindex='-1' href='https://${b}'>${b}</a> `;
}
}
let html = `
<div class='expect' tabindex='0' data-id='${test.expectId}'><div class='details'></div>${Report.printFlag(test)}${bug}
${pathParser.dir}<a target='test' tabindex='-1' href='${pathParser.testHref}'>${pathParser.file}</a>
[ ${status} ]
</div>
`;
traversal.html.push(html);
},
printWithKey: (test, path, traversal, key_title) => {
let pathParser = new PathParser(path);
let key = test[key_title];
let html = ""
+ `${Report.printFlag(test)}`
+ pathParser.dir
+ "<a target='test' tabindex='-1' href='" + pathParser.testHref + "'>"
+ pathParser.file + "</a>";
html = "<div class='expect' tabindex='0' data-id='"+ test.expectId +"'><div class='details'></div>" + html + "</div>";
traversal.html.push({key: key, html: html});
},
printCrashSite: (test, path, traversal) => {
Report.printWithKey(test, path, traversal, "crash_site");
},
printTextMismatch: (test, path, traversal) => {
Report.printWithKey(test, path, traversal, "text_mismatch");
},
printRebaseline: (test, path, traversal) => {
let parser = new PathParser(path);
let actualNames = [];
let expectedNames = [];
switch (test.actualFinal) {
case "IMAGE+TEXT":
actualNames.push("-actual.txt");
expectedNames.push("-expected.txt");
// fall through IMAGE
case "IMAGE":
actualNames.push("-actual.png");
expectedNames.push("-expected.png");
break;
case "TEXT":
actualNames.push("-actual.txt");
expectedNames.push("-expected.txt");
break;
default:
return;
}
// Change directory if in wrong place
let dir = parser.dir;
if (dir != traversal.rebaselineDir) {
if (traversal.rebaselineDir) {
let depth = (traversal.rebaselineDir.match(/\//g) || []).length;
if (depth)
traversal.html.push(`<div>cd ${'..' + '/..'.repeat(depth - 1)};</div>`);
}
traversal.html.push(`<div>mkdir -p ${dir};</div>`);
traversal.html.push(`<div>cd ${dir};</div>`);
traversal.rebaselineDir = dir;
}
for (let i=0; i<actualNames.length; i++) {
let url = new URL(parser.resultLink(actualNames[i]), window.location.href);
if (url.href.startsWith('file://')) {
let actualFile = url.href.substring('file://'.length);
traversal.html.push(`<div>cp ${actualFile} ${parser.basename + expectedNames[i]};</div>`);
} else {
traversal.html.push(`<div>wget ${url.href};</div>`);
traversal.html.push(`<div>mv ${parser.basename + actualNames[i]} ${parser.basename + expectedNames[i]};</div>`);
}
}
},
indicateNone: (report) => {
let pre = document.createElement("div");
pre.id = "none";
pre.textContent = "None";
report.appendChild(pre);
},
renderResultList: (html, report) => {
if (report.childNodes.length === 0 && html.length === 0) {
Report.indicateNone(report);
return;
}
let pre = document.createElement("div");
pre.innerHTML = html.join("\n");
report.appendChild(pre);
},
createContainerForGroup: (report, key, keyed_title, null_title) => {
let container = document.createElement("div");
container.setAttribute("key", key);
container.innerHTML = ""
+ "<br><b>"
+ (key !== 'null' ? `### ${keyed_title}: ${key}`
: `### ${null_title}`)
+ "</b>";
// The containers are sorted by key (except the "null" key) alphabetically.
// The "null" container always appears at the last.
if (key == "null") {
report.appendChild(container);
} else {
let inserted = false;
report.childNodes.forEach(sibling => {
if (inserted)
return;
let siblingKey = sibling.getAttribute("key");
if (siblingKey == "null" || siblingKey > key) {
report.insertBefore(container, sibling);
inserted = true;
}
});
if (!inserted)
report.appendChild(container);
}
return container;
},
renderGroup: (html, report, keyed_title, null_title) => {
if (report.childNodes.length === 0 && html.length === 0) {
Report.indicateNone(report);
return;
}
let renderMap = {};
html.forEach(result => {
let key = result.key || "null";
if (!(key in renderMap)) {
const number_of_items = html.filter(result => (result.key || "null") === key).length;
let container =
report.querySelector(`div[key="${key}"]`) ||
Report.createContainerForGroup(report, key,
`${keyed_title} ${number_of_items}`,
`${null_title} ${number_of_items}`);
renderMap[key] = {container: container, html: ""};
}
renderMap[key].html += result.html;
});
for (let key in renderMap)
renderMap[key].container.insertAdjacentHTML('beforeend', renderMap[key].html);
},
renderGroupCrashSite: (html, report) => {
Report.renderGroup(html, report, "Crash site", "Didn't crash");
},
renderGroupTextMismatch: (html, report) => {
Report.renderGroup(html, report, "Text mismatch failure", "Didn't find text mismatch");
},
getTestById: (testId) => {
return globalTestMap.get(parseInt(testId));
},
// Returns toolbar DOM
getResultToolbars: (test) => {
let toolbars = [];
let pathParser = new PathParser(test.expectPath);
toolbars.push(new PlainHtmlToolbar().createDom(test.actual));
for (let result of test.actualMap.keys()) {
switch(result) {
case "PASS":
case "SLOW":
if (Filters.unexpectedPass(test))
toolbars.push(new PlainHtmlToolbar().createDom("Expected: " + test.expected));
if (!test.has_stderr)
toolbars.push(new PlainHtmlToolbar().createDom("No errors"));
break;
case "SKIP":
toolbars.push(new PlainHtmlToolbar().createDom("Test did not run."));
break;
case "CRASH":
toolbars.push(new SimpleLinkToolbar().createDom(
pathParser.resultLink("-crash-log.txt"), "Crash log", "crash log"));
break;
case "TIMEOUT":
toolbars.push(new PlainHtmlToolbar().createDom("Test timed out. "
+ ("time" in test ? `(${test.time}s)` : "")));
if (test.text_mismatch)
toolbars.push(new TextResultsToolbar().createDom(test));
break;
case "TEXT":
toolbars.push(new TextResultsToolbar().createDom(test));
break;
case "IMAGE":
toolbars.push(new ImageResultsToolbar().createDom(test));
break;
case "IMAGE+TEXT":
toolbars.push(new ImageResultsToolbar().createDom(test));
toolbars.push(new TextResultsToolbar().createDom(test));
break;
case "AUDIO":
toolbars.push(new AudioResultsToolbar().createDom(test));
break;
case "MISSING":
toolbars.push(new PlainHtmlToolbar().createDom("Expectations are missing."));
toolbars.push(new TextResultsToolbar().createDom(test));
toolbars.push(new ImageResultsToolbar().createDom(test));
break;
case "LEAK":
toolbars.push(new SimpleLinkToolbar().createDom(
pathParser.resultLink("-leak-log.txt"), "Leak log", "leak log"));
break;
default:
console.error("unexpected actual", test.actual);
}
}
if (test.has_stderr) {
toolbars.push(new SimpleLinkToolbar().createDom(
pathParser.resultLink("-stderr.txt"), "standard error", "stderr"));
}
return toolbars;
},
getResultsDiv: (test) => {
let div = document.createElement("div");
div.classList.add("result-frame");
div.innerHTML = `
<ul class="result-menu"></ul>
<div class="result-output"></div>
`;
// Initialize the results.
let menu = div.querySelector(".result-menu");
for (let toolbar of Report.getResultToolbars(test))
menu.appendChild(toolbar);
return div;
}
}; // Report
// Query generates a report for a given query.
const Query = {
lastReport: null,
currentRAF: null,
currentPromise: null,
currentResolve: null,
currentReject: null,
createReportPromise: function() {
if (this.currentPromise) {
this.currentPromise = null;
this.currentReject();
}
this.currentPromise = new Promise( (resolve, reject) => {
this.currentResolve = resolve;
this.currentReject = reject;
});
this.currentPromise.catch( _ => {}); // stops uncaught rejection errors.
},
completeReportPromise: function(traversal) {
if (this.currentResolve)
this.currentResolve(traversal);
this.currentPromise = null;
this.currentResolve = null;
this.currentReject = null;
},
resetFilters: function() {
// Reset all filters.
for (let el of Array.from(
document.querySelectorAll("#filters > label"))) {
el.querySelector('input').checked = true;
}
},
updateFilters: function() {
for (let el of Array.from(
document.querySelectorAll("#filters > label"))) {
let count = this.resultCounts[el.id.replace("_", "+")];
if (count > 0) {
el.classList.remove("hidden");
el.querySelector('span').innerText = count;
} else {
el.classList.add("hidden");
el.querySelector("span").innerText = "";
}
}
},
filterChanged: function(ev) {
this.query();
},
applyFilters: function(queryFilter) {
var filterMap = new Map();
for (let el of Array.from(
document.querySelectorAll("#filters > label"))) {
if (el.querySelector('input').checked)
filterMap.set(el.id.replace("_", "+"), true);
}
let searchText = document.querySelector("#text-filter").value;
let textFilter = (!searchText || searchText.length < 1)
? _ => true
: test => {
if (test.expectPath.includes(searchText))
return true;
if (Array.isArray(test.bugs)) {
for (let bug of test.bugs) {
if (bug.includes(searchText))
return true;
}
}
return false;
};
return test => {
if (!queryFilter(test) || !textFilter(test))
return false;
let resultKey = test.is_testharness_test ? "HARNESS" : test.actualFinal;
// Ignore all results except final one, or not?
if (!(resultKey in this.resultCounts))
this.resultCounts[resultKey] = 0;
this.resultCounts[resultKey]++;
if (test.actualFinal == "TEXT" && test.is_testharness_test) {
return filterMap.has("HARNESS");
}
return filterMap.has(test.actualFinal);
};
},
query: function(name, queryFilter, reset) {
queryFilter = queryFilter || this.lastQueryFilter;
if (reset) {
this.resetFilters();
this.lastQueryFilter = queryFilter;
}
let composedFilter = this.applyFilters(queryFilter);
this.generateReport(name, composedFilter);
},
// generateReport is async, returns promise.
// promise id fullfilled when traversal completes. Display will continue async.
generateReport: function(name, filter, report) {
console.time("generateReport");
if (this.currentRAF)
window.cancelAnimationFrame(this.currentRAF);
report = report || Report.getDefaultPrinter();
filter = filter || this.lastReport.filter;
name = name || this.lastReport.name;
// Store last report to redisplay.
this.lastReport = {name: name, filter: filter};
this.createReportPromise();
this.resultCounts = {};
document.querySelector("#report").innerHTML = "";
document.querySelector("#report_title").innerHTML = name;
document.querySelector("#progress").style.width = "1%";
document.querySelector("#copy_report").disabled = true;
document.querySelector("#copy_single_line").disabled = true;
document.querySelector("#report_count").innerText = "";
let traversal = new Traversal(globalResults.tests);
let chunkSize = 1000;
let index = 0;
let callback = _ => {
this.currentRAF = null;
let html = traversal.html.slice(index, index + chunkSize);
report.render(html, document.querySelector("#report"));
index += chunkSize;
document.querySelector("#progress").style.width = Math.min((index / traversal.html.length * 100), 100) + "%";
if (index < traversal.html.length) {
this.currentRAF = window.requestAnimationFrame(callback);
} else {
document.querySelector("#report_count").innerText = traversal.filteredCount;
GUI.updateCopyButtons();
console.timeEnd("generateReport");
}
};
window.setTimeout( _ => {
traversal.traverse(filter, report.print);
this.updateFilters();
this.completeReportPromise(traversal);
this.currentRAF = window.requestAnimationFrame(callback);
}, 0);
return this.currentPromise;
}
}; // Query
// Test filters for queries.
const Filters = {
containsPass: function (map) {
return map.has("PASS") || map.has("SLOW");
},
containsNoPass: function(map) {
return map.has("FAIL")
|| map.has("WONTFIX")
|| map.has("SKIP")
|| map.has("CRASH");
},
unexpectedPass: test => {
return !Filters.containsPass(test.expectedMap) && Filters.containsPass(test.actualMap);
},
regressionFromExpectedMap: (finalResult, expectedMap) => {
if (expectedMap.has("NEEDSREBASELINE")
|| expectedMap.has("WONTFIX"))
return false;
switch (finalResult) {
case "SKIP":
return false;
case "CRASH":
case "TIMEOUT":
case "LEAK":
if (expectedMap.has(finalResult))
return false;
break;
case "TEXT":
case "IMAGE":
case "IMAGE+TEXT":
case "AUDIO":
if (expectedMap.has("FAIL"))
return false;
break;
case "MISSING":
break;
default:
console.error("Unexpected test result", finalResult);
}
return true;
},
regression: test => {
if (Filters.containsPass(test.actualMap))
return false;
return Filters.regressionFromExpectedMap(test.actualFinal, test.expectedMap);
},
notpass: test => test.actualFinal != "PASS" && test.expected != "WONTFIX",
actual: tag => { // Returns comparator for tag.
return function(test) {
return test.actualMap.has(tag);
};
},
wontfix: test => test.expected == "WONTFIX",
all: _ => true,
flaky: test => test.actualMap.size > 1,
unexpectedFlaky: test => test.actualMap.size > 1 && test.expectedMap.size <= 1,
flagged: test => test.flagged,
flagFailure: test => { // Tests that are failing, but expected to pass in base.
if (Filters.containsPass(test.actualMap))
return false;
if (test.expectedMap.has("NEEDSREBASELINE")
|| test.expectedMap.has("WONTFIX"))
return false;
let baseMap = test.flagMap ? test.baseMap : test.expectedMap;
switch (test.actualFinal) {
case "SKIP":
return false;
case "CRASH":
case "TIMEOUT":
case "LEAK":
if (baseMap && baseMap.has(test.actualFinal))
return false;
break;
case "TEXT":
case "IMAGE":
case "IMAGE+TEXT":
case "AUDIO":
if (baseMap && baseMap.has("FAIL"))
return false;
break;
case "MISSING":
break;
default:
console.error("Unexpected test result", test.actualFinal);
}
return true;
},
flagPass: test => {
return test.baseMap && (Filters.containsPass(test.actualMap) && !Filters.containsPass(test.baseMap));
}
}; // Filters
// Event handling, initialization.
const GUI = {
initPage: function(results) {
results.tests = GUI.convertToMap(results.tests);
globalResults = results;
if (window.localStorage.getItem("reportFormat")) {
document.querySelector("#report_format").value = window.localStorage.getItem("reportFormat");
}
GUI.optimizeResults(globalResults);
GUI.printSummary(globalResults);
GUI.initEvents();
PathParser.initGlobals(results);
// Show regressions on startup.
document.querySelector("#button_regressions").click();
},
hasBaseExpectations : false,
isTest: function(o) {
return "actual" in o;
},
convertToMap: function(o) {
if (GUI.isTest(o))
return o;
else {
let map = new Map();
var keys = Object.keys(o).sort((a, b) => {
let a_isTest = GUI.isTest(o[a]);
if (a_isTest == GUI.isTest(o[b]))
return a < b ? -1 : +(a > b);
return a_isTest ? -1 : 1;
});
for (let p of keys)
map.set(p, GUI.convertToMap(o[p]));
return map;
}
},
optimizeResults: function(fullResults) {
// Optimizes fullResults for querying.
let t = new Traversal(fullResults.tests);
// To all tests add:
// - test.expectId, a unique id
// - test.expectPath, full path to test
// - test.actualMap, map of actual results
// - test.actualFinal, last result
// - test.expectedMap, maps of all expected results
// - test.baseMap, map of base expected results. Can be undefined.
// - test.flagMap, map of flag expected results. Can be undefined.
// For all crashing tests without crash_site, set crash_site to "Can't identify".
let nextId = 1;
let baseCount = 0;
t.traverse(
test => true,
(test, path) => {
test.expectId = nextId++;
globalTestMap.set(test.expectId, test);
test.expectPath = path;
test.actualMap = new Map();
for (let result of test.actual.split(" ")) {
test.actualFinal = result; // last result count as definite.
test.actualMap.set(result, true);
}
test.expectedMap = new Map();
for (let result of test.expected.split(" ")) {
test.expectedMap.set(result, true);
}
if (test.actualMap.has("CRASH") && !test["crash_site"]) {
test["crash_site"] = "Can't identify";
}
if ("base_expectations" in test) {
GUI.hasBaseExpectations = true;
baseCount++;
test.baseMap = new Map();
test.base_expectations.forEach( result => test.baseMap.set(result, true));
}
if ("flag_expectations" in test) {
test.flagMap = new Map();
test.flag_expectations.forEach( result => test.flagMap.set(result, true));
}
}
);
},
nextExpectation: function(expectation) {
let nextSiblingWithKeyedParentSkip = function(el) {
let sibling = el.nextElementSibling;
if (sibling == null && el.parentNode.parentNode.id == "report") {
let nextContainer = el.parentNode.nextElementSibling;
while (nextContainer != null && nextContainer.firstElementChild == null)
nextContainer = nextContainer.nextElementSibling;
if (nextContainer)
sibling = nextContainer.firstElementChild;
}
return sibling;
};
if (expectation == null)
return null;
let sibling = nextSiblingWithKeyedParentSkip(expectation);
while (sibling) {
if (sibling.classList.contains("expect"))
return sibling;
else
sibling = nextSiblingWithKeyedParentSkip(sibling);
}
},
previousExpectation: function(expectation) {
let previousSiblingWithKeyedParentSkip = function(el) {
let sibling = el.previousElementSibling;
if (sibling == null && el.parentNode.parentNode.id == "report") {
let previousContainer = el.parentNode.previousElementSibling;
while (previousContainer != null && previousContainer.firstElementChild == null)
previousContainer = previousContainer.previousElementSibling;
if (previousContainer)
sibling = previousContainer.lastElementChild;
}
return sibling;
};
if (expectation == null)
return null;
let sibling = previousSiblingWithKeyedParentSkip(expectation);
while (sibling) {
if (sibling.classList.contains("expect"))
return sibling;
else
sibling = previousSiblingWithKeyedParentSkip(sibling);
}
},
showNextExpectation: function(backward) {
let nextExpectation;
let activeExpectation = GUI.activeExpectation();
let openExpectation = GUI.openExpectation();
if (openExpectation)
GUI.hideResults(openExpectation);
if (openExpectation && openExpectation == activeExpectation) {
nextExpectation = backward ?
GUI.previousExpectation(openExpectation) :
GUI.nextExpectation(openExpectation);
} else {
if (activeExpectation)
nextExpectation = activeExpectation;
else
nextExpectation = document.querySelector(".expect");
}
if (nextExpectation) {
nextExpectation.focus();
GUI.showResults(nextExpectation);
return true;
}
},
openExpectation: function() {
let openDetails = document.querySelector(".details.open");
return openDetails && GUI.getExpectation(openDetails);
},
activeExpectation: function() {
return GUI.getExpectation(document.activeElement) || GUI.openExpectation();
},
initEvents: function() {
document.querySelector("#report").addEventListener("mousedown",
(ev) => { if (GUI.isFlag(ev.target)) ev.preventDefault() }
);
document.querySelector("#report").addEventListener("click", function(ev) {
let expectation = GUI.getExpectation(ev.target);
if (ev.target.nodeName == "A" || ev.target.nodeName == "INPUT" ||
GUI.closest(ev.target, "result-frame")) {
; // Clicks in anchor, input or result-frame should perform default action
} else if (GUI.isFlag(ev.target)) {
GUI.toggleFlag(ev.target, ev);
} else if (expectation && window.getSelection().type != "Range") {
GUI.toggleResults(expectation, ev);
ev.preventDefault();
ev.stopPropagation();
}
});
document.addEventListener("keydown", ev => {
{
if (ev.target.nodeName == "INPUT")
return;
switch(ev.key) {
case "Escape": {
// Close/hide divs.
for (let el of Array.from(document.querySelectorAll(".close-on-esc")))
el.remove();
for (let el of Array.from(document.querySelectorAll(".hide-on-esc")))
el.classList.add("hidden");
if (document.activeElement && document.activeElement.classList.contains("expect"))
GUI.hideResults(document.activeElement);
document.getSelection().removeAllRanges();
}
break;
case " ": // Scroll to next expectation.
if (GUI.showNextExpectation(ev.shiftKey))
ev.preventDefault();
break;
case "j":
case "J": {
let current = GUI.activeExpectation();
let nextExpectation = current ? GUI.nextExpectation(current) : document.querySelector(".expect");
if (nextExpectation)
nextExpectation.focus();
}
break;
case "k":
case "K": {
let current = GUI.activeExpectation();
let nextExpectation = current ? GUI.previousExpectation(current) : document.querySelector(".expect");
if (nextExpectation)
nextExpectation.focus();
}
break;
case "Enter":
let expectation = GUI.getExpectation(ev.target);
if (expectation)
GUI.toggleResults(expectation, ev);
break;
case "a":
case "A":
if (ev.ctrlKey) {
GUI.selectText(document.querySelector("#report"));
ev.preventDefault();
}
break;
case "f":
case "F": {
let expectation = GUI.getExpectation(ev.target);
if (expectation)
GUI.toggleFlag(expectation.querySelector(".flag"), ev);
}
break;
case "?":
case "/": // Blinks currect image error rect
if (document.querySelector(".image-viewer"))
document.querySelector(".image-viewer").controller.blinkDiffRect();
break;
case "p": {
let expectation = GUI.getExpectation(ev.target);
if (expectation)
console.log(Report.getTestById(expectation.getAttribute("data-id")));
break;
}
}
}
});
for (let checkbox of Array.from(document.querySelectorAll("#filters input[type=checkbox]"))) {
checkbox.addEventListener("change", ev => Query.filterChanged(ev));
}
},
selectText: function(el) {
let range = document.createRange();
range.setStart(el, 0);
range.setEnd(el, el.childNodes.length);
let selection = document.getSelection();
selection.removeAllRanges();
selection.addRange(range);
},
copyText: function(el) {
GUI.selectText(el);
document.execCommand("Copy");
document.getSelection().removeAllRanges();
document.querySelector("#copied").classList.remove("hidden");
if (GUI.endCopyTextTimer)
window.clearTimeout(GUI.endCopyTextTimer);
GUI.endCopyTextTimer = window.setTimeout(_ => {
document.querySelector("#copied").classList.add("hidden");
GUI.endCopyTextTimer = null;
}, 2000);
},
copyResult: function(asSingleLine) {
console.time("copyResult");
let flaggedOnly = document.querySelector(".flagged") &&
document.querySelector("#flagged_only_checkbox").checked;
if (!flaggedOnly && !asSingleLine) {
GUI.copyText(document.querySelector("#report"));
return;
}
let printer = Report.getDefaultPrinter();
let singleLine = "";
let traversal = new Traversal(globalResults.tests);
traversal.traverse(Query.lastReport.filter, (test, path) => {
if (flaggedOnly && !test.flagged)
return;
if (asSingleLine) {
let pathParser = new PathParser(path);
singleLine += pathParser.dir + pathParser.file + " ";
} else {
printer.print(test, path, traversal);
}
});
let div = document.createElement("div");
div.setAttribute("style", "overflow: hidden; color: white; height: 1px");
if (asSingleLine)
div.textContent = singleLine;
else
printer.render(traversal.html, div);
document.body.appendChild(div);
GUI.copyText(div);
document.body.removeChild(div);
console.timeEnd("copyResult");
},
updateCopyButtons: function() {
let noResult = document.querySelector("#none");
document.querySelector("#copy_report").disabled = noResult;
document.querySelector("#copy_single_line").disabled = noResult;
let flagged_only = document.querySelector("#flagged_only");
if (document.querySelector(".flagged"))
flagged_only.classList.remove("hidden");
else
flagged_only.classList.add("hidden");
},
printSummary: function (fullResults) {
document.querySelector("#builder_name").innerText = fullResults.builder_name || document.lastModified;
document.querySelector("#summary_passed").innerText = fullResults.num_passes;
document.querySelector("#summary_regressions").innerText = fullResults.num_regressions;
let failures = fullResults["num_failures_by_type"];
var totalFailures = 0;
let resultsText = "";
for (let p in failures) {
if (failures[p])
resultsText += p.toLowerCase() + ": " + failures[p] + " ";
}
document.querySelector("#summary_details").innerText = resultsText;
// Initialize query counts.
let counts = {
"count_unexpected_pass": 0,
"count_regressions": 0,
"count_testexpectations": 0,
"count_flaky": 0,
"count_unexpected_flaky": 0,
"count_all": 0,
"count_flagfailure": 0,
"count_flagpass" : 0
};
var t = new Traversal(fullResults.tests);
t.traverse( test => {
counts.count_all++;
if (Filters.unexpectedPass(test))
counts.count_unexpected_pass++;
if (Filters.regression(test))
counts.count_regressions++;
if (Filters.notpass(test))
counts.count_testexpectations++;
if (Filters.flaky(test))
counts.count_flaky++;
if (Filters.unexpectedFlaky(test))
counts.count_unexpected_flaky++;
if (Filters.flagFailure(test))
counts.count_flagfailure++;
if (Filters.flagPass(test))
counts.count_flagpass++;
});
console.assert(
counts.count_regressions == fullResults.num_regressions,
"Numbers of regressions mismatch: in fullResult:" + fullResults.num_regressions +
" filtered:" + counts.count_regressions);
for (let p in counts)
document.querySelector("#" + p).innerText = counts[p];
if (counts.count_regressions > 0)
document.querySelector("#count_regressions").parentElement.classList.add("bad");
if (counts.count_unexpected_flaky > 0)
document.querySelector("#count_unexpected_flaky").parentElement.classList.add("bad");
if (counts.count_unexpected_pass > 0)
document.querySelector("#count_unexpected_pass").parentElement.classList.add("good");
document.querySelector("#summary_total").innerText = counts.count_all;
if (!GUI.hasBaseExpectations)
document.querySelector("#flag-toolbar").classList.add("hidden");
else {
document.querySelector("#flag-toolbar").classList.remove("hidden");
let flagName = fullResults.flag_name || "";
flagName = flagName.replace('/', '')
.replace('enable-blink-features', '').replace('=', '');
Array.from(document.querySelectorAll(".flag_name")).forEach( el => {
el.innerText = flagName;
});
}
},
getExpectation: function(el) {
let result = GUI.closest(el, "expect");
if (result)
return result;
result = GUI.closest(el, "result-frame");
return result ? result.previousElementSibling : null;
},
isFlag: function(el) {
return el.classList.contains("flag");
},
toggleVisibility: function(id) {
document.querySelector("#" + id).classList.toggle("hidden");
},
toggleFlag: function(el, ev) {
let expectation = GUI.getExpectation(el);
let test = Report.getTestById(expectation.getAttribute("data-id"));
if (!test)
throw "could not find test by id";
el.classList.toggle("flagged");
if (el.classList.contains("flagged"))
test.flagged = true;
else
test.flagged = false;
if (ev.metaKey) { // apply to all
for (let expectation of Array.from(document.querySelectorAll(".expect"))) {
let newTest = Report.getTestById(expectation.getAttribute("data-id"));
let toggleEl = expectation.querySelector(".flag");
if (test.flagged) {
toggleEl.classList.add("flagged");
newTest.flagged = true;
} else {
toggleEl.classList.remove("flagged");
newTest.flagged = false;
}
}
}
GUI.updateCopyButtons();
},
toggleResults: function(expectation, event) {
let applyToAll = event && event.metaKey;
let closeOthers = !applyToAll && event && !event.shiftKey;
let details = expectation.querySelector(".details");
let isOpen = details.classList.contains("open");
if (applyToAll) {
let allExpectations = Array.from(document.querySelectorAll(".expect"));
if (allExpectations.length > 100) {
console.error("Too many details to be shown at once");
} else {
for (let e of allExpectations)
if (e != expectation)
isOpen ? GUI.hideResults(e) : GUI.showResults(e, true);
}
}
if (closeOthers) {
for (let el of Array.from(document.querySelectorAll(".details.open")))
GUI.hideResults(el.parentNode);
}
if (isOpen) {
GUI.hideResults(expectation);
expectation.focus();
}
else {
GUI.showResults(expectation);
}
},
getResultViewer: function(toolbar) {
let output = GUI.closest(toolbar, "result-frame").querySelector(".result-output");
if (output && output.children.length > 0)
return output.children[0];
},
setResultViewer: function(toolbar, viewer) {
let output = GUI.closest(toolbar, "result-frame").querySelector(".result-output");
output.innerHTML = "";
output.appendChild(viewer);
},
closest: function (el, className) {
while (el && el.classList) {
if (el.classList.contains(className))
return el;
else
el = el.parentNode;
}
},
showResults: function(expectation, doNotScroll) {
let details = expectation.querySelector(".details");
if (details.classList.contains("open"))
return;
details.classList.add("open");
let testId = parseInt(expectation.getAttribute("data-id"));
let test = Report.getTestById(testId);
if (!test)
console.error("could not find test by id");
let results = Report.getResultsDiv(test);
results.classList.add("results");
expectation.parentNode.insertBefore(results, expectation.nextSibling);
for (let toolbar of Array.from(results.querySelectorAll(".tx-toolbar"))) {
if (toolbar.showDefault) {
toolbar.showDefault();
break;
}
}
if (doNotScroll) {
return;
}
// Scroll result into view, leaving space for image zoom, and
// test title on top.
let zoomHeight = window.innerWidth / 3 + 10;
let resultHeight = Math.min(window.innerHeight - 80, 630) + 34;
let overflow = zoomHeight + resultHeight + expectation.offsetHeight - window.innerHeight;
if (overflow > 0)
zoomHeight = Math.max(0, zoomHeight - overflow);
window.scrollTo(0, expectation.offsetTop - zoomHeight);
},
hideResults: function(expectation) {
let details = expectation.querySelector(".details");
if (!details.classList.contains("open"))
return;
expectation.querySelector(".details").classList.remove("open");
expectation.nextSibling.remove();
}
}; // GUI
</script>
<script>
// test-expectation components
// These components are in a separate script tag.
// They are independent of the rest of the page so they can be reused
// in different pages in the future.
//
// Current components include toolbars, and viewers.
// Toolbars control viewers.
// Viewers present different views into test results.
class TXToolbar {
importStyle() {
if (document.querySelector("#TXToolbarCSS"))
return;
let style = document.createElement("style");
style.setAttribute("type", "text/css");
style.setAttribute("id", "TXToolbarCSS");
style.innerText =
`.tx-toolbar a[data-selected] {
background-color: #DDD;
text-decoration-color: aquamarine;
}
.tx-toolbar a {
padding-left: 4px;
padding-right: 4px;
}
`;
document.head.appendChild(style);
}
getViewer() {
let viewer = GUI.getResultViewer(this.toolbar);
return (viewer && viewer.owner == this) ? viewer : null;
}
defaultExtendSelection(anchor) {
return false;
}
selectAnchor(target, extendSelection) {
let toggle = false;
if (extendSelection === undefined) {
toggle = true;
extendSelection = this.defaultExtendSelection(target);
}
for (let anchor of Array.from(this.toolbar.querySelectorAll("a"))) {
if (anchor == target) {
if (toggle) {
if (anchor.hasAttribute("data-selected"))
anchor.removeAttribute("data-selected");
else
anchor.setAttribute("data-selected", "");
} else {
anchor.setAttribute("data-selected", "");
}
} else if (!extendSelection && !this.defaultExtendSelection(anchor)) {
anchor.removeAttribute("data-selected");
}
}
this.updateViewer(target);
}
} // class TXToolbar
class ImageResultsToolbar extends TXToolbar {
constructor() {
super();
this.boundViewOptionsChangeHandler = this.viewOptionsChangeHandler.bind(this);
}
createDom(test) {
this.importStyle();
let pathParser = new PathParser(test.expectPath);
this.toolbar = document.createElement("li");
this.toolbar.showDefault = _ => this.viewOptionsChangeHandler({target: this.toolbar.querySelector(".view-options")});
this.toolbar.classList.add("image-toolbar");
this.toolbar.classList.add("tx-toolbar");
let missing = test.actual == "MISSING";
let html = `
image:
<a class="actual" href="${pathParser.resultLink("-actual.png")}" title="Actual result">actual</a>`;
if (!missing)
html += `
<a class="expected" href="${pathParser.resultLink("-expected.png")}" title="Expected result">expected</a>
<a class="diff" href="${pathParser.resultLink("-diff.png")}" title="Difference">diff</a>
`;
html += `
<label><select class="view-options">
<option value="single">Single view</option>
<option value="animated">Animated view</option>
<option value="multiple">Side by side view</option>
</label>
`;
this.toolbar.innerHTML = html;
this.toolbar.addEventListener("click", ev => {
if (ev.target.tagName == "A") {
this.selectAnchor(ev.target);
ev.preventDefault();
if (this.animationIntervalId) {
// Suspend animation to show the clicked view for one second.
this.animationPaused = true;
window.setTimeout(_ => { this.animationPaused = false }, 1000);
}
}
});
let viewOptions = this.toolbar.querySelector(".view-options");
viewOptions.addEventListener("change", this.boundViewOptionsChangeHandler);
if (window.localStorage.getItem("ImageToolbarView"))
viewOptions.value = window.localStorage.getItem("ImageToolbarView");
else
viewOptions.value = "animated";
return this.toolbar;
}
viewOptionsChangeHandler(ev) {
let viewOptions = ev.target;
try {
window.localStorage.setItem("ImageToolbarView", viewOptions.value);
} catch(ex) {}
switch(viewOptions.value) {
case "animated": {
let selectedAnchors = Array.from(this.toolbar.querySelectorAll("a[data-selected]"));
if (selectedAnchors.length == 0)
this.selectAnchor(this.toolbar.querySelector("a"));
this.setAnimation(true);
}
break;
case "single": {
this.setAnimation(false);
// Make sure only one is selected.
let selectedAnchors = Array.from(this.toolbar.querySelectorAll("a[data-selected]"));
if (selectedAnchors.length == 0)
this.selectAnchor(this.toolbar.querySelector("a"));
else
this.selectAnchor(selectedAnchors[0]);
}
break;
case "multiple": {
this.setAnimation(false);
for (let anchor of Array.from(this.toolbar.querySelectorAll("a")))
this.selectAnchor(anchor, true);
}
break;
default:
console.error("unknown view option");
}
}
defaultExtendSelection(anchor) {
return this.toolbar.querySelector(".view-options").value == "multiple";
}
updateViewer(element) {
// Find currently selected anchor.
let selectedAnchors = Array.from(this.toolbar.querySelectorAll("a[data-selected]"));
if (selectedAnchors.length == 0) {
selectedAnchors.push(this.toolbar.querySelector("a"));
selectedAnchors[0].setAttribute("data-selected", "");
}
let viewer = this.getViewer();
if (!viewer) {
let imgInfo = Array.from(this.toolbar.querySelectorAll("a")).map(
anchor => { return {src: anchor.href, title: anchor.title}; });
viewer = (new ImageResultsViewer()).createDom(imgInfo, this);
this.setAnimation(this.toolbar.querySelector(".view-options").value == "animated");
GUI.setResultViewer(element, viewer);
}
viewer.showImages(selectedAnchors.map(anchor => anchor.href));
}
setAnimation(animate) {
if (animate) {
if (!this.animationIntervalId)
this.animationIntervalId = window.setInterval(_ => {
if (!this.getViewer()) {
console.log("element is gone");
window.clearInterval(this.animationIntervalId);
this.animationIntervalId = null;
return;
}
if (this.animationPaused)
return;
// Find next anchor.
let allAnchors = Array.from(this.toolbar.querySelectorAll("a"));
let nextAnchor = allAnchors[0];
for (let i = 0; i < allAnchors.length; i++) {
if (allAnchors[i].hasAttribute("data-selected")) {
nextAnchor = allAnchors[(i + 1) % (allAnchors.length - 1)];
break;
}
}
this.selectAnchor(nextAnchor, false);
}, 400);
} else {
this.animationPaused = false;
if (this.animationIntervalId) {
window.clearInterval(this.animationIntervalId);
this.animationIntervalId = null;
}
}
}
} // class ImageResultsToolbar
class ImageResultsViewer {
constructor() {
this.boundEventMoveHandler = this.eventMoveHandler.bind(this);
this.boundTileScrollHandler = this.tileScrollHandler.bind(this);
this.boundWheelHandler = this.tileWheelHandler.bind(this);
}
importStyle() {
if (document.querySelector("#ImageViewerCSS"))
return;
let style = document.createElement("style");
style.setAttribute("type", "text/css");
style.setAttribute("id", "ImageViewerCSS");
style.innerText =
` .image-viewer {
position: relative;
display: flex;
}
.image-viewer-tile {
flex-shrink: 1;
border: 1px solid #EEE;
overflow: auto;
max-height: calc(100vh - 80px);
margin-right: 8px;
min-height: 605px;
}
.image-viewer-highlight {
position: absolute;
background-color: red;
opacity: 0.5;
box-shadow: 0px 0px 8px 2px rgba(255,0,0,1);
}
.image-viewer-highlight.animate {
animation-name: highlight-animation;
animation-duration: 0.15s;
animation-timing-function: ease-out;
animation-delay: 0s;
animation-direction: alternate;
animation-iteration-count: 2;
}
.image-viewer-highlight.animate-long {
animation-iteration-count: 4;
}
@keyframes highlight-animation {
0% {
display: block;
opacity: 0;
transform: scale(1.0);
}
100% {
display: block;
opacity: 0.5;
transform: scale(1.1);
}
}`;
document.head.appendChild(style);
}
/* ImageResultsViewer dom
<div class="image-viewer">
<div class="image-viewer-tile">
<img src="...-actual" title="Actual result">
</div>
<div class="image-viewer-tile">
<img src="...-expected" title="Expected result">
</div>
<div class="image-viewer-tile">
<img src="...-diff" title="Difference">
</div>
</div>
*/
createDom(imgInfo, owner) {
this.importStyle();
this.viewer = document.createElement("div");
// Viewer DOM API
this.viewer.showImages = this.showImages.bind(this);
this.viewer.owner = owner;
this.viewer.controller = this;
this.viewer.classList.add("image-viewer");
this.viewer.addEventListener("click", (ev) => {
this.toggleZoom({x: ev.offsetX, y: ev.offsetY}, ev.target);
});
for (let info of imgInfo) {
let imgTile = document.createElement("div");
imgTile.classList.add("image-viewer-tile");
imgTile.classList.add("hidden");
imgTile.innerHTML = `<img src="${info.src}" title="${info.title}">`;
imgTile.addEventListener("scroll", this.boundTileScrollHandler, {passive: true});
imgTile.addEventListener("wheel", this.boundWheelHandler);
this.viewer.appendChild(imgTile);
if (info.title == "Difference") {
this.diffImage = new Image();
this.diffImage.addEventListener("load", function(ev) {
this.computeDiffRect(ev.target);
}.bind(this), false);
this.diffImage.src = info.src;
}
}
return this.viewer;
}
blinkDiffRect() {
this.computeDiffRect(this.diffImage);
}
// public API
showImages(sources) {
let tiles = Array.from(this.viewer.querySelectorAll(".image-viewer-tile"));
// Newly shown tiles should have the same scroll position as existing tiles.
let scrollPosition;
for (let tile of tiles) {
if (!tile.classList.contains("hidden")) {
scrollPosition = {top: tile.scrollTop, left: tile.scrollLeft};
break;
}
}
for (let tile of tiles) {
let tileImage = tile.querySelector("img");
let visible = sources.includes(tileImage.src);
if (visible) {
tile.classList.remove("hidden");
if (scrollPosition) {
tile.scrollTop = scrollPosition.top;
tile.scrollLeft = scrollPosition.left;
}
}
else
tile.classList.add("hidden");
}
}
findVisibleImage(preferredImage) {
if (preferredImage && !preferredImage.parentNode.classList.contains("hidden"))
return preferredImage;
for (let image of Array.from(this.viewer.querySelectorAll("img")))
if (!image.parentNode.classList.contains("hidden") && image)
return image;
}
tileScrollHandler(ev) {
let sourceTile = ev.target;
if (sourceTile.classList.contains("hidden"))
return;
for (let tile of Array.from(this.viewer.querySelectorAll(".image-viewer-tile"))) {
if (tile != sourceTile) {
tile.scrollTop = sourceTile.scrollTop;
tile.scrollLeft = sourceTile.scrollLeft;
}
}
}
tileWheelHandler(ev) {
// Prevent page back/forward gestures when scrolling inside tiles.
if (Math.abs(ev.deltaY) > Math.abs(ev.deltaX))
return;
let target = ev.currentTarget;
let blockScrollRight = (target.scrollLeft + target.clientWidth) == target.scrollWidth;
let blockScrollLeft = target.scrollLeft == 0;
if (blockScrollRight && ev.deltaX > 0) {
ev.preventDefault();
}
if (blockScrollLeft && ev.deltaX < 0) {
ev.preventDefault();
}
}
eventMoveHandler(ev) {
let imageRect = this.findVisibleImage(this.zoomTarget).getBoundingClientRect();
let zoom = this.viewer.querySelector(".image-viewer-zoom");
if (zoom)
zoom.showLocation({
x: ev.pageX - (imageRect.left + window.scrollX),
y: ev.pageY - (imageRect.top + window.scrollY)
});
}
toggleZoom(location, zoomTarget) {
let zoom = this.viewer.querySelector(".image-viewer-zoom");
if (zoom) {
zoom.remove();
delete this.zoomTarget;
this.viewer.removeEventListener("mousemove", this.boundEventMoveHandler);
this.viewer.removeEventListener("touchmove", this.boundEventMoveHandler);
} else {
this.zoomTarget = zoomTarget;
let zoomElement = (new ImageViewerZoom()).createDom(this.viewer, this.viewer.querySelectorAll("img"));
this.viewer.addEventListener("mousemove", this.boundEventMoveHandler);
this.viewer.addEventListener("touchmove", this.boundEventMoveHandler);
zoomElement.showLocation(location);
}
}
computeDiffRect(image) {
let canvas = document.createElement("canvas");
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
let ctx = canvas.getContext("2d");
ctx.drawImage(image, 0, 0);
let top = image.naturalHeight;
let bottom = 0;
let left = image.naturalWidth;
let right = 0;
try {
let imageData = ctx.getImageData(0, 0, image.naturalWidth, image.naturalHeight);
for (let x = 0; x < imageData.width; x++)
for (let y = 0; y < imageData.height; y++) {
let offset = y * imageData.width * 4 + x * 4;
if (imageData.data[offset] == 255 && imageData.data[offset+1] == 0 && imageData.data[offset+2] == 0) {
if (x < left) left = x;
if (x > right) right = x;
if (y < top) top = y;
if (y > bottom) bottom = y;
}
}
if (bottom != 0 && right != 0)
this.animateDiffRect({top: top, left: left, width: Math.max(0, right - left), height: Math.max(0, bottom - top)}, image);
} catch(ex) {
console.error("cannot show error rect on local files because of cross origin taint");
}
}
animateDiffRect(diffRect, image) {
if (!image)
return;
if (!image.complete) {
image.addEventListener("load", _ => this.animateDiffRect(diffRect, image));
return;
}
let highlight = document.createElement("div");
highlight.classList.add("image-viewer-highlight");
highlight.style.top = image.offsetTop + diffRect.top + "px";
highlight.style.left = image.offsetLeft + diffRect.left + "px";
highlight.style.width = Math.max(5, diffRect.width) + "px";
highlight.style.height = Math.max(5, diffRect.height) + "px";
this.viewer.appendChild(highlight);
highlight.addEventListener("animationend", _ => highlight.remove());
if (diffRect.width < 100 || diffRect.height < 100) {
highlight.classList.add("animate-long");
}
highlight.classList.add("animate");
}
} // class ImageResultsViewer
class ImageViewerZoom {
constructor(zoomFactor = 6) {
this.zoomFactor = zoomFactor;
}
importStyle() {
if (document.querySelector("#ImageViewerZoomCSS"))
return;
let style = document.createElement("style");
style.setAttribute("type", "text/css");
style.setAttribute("id", "ImageViewerZoomCSS");
style.innerText = `
.image-viewer-zoom {
display: flex;
background-color: #555;
position: fixed;
padding-left: 4px;
padding-right: 4px;
top: 20px;
left: 16px;
bottom: 16px;
right: 16px;
height: calc((100vw - 32px) / 3);
max-height: 50vh;
box-shadow: 0px 0px 10px 2px rgba(0,0,0,0.68);
}
.image-viewer-zoom-tile {
position: relative;
flex-grow: 1;
flex-shrink: 1;
margin: 16px 8px 16px 8px;
}
.image-viewer-zoom-tile > canvas {
width: 100%;
height: 100%;
}
.image-viewer-zoom-tile > .title {
position: absolute;
top: -36px;
left: -px;
background-color: white;
white-space: nowrap;
overflow: hidden;
}
.image-viewer-zoom-color {
font-size: smaller;
}
`;
document.head.appendChild(style);
}
/*
<div class="image-viewer-zoom">
<div class="image-viewer-zoom-tile"> // repeat for each image
<canvas></canvas>
<div class="title"><span class="color"></span></div>
</div>
*/
createDom(container, images) {
this.importStyle();
this.images = Array.from(images);
this.zoomElement = document.createElement("div");
// ImageViewer DOM API
this.zoomElement.showLocation = this.showLocation.bind(this);
this.zoomElement.classList.add("image-viewer-zoom");
this.zoomElement.classList.add("close-on-esc");
container.appendChild(this.zoomElement);
// Add canvas for each image.
for (let img of this.images) {
let tile = document.createElement("div");
tile.classList.add("image-viewer-zoom-tile");
tile.innerHTML = `<canvas></canvas><div class="title">${img.title} <span class="image-viewer-zoom-color"></span></div>`;
this.zoomElement.appendChild(tile);
}
return this.zoomElement;
}
showLocation(location) {
let canvases = Array.from(this.zoomElement.querySelectorAll("canvas"));
let naturalSize;
// Find non-hidden image to determine size.
for (let image of this.images) {
if (image.complete && !image.classList.contains("hidden")) {
naturalSize = {width: image.naturalWidth, height: image.naturalHeight};
break;
}
}
if (!naturalSize) {
console.warn("image not loaded");
return;
}
for (let i = 0; i < canvases.length; i++) {
// Set canvas to right size if window resized.
let canvasWidth = canvases[i].clientWidth;
let canvasHeight = canvases[i].clientHeight;
canvases[i].width = canvasWidth;
canvases[i].height = canvasHeight;
let ctx = canvases[i].getContext("2d");
ctx.imageSmoothingEnabled = false;
// Copy over zoomed image.
let sWidth = canvasWidth / this.zoomFactor;
let sHeight = canvasHeight / this.zoomFactor;
let pad = 20;
let sx = Math.floor(Math.min(Math.max(-pad, location.x - sWidth / 2), naturalSize.width + pad - sWidth));
let sy = Math.floor(Math.min(Math.max(-pad, location.y - sHeight / 2), naturalSize.height + pad - sHeight));
ctx.drawImage(this.images[i], sx, sy, sWidth, sHeight, 0, 0, canvasWidth, canvasHeight);
// Draw grid.
ctx.strokeStyle = "rgba(0,0,0,0.05)";
let pixelSize = canvasWidth / sWidth;
ctx.beginPath();
for (let y = 1; y < sHeight; y++) {
ctx.moveTo(0, y * pixelSize);
ctx.lineTo(canvasWidth, y * pixelSize);
}
for (let x = 1; x < sWidth; x++) {
ctx.moveTo(x*pixelSize, 0);
ctx.lineTo(x*pixelSize, canvasHeight);
}
ctx.closePath();
ctx.stroke();
// Highlight middle pixel whose color is measured.
try { // getImageData throws on local file system.
let middleX = Math.floor(sWidth / 2);
let middleY = Math.floor(sHeight / 2);
let imageData = ctx.getImageData(middleX * pixelSize + 2, middleY * pixelSize + 2, 1, 1);
let r = imageData.data[0];
let g = imageData.data[1];
let b = imageData.data[2];
let a = imageData.data[3];
let colorSpan = canvases[i].parentNode.querySelector(".image-viewer-zoom-color");
let color = `rgba(${r}, ${g}, ${b}, ${a})`;
colorSpan.innerText = `(${sx + middleX}, ${sy + middleY}) ${color}`;
colorSpan.style.backgroundColor = color;
colorSpan.style.color = ((r + g + b) > 300 ) ? "black" : "white";
ctx.beginPath();
ctx.moveTo(middleX * pixelSize, middleY * pixelSize);
ctx.lineTo((middleX + 1) * pixelSize, middleY * pixelSize);
ctx.lineTo((middleX + 1) * pixelSize, (middleY + 1) * pixelSize);
ctx.lineTo(middleX * pixelSize, (middleY + 1) * pixelSize);
ctx.lineTo(middleX * pixelSize, middleY * pixelSize);
ctx.strokeStyle = "rgba(0,0,0,0.2)";
ctx.closePath();
ctx.stroke();
} catch(ex) {}
}
}
} // class ImageViewerZoom
class TextResultsToolbar extends TXToolbar {
constructor() {
super();
}
defaultExtendSelection(anchor) {
return anchor.className == "repaint";
}
createDom(test) {
this.importStyle();
let pathParser = new PathParser(test.expectPath);
this.toolbar = document.createElement("li");
this.toolbar.showDefault = (_ => {
this.selectAnchor(
this.toolbar.querySelector("a.pretty") || this.toolbar.querySelector("a.actual"));
let repaintOverlay = this.toolbar.querySelector("a.repaint");
if (repaintOverlay)
this.selectAnchor(repaintOverlay, true);
}).bind(this);
this.toolbar.classList.add("text-results-toolbar");
this.toolbar.classList.add("tx-toolbar");
let missing = test.actual == "MISSING";
let html = "text:";
if (!test.is_testharness_test && !missing)
html += `<a class="pretty" href="${pathParser.resultLink("-pretty-diff.html")}" title="Pretty difference">pretty</a>`;
html += `<a class="actual" href="${pathParser.resultLink("-actual.txt")}" title="Actual text">actual</a>`;
if (!test.is_testharness_test && !missing) {
html += `<a class="expected" href="${pathParser.resultLink("-expected.txt")}" title="Expected result">expected</a><a
class="diff" href="${pathParser.resultLink("-diff.txt")}" title="Difference">diff</a>`;
}
if (test.has_repaint_overlay)
html += ` <a class="repaint" href="${pathParser.repaintOverlayLink()}" title="Repaint overlay">repaint</a>`;
this.toolbar.innerHTML = html + "\n";
this.toolbar.addEventListener("click", ev => {
if (ev.target.tagName == "A") {
this.selectAnchor(ev.target);
ev.preventDefault();
}
});
return this.toolbar;
}
updateViewer() {
// Find currently selected anchor.
let selectedAnchors = Array.from(this.toolbar.querySelectorAll("a[data-selected]"));
if (selectedAnchors.length == 0) {
selectedAnchors.push(this.toolbar.querySelector("a"));
selectedAnchors[0].setAttribute("data-selected", "");
}
let viewer = this.getViewer();
if (!viewer) {
let fileInfo = Array.from(this.toolbar.querySelectorAll("a")).map(
anchor => { return {src: anchor.href, title: anchor.title}; });
viewer = (new TextFileViewer()).createDom(fileInfo, this);
GUI.setResultViewer(this.toolbar, viewer);
}
viewer.showFile(selectedAnchors.map( anchor => anchor.href));
}
} // class TextResultsToolbar
class TextFileViewer {
importStyle() {
if (document.querySelector("#TextFileViewerCSS"))
return;
let style = document.createElement("style");
style.setAttribute("type", "text/css");
style.setAttribute("id", "TextFileViewerCSS");
style.innerText =
` .text-file-viewer {
position: relative;
display: flex;
}
.text-file-viewer > iframe {
flex-shrink: 1;
flex-grow: 1;
border: 1px solid #888;
overflow: scroll;
max-height: calc(100vh - 80px);
margin-right: 8px;
height: 600px;
}
`;
document.head.appendChild(style);
}
/* TextFileViewer dom
<div class="text-file-viewer">
<iframe class="text-file-viewer-iframe" src="file..">
<iframe class="text-file-viewer-iframe" src="file..">
<iframe class="text-file-viewer-iframe" src="file..">
</div>
*/
createDom(fileInfo, owner) {
this.importStyle();
this.viewer = document.createElement("div");
// TextFileViewer DOM API
this.viewer.owner = owner;
this.viewer.showFile = this.showFile.bind(this);
this.viewer.classList.add("text-file-viewer");
for (let info of fileInfo) {
let iframe = document.createElement("iframe");
iframe.classList.add("text-file-viewer-iframe");
iframe.classList.add("hidden");
iframe.src = info.src;
iframe.setAttribute("tabindex", -1);
this.viewer.appendChild(iframe);
}
return this.viewer;
}
showFile(sources) {
let iframes = Array.from(this.viewer.querySelectorAll(".text-file-viewer-iframe"));
for (let iframe of iframes) {
let visible = sources.includes(iframe.src);
if (visible) {
iframe.classList.remove("hidden");
} else {
iframe.classList.add("hidden");
}
}
}
} // class TextFileViewer
class PlainHtmlToolbar extends TXToolbar {
createDom(html) {
super.importStyle();
let dom = document.createElement("li");
dom.classList.add("tx-toolbar");
dom.innerHTML = html;
return dom;
}
} // class PlainHtmlToolbar
class SimpleLinkToolbar extends TXToolbar {
createDom(href, title, text) {
super.importStyle();
this.toolbar = document.createElement("li");
// Toolbar DOM API
this.toolbar.showDefault = (_ => {this.selectAnchor(this.toolbar.querySelector("a"));}).bind(this);
this.toolbar.classList.add("tx-toolbar");
this.toolbar.innerHTML = `<a href="${href}" title="${title}">${text}</a>`;
this.toolbar.addEventListener("click", ev => {
if (ev.target.tagName == "A") {
this.selectAnchor(ev.target);
ev.preventDefault();
}
});
return this.toolbar;
}
updateViewer() {
// Find currently selected anchor.
let selectedAnchors = Array.from(this.toolbar.querySelectorAll("a[data-selected]"));
if (selectedAnchors.length == 0) {
selectedAnchors.push(this.toolbar.querySelector("a"));
selectedAnchors[0].setAttribute("data-selected", "");
}
let viewer = this.getViewer();
if (!viewer) {
let fileInfo = Array.from(this.toolbar.querySelectorAll("a")).map(
anchor => { return {src: anchor.href, title: anchor.title};});
viewer = (new TextFileViewer()).createDom(fileInfo, this);
GUI.setResultViewer(this.toolbar, viewer);
}
viewer.showFile(selectedAnchors.map( anchor => anchor.href));
}
} // class SimpleLinkToolbar
// Audio results.
class AudioResultsToolbar extends TXToolbar {
createDom(test) {
this.importStyle();
this.test = test;
let pathParser = new PathParser(test.expectPath);
this.toolbar = document.createElement("li");
this.toolbar.showDefault = (_ => {
this.selectAnchor(this.toolbar.querySelector("a"));
}).bind(this);
this.toolbar.classList.add("tx-toolbar");
this.toolbar.innerHTML = `<a href="#audioresults">audio results</a>\n`;
this.toolbar.addEventListener("click", ev => {
if (ev.target.tagName == "A") {
this.selectAnchor(ev.target);
ev.preventDefault();
}
});
return this.toolbar;
}
updateViewer() {
let viewer = this.getViewer();
if (!viewer) {
viewer = (new AudioResultsViewer()).createDom(this.test);
GUI.setResultViewer(this.toolbar, viewer);
}
}
}
class AudioResultsViewer {
createDom(test) {
this.viewer = document.createElement("div");
this.viewer.classList.add("audio-results-viewer");
let pathParser = new PathParser(test.expectPath);
this.viewer.innerHTML = `
<p>Actual:
<audio controls src="${pathParser.resultLink('-actual.wav')}"></audio>
<a download="actual.wav" href="${pathParser.resultLink('-actual.wav')}">download</a>
</p>
<p>Expected:
<audio controls src="${pathParser.resultLink('-expected.wav')}"></audio>
<a download="expected.wav" href="${pathParser.resultLink('-expected.wav')}">download</a>
</p>
`;
return this.viewer;
}
} // class AudioResultsViewer
</script>
<script>
// jsonp callback
function ADD_FULL_RESULTS(results) {
GUI.initPage(results);
}
</script>
<script src="full_results_jsonp.js"></script>