/* -*- js-indent-level: 2; tab-width: 2; indent-tabs-mode: nil -*- */
/* vim:set ts=2 sw=2 sts=2 et: */

// Generally gTestPath should be set by the harness.
/* global gTestPath */

/**
 * SimpleTest framework object.
 * @class
 */
var SimpleTest = {};
var parentRunner = null;

// Using a try/catch rather than SpecialPowers.Cu.isRemoteProxy() because
// it doesn't cover the case where an iframe is xorigin but fission is
// not enabled.
let isSameOrigin = function (w) {
  try {
    w.top.TestRunner;
  } catch (e) {
    if (e instanceof DOMException) {
      return false;
    }
  }
  return true;
};
let isXOrigin = !isSameOrigin(window);

// Note: duplicated in browser-test.js . See also bug 1820150.
function isErrorOrException(err) {
  // It'd be nice if we had either `Error.isError(err)` or `Error.isInstance(err)`
  // but we don't, so do it ourselves:
  if (!err) {
    return false;
  }
  if (err instanceof SpecialPowers.Ci.nsIException) {
    return true;
  }
  try {
    let glob = SpecialPowers.Cu.getGlobalForObject(err);
    return err instanceof glob.Error;
  } catch {
    // getGlobalForObject can be upset if it doesn't get passed an object.
    // Just do a standard instanceof check using this global and cross fingers:
  }
  return err instanceof Error;
}

// In normal test runs, the window that has a TestRunner in its parent is
// the primary window.  In single test runs, if there is no parent and there
// is no opener then it is the primary window.
var isSingleTestRun =
  parent == window &&
  !(opener || (window.arguments && window.arguments[0].SimpleTest));
try {
  var isPrimaryTestWindow =
    (isXOrigin && parent != window && parent == top) ||
    (!isXOrigin && (!!parent.TestRunner || isSingleTestRun));
} catch (e) {
  dump(
    "TEST-UNEXPECTED-FAIL, Exception caught: " +
      e.message +
      ", at: " +
      e.fileName +
      " (" +
      e.lineNumber +
      "), location: " +
      window.location.href +
      "\n"
  );
}

let xOriginRunner = {
  init(harnessWindow) {
    this.harnessWindow = harnessWindow;
    let url = new URL(document.URL);
    this.testFile = url.pathname;
    this.showTestReport = url.searchParams.get("showTestReport") == "true";
    this.expected = url.searchParams.get("expected");
  },
  callHarnessMethod(applyOn, command, ...params) {
    // Message handled by xOriginTestRunnerHandler in TestRunner.js
    this.harnessWindow.postMessage(
      {
        harnessType: "SimpleTest",
        applyOn,
        command,
        params,
      },
      "*"
    );
  },
  getParameterInfo() {
    let url = new URL(document.URL);
    return {
      currentTestURL: url.searchParams.get("currentTestURL"),
      testRoot: url.searchParams.get("testRoot"),
    };
  },
  addFailedTest(test) {
    this.callHarnessMethod("runner", "addFailedTest", test);
  },
  expectAssertions(min, max) {
    this.callHarnessMethod("runner", "expectAssertions", min, max);
  },
  expectChildProcessCrash() {
    this.callHarnessMethod("runner", "expectChildProcessCrash");
  },
  requestLongerTimeout(factor) {
    this.callHarnessMethod("runner", "requestLongerTimeout", factor);
  },
  _lastAssertionCount: 0,
  testFinished(tests) {
    var newAssertionCount = SpecialPowers.assertionCount();
    var numAsserts = newAssertionCount - this._lastAssertionCount;
    this._lastAssertionCount = newAssertionCount;
    this.callHarnessMethod("runner", "addAssertionCount", numAsserts);
    this.callHarnessMethod("runner", "testFinished", tests);
  },
  structuredLogger: {
    info(msg) {
      xOriginRunner.callHarnessMethod("logger", "structuredLogger.info", msg);
    },
    warning(msg) {
      xOriginRunner.callHarnessMethod(
        "logger",
        "structuredLogger.warning",
        msg
      );
    },
    error(msg) {
      xOriginRunner.callHarnessMethod("logger", "structuredLogger.error", msg);
    },
    activateBuffering() {
      xOriginRunner.callHarnessMethod(
        "logger",
        "structuredLogger.activateBuffering"
      );
    },
    deactivateBuffering() {
      xOriginRunner.callHarnessMethod(
        "logger",
        "structuredLogger.deactivateBuffering"
      );
    },
    testStatus(url, subtest, status, expected, diagnostic, stack) {
      xOriginRunner.callHarnessMethod(
        "logger",
        "structuredLogger.testStatus",
        url,
        subtest,
        status,
        expected,
        diagnostic,
        stack
      );
    },
  },
};

// Finds the TestRunner for this test run and the SpecialPowers object (in
// case it is not defined) from a parent/opener window.
//
// Finding the SpecialPowers object is needed when we have ChromePowers in
// harness.xhtml and we need SpecialPowers in the iframe, and also for tests
// like test_focus.xhtml where we open a window which opens another window which
// includes SimpleTest.js.
(function () {
  function ancestor(w) {
    return w.parent != w
      ? w.parent
      : w.opener ||
          (!isXOrigin &&
            w.arguments &&
            SpecialPowers.wrap(Window).isInstance(w.arguments[0]) &&
            w.arguments[0]);
  }

  var w = ancestor(window);
  while (w && !parentRunner) {
    isXOrigin = !isSameOrigin(w);

    if (isXOrigin) {
      if (w.parent != w) {
        w = w.top;
      }
      xOriginRunner.init(w);
      parentRunner = xOriginRunner;
    }

    if (!parentRunner) {
      parentRunner = w.TestRunner;
      if (!parentRunner && w.wrappedJSObject) {
        parentRunner = w.wrappedJSObject.TestRunner;
      }
    }
    w = ancestor(w);
  }

  if (parentRunner) {
    SimpleTest.harnessParameters = parentRunner.getParameterInfo();
  }
})();

/* Helper functions pulled out of various MochiKit modules */
if (typeof repr == "undefined") {
  this.repr = function repr(o) {
    if (typeof o == "undefined") {
      return "undefined";
    } else if (o === null) {
      return "null";
    }
    try {
      if (typeof o.__repr__ == "function") {
        return o.__repr__();
      } else if (typeof o.repr == "function" && o.repr != repr) {
        return o.repr();
      }
    } catch (e) {}
    try {
      if (
        typeof o.NAME == "string" &&
        (o.toString == Function.prototype.toString ||
          o.toString == Object.prototype.toString)
      ) {
        return o.NAME;
      }
    } catch (e) {}
    var ostring;
    try {
      if (o === 0) {
        ostring = 1 / o > 0 ? "+0" : "-0";
      } else if (typeof o === "string") {
        ostring = JSON.stringify(o);
      } else if (Array.isArray(o)) {
        ostring = "[" + o.map(val => repr(val)).join(", ") + "]";
      } else {
        ostring = o + "";
      }
    } catch (e) {
      return "[" + typeof o + "]";
    }
    if (typeof o == "function") {
      o = ostring.replace(/^\s+/, "");
      var idx = o.indexOf("{");
      if (idx != -1) {
        o = o.substr(0, idx) + "{...}";
      }
    }
    return ostring;
  };
}

/* This returns a function that applies the previously given parameters.
 * This is used by SimpleTest.showReport
 */
if (typeof partial == "undefined") {
  this.partial = function (func) {
    var args = [];
    for (let i = 1; i < arguments.length; i++) {
      args.push(arguments[i]);
    }
    return function () {
      if (arguments.length) {
        for (let i = 1; i < arguments.length; i++) {
          args.push(arguments[i]);
        }
      }
      func(args);
    };
  };
}

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

SimpleTest._newCallStack = function (path) {
  var rval = function callStackHandler() {
    var callStack = callStackHandler.callStack;
    for (var i = 0; i < callStack.length; i++) {
      if (callStack[i].apply(this, arguments) === false) {
        break;
      }
    }
    try {
      this[path] = null;
    } catch (e) {
      // pass
    }
  };
  rval.callStack = [];
  return rval;
};

if (typeof addLoadEvent == "undefined") {
  this.addLoadEvent = function (func) {
    var existing = window.onload;
    var regfunc = existing;
    if (
      !(
        typeof existing == "function" &&
        typeof existing.callStack == "object" &&
        existing.callStack !== null
      )
    ) {
      regfunc = SimpleTest._newCallStack("onload");
      if (typeof existing == "function") {
        regfunc.callStack.push(existing);
      }
      window.onload = regfunc;
    }
    regfunc.callStack.push(func);
  };
}

function createEl(type, attrs, html) {
  //use createElementNS so the xul/xhtml tests have no issues
  var el;
  if (!document.body) {
    el = document.createElementNS("http://www.w3.org/1999/xhtml", type);
  } else {
    el = document.createElement(type);
  }
  if (attrs !== null && attrs !== undefined) {
    for (var k in attrs) {
      el.setAttribute(k, attrs[k]);
    }
  }
  if (html !== null && html !== undefined) {
    el.appendChild(document.createTextNode(html));
  }
  return el;
}

/* lots of tests use this as a helper to get css properties */
if (typeof computedStyle == "undefined") {
  this.computedStyle = function (elem, cssProperty) {
    elem = getElement(elem);
    if (elem.currentStyle) {
      return elem.currentStyle[cssProperty];
    }
    if (typeof document.defaultView == "undefined" || document === null) {
      return undefined;
    }
    var style = document.defaultView.getComputedStyle(elem);
    if (typeof style == "undefined" || style === null) {
      return undefined;
    }

    var selectorCase = cssProperty.replace(/([A-Z])/g, "-$1").toLowerCase();

    return style.getPropertyValue(selectorCase);
  };
}

SimpleTest._tests = [];
SimpleTest._stopOnLoad = true;
SimpleTest._cleanupFunctions = [];
SimpleTest._taskCleanupFunctions = [];
SimpleTest._currentTask = null;
SimpleTest._timeoutFunctions = [];
SimpleTest._inChaosMode = false;
// When using failure pattern file to filter unexpected issues,
// SimpleTest.expected would be an array of [pattern, expected count],
// and SimpleTest.num_failed would be an array of actual counts which
// has the same length as SimpleTest.expected.
SimpleTest.expected = "pass";
SimpleTest.num_failed = 0;

SpecialPowers.setAsDefaultAssertHandler();

function usesFailurePatterns() {
  return Array.isArray(SimpleTest.expected);
}

/**
 * Checks whether there is any failure pattern matches the given error
 * message, and if found, bumps the counter of the failure pattern.
 *
 * @return {boolean} Whether a matched failure pattern is found.
 */
function recordIfMatchesFailurePattern(name, diag) {
  let index = SimpleTest.expected.findIndex(([pat]) => {
    return (
      pat == null ||
      (typeof name == "string" && name.includes(pat)) ||
      (typeof diag == "string" && diag.includes(pat))
    );
  });
  if (index >= 0) {
    SimpleTest.num_failed[index]++;
    return true;
  }
  return false;
}

SimpleTest.setExpected = function () {
  if (!parentRunner) {
    return;
  }
  if (!Array.isArray(parentRunner.expected)) {
    SimpleTest.expected = parentRunner.expected;
  } else {
    // Assertions are checked by the runner.
    SimpleTest.expected = parentRunner.expected.filter(
      ([pat]) => pat != "ASSERTION"
    );
    SimpleTest.num_failed = new Array(SimpleTest.expected.length);
    SimpleTest.num_failed.fill(0);
  }
};
SimpleTest.setExpected();

/**
 * Something like assert.
 **/
SimpleTest.ok = function (condition, name) {
  if (arguments.length > 2) {
    const diag = "Too many arguments passed to `ok(condition, name)`";
    SimpleTest.record(false, name, diag);
  } else {
    SimpleTest.record(condition, name);
  }
};

SimpleTest.record = function (condition, name, diag, stack, expected) {
  var test = { result: !!condition, name, diag };
  let successInfo;
  let failureInfo;
  if (SimpleTest.expected == "fail") {
    if (!test.result) {
      SimpleTest.num_failed++;
      test.result = true;
    }
    successInfo = {
      status: "PASS",
      expected: "PASS",
      message: "TEST-PASS",
    };
    failureInfo = {
      status: "FAIL",
      expected: "FAIL",
      message: "TEST-KNOWN-FAIL",
    };
  } else if (!test.result && usesFailurePatterns()) {
    if (recordIfMatchesFailurePattern(name, diag)) {
      test.result = true;
      // Add a mark for unexpected failures suppressed by failure pattern.
      name = "[suppressed] " + name;
    }
    successInfo = {
      status: "FAIL",
      expected: "FAIL",
      message: "TEST-KNOWN-FAIL",
    };
    failureInfo = {
      status: "FAIL",
      expected: "PASS",
      message: "TEST-UNEXPECTED-FAIL",
    };
  } else if (expected == "fail") {
    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",
    };
  }

  if (condition) {
    stack = null;
  } else if (!stack) {
    stack = new Error().stack
      .replace(/^(.*@)http:\/\/mochi.test:8888\/tests\//gm, "    $1")
      .split("\n");
    stack.splice(0, 1);
    stack = stack.join("\n");
  }
  SimpleTest._logResult(test, successInfo, failureInfo, stack);
  SimpleTest._tests.push(test);
};

/**
 * Roughly equivalent to ok(Object.is(a, b), name)
 **/
SimpleTest.is = function (a, b, name) {
  // Be lazy and use Object.is til we want to test a browser without it.
  var pass = Object.is(a, b);
  var diag = pass ? "" : "got " + repr(a) + ", expected " + repr(b);
  SimpleTest.record(pass, name, diag);
};

SimpleTest.isfuzzy = function (a, b, epsilon, name) {
  var pass = a >= b - epsilon && a <= b + epsilon;
  var diag = pass
    ? ""
    : "got " +
      repr(a) +
      ", expected " +
      repr(b) +
      " epsilon: +/- " +
      repr(epsilon);
  SimpleTest.record(pass, name, diag);
};

SimpleTest.isnot = function (a, b, name) {
  var pass = !Object.is(a, b);
  var diag = pass ? "" : "didn't expect " + repr(a) + ", but got it";
  SimpleTest.record(pass, name, diag);
};

/**
 * Check that the function call throws an exception.
 */
SimpleTest.doesThrow = function (fn, name) {
  var gotException = false;
  try {
    fn();
  } catch (ex) {
    gotException = true;
  }
  ok(gotException, name);
};

//  --------------- Test.Builder/Test.More todo() -----------------

SimpleTest.todo = function (condition, name, diag) {
  var test = { result: !!condition, name, diag, todo: true };
  if (
    test.result &&
    usesFailurePatterns() &&
    recordIfMatchesFailurePattern(name, diag)
  ) {
    // Flipping the result to false so we don't get unexpected result. There
    // is no perfect way here. A known failure can trigger unexpected pass,
    // in which case, tagging it as KNOWN-FAIL probably makes more sense than
    // marking it PASS.
    test.result = false;
    // Add a mark for unexpected failures suppressed by failure pattern.
    name = "[suppressed] " + name;
  }
  var successInfo = {
    status: "PASS",
    expected: "FAIL",
    message: "TEST-UNEXPECTED-PASS",
  };
  var failureInfo = {
    status: "FAIL",
    expected: "FAIL",
    message: "TEST-KNOWN-FAIL",
  };
  SimpleTest._logResult(test, successInfo, failureInfo);
  SimpleTest._tests.push(test);
};

/*
 * Returns the absolute URL to a test data file from where tests
 * are served. i.e. the file doesn't necessarely exists where tests
 * are executed.
 *
 * (For android, mochitest are executed on the device, while
 * all mochitest html (and others) files are served from the test runner
 * slave)
 */
SimpleTest.getTestFileURL = function (path) {
  var location = window.location;
  // Remove mochitest html file name from the path
  var remotePath = location.pathname.replace(/\/[^\/]+?$/, "");
  var url = location.origin + remotePath + "/" + path;
  return url;
};

SimpleTest._getCurrentTestURL = function () {
  return (
    (SimpleTest.harnessParameters &&
      SimpleTest.harnessParameters.currentTestURL) ||
    (parentRunner && parentRunner.currentTestURL) ||
    (typeof gTestPath == "string" && gTestPath) ||
    "unknown test url"
  );
};

SimpleTest._forceLogMessageOutput = false;

/**
 * Force all test messages to be displayed.  Only applies for the current test.
 */
SimpleTest.requestCompleteLog = function () {
  if (!parentRunner || SimpleTest._forceLogMessageOutput) {
    return;
  }

  parentRunner.structuredLogger.deactivateBuffering();
  SimpleTest._forceLogMessageOutput = true;

  SimpleTest.registerCleanupFunction(function () {
    parentRunner.structuredLogger.activateBuffering();
    SimpleTest._forceLogMessageOutput = false;
  });
};

SimpleTest._logResult = function (test, passInfo, failInfo, stack) {
  var url = SimpleTest._getCurrentTestURL();
  var result = test.result ? passInfo : failInfo;
  var diagnostic = test.diag || null;
  // BUGFIX : coercing test.name to a string, because some a11y tests pass an xpconnect object
  var subtest = test.name ? String(test.name) : null;
  var isError = !test.result == !test.todo;

  if (parentRunner) {
    if (!result.status || !result.expected) {
      if (diagnostic) {
        parentRunner.structuredLogger.info(diagnostic);
      }
      return;
    }

    if (isError) {
      parentRunner.addFailedTest(url);
    }

    parentRunner.structuredLogger.testStatus(
      url,
      subtest,
      result.status,
      result.expected,
      diagnostic,
      stack
    );
  } else if (typeof dump === "function") {
    var diagMessage = test.name + (test.diag ? " - " + test.diag : "");
    var debugMsg = [result.message, url, diagMessage].join(" | ");
    dump(debugMsg + "\n");
  } else {
    // Non-Mozilla browser?  Just do nothing.
  }
};

SimpleTest.info = function (name, message) {
  var log = message ? name + " | " + message : name;
  if (parentRunner) {
    parentRunner.structuredLogger.info(log);
  } else {
    dump(log + "\n");
  }
};

/**
 * Copies of is and isnot with the call to ok replaced by a call to todo.
 **/

SimpleTest.todo_is = function (a, b, name) {
  var pass = Object.is(a, b);
  var diag = pass
    ? repr(a) + " should equal " + repr(b)
    : "got " + repr(a) + ", expected " + repr(b);
  SimpleTest.todo(pass, name, diag);
};

SimpleTest.todo_isnot = function (a, b, name) {
  var pass = !Object.is(a, b);
  var diag = pass
    ? repr(a) + " should not equal " + repr(b)
    : "didn't expect " + repr(a) + ", but got it";
  SimpleTest.todo(pass, name, diag);
};

/**
 * Makes a test report, returns it as a DIV element.
 **/
SimpleTest.report = function () {
  var passed = 0;
  var failed = 0;
  var todo = 0;

  var tallyAndCreateDiv = function (test) {
    var cls, msg, div;
    var diag = test.diag ? " - " + test.diag : "";
    if (test.todo && !test.result) {
      todo++;
      cls = "test_todo";
      msg = "todo | " + test.name + diag;
    } else if (test.result && !test.todo) {
      passed++;
      cls = "test_ok";
      msg = "passed | " + test.name + diag;
    } else {
      failed++;
      cls = "test_not_ok";
      msg = "failed | " + test.name + diag;
    }
    div = createEl("div", { class: cls }, msg);
    return div;
  };
  var results = [];
  for (var d = 0; d < SimpleTest._tests.length; d++) {
    results.push(tallyAndCreateDiv(SimpleTest._tests[d]));
  }

  var summary_class =
    // eslint-disable-next-line no-nested-ternary
    failed != 0 ? "some_fail" : passed == 0 ? "todo_only" : "all_pass";

  var div1 = createEl("div", { class: "tests_report" });
  var div2 = createEl("div", { class: "tests_summary " + summary_class });
  var div3 = createEl("div", { class: "tests_passed" }, "Passed: " + passed);
  var div4 = createEl("div", { class: "tests_failed" }, "Failed: " + failed);
  var div5 = createEl("div", { class: "tests_todo" }, "Todo: " + todo);
  div2.appendChild(div3);
  div2.appendChild(div4);
  div2.appendChild(div5);
  div1.appendChild(div2);
  for (var t = 0; t < results.length; t++) {
    //iterate in order
    div1.appendChild(results[t]);
  }
  return div1;
};

/**
 * Toggle element visibility
 **/
SimpleTest.toggle = function (el) {
  if (computedStyle(el, "display") == "block") {
    el.style.display = "none";
  } else {
    el.style.display = "block";
  }
};

/**
 * Toggle visibility for divs with a specific class.
 **/
SimpleTest.toggleByClass = function (cls, evt) {
  var children = document.getElementsByTagName("div");
  var elements = [];
  for (var i = 0; i < children.length; i++) {
    var child = children[i];
    var clsName = child.className;
    if (!clsName) {
      continue;
    }
    var classNames = clsName.split(" ");
    for (var j = 0; j < classNames.length; j++) {
      if (classNames[j] == cls) {
        elements.push(child);
        break;
      }
    }
  }
  for (var t = 0; t < elements.length; t++) {
    //TODO: again, for-in loop over elems seems to break this
    SimpleTest.toggle(elements[t]);
  }
  if (evt) {
    evt.preventDefault();
  }
};

/**
 * Shows the report in the browser
 **/
SimpleTest.showReport = function () {
  var togglePassed = createEl("a", { href: "#" }, "Toggle passed checks");
  var toggleFailed = createEl("a", { href: "#" }, "Toggle failed checks");
  var toggleTodo = createEl("a", { href: "#" }, "Toggle todo checks");
  togglePassed.onclick = partial(SimpleTest.toggleByClass, "test_ok");
  toggleFailed.onclick = partial(SimpleTest.toggleByClass, "test_not_ok");
  toggleTodo.onclick = partial(SimpleTest.toggleByClass, "test_todo");
  var body = document.body; // Handles HTML documents
  if (!body) {
    // Do the XML thing.
    body = document.getElementsByTagNameNS(
      "http://www.w3.org/1999/xhtml",
      "body"
    )[0];
  }
  var firstChild = body.childNodes[0];
  var addNode;
  if (firstChild) {
    addNode = function (el) {
      body.insertBefore(el, firstChild);
    };
  } else {
    addNode = function (el) {
      body.appendChild(el);
    };
  }
  addNode(togglePassed);
  addNode(createEl("span", null, " "));
  addNode(toggleFailed);
  addNode(createEl("span", null, " "));
  addNode(toggleTodo);
  addNode(SimpleTest.report());
  // Add a separator from the test content.
  addNode(createEl("hr"));
};

/**
 * Tells SimpleTest to don't finish the test when the document is loaded,
 * useful for asynchronous tests.
 *
 * When SimpleTest.waitForExplicitFinish is called,
 * explicit SimpleTest.finish() is required.
 **/
SimpleTest.waitForExplicitFinish = function () {
  SimpleTest._stopOnLoad = false;
};

/**
 * Multiply the timeout the parent runner uses for this test by the
 * given factor.
 *
 * For example, in a test that may take a long time to complete, using
 * "SimpleTest.requestLongerTimeout(5)" will give it 5 times as long to
 * finish.
 *
 * @param {Number} factor
 *        The multiplication factor to use on the timeout for this test.
 */
SimpleTest.requestLongerTimeout = function (factor) {
  if (parentRunner) {
    parentRunner.requestLongerTimeout(factor);
  } else {
    dump(
      "[SimpleTest.requestLongerTimeout()] ignoring request, maybe you meant to call the global `requestLongerTimeout` instead?\n"
    );
  }
};

/**
 * Note that the given range of assertions is to be expected.  When
 * this function is not called, 0 assertions are expected.  When only
 * one argument is given, that number of assertions are expected.
 *
 * A test where we expect to have assertions (which should largely be a
 * transitional mechanism to get assertion counts down from our current
 * situation) can call the SimpleTest.expectAssertions() function, with
 * either one or two arguments:  one argument gives an exact number
 * expected, and two arguments give a range.  For example, a test might do
 * one of the following:
 *
 * @example
 *
 *   // Currently triggers two assertions (bug NNNNNN).
 *   SimpleTest.expectAssertions(2);
 *
 *   // Currently triggers one assertion on Mac (bug NNNNNN).
 *   if (navigator.platform.indexOf("Mac") == 0) {
 *     SimpleTest.expectAssertions(1);
 *   }
 *
 *   // Currently triggers two assertions on all platforms (bug NNNNNN),
 *   // but intermittently triggers two additional assertions (bug NNNNNN)
 *   // on Windows.
 *   if (navigator.platform.indexOf("Win") == 0) {
 *     SimpleTest.expectAssertions(2, 4);
 *   } else {
 *     SimpleTest.expectAssertions(2);
 *   }
 *
 *   // Intermittently triggers up to three assertions (bug NNNNNN).
 *   SimpleTest.expectAssertions(0, 3);
 */
SimpleTest.expectAssertions = function (min, max) {
  if (parentRunner) {
    parentRunner.expectAssertions(min, max);
  }
};

SimpleTest._flakyTimeoutIsOK = false;
SimpleTest._originalSetTimeout = window.setTimeout;
window.setTimeout = function SimpleTest_setTimeoutShim() {
  // Don't break tests that are loaded without a parent runner.
  if (parentRunner) {
    // Right now, we only enable these checks for mochitest-plain.
    switch (SimpleTest.harnessParameters.testRoot) {
      case "browser":
      case "chrome":
      case "a11y":
        break;
      default:
        if (
          !SimpleTest._alreadyFinished &&
          arguments.length > 1 &&
          arguments[1] > 0
        ) {
          if (SimpleTest._flakyTimeoutIsOK) {
            SimpleTest.todo(
              false,
              "The author of the test has indicated that flaky timeouts are expected.  Reason: " +
                SimpleTest._flakyTimeoutReason
            );
          } else {
            SimpleTest.ok(
              false,
              "Test attempted to use a flaky timeout value " + arguments[1]
            );
          }
        }
    }
  }
  return SimpleTest._originalSetTimeout.apply(window, arguments);
};

/**
 * Request the framework to allow usage of setTimeout(func, timeout)
 * where ``timeout > 0``.  This is required to note that the author of
 * the test is aware of the inherent flakiness in the test caused by
 * that, and asserts that there is no way around using the magic timeout
 * value number for some reason.
 *
 * Use of this function is **STRONGLY** discouraged.  Think twice before
 * using it.  Such magic timeout values could result in intermittent
 * failures in your test, and are almost never necessary!
 *
 * @param {String} reason
 *        A string representation of the reason why the test needs timeouts.
 *
 */
SimpleTest.requestFlakyTimeout = function (reason) {
  SimpleTest.is(typeof reason, "string", "A valid string reason is expected");
  SimpleTest.isnot(reason, "", "Reason cannot be empty");
  SimpleTest._flakyTimeoutIsOK = true;
  SimpleTest._flakyTimeoutReason = reason;
};

/**
 * If the page is not yet loaded, waits for the load event. If the page is
 * not yet focused, focuses and waits for the window to be focused.
 * If the current page is 'about:blank', then the page is assumed to not
 * yet be loaded. Pass true for expectBlankPage to not make this assumption
 * if you expect a blank page to be present.
 *
 * The target object should be specified if it is different than 'window'. The
 * actual focused window may be a descendant window of aObject.
 *
 * @param {Window|browser|BrowsingContext} [aObject]
 *        Optional object to be focused, and may be any of:
 *          window - a window object to focus
 *          browser - a <browser>/<iframe> element. The top-level window
 *                    within the frame will be focused.
 *          browsing context - a browsing context containing a window to focus
 *        If not specified, defaults to the global 'window'.
 * @param {boolean} [expectBlankPage=false]
 *        True if targetWindow.location is 'about:blank'.
 * @param {boolean} [aBlurSubframe=false]
 *        If true, and a subframe within the window to focus is focused, blur
 *        it so that the specified window or browsing context will receive
 *        focus events.
 *
 * @returns The browsing context that was focused.
 */
SimpleTest.promiseFocus = async function (
  aObject,
  aExpectBlankPage = false,
  aBlurSubframe = false
) {
  let browser;
  let browsingContext;
  let windowToFocus;

  if (!aObject) {
    aObject = window;
  }

  async function waitForEvent(aTarget, aEventName) {
    return new Promise(resolve => {
      aTarget.addEventListener(aEventName, resolve, {
        capture: true,
        once: true,
      });
    });
  }

  if (SpecialPowers.wrap(Window).isInstance(aObject)) {
    windowToFocus = aObject;

    let isBlank = windowToFocus.location.href == "about:blank";
    if (
      aExpectBlankPage != isBlank ||
      windowToFocus.document.readyState != "complete"
    ) {
      info("must wait for load");
      await waitForEvent(windowToFocus, "load");
    }
  } else {
    if (SpecialPowers.wrap(Element).isInstance(aObject)) {
      // assume this is a browser/iframe element
      browsingContext = aObject.browsingContext;
    } else {
      browsingContext = aObject;
    }

    browser =
      browsingContext == aObject ? aObject.top.embedderElement : aObject;
    windowToFocus = browser.ownerGlobal;
  }

  if (!windowToFocus.document.hasFocus()) {
    info("must wait for focus");
    let focusPromise = waitForEvent(windowToFocus.document, "focus");
    SpecialPowers.focus(windowToFocus);
    await focusPromise;
  }

  if (browser) {
    if (windowToFocus.document.activeElement != browser) {
      browser.focus();
    }

    info("must wait for focus in content");

    // Make sure that the child process thinks it is focused as well.
    await SpecialPowers.ensureFocus(browsingContext, aBlurSubframe);
  } else {
    if (aBlurSubframe) {
      SpecialPowers.clearFocus(windowToFocus);
    }

    browsingContext = windowToFocus.browsingContext;
  }

  // Some tests rely on this delay, likely expecting layout or paint to occur.
  await new Promise(resolve => {
    SimpleTest.executeSoon(resolve);
  });

  return browsingContext;
};

/**
 * Version of promiseFocus that uses a callback. For compatibility,
 * the callback is passed one argument, the window that was focused.
 * If the focused window is not in the same process, null is supplied.
 */
SimpleTest.waitForFocus = function (callback, aObject, expectBlankPage) {
  SimpleTest.promiseFocus(aObject, expectBlankPage).then(focusedBC => {
    callback(focusedBC?.window);
  });
};
/* eslint-enable mozilla/use-services */

SimpleTest.stripLinebreaksAndWhitespaceAfterTags = function (aString) {
  return aString.replace(/(>\s*(\r\n|\n|\r)*\s*)/gm, ">");
};

/*
 * `navigator.platform` should include this, when the platform is Windows.
 */
const kPlatformWindows = "Win";

/*
 * See `SimpleTest.waitForClipboard`.
 */
const kTextHtmlPrefixClipboardDataWindows =
  "<html><body>\n<!--StartFragment-->";

/*
 * See `SimpleTest.waitForClipboard`.
 */
const kTextHtmlSuffixClipboardDataWindows =
  "<!--EndFragment-->\n</body>\n</html>";

/*
 * Polls the clipboard waiting for the expected value. A known value different than
 * the expected value is put on the clipboard first (and also polled for) so we
 * can be sure the value we get isn't just the expected value because it was already
 * on the clipboard. This only uses the global clipboard and only for text/plain
 * values.
 *
 * @param {String|Function} aExpectedStringOrValidatorFn
 *        The string value that is expected to be on the clipboard, or a
 *        validator function getting expected clipboard data and returning a bool.
 *        If you specify string value, line breakers in clipboard are treated
 *        as LineFeed.  Therefore, you cannot include CarriageReturn to the
 *        string.
 *        If you specify string value and expect "text/html" data, this wraps
 *        the expected value with `kTextHtmlPrefixClipboardDataWindows` and
 *        `kTextHtmlSuffixClipboardDataWindows` only when it runs on Windows
 *        because they are appended only by nsDataObj.cpp for Windows.
 *        https://searchfox.org/mozilla-central/rev/8f7b017a31326515cb467e69eef1f6c965b4f00e/widget/windows/nsDataObj.cpp#1798-1805,1839-1840,1842
 *        Therefore, you can specify selected (copied) HTML data simply on any
 *        platforms.
 * @param {Function} aSetupFn
 *        A function responsible for setting the clipboard to the expected value,
 *        called after the known value setting succeeds.
 * @param {Function} aSuccessFn
 *        A function called when the expected value is found on the clipboard.
 * @param {Function} aFailureFn
 *        A function called if the expected value isn't found on the clipboard
 *        within 5s. It can also be called if the known value can't be found.
 * @param {String} [aFlavor="text/plain"]
 *        The flavor to look for.
 * @param {Number} [aTimeout=5000]
 *        The timeout (in milliseconds) to wait for a clipboard change.
 * @param {boolean} [aExpectFailure=false]
 *        If true, fail if the clipboard contents are modified within the timeout
 *        interval defined by aTimeout.  When aExpectFailure is true, the argument
 *        aExpectedStringOrValidatorFn must be null, as it won't be used.
 * @param {boolean} [aDontInitializeClipboardIfExpectFailure=false]
 *        If aExpectFailure and this is set to true, this does NOT initialize
 *        clipboard with random data before running aSetupFn.
 */
SimpleTest.waitForClipboard = function (
  aExpectedStringOrValidatorFn,
  aSetupFn,
  aSuccessFn,
  aFailureFn,
  aFlavor,
  aTimeout,
  aExpectFailure,
  aDontInitializeClipboardIfExpectFailure
) {
  let promise = SimpleTest.promiseClipboardChange(
    aExpectedStringOrValidatorFn,
    aSetupFn,
    aFlavor,
    aTimeout,
    aExpectFailure,
    aDontInitializeClipboardIfExpectFailure
  );
  promise.then(aSuccessFn).catch(aFailureFn);
};

/**
 * Promise-oriented version of waitForClipboard.
 */
SimpleTest.promiseClipboardChange = async function (
  aExpectedStringOrValidatorFn,
  aSetupFn,
  aFlavor,
  aTimeout,
  aExpectFailure,
  aDontInitializeClipboardIfExpectFailure
) {
  let requestedFlavor = aFlavor || "text/plain";

  // The known value we put on the clipboard before running aSetupFn
  let initialVal = "waitForClipboard-known-value-" + Math.random();
  let preExpectedVal = initialVal;

  let inputValidatorFn;
  if (aExpectFailure) {
    // If we expect failure, the aExpectedStringOrValidatorFn should be null
    if (aExpectedStringOrValidatorFn !== null) {
      SimpleTest.ok(
        false,
        "When expecting failure, aExpectedStringOrValidatorFn must be null"
      );
    }

    inputValidatorFn = function (aData) {
      return aData != initialVal;
    };
    // Build a default validator function for common string input.
  } else if (typeof aExpectedStringOrValidatorFn == "string") {
    if (aExpectedStringOrValidatorFn.includes("\r")) {
      throw new Error(
        "Use function instead of string to compare raw line breakers in clipboard"
      );
    }
    if (requestedFlavor === "text/html" && navigator.platform.includes("Win")) {
      inputValidatorFn = function (aData) {
        return (
          aData.replace(/\r\n?/g, "\n") ===
          kTextHtmlPrefixClipboardDataWindows +
            aExpectedStringOrValidatorFn +
            kTextHtmlSuffixClipboardDataWindows
        );
      };
    } else {
      inputValidatorFn = function (aData) {
        return aData.replace(/\r\n?/g, "\n") === aExpectedStringOrValidatorFn;
      };
    }
  } else {
    inputValidatorFn = aExpectedStringOrValidatorFn;
  }

  let maxPolls = aTimeout ? aTimeout / 100 : 50;

  async function putAndVerify(operationFn, validatorFn, flavor, expectFailure) {
    await operationFn();

    let data;
    for (let i = 0; i < maxPolls; i++) {
      data = SpecialPowers.getClipboardData(flavor);
      if (validatorFn(data)) {
        // Don't show the success message when waiting for preExpectedVal
        if (preExpectedVal) {
          preExpectedVal = null;
        } else {
          SimpleTest.ok(
            !expectFailure,
            "Clipboard has the given value: '" + data + "'"
          );
        }

        return data;
      }

      // Wait 100ms and check again.
      await new Promise(resolve => {
        SimpleTest._originalSetTimeout.apply(window, [resolve, 100]);
      });
    }

    let errorMsg = `Timed out while polling clipboard for ${
      preExpectedVal ? "initialized" : "requested"
    } data, got: ${data}`;
    SimpleTest.ok(expectFailure, errorMsg);
    if (!expectFailure) {
      throw new Error(errorMsg);
    }
    return data;
  }

  if (!aExpectFailure || !aDontInitializeClipboardIfExpectFailure) {
    // First we wait for a known value different from the expected one.
    SimpleTest.info(`Initializing clipboard with "${preExpectedVal}"...`);
    await putAndVerify(
      function () {
        SpecialPowers.clipboardCopyString(preExpectedVal);
      },
      function (aData) {
        return aData == preExpectedVal;
      },
      "text/plain",
      false
    );

    SimpleTest.info(
      "Succeeded initializing clipboard, start requested things..."
    );
  } else {
    preExpectedVal = null;
  }

  return putAndVerify(
    aSetupFn,
    inputValidatorFn,
    requestedFlavor,
    aExpectFailure
  );
};

/**
 * Wait for a condition for a while (actually up to 3s here).
 *
 * @param {Function} aCond
 *        A function returns the result of the condition
 * @param {Function} aCallback
 *        A function called after the condition is passed or timeout.
 * @param {String} aErrorMsg
 *        The message displayed when the condition failed to pass
 *        before timeout.
 */
SimpleTest.waitForCondition = function (aCond, aCallback, aErrorMsg) {
  this.promiseWaitForCondition(aCond, aErrorMsg).then(() => aCallback());
};
SimpleTest.promiseWaitForCondition = async function (aCond, aErrorMsg) {
  for (let tries = 0; tries < 30; ++tries) {
    // Wait 100ms between checks.
    await new Promise(resolve => {
      SimpleTest._originalSetTimeout.apply(window, [resolve, 100]);
    });

    let conditionPassed;
    try {
      conditionPassed = await aCond();
    } catch (e) {
      ok(false, `${e}\n${e.stack}`);
      conditionPassed = false;
    }
    if (conditionPassed) {
      return;
    }
  }
  ok(false, aErrorMsg);
};

/**
 * Executes a function shortly after the call, but lets the caller continue
 * working (or finish).
 *
 * @param {Function} aFunc
 *        Function to execute soon.
 */
SimpleTest.executeSoon = function (aFunc) {
  if ("SpecialPowers" in window) {
    return SpecialPowers.executeSoon(aFunc, window);
  }
  setTimeout(aFunc, 0);
  return null; // Avoid warning.
};

/**
 * Register a cleanup/teardown function (which may be async) to run after all
 * tasks have finished, before running the next test. If async (or the function
 * returns a promise), the framework will wait for the promise/async function
 * to resolve.
 *
 * @param {Function} aFunc
 *        The cleanup/teardown function to run.
 */
SimpleTest.registerCleanupFunction = function (aFunc) {
  SimpleTest._cleanupFunctions.push(aFunc);
};

/**
 * Register a cleanup/teardown function (which may be async) to run after the
 * current task has finished, before running the next task. If async (or the
 * function returns a promise), the framework will wait for the promise/async
 * function to resolve.
 *
 * @param {Function} aFunc
 *        The cleanup/teardown function to run.
 */
SimpleTest.registerCurrentTaskCleanupFunction = function (aFunc) {
  if (!SimpleTest._currentTask) {
    return;
  }
  SimpleTest._currentTask._cleanupFunctions ||= [];
  SimpleTest._currentTask._cleanupFunctions.push(aFunc);
};

/**
 * Register a cleanup/teardown function (which may be async) to run after each
 * task has finished, before running the next task. If async (or the
 * function returns a promise), the framework will wait for the promise/async
 * function to resolve.
 *
 * @param {Function} aFunc
 *        The cleanup/teardown function to run.
 */
SimpleTest.registerTaskCleanupFunction = function (aFunc) {
  SimpleTest._taskCleanupFunctions.push(aFunc);
};

SimpleTest.registerTimeoutFunction = function (aFunc) {
  SimpleTest._timeoutFunctions.push(aFunc);
};

SimpleTest.testInChaosMode = function () {
  if (SimpleTest._inChaosMode) {
    // It's already enabled for this test, don't enter twice
    return;
  }
  SpecialPowers.DOMWindowUtils.enterChaosMode();
  SimpleTest._inChaosMode = true;
  // increase timeout here as chaosmode is very slow (i.e. 10x)
  // doing 20x as this overwrites anything the tests set
  SimpleTest.requestLongerTimeout(20);
};

SimpleTest.timeout = async function () {
  for (const func of SimpleTest._timeoutFunctions) {
    await func();
  }
  SimpleTest._timeoutFunctions = [];
};

SimpleTest.finishWithFailure = function (msg) {
  SimpleTest.ok(false, msg);
  SimpleTest.finish();
};

/**
 * Finishes the tests. This is automatically called, except when
 * SimpleTest.waitForExplicitFinish() has been invoked.
 **/
SimpleTest.finish = function () {
  if (SimpleTest._alreadyFinished) {
    var err =
      "TEST-UNEXPECTED-FAIL | SimpleTest | this test already called finish!";
    if (parentRunner) {
      parentRunner.structuredLogger.error(err);
    } else {
      dump(err + "\n");
    }
  }

  if (SimpleTest.expected == "fail" && SimpleTest.num_failed <= 0) {
    let msg = "We expected at least one failure";
    let test = {
      result: false,
      name: "fail-if condition in manifest",
      diag: msg,
    };
    let successInfo = {
      status: "FAIL",
      expected: "FAIL",
      message: "TEST-KNOWN-FAIL",
    };
    let failureInfo = {
      status: "PASS",
      expected: "FAIL",
      message: "TEST-UNEXPECTED-PASS",
    };
    SimpleTest._logResult(test, successInfo, failureInfo);
    SimpleTest._tests.push(test);
  } else if (usesFailurePatterns()) {
    SimpleTest.expected.forEach(([pat, expected_count], i) => {
      let count = SimpleTest.num_failed[i];
      let diag;
      if (expected_count === null && count == 0) {
        diag = "expected some failures but got none";
      } else if (expected_count !== null && expected_count != count) {
        diag = `expected ${expected_count} failures but got ${count}`;
      } else {
        return;
      }
      let name = pat
        ? `failure pattern \`${pat}\` in this test`
        : "failures in this test";
      let test = { result: false, name, diag };
      let successInfo = {
        status: "PASS",
        expected: "PASS",
        message: "TEST-PASS",
      };
      let failureInfo = {
        status: "FAIL",
        expected: "PASS",
        message: "TEST-UNEXPECTED-FAIL",
      };
      SimpleTest._logResult(test, successInfo, failureInfo);
      SimpleTest._tests.push(test);
    });
  }

  SimpleTest._timeoutFunctions = [];

  SimpleTest.testsLength = SimpleTest._tests.length;

  SimpleTest._alreadyFinished = true;

  if (SimpleTest._inChaosMode) {
    SpecialPowers.DOMWindowUtils.leaveChaosMode();
    SimpleTest._inChaosMode = false;
  }

  var afterCleanup = async function () {
    SpecialPowers.removeFiles();

    if (SpecialPowers.DOMWindowUtils.isTestControllingRefreshes) {
      SimpleTest.ok(false, "test left refresh driver under test control");
      SpecialPowers.DOMWindowUtils.restoreNormalRefresh();
    }
    if (SimpleTest._expectingUncaughtException) {
      SimpleTest.ok(
        false,
        "expectUncaughtException was called but no uncaught exception was detected!"
      );
    }
    if (!SimpleTest._tests.length) {
      SimpleTest.ok(
        false,
        "[SimpleTest.finish()] No checks actually run. " +
          "(You need to call ok(), is(), or similar " +
          "functions at least once.  Make sure you use " +
          "SimpleTest.waitForExplicitFinish() if you need " +
          "it.)"
      );
    }

    let workers = await SpecialPowers.registeredServiceWorkers();
    let promise = null;
    if (SimpleTest._expectingRegisteredServiceWorker) {
      if (workers.length === 0) {
        SimpleTest.ok(
          false,
          "This test is expected to leave a service worker registered"
        );
      }
    } else if (workers.length) {
      let FULL_PROFILE_WORKERS_TO_IGNORE = [];
      if (parentRunner.conditionedProfile) {
        // Full profile has service workers in the profile, without clearing the
        // profile service workers will be leftover.  We perform a startsWith
        // check below because some origins (s.0cf.io) use a cache-busting query
        // parameter.
        FULL_PROFILE_WORKERS_TO_IGNORE = [
          "https://www.youtube.com/sw.js",
          "https://s.0cf.io/sw.js",
        ];
      } else {
        SimpleTest.ok(
          false,
          "This test left a service worker registered without cleaning it up"
        );
      }

      for (let worker of workers) {
        if (
          FULL_PROFILE_WORKERS_TO_IGNORE.some(ignoreBase =>
            worker.scriptSpec.startsWith(ignoreBase)
          )
        ) {
          continue;
        }
        SimpleTest.ok(
          false,
          `Left over worker: ${worker.scriptSpec} (scope: ${worker.scope})`
        );
      }
      promise = SpecialPowers.removeAllServiceWorkerData();
    }

    // If we want to wait for removeAllServiceWorkerData to finish, above,
    // there's a small chance that spinning the event loop could cause
    // SpecialPowers and SimpleTest to go away (e.g. if the test did
    // document.open). promise being non-null should be rare (a test would
    // have had to already fail by leaving a service worker around), so
    // limit the chances of the async wait happening to that case.
    function finish() {
      if (parentRunner) {
        /* We're running in an iframe, and the parent has a TestRunner */
        parentRunner.testFinished(SimpleTest._tests);
      }

      if (!parentRunner || parentRunner.showTestReport) {
        SpecialPowers.flushPermissions(function () {
          SpecialPowers.flushPrefEnv(function () {
            SimpleTest.showReport();
          });
        });
      }
    }

    if (promise) {
      promise.then(finish);
    } else {
      finish();
    }
  };

  var executeCleanupFunction = function () {
    var func = SimpleTest._cleanupFunctions.pop();

    if (!func) {
      afterCleanup();
      return;
    }

    var ret;
    try {
      ret = func();
    } catch (ex) {
      SimpleTest.ok(false, "Cleanup function threw exception: " + ex);
    }

    if (ret && ret.constructor.name == "Promise") {
      ret.then(executeCleanupFunction, ex =>
        SimpleTest.ok(false, "Cleanup promise rejected: " + ex)
      );
    } else {
      executeCleanupFunction();
    }
  };

  executeCleanupFunction();

  SpecialPowers.notifyObservers(null, "test-complete");
};

/**
 * Monitor console output from now until endMonitorConsole is called.
 *
 * Expect to receive all console messages described by the elements of
 * ``msgs``, an array, in the order listed in ``msgs``; each element is an
 * object which may have any number of the following properties:
 *
 *   message, errorMessage, sourceName, sourceLine, category: string or regexp
 *   lineNumber, columnNumber: number
 *   isScriptError, isWarning: boolean
 *
 * Strings, numbers, and booleans must compare equal to the named
 * property of the Nth console message.  Regexps must match.  Any
 * fields present in the message but not in the pattern object are ignored.
 *
 * In addition to the above properties, elements in ``msgs`` may have a ``forbid``
 * boolean property.  When ``forbid`` is true, a failure is logged each time a
 * matching message is received.
 *
 * If ``forbidUnexpectedMsgs`` is true, then the messages received in the console
 * must exactly match the non-forbidden messages in ``msgs``; for each received
 * message not described by the next element in ``msgs``, a failure is logged.  If
 * false, then other non-forbidden messages are ignored, but all expected
 * messages must still be received.
 *
 * After endMonitorConsole is called, ``continuation`` will be called
 * asynchronously.  (Normally, you will want to pass ``SimpleTest.finish`` here.)
 *
 * It is incorrect to use this function in a test which has not called
 * SimpleTest.waitForExplicitFinish.
 */
SimpleTest.monitorConsole = function (
  continuation,
  msgs,
  forbidUnexpectedMsgs
) {
  if (SimpleTest._stopOnLoad) {
    ok(false, "Console monitoring requires use of waitForExplicitFinish.");
  }

  function msgMatches(msg, pat) {
    for (var k in pat) {
      if (!(k in msg)) {
        return false;
      }
      if (pat[k] instanceof RegExp && typeof msg[k] === "string") {
        if (!pat[k].test(msg[k])) {
          return false;
        }
      } else if (msg[k] !== pat[k]) {
        return false;
      }
    }
    return true;
  }

  var forbiddenMsgs = [];
  var i = 0;
  while (i < msgs.length) {
    let pat = msgs[i];
    if ("forbid" in pat) {
      var forbid = pat.forbid;
      delete pat.forbid;
      if (forbid) {
        forbiddenMsgs.push(pat);
        msgs.splice(i, 1);
        continue;
      }
    }
    i++;
  }

  var counter = 0;
  var assertionLabel = JSON.stringify(msgs);
  function listener(msg) {
    if (msg.message === "SENTINEL" && !msg.isScriptError) {
      is(
        counter,
        msgs.length,
        "monitorConsole | number of messages " + assertionLabel
      );
      SimpleTest.executeSoon(continuation);
      return;
    }
    for (let pat of forbiddenMsgs) {
      if (msgMatches(msg, pat)) {
        ok(
          false,
          "monitorConsole | observed forbidden message " + JSON.stringify(msg)
        );
        return;
      }
    }
    if (counter >= msgs.length) {
      var str = "monitorConsole | extra message | " + JSON.stringify(msg);
      if (forbidUnexpectedMsgs) {
        ok(false, str);
      } else {
        info(str);
      }
      return;
    }
    var matches = msgMatches(msg, msgs[counter]);
    if (forbidUnexpectedMsgs) {
      ok(
        matches,
        "monitorConsole | [" + counter + "] must match " + JSON.stringify(msg)
      );
    } else {
      info(
        "monitorConsole | [" +
          counter +
          "] " +
          (matches ? "matched " : "did not match ") +
          JSON.stringify(msg)
      );
    }
    if (matches) {
      counter++;
    }
  }
  SpecialPowers.registerConsoleListener(listener);
};

/**
 * Stop monitoring console output.
 */
SimpleTest.endMonitorConsole = function () {
  SpecialPowers.postConsoleSentinel();
};

/**
 * Run ``testfn`` synchronously, and monitor its console output.
 *
 * ``msgs`` is handled as described above for monitorConsole.
 *
 * After ``testfn`` returns, console monitoring will stop, and ``continuation``
 * will be called asynchronously.
 *
 */
SimpleTest.expectConsoleMessages = function (testfn, msgs, continuation) {
  SimpleTest.monitorConsole(continuation, msgs);
  testfn();
  SimpleTest.executeSoon(SimpleTest.endMonitorConsole);
};

/**
 * Wrapper around ``expectConsoleMessages`` for the case where the test has
 * only one ``testfn`` to run.
 */
SimpleTest.runTestExpectingConsoleMessages = function (testfn, msgs) {
  SimpleTest.waitForExplicitFinish();
  SimpleTest.expectConsoleMessages(testfn, msgs, SimpleTest.finish);
};

/**
 * Indicates to the test framework that the current test expects one or
 * more crashes (from plugins or IPC documents), and that the minidumps from
 * those crashes should be removed.
 */
SimpleTest.expectChildProcessCrash = function () {
  if (parentRunner) {
    parentRunner.expectChildProcessCrash();
  }
};

/**
 * Indicates to the test framework that the next uncaught exception during
 * the test is expected, and should not cause a test failure.
 */
SimpleTest.expectUncaughtException = function (aExpecting) {
  SimpleTest._expectingUncaughtException =
    aExpecting === void 0 || !!aExpecting;
};

/**
 * Returns whether the test has indicated that it expects an uncaught exception
 * to occur.
 */
SimpleTest.isExpectingUncaughtException = function () {
  return SimpleTest._expectingUncaughtException;
};

/**
 * Indicates to the test framework that all of the uncaught exceptions
 * during the test are known problems that should be fixed in the future,
 * but which should not cause the test to fail currently.
 */
SimpleTest.ignoreAllUncaughtExceptions = function (aIgnoring) {
  SimpleTest._ignoringAllUncaughtExceptions =
    aIgnoring === void 0 || !!aIgnoring;
};

/**
 * Returns whether the test has indicated that all uncaught exceptions should be
 * ignored.
 */
SimpleTest.isIgnoringAllUncaughtExceptions = function () {
  return SimpleTest._ignoringAllUncaughtExceptions;
};

/**
 * Indicates to the test framework that this test is expected to leave a
 * service worker registered when it finishes.
 */
SimpleTest.expectRegisteredServiceWorker = function () {
  SimpleTest._expectingRegisteredServiceWorker = true;
};

/**
 * Resets any state this SimpleTest object has.  This is important for
 * browser chrome mochitests, which reuse the same SimpleTest object
 * across a run.
 */
SimpleTest.reset = function () {
  SimpleTest._ignoringAllUncaughtExceptions = false;
  SimpleTest._expectingUncaughtException = false;
  SimpleTest._expectingRegisteredServiceWorker = false;
  SimpleTest._bufferedMessages = [];
};

if (isPrimaryTestWindow) {
  addLoadEvent(function () {
    if (SimpleTest._stopOnLoad) {
      SimpleTest.finish();
    }
  });
}

//  --------------- Test.Builder/Test.More isDeeply() -----------------

SimpleTest.DNE = { dne: "Does not exist" };
SimpleTest.LF = "\r\n";

SimpleTest._deepCheck = function (e1, e2, stack, seen) {
  var ok = false;
  if (Object.is(e1, e2)) {
    // Handles identical primitives and references.
    ok = true;
  } else if (
    typeof e1 != "object" ||
    typeof e2 != "object" ||
    e1 === null ||
    e2 === null
  ) {
    // If either argument is a primitive or function, don't consider the arguments the same.
    ok = false;
  } else if (e1 == SimpleTest.DNE || e2 == SimpleTest.DNE) {
    ok = false;
  } else if (SimpleTest.isa(e1, "Array") && SimpleTest.isa(e2, "Array")) {
    ok = SimpleTest._eqArray(e1, e2, stack, seen);
  } else {
    ok = SimpleTest._eqAssoc(e1, e2, stack, seen);
  }
  return ok;
};

SimpleTest._eqArray = function (a1, a2, stack, seen) {
  // Return if they're the same object.
  if (a1 == a2) {
    return true;
  }

  // JavaScript objects have no unique identifiers, so we have to store
  // references to them all in an array, and then compare the references
  // directly. It's slow, but probably won't be much of an issue in
  // practice. Start by making a local copy of the array to as to avoid
  // confusing a reference seen more than once (such as [a, a]) for a
  // circular reference.
  for (var j = 0; j < seen.length; j++) {
    if (seen[j][0] == a1) {
      return seen[j][1] == a2;
    }
  }

  // If we get here, we haven't seen a1 before, so store it with reference
  // to a2.
  seen.push([a1, a2]);

  var ok = true;
  // Only examines enumerable attributes. Only works for numeric arrays!
  // Associative arrays return 0. So call _eqAssoc() for them, instead.
  var max = Math.max(a1.length, a2.length);
  if (max == 0) {
    return SimpleTest._eqAssoc(a1, a2, stack, seen);
  }
  for (var i = 0; i < max; i++) {
    var e1 = i < a1.length ? a1[i] : SimpleTest.DNE;
    var e2 = i < a2.length ? a2[i] : SimpleTest.DNE;
    stack.push({ type: "Array", idx: i, vals: [e1, e2] });
    ok = SimpleTest._deepCheck(e1, e2, stack, seen);
    if (ok) {
      stack.pop();
    } else {
      break;
    }
  }
  return ok;
};

SimpleTest._eqAssoc = function (o1, o2, stack, seen) {
  // Return if they're the same object.
  if (o1 == o2) {
    return true;
  }

  // JavaScript objects have no unique identifiers, so we have to store
  // references to them all in an array, and then compare the references
  // directly. It's slow, but probably won't be much of an issue in
  // practice. Start by making a local copy of the array to as to avoid
  // confusing a reference seen more than once (such as [a, a]) for a
  // circular reference.
  seen = seen.slice(0);
  for (let j = 0; j < seen.length; j++) {
    if (seen[j][0] == o1) {
      return seen[j][1] == o2;
    }
  }

  // If we get here, we haven't seen o1 before, so store it with reference
  // to o2.
  seen.push([o1, o2]);

  // They should be of the same class.

  var ok = true;
  // Only examines enumerable attributes.
  var o1Size = 0;
  // eslint-disable-next-line no-unused-vars
  for (let i in o1) {
    o1Size++;
  }
  var o2Size = 0;
  // eslint-disable-next-line no-unused-vars
  for (let i in o2) {
    o2Size++;
  }
  var bigger = o1Size > o2Size ? o1 : o2;
  for (let i in bigger) {
    var e1 = i in o1 ? o1[i] : SimpleTest.DNE;
    var e2 = i in o2 ? o2[i] : SimpleTest.DNE;
    stack.push({ type: "Object", idx: i, vals: [e1, e2] });
    ok = SimpleTest._deepCheck(e1, e2, stack, seen);
    if (ok) {
      stack.pop();
    } else {
      break;
    }
  }
  return ok;
};

SimpleTest._formatStack = function (stack) {
  var variable = "$Foo";
  for (let i = 0; i < stack.length; i++) {
    var entry = stack[i];
    var type = entry.type;
    var idx = entry.idx;
    if (idx != null) {
      if (type == "Array") {
        // Numeric array index.
        variable += "[" + idx + "]";
      } else {
        // Associative array index.
        idx = idx.replace("'", "\\'");
        variable += "['" + idx + "']";
      }
    }
  }

  var vals = stack[stack.length - 1].vals.slice(0, 2);
  var vars = [
    variable.replace("$Foo", "got"),
    variable.replace("$Foo", "expected"),
  ];

  var out = "Structures begin differing at:" + SimpleTest.LF;
  for (let i = 0; i < vals.length; i++) {
    var val = vals[i];
    if (val === SimpleTest.DNE) {
      val = "Does not exist";
    } else {
      val = repr(val);
    }
    out += vars[i] + " = " + val + SimpleTest.LF;
  }

  return "    " + out;
};

SimpleTest.isDeeply = function (it, as, name) {
  var stack = [{ vals: [it, as] }];
  var seen = [];
  if (SimpleTest._deepCheck(it, as, stack, seen)) {
    SimpleTest.record(true, name);
  } else {
    SimpleTest.record(false, name, SimpleTest._formatStack(stack));
  }
};

SimpleTest.typeOf = function (object) {
  var c = Object.prototype.toString.apply(object);
  var name = c.substring(8, c.length - 1);
  if (name != "Object") {
    return name;
  }
  // It may be a non-core class. Try to extract the class name from
  // the constructor function. This may not work in all implementations.
  if (/function ([^(\s]+)/.test(Function.toString.call(object.constructor))) {
    return RegExp.$1;
  }
  // No idea. :-(
  return name;
};

SimpleTest.isa = function (object, clas) {
  return SimpleTest.typeOf(object) == clas;
};

// Global symbols:
var ok = SimpleTest.ok;
var record = SimpleTest.record;
var is = SimpleTest.is;
var isfuzzy = SimpleTest.isfuzzy;
var isnot = SimpleTest.isnot;
var todo = SimpleTest.todo;
var todo_is = SimpleTest.todo_is;
var todo_isnot = SimpleTest.todo_isnot;
var isDeeply = SimpleTest.isDeeply;
var info = SimpleTest.info;

var gOldOnError = window.onerror;
window.onerror = function simpletestOnerror(
  errorMsg,
  url,
  lineNumber,
  columnNumber,
  originalException
) {
  // Log the message.
  // XXX Chrome mochitests sometimes trigger this window.onerror handler,
  // but there are a number of uncaught JS exceptions from those tests.
  // For now, for tests that self identify as having unintentional uncaught
  // exceptions, just dump it so that the error is visible but doesn't cause
  // a test failure.  See bug 652494.
  var isExpected = !!SimpleTest._expectingUncaughtException;
  var message = (isExpected ? "expected " : "") + "uncaught exception";
  var error = errorMsg + " at ";
  try {
    error += originalException.stack;
  } catch (e) {
    // At least use the url+line+column we were given
    error += url + ":" + lineNumber + ":" + columnNumber;
  }
  if (!SimpleTest._ignoringAllUncaughtExceptions) {
    // Don't log if SimpleTest.finish() is already called, it would cause failures
    if (!SimpleTest._alreadyFinished) {
      SimpleTest.record(isExpected, message, error);
    }
    SimpleTest._expectingUncaughtException = false;
  } else {
    SimpleTest.todo(false, message + ": " + error);
  }
  // There is no Components.stack.caller to log. (See bug 511888.)

  // Call previous handler.
  if (gOldOnError) {
    try {
      // Ignore return value: always run default handler.
      gOldOnError(errorMsg, url, lineNumber);
    } catch (e) {
      // Log the error.
      SimpleTest.info("Exception thrown by gOldOnError(): " + e);
      // Log its stack.
      if (e.stack) {
        SimpleTest.info("JavaScript error stack:\n" + e.stack);
      }
    }
  }

  if (!SimpleTest._stopOnLoad && !isExpected && !SimpleTest._alreadyFinished) {
    // Need to finish() manually here, yet let the test actually end first.
    SimpleTest.executeSoon(SimpleTest.finish);
  }
};

// Lifted from dom/media/test/manifest.js
// Make sure to not touch navigator in here, since we want to push prefs that
// will affect the APIs it exposes, but the set of exposed APIs is determined
// when Navigator.prototype is created.  So if we touch navigator before pushing
// the prefs, the APIs it exposes will not take those prefs into account.  We
// work around this by using a navigator object from a different global for our
// UA string testing.
var gAndroidSdk = null;
function getAndroidSdk() {
  if (gAndroidSdk === null) {
    var iframe = document.documentElement.appendChild(
      document.createElement("iframe")
    );
    iframe.style.display = "none";
    var nav = iframe.contentWindow.navigator;
    if (
      !nav.userAgent.includes("Mobile") &&
      !nav.userAgent.includes("Tablet")
    ) {
      gAndroidSdk = -1;
    } else {
      // See nsSystemInfo.cpp, the getProperty('version') returns different value
      // on each platforms, so we need to distinguish the android platform.
      var versionString = nav.userAgent.includes("Android")
        ? "version"
        : "sdk_version";
      gAndroidSdk = SpecialPowers.Services.sysinfo.getProperty(versionString);
    }
    document.documentElement.removeChild(iframe);
  }
  return gAndroidSdk;
}

// add_task(generatorFunction):
// Call `add_task(generatorFunction)` for each separate
// asynchronous task in a mochitest. Tasks are run consecutively.
// Before the first task, `SimpleTest.waitForExplicitFinish()`
// will be called automatically, and after the last task,
// `SimpleTest.finish()` will be called.
var add_task = (function () {
  // The list of tasks to run.
  var task_list = [];
  var run_only_this_task = null;

  function isGenerator(value) {
    return (
      value && typeof value === "object" && typeof value.next === "function"
    );
  }

  // The "add_task" function
  return function (generatorFunction, options = { isSetup: false }) {
    if (task_list.length === 0) {
      // This is the first time add_task has been called.
      // First, confirm that SimpleTest is available.
      if (!SimpleTest) {
        throw new Error("SimpleTest not available.");
      }
      // Don't stop tests until asynchronous tasks are finished.
      SimpleTest.waitForExplicitFinish();
      // Because the client is using add_task for this set of tests,
      // we need to spawn a "master task" that calls each task in succesion.
      // Use setTimeout to ensure the master task runs after the client
      // script finishes.
      setTimeout(function nextTick() {
        // If we are in a HTML document, we should wait for the document
        // to be fully loaded.
        // These checks ensure that we are in an HTML document without
        // throwing TypeError; also I am told that readyState in XUL documents
        // are totally bogus so we don't try to do this there.
        if (
          typeof window !== "undefined" &&
          typeof HTMLDocument !== "undefined" &&
          window.document instanceof HTMLDocument &&
          window.document.readyState !== "complete"
        ) {
          setTimeout(nextTick);
          return;
        }

        (async () => {
          // Allow for a task to be skipped; we need only use the structured logger
          // for this, whilst deactivating log buffering to ensure that messages
          // are always printed to stdout.
          function skipTask(name) {
            let logger = parentRunner && parentRunner.structuredLogger;
            if (!logger) {
              info("add_task | Skipping test " + name);
              return;
            }
            logger.deactivateBuffering();
            logger.testStatus(SimpleTest._getCurrentTestURL(), name, "SKIP");
            logger.warning("add_task | Skipping test " + name);
            logger.activateBuffering();
          }

          // We stop the entire test file at the first exception because this
          // may mean that the state of subsequent tests may be corrupt.
          try {
            for (var task of task_list) {
              SimpleTest._currentTask = task;
              var name = task.name || "";
              if (
                task.__skipMe ||
                (run_only_this_task && task != run_only_this_task)
              ) {
                skipTask(name);
                continue;
              }
              const taskInfo = action =>
                info(
                  `${
                    task.isSetup ? "add_setup" : "add_task"
                  } | ${action} ${name}`
                );
              taskInfo("Entering");
              let result = await task();
              if (isGenerator(result)) {
                ok(false, "Task returned a generator");
              }
              if (task._cleanupFunctions) {
                for (const fn of task._cleanupFunctions) {
                  await fn();
                }
              }
              for (const fn of SimpleTest._taskCleanupFunctions) {
                await fn();
              }
              taskInfo("Leaving");
              delete SimpleTest._currentTask;
            }
          } catch (ex) {
            try {
              let serializedEx;
              if (
                typeof ex == "string" ||
                (typeof ex == "object" && isErrorOrException(ex))
              ) {
                serializedEx = `${ex}`;
              } else {
                serializedEx = JSON.stringify(ex);
              }

              SimpleTest.record(
                false,
                serializedEx,
                "Should not throw any errors",
                ex.stack
              );
            } catch (ex2) {
              SimpleTest.record(
                false,
                "(The exception cannot be converted to string.)",
                "Should not throw any errors",
                ex.stack
              );
            }
          }
          // All tasks are finished.
          SimpleTest.finish();
        })();
      });
    }
    generatorFunction.skip = () => (generatorFunction.__skipMe = true);
    generatorFunction.only = () => (run_only_this_task = generatorFunction);
    // Add the task to the list of tasks to run after
    // the main thread is finished.
    if (options.isSetup) {
      generatorFunction.isSetup = true;
      let lastSetupIndex = task_list.findLastIndex(t => t.isSetup) + 1;
      task_list.splice(lastSetupIndex, 0, generatorFunction);
    } else {
      task_list.push(generatorFunction);
    }
    return generatorFunction;
  };
})();

// Like add_task, but setup tasks are executed first.
function add_setup(generatorFunction) {
  return add_task(generatorFunction, { isSetup: true });
}

// Request complete log when using failure patterns so that failure info
// from infra can be useful.
if (usesFailurePatterns()) {
  SimpleTest.requestCompleteLog();
}

addEventListener("message", async event => {
  if (event.data == "SimpleTest:timeout") {
    await SimpleTest.timeout();
    SimpleTest.finish();
  }
});
