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

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

'use strict';

// The original color of the page that we're injecting into.
const ORIGINAL_COLOR = 'rgb(255, 0, 0)';  // red

// The color we inject into the page (first).
const INJECTED_COLOR = 'rgb(0, 128, 0)';  // green

// A secondary color we inject into the page (so that we can differentiate
// between the original and first injected color).
const INJECTED_COLOR2 = 'rgb(255, 255, 0)';  // yellow

// CSS to inject, corresopnding to `INJECTED_COLOR`.
const CSS = '#main { color: green !important; }';
// CSS to inject, corresponding to `INJECTED_COLOR2`.
const CSS2 = CSS.replace('green', 'yellow');
// A file to inject. This also sets the color to `INJECTED_COLOR`.
const FILE = '/file.css';
// A second file to inject. This sets the color to `INJECTED_COLOR2`.
const FILE2 = '/file2.css';

// Aliases for brevity.
const insertCSS = chrome.scripting.insertCSS;
const removeCSS = chrome.scripting.removeCSS;

// Returns the frame IDs from the tab with the given `tabId`.
async function getFrameIds(tabId) {
  // TODO(devlin: Promise-ify webNavigation.
  let frames = await new Promise(resolve => {
      chrome.webNavigation.getAllFrames({tabId}, resolve);
  });

  // Sort frames by frameId.
  let sortedFrames = frames.sort(
      (a, b) => a.frameId < b.frameId ? -1 : a.frameId > b.frameId ? 1 : 0);

  // Validate the frames - there should be 5 total, and the main frame should
  // be first.
  chrome.test.assertEq(5, sortedFrames.length);
  chrome.test.assertEq(sortedFrames[0].frameId, 0 /*main frame id*/);
  chrome.test.assertTrue(
      sortedFrames[1].frameId > 0 /* first non-main-frame id */);

  // Return the array of frame IDs.
  return sortedFrames.map(frame => frame.frameId);
}

// Returns the current color of the frame with `frameId` in the tab with
// `tabId`.
async function getCurrentColor(tabId, frameId) {
  const scriptResults = await chrome.scripting.executeScript({
    target: {tabId, frameIds: [frameId]},
    func: () => {
      const element = document.getElementById('main');
      const style = getComputedStyle(element);
      return style.getPropertyValue('color');
    },
  });
  chrome.test.assertEq(1, scriptResults.length)
  return scriptResults[0].result;
}

let tabId = -1;
let frameIds = [];

// `expectedColorsForFrames` holds a snapshot of expected values of the CSS
// colors being inserted/removed, for each of the frame ids. The array is
// sorted in the order of the frame IDs, such that the color at
// `expectedColorsForFrames[0] corresponds to the color for the frame with
// id `frameIds[0]`. Values get validated at the completion of the each test
// below.
//
// Each frame is a child of the frame preceding it. Frames 0 through 3 are
// <iframe src="..."> while frame 4 is <iframe srcdoc="..."> (about:srcdoc).
let expectedColorsForFrames = [
  ORIGINAL_COLOR, ORIGINAL_COLOR, ORIGINAL_COLOR, ORIGINAL_COLOR,
  ORIGINAL_COLOR
];

// Updates the expected state in `expectedColorsForFrames`. Undefined values in
// `delta` correspond to "no change" in `expectedColorsForFrames`.
function updateExpectedState(delta) {
  Object.assign(expectedColorsForFrames, delta);
}

// Validates that the colors are as expected for each frame.
async function checkColors() {
  for (let i = 0; i < frameIds.length; ++i) {
    const frameId = frameIds[i];
    let color = await getCurrentColor(tabId, frameId);
    chrome.test.assertEq(
        expectedColorsForFrames[i], color,
        `Improper color value for frame: ${frameId}`);
  }
}

// Loads `url` in a new tab, waits for it to finish loading, and returns the
// tabId of the newly-created tab.
// TODO(crbug.com/40568208): Update this to use
// test_resources/tabs_util.js when extension service workers support
// modules.
async function createTab(url) {
  return new Promise(resolve => {
    // Wait for `url` to finish loading.
    chrome.tabs.onUpdated.addListener(
        async function listener(updatedTabId, {status}) {
      if (status != 'complete')
        return;
      chrome.tabs.onUpdated.removeListener(listener);

      resolve(updatedTabId);
    });

    chrome.tabs.create({url});
  });
}

chrome.test.runTests([
  async function loadTab() {
    const config = await new Promise(resolve => {
      chrome.test.getConfig(resolve);
    });
    const testUrl = `http://example.com:${config.testServer.port}` +
        '/extensions/api_test/scripting/remove_css/test.html';
    tabId = await createTab(testUrl);
    frameIds = await getFrameIds(tabId);

    chrome.test.succeed();
  },
  async function insertCSSShouldSucceed() {
    // Insert CSS into every frame.
    await insertCSS({target: {tabId, allFrames: true}, css: CSS});
    updateExpectedState(
        [INJECTED_COLOR, INJECTED_COLOR, INJECTED_COLOR,
         INJECTED_COLOR, INJECTED_COLOR]);
    await checkColors();
    chrome.test.succeed();
  },
  async function removeCSSShouldSucceed() {
    // When no frame ID is specified, the CSS is removed from the top frame
    // Others should be unaffected (and keep the injected color).
    await removeCSS({target: {tabId}, css: CSS});
    updateExpectedState([ORIGINAL_COLOR, , , , , ]);
    await checkColors();
    chrome.test.succeed();
  },
  async function removeCSSWithDifferentCodeShouldDoNothing() {
    // If the specified code differs by even one character, it does not
    // match any inserted CSS and therefore nothing is removed.
    const slightlyOffCSS = CSS + ' ';
    // TODO(devlin): We don't currently return an error if the CSS to remove
    // did not match an inserted stylesheet. We could, which would make it
    // easier for developers to catch mistakes.
    await removeCSS({target: {tabId, allFrames: true}, css: slightlyOffCSS});
    // Note: no change in expected state.
    await checkColors();
    chrome.test.succeed();
  },
  async function removeCSSWithDifferentCSSOriginShouldDoNothing() {
    // If only the CSS origin differs, nothing is removed.
    await removeCSS({target: {tabId, frameIds}, css: CSS, origin: 'USER'});
    await checkColors();
    chrome.test.succeed();
  },
  async function removeCSSWithFrameIdShouldSucceed() {
    // When a frame ID is specified, the CSS is removed only from the given
    // frame.
    await removeCSS(
        {target: {tabId, frameIds: [frameIds[1]]}, css: CSS});
    updateExpectedState([ , ORIGINAL_COLOR, , , , ]);
    await checkColors();
    chrome.test.succeed();
  },
  async function removeCSSWithAllFramesShouldSucceed() {
    // When "allFrames" is set to true, the CSS is removed from all
    // frames.
    await removeCSS({target: {tabId, allFrames: true}, css: CSS});
    updateExpectedState([ORIGINAL_COLOR, ORIGINAL_COLOR, ORIGINAL_COLOR,
                         ORIGINAL_COLOR, ORIGINAL_COLOR]);
    await checkColors();
    chrome.test.succeed();
  },
  async function insertCSSWithFileShouldSucceed() {
    // Insert some CSS using a file (to then be removed).
    await insertCSS({target: {tabId, allFrames: true}, files: [FILE]});
    updateExpectedState([INJECTED_COLOR, INJECTED_COLOR, INJECTED_COLOR,
                         INJECTED_COLOR, INJECTED_COLOR]);
    await checkColors();
    chrome.test.succeed();
  },
  async function removeCSSWithFileShouldSucceed() {
    // When no frame ID is specified, the CSS is removed from the top frame.
    await removeCSS({target: {tabId}, files: [FILE]});
    updateExpectedState([ORIGINAL_COLOR, , , , , ]);
    await checkColors();
    chrome.test.succeed();
  },
  async function removeCSSWithDifferentFileShouldDoNothing() {
    // The CSS is not removed when passing a different file (even though the
    // contents of /file.css and /other.css are identical).
    await removeCSS(
        {target: {tabId, allFrames: true}, files: ['/other.css']});
    await checkColors();
    chrome.test.succeed();
  },
  async function insertAndRemoveCSSWithMultipleFilesShouldSucceed() {
    // Insert two style sheets. The second should "win", since it's latest
    // injected.
    await insertCSS({target: {tabId}, files: [FILE, FILE2]});
    updateExpectedState([INJECTED_COLOR2, , , , , ]);
    await checkColors();

    // Remove both previously-injected files.
    await removeCSS({target: {tabId}, files: [FILE, FILE2]});
    updateExpectedState([ORIGINAL_COLOR, , , , , ]);
    await checkColors();

    chrome.test.succeed();
  },
  async function insertMultipleFilesAndRemoveOneAtATime() {
    // Insert two style sheets. The second should "win", since it's latest
    // injected.
    await insertCSS({target: {tabId}, files: [FILE, FILE2]});
    updateExpectedState([INJECTED_COLOR2, , , , , ]);
    await checkColors();

    // Remove only one of the previously-injected files.
    await removeCSS({target: {tabId}, files: [FILE2]});
    updateExpectedState([INJECTED_COLOR, , , , , ]);
    await checkColors();

    // Now, remove the second.
    await removeCSS({target: {tabId}, files: [FILE]});
    updateExpectedState([ORIGINAL_COLOR, , , , , ]);
    await checkColors();

    chrome.test.succeed();
  },
  async function insertCSSWithDuplicateCodeShouldSucceed() {
    // Start by inserting the second CSS (which is a different color) into the
    // top frame.
    await insertCSS({target: {tabId}, css: CSS2});
    updateExpectedState([INJECTED_COLOR2, , , , , ]);
    await checkColors();
    // Then, re-insert the first CSS. The top frame should be updated.
    await insertCSS({target: {tabId}, css: CSS});
    updateExpectedState([INJECTED_COLOR, , , , , ]);
    await checkColors();
    chrome.test.succeed();
  },
  async function removeCSSWithDuplicateCodeShouldSucceed() {
    // Remove the first CSS. The second-inserted CSS should take effect again.
    await removeCSS({target: {tabId}, css: CSS});
    updateExpectedState([INJECTED_COLOR2, , , , , ]);
    await checkColors();

    // Remove the second CSS. The color should go back to the original color of
    // the frame.
    await removeCSS({target: {tabId}, css: CSS2});
    updateExpectedState([ORIGINAL_COLOR, , , , , ]);
    await checkColors();
    chrome.test.succeed();
  },
  async function noSuchTab() {
    const nonExistentTabId = 99999;
    await chrome.test.assertPromiseRejects(
        removeCSS({
          target: {
            tabId: nonExistentTabId,
          },
          css: CSS,
        }),
        `Error: No tab with id: ${nonExistentTabId}`);
    await checkColors();
    chrome.test.succeed();
  },
  async function noSuchFile() {
    const noSuchFile = 'no_such_file.js';
    // Edge case: When removing inserted files, we don't actually read
    // the file content (because it's unnecessary, and would be wasteful).
    // We also don't fire an error when there was no matching CSS inserted
    // (see "removeCSSWithDifferentCodeShouldDoNothing()" test case). This
    // combines to mean that even though there's no such file here, we
    // don't actually fire an error. This will be fixed if/when we return
    // an error for removing a non-existent stylesheet.
    await removeCSS({
      target: {tabId},
      files: [noSuchFile],
    });
    await checkColors();
    chrome.test.succeed();
  },
  async function disallowedPermission() {
    const config = await new Promise(resolve => {
      chrome.test.getConfig(resolve);
    });
    const testUrl = `http://google.com:${config.testServer.port}` +
        '/extensions/api_test/scripting/remove_css/test.html';
    tabId = await createTab(testUrl);
    // Note: We don't test the expected colors here, because that relies on the
    // extension having access to the host (in order to inject the script to
    // retrieve the colors), which it doesn't have here.
    await chrome.test.assertPromiseRejects(
        removeCSS({
          target: {tabId},
          css: CSS,
        }),
        'Error: Cannot access contents of the page. ' +
            'Extension manifest must request permission to ' +
            'access the respective host.');
    chrome.test.succeed();
  },
]);