/* -*- js-indent-level: 4; indent-tabs-mode: nil -*- */
/*
 * e10s event dispatcher from content->chrome
 *
 * type = eventName (QuitApplication)
 * data = json object {"filename":filename} <- for LoggerInit
 */

// This file expects the following files to be loaded.
/* import-globals-from LogController.js */
/* import-globals-from MemoryStats.js */
/* import-globals-from MozillaLogger.js */

/* eslint-disable no-unsanitized/property */

"use strict";

const { StructuredLogger, StructuredFormatter } =
  SpecialPowers.ChromeUtils.importESModule(
    "resource://testing-common/StructuredLog.sys.mjs"
  );

function getElement(id) {
  return typeof id == "string" ? document.getElementById(id) : id;
}

this.$ = this.getElement;

function contentDispatchEvent(type, data, sync) {
  if (typeof data == "undefined") {
    data = {};
  }

  var event = new CustomEvent("contentEvent", {
    bubbles: true,
    detail: {
      sync,
      type,
      data: JSON.stringify(data),
    },
  });
  document.dispatchEvent(event);
}

function contentAsyncEvent(type, data) {
  contentDispatchEvent(type, data, 0);
}

/* Helper Function */
function extend(obj, /* optional */ skip) {
  // Extend an array with an array-like object starting
  // from the skip index
  if (!skip) {
    skip = 0;
  }
  if (obj) {
    var l = obj.length;
    var ret = [];
    for (var i = skip; i < l; i++) {
      ret.push(obj[i]);
    }
  }
  return ret;
}

function flattenArguments(/* ...*/) {
  var res = [];
  var args = extend(arguments);
  while (args.length) {
    var o = args.shift();
    if (o && typeof o == "object" && typeof o.length == "number") {
      for (var i = o.length - 1; i >= 0; i--) {
        args.unshift(o[i]);
      }
    } else {
      res.push(o);
    }
  }
  return res;
}

function testInXOriginFrame() {
  // Check if the test running in an iframe is a cross origin test.
  try {
    $("testframe").contentWindow.origin;
    return false;
  } catch (e) {
    return true;
  }
}

function testInDifferentProcess() {
  // Check if the test running in an iframe that is loaded in a different process.
  return SpecialPowers.Cu.isRemoteProxy($("testframe").contentWindow);
}

/**
 * TestRunner: A test runner for SimpleTest
 * TODO:
 *
 *  * Avoid moving iframes: That causes reloads on mozilla and opera.
 *
 *
 **/
var TestRunner = {};
TestRunner.logEnabled = false;
TestRunner._currentTest = 0;
TestRunner._lastTestFinished = -1;
TestRunner._loopIsRestarting = false;
TestRunner.currentTestURL = "";
TestRunner.originalTestURL = "";
TestRunner._urls = [];
TestRunner._lastAssertionCount = 0;
TestRunner._expectedMinAsserts = 0;
TestRunner._expectedMaxAsserts = 0;

TestRunner.timeout = 300 * 1000; // 5 minutes.
TestRunner.maxTimeouts = 4; // halt testing after too many timeouts
TestRunner.runSlower = false;
TestRunner.dumpOutputDirectory = "";
TestRunner.dumpAboutMemoryAfterTest = false;
TestRunner.dumpDMDAfterTest = false;
TestRunner.slowestTestTime = 0;
TestRunner.slowestTestURL = "";
TestRunner.interactiveDebugger = false;
TestRunner.cleanupCrashes = false;
TestRunner.timeoutAsPass = false;
TestRunner.conditionedProfile = false;
TestRunner.comparePrefs = false;

TestRunner._expectingProcessCrash = false;
TestRunner._structuredFormatter = new StructuredFormatter();

/**
 * Make sure the tests don't hang indefinitely.
 **/
TestRunner._numTimeouts = 0;
TestRunner._currentTestStartTime = new Date().valueOf();
TestRunner._timeoutFactor = 1;

/**
 * Used to collect code coverage with the js debugger.
 */
TestRunner.jscovDirPrefix = "";
var coverageCollector = {};

function record(succeeded, expectedFail, msg) {
  let successInfo;
  let failureInfo;
  if (expectedFail) {
    successInfo = {
      status: "PASS",
      expected: "FAIL",
      message: "TEST-UNEXPECTED-PASS",
    };
    failureInfo = {
      status: "FAIL",
      expected: "FAIL",
      message: "TEST-KNOWN-FAIL",
    };
  } else {
    successInfo = {
      status: "PASS",
      expected: "PASS",
      message: "TEST-PASS",
    };
    failureInfo = {
      status: "FAIL",
      expected: "PASS",
      message: "TEST-UNEXPECTED-FAIL",
    };
  }

  let result = succeeded ? successInfo : failureInfo;

  TestRunner.structuredLogger.testStatus(
    TestRunner.currentTestURL,
    msg,
    result.status,
    result.expected,
    "",
    ""
  );
}

TestRunner._checkForHangs = function () {
  function reportError(win, msg) {
    if (testInXOriginFrame() || "SimpleTest" in win) {
      record(false, TestRunner.timeoutAsPass, msg);
    } else if ("W3CTest" in win) {
      win.W3CTest.logFailure(msg);
    }
  }

  async function killTest(win) {
    if (testInXOriginFrame()) {
      win.postMessage("SimpleTest:timeout", "*");
    } else if ("SimpleTest" in win) {
      await win.SimpleTest.timeout();
      win.SimpleTest.finish();
    } else if ("W3CTest" in win) {
      await win.W3CTest.timeout();
    }
  }

  if (TestRunner._currentTest < TestRunner._urls.length) {
    var runtime = new Date().valueOf() - TestRunner._currentTestStartTime;
    if (
      !TestRunner.interactiveDebugger &&
      runtime >= TestRunner.timeout * TestRunner._timeoutFactor
    ) {
      let testIframe = $("testframe");
      var frameWindow =
        (!testInXOriginFrame() && testIframe.contentWindow.wrappedJSObject) ||
        testIframe.contentWindow;
      reportError(frameWindow, "Test timed out.");
      TestRunner.updateUI([{ result: false }]);

      // If we have too many timeouts, give up. We don't want to wait hours
      // for results if some bug causes lots of tests to time out.
      if (
        ++TestRunner._numTimeouts >= TestRunner.maxTimeouts ||
        TestRunner.runUntilFailure
      ) {
        TestRunner._haltTests = true;

        TestRunner.currentTestURL = "(SimpleTest/TestRunner.js)";
        reportError(
          frameWindow,
          TestRunner.maxTimeouts + " test timeouts, giving up."
        );
        var skippedTests = TestRunner._urls.length - TestRunner._currentTest;
        reportError(
          frameWindow,
          "Skipping " + skippedTests + " remaining tests."
        );
      }

      // Add a little (1 second) delay to ensure automation.py has time to notice
      // "Test timed out" log and process it (= take a screenshot).
      setTimeout(async function delayedKillTest() {
        try {
          await killTest(frameWindow);
        } catch (e) {
          reportError(frameWindow, "Test error: " + e);
          TestRunner.updateUI([{ result: false }]);
        }
      }, 1000);

      if (TestRunner._haltTests) {
        return;
      }
    }

    setTimeout(TestRunner._checkForHangs, 30000);
  }
};

TestRunner.requestLongerTimeout = function (factor) {
  TestRunner._timeoutFactor = factor;
};

/**
 * This is used to loop tests
 **/
TestRunner.repeat = 0;
TestRunner._currentLoop = 1;

TestRunner.expectAssertions = function (min, max) {
  if (typeof max == "undefined") {
    max = min;
  }
  if (
    typeof min != "number" ||
    typeof max != "number" ||
    min < 0 ||
    max < min
  ) {
    throw new Error("bad parameter to expectAssertions");
  }
  TestRunner._expectedMinAsserts = min;
  TestRunner._expectedMaxAsserts = max;
};

/**
 * This function is called after generating the summary.
 **/
TestRunner.onComplete = null;

/**
 * Adds a failed test case to a list so we can rerun only the failed tests
 **/
TestRunner._failedTests = {};
TestRunner._failureFile = "";

TestRunner.addFailedTest = function (testName) {
  if (TestRunner._failedTests[testName] == undefined) {
    TestRunner._failedTests[testName] = "";
  }
};

TestRunner.setFailureFile = function (fileName) {
  TestRunner._failureFile = fileName;
};

TestRunner.generateFailureList = function () {
  if (TestRunner._failureFile) {
    var failures = new MozillaFileLogger(TestRunner._failureFile);
    failures.log(JSON.stringify(TestRunner._failedTests));
    failures.close();
  }
};

/**
 * If logEnabled is true, this is the logger that will be used.
 **/

// This delimiter is used to avoid interleaving Mochitest/Gecko logs.
var LOG_DELIMITER = "\ue175\uee31\u2c32\uacbf";

// A log callback for StructuredLog.sys.mjs
TestRunner._dumpMessage = function (message) {
  var str;

  // This is a directive to python to format these messages
  // for compatibility with mozharness. This can be removed
  // with the MochitestFormatter (see bug 1045525).
  message.js_source = "TestRunner.js";
  if (
    TestRunner.interactiveDebugger &&
    message.action in TestRunner._structuredFormatter
  ) {
    str = TestRunner._structuredFormatter[message.action](message);
  } else {
    str = LOG_DELIMITER + JSON.stringify(message) + LOG_DELIMITER;
  }
  // BUGFIX: browser-chrome tests don't use LogController
  if (Object.keys(LogController.listeners).length !== 0) {
    LogController.log(str);
  } else {
    dump("\n" + str + "\n");
  }
  // Checking for error messages
  if (message.expected || message.level === "ERROR") {
    TestRunner.failureHandler();
  }
};

// From https://searchfox.org/mozilla-central/source/testing/modules/StructuredLog.sys.mjs
TestRunner.structuredLogger = new StructuredLogger(
  "mochitest",
  TestRunner._dumpMessage,
  [],
  TestRunner
);
TestRunner.structuredLogger.deactivateBuffering = function () {
  TestRunner.structuredLogger.logData("buffering_off");
};
TestRunner.structuredLogger.activateBuffering = function () {
  TestRunner.structuredLogger.logData("buffering_on");
};

TestRunner.log = function (msg) {
  if (TestRunner.logEnabled) {
    TestRunner.structuredLogger.info(msg);
  } else {
    dump(msg + "\n");
  }
};

TestRunner.error = function (msg) {
  if (TestRunner.logEnabled) {
    TestRunner.structuredLogger.error(msg);
  } else {
    dump(msg + "\n");
    TestRunner.failureHandler();
  }
};

TestRunner.failureHandler = function () {
  if (TestRunner.runUntilFailure) {
    TestRunner._haltTests = true;
  }

  if (TestRunner.debugOnFailure) {
    // You've hit this line because you requested to break into the
    // debugger upon a testcase failure on your test run.
    // eslint-disable-next-line no-debugger
    debugger;
  }
};

/**
 * Toggle element visibility
 **/
TestRunner._toggle = function (el) {
  if (el.className == "noshow") {
    el.className = "";
    el.style.cssText = "";
  } else {
    el.className = "noshow";
    el.style.cssText = "width:0px; height:0px; border:0px;";
  }
};

/**
 * Creates the iframe that contains a test
 **/
TestRunner._makeIframe = function (url, retry) {
  var iframe = $("testframe");
  if (
    url != "about:blank" &&
    (("hasFocus" in document && !document.hasFocus()) ||
      ("activeElement" in document && document.activeElement != iframe))
  ) {
    contentAsyncEvent("Focus");
    window.focus();
    SpecialPowers.focus();
    iframe.focus();
    if (retry < 3) {
      window.setTimeout(function () {
        TestRunner._makeIframe(url, retry + 1);
      });
      return;
    }

    TestRunner.structuredLogger.info(
      "Error: Unable to restore focus, expect failures and timeouts."
    );
  }
  window.scrollTo(0, $("indicator").offsetTop);
  try {
    let urlObj = new URL(url);
    if (TestRunner.xOriginTests) {
      // The test will run in a xorigin iframe, so we pass in additional test params in the
      // URL since the content process won't be able to access them from the parentRunner
      // directly.
      let params = TestRunner.getParameterInfo();
      urlObj.searchParams.append(
        "currentTestURL",
        urlObj.pathname.replace("/tests/", "")
      );
      urlObj.searchParams.append("closeWhenDone", params.closeWhenDone);
      urlObj.searchParams.append("showTestReport", TestRunner.showTestReport);
      urlObj.searchParams.append("expected", TestRunner.expected);
      iframe.src = urlObj.href;
    } else {
      iframe.src = url;
    }
  } catch {
    // If the provided `url` is not a valid URL (i.e. doesn't include a protocol)
    // then the new URL() constructor will raise a TypeError. This is expected in the
    // usual case (i.e. non-xorigin iFrame tests) so set the URL in the usual way.
    iframe.src = url;
  }
  iframe.name = url;
  iframe.width = "500";
};

/**
 * Returns the current test URL.
 * We use this to tell whether the test has navigated to another test without
 * being finished first.
 */
TestRunner.getLoadedTestURL = function () {
  if (!testInXOriginFrame()) {
    var prefix = "";
    // handle mochitest-chrome URIs
    if ($("testframe").contentWindow.location.protocol == "chrome:") {
      prefix = "chrome://mochitests";
    }
    return prefix + $("testframe").contentWindow.location.pathname;
  }
  return TestRunner.currentTestURL;
};

TestRunner.setParameterInfo = function (params) {
  this._params = params;
};

TestRunner.getParameterInfo = function () {
  return this._params;
};

/**
 * Print information about which prefs are set.
 * This is used to help validate that the tests are actually
 * running in the expected context.
 */
TestRunner.dumpPrefContext = function () {
  let prefs = ["fission.autostart"];

  let message = ["Dumping test context:"];
  prefs.forEach(function formatPref(pref) {
    let val = SpecialPowers.getBoolPref(pref);
    message.push(pref + "=" + val);
  });
  TestRunner.structuredLogger.info(message.join("\n  "));
};

/**
 * TestRunner entry point.
 *
 * The arguments are the URLs of the test to be ran.
 *
 **/
TestRunner.runTests = function (/*url...*/) {
  TestRunner.structuredLogger.info("SimpleTest START");
  TestRunner.dumpPrefContext();
  TestRunner.originalTestURL = $("current-test").innerHTML;

  SpecialPowers.registerProcessCrashObservers();

  // Initialize code coverage
  if (TestRunner.jscovDirPrefix != "") {
    var { CoverageCollector } = SpecialPowers.ChromeUtils.importESModule(
      "resource://testing-common/CoverageUtils.sys.mjs"
    );
    coverageCollector = new CoverageCollector(TestRunner.jscovDirPrefix);
  }

  SpecialPowers.requestResetCoverageCounters().then(() => {
    TestRunner._urls = flattenArguments(arguments);

    var singleTestRun = this._urls.length <= 1 && TestRunner.repeat <= 1;
    TestRunner.showTestReport = singleTestRun;
    var frame = $("testframe");
    frame.src = "";
    if (singleTestRun) {
      // Can't use document.body because this runs in a XUL doc as well...
      var body = document.getElementsByTagName("body")[0];
      body.setAttribute("singletest", "true");
      frame.removeAttribute("scrolling");
    }
    TestRunner._checkForHangs();
    TestRunner.runNextTest();
  });
};

/**
 * Used for running a set of tests in a loop for debugging purposes
 * Takes an array of URLs
 **/
TestRunner.resetTests = function (listURLs) {
  TestRunner._currentTest = 0;
  // Reset our "Current-test" line - functionality depends on it
  $("current-test").innerHTML = TestRunner.originalTestURL;
  if (TestRunner.logEnabled) {
    TestRunner.structuredLogger.info(
      "SimpleTest START Loop " + TestRunner._currentLoop
    );
  }

  TestRunner._urls = listURLs;
  $("testframe").src = "";
  TestRunner._checkForHangs();
  TestRunner.runNextTest();
};

TestRunner.getNextUrl = function () {
  var url = "";
  // sometimes we have a subtest/harness which doesn't use a manifest
  if (
    TestRunner._urls[TestRunner._currentTest] instanceof Object &&
    "test" in TestRunner._urls[TestRunner._currentTest]
  ) {
    url = TestRunner._urls[TestRunner._currentTest].test.url;
    TestRunner.expected =
      TestRunner._urls[TestRunner._currentTest].test.expected;
  } else {
    url = TestRunner._urls[TestRunner._currentTest];
    TestRunner.expected = "pass";
  }
  return url;
};

/**
 * Run the next test. If no test remains, calls onComplete().
 **/
TestRunner._haltTests = false;
async function _runNextTest() {
  if (
    TestRunner._currentTest < TestRunner._urls.length &&
    !TestRunner._haltTests
  ) {
    var url = TestRunner.getNextUrl();
    TestRunner.currentTestURL = url;

    $("current-test-path").innerHTML = url;

    TestRunner._currentTestStartTimestamp = SpecialPowers.Cu.now();
    TestRunner._currentTestStartTime = new Date().valueOf();
    TestRunner._timeoutFactor = 1;
    TestRunner._expectedMinAsserts = 0;
    TestRunner._expectedMaxAsserts = 0;

    TestRunner.structuredLogger.testStart(url);

    if (TestRunner._urls[TestRunner._currentTest].test.allow_xul_xbl) {
      await SpecialPowers.pushPermissions([
        { type: "allowXULXBL", allow: true, context: "http://mochi.test:8888" },
        { type: "allowXULXBL", allow: true, context: "http://example.org" },
      ]);
    }
    if (TestRunner._urls[TestRunner._currentTest].test.https_first_disabled) {
      await SpecialPowers.pushPrefEnv({
        set: [["dom.security.https_first", false]],
      });
    }
    TestRunner._makeIframe(url, 0);
  } else {
    $("current-test").innerHTML = "<b>Finished</b>";
    // Only unload the last test to run if we're running more than one test.
    if (TestRunner._urls.length > 1) {
      TestRunner._makeIframe("about:blank", 0);
    }

    var passCount = parseInt($("pass-count").innerHTML, 10);
    var failCount = parseInt($("fail-count").innerHTML, 10);
    var todoCount = parseInt($("todo-count").innerHTML, 10);

    if (passCount === 0 && failCount === 0 && todoCount === 0) {
      // No |$('testframe').contentWindow|, so manually update: ...
      // ... the log,
      TestRunner.structuredLogger.error(
        "TEST-UNEXPECTED-FAIL | SimpleTest/TestRunner.js | No checks actually run"
      );
      // ... the count,
      $("fail-count").innerHTML = 1;
      // ... the indicator.
      var indicator = $("indicator");
      indicator.innerHTML = "Status: Fail (No checks actually run)";
      indicator.style.backgroundColor = "red";
    }

    let e10sMode = SpecialPowers.isMainProcess() ? "non-e10s" : "e10s";

    TestRunner.structuredLogger.info("TEST-START | Shutdown");
    TestRunner.structuredLogger.info("Passed:  " + passCount);
    TestRunner.structuredLogger.info("Failed:  " + failCount);
    TestRunner.structuredLogger.info("Todo:    " + todoCount);
    TestRunner.structuredLogger.info("Mode:    " + e10sMode);
    TestRunner.structuredLogger.info(
      "Slowest: " +
        TestRunner.slowestTestTime +
        "ms - " +
        TestRunner.slowestTestURL
    );

    // If we are looping, don't send this cause it closes the log file,
    // also don't unregister the crash observers until we're done.
    if (TestRunner.repeat === 0) {
      SpecialPowers.unregisterProcessCrashObservers();
      TestRunner.structuredLogger.info("SimpleTest FINISHED");
    }

    if (TestRunner.repeat === 0 && TestRunner.onComplete) {
      TestRunner.onComplete();
    }

    if (
      TestRunner._currentLoop <= TestRunner.repeat &&
      !TestRunner._haltTests
    ) {
      TestRunner._currentLoop++;
      TestRunner.resetTests(TestRunner._urls);
      TestRunner._loopIsRestarting = true;
    } else {
      // Loops are finished
      if (TestRunner.logEnabled) {
        TestRunner.structuredLogger.info(
          "TEST-INFO | Ran " + TestRunner._currentLoop + " Loops"
        );
        TestRunner.structuredLogger.info("SimpleTest FINISHED");
      }

      if (TestRunner.onComplete) {
        TestRunner.onComplete();
      }
    }
    TestRunner.generateFailureList();

    if (TestRunner.jscovDirPrefix != "") {
      coverageCollector.finalize();
    }
  }
}
TestRunner.runNextTest = _runNextTest;

TestRunner.expectChildProcessCrash = function () {
  TestRunner._expectingProcessCrash = true;
};

/**
 * This stub is called by SimpleTest when a test is finished.
 **/
TestRunner.testFinished = function (tests) {
  // Need to track subtests recorded here separately or else they'll
  // trigger the `result after SimpleTest.finish()` error.
  var extraTests = [];
  var result = "OK";

  // Prevent a test from calling finish() multiple times before we
  // have a chance to unload it.
  if (
    TestRunner._currentTest == TestRunner._lastTestFinished &&
    !TestRunner._loopIsRestarting
  ) {
    TestRunner.structuredLogger.testEnd(
      TestRunner.currentTestURL,
      "ERROR",
      "OK",
      "called finish() multiple times"
    );
    TestRunner.updateUI([{ result: false }]);
    return;
  }

  if (TestRunner.jscovDirPrefix != "") {
    coverageCollector.recordTestCoverage(TestRunner.currentTestURL);
  }

  SpecialPowers.requestDumpCoverageCounters().then(() => {
    TestRunner._lastTestFinished = TestRunner._currentTest;
    TestRunner._loopIsRestarting = false;

    // TODO : replace this by a function that returns the mem data as an object
    // that's dumped later with the test_end message
    MemoryStats.dump(
      TestRunner._currentTest,
      TestRunner.currentTestURL,
      TestRunner.dumpOutputDirectory,
      TestRunner.dumpAboutMemoryAfterTest,
      TestRunner.dumpDMDAfterTest
    );

    async function cleanUpCrashDumpFiles() {
      if (
        !(await SpecialPowers.removeExpectedCrashDumpFiles(
          TestRunner._expectingProcessCrash
        ))
      ) {
        let subtest = "expected-crash-dump-missing";
        TestRunner.structuredLogger.testStatus(
          TestRunner.currentTestURL,
          subtest,
          "ERROR",
          "PASS",
          "This test did not leave any crash dumps behind, but we were expecting some!"
        );
        extraTests.push({ name: subtest, result: false });
        result = "ERROR";
      }

      var unexpectedCrashDumpFiles =
        await SpecialPowers.findUnexpectedCrashDumpFiles();
      TestRunner._expectingProcessCrash = false;
      if (unexpectedCrashDumpFiles.length) {
        let subtest = "unexpected-crash-dump-found";
        TestRunner.structuredLogger.testStatus(
          TestRunner.currentTestURL,
          subtest,
          "ERROR",
          "PASS",
          "This test left crash dumps behind, but we " +
            "weren't expecting it to!",
          null,
          { unexpected_crashdump_files: unexpectedCrashDumpFiles }
        );
        extraTests.push({ name: subtest, result: false });
        result = "CRASH";
        unexpectedCrashDumpFiles.sort().forEach(function (aFilename) {
          TestRunner.structuredLogger.info(
            "Found unexpected crash dump file " + aFilename + "."
          );
        });
      }

      if (TestRunner.cleanupCrashes) {
        if (await SpecialPowers.removePendingCrashDumpFiles()) {
          TestRunner.structuredLogger.info(
            "This test left pending crash dumps"
          );
        }
      }
    }

    function runNextTest() {
      if (TestRunner.currentTestURL != TestRunner.getLoadedTestURL()) {
        TestRunner.structuredLogger.testStatus(
          TestRunner.currentTestURL,
          TestRunner.getLoadedTestURL(),
          "FAIL",
          "PASS",
          "finished in a non-clean fashion, probably" +
            " because it didn't call SimpleTest.finish()",
          { loaded_test_url: TestRunner.getLoadedTestURL() }
        );
        extraTests.push({ name: "clean-finish", result: false });
        result = result != "CRASH" ? "ERROR" : result;
      }

      SpecialPowers.addProfilerMarker(
        "TestRunner",
        { category: "Test", startTime: TestRunner._currentTestStartTimestamp },
        TestRunner.currentTestURL
      );
      var runtime = new Date().valueOf() - TestRunner._currentTestStartTime;

      if (
        TestRunner.slowestTestTime < runtime &&
        TestRunner._timeoutFactor >= 1
      ) {
        TestRunner.slowestTestTime = runtime;
        TestRunner.slowestTestURL = TestRunner.currentTestURL;
      }

      TestRunner.updateUI(tests.concat(extraTests));

      // Don't show the interstitial if we just run one test with no repeats:
      if (TestRunner._urls.length == 1 && TestRunner.repeat <= 1) {
        TestRunner.testUnloaded(result, runtime);
        return;
      }

      var interstitialURL;
      if (
        !testInXOriginFrame() &&
        $("testframe").contentWindow.location.protocol == "chrome:"
      ) {
        interstitialURL =
          "tests/SimpleTest/iframe-between-tests.html?result=" +
          result +
          "&runtime=" +
          runtime;
      } else {
        interstitialURL =
          "/tests/SimpleTest/iframe-between-tests.html?result=" +
          result +
          "&runtime=" +
          runtime;
      }
      // check if there were test run after SimpleTest.finish, which should never happen
      if (!testInXOriginFrame()) {
        $("testframe").contentWindow.addEventListener("unload", function () {
          var testwin = $("testframe").contentWindow;
          if (testwin.SimpleTest) {
            if (typeof testwin.SimpleTest.testsLength === "undefined") {
              TestRunner.structuredLogger.error(
                "TEST-UNEXPECTED-FAIL | " +
                  TestRunner.currentTestURL +
                  " fired an unload callback with missing test data," +
                  " possibly due to the test navigating or reloading"
              );
              TestRunner.updateUI([{ result: false }]);
            } else if (
              testwin.SimpleTest._tests.length != testwin.SimpleTest.testsLength
            ) {
              var didReportError = false;
              var wrongtestlength =
                testwin.SimpleTest._tests.length -
                testwin.SimpleTest.testsLength;
              var wrongtestname = "";
              for (var i = 0; i < wrongtestlength; i++) {
                wrongtestname =
                  testwin.SimpleTest._tests[testwin.SimpleTest.testsLength + i]
                    .name;
                TestRunner.structuredLogger.error(
                  "TEST-UNEXPECTED-FAIL | " +
                    TestRunner.currentTestURL +
                    " logged result after SimpleTest.finish(): " +
                    wrongtestname
                );
                didReportError = true;
              }
              if (!didReportError) {
                // This clause shouldn't be reachable, but if we somehow get
                // here (e.g. if wrongtestlength is somehow negative), it's
                // important that we log *something* for the { result: false }
                // test-failure that we're about to post.
                TestRunner.structuredLogger.error(
                  "TEST-UNEXPECTED-FAIL | " +
                    TestRunner.currentTestURL +
                    " hit an unexpected condition when checking for" +
                    " logged results after SimpleTest.finish()"
                );
              }
              TestRunner.updateUI([{ result: false }]);
            }
          }
        });
      }
      TestRunner._makeIframe(interstitialURL, 0);
    }

    SpecialPowers.executeAfterFlushingMessageQueue(async function () {
      await SpecialPowers.waitForCrashes(TestRunner._expectingProcessCrash);
      await cleanUpCrashDumpFiles();
      await SpecialPowers.flushPermissions();
      await SpecialPowers.flushPrefEnv();
      SpecialPowers.cleanupAllClipboard(window);
      runNextTest();
    });
  });
};

/**
 * This stub is called by XOrigin Tests to report assertion count.
 **/
TestRunner._xoriginAssertionCount = 0;
TestRunner.addAssertionCount = function (count) {
  if (!testInXOriginFrame()) {
    TestRunner.error(
      `addAssertionCount should only be called by a cross origin test`
    );
    return;
  }

  if (testInDifferentProcess()) {
    TestRunner._xoriginAssertionCount += count;
  }
};

TestRunner.testUnloaded = function (result, runtime) {
  // If we're in a debug build, check assertion counts.  This code is
  // similar to the code in Tester_nextTest in browser-test.js used
  // for browser-chrome mochitests.
  if (SpecialPowers.isDebugBuild) {
    var newAssertionCount =
      SpecialPowers.assertionCount() + TestRunner._xoriginAssertionCount;
    var numAsserts = newAssertionCount - TestRunner._lastAssertionCount;
    TestRunner._lastAssertionCount = newAssertionCount;

    var max = TestRunner._expectedMaxAsserts;
    var min = TestRunner._expectedMinAsserts;
    if (Array.isArray(TestRunner.expected)) {
      // Accumulate all assertion counts recorded in the failure pattern file.
      let additionalAsserts = TestRunner.expected.reduce(
        (acc, [pat, count]) => {
          return pat == "ASSERTION" ? acc + count : acc;
        },
        0
      );
      min += additionalAsserts;
      max += additionalAsserts;
    }

    TestRunner.structuredLogger.assertionCount(
      TestRunner.currentTestURL,
      numAsserts,
      min,
      max
    );

    if (numAsserts < min || numAsserts > max) {
      result = "ERROR";

      var direction = "more";
      var target = max;
      if (numAsserts < min) {
        direction = "less";
        target = min;
      }
      TestRunner.structuredLogger.testStatus(
        TestRunner.currentTestURL,
        "Assertion Count",
        "ERROR",
        "PASS",
        numAsserts +
          " is " +
          direction +
          " than expected " +
          target +
          " assertions"
      );

      // reset result so we don't print a second error on test-end
      result = "OK";
    }
  }

  TestRunner.structuredLogger.testEnd(
    TestRunner.currentTestURL,
    result,
    "OK",
    "Finished in " + runtime + "ms",
    { runtime }
  );

  // Always do this, so we can "reset" preferences between tests.
  // Note: this is for mochitest-plain only; browser tests do not
  // unconditionally reset between tests, see
  // checkPreferencesAfterTest in testing/mochitest/browser-test.js
  SpecialPowers.comparePrefsToBaseline(
    TestRunner.ignorePrefs,
    TestRunner.verifyPrefsNextTest
  );
};

TestRunner.verifyPrefsNextTest = function (p) {
  if (TestRunner.comparePrefs) {
    let prefs = Array.from(SpecialPowers.Cu.waiveXrays(p), x =>
      SpecialPowers.unwrapIfWrapped(SpecialPowers.Cu.unwaiveXrays(x))
    );
    prefs.forEach(pr =>
      TestRunner.structuredLogger.error(
        "TEST-UNEXPECTED-FAIL | " +
          TestRunner.currentTestURL +
          " | changed preference: " +
          pr
      )
    );
  }
  TestRunner.doNextTest();
};

TestRunner.doNextTest = function () {
  TestRunner._currentTest++;
  if (TestRunner.runSlower) {
    setTimeout(TestRunner.runNextTest, 1000);
  } else {
    TestRunner.runNextTest();
  }
};

/**
 * Get the results.
 */
TestRunner.countResults = function (tests) {
  var nOK = 0;
  var nNotOK = 0;
  var nTodo = 0;
  for (var i = 0; i < tests.length; ++i) {
    var test = tests[i];
    if (test.todo && !test.result) {
      nTodo++;
    } else if (test.result && !test.todo) {
      nOK++;
    } else {
      nNotOK++;
    }
  }
  return { OK: nOK, notOK: nNotOK, todo: nTodo };
};

/**
 * Print out table of any error messages found during looped run
 */
TestRunner.displayLoopErrors = function (tableName, tests) {
  if (TestRunner.countResults(tests).notOK > 0) {
    var table = $(tableName);
    var curtest;
    if (!table.rows.length) {
      //if table headers are not yet generated, make them
      var row = table.insertRow(table.rows.length);
      var cell = row.insertCell(0);
      var textNode = document.createTextNode("Test File Name:");
      cell.appendChild(textNode);
      cell = row.insertCell(1);
      textNode = document.createTextNode("Test:");
      cell.appendChild(textNode);
      cell = row.insertCell(2);
      textNode = document.createTextNode("Error message:");
      cell.appendChild(textNode);
    }

    //find the broken test
    for (var testnum in tests) {
      curtest = tests[testnum];
      if (
        !(
          (curtest.todo && !curtest.result) ||
          (curtest.result && !curtest.todo)
        )
      ) {
        //this is a failed test or the result of todo test. Display the related message
        row = table.insertRow(table.rows.length);
        cell = row.insertCell(0);
        textNode = document.createTextNode(TestRunner.currentTestURL);
        cell.appendChild(textNode);
        cell = row.insertCell(1);
        textNode = document.createTextNode(curtest.name);
        cell.appendChild(textNode);
        cell = row.insertCell(2);
        textNode = document.createTextNode(curtest.diag ? curtest.diag : "");
        cell.appendChild(textNode);
      }
    }
  }
};

TestRunner.updateUI = function (tests) {
  var results = TestRunner.countResults(tests);
  var passCount = parseInt($("pass-count").innerHTML) + results.OK;
  var failCount = parseInt($("fail-count").innerHTML) + results.notOK;
  var todoCount = parseInt($("todo-count").innerHTML) + results.todo;
  $("pass-count").innerHTML = passCount;
  $("fail-count").innerHTML = failCount;
  $("todo-count").innerHTML = todoCount;

  // Set the top Green/Red bar
  var indicator = $("indicator");
  if (failCount > 0) {
    indicator.innerHTML = "Status: Fail";
    indicator.style.backgroundColor = "red";
  } else if (passCount > 0) {
    indicator.innerHTML = "Status: Pass";
    indicator.style.backgroundColor = "#0d0";
  } else {
    indicator.innerHTML = "Status: ToDo";
    indicator.style.backgroundColor = "orange";
  }

  // Set the table values
  var trID = "tr-" + $("current-test-path").innerHTML;
  var row = $(trID);

  // Only update the row if it actually exists (autoUI)
  if (row != null) {
    var tds = row.getElementsByTagName("td");
    tds[0].style.backgroundColor = "#0d0";
    tds[0].innerHTML = parseInt(tds[0].innerHTML) + parseInt(results.OK);
    tds[1].style.backgroundColor = results.notOK > 0 ? "red" : "#0d0";
    tds[1].innerHTML = parseInt(tds[1].innerHTML) + parseInt(results.notOK);
    tds[2].style.backgroundColor = results.todo > 0 ? "orange" : "#0d0";
    tds[2].innerHTML = parseInt(tds[2].innerHTML) + parseInt(results.todo);
  }

  //if we ran in a loop, display any found errors
  if (TestRunner.repeat > 0) {
    TestRunner.displayLoopErrors("fail-table", tests);
  }
};

// XOrigin Tests
// If "--enable-xorigin-tests" is set, mochitests are run in a cross origin iframe.
// The parent process will run at http://mochi.xorigin-test:8888", and individual
// mochitests will be launched in a cross-origin iframe at http://mochi.test:8888.

var xOriginDispatchMap = {
  runner: TestRunner,
  logger: TestRunner.structuredLogger,
  addFailedTest: TestRunner.addFailedTest,
  expectAssertions: TestRunner.expectAssertions,
  expectChildProcessCrash: TestRunner.expectChildProcessCrash,
  requestLongerTimeout: TestRunner.requestLongerTimeout,
  "structuredLogger.deactivateBuffering":
    TestRunner.structuredLogger.deactivateBuffering,
  "structuredLogger.activateBuffering":
    TestRunner.structuredLogger.activateBuffering,
  "structuredLogger.testStatus": TestRunner.structuredLogger.testStatus,
  "structuredLogger.info": TestRunner.structuredLogger.info,
  "structuredLogger.warning": TestRunner.structuredLogger.warning,
  "structuredLogger.error": TestRunner.structuredLogger.error,
  testFinished: TestRunner.testFinished,
  addAssertionCount: TestRunner.addAssertionCount,
};

function xOriginTestRunnerHandler(event) {
  if (event.data.harnessType != "SimpleTest") {
    return;
  }
  // Handles messages from xOriginRunner in SimpleTest.js.
  if (event.data.command in xOriginDispatchMap) {
    xOriginDispatchMap[event.data.command].apply(
      xOriginDispatchMap[event.data.applyOn],
      event.data.params
    );
  } else {
    TestRunner.error(`Command ${event.data.command} not found
      in xOriginDispatchMap`);
  }
}

TestRunner.setXOriginEventHandler = function () {
  window.addEventListener("message", xOriginTestRunnerHandler);
};
