<!--
Copyright (c) 2019 The Khronos Group Inc.
Use of this source code is governed by an MIT-style license that can be
found in the LICENSE.txt file.
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- Prevents Chrome from offering to translate tests which generate
     random characters for things like attribute names -->
<meta name="google" value="notranslate">
<meta name="viewport" content="width=device-width">
<title>WebGL Conformance Tests</title>
<style>
  * {
    box-sizing: border-box;
  }

  body {
    border: 0;
    margin: 0;
    padding: 0;
    height: 100%;
    max-height:100%;
    font-family: Verdana, Arial, sans-serif;
    font-size: 0.8em;
  }

  input[type=button], select {
    padding: 2px 6px 2px 6px;
    margin: 0;
    border: 1px solid #888;
    border-radius: 2px;
    background: #f4f4f4;
  }

  a {
    color: #88F;
    text-decoration: none;
  }

  a:hover {
    border-bottom: 1px solid #66D;
  }

  label {
    white-space: nowrap;
  }

  #testlist {
    position:fixed;
    top:180px;
    left:0;
    right: calc(10% + 50px);
    bottom:0px;
    overflow:auto;
    min-height: 200px;
  }

  @media screen and (max-width: 500px) {
    #testlist {
      font-size: 80%;
    }
  }

  #header {
    position:absolute;
    top:0;
    left:0;
    width:100%;
    height: 160px;
    overflow: scroll;
    border-bottom: 1px solid #CCC;
  }

  #info {
    margin: 0 auto;
    max-width: 280px;
  }
  #logo {
    width: 68px;
    height: 40px;
  }

  #iframe-container {
    color: white;
    display: block;
    position: fixed;
    width: 90%;
    height: calc(100% - 170px);
    bottom: 0px;
    left: calc(90% - 50px);
    transition: left 0.15s;
  }
  #iframe-container.iframe-shown {
    left: 10%;
  }
  #iframe-toggle {
    display: inline-block;
    vertical-align: middle;
    width: 20px;
    height: 100%;
    padding: 0;
    -webkit-appearance: none;
  }
  #test-iframe {
    display: inline-block;
    vertical-align: middle;
    background: white;
    width: calc(100% - 20px);
    height: 100%;
    border: 1px solid black;
  }

  .folder {
    margin-bottom: 1.5em;
  }

  .folderHeader {
    white-space: nowrap;
    position: sticky;
    top: 0;
  }
  .folderHeaderInner {
    background: white;
    /* to hide checkboxes from parent headers */
    position: relative;
    left: -2em;
    padding-left: 2em;
  }

  .folderName {
    font-weight: bold;
  }

  .folderMessage {
    margin-left: 1em;
    font-size: 0.9em;
  }

  .pageHeader {
    white-space: nowrap;
  }

  .testpage {
    border-style: solid;
    border-color: #CCC;
    border-width: 0px 0 1px 0;
    background-color: #FFF;
    padding: 4px 0 4px 0;

    -webkit-transition: background-color 0.25s;
    -moz-transition: background-color 0.25s;
    transition: background-color 0.25s;
  }

  .testpage:first-child {
    border-width: 1px 0 1px 0;
  }

  .timeout { }
  .success { }
  .fail { }
  .testpagesuccess { background-color: #8F8; }
  .testpagefail { background-color: #F88; }
  .testpageskipped { background-color: #888; }
  .testpagetimeout { background-color: #FC8; }
  .nowebgl { font-weight: bold; color: red; }
  #error-wrap {
      float: left;
      position: relative;
      left: 50%;
  }
  #error {
     color: red;
     float: left;
     position: relative;
     left: -50%;
     text-align: left;
  }
  ul {
    list-style: none;
    padding-left: 1em;
  }
</style>
<script type="application/javascript" src="js/webgl-test-harness.js"></script>
<script>
"use strict";

window.onbeforeunload = function() {
  // Prompt user before reloading
  return false;
}

var DEFAULT_CONFORMANCE_TEST_VERSION = "2.0.1 (beta)";

var OPTIONS = {
  version: DEFAULT_CONFORMANCE_TEST_VERSION,
  frames: 1,
  allowSkip: 0,
  root: null,
  quiet: 0
};

var testVersions = [
  "1.0.4 (beta)",
  "2.0.1 (beta)"
];

function start() {

  function log(msg) {
    if (window.console && window.console.log) {
      window.console.log(msg);
    }
  }

  function createStylesheet() {
    var style = document.createElement("style");
    style.appendChild(document.createTextNode(""));
    document.head.appendChild(style);
    return style.sheet;
  }

  function create3DContext(canvas, attrs, version) {
    if (!canvas) {
      canvas = document.createElement("canvas");
    }
    var context = null;
    var names;
    switch (version) {
      case 2:
        names = ["webgl2"]; break;
      default:
        names = ["webgl", "experimental-webgl"]; break;
    }
    for (var i = 0; i < names.length; ++i) {
      try {
        context = canvas.getContext(names[i], attrs);
      } catch (e) {
      }
      if (context) {
        break;
      }
    }
    return context;
  }

  var reportType = WebGLTestHarnessModule.TestHarness.reportType;
  var pageCount = 0;
  var folderCount = 0;
  var autoScrollEnabled = true; // Whether the user prefers to auto scroll
  var autoScroll = true; // Whether auto scroll is actually performed
  let quickTestMode = true;

  var Page = function(reporter, folder, testIndex, url) {
    this.reporter = reporter;
    this.folder = folder;
    this.url = url;
    this.totalTests = 0;
    this.totalSuccessful = 0;
    this.totalTimeouts = 0;
    this.totalSkipped = 0;
    this.totalFailed = 0;
    this.testIndex = testIndex;
    this.startTime = new Date();
    this.totalTime = 0;
    var that = this;

    this.elementId = "page" + pageCount++;
    var li = reporter.localDoc.createElement('li');
    li.id = this.elementId;
    var div = reporter.localDoc.createElement('div');
    div.classList.add('pageHeader');
    var check = reporter.localDoc.createElement('input');
    check.type = 'checkbox';
    check.checked = true;
    check.onclick = function() {
      if (this.checked) {
        that.folder.enableUp_();
      }
      else {
        that.folder.disableUp_();
      }
    };
    div.appendChild(check);
    var button = reporter.localDoc.createElement('input');
    button.type = 'button';
    button.id = this.elementId + "-button";
    button.value = 'run';
    button.onclick = function() {
      autoScroll = false;
      reporter.runTest(url);
    };
    if (reporter.noSelectedWebGLVersion) {
      button.disabled = true;
    }
    div.appendChild(button);
    var a = reporter.localDoc.createElement('a');
    a.href = WebGLTestHarnessModule.getURLWithOptions(url, {
      webglVersion: reporter.selectedWebGLVersion,
      quiet: OPTIONS.quiet,
      quick: quickTestMode ? 1 : 0,
    });
    a.target = "_blank";
    const folderName = that.folder.displayName;
    console.assert(folderName.startsWith("all/"));
    console.assert(url.startsWith(folderName.substring(4) + "/"));
    const urlWithoutFolder = url.substring(folderName.length - 4 + 1);
    var node = reporter.localDoc.createTextNode(urlWithoutFolder);
    a.appendChild(node);
    div.appendChild(a);
    li.setAttribute('class', 'testpage');
    li.appendChild(div);
    var ul = reporter.localDoc.createElement('ul');
    var node = reporter.localDoc.createTextNode('');
    li.appendChild(ul);
    div.appendChild(node);
    this.totalsElem = node;
    this.resultElem = ul;
    this.elem = li;
    this.check = check;
  };

  Page.prototype.checked = function() {
    return this.check.checked;
  }

  Page.prototype.addResult = function(msg, success, skipped) {
    ++this.totalTests;
    if (success === undefined) {
      ++this.totalTimeouts;
      var result = "timeout";
      var css = "timeout";
    } else if (success) {
      ++this.totalSuccessful;
      // don't report success.
      return;
    } else {
      ++this.totalFailed;
      if (skipped) {
        // Skipped tests are counted as both skips and failures (because we
        // don't want to accidentally accept a conformance submission with
        // skipped tests).
        ++this.totalSkipped;
      }
      var result = "failed";
      var css = "fail";
    }

    var node = this.reporter.localDoc.createTextNode(result + ': ' + msg);
    var li = this.reporter.localDoc.createElement('li');
    li.appendChild(node);
    li.setAttribute('class', css);
    this.resultElem.appendChild(li);
  };

  Page.prototype.startPage = function() {
    if (autoScroll && this.elem.scrollIntoView) {
      this.elem.scrollIntoView(false);
    }
    this.totalTests = 0;
    this.totalSuccessful = 0;
    this.totalSkipped = 0;
    this.totalFailed = 0;
    this.totalTimeouts = 0;
    this.totalTime = 0;
    // remove previous results.
    while (this.resultElem.hasChildNodes()) {
      this.resultElem.removeChild(this.resultElem.childNodes[0]);
    }
    this.totalsElem.textContent = '';

    var shouldRun = this.check.checked && this.folder.checked();

    if (shouldRun) {
      this.elem.classList.remove('testpagetimeout');
      this.elem.classList.remove('testpageskipped');
      this.elem.classList.remove('testpagefail');
      this.elem.classList.remove('testpagesuccess');
      this.startTime = Date.now();
    }

    return this.check.checked && this.folder.checked();
  };

  Page.prototype.firstTestIndex = function() {
    return this.testIndex;
  };

  Page.prototype.finishPage = function(success) {
    var shouldRun = this.check.checked && this.folder.checked();
    if (shouldRun) {
      this.totalTime = Date.now() - this.startTime;
    } else {
      this.totalTime = 0;
    }

    var passedMsg = ' (Passed: ' + this.totalSuccessful + '/' + this.totalTests;
    var skippedMsg = '';
    if (this.totalSkipped > 0) {
      skippedMsg = ' Skipped: ' + this.totalSkipped + '/' + this.totalTests;
    }
    var failedMsg = '';
    if (this.totalFailed > 0) {
      failedMsg = ' Failed: ' + this.totalFailed + '/' + this.totalTests;
    }
    var timeoutMsg = '';
    if (this.totalTimeouts > 0) {
      timeoutMsg = ' Timeout: ' + this.totalTimeouts + '/' + this.totalTests;
    }
    var msg = passedMsg + skippedMsg + failedMsg + timeoutMsg + ' in ' + this.totalTime.toFixed(1) + ' ms)';

    if (success === undefined) {
      var css = 'testpagetimeout';
      msg = '(*timeout*)';
      ++this.totalTests;
      ++this.totalTimeouts;
    } else if (this.totalSkipped) {
      var css = 'testpageskipped';
    } else if (this.totalSuccessful != this.totalTests) {
      var css = 'testpagefail';
    } else {
      var css = 'testpagesuccess';
    }
    this.elem.classList.add(css);
    this.totalsElem.textContent = msg;
    this.folder.pageFinished(this, success);
  };

  Page.prototype.enableTest = function(re) {
    if (this.url.match(re)) {
      this.check.checked = true;
      this.folder.enableUp_();
    }
  };

  Page.prototype.disableTest = function(re) {
    if (this.url.match(re)) {
      this.check.checked = false;
    }
  };

  Page.prototype.toJSON = function() {
    return {
      'subtests': this.totalTests,
      'successful': this.totalSuccessful,
      'skipped': this.totalSkipped,
      'failed': this.totalFailed,
      'timedOut': this.totalTimeouts,
      'totalTime': this.totalTime,
    };
  };


  var Folder = function(reporter, folder, depth, opt_name) {
    this.reporter = reporter;
    this.depth = depth;
    this.name = opt_name || "";
    this.displayName = this.name;
    if (folder && folder.displayName) {
      this.displayName = folder.displayName + '/' + this.displayName;
    }
    this.subFolders = {};
    this.pages = [];
    this.items = [];
    this.folder = folder;
    this.cachedTotalTime = 0;
    this.cachedTotalSuccessful = 0;
    this.cachedTotalSkipped = 0;
    this.cachedTotalTimeouts = 0;
    this.cachedTotalTests = 0;
    var that = this;

    var doc = reporter.localDoc;
    this.elementId = "folder" + folderCount++;
    var li = doc.createElement('li');
    li.id = this.elementId;
    li.classList.add("folder");
    var folderHeader = doc.createElement('div');
    folderHeader.classList.add('folderHeader');
    var folderHeaderInner = doc.createElement('div');
    folderHeaderInner.classList.add('folderHeaderInner');
    folderHeader.appendChild(folderHeaderInner);
    var check = doc.createElement('input');
    check.type = 'checkbox';
    check.checked = true;
    check.onclick = function() {
      if (this.checked) {
        that.enableTest(".*");
      }
      else {
        that.disableTest(".*", true);
      }
    };
    folderHeaderInner.appendChild(check);
    var button = doc.createElement('input');
    button.type = 'button';
    button.value = 'run';
    button.onclick = function() {
      autoScroll = autoScrollEnabled;
      that.run();
    };
    if (reporter.noSelectedWebGLVersion) {
      button.disabled = true;
    }
    folderHeaderInner.appendChild(button);
    var h = doc.createElement('span');
    h.classList.add('folderName');
    h.appendChild(doc.createTextNode(this.displayName));
    folderHeaderInner.appendChild(h);
    var m = doc.createElement('span');
    m.classList.add('folderMessage');
    this.msgNode = doc.createTextNode('');
    m.appendChild(this.msgNode);
    folderHeaderInner.appendChild(m);
    var ul = doc.createElement('ul');
    li.appendChild(folderHeader);
    li.appendChild(ul);
    this.childUL = ul;
    this.elem = li;
    this.check = check;
    this.folderHeader = folderHeader;
  };

  Folder.prototype.checked = function() {
    return this.check.checked &&
        (this.folder ? this.folder.checked() : true);
  };

  Folder.prototype.firstTestIndex = function() {
    return this.items[0].firstTestIndex();
  };

  Folder.prototype.numChildren = function() {
    var numChildren = 0;
    for (var name in this.subFolders) {
      numChildren += this.subFolders[name].numChildren();
    }
    return numChildren + this.pages.length;
  };

  Folder.prototype.totalTime = function() {
    // Check to see if the cached total time needs to be recomputed
    if (this.cachedTotalTime == -1) {
      this.cachedTotalTime = 0;
      for (var name in this.subFolders) {
        this.cachedTotalTime += this.subFolders[name].totalTime();
      }
      for (var ii = 0; ii < this.pages.length; ++ii) {
        this.cachedTotalTime += this.pages[ii].totalTime;
      }
    }
    return this.cachedTotalTime;
  };

  Folder.prototype.totalSuccessful = function() {
    if (this.cachedTotalSuccessful == -1) {
      this.cachedTotalSuccessful = 0;
      for (var name in this.subFolders) {
        this.cachedTotalSuccessful += this.subFolders[name].totalSuccessful();
      }
      for (var ii = 0; ii < this.pages.length; ++ii) {
        this.cachedTotalSuccessful += this.pages[ii].totalSuccessful;
      }
    }
    return this.cachedTotalSuccessful;
  };

  Folder.prototype.totalSkipped = function() {
    if (this.cachedTotalSkipped == -1) {
      this.cachedTotalSkipped = 0;
      for (var name in this.subFolders) {
        this.cachedTotalSkipped += this.subFolders[name].totalSkipped();
      }
      for (var ii = 0; ii < this.pages.length; ++ii) {
        this.cachedTotalSkipped += this.pages[ii].totalSkipped;
      }
    }
    return this.cachedTotalSkipped;
  };

  Folder.prototype.totalFailed = function() {
    if (this.cachedTotalFailed == -1) {
      this.cachedTotalFailed = 0;
      for (var name in this.subFolders) {
        this.cachedTotalFailed += this.subFolders[name].totalFailed();
      }
      for (var ii = 0; ii < this.pages.length; ++ii) {
        this.cachedTotalFailed += this.pages[ii].totalFailed;
      }
    }
    return this.cachedTotalFailed;
  };

  Folder.prototype.totalTimeouts = function() {
    if (this.cachedTotalTimeouts == -1) {
      this.cachedTotalTimeouts = 0;
      for (var name in this.subFolders) {
        this.cachedTotalTimeouts += this.subFolders[name].totalTimeouts();
      }
      for (var ii = 0; ii < this.pages.length; ++ii) {
        this.cachedTotalTimeouts += this.pages[ii].totalTimeouts;
      }
    }
    return this.cachedTotalTimeouts;
  };

  Folder.prototype.totalTests = function() {
    if (this.cachedTotalTests == -1) {
      this.cachedTotalTests = 0;
      for (var name in this.subFolders) {
        this.cachedTotalTests += this.subFolders[name].totalTests();
      }
      for (var ii = 0; ii < this.pages.length; ++ii) {
        this.cachedTotalTests += this.pages[ii].totalTests;
      }
    }
    return this.cachedTotalTests;
  };

  Folder.prototype.run = function() {
    this.msgNode.textContent = '';
    var firstTestIndex = this.firstTestIndex();
    var count = this.numChildren();
    log("run tests: " + firstTestIndex + " to " + (firstTestIndex + count - 1))
    testHarness.runTests({start: firstTestIndex, count: count});
  };

  Folder.prototype.pageFinished = function(page, success) {
    this.cachedTotalTime = -1;
    this.cachedTotalSuccessful = -1;
    this.cachedTotalSkipped = -1;
    this.cachedTotalFailed = -1;
    this.cachedTotalTimeouts = -1;
    this.cachedTotalTests = -1;
    var passedMsg = ' (Passed: ' + this.totalSuccessful() + '/' + this.totalTests();
    var skippedMsg = '';
    if (this.totalSkipped() > 0) {
      skippedMsg = ' Skipped: ' + this.totalSkipped() + '/' + this.totalTests();
    }
    var failedMsg = '';
    if (this.totalFailed() > 0) {
      failedMsg = ' Failed: ' + this.totalFailed() + '/' + this.totalTests();
    }
    var timeoutMsg = '';
    if (this.totalTimeouts() > 0) {
      timeoutMsg = ' Timeout: ' + this.totalTimeouts() + '/' + this.totalTests();
    }
    this.msgNode.textContent = passedMsg + skippedMsg + failedMsg + timeoutMsg + ' in ' + (this.totalTime() / 1000).toFixed(2) + ' seconds)';
    if (this.folder) {
      this.folder.pageFinished(page, success);
    }
  };

  Folder.prototype.getSubFolder = function(name) {
    var subFolder = this.subFolders[name];
    if (subFolder === undefined) {
      subFolder = new Folder(this.reporter, this, this.depth + 1, name);
      this.subFolders[name] = subFolder;
      this.items.push(subFolder);
      this.childUL.appendChild(subFolder.elem);
    }
    return subFolder;
  };

  Folder.prototype.getOrCreateFolder = function(url) {
    var parts = url.split('/');
    var folder = this;
    for (var pp = 0; pp < parts.length - 1; ++pp) {
      folder = folder.getSubFolder(parts[pp]);
    }
    return folder;
  };

  Folder.prototype.addPage = function(page) {
    this.pages.push(page);
    this.items.push(page);
    this.childUL.appendChild(page.elem);
    this.folderHeader.classList.add('hasPages');
  };

  Folder.prototype.disableTest = function(re, opt_forceRecurse) {
    var recurse = true;
    if (this.name.match(re)) {
      this.check.checked = false;
      recurse = opt_forceRecurse;
    }
    if (recurse) {
      for (var name in this.subFolders) {
        this.subFolders[name].disableTest(re, opt_forceRecurse);
      }
      for (var ii = 0; ii < this.pages.length; ++ii) {
        this.pages[ii].disableTest(re);
      }
    }
  };

  Folder.prototype.enableUp_ = function() {
    this.check.checked = true;
    var parent = this.folder;
    if (parent) {
      parent.enableUp_();
    }
  }

  Folder.prototype.disableUp_ = function() {
    var checked = false;
    for (var name in this.subFolders) {
      checked = this.subFolders[name].checked();
      if (checked) {
        break;
      }
    }
    for (var ii = 0; ii < this.pages.length && checked == false; ++ii) {
      checked = this.pages[ii].checked();
    }
    this.check.checked = checked;
    var parent = this.folder;
    if (parent) {
      parent.disableUp_();
    }
  }

  Folder.prototype.enableTest = function(re) {
    if (this.name.match(re)) {
      this.enableUp_();
    }
    for (var name in this.subFolders) {
      this.subFolders[name].enableTest(re);
    }
    for (var ii = 0; ii < this.pages.length; ++ii) {
      this.pages[ii].enableTest(re);
    }
  };

  var Reporter = function(iframes) {
    this.localDoc = document;
    this.resultElem = document.getElementById("results");
    this.fullResultsElem = document.getElementById("fullresults");
    var node = this.localDoc.createTextNode('');
    this.fullResultsElem.appendChild(node);
    this.fullResultsNode = node;
    this.iframes = iframes;
    this.currentPageElem = null;
    this.totalPages = 0;
    this.pagesByURL = {};

    // Check to see if WebGL is supported
    var canvas = document.createElement("canvas");
    var ctx = create3DContext(canvas, null, 1);

    // Check to see if WebGL2 is supported
    var canvas2 = document.createElement("canvas");
    var ctx2 = create3DContext(canvas2, null, 2);

    this.noSelectedWebGLVersion = false;
    this.selectedWebGLVersion = WebGLTestHarnessModule.getMajorVersion(OPTIONS.version);
    if (this.selectedWebGLVersion == 2 && !ctx2) {
        this.noSelectedWebGLVersion = true;
    } else if (this.selectedWebGLVersion == 1 && !ctx) {
        this.noSelectedWebGLVersion = true;
    }

    // If the WebGL2 context could be created use it to get context info
    if (ctx2) {
      ctx = ctx2;
    }

    this.noWebGL = !ctx;

    this.contextInfo = {};
    this.root = new Folder(this, null, 0, "all");
    this.resultElem.appendChild(this.root.elem);
    this.callbacks = { };
    this.startTime = new Date();

    if (ctx) {
      this.contextInfo["VENDOR"] = ctx.getParameter(ctx.VENDOR);
      this.contextInfo["VERSION"] = ctx.getParameter(ctx.VERSION);
      this.contextInfo["RENDERER"] = ctx.getParameter(ctx.RENDERER);
      this.contextInfo["RED_BITS"] = ctx.getParameter(ctx.RED_BITS);
      this.contextInfo["GREEN_BITS"] = ctx.getParameter(ctx.GREEN_BITS);
      this.contextInfo["BLUE_BITS"] = ctx.getParameter(ctx.BLUE_BITS);
      this.contextInfo["ALPHA_BITS"] = ctx.getParameter(ctx.ALPHA_BITS);
      this.contextInfo["DEPTH_BITS"] = ctx.getParameter(ctx.DEPTH_BITS);
      this.contextInfo["STENCIL_BITS"] = ctx.getParameter(ctx.STENCIL_BITS);

      var ext = ctx.getExtension("WEBGL_debug_renderer_info");
      if (ext) {
        this.contextInfo["UNMASKED_VENDOR"] = ctx.getParameter(ext.UNMASKED_VENDOR_WEBGL);
        this.contextInfo["UNMASKED_RENDERER"] = ctx.getParameter(ext.UNMASKED_RENDERER_WEBGL);
      }
    }
  };

  Reporter.prototype.enableTest = function(name) {
    this.root.enableTest(name);
  };

  Reporter.prototype.disableTest = function(name) {
    this.root.disableTest(name);
  };

  Reporter.prototype.disableAllTests = function() {
    this.root.disableTest(".*", true);
  };

  Reporter.prototype.addEventListener = function(type, func) {
    if (!this.callbacks[type]) {
      this.callbacks[type] = [];
    }
    this.callbacks[type].push(func);
  };

  Reporter.prototype.executeListenerEvents_ = function(type) {
    var callbacks = this.callbacks[type].slice(0);
    for (var ii = 0; ii < callbacks.length; ++ii) {
      setTimeout(callbacks[ii], 0);
    }
  };

  Reporter.prototype.runTest = function(url) {
    var page = this.pagesByURL[url];
    testHarness.runTests({start: page.firstTestIndex(), count: 1});
  };

  Reporter.prototype.getFolder = function(url) {
    return this.root.getOrCreateFolder(url);
  };

  Reporter.prototype.addPage = function(url) {
    var folder = this.getFolder(url);
    var page = new Page(this, folder, this.totalPages, url);
    folder.addPage(page);
    ++this.totalPages;
    this.pagesByURL[url] = page;
  };

  Reporter.prototype.startPage = function(url) {
    var page = this.pagesByURL[url];
    return page.startPage();
  };

  Reporter.prototype.addResult = function(url, msg, success, skipped) {
    var page = this.pagesByURL[url];
    page.addResult(msg, success, skipped);
  };

  Reporter.prototype.finishPage = function(url, success) {
    var page = this.pagesByURL[url];
    page.finishPage(success);
    if (OPTIONS.dumpShaders == 1) {
      var xhr = new XMLHttpRequest();
      xhr.open('POST', "/finishIndividualTest", true);
      xhr.send(null);
    }
  };

  Reporter.prototype.displayFinalResults = function(msg, success) {
    if (success) {
      var totalTests = 0;
      var testsSucceeded = 0;
      var testsFailed = 0;
      var testsSkipped = 0;
      var testsTimedOut = 0;

      var subtestsHit = 0;
      var subtestsSucceeded = 0;
      var subtestsTimedOut = 0;
      var subtestsSkipped = 0;
      var subtestsFailed = 0;

      var totalTime = Date.now() - this.startTime;

      for (var url in this.pagesByURL) {
        var page = this.pagesByURL[url];
        totalTests += 1;
        if (page.totalSkipped) {
          testsSkipped += 1;
        }
        if (page.totalFailed) {
          testsFailed += 1;
        } else if (page.totalTimeouts) {
          testsTimedOut += 1;
        } else if (page.totalSuccessful) {
          if (page.totalSuccessful != page.totalTests)
            throw successes_not_equal_total;
          testsSucceeded += 1;
        }

        subtestsHit += page.totalTests;
        subtestsSucceeded += page.totalSuccessful;
        subtestsTimedOut += page.totalTimeouts;
        subtestsSkipped += page.totalSkipped;
        subtestsFailed += page.totalFailed;
      }

      function ratio_str(x, y, name) {
        return x + '/' + y + ' ' + name + ' (' + (x / y * 100).toFixed(2) + '%)';
      }
      var testsSucceededRatio = ratio_str(testsSucceeded, totalTests, 'tests');
      var passedMsg = 'Passed ' + testsSucceededRatio + ', ' +
                      ratio_str(subtestsSucceeded, subtestsHit, 'subtests');
      var skippedMsg = '';
      if (testsSkipped > 0) {
        skippedMsg = ' Skipped ' + ratio_str(testsSkipped, totalTests, 'tests');
      }
      var failedMsg = '';
      if (testsFailed > 0) {
        failedMsg = ' Failed ' + ratio_str(testsFailed, totalTests, 'tests') + ', ' +
                    ratio_str(subtestsFailed, subtestsHit, 'subtests');
      }
      var timeoutMsg = '';
      if (testsTimedOut > 0) {
        timeoutMsg = ' Timeout ' + ratio_str(testsTimedOut, totalTests, 'tests');
      }
      var msg = passedMsg + skippedMsg + failedMsg + timeoutMsg;
      this.fullResultsNode.textContent = msg;

      // generate a text summary
      var tx = "";
      tx += "WebGL Conformance Test Results\n";
      tx += "Version " + OPTIONS.version + "\n";
      tx += "Generated on: " + (new Date()).toString() + "\n";
      tx += "\n";
      tx += "-------------------\n\n";
      tx += "User Agent: " + (navigator.userAgent ? navigator.userAgent : "(navigator.userAgent is null)") + "\n";
      tx += "WebGL VENDOR: " + this.contextInfo["VENDOR"] + "\n";
      tx += "WebGL VERSION: " + this.contextInfo["VERSION"] + "\n";
      tx += "WebGL RENDERER: " + this.contextInfo["RENDERER"] + "\n";
      tx += "Unmasked VENDOR: " + this.contextInfo["UNMASKED_VENDOR"] + "\n";
      tx += "Unmasked RENDERER: " + this.contextInfo["UNMASKED_RENDERER"] + "\n";
      tx += "WebGL R/G/B/A/Depth/Stencil bits (default config): " + this.contextInfo["RED_BITS"] + "/" + this.contextInfo["GREEN_BITS"] + "/" + this.contextInfo["BLUE_BITS"] + "/" + this.contextInfo["ALPHA_BITS"] + "/" + this.contextInfo["DEPTH_BITS"] + "/" + this.contextInfo["STENCIL_BITS"] + "\n";
      tx += "\n-------------------\n\n";

      var result;
      if (totalTests && testsSucceeded == totalTests) {
        result = 'PASS';
      } else {
        result = 'FAIL';
      }
      tx += "Test Summary: " + result + " (" + totalTests + " tests):\n";
      tx += subtestsHit + " subtests ran in " + (totalTime / 1000.0).toFixed(2) + " seconds\n";
      function record(what, tests, subtests) {
        tx += what + ": " + tests + " tests, " + subtests + " subtests\n";
      }
      record('PASSED', testsSucceeded, subtestsSucceeded);
      record('NOT PASSED', totalTests - testsSucceeded, subtestsHit - subtestsSucceeded);

      record('FAILED', testsFailed, subtestsFailed);
      record('TIMED OUT', testsTimedOut, subtestsTimedOut);
      record('SKIPPED', testsSkipped, subtestsSkipped);

      tx += "\n-------------------\n\n";

      const failureLines = [];
      const timeoutLines = [];
      const resultLines = [];

      for (var url in this.pagesByURL) {
        var page = this.pagesByURL[url];
        resultLines.push('    "' + url + '":' + JSON.stringify(page.toJSON()));

        if (page.totalFailed) {
          failureLines.push('    "' + url + '",');
        }
        if (page.totalTimeouts) {
          timeoutLines.push('    "' + url + '",');
        }
      }

      const lines = [].concat(
        [
          '{',
          '  "failures": [',
        ],
        failureLines,
        [
          '  ],',
          '  "timeouts": [',
        ],
        timeoutLines,
        [
          '  ],',
          '  "results": {',
        ],
        resultLines,
        [
        '  },',
        '}',
        ]
      );

      tx += lines.join('\n');

      var r = document.getElementById("testResultsAsText");
      while (r.firstChild) r.removeChild(r.firstChild);
      r.appendChild(document.createTextNode(tx));
      document.getElementById("showTextSummary").disabled = false;
      document.getElementById("dlTextSummary").disabled = false;

      this.postResultsToServer(tx);
    } else {
      var e = document.getElementById("error");
      e.innerHTML = msg;
      this.postResultsToServer(msg);
    }
  };

  Reporter.prototype.postTestStartToServer = function(resultText) {
    this.startTime = Date.now();
    if(OPTIONS.postResults == undefined || OPTIONS.postResults == 0) {
      return;
    }

    var xhr = new XMLHttpRequest();
    xhr.open('POST', "/start", true);
    xhr.send(null);
  };

  Reporter.prototype.postResultsToServer = function(resultText) {
    if(OPTIONS.postResults == undefined || OPTIONS.postResults == 0) {
      return;
    }

    var xhr = new XMLHttpRequest();
    xhr.open('POST', "/finish", true);
    xhr.setRequestHeader("Content-Type", "text/plain");
    xhr.send(resultText);
  };

  Reporter.prototype.ready = function() {
    var loading = document.getElementById("loading");
    loading.style.display = "none";
    if (!this.noSelectedWebGLVersion) {
      var button = document.getElementById("runTestsButton");
      button.disabled = false;
      this.executeListenerEvents_("ready");
    }
  };

  Reporter.prototype.reportFunc = function(type, url, msg, success, skipped) {
    switch (type) {
      case reportType.ADD_PAGE:
        return this.addPage(msg);
      case reportType.READY:
        return this.ready();
      case reportType.START_PAGE:
        return this.startPage(url);
      case reportType.TEST_RESULT:
        return this.addResult(url, msg, success, skipped);
      case reportType.FINISH_PAGE:
        return this.finishPage(url, success);
      case reportType.FINISHED_ALL_TESTS:
        return this.displayFinalResults(msg, success);
      default:
        throw 'unhandled';
        break;
    };
  };

  var getURLOptions = function(obj) {
    var s = window.location.href;
    var q = s.indexOf("?");
    var e = s.indexOf("#");
    if (e < 0) {
      e = s.length;
    }
    var query = s.substring(q + 1, e);
    var pairs = query.split("&");
    for (var ii = 0; ii < pairs.length; ++ii) {
      var keyValue = pairs[ii].split("=");
      var key = keyValue[0];
      var value = decodeURIComponent(keyValue[1]);
      obj[key] = value;
    }
  };

  getURLOptions(OPTIONS);

  var makeVersionSelect = function(currentVersion) {
    var versionSelect = document.getElementById("testVersion");
    var foundCurrentVersion = false;
    var numericCurrentVersion = currentVersion.replace(/[^\d.]/g, '');

    for (var i in testVersions) {
      var version = testVersions[i];
      var numericVersion = version.replace(/[^\d.]/g, '');
      var option = document.createElement("option");
      option.setAttribute('value', numericVersion);
      option.innerHTML = version;

      if (numericVersion == numericCurrentVersion) {
        foundCurrentVersion = true;
        option.selected = true;
      }

      versionSelect.appendChild(option);
    }

    // If the version requested by the query string isn't in the list add it.
    if (!foundCurrentVersion) {
      var option = document.createElement("option");
      option.setAttribute('value', numericCurrentVersion);
      option.innerHTML = currentVersion + " (unknown)";
      option.selected = true;

      versionSelect.appendChild(option);
    }

    versionSelect.addEventListener('change', function(ev) {
      window.location.href = "?version=" + versionSelect.value;
    }, false);
  }

  makeVersionSelect(OPTIONS.version);

  // Make iframes
  var iframes = [document.getElementById("test-iframe")];

  var testPath = "00_test_list.txt";
  if (OPTIONS.root) {
    testPath = OPTIONS.root + "/" + testPath;
  }

  var reporter = new Reporter(iframes);
  var testHarness = new WebGLTestHarnessModule.TestHarness(
      iframes,
      testPath,
      function(type, url, msg, success, skipped) {
        return reporter.reportFunc(type, url, msg, success, skipped);
      },
      OPTIONS);
  reporter.addEventListener("ready", function() {
    // Set which tests to include.
    if (OPTIONS.include) {
      reporter.disableAllTests();
      var includes = OPTIONS.include.split(",")
      for (var ii = 0; ii < includes.length; ++ii) {
        reporter.enableTest(new RegExp(includes[ii]));
      }
    }
    // Remove tests based on skip=re1,re2 in URL.
    if (OPTIONS.skip) {
      var skips = OPTIONS.skip.split(",")
      for (var ii = 0; ii < skips.length; ++ii) {
        reporter.disableTest(new RegExp(skips[ii]));
      }
    }
    // Auto run the tests if the run=1 in URL
    if (OPTIONS.run != undefined && OPTIONS.run != 0) {
      reporter.postTestStartToServer();
      testHarness.runTests();
    }
  });
  window.webglTestHarness = testHarness;
  var button = document.getElementById("runTestsButton");
  button.disabled = true;
  button.onclick = function() {
    autoScroll = autoScrollEnabled;
    reporter.postTestStartToServer();
    testHarness.runTests();
  };
  var autoScrollCheckbox = document.getElementById("autoScrollCheckbox");
  autoScrollCheckbox.checked = autoScrollEnabled;
  autoScrollCheckbox.onclick = function() {
    autoScrollEnabled = autoScrollCheckbox.checked;
    autoScroll = autoScrollEnabled;
  };

  var hidePassedSheet = createStylesheet();
  var hidePassedCheckbox = document.getElementById("hidePassedCheckbox");
  hidePassedCheckbox.checked = false;
  hidePassedCheckbox.onclick = function() {
    var hidePassedTests = hidePassedCheckbox.checked;
    if (hidePassedTests) {
        hidePassedSheet.insertRule(".testpagesuccess { display: none; }", 0);
    } else {
        hidePassedSheet.deleteRule(0);
    }
  };

  var quickTestModeCheckbox = document.getElementById("quickTestModeCheckbox");
  quickTestModeCheckbox.checked = quickTestMode;
  quickTestModeCheckbox.onclick = function() {
    quickTestMode = quickTestModeCheckbox.checked;
  };

  var textbutton = document.getElementById("showTextSummary");
  textbutton.onclick = function() {
    log("click");
    var htmldiv = document.getElementById("testResultsHTML");
    var textdiv = document.getElementById("testResultsText");
    if (textdiv.style.display == "none") {
      textdiv.style.display = "block";
      htmldiv.style.display = "none";
      textbutton.setAttribute("value", "display html summary");
    } else {
      textdiv.style.display = "none";
      htmldiv.style.display = "block";
      textbutton.setAttribute("value", "display text summary");
    }
  };

  function download(filename, text) {
    var element = document.createElement("a");
    element.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(text));
    element.setAttribute("download", filename);
    element.style.display = "none";
    document.body.appendChild(element);
    element.click();
    document.body.removeChild(element);
  }
  var dltextbutton = document.getElementById("dlTextSummary");
  dltextbutton.onclick = function() {
    var textdiv = document.getElementById("testResultsText");
    download("webgl-conformance-" + OPTIONS.version + ".txt", textdiv.innerText.trim());
  };

  if (reporter.noSelectedWebGLVersion) {
    button.disabled = true;
  }
  if (reporter.noWebGL) {
    var elem = document.getElementById("nowebgl");
    elem.style.display = "";
    reporter.postResultsToServer("Browser does not appear to support WebGL");
  } else if (reporter.noSelectedWebGLVersion) {
    var elem = document.getElementById("noselectedwebgl");
    elem.style.display = "";
    reporter.postResultsToServer("Browser does not appear to support the selected version of WebGL");
  }

  const iframeContainer = document.getElementById("iframe-container");
  const iframeToggle = document.getElementById("iframe-toggle");
  iframeToggle.value = iframeToggle.getAttribute("data-value-hidden");
  iframeToggle.onclick = function() {
    const expanded = iframeToggle.myExpanded = !iframeToggle.myExpanded;
    if (expanded) {
      iframeContainer.classList.add("iframe-shown");
      iframeToggle.value = iframeToggle.getAttribute("data-value-shown");
    } else {
      iframeContainer.classList.remove("iframe-shown");
      iframeToggle.value = iframeToggle.getAttribute("data-value-hidden");
    }
  };
}
</script>
</head>
<body onload="start()">

<div id="testlist">
  <div id="testResultsHTML">
    <ul id="results">
    </ul>
  </div>
  <div style="display: none;" id="testResultsText">
    <pre id="testResultsAsText"></pre>
  </div>
</div> <!-- end of container -->

<div id="iframe-container">
  <input type="button" data-value-hidden="◄" data-value-shown="►" id="iframe-toggle" aria-hidden="true"
  ><iframe id="test-iframe"></iframe>
</div>

<div id="header">
  <div id="info">
    <div style="text-align:center">
      <img src="resources/webgl-logo.png" alt="WebGL" id="logo"/>
      <br/>
      Conformance Test Runner
    </div>
    Version
    <select id="testVersion">
    </select>
    <a href="../../conformance-suites/">(older versions?)</a>
    <br/>
    <input type="button" value="run tests" id="runTestsButton"/>
    <label for="autoScrollCheckbox"><input type="checkbox" id="autoScrollCheckbox"/>auto scroll</label>
    <label for="hidePassedCheckbox"><input type="checkbox" id="hidePassedCheckbox"/>hide passed</label>
    <label for="quickTestModeCheckbox"><input type="checkbox" id="quickTestModeCheckbox"/>quick test mode</label>
    <br/>
    <input type="button" disabled value="show text summary" id="showTextSummary"/>
    <input type="button" disabled value="download text" id="dlTextSummary"/>
    <div id="nowebgl" class="nowebgl" style="display: none;">
      This browser does not appear to support WebGL
    </div>
    <div id="noselectedwebgl" class="nowebgl" style="display: none;">
      This browser does not appear to support the selected version of WebGL
    </div>
    <div id="loading">
      Loading Tests...
    </div>
    <div id="fullresults">
    </div>
  </div>
  <div id="error-wrap">
    <pre id="error"></pre>
  </div>
</div> <!-- end of header -->

</body>
</html>