chromium/chrome/test/data/extensions/api_test/scripting/main_frame/worker.js

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

const NEW_TITLE_FROM_FUNCTION = 'Hello, world!';
const NEW_TITLE_FROM_FILE = 'Goodnight';

function injectedFunction() {
  // NOTE(devlin): We currently need to (re)hard-code this title, since the
  // injected function won't keep the execution context from the surrounding
  // script.
  document.title = 'Hello, world!';
  return document.title;
}

function injectedFunctionWithArgument(newTitle) {
  document.title = newTitle;
  return document.title;
}

function echoArguments() {
  const args = Array.from(arguments);
  return args;
}

// A helper function to return "flags" set by scripts in the isolated and main
// worlds. Note that the main world script flag is set by a script in the html
// file.
function getExecutionWorldFlags() {
  // Note: We use '<none>' here because undefined and null values aren't
  // preserved in return results from executeScript() calls.
  return {
    isolatedWorld: window.isolatedWorldFlag || '<none>',
    mainWorld: window.mainWorldFlag || '<none>',
  };
}

async function getSingleTab(query) {
  const tabs = await new Promise(resolve => {
    chrome.tabs.query(query, resolve);
  });
  chrome.test.assertEq(1, tabs.length);
  return tabs[0];
}

chrome.test.runTests([
  async function changeTitleFromFunction() {
    const query = {url: 'http://example.com/*'};
    let tab = await getSingleTab(query);
    const results = await chrome.scripting.executeScript({
      target: {
        tabId: tab.id,
      },
      func: injectedFunction,
    });
    chrome.test.assertEq(1, results.length);
    chrome.test.assertEq(NEW_TITLE_FROM_FUNCTION, results[0].result);
    tab = await getSingleTab(query);
    chrome.test.assertEq(NEW_TITLE_FROM_FUNCTION, tab.title);
    chrome.test.succeed();
  },

  async function changeTitleWithCurriedArguments() {
    const query = {url: 'http://example.com/*'};
    let tab = await getSingleTab(query);
    const customNewTitle = 'Custom Title';
    const results = await chrome.scripting.executeScript({
      target: {
        tabId: tab.id,
      },
      func: injectedFunctionWithArgument,
      args: [customNewTitle],
    });
    chrome.test.assertEq(1, results.length);
    chrome.test.assertEq(customNewTitle, results[0].result);
    tab = await getSingleTab(query);
    chrome.test.assertEq(customNewTitle, tab.title);
    chrome.test.succeed();
  },

  async function echoArgsOfDifferentTypes() {
    const query = {url: 'http://example.com/*'};
    let tab = await getSingleTab(query);
    const args = [
        42,
        0.07,
        'foo',
        true,
        [1, 2, 3],
        { key: 'value' },
        null,
    ];
    const results = await chrome.scripting.executeScript({
      target: {
        tabId: tab.id,
      },
      func: echoArguments,
      args: args,
    });
    chrome.test.assertEq(1, results.length);
    chrome.test.assertEq(args, results[0].result);
    chrome.test.succeed();
  },

  async function nullInArgsIsNotPreserved() {
    const query = {url: 'http://example.com/*'};
    let tab = await getSingleTab(query);
    const args = [
        { key: 'value', nullKey: null },
    ];
    const results = await chrome.scripting.executeScript({
      target: {
        tabId: tab.id,
      },
      func: echoArguments,
      args: args,
    });
    chrome.test.assertEq(1, results.length);
    // Currently, null values in objects are not preserved. We should fix this,
    // but the IDL extension schema currently does not support the preserveNull
    // attribute, and adding it in for arrays is non-trivial.
    chrome.test.assertEq([{ key: 'value' }], results[0].result);
    chrome.test.succeed();
  },

  async function changeTitleFromFile() {
    const query = {url: 'http://example.com/*'};
    let tab = await getSingleTab(query);
    const results = await chrome.scripting.executeScript({
      target: {
        tabId: tab.id,
      },
      files: ['script_file.js'],
    });
    chrome.test.assertEq(1, results.length);
    chrome.test.assertEq(NEW_TITLE_FROM_FILE, results[0].result);
    tab = await getSingleTab(query);
    chrome.test.assertEq(NEW_TITLE_FROM_FILE, tab.title);
    chrome.test.succeed();
  },

  async function injectedFunctionReturnsNothing() {
    const query = {url: 'http://example.com/*'};
    let tab = await getSingleTab(query);
    const results = await chrome.scripting.executeScript({
      target: {
        tabId: tab.id,
      },
      // Note: This function has no return statement; in JS, this means
      // the return value will be undefined.
      func: () => {},
    });
    chrome.test.assertEq(1, results.length);
    // NOTE: Undefined results are mapped to null in our bindings layer,
    // because they converted from empty base::Values in the same way.
    // NOTE AS WELL: We use `val === null` (rather than
    // `assertEq(null, val)` because assertEq will classify null and undefined
    // as equal.
    chrome.test.assertTrue(results[0].result === null);
    chrome.test.succeed();
  },

  async function injectedFunctionReturnsNull() {
    const query = {url: 'http://example.com/*'};
    let tab = await getSingleTab(query);
    const results = await chrome.scripting.executeScript({
      target: {
        tabId: tab.id,
      },
      func: () => {
        return null;
      },
    });
    chrome.test.assertEq(1, results.length);
    // NOTE: We use `val === null` (rather than `assertEq(null, val)` because
    // assertEq will classify null and undefined as equal.
    chrome.test.assertTrue(results[0].result === null);
    chrome.test.succeed();
  },

  async function scriptsInjectIntoSameIsolatedWorld() {
    const query = {url: 'http://example.com/*'};
    let tab = await getSingleTab(query);
    const target = {tabId: tab.id};
    // When `world` is unspecified, it defaults to an isolated world.
    await chrome.scripting.executeScript({
      target: target,
      func: () => { window.isolatedWorldFlag = 'from isolated world' },
    });
    let results = await chrome.scripting.executeScript({
      target: target,
      func: getExecutionWorldFlags,
    });
    chrome.test.assertEq(1, results.length);
    chrome.test.assertEq(
        {isolatedWorld: 'from isolated world', mainWorld: '<none>'},
        results[0].result);

    // Subsequent scripts should execute in the same isolated world.
    results = await chrome.scripting.executeScript({
      target: target,
      func: getExecutionWorldFlags,
      world: chrome.scripting.ExecutionWorld.ISOLATED,
    });
    chrome.test.assertEq(1, results.length);
    chrome.test.assertEq(
        {isolatedWorld: 'from isolated world', mainWorld: '<none>'},
        results[0].result);

    chrome.test.succeed();
  },

  async function scriptsCanRunInMainWorld() {
    const query = {url: 'http://example.com/*'};
    let tab = await getSingleTab(query);
    const target = {tabId: tab.id};
    // Set a flag in the isolated world.
    await chrome.scripting.executeScript({
      target: target,
      func: () => { window.isolatedWorldFlag = 'from isolated world' },
    });

    // The script executing in the main world should not see the flag from the
    // isolated world, but should see the one the page set in the main world.
    const results = await chrome.scripting.executeScript({
      target: target,
      func: getExecutionWorldFlags,
      world: chrome.scripting.ExecutionWorld.MAIN,
    });
    chrome.test.assertEq(1, results.length);
    chrome.test.assertEq(
        {isolatedWorld: '<none>', mainWorld: 'from main world'},
        results[0].result);

    chrome.test.succeed();
  },

  async function promisesAreResolved() {
    const query = {url: 'http://example.com/*'};
    let tab = await getSingleTab(query);
    const target = {tabId: tab.id};

    const promiseFunc = async () => {
      // Return a promise that resolves asynchronously.
      let result = await new Promise((r) => {
        setTimeout(r, 50, 'Hello, World!');
      });
      return result;
    };
    const results = await chrome.scripting.executeScript({
      target: target,
      func: promiseFunc,
    });

    chrome.test.assertEq(1, results.length);
    chrome.test.assertEq('Hello, World!', results[0].result);
    chrome.test.succeed();
  },

  async function injectedFunctionHasError() {
    const query = {url: 'http://example.com/*'};
    let tab = await getSingleTab(query);
    const results = await chrome.scripting.executeScript({
      target: {
        tabId: tab.id,
      },
      // This will throw a runtime error, since foo, bar, and baz aren't
      // defined.
      func: () => {
        foo.bar = baz;
        return 3;
      },
    });

    // TODO(devlin): Currently, we don't pass the error from the injected
    // script back to the extension in any way. It'd be helpful to pass
    // this along to the extension.
    chrome.test.assertEq(1, results.length);
    chrome.test.assertEq(null, results[0].result);
    chrome.test.succeed();
  },

  // The `func` property used to be called `function`. This should still work
  // for backwards compatibility.
  async function usingOldFunctionPropertyNameWorks() {
    const changeTitleAgain = function() {
      document.title = 'Some New Title';
      return document.title;
    };
    const query = {url: 'http://example.com/*'};
    let tab = await getSingleTab(query);
    const results = await chrome.scripting.executeScript({
      target: {
        tabId: tab.id,
      },
      function: changeTitleAgain,
    });
    const newTitle = 'Some New Title';
    chrome.test.assertEq(1, results.length);
    chrome.test.assertEq(newTitle, results[0].result);
    tab = await getSingleTab(query);
    chrome.test.assertEq(newTitle, tab.title);
    chrome.test.succeed();
  },

  async function multipleFilesSpecified() {
    const query = {url: 'http://example.com/*'};
    let tab = await getSingleTab(query);
    // Double-check that the title is not the one from the script file to be
    // injected.
    chrome.test.assertNe(NEW_TITLE_FROM_FILE, tab.title);
    const results = await chrome.scripting.executeScript({
      target: {
        tabId: tab.id,
      },
      files: ['script_file.js', 'script_file2.js'],
    });
    // The call injected two scripts; the first changes the title, and the
    // second reports it plus a suffix. This checks that both scripts inject
    // and that the order was preserved (since the first sets the title used
    // in the second).
    chrome.test.assertEq(1, results.length);
    chrome.test.assertEq(NEW_TITLE_FROM_FILE + ' From Second Script',
                         results[0].result);
    chrome.test.succeed();
  },

  async function onlyOneOfFunctionAndFunc() {
    const query = {url: 'http://example.com/*'};
    let tab = await getSingleTab(query);
    await chrome.test.assertPromiseRejects(
        chrome.scripting.executeScript({
          target: {
            tabId: tab.id,
          },
          func: injectedFunction,
          function: injectedFunction,
        }),
        `Error: Both 'func' and 'function' were specified. ` +
        `Only 'func' should be used.`);
    chrome.test.succeed();
  },

  async function noSuchTab() {
    const nonExistentTabId = 99999;
    await chrome.test.assertPromiseRejects(
        chrome.scripting.executeScript({
          target: {
            tabId: nonExistentTabId,
          },
          func: injectedFunction,
        }),
        `Error: No tab with id: ${nonExistentTabId}`);
    chrome.test.succeed();
  },

  async function noSuchFile() {
    const noSuchFile = 'no_such_file.js';
    const query = {url: 'http://example.com/*'};
    let tab = await getSingleTab(query);
    await chrome.test.assertPromiseRejects(
        chrome.scripting.executeScript({
          target: {
            tabId: tab.id,
          },
          files: [noSuchFile],
        }),
        `Error: Could not load file: '${noSuchFile}'.`);
    chrome.test.succeed();
  },

  async function noFilesSpecified() {
    const query = {url: 'http://example.com/*'};
    let tab = await getSingleTab(query);
    await chrome.test.assertPromiseRejects(
        chrome.scripting.executeScript({
          target: {
            tabId: tab.id,
          },
          files: [],
        }),
        'Error: At least one file must be specified.');
    chrome.test.succeed();
  },

  async function duplicateFilesSpecified() {
    const query = {url: 'http://example.com/*'};
    let tab = await getSingleTab(query);
    await chrome.test.assertPromiseRejects(
        chrome.scripting.executeScript({
          target: {
            tabId: tab.id,
          },
          files: ['script_file.js', 'script_file.js'],
        }),
        `Error: Duplicate file specified: 'script_file.js'.`);

    // Try again with a preceding slash.
    await chrome.test.assertPromiseRejects(
        chrome.scripting.executeScript({
          target: {
            tabId: tab.id,
          },
          files: ['script_file.js', '/script_file.js'],
        }),
        `Error: Duplicate file specified: '/script_file.js'.`);
    chrome.test.succeed();
  },

  async function disallowedPermission() {
    const query = {url: 'http://chromium.org/*'};
    let tab = await getSingleTab(query);
    const expectedTitle = 'Title Of Awesomeness';
    chrome.test.assertEq(expectedTitle, tab.title);
    await chrome.test.assertPromiseRejects(
        chrome.scripting.executeScript({
          target: {
            tabId: tab.id,
          },
          func: injectedFunction,
        }),
        `Error: Cannot access contents of url "${tab.url}". ` +
            'Extension manifest must request permission ' +
            'to access this host.');
    tab = await getSingleTab(query);
    chrome.test.assertEq(expectedTitle, tab.title);
    chrome.test.succeed();
  },

  async function unserializableCurriedArguments() {
    const query = {url: 'http://example.com/*'};
    let tab = await getSingleTab(query);
    const expectedError =
        'Error in invocation of scripting.executeScript(' +
        'scripting.ScriptInjection injection, optional function callback): ' +
        'Error at parameter \'injection\': Error at property \'args\': ' +
        'Error at index 0: Value is unserializable.';
    chrome.test.assertThrows(
        chrome.scripting.executeScript,
        [{
          target: {
            tabId: tab.id,
          },
          func: echoArguments,
          args: [function() {}],
        }],
        expectedError);
    chrome.test.succeed();
  },

  async function argsPassedWithFiles() {
    const query = {url: 'http://example.com/*'};
    let tab = await getSingleTab(query);
    const expectedError =
    await chrome.test.assertPromiseRejects(
        chrome.scripting.executeScript({
          target: {
            tabId: tab.id,
          },
          files: ['script_file.js'],
          args: ['foo'],
        }),
        `Error: 'args' may not be used with file injections.`);
    chrome.test.succeed();
  }
]);