From 342377f7d119e4970eadc8fd13a3f65b1feffd07 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C3=ABl=20Zasso?= <targos@protonmail.com>
Date: Sun, 21 Feb 2021 18:36:29 +0100
Subject: [PATCH] test: update all Web Platform Tests

---
 test/fixtures/wpt/README.md                   |  18 +-
 .../common/third_party/reftest-analyzer.xhtml | 934 ++++++++++++++++++
 .../gb18030/gb18030-decoder.any.js            |  63 ++
 .../legacy-mb-schinese/gbk/gbk-decoder.any.js |  28 +
 .../wpt/encoding/textdecoder-arguments.any.js |  49 +
 .../wpt/encoding/textdecoder-labels.any.js    |   1 +
 test/fixtures/wpt/hr-time/basic.any.js        |   9 +
 .../timers/negative-setinterval.any.js        |  12 +
 .../timers/negative-settimeout.any.js         |   3 +
 .../timers/type-long-setinterval.any.js       |   8 +
 .../timers/type-long-settimeout.any.js        |   3 +
 test/fixtures/wpt/interfaces/html.idl         |  97 +-
 test/fixtures/wpt/resources/idlharness.js     |  18 +-
 test/fixtures/wpt/resources/test-only-api.js  |  69 --
 test/fixtures/wpt/resources/testharness.js    | 379 ++++---
 test/fixtures/wpt/url/url-constructor.any.js  |  39 +
 test/fixtures/wpt/url/url-origin.any.js       |  17 +
 .../url/urlsearchparams-constructor.any.js    |   2 +-
 .../wpt/url/urlsearchparams-foreach.any.js    |  10 +-
 test/fixtures/wpt/versions.json               |  50 +-
 test/wpt/status/encoding.json                 |  11 +
 test/wpt/status/hr-time.json                  |   9 +-
 test/wpt/status/html/webappapis/timers.json   |  15 +-
 test/wpt/status/url.json                      |   6 +
 test/wpt/test-hr-time.js                      |   3 +
 25 files changed, 1573 insertions(+), 280 deletions(-)
 create mode 100644 test/fixtures/wpt/common/third_party/reftest-analyzer.xhtml
 create mode 100644 test/fixtures/wpt/encoding/legacy-mb-schinese/gb18030/gb18030-decoder.any.js
 create mode 100644 test/fixtures/wpt/encoding/legacy-mb-schinese/gbk/gbk-decoder.any.js
 create mode 100644 test/fixtures/wpt/encoding/textdecoder-arguments.any.js
 create mode 100644 test/fixtures/wpt/html/webappapis/timers/negative-setinterval.any.js
 create mode 100644 test/fixtures/wpt/html/webappapis/timers/negative-settimeout.any.js
 create mode 100644 test/fixtures/wpt/html/webappapis/timers/type-long-setinterval.any.js
 create mode 100644 test/fixtures/wpt/html/webappapis/timers/type-long-settimeout.any.js
 create mode 100644 test/fixtures/wpt/url/url-constructor.any.js
 create mode 100644 test/fixtures/wpt/url/url-origin.any.js

diff --git a/test/fixtures/wpt/README.md b/test/fixtures/wpt/README.md
index a3cbbe667434e2..ee3b1cb61460b8 100644
--- a/test/fixtures/wpt/README.md
+++ b/test/fixtures/wpt/README.md
@@ -10,17 +10,17 @@ See [test/wpt](../../wpt/README.md) for information on how these tests are run.
 
 Last update:
 
+- common: https://github.com/web-platform-tests/wpt/tree/3586ff740b/common
 - console: https://github.com/web-platform-tests/wpt/tree/3b1f72e99a/console
-- encoding: https://github.com/web-platform-tests/wpt/tree/3c9820d1cc/encoding
-- url: https://github.com/web-platform-tests/wpt/tree/1783c9bccf/url
-- resources: https://github.com/web-platform-tests/wpt/tree/351a99782b/resources
-- interfaces: https://github.com/web-platform-tests/wpt/tree/b4be9a3fdf/interfaces
-- html/webappapis/microtask-queuing: https://github.com/web-platform-tests/wpt/tree/2c5c3c4c27/html/webappapis/microtask-queuing
-- html/webappapis/timers: https://github.com/web-platform-tests/wpt/tree/264f12bc7b/html/webappapis/timers
-- hr-time: https://github.com/web-platform-tests/wpt/tree/a5d1774ecf/hr-time
-- common: https://github.com/web-platform-tests/wpt/tree/841a51412f/common
-- dom/abort: https://github.com/web-platform-tests/wpt/tree/7caa3de747/dom/abort
+- dom/abort: https://github.com/web-platform-tests/wpt/tree/625e1310ce/dom/abort
+- encoding: https://github.com/web-platform-tests/wpt/tree/35f70910d3/encoding
 - FileAPI: https://github.com/web-platform-tests/wpt/tree/3b279420d4/FileAPI
+- hr-time: https://github.com/web-platform-tests/wpt/tree/9910784394/hr-time
+- html/webappapis/microtask-queuing: https://github.com/web-platform-tests/wpt/tree/2c5c3c4c27/html/webappapis/microtask-queuing
+- html/webappapis/timers: https://github.com/web-platform-tests/wpt/tree/5873f2d8f1/html/webappapis/timers
+- interfaces: https://github.com/web-platform-tests/wpt/tree/8602e9c9a1/interfaces
+- resources: https://github.com/web-platform-tests/wpt/tree/e366371a19/resources
+- url: https://github.com/web-platform-tests/wpt/tree/59d28c8f2d/url
 
 [Web Platform Tests]: https://github.com/web-platform-tests/wpt
 [`git node wpt`]: https://github.com/nodejs/node-core-utils/blob/master/docs/git-node.md#git-node-wpt
diff --git a/test/fixtures/wpt/common/third_party/reftest-analyzer.xhtml b/test/fixtures/wpt/common/third_party/reftest-analyzer.xhtml
new file mode 100644
index 00000000000000..4c7b26511acfa0
--- /dev/null
+++ b/test/fixtures/wpt/common/third_party/reftest-analyzer.xhtml
@@ -0,0 +1,934 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- -*- Mode: HTML; tab-width: 2; indent-tabs-mode: nil; -*- -->
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent expandtab: -->
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!--
+
+Features to add:
+* make the left and right parts of the viewer independently scrollable
+* make the test list filterable
+** default to only showing unexpecteds
+* add other ways to highlight differences other than circling?
+* add zoom/pan to images
+* Add ability to load log via XMLHttpRequest (also triggered via URL param)
+* color the test list based on pass/fail and expected/unexpected/random/skip
+* ability to load multiple logs ?
+** rename them by clicking on the name and editing
+** turn the test list into a collapsing tree view
+** move log loading into popup from viewer UI
+
+-->
+<!DOCTYPE html>
+<html lang="en-US" xml:lang="en-US" xmlns="http://www.w3.org/1999/xhtml">
+<head>
+  <title>Reftest analyzer</title>
+  <style type="text/css"><![CDATA[
+
+  html, body { margin: 0; }
+  html { padding: 0; }
+  body { padding: 4px; }
+
+  #pixelarea, #itemlist, #images { position: absolute; }
+  #itemlist, #images { overflow: auto; }
+  #pixelarea { top: 0; left: 0; width: 320px; height: 84px; overflow: visible }
+  #itemlist { top: 84px; left: 0; width: 320px; bottom: 0; }
+  #images { top: 0; bottom: 0; left: 320px; right: 0; }
+
+  #leftpane { width: 320px; }
+  #images { position: fixed; top: 10px; left: 340px; }
+
+  form#imgcontrols { margin: 0; display: block; }
+
+  #itemlist > table { border-collapse: collapse; }
+  #itemlist > table > tbody > tr > td { border: 1px solid; padding: 1px; }
+  #itemlist td.activeitem { background-color: yellow; }
+
+  /*
+  #itemlist > table > tbody > tr.pass > td.url { background: lime; }
+  #itemlist > table > tbody > tr.fail > td.url { background: red; }
+  */
+
+  #magnification > svg { display: block; width: 84px; height: 84px; }
+
+  #pixelinfo { font: small sans-serif; position: absolute; width: 200px; left: 84px; }
+  #pixelinfo table { border-collapse: collapse; }
+  #pixelinfo table th { white-space: nowrap; text-align: left; padding: 0; }
+  #pixelinfo table td { font-family: monospace; padding: 0 0 0 0.25em; }
+
+  #pixelhint { display: inline; color: #88f; cursor: help; }
+  #pixelhint > * { display: none; position: absolute; margin: 8px 0 0 8px; padding: 4px; width: 400px; background: #ffa; color: black; box-shadow: 3px 3px 2px #888; z-index: 1; }
+  #pixelhint:hover { color: #000; }
+  #pixelhint:hover > * { display: block; }
+  #pixelhint p { margin: 0; }
+  #pixelhint p + p { margin-top: 1em; }
+
+  ]]></style>
+  <script type="text/javascript"><![CDATA[
+
+var XLINK_NS = "http://www.w3.org/1999/xlink";
+var SVG_NS = "http://www.w3.org/2000/svg";
+var IMAGE_NOT_AVAILABLE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKAAAAASCAYAAADczdVTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAHy0lEQVRoge2aX2hb5xnGf2dYabROgqQkpMuKnWUJLmxHMFaa/SscteQiF5EvUgqLctEVrDJKK1+MolzkQr4IctgW+SLIheJc1BpFpswJw92FbaZsTCGTL0465AtntUekJdJ8lByVHbnnwLsLKbKdSJbiZBVjeuAYn+/P+z3fc97vfd9zbEVEhB566BK+1m0CPfx/o+eAPXQVbR3QqVapOl8FlR46h0O1Wu02iacCZfsasMKEz8vbx1JYE6fY/dXx6mEbFObPcvDVDBlznpc9G+2r8xNcvLqK2w39r4UI+fs7tFjmytgFFu718865EIebPGincI3zFz7Bcrtx97/GL0P+p+IPbSOgRwXtW3vpewqL/a/g5rgf39hit2m0hGUAHOHrrq3trmef4/lDB7Ay57n01zuPZXPX7jUunv+Yf9ktR7D/0CHca7/n3KXPsHbAuynkCWCZptgiImKLaVqP9NuW1bT9ceybpr3j+WJbYrVa3rbEatGZi2uixvWdrysilmWKae2M+5PqlktoosayLfubcrN10dAk24aynUsIxMVsadwUs+EX7dEyAlaXLqMoCj6fj5HkUqO9MD+Govjx+xXcXi+uoRAhvwuv182Z8Ws4AJUlxoZ8uNxuvF43ii/EtdXNNUuV68lR/IqC4gsxPj7KkE/BF5qmClRXrzFSt+/1ulDOjLNU6eQ4OcyPDqH4hhg5O4LicuN2K4xcvk6jjHUKJM8O1fvcKMoZkouFOq1VPp1OcuXGAvrvfsv0lWmSySTzN0sdH+jyYhK/ouB2e/G6XfjPJikBVG8SUhT8fl99nwVGfQp+vx+f4iO5VO1AtwJjfgXF58M/kqSVJP9ef0xuAI6NlwWmL41xxqeg+PyMXr72yBqW3cI4JaZHh1DcXrxeLy5liORiB7q1PiZFyeV0mQqz9TRZeUmFVUGLSjqdkgCIFp2RTCosEJOiiIihSyKWkDl9WYrFnCQCCNF0w0QmHhBQJTEzJ+nZSQmAoEYks2KIGBkJgASiM5I3LbGMnCSCCEQl38GJMvMZiag1e+nlFcmmIgKaZEwREaPGhWGZ1VfEMFZkNj4sgCSyhoihSzwSlqCGoAUlEo1IJByW+Oxyh+dZJJ+eklhiRnIrRcnrM6KCxLOmiNiipyICSGR2pTY2O1m7T2XEsNrrJmJLfjkn6amwoMbFaMEhG28eAVtzExErW3sOBCWVzkpmNiEqCOEZ2RyLTT3eJAKaMhVEUMOSXjHEtg3JTIUFkNTK9rGwbQrWm2xGb6QoWxIqEtdtEWO28aDtoi6JSFCAjUtL1AUzJA4SSW/IZ2VjjU0V0zEBJBiJSzwWk1g8IZEAAmrdidrBkoSKxB4IW08tGVNEzIxoIJM5a8v4SQ1RY5lGSy6x8xScz6QkHFBre1Zre49nH+y1KDEQLV7TcyU1LBCtHVppp9smxk2dYAMtHXA7blZWNJDZ4sZ4MxPbdHjrbc3WNuvOq4YlkYhLLBaXeKx2sLcrBUS2ScFtUbUBh3WgajvgOYgGuKjw4Rsqb1uvkssbWLbJXFQFqL/I9IEKa2WzYcqy16E2BNteB1R+cuwoRwcHGRx4nlfenWMuPclRDx3goSraqd+7Gj/Y5d76SrXLu3VKLYW1rMZbo/QpB4+9zt6fT1I0Law/LRMBaLzC7ePNuSgL7/2GpcotLr7+AZG5t9gH0Fa3zuFq1tiWG4DKs5tebV1NDDW1XYd26iWO9A8wODjAUfUN5ubm+Ch4ZFuuLRzQoVwqUCqXyN9fg3tFSuUShVIZhyr5O2vo94o42DwD/PP23fq8Bf5urLO+BoHBwxzc20c++wcmz+lAkWLFATwcf3+YDwIDhMYmuDw+wt5j5+C5ZwDYP/gSoLP6xX5+fOIkJ47/lIP8g49/Nc3tDj59OZUiRR3uFYsAVO/eZoE1yvkyeA6gAaff+zU3SxUcp8LilQucnoFTP3hhix19/garlQqFW9eZOBti9Mqt9mubXwBw+NALeDC4cfVDzgP3i3keUN/nf4uo+hEver/DRaK84/9mY/72uoFTKVMolVn5/HPgPvlSmVKhRL2bSrlEqVyidH8N/d7t2u/lakfcKneLgM4rvxhncbXA6tI8kTffB+0NjnrAqZYplcrk83ceXdtzgB+psHD7S/pfPs7JkydQB1x8dnWS2SVje9GaxkVLl+DmNNC4NJn/S6JxH5nJyNRwrW7Qi7oMgxBMyd9molvmRKO1cExgshG6l9NTEhkOynAkLlOJoKBuhPV8ZlK0h9aNTqVbv3ltEK/VIiAQEN0yZVLbuM+aImLoEgts3VdsJrfFil1M1/ZSv9RAROaWO8n/hkyF1Q3bgeFGygvPrDRG5Wcf1IJbq9rlNrrNbra96aqlUVMSWrNnNiw5uw23T/4o4Xq7FtA29h2My3K9WtETgRZr13UxdIk+pGswkpCcsX0N2OZD9BOgWqFsgWePp20KWb0ywkDgEIa8y55Gq0O5XKHP7cGz++l/haxWylgOuD17aG7eoVpxwL27RX8b27jZ42n1qdahXKrg2bfnUW0eQ7edoD232l+/LPp2pHvNfh8eT2f8/3sO2AZLyRAvns6gqToLOgxP6Uz87HvdoNJDF9E1B6ysLrLw5yW+3PUNvv3dH/L9wX3doNFDl9E1B+yhB+j9O1YPXcZ/AAl9BWJNvZE7AAAAAElFTkSuQmCC";
+
+var gPhases = null;
+
+var gIDCache = {};
+
+var gMagPixPaths = [];     // 2D array of array-of-two <path> objects used in the pixel magnifier
+var gMagWidth = 5;         // number of zoomed in pixels to show horizontally
+var gMagHeight = 5;        // number of zoomed in pixels to show vertically
+var gMagZoom = 16;         // size of the zoomed in pixels
+var gImage1Data;           // ImageData object for the reference image
+var gImage2Data;           // ImageData object for the test output image
+var gFlashingPixels = [];  // array of <path> objects that should be flashed due to pixel color mismatch
+var gParams;
+
+function ID(id) {
+  if (!(id in gIDCache))
+    gIDCache[id] = document.getElementById(id);
+  return gIDCache[id];
+}
+
+function hash_parameters() {
+  var result = { };
+  var params = window.location.hash.substr(1).split(/[&;]/);
+  for (var i = 0; i < params.length; i++) {
+    var parts = params[i].split("=");
+    result[parts[0]] = unescape(unescape(parts[1]));
+  }
+  return result;
+}
+
+function load() {
+  gPhases = [ ID("entry"), ID("loading"), ID("viewer") ];
+  build_mag();
+  gParams = hash_parameters();
+  if (gParams.log) {
+    show_phase("loading");
+    process_log(gParams.log);
+  } else if (gParams.logurl) {
+    show_phase("loading");
+    var req = new XMLHttpRequest();
+    req.onreadystatechange = function() {
+      if (req.readyState === 4) {
+        process_log(req.responseText);
+      }
+    };
+    req.open('GET', gParams.logurl, true);
+    req.send();
+  }
+  window.addEventListener('keypress', handle_keyboard_shortcut);
+  window.addEventListener('keydown',  handle_keydown);
+  ID("image1").addEventListener('error', image_load_error);
+  ID("image2").addEventListener('error', image_load_error);
+}
+
+function image_load_error(e) {
+  e.target.setAttributeNS(XLINK_NS, "xlink:href", IMAGE_NOT_AVAILABLE);
+}
+
+function build_mag() {
+  var mag = ID("mag");
+
+  var r = document.createElementNS(SVG_NS, "rect");
+  r.setAttribute("x", gMagZoom * -gMagWidth / 2);
+  r.setAttribute("y", gMagZoom * -gMagHeight / 2);
+  r.setAttribute("width", gMagZoom * gMagWidth);
+  r.setAttribute("height", gMagZoom * gMagHeight);
+  mag.appendChild(r);
+
+  mag.setAttribute("transform", "translate(" + (gMagZoom * (gMagWidth / 2) + 1) + "," + (gMagZoom * (gMagHeight / 2) + 1) + ")");
+
+  for (var x = 0; x < gMagWidth; x++) {
+    gMagPixPaths[x] = [];
+    for (var y = 0; y < gMagHeight; y++) {
+      var p1 = document.createElementNS(SVG_NS, "path");
+      p1.setAttribute("d", "M" + ((x - gMagWidth / 2) + 1) * gMagZoom + "," + (y - gMagHeight / 2) * gMagZoom + "h" + -gMagZoom + "v" + gMagZoom);
+      p1.setAttribute("stroke", "black");
+      p1.setAttribute("stroke-width", "1px");
+      p1.setAttribute("fill", "#aaa");
+
+      var p2 = document.createElementNS(SVG_NS, "path");
+      p2.setAttribute("d", "M" + ((x - gMagWidth / 2) + 1) * gMagZoom + "," + (y - gMagHeight / 2) * gMagZoom + "v" + gMagZoom + "h" + -gMagZoom);
+      p2.setAttribute("stroke", "black");
+      p2.setAttribute("stroke-width", "1px");
+      p2.setAttribute("fill", "#888");
+
+      mag.appendChild(p1);
+      mag.appendChild(p2);
+      gMagPixPaths[x][y] = [p1, p2];
+    }
+  }
+
+  var flashedOn = false;
+  setInterval(function() {
+    flashedOn = !flashedOn;
+    flash_pixels(flashedOn);
+  }, 500);
+}
+
+function show_phase(phaseid) {
+  for (var i in gPhases) {
+    var phase = gPhases[i];
+    phase.style.display = (phase.id == phaseid) ? "" : "none";
+  }
+
+  if (phase == "viewer")
+    ID("images").style.display = "none";
+}
+
+function fileentry_changed() {
+  show_phase("loading");
+  var input = ID("fileentry");
+  var files = input.files;
+  if (files.length > 0) {
+    // Only handle the first file; don't handle multiple selection.
+    // The parts of the log we care about are ASCII-only.  Since we
+    // can ignore lines we don't care about, best to read in as
+    // iso-8859-1, which guarantees we don't get decoding errors.
+    var fileReader = new FileReader();
+    fileReader.onload = function(e) {
+      var log = null;
+
+      log = e.target.result;
+
+      if (log)
+        process_log(log);
+      else
+        show_phase("entry");
+    }
+    fileReader.readAsText(files[0], "iso-8859-1");
+  }
+  // So the user can process the same filename again (after
+  // overwriting the log), clear the value on the form input so we
+  // will always get an onchange event.
+  input.value = "";
+}
+
+function log_pasted() {
+  show_phase("loading");
+  var entry = ID("logentry");
+  var log = entry.value;
+  entry.value = "";
+  process_log(log);
+}
+
+var gTestItems;
+
+// This function is not used in production code, but can be invoked manually
+// from the devtools console in order to test changes to the parsing regexes
+// in process_log.
+function test_parsing() {
+  // Note that the logs in these testcases have been manually edited to strip
+  // out stuff for brevity.
+  var testcases = [
+    { "name": "empty log",
+      "log": "",
+      "expected": { "pass": 0, "unexpected": 0, "random": 0, "skip": 0 },
+      "expected_images": 0,
+    },
+    { "name": "android log",
+      "log": `[task 2018-12-28T10:36:45.718Z] 10:36:45     INFO -  REFTEST TEST-START | a == b
+[task 2018-12-28T10:36:45.719Z] 10:36:45     INFO -  REFTEST TEST-LOAD | a | 78 / 275 (28%)
+[task 2018-12-28T10:36:56.138Z] 10:36:56     INFO -  REFTEST TEST-LOAD | b | 78 / 275 (28%)
+[task 2018-12-28T10:37:06.559Z] 10:37:06     INFO -  REFTEST TEST-UNEXPECTED-FAIL | a == b | image comparison, max difference: 255, number of differing pixels: 5950
+[task 2018-12-28T10:37:06.568Z] 10:37:06     INFO -  REFTEST   IMAGE 1 (TEST): data:image/png;base64,
+[task 2018-12-28T10:37:06.577Z] 10:37:06     INFO -  REFTEST   IMAGE 2 (REFERENCE): data:image/png;base64,
+[task 2018-12-28T10:37:06.577Z] 10:37:06     INFO -  REFTEST INFO | Saved log: stuff trimmed here
+[task 2018-12-28T10:37:06.582Z] 10:37:06     INFO -  REFTEST TEST-END | a == b
+[task 2018-12-28T10:37:06.583Z] 10:37:06     INFO -  REFTEST TEST-START | a2 == b2
+[task 2018-12-28T10:37:06.583Z] 10:37:06     INFO -  REFTEST TEST-LOAD | a2 | 79 / 275 (28%)
+[task 2018-12-28T10:37:06.584Z] 10:37:06     INFO -  REFTEST TEST-LOAD | b2 | 79 / 275 (28%)
+[task 2018-12-28T10:37:16.982Z] 10:37:16     INFO -  REFTEST TEST-PASS | a2 == b2 | image comparison, max difference: 0, number of differing pixels: 0
+[task 2018-12-28T10:37:16.982Z] 10:37:16     INFO -  REFTEST TEST-END | a2 == b2`,
+      "expected": { "pass": 1, "unexpected": 1, "random": 0, "skip": 0 },
+      "expected_images": 2,
+    },
+    { "name": "local reftest run (Linux)",
+      "log": `REFTEST TEST-START | file:///a == file:///b
+REFTEST TEST-LOAD | file:///a | 73 / 86 (84%)
+REFTEST TEST-LOAD | file:///b | 73 / 86 (84%)
+REFTEST TEST-PASS | file:///a == file:///b | image comparison, max difference: 0, number of differing pixels: 0
+REFTEST TEST-END | file:///a == file:///b`,
+      "expected": { "pass": 1, "unexpected": 0, "random": 0, "skip": 0 },
+      "expected_images": 0,
+    },
+    { "name": "wpt reftests (Linux automation)",
+      "log": `16:50:43     INFO - TEST-START | /a
+16:50:43     INFO - PID 4276 | 1548694243694	Marionette	INFO	Testing http://web-platform.test:8000/a == http://web-platform.test:8000/b
+16:50:43     INFO - PID 4276 | 1548694243963	Marionette	INFO	No differences allowed
+16:50:44     INFO - TEST-PASS | /a | took 370ms
+16:50:44     INFO - TEST-START | /a2
+16:50:44     INFO - PID 4276 | 1548694244066	Marionette	INFO	Testing http://web-platform.test:8000/a2 == http://web-platform.test:8000/b2
+16:50:44     INFO - PID 4276 | 1548694244792	Marionette	INFO	No differences allowed
+16:50:44     INFO - PID 4276 | 1548694244792	Marionette	INFO	Found 28 pixels different, maximum difference per channel 14
+16:50:44     INFO - TEST-UNEXPECTED-FAIL | /a2 | Testing http://web-platform.test:8000/a2 == http://web-platform.test:8000/b2
+16:50:44     INFO - REFTEST   IMAGE 1 (TEST): data:image/png;base64,
+16:50:44     INFO - REFTEST   IMAGE 2 (REFERENCE): data:image/png;base64,
+16:50:44     INFO - TEST-INFO took 840ms`,
+      "expected": { "pass": 1, "unexpected": 1, "random": 0, "skip": 0 },
+      "expected_images": 2,
+    },
+    { "name": "windows log",
+      "log": `12:17:14     INFO - REFTEST TEST-START | a == b
+12:17:14     INFO - REFTEST TEST-LOAD | a | 1603 / 2053 (78%)
+12:17:14     INFO - REFTEST TEST-LOAD | b | 1603 / 2053 (78%)
+12:17:14     INFO - REFTEST TEST-PASS(EXPECTED RANDOM) | a == b | image comparison, max difference: 0, number of differing pixels: 0
+12:17:14     INFO - REFTEST TEST-END | a == b
+12:17:14     INFO - REFTEST TEST-START | a2 == b2
+12:17:14     INFO - REFTEST TEST-LOAD | a2 | 1604 / 2053 (78%)
+12:17:14     INFO - REFTEST TEST-LOAD | b2 | 1604 / 2053 (78%)
+12:17:14     INFO - REFTEST TEST-UNEXPECTED-FAIL | a2 == b2 | image comparison, max difference: 255, number of differing pixels: 9976
+12:17:14     INFO - REFTEST   IMAGE 1 (TEST): data:image/png;base64,
+12:17:14     INFO - REFTEST   IMAGE 2 (REFERENCE): data:image/png;base64,
+12:17:14     INFO - REFTEST INFO | Saved log: stuff trimmed here
+12:17:14     INFO - REFTEST TEST-END | a2 == b2
+12:01:09     INFO - REFTEST TEST-START | a3 == b3
+12:01:09     INFO - REFTEST TEST-LOAD | a3 | 66 / 189 (34%)
+12:01:09     INFO - REFTEST TEST-LOAD | b3 | 66 / 189 (34%)
+12:01:09     INFO - REFTEST TEST-KNOWN-FAIL | a3 == b3 | image comparison, max difference: 255, number of differing pixels: 9654
+12:01:09     INFO - REFTEST TEST-END | a3 == b3`,
+      "expected": { "pass": 1, "unexpected": 1, "random": 1, "skip": 0 },
+      "expected_images": 2,
+    },
+    { "name": "webrender wrench log (windows)",
+      "log": `[task 2018-12-29T04:29:48.800Z] REFTEST a == b
+[task 2018-12-29T04:29:48.984Z] REFTEST a2 == b2
+[task 2018-12-29T04:29:49.053Z] REFTEST TEST-UNEXPECTED-FAIL | a2 == b2 | image comparison, max difference: 255, number of differing pixels: 3128
+[task 2018-12-29T04:29:49.053Z] REFTEST   IMAGE 1 (TEST): data:image/png;
+[task 2018-12-29T04:29:49.053Z] REFTEST   IMAGE 2 (REFERENCE): data:image/png;
+[task 2018-12-29T04:29:49.053Z] REFTEST TEST-END | a2 == b2`,
+      "expected": { "pass": 0, "unexpected": 1, "random": 0, "skip": 0 },
+      "expected_images": 2,
+    },
+    { "name": "wpt reftests (Linux local; Bug 1530008)",
+      "log": `SUITE-START | Running 1 tests
+TEST-START | /css/css-backgrounds/border-image-6.html
+TEST-UNEXPECTED-FAIL | /css/css-backgrounds/border-image-6.html | Testing http://web-platform.test:8000/css/css-backgrounds/border-image-6.html == http://web-platform.test:8000/css/css-backgrounds/border-image-6-ref.html
+REFTEST   IMAGE 1 (TEST): data:image/png;base64,
+REFTEST   IMAGE 2 (REFERENCE): data:image/png;base64,
+TEST-INFO took 425ms
+SUITE-END | took 2s`,
+      "expected": { "pass": 0, "unexpected": 1, "random": 0, "skip": 0 },
+      "expected_images": 2,
+    },
+    { "name": "wpt reftests (taskcluster log from macOS CI)",
+      "log": `[task 2020-06-26T01:35:29.065Z] 01:35:29     INFO - TEST-START | /html/rendering/non-replaced-elements/the-page/iframe-scrolling-attribute-values.html
+[task 2020-06-26T01:35:29.065Z] 01:35:29     INFO - PID 1353 | 1593135329040    Marionette  INFO    Testing http://web-platform.test:8000/html/rendering/non-replaced-elements/the-page/iframe-scrolling-attribute-values.html == http://web-platform.test:8000/html/rendering/non-replaced-elements/the-page/iframe-scrolling-attribute-values-ref.html
+[task 2020-06-26T01:35:29.673Z] 01:35:29     INFO - PID 1353 | 1593135329633    Marionette  INFO    No differences allowed
+[task 2020-06-26T01:35:29.726Z] 01:35:29     INFO - TEST-KNOWN-INTERMITTENT-FAIL | /html/rendering/non-replaced-elements/the-page/iframe-scrolling-attribute-values.html | took 649ms
+[task 2020-06-26T01:35:29.726Z] 01:35:29     INFO - REFTEST   IMAGE 1 (TEST): data:image/png;
+[task 2020-06-26T01:35:29.726Z] 01:35:29     INFO - REFTEST   IMAGE 2 (REFERENCE): data:image/png;`,
+      "expected": { "pass": 0, "unexpected": 0, "random": 1, "skip": 0 },
+      "expected_images": 2,
+    },
+    { "name": "wpt reftests (taskcluster log from Windows CI)",
+      "log": `[task 2020-06-26T01:41:19.205Z] 01:41:19     INFO - TEST-START | /html/rendering/non-replaced-elements/the-page/iframe-scrolling-attribute-values.html
+[task 2020-06-26T01:41:19.214Z] 01:41:19     INFO - PID 5920 | 1593135679202    Marionette  WARN    [24] http://web-platform.test:8000/css/WOFF2/metadatadisplay-schema-license-022-ref.xht overflows viewport (width: 783, height: 731)
+[task 2020-06-26T01:41:19.214Z] 01:41:19     INFO - PID 9692 | 1593135679208    Marionette  INFO    Testing http://web-platform.test:8000/html/rendering/non-replaced-elements/the-page/iframe-scrolling-attribute-values.html == http://web-platform.test:8000/html/rendering/non-replaced-elements/the-page/iframe-scrolling-attribute-values-ref.html
+[task 2020-06-26T01:41:19.638Z] 01:41:19     INFO - PID 9692 | 1593135679627    Marionette  INFO    No differences allowed
+[task 2020-06-26T01:41:19.688Z] 01:41:19     INFO - TEST-KNOWN-INTERMITTENT-PASS | /html/rendering/non-replaced-elements/the-page/iframe-scrolling-attribute-values.html | took 474ms
+[task 2020-06-26T01:41:19.688Z] 01:41:19     INFO - REFTEST   IMAGE 1 (TEST): data:image/png;
+[task 2020-06-26T01:41:19.689Z] 01:41:19     INFO - REFTEST   IMAGE 2 (REFERENCE): data:image/png;`,
+      "expected": { "pass": 1, "unexpected": 0, "random": 1, "skip": 0 },
+      "expected_images": 2,
+    },
+    { "name": "local reftest run with timestamps (Linux; Bug 1167712)",
+      "log": ` 0:05.21 REFTEST TEST-START | a
+ 0:05.21 REFTEST REFTEST TEST-LOAD | a | 0 / 1 (0%)
+ 0:05.27 REFTEST REFTEST TEST-LOAD | b | 0 / 1 (0%)
+ 0:05.66 REFTEST TEST-UNEXPECTED-FAIL | a | image comparison (==), max difference: 106, number of differing pixels: 800
+ 0:05.67 REFTEST REFTEST   IMAGE 1 (TEST): data:image/png;base64,
+ 0:05.67 REFTEST REFTEST   IMAGE 2 (REFERENCE): data:image/png;base64,
+ 0:05.73 REFTEST REFTEST TEST-END | a`,
+      "expected": { "pass": 0, "unexpected": 1, "random": 0, "skip": 0 },
+      "expected_images": 2,
+    },
+    { "name": "reftest run with whitespace compressed (Treeherder; Bug 1084322)",
+      "log": ` REFTEST TEST-START | a
+REFTEST TEST-LOAD | a | 0 / 1 (0%)
+REFTEST TEST-LOAD | b | 0 / 1 (0%)
+REFTEST TEST-UNEXPECTED-FAIL | a | image comparison (==), max difference: 106, number of differing pixels: 800
+REFTEST REFTEST IMAGE 1 (TEST): data:image/png;base64,
+REFTEST REFTEST IMAGE 2 (REFERENCE): data:image/png;base64,
+REFTEST REFTEST TEST-END | a`,
+      "expected": { "pass": 0, "unexpected": 1, "random": 0, "skip": 0 },
+      "expected_images": 2,
+    },
+  ];
+
+  var current_test = 0;
+
+  // Override the build_viewer function invoked at the end of process_log to
+  // actually just check the results of parsing.
+  build_viewer = function() {
+    var expected = testcases[current_test].expected;
+    var expected_images = testcases[current_test].expected_images;
+    for (var result of gTestItems) {
+      for (let type in expected) { // type is "pass", "unexpected" etc.
+        if (result[type]) {
+          expected[type]--;
+        }
+      }
+    }
+    var failed = false;
+    for (let type in expected) {
+      if (expected[type] != 0) {
+        console.log(`Failure: for testcase ${testcases[current_test].name} got ${expected[type]} fewer ${type} results than expected!`);
+        failed = true;
+      }
+    }
+
+    let total_images = 0;
+    for (var result of gTestItems) {
+      total_images += result.images.length;
+    }
+    if (total_images !== expected_images) {
+      console.log(`Failure: for testcase ${testcases[current_test].name} got ${total_images} images, expected ${expected_images}`);
+      failed = true;
+    }
+
+    if (!failed) {
+      console.log(`Success for testcase ${testcases[current_test].name}`);
+    }
+  };
+
+  while (current_test < testcases.length) {
+    process_log(testcases[current_test].log);
+    current_test++;
+  }
+}
+
+function process_log(contents) {
+  var lines = contents.split(/[\r\n]+/);
+  gTestItems = [];
+  for (var j in lines) {
+
+    // !!!!!!
+    // When making any changes to this code, please add a test to the
+    // test_parsing function above, and ensure all existing tests pass.
+    // !!!!!!
+
+    var line = lines[j];
+    // Ignore duplicated output in logcat.
+    if (line.match(/I\/Gecko.*?REFTEST/))
+      continue;
+    var match = line.match(/^.*?(?:REFTEST\s+)+(.*)$/);
+    if (!match) {
+      // WPT reftests don't always have the "REFTEST" prefix but do have
+      // mozharness prefixing. Trying to match both prefixes optionally with a
+      // single regex either makes an unreadable mess or matches everything so
+      // we do them separately.
+      match = line.match(/^(?:.*? (?:INFO|ERROR) -\s+)(.*)$/);
+    }
+    if (match)
+      line = match[1];
+    match = line.match(/^(TEST-PASS|TEST-UNEXPECTED-PASS|TEST-FAIL|TEST-KNOWN-FAIL|TEST-UNEXPECTED-FAIL|TEST-DEBUG-INFO|TEST-KNOWN-INTERMITTENT-FAIL|TEST-KNOWN-INTERMITTENT-PASS)(\(EXPECTED RANDOM\)|) \| ([^\|]+)(?: \|(.*)|$)/);
+    if (match) {
+      var state = match[1];
+      var random = match[2];
+      var url = match[3];
+      var extra = match[4];
+      gTestItems.push(
+        {
+          pass: !state.match(/DEBUG-INFO$|FAIL$/),
+          // only one of the following three should ever be true
+          unexpected: !!state.match(/^TEST-UNEXPECTED/),
+          random: (random == "(EXPECTED RANDOM)" || state == "TEST-KNOWN-INTERMITTENT-FAIL" || state == "TEST-KNOWN-INTERMITTENT-PASS"),
+          skip: (extra == " (SKIP)"),
+          url: url,
+          images: [],
+          imageLabels: []
+        });
+      continue;
+    }
+    match = line.match(/^IMAGE([^:]*): (data:.*)$/);
+    if (match) {
+      var item = gTestItems[gTestItems.length - 1];
+      item.images.push(match[2]);
+      item.imageLabels.push(match[1]);
+    }
+  }
+
+  build_viewer();
+}
+
+function build_viewer() {
+  if (gTestItems.length == 0) {
+    show_phase("entry");
+    return;
+  }
+
+  var cell = ID("itemlist");
+  while (cell.childNodes.length > 0)
+    cell.removeChild(cell.childNodes[cell.childNodes.length - 1]);
+
+  var table = document.createElement("table");
+  var tbody = document.createElement("tbody");
+  table.appendChild(tbody);
+
+  for (var i in gTestItems) {
+    var item = gTestItems[i];
+
+    // optional url filter for only showing unexpected results
+    if (parseInt(gParams.only_show_unexpected) && !item.unexpected)
+      continue;
+
+    // XXX regardless skip expected pass items until we have filtering UI
+    if (item.pass && !item.unexpected)
+      continue;
+
+    var tr = document.createElement("tr");
+    var rowclass = item.pass ? "pass" : "fail";
+    var td;
+    var text;
+
+    td = document.createElement("td");
+    text = "";
+    if (item.unexpected) { text += "!"; rowclass += " unexpected"; }
+    if (item.random) { text += "R"; rowclass += " random"; }
+    if (item.skip) { text += "S"; rowclass += " skip"; }
+    td.appendChild(document.createTextNode(text));
+    tr.appendChild(td);
+
+    td = document.createElement("td");
+    td.id = "item" + i;
+    td.className = "url";
+    // Only display part of URL after "/mozilla/".
+    var match = item.url.match(/\/mozilla\/(.*)/);
+    text = document.createTextNode(match ? match[1] : item.url);
+    if (item.images.length > 0) {
+      var a = document.createElement("a");
+      a.href = "javascript:show_images(" + i + ")";
+      a.appendChild(text);
+      td.appendChild(a);
+    } else {
+      td.appendChild(text);
+    }
+    tr.appendChild(td);
+
+    tbody.appendChild(tr);
+  }
+
+  cell.appendChild(table);
+
+  show_phase("viewer");
+}
+
+function get_image_data(src, whenReady) {
+  var img = new Image();
+  img.onload = function() {
+    var canvas = document.createElement("canvas");
+    canvas.width = img.naturalWidth;
+    canvas.height = img.naturalHeight;
+
+    var ctx = canvas.getContext("2d");
+    ctx.drawImage(img, 0, 0);
+
+    whenReady(ctx.getImageData(0, 0, img.naturalWidth, img.naturalHeight));
+  };
+  img.src = src;
+}
+
+function sync_svg_size(imageData) {
+  // We need the size of the 'svg' and its 'image' elements to match the size
+  // of the ImageData objects that we're going to read pixels from or else our
+  // magnify() function will be very broken.
+  ID("svg").setAttribute("width", imageData.width);
+  ID("svg").setAttribute("height", imageData.height);
+}
+
+function show_images(i) {
+  var item = gTestItems[i];
+  var cell = ID("images");
+
+  // Remove activeitem class from any existing elements
+  var activeItems = document.querySelectorAll(".activeitem");
+  for (var activeItemIdx = activeItems.length; activeItemIdx-- != 0;) {
+    activeItems[activeItemIdx].classList.remove("activeitem");
+  }
+
+  ID("item" + i).classList.add("activeitem");
+  ID("image1").style.display = "";
+  ID("image2").style.display = "none";
+  ID("diffrect").style.display = "none";
+  ID("imgcontrols").reset();
+  ID("pixel-differences").textContent = "";
+
+  ID("image1").setAttributeNS(XLINK_NS, "xlink:href", item.images[0]);
+  // Making the href be #image1 doesn't seem to work
+  ID("feimage1").setAttributeNS(XLINK_NS, "xlink:href", item.images[0]);
+  if (item.images.length == 1) {
+    ID("imgcontrols").style.display = "none";
+  } else {
+    ID("imgcontrols").style.display = "";
+
+    ID("image2").setAttributeNS(XLINK_NS, "xlink:href", item.images[1]);
+    // Making the href be #image2 doesn't seem to work
+    ID("feimage2").setAttributeNS(XLINK_NS, "xlink:href", item.images[1]);
+
+    ID("label1").textContent = 'Image ' + item.imageLabels[0];
+    ID("label2").textContent = 'Image ' + item.imageLabels[1];
+  }
+
+  cell.style.display = "";
+
+  let loaded = [false, false];
+
+  function images_loaded(id) {
+    loaded[id] = true;
+    if (loaded.every(x => x)) {
+      update_pixel_difference_text()
+    }
+  }
+
+  get_image_data(item.images[0], function(data) { gImage1Data = data; sync_svg_size(gImage1Data); images_loaded(0)});
+  get_image_data(item.images[1], function(data) { gImage2Data = data; images_loaded(1)});
+
+}
+
+function update_pixel_difference_text() {
+  let differenceText;
+  if (gImage1Data.height !== gImage2Data.height ||
+      gImage1Data.width !== gImage2Data.width) {
+    differenceText = "Images are different sizes"
+  } else {
+    let [numPixels, maxPerChannel] = get_pixel_differences();
+    if (!numPixels) {
+      differenceText = "Images are identical";
+    } else {
+      differenceText = `Maximum difference per channel ${maxPerChannel}, ${numPixels} pixels differ`;
+    }
+  }
+  // Disable this for now, because per bug 1633504, the numbers may be
+  // inaccurate and dependent on the browser's configuration.
+  // ID("pixel-differences").textContent = differenceText;
+}
+
+function get_pixel_differences() {
+  let numPixels = 0;
+  let maxPerChannel = 0;
+  for (var i=0; i<gImage1Data.data.length; i+=4) {
+    let r1 = gImage1Data.data[i];
+    let r2 = gImage2Data.data[i];
+    let g1 = gImage1Data.data[i+1];
+    let g2 = gImage2Data.data[i+1];
+    let b1 = gImage1Data.data[i+2];
+    let b2 = gImage2Data.data[i+2];
+    // Ignore alpha.
+    if (r1 == r2 && g1 == g2 && b1 == b2) {
+      continue;
+    }
+    numPixels += 1;
+    let maxDiff = Math.max(Math.abs(r1-r2),
+                           Math.abs(g1-g2),
+                           Math.abs(b1-b2));
+    if (maxDiff > maxPerChannel) {
+      maxPerChannel = maxDiff
+    }
+  }
+  return [numPixels, maxPerChannel];
+}
+
+function show_image(i) {
+  if (i == 1) {
+    ID("image1").style.display = "";
+    ID("image2").style.display = "none";
+  } else {
+    ID("image1").style.display = "none";
+    ID("image2").style.display = "";
+  }
+}
+
+function handle_keyboard_shortcut(event) {
+  switch (event.charCode) {
+  case 49: // "1" key
+    document.getElementById("radio1").checked = true;
+    show_image(1);
+    break;
+  case 50: // "2" key
+    document.getElementById("radio2").checked = true;
+    show_image(2);
+    break;
+  case 100: // "d" key
+    document.getElementById("differences").click();
+    break;
+  case 112: // "p" key
+    shift_images(-1);
+    break;
+  case 110: // "n" key
+    shift_images(1);
+    break;
+  }
+}
+
+function handle_keydown(event) {
+  switch (event.keyCode) {
+  case 37:  // left arrow
+    move_pixel(-1, 0);
+    break;
+  case 38:  // up arrow
+    move_pixel(0,-1);
+    break;
+  case 39:  // right arrow
+    move_pixel(1, 0);
+    break;
+  case 40:  // down arrow
+    move_pixel(0, 1);
+    break;
+  }
+}
+
+function shift_images(dir) {
+  var activeItem = document.querySelector(".activeitem");
+  if (!activeItem) {
+    return;
+  }
+  for (var elm = activeItem; elm; elm = elm.parentElement) {
+    if (elm.tagName != "tr") {
+      continue;
+    }
+    elm = dir > 0 ? elm.nextElementSibling : elm.previousElementSibling;
+    if (elm) {
+      elm.getElementsByTagName("a")[0].click();
+    }
+    return;
+  }
+}
+
+function show_differences(cb) {
+  ID("diffrect").style.display = cb.checked ? "" : "none";
+}
+
+function flash_pixels(on) {
+  var stroke = on ? "red" : "black";
+  var strokeWidth = on ? "2px" : "1px";
+  for (var i = 0; i < gFlashingPixels.length; i++) {
+    gFlashingPixels[i].setAttribute("stroke", stroke);
+    gFlashingPixels[i].setAttribute("stroke-width", strokeWidth);
+  }
+}
+
+function cursor_point(evt) {
+  var m = evt.target.getScreenCTM().inverse();
+  var p = ID("svg").createSVGPoint();
+  p.x = evt.clientX;
+  p.y = evt.clientY;
+  p = p.matrixTransform(m);
+  return { x: Math.floor(p.x), y: Math.floor(p.y) };
+}
+
+function hex2(i) {
+  return (i < 16 ? "0" : "") + i.toString(16);
+}
+
+function canvas_pixel_as_hex(data, x, y) {
+  var offset = (y * data.width + x) * 4;
+  var r = data.data[offset];
+  var g = data.data[offset + 1];
+  var b = data.data[offset + 2];
+  return "#" + hex2(r) + hex2(g) + hex2(b);
+}
+
+function hex_as_rgb(hex) {
+  return "rgb(" + [parseInt(hex.substring(1, 3), 16), parseInt(hex.substring(3, 5), 16), parseInt(hex.substring(5, 7), 16)] + ")";
+}
+
+function magnify(evt) {
+  var { x: x, y: y } = cursor_point(evt);
+  do_magnify(x, y);
+}
+
+function do_magnify(x, y) {
+  var centerPixelColor1, centerPixelColor2;
+
+  var dx_lo = -Math.floor(gMagWidth / 2);
+  var dx_hi = Math.floor(gMagWidth / 2);
+  var dy_lo = -Math.floor(gMagHeight / 2);
+  var dy_hi = Math.floor(gMagHeight / 2);
+
+  flash_pixels(false);
+  gFlashingPixels = [];
+  for (var j = dy_lo; j <= dy_hi; j++) {
+    for (var i = dx_lo; i <= dx_hi; i++) {
+      var px = x + i;
+      var py = y + j;
+      var p1 = gMagPixPaths[i + dx_hi][j + dy_hi][0];
+      var p2 = gMagPixPaths[i + dx_hi][j + dy_hi][1];
+      // Here we just use the dimensions of gImage1Data since we expect test
+      // and reference to have the same dimensions.
+      if (px < 0 || py < 0 || px >= gImage1Data.width || py >= gImage1Data.height) {
+        p1.setAttribute("fill", "#aaa");
+        p2.setAttribute("fill", "#888");
+      } else {
+        var color1 = canvas_pixel_as_hex(gImage1Data, x + i, y + j);
+        var color2 = canvas_pixel_as_hex(gImage2Data, x + i, y + j);
+        p1.setAttribute("fill", color1);
+        p2.setAttribute("fill", color2);
+        if (color1 != color2) {
+          gFlashingPixels.push(p1, p2);
+          p1.parentNode.appendChild(p1);
+          p2.parentNode.appendChild(p2);
+        }
+        if (i == 0 && j == 0) {
+          centerPixelColor1 = color1;
+          centerPixelColor2 = color2;
+        }
+      }
+    }
+  }
+  flash_pixels(true);
+  show_pixelinfo(x, y, centerPixelColor1, hex_as_rgb(centerPixelColor1), centerPixelColor2, hex_as_rgb(centerPixelColor2));
+}
+
+function show_pixelinfo(x, y, pix1rgb, pix1hex, pix2rgb, pix2hex) {
+  var pixelinfo = ID("pixelinfo");
+  ID("coords").textContent = [x, y];
+  ID("pix1hex").textContent = pix1hex;
+  ID("pix1rgb").textContent = pix1rgb;
+  ID("pix2hex").textContent = pix2hex;
+  ID("pix2rgb").textContent = pix2rgb;
+}
+
+function move_pixel(deltax, deltay) {
+  coords = ID("coords").textContent.split(',');
+  x = parseInt(coords[0]);
+  y = parseInt(coords[1]);
+  if (isNaN(x) || isNaN(y)) {
+    return;
+  }
+  x = x + deltax;
+  y = y + deltay;
+  if (x >= 0 && y >= 0 && x < gImage1Data.width && y < gImage1Data.height) {
+    do_magnify(x, y);
+  }
+}
+
+  ]]></script>
+
+</head>
+<body onload="load()">
+
+<div id="entry">
+
+<h1>Reftest analyzer: load reftest log</h1>
+
+<p>Either paste your log into this textarea:<br />
+<textarea cols="80" rows="10" id="logentry"/><br/>
+<input type="button" value="Process pasted log" onclick="log_pasted()" /></p>
+
+<p>... or load it from a file:<br/>
+<input type="file" id="fileentry" onchange="fileentry_changed()" />
+</p>
+</div>
+
+<div id="loading" style="display:none">Loading log...</div>
+
+<div id="viewer" style="display:none">
+  <div id="pixelarea">
+    <div id="pixelinfo">
+      <table>
+        <tbody>
+          <tr><th>Pixel at:</th><td colspan="2" id="coords"/></tr>
+          <tr><th>Image 1:</th><td id="pix1rgb"></td><td id="pix1hex"></td></tr>
+          <tr><th>Image 2:</th><td id="pix2rgb"></td><td id="pix2hex"></td></tr>
+        </tbody>
+      </table>
+      <div>
+        <div id="pixelhint">★
+          <div>
+            <p>Move the mouse over the reftest image on the right to show
+            magnified pixels on the left.  The color information above is for
+            the pixel centered in the magnified view.</p>
+            <p>Image 1 is shown in the upper triangle of each pixel and Image 2
+            is shown in the lower triangle.</p>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div id="magnification">
+      <svg xmlns="http://www.w3.org/2000/svg" width="84" height="84" shape-rendering="optimizeSpeed">
+        <g id="mag"/>
+      </svg>
+    </div>
+  </div>
+  <div id="itemlist"></div>
+  <div id="images" style="display:none">
+    <form id="imgcontrols">
+    <input id="radio1" type="radio" name="which" value="0" onchange="show_image(1)" checked="checked" /><label id="label1" title="1" for="radio1">Image 1</label>
+    <input id="radio2" type="radio" name="which" value="1" onchange="show_image(2)"                   /><label id="label2" title="2" for="radio2">Image 2</label>
+    <label><input id="differences" type="checkbox" onchange="show_differences(this)" />Circle differences</label>
+    </form>
+    <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="800" height="1000" id="svg">
+      <defs>
+        <!-- use sRGB to avoid loss of data -->
+        <filter id="showDifferences" x="0%" y="0%" width="100%" height="100%"
+                style="color-interpolation-filters: sRGB">
+          <feImage id="feimage1" result="img1" xlink:href="#image1" />
+          <feImage id="feimage2" result="img2" xlink:href="#image2" />
+          <!-- inv1 and inv2 are the images with RGB inverted -->
+          <feComponentTransfer result="inv1" in="img1">
+            <feFuncR type="linear" slope="-1" intercept="1" />
+            <feFuncG type="linear" slope="-1" intercept="1" />
+            <feFuncB type="linear" slope="-1" intercept="1" />
+          </feComponentTransfer>
+          <feComponentTransfer result="inv2" in="img2">
+            <feFuncR type="linear" slope="-1" intercept="1" />
+            <feFuncG type="linear" slope="-1" intercept="1" />
+            <feFuncB type="linear" slope="-1" intercept="1" />
+          </feComponentTransfer>
+          <!-- w1 will have non-white pixels anywhere that img2
+               is brighter than img1, and w2 for the reverse.
+               It would be nice not to have to go through these
+               intermediate states, but feComposite
+               type="arithmetic" can't transform the RGB channels
+               and leave the alpha channel untouched. -->
+          <feComposite result="w1" in="img1" in2="inv2" operator="arithmetic" k2="1" k3="1" />
+          <feComposite result="w2" in="img2" in2="inv1" operator="arithmetic" k2="1" k3="1" />
+          <!-- c1 will have non-black pixels anywhere that img2
+               is brighter than img1, and c2 for the reverse -->
+          <feComponentTransfer result="c1" in="w1">
+            <feFuncR type="linear" slope="-1" intercept="1" />
+            <feFuncG type="linear" slope="-1" intercept="1" />
+            <feFuncB type="linear" slope="-1" intercept="1" />
+          </feComponentTransfer>
+          <feComponentTransfer result="c2" in="w2">
+            <feFuncR type="linear" slope="-1" intercept="1" />
+            <feFuncG type="linear" slope="-1" intercept="1" />
+            <feFuncB type="linear" slope="-1" intercept="1" />
+          </feComponentTransfer>
+          <!-- c will be nonblack (and fully on) for every pixel+component where there are differences -->
+          <feComposite result="c" in="c1" in2="c2" operator="arithmetic" k2="255" k3="255" />
+          <!-- a will be opaque for every pixel with differences and transparent for all others -->
+          <feColorMatrix result="a" type="matrix" values="0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  1 1 1 0 0" />
+
+          <!-- a, dilated by 1 pixel -->
+          <feMorphology result="dila1" in="a" operator="dilate" radius="1" />
+          <!-- a, dilated by 2 pixels -->
+          <feMorphology result="dila2" in="dila1" operator="dilate" radius="1" />
+
+          <!-- all the pixels in the 2-pixel dilation of a but not in the 1-pixel dilation, to highlight the diffs -->
+          <feComposite result="highlight" in="dila2" in2="dila1" operator="out" />
+
+          <feFlood result="red" flood-color="red" />
+          <feComposite result="redhighlight" in="red" in2="highlight" operator="in" />
+          <feFlood result="black" flood-color="black" flood-opacity="0.5" />
+          <feMerge>
+            <feMergeNode in="black" />
+            <feMergeNode in="redhighlight" />
+          </feMerge>
+        </filter>
+      </defs>
+      <g onmousemove="magnify(evt)">
+        <image x="0" y="0" width="100%" height="100%" id="image1" />
+        <image x="0" y="0" width="100%" height="100%" id="image2" />
+      </g>
+      <rect id="diffrect" filter="url(#showDifferences)" pointer-events="none" x="0" y="0" width="100%" height="100%" />
+    </svg>
+    <div id="pixel-differences"></div>
+  </div>
+</div>
+
+</body>
+</html>
diff --git a/test/fixtures/wpt/encoding/legacy-mb-schinese/gb18030/gb18030-decoder.any.js b/test/fixtures/wpt/encoding/legacy-mb-schinese/gb18030/gb18030-decoder.any.js
new file mode 100644
index 00000000000000..99a0253ba6b680
--- /dev/null
+++ b/test/fixtures/wpt/encoding/legacy-mb-schinese/gb18030/gb18030-decoder.any.js
@@ -0,0 +1,63 @@
+// META: script=./resources/ranges.js
+
+const decode = (input, output, desc) => {
+  test(function () {
+    for (const encoding of ["gb18030", "gbk"]) {
+      assert_equals(
+        new TextDecoder(encoding).decode(new Uint8Array(input)),
+        output,
+      );
+    }
+  }, "gb18030 decoder: " + desc);
+};
+
+decode([115], "s", "ASCII");
+decode([0x80], "\u20AC", "euro");
+decode([0xFF], "\uFFFD", "initial byte out of accepted ranges");
+decode([0x81], "\uFFFD", "end of queue, gb18030 first not 0");
+decode([0x81, 0x28], "\ufffd(", "two bytes 0x81 0x28");
+decode([0x81, 0x40], "\u4E02", "two bytes 0x81 0x40");
+decode([0x81, 0x7E], "\u4E8A", "two bytes 0x81 0x7e");
+decode([0x81, 0x7F], "\ufffd\u007f", "two bytes 0x81 0x7f");
+decode([0x81, 0x80], "\u4E90", "two bytes 0x81 0x80");
+decode([0x81, 0xFE], "\u4FA2", "two bytes 0x81 0xFE");
+decode([0x81, 0xFF], "\ufffd", "two bytes 0x81 0xFF");
+decode([0xFE, 0x40], "\uFA0C", "two bytes 0xFE 0x40");
+decode([0xFE, 0xFE], "\uE4C5", "two bytes 0xFE 0xFE");
+decode([0xFE, 0xFF], "\ufffd", "two bytes 0xFE 0xFF");
+decode([0x81, 0x30], "\ufffd", "two bytes 0x81 0x30");
+decode([0x81, 0x30, 0xFE], "\ufffd", "three bytes 0x81 0x30 0xFE");
+decode([0x81, 0x30, 0xFF], "\ufffd0\ufffd", "three bytes 0x81 0x30 0xFF");
+decode(
+  [0x81, 0x30, 0xFE, 0x29],
+  "\ufffd0\ufffd)",
+  "four bytes 0x81 0x30 0xFE 0x29",
+);
+decode([0xFE, 0x39, 0xFE, 0x39], "\ufffd", "four bytes 0xFE 0x39 0xFE 0x39");
+decode([0x81, 0x35, 0xF4, 0x36], "\u1E3E", "pointer 7458");
+decode([0x81, 0x35, 0xF4, 0x37], "\ue7c7", "pointer 7457");
+decode([0x81, 0x35, 0xF4, 0x38], "\u1E40", "pointer 7459");
+decode([0x84, 0x31, 0xA4, 0x39], "\uffff", "pointer 39419");
+decode([0x84, 0x31, 0xA5, 0x30], "\ufffd", "pointer 39420");
+decode([0x8F, 0x39, 0xFE, 0x39], "\ufffd", "pointer 189999");
+decode([0x90, 0x30, 0x81, 0x30], "\u{10000}", "pointer 189000");
+decode([0xE3, 0x32, 0x9A, 0x35], "\u{10FFFF}", "pointer 1237575");
+decode([0xE3, 0x32, 0x9A, 0x36], "\ufffd", "pointer 1237576");
+decode([0x83, 0x36, 0xC8, 0x30], "\uE7C8", "legacy ICU special case 1");
+decode([0xA1, 0xAD], "\u2026", "legacy ICU special case 2");
+decode([0xA1, 0xAB], "\uFF5E", "legacy ICU special case 3");
+
+let i = 0;
+for (const range of ranges) {
+  const pointer = range[0];
+  decode(
+    [
+      Math.floor(pointer / 12600) + 0x81,
+      Math.floor((pointer % 12600) / 1260) + 0x30,
+      Math.floor((pointer % 1260) / 10) + 0x81,
+      pointer % 10 + 0x30,
+    ],
+    range[1],
+    "range " + i++,
+  );
+}
diff --git a/test/fixtures/wpt/encoding/legacy-mb-schinese/gbk/gbk-decoder.any.js b/test/fixtures/wpt/encoding/legacy-mb-schinese/gbk/gbk-decoder.any.js
new file mode 100644
index 00000000000000..c0221480da156d
--- /dev/null
+++ b/test/fixtures/wpt/encoding/legacy-mb-schinese/gbk/gbk-decoder.any.js
@@ -0,0 +1,28 @@
+const gbkPointers = [
+    6432, 7533, 7536, 7672, 7673, 7674, 7675, 7676, 7677, 7678, 7679, 7680, 7681, 7682, 7683, 7684,
+    23766, 23770, 23771, 23772, 23773, 23774, 23776, 23777, 23778, 23779, 23780, 23781, 23782, 23784, 23785, 23786,
+    23787, 23790, 23791, 23792, 23793, 23796, 23797, 23798, 23799, 23800, 23801, 23802, 23803, 23805, 23806, 23807,
+    23808, 23809, 23810, 23811, 23813, 23814, 23815, 23816, 23817, 23818, 23819, 23820, 23821, 23822, 23823, 23824,
+    23825, 23826, 23827, 23828, 23831, 23832, 23833, 23834, 23835, 23836, 23837, 23838, 23839, 23840, 23841, 23842,
+    23843, 23844
+];
+const codePoints = [
+    0x20ac, 0x1e3f, 0x01f9, 0x303e, 0x2ff0, 0x2ff1, 0x2ff2, 0x2ff3, 0x2ff4, 0x2ff5, 0x2ff6, 0x2ff7, 0x2ff8, 0x2ff9, 0x2ffa, 0x2ffb,
+    0x2e81, 0x2e84, 0x3473, 0x3447, 0x2e88, 0x2e8b, 0x359e, 0x361a, 0x360e, 0x2e8c, 0x2e97, 0x396e, 0x3918, 0x39cf, 0x39df, 0x3a73,
+    0x39d0, 0x3b4e, 0x3c6e, 0x3ce0, 0x2ea7, 0x2eaa, 0x4056, 0x415f, 0x2eae, 0x4337, 0x2eb3, 0x2eb6, 0x2eb7, 0x43b1, 0x43ac, 0x2ebb,
+    0x43dd, 0x44d6, 0x4661, 0x464c, 0x4723, 0x4729, 0x477c, 0x478d, 0x2eca, 0x4947, 0x497a, 0x497d, 0x4982, 0x4983, 0x4985, 0x4986,
+    0x499f, 0x499b, 0x49b7, 0x49b6, 0x4ca3, 0x4c9f, 0x4ca0, 0x4ca1, 0x4c77, 0x4ca2, 0x4d13, 0x4d14, 0x4d15, 0x4d16, 0x4d17, 0x4d18,
+    0x4d19, 0x4dae
+];
+
+for (let i = 0; i < gbkPointers.length; i++) {
+    const pointer = gbkPointers[i];
+    test(function() {
+        const lead = pointer / 190 + 0x81;
+        const trail = pointer % 190;
+        const offset = trail < 0x3F ? 0x40 : 0x41;
+        const encoded = [lead, trail + offset];
+        const decoded = new TextDecoder("GBK").decode(new Uint8Array(encoded)).charCodeAt(0);
+        assert_equals(decoded, codePoints[i]);
+    }, "gbk pointer: " + pointer)
+}
diff --git a/test/fixtures/wpt/encoding/textdecoder-arguments.any.js b/test/fixtures/wpt/encoding/textdecoder-arguments.any.js
new file mode 100644
index 00000000000000..f469dcd30eaf87
--- /dev/null
+++ b/test/fixtures/wpt/encoding/textdecoder-arguments.any.js
@@ -0,0 +1,49 @@
+// META: title=Encoding API: TextDecoder decode() optional arguments
+
+test(t => {
+  const decoder = new TextDecoder();
+
+  // Just passing nothing.
+  assert_equals(
+    decoder.decode(undefined), '',
+    'Undefined as first arg should decode to empty string');
+
+  // Flushing an incomplete sequence.
+  decoder.decode(new Uint8Array([0xc9]), {stream: true});
+  assert_equals(
+    decoder.decode(undefined), '\uFFFD',
+    'Undefined as first arg should flush the stream');
+
+}, 'TextDecoder decode() with explicit undefined');
+
+test(t => {
+  const decoder = new TextDecoder();
+
+  // Just passing nothing.
+  assert_equals(
+    decoder.decode(undefined, undefined), '',
+    'Undefined as first arg should decode to empty string');
+
+  // Flushing an incomplete sequence.
+  decoder.decode(new Uint8Array([0xc9]), {stream: true});
+  assert_equals(
+    decoder.decode(undefined, undefined), '\uFFFD',
+    'Undefined as first arg should flush the stream');
+
+}, 'TextDecoder decode() with undefined and undefined');
+
+test(t => {
+  const decoder = new TextDecoder();
+
+  // Just passing nothing.
+  assert_equals(
+    decoder.decode(undefined, {}), '',
+    'Undefined as first arg should decode to empty string');
+
+  // Flushing an incomplete sequence.
+  decoder.decode(new Uint8Array([0xc9]), {stream: true});
+  assert_equals(
+    decoder.decode(undefined, {}), '\uFFFD',
+    'Undefined as first arg should flush the stream');
+
+}, 'TextDecoder decode() with undefined and options');
diff --git a/test/fixtures/wpt/encoding/textdecoder-labels.any.js b/test/fixtures/wpt/encoding/textdecoder-labels.any.js
index ed407a32547923..efc4ae2d2ea586 100644
--- a/test/fixtures/wpt/encoding/textdecoder-labels.any.js
+++ b/test/fixtures/wpt/encoding/textdecoder-labels.any.js
@@ -1,5 +1,6 @@
 // META: title=Encoding API: Encoding labels
 // META: script=resources/encodings.js
+// META: timeout=long
 
 var whitespace = [' ', '\t', '\n', '\f', '\r'];
 encodings_table.forEach(function(section) {
diff --git a/test/fixtures/wpt/hr-time/basic.any.js b/test/fixtures/wpt/hr-time/basic.any.js
index 364dd81a344818..4cd6e42dfc2274 100644
--- a/test/fixtures/wpt/hr-time/basic.any.js
+++ b/test/fixtures/wpt/hr-time/basic.any.js
@@ -26,3 +26,12 @@ async_test(function() {
     this.done();
   }, 2000);
 }, 'High resolution time has approximately the right relative magnitude');
+
+test(function() {
+  var didHandle = false;
+  self.performance.addEventListener("testEvent", function() {
+    didHandle = true;
+  }, { once: true} );
+  self.performance.dispatchEvent(new Event("testEvent"));
+  assert_true(didHandle, "Performance extends EventTarget, so event dispatching should work.");
+}, "Performance interface extends EventTarget.");
diff --git a/test/fixtures/wpt/html/webappapis/timers/negative-setinterval.any.js b/test/fixtures/wpt/html/webappapis/timers/negative-setinterval.any.js
new file mode 100644
index 00000000000000..5646140c2a45f3
--- /dev/null
+++ b/test/fixtures/wpt/html/webappapis/timers/negative-setinterval.any.js
@@ -0,0 +1,12 @@
+setup({ single_test: true });
+var i = 0;
+var interval;
+function next() {
+  i++;
+  if (i === 20) {
+    clearInterval(interval);
+    done();
+  }
+}
+setTimeout(assert_unreached, 1000);
+interval = setInterval(next, -100);
diff --git a/test/fixtures/wpt/html/webappapis/timers/negative-settimeout.any.js b/test/fixtures/wpt/html/webappapis/timers/negative-settimeout.any.js
new file mode 100644
index 00000000000000..da191f1bf00d92
--- /dev/null
+++ b/test/fixtures/wpt/html/webappapis/timers/negative-settimeout.any.js
@@ -0,0 +1,3 @@
+setup({ single_test: true });
+setTimeout(done, -100);
+setTimeout(assert_unreached, 10);
diff --git a/test/fixtures/wpt/html/webappapis/timers/type-long-setinterval.any.js b/test/fixtures/wpt/html/webappapis/timers/type-long-setinterval.any.js
new file mode 100644
index 00000000000000..164527f18b1e6f
--- /dev/null
+++ b/test/fixtures/wpt/html/webappapis/timers/type-long-setinterval.any.js
@@ -0,0 +1,8 @@
+setup({ single_test: true });
+var interval;
+function next() {
+  clearInterval(interval);
+  done();
+}
+interval = setInterval(next, Math.pow(2, 32));
+setTimeout(assert_unreached, 100);
diff --git a/test/fixtures/wpt/html/webappapis/timers/type-long-settimeout.any.js b/test/fixtures/wpt/html/webappapis/timers/type-long-settimeout.any.js
new file mode 100644
index 00000000000000..9092f13f3b1000
--- /dev/null
+++ b/test/fixtures/wpt/html/webappapis/timers/type-long-settimeout.any.js
@@ -0,0 +1,3 @@
+setup({ single_test: true });
+setTimeout(done, Math.pow(2, 32));
+setTimeout(assert_unreached, 100);
diff --git a/test/fixtures/wpt/interfaces/html.idl b/test/fixtures/wpt/interfaces/html.idl
index dfe4e1e586b5a2..08cc505da91ba0 100644
--- a/test/fixtures/wpt/interfaces/html.idl
+++ b/test/fixtures/wpt/interfaces/html.idl
@@ -1397,11 +1397,11 @@ interface mixin CanvasDrawImage {
 
 interface mixin CanvasImageData {
   // pixel manipulation
-  ImageData createImageData(long sw, long sh);
+  ImageData createImageData([EnforceRange] long sw, [EnforceRange] long sh);
   ImageData createImageData(ImageData imagedata);
-  ImageData getImageData(long sx, long sy, long sw, long sh);
-  undefined putImageData(ImageData imagedata, long dx, long dy);
-  undefined putImageData(ImageData imagedata, long dx, long dy, long dirtyX, long dirtyY, long dirtyWidth, long dirtyHeight);
+  ImageData getImageData([EnforceRange] long sx, [EnforceRange] long sy, [EnforceRange] long sw, [EnforceRange] long sh);
+  undefined putImageData(ImageData imagedata, [EnforceRange] long dx, [EnforceRange] long dy);
+  undefined putImageData(ImageData imagedata, [EnforceRange] long dx, [EnforceRange] long dy, [EnforceRange] long dirtyX, [EnforceRange] long dirtyY, [EnforceRange] long dirtyWidth, [EnforceRange] long dirtyHeight);
 };
 
 enum CanvasLineCap { "butt", "round", "square" };
@@ -1440,8 +1440,8 @@ interface mixin CanvasPath {
   undefined bezierCurveTo(unrestricted double cp1x, unrestricted double cp1y, unrestricted double cp2x, unrestricted double cp2y, unrestricted double x, unrestricted double y);
   undefined arcTo(unrestricted double x1, unrestricted double y1, unrestricted double x2, unrestricted double y2, unrestricted double radius);
   undefined rect(unrestricted double x, unrestricted double y, unrestricted double w, unrestricted double h);
-  undefined arc(unrestricted double x, unrestricted double y, unrestricted double radius, unrestricted double startAngle, unrestricted double endAngle, optional boolean anticlockwise = false);
-  undefined ellipse(unrestricted double x, unrestricted double y, unrestricted double radiusX, unrestricted double radiusY, unrestricted double rotation, unrestricted double startAngle, unrestricted double endAngle, optional boolean anticlockwise = false);
+  undefined arc(unrestricted double x, unrestricted double y, unrestricted double radius, unrestricted double startAngle, unrestricted double endAngle, optional boolean counterclockwise = false);
+  undefined ellipse(unrestricted double x, unrestricted double y, unrestricted double radiusX, unrestricted double radiusY, unrestricted double rotation, unrestricted double startAngle, unrestricted double endAngle, optional boolean counterclockwise = false);
 };
 
 [Exposed=(Window,Worker)]
@@ -2027,48 +2027,6 @@ interface mixin NavigatorCookies {
   readonly attribute boolean cookieEnabled;
 };
 
-interface mixin NavigatorPlugins {
-  [SameObject] readonly attribute PluginArray plugins;
-  [SameObject] readonly attribute MimeTypeArray mimeTypes;
-  boolean javaEnabled();
-};
-
-[Exposed=Window,
- LegacyUnenumerableNamedProperties]
-interface PluginArray {
-  undefined refresh(optional boolean reload = false);
-  readonly attribute unsigned long length;
-  getter Plugin? item(unsigned long index);
-  getter Plugin? namedItem(DOMString name);
-};
-
-[Exposed=Window,
- LegacyUnenumerableNamedProperties]
-interface MimeTypeArray {
-  readonly attribute unsigned long length;
-  getter MimeType? item(unsigned long index);
-  getter MimeType? namedItem(DOMString name);
-};
-
-[Exposed=Window,
- LegacyUnenumerableNamedProperties]
-interface Plugin {
-  readonly attribute DOMString name;
-  readonly attribute DOMString description;
-  readonly attribute DOMString filename;
-  readonly attribute unsigned long length;
-  getter MimeType? item(unsigned long index);
-  getter MimeType? namedItem(DOMString name);
-};
-
-[Exposed=Window]
-interface MimeType {
-  readonly attribute DOMString type;
-  readonly attribute DOMString description;
-  readonly attribute DOMString suffixes; // comma-separated
-  readonly attribute Plugin enabledPlugin;
-};
-
 [Exposed=(Window,Worker), Serializable, Transferable]
 interface ImageBitmap {
   readonly attribute unsigned long width;
@@ -2396,10 +2354,6 @@ interface HTMLMarqueeElement : HTMLElement {
   [CEReactions] attribute unsigned long vspace;
   [CEReactions] attribute DOMString width;
 
-  attribute EventHandler onbounce;
-  attribute EventHandler onfinish;
-  attribute EventHandler onstart;
-
   undefined start();
   undefined stop();
 };
@@ -2678,3 +2632,42 @@ interface External {
   undefined AddSearchProvider();
   undefined IsSearchProviderInstalled();
 };
+
+interface mixin NavigatorPlugins {
+  [SameObject] readonly attribute PluginArray plugins;
+  [SameObject] readonly attribute MimeTypeArray mimeTypes;
+  boolean javaEnabled();
+};
+
+[Exposed=Window]
+interface PluginArray {
+  undefined refresh();
+  readonly attribute unsigned long length;
+  getter object? item(unsigned long index);
+  object? namedItem(DOMString name);
+};
+
+[Exposed=Window]
+interface MimeTypeArray {
+  readonly attribute unsigned long length;
+  getter object? item(unsigned long index);
+  object? namedItem(DOMString name);
+};
+
+[Exposed=Window]
+interface Plugin {
+  readonly attribute undefined name;
+  readonly attribute undefined description;
+  readonly attribute undefined filename;
+  readonly attribute undefined length;
+  getter undefined item(unsigned long index);
+  undefined namedItem(DOMString name);
+};
+
+[Exposed=Window]
+interface MimeType {
+  readonly attribute undefined type;
+  readonly attribute undefined description;
+  readonly attribute undefined suffixes;
+  readonly attribute undefined enabledPlugin;
+};
diff --git a/test/fixtures/wpt/resources/idlharness.js b/test/fixtures/wpt/resources/idlharness.js
index 994a0d82ef444b..76131e7c9602b9 100644
--- a/test/fixtures/wpt/resources/idlharness.js
+++ b/test/fixtures/wpt/resources/idlharness.js
@@ -3483,6 +3483,22 @@ IdlNamespace.prototype.test_self = function ()
     subsetTestByKey(this.name, test, () => {
         assert_equals(typeof namespaceObject, "object");
     }, `${this.name} namespace: typeof is "object"`);
+
+    subsetTestByKey(this.name, test, () => {
+        assert_equals(
+            Object.getOwnPropertyDescriptor(namespaceObject, "length"),
+            undefined,
+            "length property must be undefined"
+        );
+    }, `${this.name} namespace: has no length property`);
+
+    subsetTestByKey(this.name, test, () => {
+        assert_equals(
+            Object.getOwnPropertyDescriptor(namespaceObject, "name"),
+            undefined,
+            "name property must be undefined"
+        );
+    }, `${this.name} namespace: has no name property`);
 };
 
 IdlNamespace.prototype.test = function ()
@@ -3527,8 +3543,6 @@ IdlNamespace.prototype.test = function ()
 function idl_test(srcs, deps, idl_setup_func) {
     return promise_test(function (t) {
         var idl_array = new IdlArray();
-        srcs = (srcs instanceof Array) ? srcs : [srcs] || [];
-        deps = (deps instanceof Array) ? deps : [deps] || [];
         var setup_error = null;
         const validationIgnored = [
             "constructor-member",
diff --git a/test/fixtures/wpt/resources/test-only-api.js b/test/fixtures/wpt/resources/test-only-api.js
index ef66e0e733f9c6..a66eb44ede7c15 100644
--- a/test/fixtures/wpt/resources/test-only-api.js
+++ b/test/fixtures/wpt/resources/test-only-api.js
@@ -29,72 +29,3 @@ function loadScript(path) {
     return p;
   }
 }
-
-/**
- * A helper for Chromium-based browsers to load Mojo JS bindings
- *
- * This is an async function that works in both workers and windows. It first
- * loads mojo_bindings.js, disables automatic dependency loading, and loads all
- * resources sequentially. The promise resolves if everything loads
- * successfully, or rejects if any exception is raised. If testharness.js is
- * used, an uncaught exception will terminate the test with a harness error
- * (unless `allow_uncaught_exception` is true), which is usually the desired
- * behaviour.
- *
- * This function also works with Blink web tests loaded from file://, in which
- * case file:// will be prepended to all '/gen/...' URLs.
- *
- * Only call this function if isChromiumBased === true.
- *
- * @param {Array.<string>} resources - A list of scripts to load: Mojo JS
- *   bindings should be of the form '/gen/../*.mojom.js' or
- *   '/gen/../*.mojom-lite.js' (requires `lite` to be true); the order does not
- *   matter. Do not include 'mojo_bindings.js' or 'mojo_bindings_lite.js'.
- * @param {boolean=} lite - Whether the lite bindings (*.mojom-lite.js) are used
- *   (default is false).
- * @returns {Promise}
- */
-async function loadMojoResources(resources, lite = false) {
-  if (!isChromiumBased) {
-    throw new Error('MojoJS not enabled; start Chrome with --enable-blink-features=MojoJS,MojoJSTest');
-  }
-  if (resources.length == 0) {
-    return;
-  }
-
-  let genPrefix = '';
-  if (self.location.pathname.includes('/web_tests/')) {
-    // Blink internal web tests
-    genPrefix = 'file://';
-  }
-
-  for (const path of resources) {
-    // We want to load mojo_bindings.js separately to set mojo.config.
-    if (path.endsWith('/mojo_bindings.js')) {
-      throw new Error('Do not load mojo_bindings.js explicitly.');
-    }
-    if (path.endsWith('/mojo_bindings_lite.js')) {
-      throw new Error('Do not load mojo_bindings_lite.js explicitly.');
-    }
-    if (lite) {
-      if (! /^\/gen\/.*\.mojom-lite\.js$/.test(path)) {
-        throw new Error(`Unrecognized resource path: ${path}`);
-      }
-    } else {
-      if (! /^\/gen\/.*\.mojom\.js$/.test(path)) {
-        throw new Error(`Unrecognized resource path: ${path}`);
-      }
-    }
-  }
-
-  if (lite) {
-    await loadScript(genPrefix + '/gen/layout_test_data/mojo/public/js/mojo_bindings_lite.js');
-  } else {
-    await loadScript(genPrefix + '/gen/layout_test_data/mojo/public/js/mojo_bindings.js');
-    mojo.config.autoLoadMojomDeps = false;
-  }
-
-  for (const path of resources) {
-    await loadScript(genPrefix + path);
-  }
-}
diff --git a/test/fixtures/wpt/resources/testharness.js b/test/fixtures/wpt/resources/testharness.js
index f7fe7531710d23..c62f0917c173b0 100644
--- a/test/fixtures/wpt/resources/testharness.js
+++ b/test/fixtures/wpt/resources/testharness.js
@@ -15,7 +15,6 @@ policies and contribution forms [3].
 
 (function (global_scope)
 {
-    var debug = false;
     // default timeout is 10 seconds, test can override if needed
     var settings = {
         output:true,
@@ -24,7 +23,8 @@ policies and contribution forms [3].
             "long":60000
         },
         test_timeout:null,
-        message_events: ["start", "test_state", "result", "completion"]
+        message_events: ["start", "test_state", "result", "completion"],
+        debug: false,
     };
 
     var xhtml_ns = "http://www.w3.org/1999/xhtml";
@@ -87,14 +87,15 @@ policies and contribution forms [3].
                                              test: test.structured_clone()});
                      }],
             completion: [add_completion_callback, remove_completion_callback,
-                         function (tests, harness_status) {
+                         function (tests, harness_status, asserts) {
                              var cloned_tests = map(tests, function(test) {
                                  return test.structured_clone();
                              });
                              this_obj._dispatch("completion_callback", [tests, harness_status],
                                                 {type: "complete",
                                                  tests: cloned_tests,
-                                                 status: harness_status.structured_clone()});
+                                                 status: harness_status.structured_clone(),
+                                                 asserts: asserts.map(assert => assert.structured_clone())});
                          }]
         }
 
@@ -131,11 +132,7 @@ policies and contribution forms [3].
                         if (has_selector) {
                             try {
                                 w[selector].apply(undefined, callback_args);
-                            } catch (e) {
-                                if (debug) {
-                                    throw e;
-                                }
-                            }
+                            } catch (e) {}
                         }
                     }
                     if (supports_post_message(w) && w !== self) {
@@ -192,8 +189,8 @@ policies and contribution forms [3].
             this_obj.output_handler.show_status();
         });
 
-        add_completion_callback(function (tests, harness_status) {
-            this_obj.output_handler.show_results(tests, harness_status);
+        add_completion_callback(function (tests, harness_status, asserts_run) {
+            this_obj.output_handler.show_results(tests, harness_status, asserts_run);
         });
         this.setup_messages(settings.message_events);
     };
@@ -314,14 +311,15 @@ policies and contribution forms [3].
                     });
                 });
         add_completion_callback(
-                function(tests, harness_status) {
+                function(tests, harness_status, asserts) {
                     this_obj._dispatch({
                         type: "complete",
                         tests: map(tests,
                             function(test) {
                                 return test.structured_clone();
                             }),
-                        status: harness_status.structured_clone()
+                        status: harness_status.structured_clone(),
+                        asserts: asserts.map(assert => assert.structured_clone()),
                     });
                 });
     };
@@ -500,11 +498,7 @@ policies and contribution forms [3].
             return new DedicatedWorkerTestEnvironment();
         }
 
-        if (!('location' in global_scope)) {
-            return new ShellTestEnvironment();
-        }
-
-        throw new Error("Unsupported test environment");
+        return new ShellTestEnvironment();
     }
 
     var test_environment = create_test_environment();
@@ -1187,19 +1181,53 @@ policies and contribution forms [3].
      * Assertions
      */
 
+    function expose_assert(f, name) {
+        function assert_wrapper(...args) {
+            let status = Test.statuses.TIMEOUT;
+            let stack = null;
+            try {
+                if (settings.debug) {
+                    console.debug("ASSERT", name, tests.current_test && tests.current_test.name, args);
+                }
+                if (tests.output) {
+                    tests.set_assert(name, ...args);
+                }
+                const rv = f(...args);
+                status = Test.statuses.PASS;
+                return rv;
+            } catch(e) {
+                if (e instanceof AssertionError) {
+                    status = Test.statuses.FAIL;
+                    stack = e.stack;
+                 } else {
+                    status = Test.statuses.ERROR;
+                 }
+                throw e;
+            } finally {
+                if (tests.output && !stack) {
+                    stack = get_stack();
+                }
+                if (tests.output) {
+                    tests.set_assert_status(status, stack);
+                }
+            }
+        }
+        expose(assert_wrapper, name);
+    }
+
     function assert_true(actual, description)
     {
         assert(actual === true, "assert_true", description,
                                 "expected true got ${actual}", {actual:actual});
     }
-    expose(assert_true, "assert_true");
+    expose_assert(assert_true, "assert_true");
 
     function assert_false(actual, description)
     {
         assert(actual === false, "assert_false", description,
                                  "expected false got ${actual}", {actual:actual});
     }
-    expose(assert_false, "assert_false");
+    expose_assert(assert_false, "assert_false");
 
     function same_value(x, y) {
         if (y !== y) {
@@ -1229,7 +1257,7 @@ policies and contribution forms [3].
                                              "expected ${expected} but got ${actual}",
                                              {expected:expected, actual:actual});
     }
-    expose(assert_equals, "assert_equals");
+    expose_assert(assert_equals, "assert_equals");
 
     function assert_not_equals(actual, expected, description)
     {
@@ -1241,7 +1269,7 @@ policies and contribution forms [3].
                                               "got disallowed value ${actual}",
                                               {actual:actual});
     }
-    expose(assert_not_equals, "assert_not_equals");
+    expose_assert(assert_not_equals, "assert_not_equals");
 
     function assert_in_array(actual, expected, description)
     {
@@ -1249,7 +1277,7 @@ policies and contribution forms [3].
                                                "value ${actual} not in array ${expected}",
                                                {actual:actual, expected:expected});
     }
-    expose(assert_in_array, "assert_in_array");
+    expose_assert(assert_in_array, "assert_in_array");
 
     // This function was deprecated in July of 2015.
     // See https://github.com/web-platform-tests/wpt/issues/2033
@@ -1287,7 +1315,7 @@ policies and contribution forms [3].
          }
          check_equal(actual, expected, []);
     }
-    expose(assert_object_equals, "assert_object_equals");
+    expose_assert(assert_object_equals, "assert_object_equals");
 
     function assert_array_equals(actual, expected, description)
     {
@@ -1344,7 +1372,7 @@ policies and contribution forms [3].
                     arrayExpected:shorten_array(expected, i), arrayActual:shorten_array(actual, i)});
         }
     }
-    expose(assert_array_equals, "assert_array_equals");
+    expose_assert(assert_array_equals, "assert_array_equals");
 
     function assert_array_approx_equals(actual, expected, epsilon, description)
     {
@@ -1372,7 +1400,7 @@ policies and contribution forms [3].
                    {i:i, expected:expected[i], actual:actual[i], epsilon:epsilon});
         }
     }
-    expose(assert_array_approx_equals, "assert_array_approx_equals");
+    expose_assert(assert_array_approx_equals, "assert_array_approx_equals");
 
     function assert_approx_equals(actual, expected, epsilon, description)
     {
@@ -1395,7 +1423,7 @@ policies and contribution forms [3].
             assert_equals(actual, expected);
         }
     }
-    expose(assert_approx_equals, "assert_approx_equals");
+    expose_assert(assert_approx_equals, "assert_approx_equals");
 
     function assert_less_than(actual, expected, description)
     {
@@ -1412,7 +1440,7 @@ policies and contribution forms [3].
                "expected a number less than ${expected} but got ${actual}",
                {expected:expected, actual:actual});
     }
-    expose(assert_less_than, "assert_less_than");
+    expose_assert(assert_less_than, "assert_less_than");
 
     function assert_greater_than(actual, expected, description)
     {
@@ -1429,7 +1457,7 @@ policies and contribution forms [3].
                "expected a number greater than ${expected} but got ${actual}",
                {expected:expected, actual:actual});
     }
-    expose(assert_greater_than, "assert_greater_than");
+    expose_assert(assert_greater_than, "assert_greater_than");
 
     function assert_between_exclusive(actual, lower, upper, description)
     {
@@ -1447,7 +1475,7 @@ policies and contribution forms [3].
                "and less than ${upper} but got ${actual}",
                {lower:lower, upper:upper, actual:actual});
     }
-    expose(assert_between_exclusive, "assert_between_exclusive");
+    expose_assert(assert_between_exclusive, "assert_between_exclusive");
 
     function assert_less_than_equal(actual, expected, description)
     {
@@ -1464,7 +1492,7 @@ policies and contribution forms [3].
                "expected a number less than or equal to ${expected} but got ${actual}",
                {expected:expected, actual:actual});
     }
-    expose(assert_less_than_equal, "assert_less_than_equal");
+    expose_assert(assert_less_than_equal, "assert_less_than_equal");
 
     function assert_greater_than_equal(actual, expected, description)
     {
@@ -1481,7 +1509,7 @@ policies and contribution forms [3].
                "expected a number greater than or equal to ${expected} but got ${actual}",
                {expected:expected, actual:actual});
     }
-    expose(assert_greater_than_equal, "assert_greater_than_equal");
+    expose_assert(assert_greater_than_equal, "assert_greater_than_equal");
 
     function assert_between_inclusive(actual, lower, upper, description)
     {
@@ -1499,7 +1527,7 @@ policies and contribution forms [3].
                "and less than or equal to ${upper} but got ${actual}",
                {lower:lower, upper:upper, actual:actual});
     }
-    expose(assert_between_inclusive, "assert_between_inclusive");
+    expose_assert(assert_between_inclusive, "assert_between_inclusive");
 
     function assert_regexp_match(actual, expected, description) {
         /*
@@ -1510,7 +1538,7 @@ policies and contribution forms [3].
                "expected ${expected} but got ${actual}",
                {expected:expected, actual:actual});
     }
-    expose(assert_regexp_match, "assert_regexp_match");
+    expose_assert(assert_regexp_match, "assert_regexp_match");
 
     function assert_class_string(object, class_string, description) {
         var actual = {}.toString.call(object);
@@ -1519,22 +1547,21 @@ policies and contribution forms [3].
                                              "expected ${expected} but got ${actual}",
                                              {expected:expected, actual:actual});
     }
-    expose(assert_class_string, "assert_class_string");
-
+    expose_assert(assert_class_string, "assert_class_string");
 
     function assert_own_property(object, property_name, description) {
         assert(object.hasOwnProperty(property_name),
                "assert_own_property", description,
                "expected property ${p} missing", {p:property_name});
     }
-    expose(assert_own_property, "assert_own_property");
+    expose_assert(assert_own_property, "assert_own_property");
 
     function assert_not_own_property(object, property_name, description) {
         assert(!object.hasOwnProperty(property_name),
                "assert_not_own_property", description,
                "unexpected property ${p} is found on object", {p:property_name});
     }
-    expose(assert_not_own_property, "assert_not_own_property");
+    expose_assert(assert_not_own_property, "assert_not_own_property");
 
     function _assert_inherits(name) {
         return function (object, property_name, description)
@@ -1560,8 +1587,8 @@ policies and contribution forms [3].
                    {p:property_name});
         };
     }
-    expose(_assert_inherits("assert_inherits"), "assert_inherits");
-    expose(_assert_inherits("assert_idl_attribute"), "assert_idl_attribute");
+    expose_assert(_assert_inherits("assert_inherits"), "assert_inherits");
+    expose_assert(_assert_inherits("assert_idl_attribute"), "assert_idl_attribute");
 
     function assert_readonly(object, property_name, description)
     {
@@ -1578,7 +1605,7 @@ policies and contribution forms [3].
              object[property_name] = initial_value;
          }
     }
-    expose(assert_readonly, "assert_readonly");
+    expose_assert(assert_readonly, "assert_readonly");
 
     /**
      * Assert a JS Error with the expected constructor is thrown.
@@ -1592,7 +1619,7 @@ policies and contribution forms [3].
         assert_throws_js_impl(constructor, func, description,
                               "assert_throws_js");
     }
-    expose(assert_throws_js, "assert_throws_js");
+    expose_assert(assert_throws_js, "assert_throws_js");
 
     /**
      * Like assert_throws_js but allows specifying the assertion type
@@ -1690,7 +1717,7 @@ policies and contribution forms [3].
         }
         assert_throws_dom_impl(type, func, description, "assert_throws_dom", constructor)
     }
-    expose(assert_throws_dom, "assert_throws_dom");
+    expose_assert(assert_throws_dom, "assert_throws_dom");
 
     /**
      * Similar to assert_throws_dom but allows specifying the assertion type
@@ -1853,7 +1880,7 @@ policies and contribution forms [3].
         assert_throws_exactly_impl(exception, func, description,
                                    "assert_throws_exactly");
     }
-    expose(assert_throws_exactly, "assert_throws_exactly");
+    expose_assert(assert_throws_exactly, "assert_throws_exactly");
 
     /**
      * Like assert_throws_exactly but allows specifying the assertion type
@@ -1881,7 +1908,7 @@ policies and contribution forms [3].
          assert(false, "assert_unreached", description,
                 "Reached unreachable code");
     }
-    expose(assert_unreached, "assert_unreached");
+    expose_assert(assert_unreached, "assert_unreached");
 
     function assert_any(assert_func, actual, expected_array)
     {
@@ -1902,7 +1929,7 @@ policies and contribution forms [3].
             throw new AssertionError(errors.join("\n\n"));
         }
     }
-    expose(assert_any, "assert_any");
+    expose_assert(assert_any, "assert_any");
 
     /**
      * Assert that a feature is implemented, based on a 'truthy' condition.
@@ -1919,7 +1946,7 @@ policies and contribution forms [3].
     function assert_implements(condition, description) {
         assert(!!condition, "assert_implements", description);
     }
-    expose(assert_implements, "assert_implements")
+    expose_assert(assert_implements, "assert_implements")
 
     /**
      * Assert that an optional feature is implemented, based on a 'truthy' condition.
@@ -1939,7 +1966,7 @@ policies and contribution forms [3].
             throw new OptionalFeatureUnsupportedError(description);
         }
     }
-    expose(assert_implements_optional, "assert_implements_optional")
+    expose_assert(assert_implements_optional, "assert_implements_optional")
 
     function Test(name, properties)
     {
@@ -1999,6 +2026,18 @@ policies and contribution forms [3].
         COMPLETE:4
     };
 
+    Test.prototype.status_formats = {
+        0: "Pass",
+        1: "Fail",
+        2: "Timeout",
+        3: "Not Run",
+        4: "Optional Feature Unsupported",
+    }
+
+    Test.prototype.format_status = function() {
+        return this.status_formats[this.status];
+    }
+
     Test.prototype.structured_clone = function()
     {
         if (!this._structured_clone) {
@@ -2023,11 +2062,16 @@ policies and contribution forms [3].
         if (this.phase > this.phases.STARTED) {
             return;
         }
+
+        if (settings.debug && this.phase !== this.phases.STARTED) {
+            console.log("TEST START", this.name);
+        }
         this.phase = this.phases.STARTED;
         //If we don't get a result before the harness times out that will be a test timeout
         this.set_status(this.TIMEOUT, "Test timed out");
 
         tests.started = true;
+        tests.current_test = this;
         tests.notify_test_state(this);
 
         if (this.timeout_id === null) {
@@ -2040,6 +2084,10 @@ policies and contribution forms [3].
             this_obj = this;
         }
 
+        if (settings.debug) {
+            console.debug("TEST STEP", this.name);
+        }
+
         try {
             return func.apply(this_obj, Array.prototype.slice.call(arguments, 2));
         } catch (e) {
@@ -2053,6 +2101,8 @@ policies and contribution forms [3].
             this.set_status(status, message, stack);
             this.phase = this.phases.HAS_RESULT;
             this.done();
+        } finally {
+            this.current_test = null;
         }
     };
 
@@ -2270,6 +2320,12 @@ policies and contribution forms [3].
             clearTimeout(this.timeout_id);
         }
 
+        if (settings.debug) {
+            console.log("TEST DONE",
+                        this.status,
+                        this.name,)
+        }
+
         this.cleanup();
     };
 
@@ -2469,6 +2525,10 @@ policies and contribution forms [3].
                 });
     }
 
+    RemoteTest.prototype.format_status = function() {
+        return Test.prototype.status_formats[this.status];
+    }
+
     /*
      * A RemoteContext listens for test events from a remote test context, such
      * as another window or a worker. These events are then used to construct
@@ -2577,6 +2637,16 @@ policies and contribution forms [3].
             tests.set_status(data.status.status, data.status.message, data.status.sack);
         }
 
+        for (let assert of data.asserts) {
+            var record = new AssertRecord();
+            record.assert_name = assert.assert_name;
+            record.args = assert.args;
+            record.test = assert.test != null ? this.tests[assert.test.index] : null;
+            record.status = assert.status;
+            record.stack = assert.stack;
+            tests.asserts_run.push(record);
+        }
+
         this.message_target.removeEventListener("message", this.message_handler);
         this.running = false;
 
@@ -2626,6 +2696,14 @@ policies and contribution forms [3].
 
     TestsStatus.prototype = merge({}, TestsStatus.statuses);
 
+    TestsStatus.prototype.formats = {
+        0: "OK",
+        1: "Error",
+        2: "Timeout",
+        3: "Optional Feature Unsupported"
+    }
+
+
     TestsStatus.prototype.structured_clone = function()
     {
         if (!this._structured_clone) {
@@ -2640,6 +2718,27 @@ policies and contribution forms [3].
         return this._structured_clone;
     };
 
+    TestsStatus.prototype.format_status = function() {
+        return this.formats[this.status];
+    }
+
+    function AssertRecord(test, assert_name, ...args) {
+        this.assert_name = assert_name;
+        this.test = test;
+        // Avoid keeping complex objects alive
+        this.args = args.map(x => format_value(x).replace(/\n/g, " "));
+        this.status = null;
+    }
+
+    AssertRecord.prototype.structured_clone = function() {
+        return {
+            assert_name: this.assert_name,
+            test: this.test ? this.test.structured_clone() : null,
+            args: this.args,
+            status: this.status,
+        }
+    }
+
     function Tests()
     {
         this.tests = [];
@@ -2679,6 +2778,18 @@ policies and contribution forms [3].
         this.hide_test_state = false;
         this.pending_remotes = [];
 
+        this.current_test = null;
+        this.asserts_run = [];
+
+        // Track whether output is enabled, and thus whether or not we should
+        // track asserts.
+        //
+        // On workers we don't get properties set from testharnessreport.js, so
+        // we don't know whether or not to track asserts. To avoid the
+        // resulting performance hit, we assume we are not meant to. This means
+        // that assert tracking does not function on workers.
+        this.output = settings.output && 'document' in global_scope;
+
         this.status = new TestsStatus();
 
         var this_obj = this;
@@ -2726,6 +2837,10 @@ policies and contribution forms [3].
                     }
                 } else if (p == "hide_test_state") {
                     this.hide_test_state = value;
+                } else if (p == "output") {
+                    this.output = value;
+                } else if (p === "debug") {
+                    settings.debug = value;
                 }
             }
         }
@@ -2750,7 +2865,7 @@ policies and contribution forms [3].
         this.wait_for_finish = true;
         this.file_is_test = true;
         // Create the test, which will add it to the list of tests
-        async_test();
+        tests.current_test = async_test();
     };
 
     Tests.prototype.set_status = function(status, message, stack)
@@ -2914,6 +3029,16 @@ policies and contribution forms [3].
                   all_complete);
     };
 
+    Tests.prototype.set_assert = function(assert_name, ...args) {
+        this.asserts_run.push(new AssertRecord(this.current_test, assert_name, ...args))
+    }
+
+    Tests.prototype.set_assert_status = function(status, stack) {
+        let assert_record = this.asserts_run[this.asserts_run.length - 1];
+        assert_record.status = status;
+        assert_record.stack = stack;
+    }
+
     /**
      * Update the harness status to reflect an unrecoverable harness error that
      * should cancel all further testing. Update all previously-defined tests
@@ -3011,7 +3136,7 @@ policies and contribution forms [3].
         forEach (this.all_done_callbacks,
                  function(callback)
                  {
-                     callback(this_obj.tests, this_obj.status);
+                     callback(this_obj.tests, this_obj.status, this_obj.asserts_run);
                  });
     };
 
@@ -3237,7 +3362,7 @@ policies and contribution forms [3].
         }
     };
 
-    Output.prototype.show_results = function (tests, harness_status) {
+    Output.prototype.show_results = function (tests, harness_status, asserts_run) {
         if (this.phase >= this.COMPLETE) {
             return;
         }
@@ -3266,23 +3391,10 @@ policies and contribution forms [3].
             heads[0].appendChild(stylesheet);
         }
 
-        var status_text_harness = {};
-        status_text_harness[harness_status.OK] = "OK";
-        status_text_harness[harness_status.ERROR] = "Error";
-        status_text_harness[harness_status.TIMEOUT] = "Timeout";
-        status_text_harness[harness_status.PRECONDITION_FAILED] = "Optional Feature Unsupported";
-
-        var status_text = {};
-        status_text[Test.prototype.PASS] = "Pass";
-        status_text[Test.prototype.FAIL] = "Fail";
-        status_text[Test.prototype.TIMEOUT] = "Timeout";
-        status_text[Test.prototype.NOTRUN] = "Not Run";
-        status_text[Test.prototype.PRECONDITION_FAILED] = "Optional Feature Unsupported";
-
         var status_number = {};
         forEach(tests,
                 function(test) {
-                    var status = status_text[test.status];
+                    var status = test.format_status();
                     if (status_number.hasOwnProperty(status)) {
                         status_number[status] += 1;
                     } else {
@@ -3299,8 +3411,7 @@ policies and contribution forms [3].
                                 ["h2", {}, "Summary"],
                                 function()
                                 {
-
-                                    var status = status_text_harness[harness_status.status];
+                                    var status = harness_status.format_status();
                                     var rv = [["section", {},
                                                ["p", {},
                                                 "Harness status: ",
@@ -3322,13 +3433,14 @@ policies and contribution forms [3].
                                 function() {
                                     var rv = [["div", {}]];
                                     var i = 0;
-                                    while (status_text.hasOwnProperty(i)) {
-                                        if (status_number.hasOwnProperty(status_text[i])) {
-                                            var status = status_text[i];
-                                            rv[0].push(["div", {"class":status_class(status)},
+                                    while (Test.prototype.status_formats.hasOwnProperty(i)) {
+                                        if (status_number.hasOwnProperty(Test.prototype.status_formats[i])) {
+                                            var status = Test.prototype.status_formats[i];
+                                            rv[0].push(["div", {},
                                                         ["label", {},
                                                          ["input", {type:"checkbox", checked:"checked"}],
-                                                         status_number[status] + " " + status]]);
+                                                         status_number[status] + " ",
+                                                         ["span", {"class":status_class(status)}, status]]]);
                                         }
                                         i++;
                                     }
@@ -3395,6 +3507,51 @@ policies and contribution forms [3].
             return '';
         }
 
+        var asserts_run_by_test = new Map();
+        asserts_run.forEach(assert => {
+            if (!asserts_run_by_test.has(assert.test)) {
+                asserts_run_by_test.set(assert.test, []);
+            }
+            asserts_run_by_test.get(assert.test).push(assert);
+        });
+
+        function get_asserts_output(test) {
+            var asserts = asserts_run_by_test.get(test);
+            if (!asserts) {
+                return "No asserts ran";
+            }
+            rv = "<table>";
+            rv += asserts.map(assert => {
+                var output_fn = "<strong>" + escape_html(assert.assert_name) + "</strong>(";
+                var prefix_len = output_fn.length;
+                var output_args = assert.args;
+                var output_len = output_args.reduce((prev, current) => prev+current, prefix_len);
+                if (output_len[output_len.length - 1] > 50) {
+                    output_args = output_args.map((x, i) =>
+                    (i > 0 ? "  ".repeat(prefix_len) : "" )+ x + (i < output_args.length - 1 ? ",\n" : ""));
+                } else {
+                    output_args = output_args.map((x, i) => x + (i < output_args.length - 1 ? ", " : ""));
+                }
+                output_fn += escape_html(output_args.join(""));
+                output_fn += ')';
+                var output_location;
+                if (assert.stack) {
+                    output_location = assert.stack.split("\n", 1)[0].replace(/@?\w+:\/\/[^ "\/]+(?::\d+)?/g, " ");
+                }
+                return "<tr><td class=" +
+                    status_class(Test.prototype.status_formats[assert.status]) + ">" +
+                    Test.prototype.status_formats[assert.status] + "</td>" +
+                    "</td>" +
+                    "<td><pre>" +
+                    output_fn +
+                    (output_location ? "\n" + escape_html(output_location) : "") +
+                    "</pre></td></tr>";
+            }
+            ).join("\n");
+            rv += "</table>";
+            return rv;
+        }
+
         log.appendChild(document.createElementNS(xhtml_ns, "section"));
         var assertions = has_assertions();
         var html = "<h2>Details</h2><table id='results' " + (assertions ? "class='assertions'" : "" ) + ">" +
@@ -3403,19 +3560,23 @@ policies and contribution forms [3].
             "<th>Message</th></tr></thead>" +
             "<tbody>";
         for (var i = 0; i < tests.length; i++) {
-            html += '<tr class="' +
-                escape_html(status_class(status_text[tests[i].status])) +
-                '"><td>' +
-                escape_html(status_text[tests[i].status]) +
+            var test = tests[i];
+            html += '<tr><td class="' +
+                status_class(test.format_status()) +
+                '">' +
+                test.format_status() +
                 "</td><td>" +
-                escape_html(tests[i].name) +
+                escape_html(test.name) +
                 "</td><td>" +
-                (assertions ? escape_html(get_assertion(tests[i])) + "</td><td>" : "") +
-                escape_html(tests[i].message ? tests[i].message : " ") +
+                (assertions ? escape_html(get_assertion(test)) + "</td><td>" : "") +
+                escape_html(test.message ? tests[i].message : " ") +
                 (tests[i].stack ? "<pre>" +
                  escape_html(tests[i].stack) +
-                 "</pre>": "") +
-                "</td></tr>";
+                 "</pre>": "");
+            if (!(test instanceof RemoteTest)) {
+                 html += "<details><summary>Asserts run</summary>" + get_asserts_output(test) + "</details>"
+            }
+            html += "</td></tr>";
         }
         html += "</tbody></table>";
         try {
@@ -3610,13 +3771,13 @@ policies and contribution forms [3].
             message = sanitize_unpaired_surrogates(message);
         }
         this.message = message;
-        this.stack = this.get_stack();
+        this.stack = get_stack();
     }
     expose(AssertionError, "AssertionError");
 
     AssertionError.prototype = Object.create(Error.prototype);
 
-    AssertionError.prototype.get_stack = function() {
+    const get_stack = function() {
         var stack = new Error().stack;
         // IE11 does not initialize 'Error.stack' until the object is thrown.
         if (!stack) {
@@ -3980,54 +4141,62 @@ table#results {\
     width:100%;\
 }\
 \
-table#results th:first-child,\
-table#results td:first-child {\
+table#results > thead > tr > th:first-child,\
+table#results > tbody > tr > td:first-child {\
     width:8em;\
 }\
 \
-table#results th:last-child,\
-table#results td:last-child {\
+table#results > thead > tr > th:last-child,\
+table#results > thead > tr > td:last-child {\
     width:50%;\
 }\
 \
-table#results.assertions th:last-child,\
-table#results.assertions td:last-child {\
+table#results.assertions > thead > tr > th:last-child,\
+table#results.assertions > tbody > tr > td:last-child {\
     width:35%;\
 }\
 \
-table#results th {\
+table#results > thead > > tr > th {\
     padding:0;\
     padding-bottom:0.5em;\
     border-bottom:medium solid black;\
 }\
 \
-table#results td {\
+table#results > tbody > tr> td {\
     padding:1em;\
     padding-bottom:0.5em;\
     border-bottom:thin solid black;\
 }\
 \
-tr.pass > td:first-child {\
+.pass {\
     color:green;\
 }\
 \
-tr.fail > td:first-child {\
+.fail {\
     color:red;\
 }\
 \
-tr.timeout > td:first-child {\
+tr.timeout {\
     color:red;\
 }\
 \
-tr.notrun > td:first-child {\
+tr.notrun {\
     color:blue;\
 }\
 \
-tr.optionalunsupported > td:first-child {\
+tr.optionalunsupported {\
     color:blue;\
 }\
 \
-.pass > td:first-child, .fail > td:first-child, .timeout > td:first-child, .notrun > td:first-child, .optionalunsupported > td:first-child {\
+.ok {\
+    color:green;\
+}\
+\
+.error {\
+    color:red;\
+}\
+\
+.pass, .fail, .timeout, .notrun, .optionalunsupported .ok, .timeout, .error {\
     font-variant:small-caps;\
 }\
 \
@@ -4044,22 +4213,6 @@ table#results span.actual {\
     font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace;\
     white-space:pre;\
 }\
-\
-span.ok {\
-    color:green;\
-}\
-\
-tr.error {\
-    color:red;\
-}\
-\
-span.timeout {\
-    color:red;\
-}\
-\
-span.ok, span.timeout, span.error {\
-    font-variant:small-caps;\
-}\
 ";
 
 })(self);
diff --git a/test/fixtures/wpt/url/url-constructor.any.js b/test/fixtures/wpt/url/url-constructor.any.js
new file mode 100644
index 00000000000000..3f4af56d2a9654
--- /dev/null
+++ b/test/fixtures/wpt/url/url-constructor.any.js
@@ -0,0 +1,39 @@
+// META: timeout=long
+
+function bURL(url, base) {
+  return new URL(url, base || "about:blank")
+}
+
+function runURLTests(urltests) {
+  for(var i = 0, l = urltests.length; i < l; i++) {
+    var expected = urltests[i]
+    if (typeof expected === "string") continue // skip comments
+
+    test(function() {
+      if (expected.failure) {
+        assert_throws_js(TypeError, function() {
+          bURL(expected.input, expected.base)
+        })
+        return
+      }
+
+      var url = bURL(expected.input, expected.base)
+      assert_equals(url.href, expected.href, "href")
+      assert_equals(url.protocol, expected.protocol, "protocol")
+      assert_equals(url.username, expected.username, "username")
+      assert_equals(url.password, expected.password, "password")
+      assert_equals(url.host, expected.host, "host")
+      assert_equals(url.hostname, expected.hostname, "hostname")
+      assert_equals(url.port, expected.port, "port")
+      assert_equals(url.pathname, expected.pathname, "pathname")
+      assert_equals(url.search, expected.search, "search")
+      if ("searchParams" in expected) {
+        assert_true("searchParams" in url)
+        assert_equals(url.searchParams.toString(), expected.searchParams, "searchParams")
+      }
+      assert_equals(url.hash, expected.hash, "hash")
+    }, "Parsing: <" + expected.input + "> against <" + expected.base + ">")
+  }
+}
+
+promise_test(() => fetch("resources/urltestdata.json").then(res => res.json()).then(runURLTests), "Loading data…");
diff --git a/test/fixtures/wpt/url/url-origin.any.js b/test/fixtures/wpt/url/url-origin.any.js
new file mode 100644
index 00000000000000..d9ef64c73b8bcc
--- /dev/null
+++ b/test/fixtures/wpt/url/url-origin.any.js
@@ -0,0 +1,17 @@
+promise_test(() => fetch("resources/urltestdata.json").then(res => res.json()).then(runURLTests), "Loading data…");
+
+function bURL(url, base) {
+  return new URL(url, base || "about:blank")
+}
+
+function runURLTests(urltests) {
+  for(var i = 0, l = urltests.length; i < l; i++) {
+    var expected = urltests[i]
+    if (typeof expected === "string" || !("origin" in expected)) continue
+
+    test(function() {
+      var url = bURL(expected.input, expected.base)
+      assert_equals(url.origin, expected.origin, "origin")
+    }, "Origin parsing: <" + expected.input + "> against <" + expected.base + ">")
+  }
+}
diff --git a/test/fixtures/wpt/url/urlsearchparams-constructor.any.js b/test/fixtures/wpt/url/urlsearchparams-constructor.any.js
index f9878373e5e067..d482911350ba0f 100644
--- a/test/fixtures/wpt/url/urlsearchparams-constructor.any.js
+++ b/test/fixtures/wpt/url/urlsearchparams-constructor.any.js
@@ -29,7 +29,7 @@ test(() => {
 test(() => {
     var params = new URLSearchParams('');
     assert_true(params != null, 'constructor returned non-null value.');
-    assert_equals(params.__proto__, URLSearchParams.prototype, 'expected URLSearchParams.prototype as prototype.');
+    assert_equals(Object.getPrototypeOf(params), URLSearchParams.prototype, 'expected URLSearchParams.prototype as prototype.');
 }, "URLSearchParams constructor, empty string as argument")
 
 test(() => {
diff --git a/test/fixtures/wpt/url/urlsearchparams-foreach.any.js b/test/fixtures/wpt/url/urlsearchparams-foreach.any.js
index 7969a0cb11a271..ff19643ac220d1 100644
--- a/test/fixtures/wpt/url/urlsearchparams-foreach.any.js
+++ b/test/fixtures/wpt/url/urlsearchparams-foreach.any.js
@@ -14,7 +14,7 @@ test(function() {
     let a = new URL("http://a.b/c?a=1&b=2&c=3&d=4");
     let b = a.searchParams;
     var c = [];
-    for (i of b) {
+    for (const i of b) {
         a.search = "x=1&y=2&z=3";
         c.push(i);
     }
@@ -26,7 +26,7 @@ test(function() {
 test(function() {
     let a = new URL("http://a.b/c");
     let b = a.searchParams;
-    for (i of b) {
+    for (const i of b) {
         assert_unreached(i);
     }
 }, "empty");
@@ -35,7 +35,7 @@ test(function() {
     const url = new URL("http://localhost/query?param0=0&param1=1&param2=2");
     const searchParams = url.searchParams;
     const seen = [];
-    for (param of searchParams) {
+    for (const param of searchParams) {
         if (param[0] === 'param0') {
             searchParams.delete('param1');
         }
@@ -50,7 +50,7 @@ test(function() {
     const url = new URL("http://localhost/query?param0=0&param1=1&param2=2");
     const searchParams = url.searchParams;
     const seen = [];
-    for (param of searchParams) {
+    for (const param of searchParams) {
         if (param[0] === 'param0') {
             searchParams.delete('param0');
             // 'param1=1' is now in the first slot, so the next iteration will see 'param2=2'.
@@ -66,7 +66,7 @@ test(function() {
     const url = new URL("http://localhost/query?param0=0&param1=1&param2=2");
     const searchParams = url.searchParams;
     const seen = [];
-    for (param of searchParams) {
+    for (const param of searchParams) {
         seen.push(param[0]);
         searchParams.delete(param[0]);
     }
diff --git a/test/fixtures/wpt/versions.json b/test/fixtures/wpt/versions.json
index 91bb213072cb92..c2cd318c2b721c 100644
--- a/test/fixtures/wpt/versions.json
+++ b/test/fixtures/wpt/versions.json
@@ -1,46 +1,46 @@
 {
+  "common": {
+    "commit": "3586ff740b00aa1fa82ab00cccbc36cca0bb8ccb",
+    "path": "common"
+  },
   "console": {
     "commit": "3b1f72e99a91d31551edd2147dc7b564eaf25d72",
     "path": "console"
   },
+  "dom/abort": {
+    "commit": "625e1310ce19e9dde25b01f9eda0452c6ec274da",
+    "path": "dom/abort"
+  },
   "encoding": {
-    "commit": "3c9820d1cc5d9d2627c26ef1268b6d54a35adf22",
+    "commit": "35f70910d3753c8b650fdfd4c716caedfefe88c9",
     "path": "encoding"
   },
-  "url": {
-    "commit": "1783c9bccf48c426ec1017f36e1ccfcf90a8af47",
-    "path": "url"
-  },
-  "resources": {
-    "commit": "351a99782b9677706b5dc0dd78e85978fa4ab130",
-    "path": "resources"
+  "FileAPI": {
+    "commit": "3b279420d40afea32506e823f9ac005448f4f3d8",
+    "path": "FileAPI"
   },
-  "interfaces": {
-    "commit": "b4be9a3fdf18459a924f88e49bc55d8b30faa93a",
-    "path": "interfaces"
+  "hr-time": {
+    "commit": "9910784394858a8e34d9eb4e5d00788765abf837",
+    "path": "hr-time"
   },
   "html/webappapis/microtask-queuing": {
     "commit": "2c5c3c4c27d27a419c1fdba3e9879c2d22037074",
     "path": "html/webappapis/microtask-queuing"
   },
   "html/webappapis/timers": {
-    "commit": "264f12bc7bf5db0c6dd064842a5d39ccbf9208c5",
+    "commit": "5873f2d8f1f7bbb9c64689e52d04498614632906",
     "path": "html/webappapis/timers"
   },
-  "hr-time": {
-    "commit": "a5d1774ecf41751d1c9357c27c709ee33bf3e279",
-    "path": "hr-time"
-  },
-  "common": {
-    "commit": "841a51412f236ad63655daca843d3b8e83e0777b",
-    "path": "common"
+  "interfaces": {
+    "commit": "8602e9c9a168c25838bc4b808e3d213ce94ccac1",
+    "path": "interfaces"
   },
-  "dom/abort": {
-    "commit": "7caa3de7471cf19b78ee9efa313c7341a462b5e3",
-    "path": "dom/abort"
+  "resources": {
+    "commit": "e366371a194459f23f8d6115cfdb57e0bc851468",
+    "path": "resources"
   },
-  "FileAPI": {
-    "commit": "3b279420d40afea32506e823f9ac005448f4f3d8",
-    "path": "FileAPI"
+  "url": {
+    "commit": "59d28c8f2d91d12533cfd0371acbe473b1825967",
+    "path": "url"
   }
 }
\ No newline at end of file
diff --git a/test/wpt/status/encoding.json b/test/wpt/status/encoding.json
index 0b2c84051d0e1b..88373a1ee38fc5 100644
--- a/test/wpt/status/encoding.json
+++ b/test/wpt/status/encoding.json
@@ -57,5 +57,16 @@
   },
   "textdecoder-copy.any.js": {
     "fail": "WebAssembly.Memory does not support shared:true"
+  },
+  "legacy-mb-schinese/gbk/gbk-decoder.any.js": {
+    "requires": ["full-icu"],
+    "skip": "The gbk encoding is not supported"
+  },
+  "legacy-mb-schinese/gb18030/gb18030-decoder.any.js": {
+    "requires": ["full-icu"],
+    "skip": "The gb18030 encoding is not supported"
+  },
+  "textdecoder-arguments.any.js": {
+    "fail": "Does not support flushing an incomplete sequence"
   }
 }
diff --git a/test/wpt/status/hr-time.json b/test/wpt/status/hr-time.json
index 12a239ab398107..4910d925b5be94 100644
--- a/test/wpt/status/hr-time.json
+++ b/test/wpt/status/hr-time.json
@@ -1,8 +1,11 @@
 {
-  "window-worker-timeOrigin.window.js": {
-    "fail": "Blob is not defined"
+  "basic.any.js": {
+    "fail": "self.performance.addEventListener is not a function"
   },
   "idlharness.any.js": {
     "skip": "TODO: update IDL parser"
+  },
+  "window-worker-timeOrigin.window.js": {
+    "fail": "depends on URL.createObjectURL(blob)"
   }
-}
\ No newline at end of file
+}
diff --git a/test/wpt/status/html/webappapis/timers.json b/test/wpt/status/html/webappapis/timers.json
index 0967ef424bce67..69d7095511f41c 100644
--- a/test/wpt/status/html/webappapis/timers.json
+++ b/test/wpt/status/html/webappapis/timers.json
@@ -1 +1,14 @@
-{}
+{
+  "negative-settimeout.any.js": {
+    "fail": "assert_unreached: Reached unreachable code"
+  },
+  "type-long-setinterval.any.js": {
+    "fail": "assert_unreached: Reached unreachable code"
+  },
+  "type-long-settimeout.any.js": {
+    "fail": "assert_unreached: Reached unreachable code"
+  },
+  "negative-setinterval.any.js": {
+    "fail": "assert_unreached: Reached unreachable code"
+  }
+}
diff --git a/test/wpt/status/url.json b/test/wpt/status/url.json
index 89601af0ca5814..36b3fd682b37f9 100644
--- a/test/wpt/status/url.json
+++ b/test/wpt/status/url.json
@@ -18,5 +18,11 @@
   },
   "urlsearchparams-constructor.any.js": {
     "fail": "FormData is not defined"
+  },
+  "url-constructor.any.js": {
+    "fail": "TODO: support relative fetch()"
+  },
+  "url-origin.any.js": {
+    "fail": "TODO: support relative fetch()"
   }
 }
diff --git a/test/wpt/test-hr-time.js b/test/wpt/test-hr-time.js
index d77ac0bbc6d9c0..6add176f5fb995 100644
--- a/test/wpt/test-hr-time.js
+++ b/test/wpt/test-hr-time.js
@@ -6,6 +6,9 @@ const { WPTRunner } = require('../common/wpt');
 const runner = new WPTRunner('hr-time');
 
 runner.setInitScript(`
+  const { Blob } = require('buffer');
+  global.Blob = Blob;
+
   const { performance, PerformanceObserver } = require('perf_hooks');
   global.performance = performance;
   global.PerformanceObserver = PerformanceObserver;