/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

const PRIORITY_SET_TOPIC =
  "process-priority-manager:TEST-ONLY:process-priority-set";

// Copied from Hal.cpp
const PROCESS_PRIORITY_FOREGROUND = "FOREGROUND";
const PROCESS_PRIORITY_BACKGROUND_PERCEIVABLE = "BACKGROUND_PERCEIVABLE";
const PROCESS_PRIORITY_BACKGROUND = "BACKGROUND";

// This is how many milliseconds we'll wait for a process priority
// change before we assume that it's just not happening.
const WAIT_FOR_CHANGE_TIME_MS = 2000;

// A convenience function for getting the child ID from a browsing context.
function browsingContextChildID(bc) {
  return bc.currentWindowGlobal?.domProcess.childID;
}

/**
 * This class is responsible for watching process priority changes, and
 * mapping them to tabs in a single window.
 */
class TabPriorityWatcher {
  /**
   * Constructing a TabPriorityWatcher should happen before any tests
   * start when there's only a single tab in the window.
   *
   * Callers must call `destroy()` on any instance that is constructed
   * when the test is completed.
   *
   * @param tabbrowser (<tabbrowser>)
   *   The tabbrowser (gBrowser) for the window to be tested.
   */
  constructor(tabbrowser) {
    this.tabbrowser = tabbrowser;
    Assert.equal(
      tabbrowser.tabs.length,
      1,
      "TabPriorityWatcher must be constructed in a window " +
        "with a single tab to start."
    );

    // This maps from childIDs to process priorities.
    this.priorityMap = new Map();

    // The keys in this map are childIDs we're not expecting to change.
    // Each value is either null (if no change has been seen) or the
    // priority that the process changed to.
    this.noChangeChildIDs = new Map();

    Services.obs.addObserver(this, PRIORITY_SET_TOPIC);
  }

  /**
   * Cleans up lingering references for an instance of
   * TabPriorityWatcher to avoid leaks. This should be called when
   * finishing the test.
   */
  destroy() {
    Services.obs.removeObserver(this, PRIORITY_SET_TOPIC);
  }

  /**
   * This returns a Promise that resolves when the process with
   * the given childID reaches the given priority.
   * This will eventually time out if that priority is never reached.
   *
   * @param childID
   *   The childID of the process to wait on.
   * @param expectedPriority (String)
   *   One of the PROCESS_PRIORITY_ constants defined at the
   *   top of this file.
   * @return Promise
   * @resolves undefined
   *   Once the browser reaches the expected priority.
   */
  async waitForPriorityChange(childID, expectedPriority) {
    await TestUtils.waitForCondition(() => {
      let currentPriority = this.priorityMap.get(childID);
      if (currentPriority == expectedPriority) {
        Assert.ok(
          true,
          `Process with child ID ${childID} reached expected ` +
            `priority: ${currentPriority}`
        );
        return true;
      }
      return false;
    }, `Waiting for process with child ID ${childID} to reach priority ${expectedPriority}`);
  }

  /**
   * Returns a Promise that resolves after a duration of
   * WAIT_FOR_CHANGE_TIME_MS. During that time, if the process
   * with the passed in child ID changes priority, a test
   * failure will be registered.
   *
   * @param childID
   *   The childID of the process that we expect to not change priority.
   * @return Promise
   * @resolves undefined
   *   Once the WAIT_FOR_CHANGE_TIME_MS duration has passed.
   */
  async ensureNoPriorityChange(childID) {
    this.noChangeChildIDs.set(childID, null);
    // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
    await new Promise(resolve => setTimeout(resolve, WAIT_FOR_CHANGE_TIME_MS));
    let priority = this.noChangeChildIDs.get(childID);
    Assert.equal(
      priority,
      null,
      `Should have seen no process priority change for child ID ${childID}`
    );
    this.noChangeChildIDs.delete(childID);
  }

  /**
   * This returns a Promise that resolves when all of the processes
   * of the browsing contexts in the browsing context tree
   * of a particular <browser> have reached a particular priority.
   * This will eventually time out if that priority is never reached.
   *
   * @param browser (<browser>)
   *   The <browser> to get the BC tree from.
   * @param expectedPriority (String)
   *   One of the PROCESS_PRIORITY_ constants defined at the
   *   top of this file.
   * @return Promise
   * @resolves undefined
   *   Once the browser reaches the expected priority.
   */
  async waitForBrowserTreePriority(browser, expectedPriority) {
    let childIDs = new Set(
      browser.browsingContext
        .getAllBrowsingContextsInSubtree()
        .map(browsingContextChildID)
    );
    let promises = [];
    for (let childID of childIDs) {
      let currentPriority = this.priorityMap.get(childID);

      promises.push(
        currentPriority == expectedPriority
          ? this.ensureNoPriorityChange(childID)
          : this.waitForPriorityChange(childID, expectedPriority)
      );
    }

    await Promise.all(promises);
  }

  /**
   * Synchronously returns the priority of a particular child ID.
   *
   * @param childID
   *   The childID to get the content process priority for.
   * @return String
   *   The priority of the child ID's process.
   */
  currentPriority(childID) {
    return this.priorityMap.get(childID);
  }

  /**
   * A utility function that takes a string passed via the
   * PRIORITY_SET_TOPIC observer notification and extracts the
   * childID and priority string.
   *
   * @param ppmDataString (String)
   *   The string data passed through the PRIORITY_SET_TOPIC observer
   *   notification.
   * @return Object
   *   An object with the following properties:
   *
   *   childID (Number)
   *     The ID of the content process that changed priority.
   *
   *   priority (String)
   *     The priority that the content process was set to.
   */
  parsePPMData(ppmDataString) {
    let [childIDStr, priority] = ppmDataString.split(":");
    return {
      childID: parseInt(childIDStr, 10),
      priority,
    };
  }

  /** nsIObserver **/
  observe(subject, topic, data) {
    if (topic != PRIORITY_SET_TOPIC) {
      Assert.ok(false, "TabPriorityWatcher is observing the wrong topic");
      return;
    }

    let { childID, priority } = this.parsePPMData(data);
    if (this.noChangeChildIDs.has(childID)) {
      this.noChangeChildIDs.set(childID, priority);
    }
    this.priorityMap.set(childID, priority);
  }
}

let gTabPriorityWatcher;

add_setup(async function() {
  // We need to turn on testMode for the process priority manager in
  // order to receive the observer notifications that this test relies on.
  await SpecialPowers.pushPrefEnv({
    set: [
      ["dom.ipc.processPriorityManager.testMode", true],
      ["dom.ipc.processPriorityManager.enabled", true],
    ],
  });
  gTabPriorityWatcher = new TabPriorityWatcher(gBrowser);
});

registerCleanupFunction(() => {
  gTabPriorityWatcher.destroy();
  gTabPriorityWatcher = null;
});

/**
 * Utility function that switches the current tabbrowser from one
 * tab to another, and ensures that the tab that goes into the background
 * has (or reaches) a particular content process priority.
 *
 * It is expected that the fromTab and toTab belong to two separate content
 * processes.
 *
 * @param Object
 *   An object with the following properties:
 *
 *   fromTab (<tab>)
 *     The tab that will be switched from to the toTab. The fromTab
 *     is the one that will be going into the background.
 *
 *   toTab (<tab>)
 *     The tab that will be switched to from the fromTab. The toTab
 *     is presumed to start in the background, and will enter the
 *     foreground.
 *
 *   fromTabExpectedPriority (String)
 *     The priority that the content process for the fromTab is
 *     expected to be (or reach) after the tab goes into the background.
 *     This should be one of the PROCESS_PRIORITY_ strings defined at the
 *     top of the file.
 *
 * @return Promise
 * @resolves undefined
 *   Once the tab switch is complete, and the two content processes for the
 *   tabs have reached the expected priority levels.
 */
async function assertPriorityChangeOnBackground({
  fromTab,
  toTab,
  fromTabExpectedPriority,
}) {
  let fromBrowser = fromTab.linkedBrowser;
  let toBrowser = toTab.linkedBrowser;

  // If the tabs aren't running in separate processes, none of the
  // rest of this is going to work.
  Assert.notEqual(
    toBrowser.frameLoader.remoteTab.osPid,
    fromBrowser.frameLoader.remoteTab.osPid,
    "Tabs should be running in separate processes."
  );

  let fromPromise = gTabPriorityWatcher.waitForBrowserTreePriority(
    fromBrowser,
    fromTabExpectedPriority
  );
  let toPromise = gTabPriorityWatcher.waitForBrowserTreePriority(
    toBrowser,
    PROCESS_PRIORITY_FOREGROUND
  );

  await BrowserTestUtils.switchTab(gBrowser, toTab);
  await Promise.all([fromPromise, toPromise]);
}

/**
 * Test that if a normal tab goes into the background,
 * it has its process priority lowered to PROCESS_PRIORITY_BACKGROUND.
 * Additionally, test priorityHint flag sets the process priority
 * appropriately to PROCESS_PRIORITY_BACKGROUND and PROCESS_PRIORITY_FOREGROUND.
 */
add_task(async function test_normal_background_tab() {
  let originalTab = gBrowser.selectedTab;

  await BrowserTestUtils.withNewTab(
    "https://example.com/browser/dom/ipc/tests/file_cross_frame.html",
    async browser => {
      let tab = gBrowser.getTabForBrowser(browser);
      await assertPriorityChangeOnBackground({
        fromTab: tab,
        toTab: originalTab,
        fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
      });

      await assertPriorityChangeOnBackground({
        fromTab: originalTab,
        toTab: tab,
        fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
      });

      let origtabID = browsingContextChildID(
        originalTab.linkedBrowser.browsingContext
      );

      Assert.equal(
        originalTab.linkedBrowser.frameLoader.remoteTab.priorityHint,
        false,
        "PriorityHint of the original tab should be false by default"
      );

      // Changing renderLayers doesn't change priority of the background tab.
      originalTab.linkedBrowser.preserveLayers(true);
      originalTab.linkedBrowser.renderLayers = true;
      await new Promise(resolve =>
        // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
        setTimeout(resolve, WAIT_FOR_CHANGE_TIME_MS)
      );
      Assert.equal(
        gTabPriorityWatcher.currentPriority(origtabID),
        PROCESS_PRIORITY_BACKGROUND,
        "Tab didn't get prioritized only due to renderLayers"
      );

      // Test when priorityHint is true, the original tab priority
      // becomes PROCESS_PRIORITY_FOREGROUND.
      originalTab.linkedBrowser.frameLoader.remoteTab.priorityHint = true;
      Assert.equal(
        gTabPriorityWatcher.currentPriority(origtabID),
        PROCESS_PRIORITY_FOREGROUND,
        "Setting priorityHint to true should set the original tab to foreground priority"
      );

      // Test when priorityHint is false, the original tab priority
      // becomes PROCESS_PRIORITY_BACKGROUND.
      originalTab.linkedBrowser.frameLoader.remoteTab.priorityHint = false;
      await new Promise(resolve =>
        // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
        setTimeout(resolve, WAIT_FOR_CHANGE_TIME_MS)
      );
      Assert.equal(
        gTabPriorityWatcher.currentPriority(origtabID),
        PROCESS_PRIORITY_BACKGROUND,
        "Setting priorityHint to false should set the original tab to background priority"
      );

      let tabID = browsingContextChildID(tab.linkedBrowser.browsingContext);

      // Test when priorityHint is true, the process priority of the
      // active tab remains PROCESS_PRIORITY_FOREGROUND.
      tab.linkedBrowser.frameLoader.remoteTab.priorityHint = true;
      Assert.equal(
        gTabPriorityWatcher.currentPriority(tabID),
        PROCESS_PRIORITY_FOREGROUND,
        "Setting priorityHint to true should maintain the new tab priority as foreground"
      );

      // Test when priorityHint is false, the process priority of the
      // active tab remains PROCESS_PRIORITY_FOREGROUND.
      tab.linkedBrowser.frameLoader.remoteTab.priorityHint = false;
      Assert.equal(
        gTabPriorityWatcher.currentPriority(tabID),
        PROCESS_PRIORITY_FOREGROUND,
        "Setting priorityHint to false should maintain the new tab priority as foreground"
      );

      originalTab.linkedBrowser.preserveLayers(false);
      originalTab.linkedBrowser.renderLayers = false;
    }
  );
});

// Load a simple page on the given host into a new tab.
async function loadKeepAliveTab(host) {
  let tab = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    host + "/browser/dom/ipc/tests/file_dummy.html"
  );
  let childID = browsingContextChildID(
    gBrowser.selectedBrowser.browsingContext
  );

  Assert.equal(
    gTabPriorityWatcher.currentPriority(childID),
    PROCESS_PRIORITY_FOREGROUND,
    "Loading a new tab should make it prioritized"
  );

  if (SpecialPowers.useRemoteSubframes) {
    // There must be only one process with a remote type for the tab we loaded
    // to ensure that when we load a new page into the iframe with that host
    // that it will end up in the same process as the initial tab.
    let remoteType = gBrowser.selectedBrowser.remoteType;
    await TestUtils.waitForCondition(() => {
      return (
        ChromeUtils.getAllDOMProcesses().filter(
          process => process.remoteType == remoteType
        ).length == 1
      );
    }, `Waiting for there to be only one process with remote type ${remoteType}`);
  }

  return { tab, childID };
}

/**
 * If an iframe in a foreground tab is navigated to a new page for
 * a different site, then the process of the new iframe page should
 * have priority PROCESS_PRIORITY_FOREGROUND. Additionally, if Fission
 * is enabled, then the old iframe page's process's priority should be
 * lowered to PROCESS_PRIORITY_BACKGROUND.
 */
add_task(async function test_iframe_navigate() {
  // This test (eventually) loads a page from the host topHost that has an
  // iframe from iframe1Host. It then navigates the iframe to iframe2Host.
  let topHost = "https://example.com";
  let iframe1Host = "https://example.org";
  let iframe2Host = "https://example.net";

  // Before we load the final test page into a tab, we need to load pages
  // from both iframe hosts into tabs. This is needed so that we are testing
  // the "load a new page" part of prioritization and not the "initial
  // process load" part. Additionally, it ensures that the process for the
  // initial iframe page doesn't shut down once we navigate away from it,
  // which will also affect its prioritization.
  let { tab: iframe1Tab, childID: iframe1TabChildID } = await loadKeepAliveTab(
    iframe1Host
  );
  let { tab: iframe2Tab, childID: iframe2TabChildID } = await loadKeepAliveTab(
    iframe2Host
  );

  await BrowserTestUtils.withNewTab(
    topHost + "/browser/dom/ipc/tests/file_cross_frame.html",
    async browser => {
      Assert.equal(
        gTabPriorityWatcher.currentPriority(iframe2TabChildID),
        PROCESS_PRIORITY_BACKGROUND,
        "Switching to another new tab should deprioritize the old one"
      );

      let topChildID = browsingContextChildID(browser.browsingContext);
      let iframe = browser.browsingContext.children[0];
      let iframe1ChildID = browsingContextChildID(iframe);

      Assert.equal(
        gTabPriorityWatcher.currentPriority(topChildID),
        PROCESS_PRIORITY_FOREGROUND,
        "The top level page in the new tab should be prioritized"
      );

      Assert.equal(
        gTabPriorityWatcher.currentPriority(iframe1ChildID),
        PROCESS_PRIORITY_FOREGROUND,
        "The iframe in the new tab should be prioritized"
      );

      if (SpecialPowers.useRemoteSubframes) {
        // Basic process uniqueness checks for the state after all three tabs
        // are initially loaded.
        Assert.notEqual(
          topChildID,
          iframe1ChildID,
          "file_cross_frame.html should be loaded into a different process " +
            "than its initial iframe"
        );

        Assert.notEqual(
          topChildID,
          iframe2TabChildID,
          "file_cross_frame.html should be loaded into a different process " +
            "than the tab containing iframe2Host"
        );

        Assert.notEqual(
          iframe1ChildID,
          iframe2TabChildID,
          "The initial iframe loaded by file_cross_frame.html should be " +
            "loaded into a different process than the tab containing " +
            "iframe2Host"
        );

        // Note: this assertion depends on our process selection logic.
        // Specifically, that we reuse an existing process for an iframe if
        // possible.
        Assert.equal(
          iframe1TabChildID,
          iframe1ChildID,
          "Both pages loaded in iframe1Host should be in the same process"
        );
      }

      // Do a cross-origin navigation in the iframe in the foreground tab.
      let iframe2URI = iframe2Host + "/browser/dom/ipc/tests/file_dummy.html";
      let loaded = BrowserTestUtils.browserLoaded(browser, true, iframe2URI);
      await SpecialPowers.spawn(iframe, [iframe2URI], async function(
        _iframe2URI
      ) {
        content.location = _iframe2URI;
      });
      await loaded;

      let iframe2ChildID = browsingContextChildID(iframe);
      let iframe1Priority = gTabPriorityWatcher.currentPriority(iframe1ChildID);
      let iframe2Priority = gTabPriorityWatcher.currentPriority(iframe2ChildID);

      if (SpecialPowers.useRemoteSubframes) {
        // Basic process uniqueness check for the state after navigating the
        // iframe. There's no need to check the top level pages because they
        // have not navigated.
        //
        // iframe1ChildID != iframe2ChildID is implied by:
        //   iframe1ChildID != iframe2TabChildID
        //   iframe2TabChildID == iframe2ChildID
        //
        // iframe2ChildID != topChildID is implied by:
        //   topChildID != iframe2TabChildID
        //   iframe2TabChildID == iframe2ChildID

        // Note: this assertion depends on our process selection logic.
        // Specifically, that we reuse an existing process for an iframe if
        // possible. If that changes, this test may need to be carefully
        // rewritten, as the whole point of the test is to check what happens
        // with the priority manager when an iframe shares a process with
        // a page in another tab.
        Assert.equal(
          iframe2TabChildID,
          iframe2ChildID,
          "Both pages loaded in iframe2Host should be in the same process"
        );

        // Now that we've established the relationship between the various
        // processes, we can finally check that the priority manager is doing
        // the right thing.
        Assert.equal(
          iframe1Priority,
          PROCESS_PRIORITY_BACKGROUND,
          "The old iframe process should have been deprioritized"
        );
      } else {
        Assert.equal(
          iframe1ChildID,
          iframe2ChildID,
          "Navigation should not have switched processes"
        );
      }

      Assert.equal(
        iframe2Priority,
        PROCESS_PRIORITY_FOREGROUND,
        "The new iframe process should be prioritized"
      );
    }
  );

  await BrowserTestUtils.removeTab(iframe2Tab);
  await BrowserTestUtils.removeTab(iframe1Tab);
});

/**
 * Test that a cross-group navigation properly preserves the process priority.
 * The goal of this test is to check that the code related to mPriorityActive in
 * CanonicalBrowsingContext::ReplacedBy works correctly, but in practice the
 * prioritization code in SetRenderLayers will also make this test pass, though
 * that prioritization happens slightly later.
 */
add_task(async function test_cross_group_navigate() {
  // This page is same-site with the page we're going to cross-group navigate to.
  let coopPage =
    "https://example.com/browser/dom/tests/browser/file_coop_coep.html";

  // Load it as a top level tab so that we don't accidentally get the initial
  // load prioritization.
  let backgroundTab = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    coopPage
  );
  let backgroundTabChildID = browsingContextChildID(
    gBrowser.selectedBrowser.browsingContext
  );

  Assert.equal(
    gTabPriorityWatcher.currentPriority(backgroundTabChildID),
    PROCESS_PRIORITY_FOREGROUND,
    "Loading a new tab should make it prioritized"
  );

  await BrowserTestUtils.withNewTab(
    "https://example.org/browser/dom/ipc/tests/file_cross_frame.html",
    async browser => {
      Assert.equal(
        gTabPriorityWatcher.currentPriority(backgroundTabChildID),
        PROCESS_PRIORITY_BACKGROUND,
        "Switching to a new tab should deprioritize the old one"
      );

      let dotOrgChildID = browsingContextChildID(browser.browsingContext);

      // Do a cross-group navigation.
      BrowserTestUtils.loadURI(browser, coopPage);
      await BrowserTestUtils.browserLoaded(browser);

      let coopChildID = browsingContextChildID(browser.browsingContext);
      let coopPriority = gTabPriorityWatcher.currentPriority(coopChildID);
      let dotOrgPriority = gTabPriorityWatcher.currentPriority(dotOrgChildID);

      Assert.equal(
        backgroundTabChildID,
        coopChildID,
        "The same site should get loaded into the same process"
      );
      Assert.notEqual(
        dotOrgChildID,
        coopChildID,
        "Navigation should have switched processes"
      );
      Assert.equal(
        dotOrgPriority,
        PROCESS_PRIORITY_BACKGROUND,
        "The old page process should have been deprioritized"
      );
      Assert.equal(
        coopPriority,
        PROCESS_PRIORITY_FOREGROUND,
        "The new page process should be prioritized"
      );
    }
  );

  await BrowserTestUtils.removeTab(backgroundTab);
});

/**
 * Test that if a tab with video goes into the background,
 * it has its process priority lowered to
 * PROCESS_PRIORITY_BACKGROUND_PERCEIVABLE if it has no audio,
 * and that it has its priority remain at
 * PROCESS_PRIORITY_FOREGROUND if it does have audio.
 */
add_task(async function test_video_background_tab() {
  let originalTab = gBrowser.selectedTab;

  await BrowserTestUtils.withNewTab("https://example.com", async browser => {
    // Let's load up a video in the tab, but mute it, so that this tab should
    // reach PROCESS_PRIORITY_BACKGROUND_PERCEIVABLE.
    await SpecialPowers.spawn(browser, [], async () => {
      let video = content.document.createElement("video");
      video.src = "https://example.net/browser/dom/ipc/tests/short.mp4";
      video.muted = true;
      content.document.body.appendChild(video);
      // We'll loop the video to avoid it ending before the test is done.
      video.loop = true;
      await video.play();
    });

    let tab = gBrowser.getTabForBrowser(browser);

    // The tab with the muted video should reach
    // PROCESS_PRIORITY_BACKGROUND_PERCEIVABLE when backgrounded.
    await assertPriorityChangeOnBackground({
      fromTab: tab,
      toTab: originalTab,
      fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND_PERCEIVABLE,
    });

    // Now switch back. The initial blank tab should reach
    // PROCESS_PRIORITY_BACKGROUND when backgrounded.
    await assertPriorityChangeOnBackground({
      fromTab: originalTab,
      toTab: tab,
      fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
    });

    // Let's unmute the video now.
    await SpecialPowers.spawn(browser, [], async () => {
      let video = content.document.querySelector("video");
      video.muted = false;
    });

    // The tab with the unmuted video should stay at
    // PROCESS_PRIORITY_FOREGROUND when backgrounded.
    await assertPriorityChangeOnBackground({
      fromTab: tab,
      toTab: originalTab,
      fromTabExpectedPriority: PROCESS_PRIORITY_FOREGROUND,
    });

    // Now switch back. The initial blank tab should reach
    // PROCESS_PRIORITY_BACKGROUND when backgrounded.
    await assertPriorityChangeOnBackground({
      fromTab: originalTab,
      toTab: tab,
      fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
    });
  });
});

/**
 * Test that if a tab with a playing <audio> element goes into
 * the background, the process priority does not change, unless
 * that audio is muted (in which case, it reaches
 * PROCESS_PRIORITY_BACKGROUND).
 */
add_task(async function test_audio_background_tab() {
  let originalTab = gBrowser.selectedTab;

  await BrowserTestUtils.withNewTab("https://example.com", async browser => {
    // Let's load up some audio in the tab, but mute it, so that this tab should
    // reach PROCESS_PRIORITY_BACKGROUND.
    await SpecialPowers.spawn(browser, [], async () => {
      let audio = content.document.createElement("audio");
      audio.src = "https://example.net/browser/dom/ipc/tests/owl.mp3";
      audio.muted = true;
      content.document.body.appendChild(audio);
      // We'll loop the audio to avoid it ending before the test is done.
      audio.loop = true;
      await audio.play();
    });

    let tab = gBrowser.getTabForBrowser(browser);

    // The tab with the muted audio should reach
    // PROCESS_PRIORITY_BACKGROUND when backgrounded.
    await assertPriorityChangeOnBackground({
      fromTab: tab,
      toTab: originalTab,
      fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
    });

    // Now switch back. The initial blank tab should reach
    // PROCESS_PRIORITY_BACKGROUND when backgrounded.
    await assertPriorityChangeOnBackground({
      fromTab: originalTab,
      toTab: tab,
      fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
    });

    // Now unmute the audio. Unfortuntely, there's a bit of a race here,
    // since the wakelock on the audio element is released and then
    // re-acquired if the audio reaches its end and loops around. This
    // will cause an unexpected priority change on the content process.
    //
    // To avoid this race, we'll seek the audio back to the beginning,
    // and lower its playback rate to the minimum to increase the
    // likelihood that the check completes before the audio loops around.
    await SpecialPowers.spawn(browser, [], async () => {
      let audio = content.document.querySelector("audio");
      let seeked = ContentTaskUtils.waitForEvent(audio, "seeked");
      audio.muted = false;
      // 0.25 is the minimum playback rate that still keeps the audio audible.
      audio.playbackRate = 0.25;
      audio.currentTime = 0;
      await seeked;
    });

    // The tab with the unmuted audio should stay at
    // PROCESS_PRIORITY_FOREGROUND when backgrounded.
    await assertPriorityChangeOnBackground({
      fromTab: tab,
      toTab: originalTab,
      fromTabExpectedPriority: PROCESS_PRIORITY_FOREGROUND,
    });

    // Now switch back. The initial blank tab should reach
    // PROCESS_PRIORITY_BACKGROUND when backgrounded.
    await assertPriorityChangeOnBackground({
      fromTab: originalTab,
      toTab: tab,
      fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
    });
  });
});

/**
 * Test that if a tab with a WebAudio playing goes into the background,
 * the process priority does not change, unless that WebAudio context is
 * suspended.
 */
add_task(async function test_web_audio_background_tab() {
  let originalTab = gBrowser.selectedTab;

  await BrowserTestUtils.withNewTab("https://example.com", async browser => {
    // Let's synthesize a basic square wave as WebAudio.
    await SpecialPowers.spawn(browser, [], async () => {
      let audioCtx = new content.AudioContext();
      let oscillator = audioCtx.createOscillator();
      oscillator.type = "square";
      oscillator.frequency.setValueAtTime(440, audioCtx.currentTime);
      oscillator.connect(audioCtx.destination);
      oscillator.start();
      while (audioCtx.state != "running") {
        info(`wait until AudioContext starts running`);
        await new Promise(r => (audioCtx.onstatechange = r));
      }
      // we'll stash the AudioContext away so that it's easier to access
      // in the next SpecialPowers.spawn.
      content.audioCtx = audioCtx;
    });

    let tab = gBrowser.getTabForBrowser(browser);

    // The tab with the WebAudio should stay at
    // PROCESS_PRIORITY_FOREGROUND when backgrounded.
    await assertPriorityChangeOnBackground({
      fromTab: tab,
      toTab: originalTab,
      fromTabExpectedPriority: PROCESS_PRIORITY_FOREGROUND,
    });

    // Now switch back. The initial blank tab should reach
    // PROCESS_PRIORITY_BACKGROUND when backgrounded.
    await assertPriorityChangeOnBackground({
      fromTab: originalTab,
      toTab: tab,
      fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
    });

    // Now suspend the WebAudio. This will cause it to stop
    // playing.
    await SpecialPowers.spawn(browser, [], async () => {
      content.audioCtx.suspend();
    });

    // The tab with the suspended WebAudio should reach
    // PROCESS_PRIORITY_BACKGROUND when backgrounded.
    await assertPriorityChangeOnBackground({
      fromTab: tab,
      toTab: originalTab,
      fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
    });

    // Now switch back. The initial blank tab should reach
    // PROCESS_PRIORITY_BACKGROUND when backgrounded.
    await assertPriorityChangeOnBackground({
      fromTab: originalTab,
      toTab: tab,
      fromTabExpectedPriority: PROCESS_PRIORITY_BACKGROUND,
    });
  });
});

/**
 * Test that foreground tab's process priority isn't changed when going back to
 * a bfcached session history entry.
 */
add_task(async function test_audio_background_tab() {
  let page1 = "https://example.com";
  let page2 = page1 + "/?2";

  await BrowserTestUtils.withNewTab(page1, async browser => {
    let childID = browsingContextChildID(browser.browsingContext);
    Assert.equal(
      gTabPriorityWatcher.currentPriority(childID),
      PROCESS_PRIORITY_FOREGROUND,
      "Loading a new tab should make it prioritized."
    );
    let loaded = BrowserTestUtils.browserLoaded(browser, false, page2);
    BrowserTestUtils.loadURI(browser, page2);
    await loaded;

    childID = browsingContextChildID(browser.browsingContext);
    Assert.equal(
      gTabPriorityWatcher.currentPriority(childID),
      PROCESS_PRIORITY_FOREGROUND,
      "Loading a new page should keep the tab prioritized."
    );

    let pageShowPromise = BrowserTestUtils.waitForContentEvent(
      browser,
      "pageshow"
    );
    browser.goBack();
    await pageShowPromise;

    childID = browsingContextChildID(browser.browsingContext);
    Assert.equal(
      gTabPriorityWatcher.currentPriority(childID),
      PROCESS_PRIORITY_FOREGROUND,
      "Loading a page from the bfcache should keep the tab prioritized."
    );
  });
});