text: // Copyright 2016 The Chromium Authors. All rights reserved.
  // Use of this source code is governed by a BSD-style license that can be
  // found in the LICENSE file.
  
  /**
   * @fileoverview using private properties isn't a Closure violation in tests.
   * @suppress {accessControls}
   */
  
  /* eslint-disable no-console */
  
  /** @type {!{logToStderr: function(), navigateSecondaryWindow: function(string), notifyDone: function()}|undefined} */
  self.testRunner;
  
  /**
   * Only tests in /LayoutTests/http/tests/devtools/startup/ need to call
   * this method because these tests need certain activities to be exercised
   * in the inspected page prior to the DevTools session.
   * @param {string} path
   * @return {!Promise<undefined>}
   */
  TestRunner.setupStartupTest = function(path) {
    const absoluteURL = TestRunner.url(path);
    self.testRunner.navigateSecondaryWindow(absoluteURL);
    return new Promise(f => TestRunner._startupTestSetupFinished = () => {
      TestRunner._initializeTargetForStartupTest();
      delete TestRunner._startupTestSetupFinished;
      f();
    });
  };
  
  TestRunner._executeTestScript = function() {
    const testScriptURL = /** @type {string} */ (Runtime.queryParam('test'));
    fetch(testScriptURL)
        .then(data => data.text())
        .then(testScript => {
          if (TestRunner._isDebugTest()) {
            TestRunner.addResult = console.log;
            TestRunner.completeTest = () => console.log('Test completed');
  
            // Auto-start unit tests
            if (!self.testRunner)
              eval(`(function test(){${testScript}})()\n//# sourceURL=${testScriptURL}`);
            else
              self.eval(`function test(){${testScript}}\n//# sourceURL=${testScriptURL}`);
            return;
          }
  
          // Convert the test script into an expression (if needed)
          testScript = testScript.trimRight();
          if (testScript.endsWith(';'))
            testScript = testScript.slice(0, testScript.length - 1);
  
          (async function() {
            try {
              await eval(testScript + `\n//# sourceURL=${testScriptURL}`);
            } catch (err) {
              TestRunner.addResult('TEST ENDED EARLY DUE TO UNCAUGHT ERROR:');
              TestRunner.addResult(err && err.stack || err);
              TestRunner.addResult('=== DO NOT COMMIT THIS INTO -expected.txt ===');
              TestRunner.completeTest();
            }
          })();
        })
        .catch(error => {
          TestRunner.addResult(`Unable to execute test script because of error: ${error}`);
          TestRunner.completeTest();
        });
  };
  
  /** @type {!Array<string>} */
  TestRunner._results = [];
  
  TestRunner.completeTest = function() {
    TestRunner.flushResults();
    self.testRunner.notifyDone();
  };
  
  /**
   * @suppressGlobalPropertiesCheck
   */
  TestRunner.flushResults = function() {
    Array.prototype.forEach.call(document.documentElement.childNodes, x => x.remove());
    const outputElement = document.createElement('div');
    // Support for svg - add to document, not body, check for style.
    if (outputElement.style) {
      outputElement.style.whiteSpace = 'pre';
      outputElement.style.height = '10px';
      outputElement.style.overflow = 'hidden';
    }
    document.documentElement.appendChild(outputElement);
    for (let i = 0; i < TestRunner._results.length; i++) {
      outputElement.appendChild(document.createTextNode(TestRunner._results[i]));
      outputElement.appendChild(document.createElement('br'));
    }
    TestRunner._results = [];
  };
  
  /**
   * @param {*} text
   */
  TestRunner.addResult = function(text) {
    TestRunner._results.push(String(text));
  };
  
  /**
   * @param {!Array<string>} textArray
   */
  TestRunner.addResults = function(textArray) {
    if (!textArray)
      return;
    for (let i = 0, size = textArray.length; i < size; ++i)
      TestRunner.addResult(textArray[i]);
  };
  
  /**
   * @param {!Array<function()>} tests
   */
  TestRunner.runTests = function(tests) {
    nextTest();
  
    function nextTest() {
      const test = tests.shift();
      if (!test) {
        TestRunner.completeTest();
        return;
      }
      TestRunner.addResult('\ntest: ' + test.name);
      let testPromise = test();
      if (!(testPromise instanceof Promise))
        testPromise = Promise.resolve();
      testPromise.then(nextTest);
    }
  };
  
  /**
   * @param {!Object} receiver
   * @param {string} methodName
   * @param {!Function} override
   * @param {boolean=} opt_sticky
   */
  TestRunner.addSniffer = function(receiver, methodName, override, opt_sticky) {
    override = TestRunner.safeWrap(override);
  
    const original = receiver[methodName];
    if (typeof original !== 'function')
      throw new Error('Cannot find method to override: ' + methodName);
  
    receiver[methodName] = function(var_args) {
      let result;
      try {
        result = original.apply(this, arguments);
      } finally {
        if (!opt_sticky)
          receiver[methodName] = original;
      }
      // In case of exception the override won't be called.
      try {
        Array.prototype.push.call(arguments, result);
        override.apply(this, arguments);
      } catch (e) {
        throw new Error('Exception in overriden method \'' + methodName + '\': ' + e);
      }
      return result;
    };
  };
  
  /**
   * @param {!Object} receiver
   * @param {string} methodName
   * @return {!Promise<*>}
   */
  TestRunner.addSnifferPromise = function(receiver, methodName) {
    return new Promise(function(resolve, reject) {
      const original = receiver[methodName];
      if (typeof original !== 'function') {
        reject('Cannot find method to override: ' + methodName);
        return;
      }
  
      receiver[methodName] = function(var_args) {
        let result;
        try {
          result = original.apply(this, arguments);
        } finally {
          receiver[methodName] = original;
        }
        // In case of exception the override won't be called.
        try {
          Array.prototype.push.call(arguments, result);
          resolve.apply(this, arguments);
        } catch (e) {
          reject('Exception in overridden method \'' + methodName + '\': ' + e);
          TestRunner.completeTest();
        }
        return result;
      };
    });
  };
  
  /** @type {function():void} */
  TestRunner._resolveOnFinishInits;
  
  /**
   * @param {string} module
   * @return {!Promise<undefined>}
   */
  TestRunner.loadModule = async function(module) {
    const promise = new Promise(resolve => TestRunner._resolveOnFinishInits = resolve);
    await self.runtime.loadModulePromise(module);
    if (!TestRunner._pendingInits)
      return;
    return promise;
  };
  
  /**
   * @param {string} panel
   * @return {!Promise.<?UI.Panel>}
   */
  TestRunner.showPanel = function(panel) {
    return UI.viewManager.showView(panel);
  };
  
  /**
   * @param {string} key
   * @param {boolean=} ctrlKey
   * @param {boolean=} altKey
   * @param {boolean=} shiftKey
   * @param {boolean=} metaKey
   * @return {!KeyboardEvent}
   */
  TestRunner.createKeyEvent = function(key, ctrlKey, altKey, shiftKey, metaKey) {
    return new KeyboardEvent('keydown', {
      key: key,
      bubbles: true,
      cancelable: true,
      ctrlKey: !!ctrlKey,
      altKey: !!altKey,
      shiftKey: !!shiftKey,
      metaKey: !!metaKey
    });
  };
  
  /**
   * @param {!Function|undefined} func
   * @param {!Function=} onexception
   * @return {!Function}
   */
  TestRunner.safeWrap = function(func, onexception) {
    /**
     * @this {*}
     */
    function result() {
      if (!func)
        return;
      const wrapThis = this;
      try {
        return func.apply(wrapThis, arguments);
      } catch (e) {
        TestRunner.addResult('Exception while running: ' + func + '\n' + (e.stack || e));
        if (onexception)
          TestRunner.safeWrap(onexception)();
        else
          TestRunner.completeTest();
      }
    }
    return result;
  };
  
  /**
   * @param {!Node} node
   * @return {string}
   */
  TestRunner.textContentWithLineBreaks = function(node) {
    function padding(currentNode) {
      let result = 0;
      while (currentNode && currentNode !== node) {
        if (currentNode.nodeName === 'OL' &&
            !(currentNode.classList && currentNode.classList.contains('object-properties-section')))
          ++result;
        currentNode = currentNode.parentNode;
      }
      return Array(result * 4 + 1).join(' ');
    }
  
    let buffer = '';
    let currentNode = node;
    let ignoreFirst = false;
    while (currentNode.traverseNextNode(node)) {
      currentNode = currentNode.traverseNextNode(node);
      if (currentNode.nodeType === Node.TEXT_NODE) {
        buffer += currentNode.nodeValue;
      } else if (currentNode.nodeName === 'LI' || currentNode.nodeName === 'TR') {
        if (!ignoreFirst)
          buffer += '\n' + padding(currentNode);
        else
          ignoreFirst = false;
      } else if (currentNode.nodeName === 'STYLE') {
        currentNode = currentNode.traverseNextNode(node);
        continue;
      } else if (currentNode.classList && currentNode.classList.contains('object-properties-section')) {
        ignoreFirst = true;
      }
    }
    return buffer;
  };
  
  /**
   * @param {!Node} node
   * @return {string}
   */
  TestRunner.textContentWithoutStyles = function(node) {
    let buffer = '';
    let currentNode = node;
    while (currentNode.traverseNextNode(node)) {
      currentNode = currentNode.traverseNextNode(node);
      if (currentNode.nodeType === Node.TEXT_NODE)
        buffer += currentNode.nodeValue;
      else if (currentNode.nodeName === 'STYLE')
        currentNode = currentNode.traverseNextNode(node);
    }
    return buffer;
  };
  
  /**
   * @param {!SDK.Target} target
   */
  TestRunner._setupTestHelpers = function(target) {
    TestRunner.BrowserAgent = target.browserAgent();
    TestRunner.CSSAgent = target.cssAgent();
    TestRunner.DeviceOrientationAgent = target.deviceOrientationAgent();
    TestRunner.DOMAgent = target.domAgent();
    TestRunner.DOMDebuggerAgent = target.domdebuggerAgent();
    TestRunner.DebuggerAgent = target.debuggerAgent();
    TestRunner.EmulationAgent = target.emulationAgent();
    TestRunner.HeapProfilerAgent = target.heapProfilerAgent();
    TestRunner.InspectorAgent = target.inspectorAgent();
    TestRunner.NetworkAgent = target.networkAgent();
    TestRunner.OverlayAgent = target.overlayAgent();
    TestRunner.PageAgent = target.pageAgent();
    TestRunner.ProfilerAgent = target.profilerAgent();
    TestRunner.RuntimeAgent = target.runtimeAgent();
    TestRunner.TargetAgent = target.targetAgent();
  
    TestRunner.networkManager = target.model(SDK.NetworkManager);
    TestRunner.securityOriginManager = target.model(SDK.SecurityOriginManager);
    TestRunner.resourceTreeModel = target.model(SDK.ResourceTreeModel);
    TestRunner.debuggerModel = target.model(SDK.DebuggerModel);
    TestRunner.runtimeModel = target.model(SDK.RuntimeModel);
    TestRunner.domModel = target.model(SDK.DOMModel);
    TestRunner.domDebuggerModel = target.model(SDK.DOMDebuggerModel);
    TestRunner.cssModel = target.model(SDK.CSSModel);
    TestRunner.cpuProfilerModel = target.model(SDK.CPUProfilerModel);
    TestRunner.overlayModel = target.model(SDK.OverlayModel);
    TestRunner.serviceWorkerManager = target.model(SDK.ServiceWorkerManager);
    TestRunner.tracingManager = target.model(SDK.TracingManager);
    TestRunner.mainTarget = target;
  };
  
  /**
   * @param {string} code
   * @return {!Promise<*>}
   */
  TestRunner.evaluateInPageRemoteObject = async function(code) {
    const response = await TestRunner._evaluateInPage(code);
    return TestRunner.runtimeModel.createRemoteObject(response.result);
  };
  
  /**
   * @param {string} code
   * @param {function(*, !Protocol.Runtime.ExceptionDetails=):void} callback
   */
  TestRunner.evaluateInPage = async function(code, callback) {
    const response = await TestRunner._evaluateInPage(code);
    TestRunner.safeWrap(callback)(response.result.value, response.exceptionDetails);
  };
  
  /** @type {number} */
  TestRunner._evaluateInPageCounter = 0;
  
  /**
   * @param {string} code
   * @return {!Promise<{response: !SDK.RemoteObject,
   *   exceptionDetails: (!Protocol.Runtime.ExceptionDetails|undefined)}>}
   */
  TestRunner._evaluateInPage = async function(code) {
    const lines = new Error().stack.split('at ');
  
    // Handles cases where the function is safe wrapped
    const testScriptURL = /** @type {string} */ (Runtime.queryParam('test'));
    const functionLine = lines.reduce((acc, line) => line.includes(testScriptURL) ? line : acc, lines[lines.length - 2]);
  
    const components = functionLine.trim().split('/');
    const source = components[components.length - 1].slice(0, -1).split(':');
    const fileName = source[0];
    const sourceURL = `test://evaluations/${TestRunner._evaluateInPageCounter++}/` + fileName;
    const lineOffset = parseInt(source[1], 10);
    code = '\n'.repeat(lineOffset - 1) + code;
    if (code.indexOf('sourceURL=') === -1)
      code += `//# sourceURL=${sourceURL}`;
    const response = await TestRunner.RuntimeAgent.invoke_evaluate({expression: code, objectGroup: 'console'});
    const error = response[Protocol.Error];
    if (error) {
      TestRunner.addResult('Error: ' + error);
      TestRunner.completeTest();
      return;
    }
    return response;
  };
  
  /**
   * Doesn't append sourceURL to snippets evaluated in inspected page
   * to avoid churning test expectations
   * @param {string} code
   * @return {!Promise<*>}
   */
  TestRunner.evaluateInPageAnonymously = async function(code) {
    const response = await TestRunner.RuntimeAgent.invoke_evaluate({expression: code, objectGroup: 'console'});
    if (!response[Protocol.Error])
      return response.result.value;
    TestRunner.addResult(
        'Error: ' +
        (response.exceptionDetails && response.exceptionDetails.text || 'exception from evaluateInPageAnonymously.'));
    TestRunner.completeTest();
  };
  
  /**
   * @param {string} code
   * @return {!Promise<*>}
   */
  TestRunner.evaluateInPagePromise = function(code) {
    return new Promise(success => TestRunner.evaluateInPage(code, success));
  };
  
  /**
   * @param {string} code
   * @return {!Promise<*>}
   */
  TestRunner.evaluateInPageAsync = async function(code) {
    const response = await TestRunner.RuntimeAgent.invoke_evaluate(
        {expression: code, objectGroup: 'console', includeCommandLineAPI: false, awaitPromise: true});
  
    const error = response[Protocol.Error];
    if (!error && !response.exceptionDetails)
      return response.result.value;
    TestRunner.addResult(
        'Error: ' +
        (error || response.exceptionDetails && response.exceptionDetails.text || 'exception while evaluation in page.'));
    TestRunner.completeTest();
  };
  
  /**
   * @param {string} name
   * @param {!Array<*>} args
   * @return {!Promise<*>}
   */
  TestRunner.callFunctionInPageAsync = function(name, args) {
    args = args || [];
    return TestRunner.evaluateInPageAsync(name + '(' + args.map(a => JSON.stringify(a)).join(',') + ')');
  };
  
  /**
   * @param {string} code
   */
  TestRunner.evaluateInPageWithTimeout = function(code) {
    // FIXME: we need a better way of waiting for chromium events to happen
    TestRunner.evaluateInPageAnonymously('setTimeout(unescape(\'' + escape(code) + '\'), 1)');
  };
  
  /**
   * @param {function():*} func
   * @param {function(*):void} callback
   */
  TestRunner.evaluateFunctionInOverlay = function(func, callback) {
    const expression = 'internals.evaluateInInspectorOverlay("(" + ' + func + ' + ")()")';
    const mainContext = TestRunner.runtimeModel.executionContexts()[0];
    mainContext
        .evaluate(
            {
              expression: expression,
              objectGroup: '',
              includeCommandLineAPI: false,
              silent: false,
              returnByValue: true,
              generatePreview: false
            },
            /* userGesture */ false, /* awaitPromise*/ false)
        .then(result => void callback(result.object.value));
  };
  
  /**
   * @param {boolean} passCondition
   * @param {string} failureText
   */
  TestRunner.check = function(passCondition, failureText) {
    if (!passCondition)
      TestRunner.addResult('FAIL: ' + failureText);
  };
  
  /**
   * @param {!Function} callback
   */
  TestRunner.deprecatedRunAfterPendingDispatches = function(callback) {
    const targets = SDK.targetManager.targets();
    const promises = targets.map(target => new Promise(resolve => target._deprecatedRunAfterPendingDispatches(resolve)));
    Promise.all(promises).then(TestRunner.safeWrap(callback));
  };
  
  /**
   * This ensures a base tag is set so all DOM references
   * are relative to the test file and not the inspected page
   * (i.e. http/tests/devtools/resources/inspected-page.html).
   * @param {string} html
   * @return {!Promise<*>}
   */
  TestRunner.loadHTML = function(html) {
    if (!html.includes('<base')) {
      // <!DOCTYPE...> tag needs to be first
      const doctypeRegex = /(<!DOCTYPE.*?>)/i;
      const baseTag = `<base href="${TestRunner.url()}">`;
      if (html.match(doctypeRegex))
        html = html.replace(doctypeRegex, '$1' + baseTag);
      else
        html = baseTag + html;
    }
    html = html.replace(/'/g, '\\\'').replace(/\n/g, '\\n');
    return TestRunner.evaluateInPageAnonymously(`document.write(\`${html}\`);document.close();`);
  };
  
  /**
   * @param {string} path
   * @return {!Promise<*>}
   */
  TestRunner.addScriptTag = function(path) {
    return TestRunner.evaluateInPageAsync(`
      (function(){
        let script = document.createElement('script');
        script.src = '${path}';
        document.head.append(script);
        return new Promise(f => script.onload = f);
      })();
    `);
  };
  
  /**
   * @param {string} path
   * @return {!Promise<*>}
   */
  TestRunner.addStylesheetTag = function(path) {
    return TestRunner.evaluateInPageAsync(`
      (function(){
        let link = document.createElement('link');
        link.rel = 'stylesheet';
        link.type = 'text/css';
        link.href = '${path}';
        link.onload = onload;
        document.head.append(link);
        let resolve;
        let promise = new Promise(r => resolve = r);
        function onload() {
          // TODO(chenwilliam): It shouldn't be necessary to force
          // style recalc here but some tests rely on it.
          window.getComputedStyle(document.body).color;
          resolve();
        }
        return promise;
      })();
    `);
  };
  
  /**
   * @param {string} path
   * @return {!Promise<*>}
   */
  TestRunner.addHTMLImport = function(path) {
    return TestRunner.evaluateInPageAsync(`
      (function(){
        let link = document.createElement('link');
        link.rel = 'import';
        link.href = '${path}';
        let promise = new Promise(r => link.onload = r);
        document.body.append(link);
        return promise;
      })();
    `);
  };
  
  /**
   * @param {string} path
   * @param {!Object|undefined} options
   * @return {!Promise<*>}
   */
  TestRunner.addIframe = function(path, options = {}) {
    options.id = options.id || '';
    options.name = options.name || '';
    return TestRunner.evaluateInPageAsync(`
      (function(){
        let iframe = document.createElement('iframe');
        iframe.src = '${path}';
        iframe.id = '${options.id}';
        iframe.name = '${options.name}';
        document.body.appendChild(iframe);
        return new Promise(f => iframe.onload = f);
      })();
    `);
  };
  
  /** @type {number} */
  TestRunner._pendingInits = 0;
  
  /**
   * The old test framework executed certain snippets in the inspected page
   * context as part of loading a test helper file.
   *
   * This is deprecated because:
   * 1) it makes the testing API less intuitive (need to read the various *TestRunner.js
   * files to know which helper functions are available in the inspected page).
   * 2) it complicates the test framework's module loading process.
   *
   * In most cases, this is used to set up inspected page functions (e.g. makeSimpleXHR)
   * which should become a *TestRunner method (e.g. NetworkTestRunner.makeSimpleXHR)
   * that calls evaluateInPageAnonymously(...).
   * @param {string} code
   */
  TestRunner.deprecatedInitAsync = async function(code) {
    TestRunner._pendingInits++;
    await TestRunner.RuntimeAgent.invoke_evaluate({expression: code, objectGroup: 'console'});
    TestRunner._pendingInits--;
    if (!TestRunner._pendingInits)
      TestRunner._resolveOnFinishInits();
  };
  
  /**
   * @param {string} title
   */
  TestRunner.markStep = function(title) {
    TestRunner.addResult('\nRunning: ' + title);
  };
  
  TestRunner.startDumpingProtocolMessages = function() {
    // TODO(chenwilliam): stop abusing Closure interface which is why
    // we need to opt out of type checking here
    const untypedConnection = /** @type {*} */ (Protocol.InspectorBackend.Connection);
    untypedConnection.prototype._dumpProtocolMessage = self.testRunner.logToStderr.bind(self.testRunner);
    Protocol.InspectorBackend.Options.dumpInspectorProtocolMessages = 1;
  };
  
  /**
   * @param {string} url
   * @param {string} content
   * @param {!SDK.ResourceTreeFrame} frame
   */
  TestRunner.addScriptForFrame = function(url, content, frame) {
    content += '\n//# sourceURL=' + url;
    const executionContext = TestRunner.runtimeModel.executionContexts().find(context => context.frameId === frame.id);
    TestRunner.RuntimeAgent.evaluate(content, 'console', false, false, executionContext.id);
  };
  
  TestRunner.formatters = {};
  
  /**
   * @param {*} value
   * @return {string}
   */
  TestRunner.formatters.formatAsTypeName = function(value) {
    return '<' + typeof value + '>';
  };
  
  /**
   * @param {*} value
   * @return {string}
   */
  TestRunner.formatters.formatAsTypeNameOrNull = function(value) {
    if (value === null)
      return 'null';
    return TestRunner.formatters.formatAsTypeName(value);
  };
  
  /**
   * @param {*} value
   * @return {string|!Date}
   */
  TestRunner.formatters.formatAsRecentTime = function(value) {
    if (typeof value !== 'object' || !(value instanceof Date))
      return TestRunner.formatters.formatAsTypeName(value);
    const delta = Date.now() - value;
    return 0 <= delta && delta < 30 * 60 * 1000 ? '<plausible>' : value;
  };
  
  /**
   * @param {string} value
   * @return {string}
   */
  TestRunner.formatters.formatAsURL = function(value) {
    if (!value)
      return value;
    const lastIndex = value.lastIndexOf('devtools/');
    if (lastIndex < 0)
      return value;
    return '.../' + value.substr(lastIndex);
  };
  
  /**
   * @param {string} value
   * @return {string}
   */
  TestRunner.formatters.formatAsDescription = function(value) {
    if (!value)
      return value;
    return '"' + value.replace(/^function [gs]et /, 'function ') + '"';
  };
  
  /**
   * @typedef {!Object<string, string>}
   */
  TestRunner.CustomFormatters;
  
  /**
   * @param {!Object} object
   * @param {!TestRunner.CustomFormatters=} customFormatters
   * @param {string=} prefix
   * @param {string=} firstLinePrefix
   */
  TestRunner.addObject = function(object, customFormatters, prefix, firstLinePrefix) {
    prefix = prefix || '';
    firstLinePrefix = firstLinePrefix || prefix;
    TestRunner.addResult(firstLinePrefix + '{');
    const propertyNames = Object.keys(object);
    propertyNames.sort();
    for (let i = 0; i < propertyNames.length; ++i) {
      const prop = propertyNames[i];
      if (!object.hasOwnProperty(prop))
        continue;
      const prefixWithName = '    ' + prefix + prop + ' : ';
      const propValue = object[prop];
      if (customFormatters && customFormatters[prop]) {
        const formatterName = customFormatters[prop];
        if (formatterName !== 'skip') {
          const formatter = TestRunner.formatters[formatterName];
          TestRunner.addResult(prefixWithName + formatter(propValue));
        }
      } else {
        TestRunner.dump(propValue, customFormatters, '    ' + prefix, prefixWithName);
      }
    }
    TestRunner.addResult(prefix + '}');
  };
  
  /**
   * @param {!Array} array
   * @param {!TestRunner.CustomFormatters=} customFormatters
   * @param {string=} prefix
   * @param {string=} firstLinePrefix
   */
  TestRunner.addArray = function(array, customFormatters, prefix, firstLinePrefix) {
    prefix = prefix || '';
    firstLinePrefix = firstLinePrefix || prefix;
    TestRunner.addResult(firstLinePrefix + '[');
    for (let i = 0; i < array.length; ++i)
      TestRunner.dump(array[i], customFormatters, prefix + '    ');
    TestRunner.addResult(prefix + ']');
  };
  
  /**
   * @param {!Node} node
   */
  TestRunner.dumpDeepInnerHTML = function(node) {
    /**
     * @param {string} prefix
     * @param {!Node} node
     */
    function innerHTML(prefix, node) {
      const openTag = [];
      if (node.nodeType === Node.TEXT_NODE) {
        if (!node.parentElement || node.parentElement.nodeName !== 'STYLE')
          TestRunner.addResult(node.nodeValue);
        return;
      }
      openTag.push('<' + node.nodeName);
      const attrs = node.attributes;
      for (let i = 0; attrs && i < attrs.length; ++i)
        openTag.push(attrs[i].name + '=' + attrs[i].value);
  
      openTag.push('>');
      TestRunner.addResult(prefix + openTag.join(' '));
      for (let child = node.firstChild; child; child = child.nextSibling)
        innerHTML(prefix + '    ', child);
      if (node.shadowRoot)
        innerHTML(prefix + '    ', node.shadowRoot);
      TestRunner.addResult(prefix + '</' + node.nodeName + '>');
    }
    innerHTML('', node);
  };
  
  /**
   * @param {!Node} node
   * @return {string}
   */
  TestRunner.deepTextContent = function(node) {
    if (!node)
      return '';
    if (node.nodeType === Node.TEXT_NODE && node.nodeValue)
      return !node.parentElement || node.parentElement.nodeName !== 'STYLE' ? node.nodeValue : '';
    let res = '';
    const children = node.childNodes;
    for (let i = 0; i < children.length; ++i)
      res += TestRunner.deepTextContent(children[i]);
    if (node.shadowRoot)
      res += TestRunner.deepTextContent(node.shadowRoot);
    return res;
  };
  
  /**
   * @param {*} value
   * @param {!TestRunner.CustomFormatters=} customFormatters
   * @param {string=} prefix
   * @param {string=} prefixWithName
   */
  TestRunner.dump = function(value, customFormatters, prefix, prefixWithName) {
    prefixWithName = prefixWithName || prefix;
    if (prefixWithName && prefixWithName.length > 80) {
      TestRunner.addResult(prefixWithName + 'was skipped due to prefix length limit');
      return;
    }
    if (value === null)
      TestRunner.addResult(prefixWithName + 'null');
    else if (value && value.constructor && value.constructor.name === 'Array')
      TestRunner.addArray(/** @type {!Array} */ (value), customFormatters, prefix, prefixWithName);
    else if (typeof value === 'object')
      TestRunner.addObject(/** @type {!Object} */ (value), customFormatters, prefix, prefixWithName);
    else if (typeof value === 'string')
      TestRunner.addResult(prefixWithName + '"' + value + '"');
    else
      TestRunner.addResult(prefixWithName + value);
  };
  
  /**
   * @param {!UI.TreeElement} treeElement
   */
  TestRunner.dumpObjectPropertyTreeElement = function(treeElement) {
    const expandedSubstring = treeElement.expanded ? '[expanded]' : '[collapsed]';
    TestRunner.addResult(expandedSubstring + ' ' + treeElement.listItemElement.deepTextContent());
  
    for (let i = 0; i < treeElement.childCount(); ++i) {
      const property = treeElement.childAt(i).property;
      const key = property.name;
      const value = property.value._description;
      TestRunner.addResult('    ' + key + ': ' + value);
    }
  };
  
  /**
   * @param {symbol} event
   * @param {!Common.Object} obj
   * @param {function(?):boolean=} condition
   * @return {!Promise}
   */
  TestRunner.waitForEvent = function(event, obj, condition) {
    condition = condition || function() {
      return true;
    };
    return new Promise(resolve => {
      obj.addEventListener(event, onEventFired);
  
      /**
       * @param {!Common.Event} event
       */
      function onEventFired(event) {
        if (!condition(event.data))
          return;
        obj.removeEventListener(event, onEventFired);
        resolve(event.data);
      }
    });
  };
  
  /**
   * @param {function(!SDK.Target):boolean} filter
   * @return {!Promise<!SDK.Target>}
   */
  TestRunner.waitForTarget = function(filter) {
    filter = filter || (target => true);
    for (const target of SDK.targetManager.targets()) {
      if (filter(target))
        return Promise.resolve(target);
    }
    return new Promise(fulfill => {
      const observer = /** @type {!SDK.TargetManager.Observer} */ ({
        targetAdded: function(target) {
          if (filter(target)) {
            SDK.targetManager.unobserveTargets(observer);
            fulfill(target);
          }
        },
        targetRemoved: function() {},
      });
      SDK.targetManager.observeTargets(observer);
    });
  };
  
  /**
   * @param {!SDK.RuntimeModel} runtimeModel
   * @return {!Promise}
   */
  TestRunner.waitForExecutionContext = function(runtimeModel) {
    if (runtimeModel.executionContexts().length)
      return Promise.resolve(runtimeModel.executionContexts()[0]);
    return runtimeModel.once(SDK.RuntimeModel.Events.ExecutionContextCreated);
  };
  
  /**
   * @param {!SDK.ExecutionContext} context
   * @return {!Promise}
   */
  TestRunner.waitForExecutionContextDestroyed = function(context) {
    const runtimeModel = context.runtimeModel;
    if (runtimeModel.executionContexts().indexOf(context) === -1)
      return Promise.resolve();
    return TestRunner.waitForEvent(
        SDK.RuntimeModel.Events.ExecutionContextDestroyed, runtimeModel,
        destroyedContext => destroyedContext === context);
  };
  
  /**
   * @param {number} a
   * @param {number} b
   * @param {string=} message
   */
  TestRunner.assertGreaterOrEqual = function(a, b, message) {
    if (a < b)
      TestRunner.addResult('FAILED: ' + (message ? message + ': ' : '') + a + ' < ' + b);
  };
  
  /**
   * @param {string} url
   * @param {function():void} callback
   */
  TestRunner.navigate = function(url, callback) {
    TestRunner._pageLoadedCallback = TestRunner.safeWrap(callback);
    TestRunner.resourceTreeModel.addEventListener(SDK.ResourceTreeModel.Events.Load, TestRunner._pageNavigated);
    // Note: injected <base> means that url is relative to test
    // and not the inspected page
    TestRunner.evaluateInPageAnonymously('window.location.replace(\'' + url + '\')');
  };
  
  /**
   * @return {!Promise}
   */
  TestRunner.navigatePromise = function(url) {
    return new Promise(fulfill => TestRunner.navigate(url, fulfill));
  };
  
  TestRunner._pageNavigated = function() {
    TestRunner.resourceTreeModel.removeEventListener(SDK.ResourceTreeModel.Events.Load, TestRunner._pageNavigated);
    TestRunner._handlePageLoaded();
  };
  
  /**
   * @param {function():void} callback
   */
  TestRunner.hardReloadPage = function(callback) {
    TestRunner._innerReloadPage(true, undefined, callback);
  };
  
  /**
   * @param {function():void} callback
   */
  TestRunner.reloadPage = function(callback) {
    TestRunner._innerReloadPage(false, undefined, callback);
  };
  
  /**
   * @param {(string|undefined)} injectedScript
   * @param {function():void} callback
   */
  TestRunner.reloadPageWithInjectedScript = function(injectedScript, callback) {
    TestRunner._innerReloadPage(false, injectedScript, callback);
  };
  
  /**
   * @return {!Promise}
   */
  TestRunner.reloadPagePromise = function() {
    return new Promise(fulfill => TestRunner.reloadPage(fulfill));
  };
  
  /**
   * @param {boolean} hardReload
   * @param {(string|undefined)} injectedScript
   * @param {function():void} callback
   */
  TestRunner._innerReloadPage = function(hardReload, injectedScript, callback) {
    TestRunner._pageLoadedCallback = TestRunner.safeWrap(callback);
    TestRunner.resourceTreeModel.addEventListener(SDK.ResourceTreeModel.Events.Load, TestRunner.pageLoaded);
    TestRunner.resourceTreeModel.reloadPage(hardReload, injectedScript);
  };
  
  TestRunner.pageLoaded = function() {
    TestRunner.resourceTreeModel.removeEventListener(SDK.ResourceTreeModel.Events.Load, TestRunner.pageLoaded);
    TestRunner.addResult('Page reloaded.');
    TestRunner._handlePageLoaded();
  };
  
  TestRunner._handlePageLoaded = async function() {
    await TestRunner.waitForExecutionContext(/** @type {!SDK.RuntimeModel} */ (TestRunner.runtimeModel));
    if (TestRunner._pageLoadedCallback) {
      const callback = TestRunner._pageLoadedCallback;
      delete TestRunner._pageLoadedCallback;
      callback();
    }
  };
  
  /**
   * @param {function():void} callback
   */
  TestRunner.waitForPageLoad = function(callback) {
    TestRunner.resourceTreeModel.addEventListener(SDK.ResourceTreeModel.Events.Load, onLoaded);
  
    function onLoaded() {
      TestRunner.resourceTreeModel.removeEventListener(SDK.ResourceTreeModel.Events.Load, onLoaded);
      callback();
    }
  };
  
  /**
   * @param {function():void} callback
   */
  TestRunner.runWhenPageLoads = function(callback) {
    const oldCallback = TestRunner._pageLoadedCallback;
    function chainedCallback() {
      if (oldCallback)
        oldCallback();
      callback();
    }
    TestRunner._pageLoadedCallback = TestRunner.safeWrap(chainedCallback);
  };
  
  /**
   * @param {!Array<function(function():void)>} testSuite
   */
  TestRunner.runTestSuite = function(testSuite) {
    const testSuiteTests = testSuite.slice();
  
    function runner() {
      if (!testSuiteTests.length) {
        TestRunner.completeTest();
        return;
      }
      const nextTest = testSuiteTests.shift();
      TestRunner.addResult('');
      TestRunner.addResult(
          'Running: ' +
          /function\s([^(]*)/.exec(nextTest)[1]);
      TestRunner.safeWrap(nextTest)(runner);
    }
    runner();
  };
  
  /**
   * @param {*} expected
   * @param {*} found
   * @param {string} message
   */
  TestRunner.assertEquals = function(expected, found, message) {
    if (expected === found)
      return;
  
    let error;
    if (message)
      error = 'Failure (' + message + '):';
    else
      error = 'Failure:';
    throw new Error(error + ' expected <' + expected + '> found <' + found + '>');
  };
  
  /**
   * @param {*} found
   * @param {string} message
   */
  TestRunner.assertTrue = function(found, message) {
    TestRunner.assertEquals(true, !!found, message);
  };
  
  /**
   * @param {!Object} receiver
   * @param {string} methodName
   * @param {!Function} override
   * @param {boolean=} opt_sticky
   * @return {!Function}
   */
  TestRunner.override = function(receiver, methodName, override, opt_sticky) {
    override = TestRunner.safeWrap(override);
  
    const original = receiver[methodName];
    if (typeof original !== 'function')
      throw new Error('Cannot find method to override: ' + methodName);
  
    receiver[methodName] = function(var_args) {
      try {
        return override.apply(this, arguments);
      } catch (e) {
        throw new Error('Exception in overriden method \'' + methodName + '\': ' + e);
      } finally {
        if (!opt_sticky)
          receiver[methodName] = original;
      }
    };
  
    return original;
  };
  
  /**
   * @param {string} text
   * @return {string}
   */
  TestRunner.clearSpecificInfoFromStackFrames = function(text) {
    let buffer = text.replace(/\(file:\/\/\/(?:[^)]+\)|[\w\/:-]+)/g, '(...)');
    buffer = buffer.replace(/\(http:\/\/(?:[^)]+\)|[\w\/:-]+)/g, '(...)');
    buffer = buffer.replace(/\(test:\/\/(?:[^)]+\)|[\w\/:-]+)/g, '(...)');
    buffer = buffer.replace(/\(<anonymous>:[^)]+\)/g, '(...)');
    buffer = buffer.replace(/VM\d+/g, 'VM');
    return buffer.replace(/\s*at[^()]+\(native\)/g, '');
  };
  
  TestRunner.hideInspectorView = function() {
    UI.inspectorView.element.setAttribute('style', 'display:none !important');
  };
  
  /**
   * @return {?SDK.ResourceTreeFrame}
   */
  TestRunner.mainFrame = function() {
    return TestRunner.resourceTreeModel.mainFrame;
  };
  
  
  TestRunner.StringOutputStream = class {
    /**
     * @param {function(string):void} callback
     */
    constructor(callback) {
      this._callback = callback;
      this._buffer = '';
    }
  
    /**
     * @param {string} fileName
     * @return {!Promise<boolean>}
     */
    async open(fileName) {
      return true;
    }
  
    /**
     * @param {string} chunk
     */
    async write(chunk) {
      this._buffer += chunk;
    }
  
    async close() {
      this._callback(this._buffer);
    }
  };
  
  /**
   * @template V
   */
  TestRunner.MockSetting = class {
    /**
     * @param {V} value
     */
    constructor(value) {
      this._value = value;
    }
  
    /**
     * @return {V}
     */
    get() {
      return this._value;
    }
  
    /**
     * @param {V} value
     */
    set(value) {
      this._value = value;
    }
  };
  
  /**
   * @return {!Array<!Runtime.Module>}
   */
  TestRunner.loadedModules = function() {
    return self.runtime._modules.filter(module => module._loadedForTest)
        .filter(module => module.name().indexOf('test_runner') === -1);
  };
  
  /**
   * @param {!Array<!Runtime.Module>} relativeTo
   * @return {!Array<!Runtime.Module>}
   */
  TestRunner.dumpLoadedModules = function(relativeTo) {
    const previous = new Set(relativeTo || []);
    function moduleSorter(left, right) {
      return String.naturalOrderComparator(left._descriptor.name, right._descriptor.name);
    }
  
    TestRunner.addResult('Loaded modules:');
    const loadedModules = TestRunner.loadedModules().sort(moduleSorter);
    for (const module of loadedModules) {
      if (previous.has(module))
        continue;
      TestRunner.addResult('    ' + module._descriptor.name);
    }
    return loadedModules;
  };
  
  /**
   * @param {!SDK.Target} target
   * @return {boolean}
   */
  TestRunner.isDedicatedWorker = function(target) {
    return target && !target.hasBrowserCapability() && target.hasJSCapability() && target.hasLogCapability();
  };
  
  /**
   * @param {!SDK.Target} target
   * @return {boolean}
   */
  TestRunner.isServiceWorker = function(target) {
    return target && !target.hasBrowserCapability() && !target.hasJSCapability() && target.hasNetworkCapability() &&
        target.hasTargetCapability();
  };
  
  /**
   * @param {!SDK.Target} target
   * @return {string}
   */
  TestRunner.describeTargetType = function(target) {
    if (TestRunner.isDedicatedWorker(target))
      return 'worker';
    if (TestRunner.isServiceWorker(target))
      return 'service-worker';
    if (!target.parentTarget())
      return 'page';
    return 'frame';
  };
  
  /**
   * @param {string} urlSuffix
   * @param {!Workspace.projectTypes=} projectType
   * @return {!Promise}
   */
  TestRunner.waitForUISourceCode = function(urlSuffix, projectType) {
    /**
     * @param {!Workspace.UISourceCode} uiSourceCode
     * @return {boolean}
     */
    function matches(uiSourceCode) {
      if (projectType && uiSourceCode.project().type() !== projectType)
        return false;
      if (!projectType && uiSourceCode.project().type() === Workspace.projectTypes.Service)
        return false;
      if (urlSuffix && !uiSourceCode.url().endsWith(urlSuffix))
        return false;
      return true;
    }
  
    for (const uiSourceCode of Workspace.workspace.uiSourceCodes()) {
      if (urlSuffix && matches(uiSourceCode))
        return Promise.resolve(uiSourceCode);
    }
  
    return TestRunner.waitForEvent(Workspace.Workspace.Events.UISourceCodeAdded, Workspace.workspace, matches);
  };
  
  /**
   * @param {!Function} callback
   */
  TestRunner.waitForUISourceCodeRemoved = function(callback) {
    Workspace.workspace.once(Workspace.Workspace.Events.UISourceCodeRemoved).then(callback);
  };
  
  /**
   * @param {string=} url
   * @return {string}
   */
  TestRunner.url = function(url = '') {
    const testScriptURL = /** @type {string} */ (Runtime.queryParam('test'));
  
    // This handles relative (e.g. "../file"), root (e.g. "/resource"),
    // absolute (e.g. "http://", "data:") and empty (e.g. "") paths
    return new URL(url, testScriptURL + '/../').href;
  };
  
  /**
   * @param {string} str
   * @param {string} mimeType
   * @return {!Promise.<undefined>}
   * @suppressGlobalPropertiesCheck
   */
  TestRunner.dumpSyntaxHighlight = function(str, mimeType) {
    const node = document.createElement('span');
    node.textContent = str;
    const javascriptSyntaxHighlighter = new UI.SyntaxHighlighter(mimeType, false);
    return javascriptSyntaxHighlighter.syntaxHighlightNode(node).then(dumpSyntax);
  
    function dumpSyntax() {
      const node_parts = [];
  
      for (let i = 0; i < node.childNodes.length; i++) {
        if (node.childNodes[i].getAttribute)
          node_parts.push(node.childNodes[i].getAttribute('class'));
        else
          node_parts.push('*');
      }
  
      TestRunner.addResult(str + ': ' + node_parts.join(', '));
    }
  };
  
  /**
   * @param {string} messageType
   */
  TestRunner._consoleOutputHook = function(messageType) {
    TestRunner.addResult(messageType + ': ' + Array.prototype.slice.call(arguments, 1));
  };
  
  /**
   * This monkey patches console functions in DevTools context so the console
   * messages are shown in the right places, instead of having all of the console
   * messages printed at the top of the test expectation file (default behavior).
   */
  TestRunner._printDevToolsConsole = function() {
    if (TestRunner._isDebugTest())
      return;
    console.log = TestRunner._consoleOutputHook.bind(TestRunner, 'log');
    console.error = TestRunner._consoleOutputHook.bind(TestRunner, 'error');
    console.info = TestRunner._consoleOutputHook.bind(TestRunner, 'info');
  };
  
  /**
   * @param {string} querySelector
   */
  TestRunner.dumpInspectedPageElementText = async function(querySelector) {
    const value = await TestRunner.evaluateInPageAsync(`document.querySelector('${querySelector}').innerText`);
    TestRunner.addResult(value);
  };
  
  /** @type {boolean} */
  TestRunner._startedTest = false;
  
  /**
   * @implements {SDK.TargetManager.Observer}
   */
  TestRunner._TestObserver = class {
    /**
     * @param {!SDK.Target} target
     * @override
     */
    targetAdded(target) {
      if (TestRunner._startedTest)
        return;
      TestRunner._startedTest = true;
      TestRunner._setupTestHelpers(target);
      if (TestRunner._isStartupTest())
        return;
      TestRunner
          .loadHTML(`
        <head>
          <base href="${TestRunner.url()}">
        </head>
        <body>
        </body>
      `).then(() => TestRunner._executeTestScript());
    }
  
    /**
     * @param {!SDK.Target} target
     * @override
     */
    targetRemoved(target) {
    }
  };
  
  /**
   * @return {boolean}
   */
  TestRunner._isDebugTest = function() {
    return !self.testRunner || !!Runtime.queryParam('debugFrontend');
  };
  
  /**
   * @return {boolean}
   */
  TestRunner._isStartupTest = function() {
    return Runtime.queryParam('test').includes('/startup/');
  };
  
  (async function() {
    /**
     * @param {string|!Event} message
     * @param {string} source
     * @param {number} lineno
     * @param {number} colno
     * @param {!Error} error
     */
    function completeTestOnError(message, source, lineno, colno, error) {
      TestRunner.addResult('TEST ENDED IN ERROR: ' + error.stack);
      TestRunner.completeTest();
    }
  
    self['onerror'] = completeTestOnError;
    TestRunner._printDevToolsConsole();
    SDK.targetManager.observeTargets(new TestRunner._TestObserver());
    if (!TestRunner._isStartupTest())
      return;
    /**
     * Startup test initialization:
     * 1. Wait for DevTools app UI to load
     * 2. Execute test script, the first line will be TestRunner.setupStartupTest(...) which:
     *    A. Navigate secondary window
     *    B. After preconditions occur, secondary window calls testRunner.inspectSecondaryWindow()
     * 3. Backend executes TestRunner._startupTestSetupFinished() which calls _initializeTarget()
     */
    TestRunner._initializeTargetForStartupTest =
        TestRunner.override(Main.Main._instanceForTest, '_initializeTarget', () => undefined)
            .bind(Main.Main._instanceForTest);
    await TestRunner.addSnifferPromise(Main.Main._instanceForTest, '_showAppUI');
    TestRunner._executeTestScript();
  })();
  