chromium/chrome/test/data/extensions/api_test/stubs/content_script.js

// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// Helper function to log message to both the local console and to the
// background page, so that the latter can output the message via the
// chrome.test.log() function.
function logToConsoleAndStdout(msg) {
  console.log(msg);
  chrome.extension.sendRequest("log: " + msg);
}

// We ask the background page to get the extension API to test against. When it
// responds we start the test.
console.log("asking for api ...");
chrome.extension.sendRequest("getApi", function(apis) {
  var apiFeatures = chrome.test.getApiFeatures();
  // TODO(crbug.com/41478937): This really should support more than two levels
  // of inheritance.
  function isAvailableToContentScripts(namespace, path) {
    const results = {
      NULL: 'null',
      NOT_FOUND: 'not_found',
      FOUND: 'found'
    };
    function searchContexts(contexts) {
      // This is tricky because the context can be either:
      //   1. Undefined, so return results.NULL.
      //   2. The string value 'all', so return results.FOUND.
      //   3. An array of string values, which cannot be empty.
      //      Return results.FOUND if 'content_script' is in the array.
      if (!contexts) {
        return results.NULL;
      }
      if (contexts == 'all' || contexts.includes('content_script')) {
        return results.FOUND;
      }
      return results.NOT_FOUND;
    }

    function searchFeature(feature) {
      if (!feature.length) {
        // Simple feature, not an array. Can return results.NULL,
        // because feature.context may be undefined.
        return searchContexts(feature.contexts);
      }
      // Complex feature. We need to return results.NULL if we didn't
      // find any contexts.
      var foundContext = false;
      for (var i = 0; i < feature.length; ++i) {
        var currentResult = searchContexts(feature[i].contexts);
        if (currentResult === results.FOUND) {
          return results.FOUND;
        }
        foundContext = currentResult !== results.NULL;
      }
      return foundContext ? results.NOT_FOUND : results.NULL;
    }

    var pathFeature = apiFeatures[path];
    if (!!pathFeature) {
      const result = searchFeature(pathFeature);
      // If we found something, use that result.
      if (result !== results.NULL) {
          return result === results.FOUND;
      }
    }

    var namespaceFeature = apiFeatures[namespace];
    // Check the namespace, if it's defined.
    if (!!namespaceFeature) {
      return searchFeature(namespaceFeature) === results.FOUND;
    }
    return false;
  } /* isAvailableToContentScripts */

  console.log("got api response");
  var privilegedPaths = [];
  var unprivilegedPaths = [];
  apis.forEach(function(module) {
    var namespace = module.namespace;

    ["functions", "events"].forEach(function(section) {
      if (typeof(module[section]) == "undefined")
        return;
      module[section].forEach(function(entry) {
        // Ignore entries that are not applicable to the manifest that we're
        // running under.
        if (entry.maximumManifestVersion && entry.maximumManifestVersion < 2) {
          return;
        }

        var path = namespace + "." + entry.name;
        if (module.unprivileged || entry.unprivileged ||
            isAvailableToContentScripts(namespace, path)) {
          unprivilegedPaths.push(path);
        } else {
          privilegedPaths.push(path);
        }
      });
    });

    if (module.properties) {
      for (var propName in module.properties) {
        var path = namespace + "." + propName;
        if (module.unprivileged || module.properties[propName].unprivileged ||
            isAvailableToContentScripts(namespace, path)) {
          unprivilegedPaths.push(path);
        } else {
          privilegedPaths.push(path);
        }
      }
    }
  });
  doTest(privilegedPaths, unprivilegedPaths);
});


// Tests whether missing properties of the chrome object correctly throw an
// error on access. The path is a namespace or function/property/event etc.
// within a namespace, and is dot-separated.
function testPath(path, expectError) {
  var parts = path.split('.');

  var module = chrome;
  for (var i = 0; i < parts.length; i++) {
    if (i < parts.length - 1) {
      // Not the last component. Allowed to be undefined because some paths are
      // only defined on some platforms.
      module = module[parts[i]];
      if (typeof(module) == "undefined")
        return true;
    } else {
      // This is the last component - we expect it to either be undefined or
      // to throw an error on access.
      if (typeof(module[parts[i]]) == "undefined" &&
          // lastError being defined depends on there being an error obviously.
          path != "extension.lastError" &&
          path != "runtime.lastError") {
        if (expectError) {
          return true;
        } else {
          logToConsoleAndStdout(" fail (should not be undefined): " + path);
          return false;
        }
      } else if (!expectError) {
        return true;
      }
    }
  }
  logToConsoleAndStdout(" fail (no error when we were expecting one): " + path);
  return false;
}

function displayResult(status) {
  var div = document.createElement("div");
  div.innerHTML = "<h1>" + status + "</h2>";
  document.body.appendChild(div);
}

function reportSuccess() {
  displayResult("pass");
  chrome.extension.sendRequest("pass");
}

function reportFailure() {
  displayResult("fail");
  // Let the "fail" show for a little while so you can see it when running
  // browser_tests in the debugger.
  setTimeout(function() {
    chrome.extension.sendRequest("fail");
  }, 1000);
}

// Runs over each string path in privilegedPaths and unprivilegedPaths, testing
// to ensure a proper error is thrown on access or the path is defined.
function doTest(privilegedPaths, unprivilegedPaths) {
  console.log("starting");

  if (!privilegedPaths || privilegedPaths.length < 1 || !unprivilegedPaths ||
      unprivilegedPaths.length < 1) {
    reportFailure();
    return;
  }

  var failures = [];
  var success = true;

  // Returns a function that will test a path and record any failures.
  function makeTestFunction(expectError) {
    return function(path) {
      // runtime.connect and runtime.sendMessage are available in all contexts,
      // unlike the runtime API in general.
      var expectErrorForPath = expectError &&
                               path != 'runtime.connect' &&
                               path != 'runtime.sendMessage';
      if (!testPath(path, expectErrorForPath)) {
        success = false;
        failures.push(path);
      }
    };
  }
  privilegedPaths.forEach(makeTestFunction(true));
  unprivilegedPaths.forEach(makeTestFunction(false));

  console.log(success ? "pass" : "fail");
  if (success) {
    reportSuccess();
  } else {
    logToConsoleAndStdout("failures on:\n" + failures.join("\n") +
        "\n\n\n>>> See comment in stubs_apitest.cc for a " +
        "hint about fixing this failure.\n\n");
    reportFailure();
  }
}