tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

head_browser_onbeforeunload.js (6552B)


      1 "use strict";
      2 
      3 const BASE_URL = "http://mochi.test:8888/browser/docshell/test/browser/";
      4 
      5 const TEST_PAGE = BASE_URL + "file_onbeforeunload_0.html";
      6 
      7 const { PromptTestUtils } = ChromeUtils.importESModule(
      8  "resource://testing-common/PromptTestUtils.sys.mjs"
      9 );
     10 
     11 async function withTabModalPromptCount(expected, task) {
     12  const DIALOG_TOPIC = "common-dialog-loaded";
     13 
     14  let count = 0;
     15  function observer() {
     16    count++;
     17  }
     18 
     19  Services.obs.addObserver(observer, DIALOG_TOPIC);
     20  try {
     21    return await task();
     22  } finally {
     23    Services.obs.removeObserver(observer, DIALOG_TOPIC);
     24    is(count, expected, "Should see expected number of tab modal prompts");
     25  }
     26 }
     27 
     28 function promiseAllowUnloadPrompt(browser, allowNavigation) {
     29  return PromptTestUtils.handleNextPrompt(
     30    browser,
     31    { modalType: Services.prompt.MODAL_TYPE_CONTENT, promptType: "confirmEx" },
     32    { buttonNumClick: allowNavigation ? 0 : 1 }
     33  );
     34 }
     35 
     36 // Maintain a pool of background tabs with our test document loaded so
     37 // we don't have to wait for a load prior to each test step (potentially
     38 // tearing down and recreating content processes in the process).
     39 const TabPool = {
     40  poolSize: 5,
     41 
     42  pendingCount: 0,
     43 
     44  readyTabs: [],
     45 
     46  readyPromise: null,
     47  resolveReadyPromise: null,
     48 
     49  spawnTabs() {
     50    while (this.pendingCount + this.readyTabs.length < this.poolSize) {
     51      this.pendingCount++;
     52      let tab = BrowserTestUtils.addTab(gBrowser, TEST_PAGE);
     53      BrowserTestUtils.browserLoaded(tab.linkedBrowser).then(() => {
     54        this.readyTabs.push(tab);
     55        this.pendingCount--;
     56 
     57        if (this.resolveReadyPromise) {
     58          this.readyPromise = null;
     59          this.resolveReadyPromise();
     60          this.resolveReadyPromise = null;
     61        }
     62 
     63        this.spawnTabs();
     64      });
     65    }
     66  },
     67 
     68  getReadyPromise() {
     69    if (!this.readyPromise) {
     70      this.readyPromise = new Promise(resolve => {
     71        this.resolveReadyPromise = resolve;
     72      });
     73    }
     74    return this.readyPromise;
     75  },
     76 
     77  async getTab() {
     78    while (!this.readyTabs.length) {
     79      this.spawnTabs();
     80      await this.getReadyPromise();
     81    }
     82 
     83    let tab = this.readyTabs.shift();
     84    this.spawnTabs();
     85 
     86    gBrowser.selectedTab = tab;
     87    return tab;
     88  },
     89 
     90  async cleanup() {
     91    this.poolSize = 0;
     92 
     93    while (this.pendingCount) {
     94      await this.getReadyPromise();
     95    }
     96 
     97    while (this.readyTabs.length) {
     98      await BrowserTestUtils.removeTab(this.readyTabs.shift());
     99    }
    100  },
    101 };
    102 
    103 const ACTIONS = {
    104  NONE: 0,
    105  LISTEN_AND_ALLOW: 1,
    106  LISTEN_AND_BLOCK: 2,
    107 };
    108 
    109 const ACTION_NAMES = new Map(Object.entries(ACTIONS).map(([k, v]) => [v, k]));
    110 
    111 function* generatePermutations(depth) {
    112  if (depth == 0) {
    113    yield [];
    114    return;
    115  }
    116  for (let subActions of generatePermutations(depth - 1)) {
    117    for (let action of Object.values(ACTIONS)) {
    118      yield [action, ...subActions];
    119    }
    120  }
    121 }
    122 
    123 const PERMUTATIONS = Array.from(generatePermutations(4));
    124 
    125 const FRAMES = [
    126  { process: 0 },
    127  { process: SpecialPowers.useRemoteSubframes ? 1 : 0 },
    128  { process: 0 },
    129  { process: SpecialPowers.useRemoteSubframes ? 1 : 0 },
    130 ];
    131 
    132 function addListener(bc, block) {
    133  return SpecialPowers.spawn(bc, [block], block => {
    134    return new Promise(resolve => {
    135      function onbeforeunload(event) {
    136        if (block) {
    137          event.preventDefault();
    138        }
    139        resolve({ event: "beforeunload" });
    140      }
    141      content.addEventListener("beforeunload", onbeforeunload, { once: true });
    142      content.unlisten = () => {
    143        content.removeEventListener("beforeunload", onbeforeunload);
    144      };
    145 
    146      content.addEventListener(
    147        "unload",
    148        () => {
    149          resolve({ event: "unload" });
    150        },
    151        { once: true }
    152      );
    153    });
    154  });
    155 }
    156 
    157 function descendants(bc) {
    158  if (bc) {
    159    return [bc, ...descendants(bc.children[0])];
    160  }
    161  return [];
    162 }
    163 
    164 async function addListeners(frames, actions, startIdx) {
    165  let process = startIdx >= 0 ? FRAMES[startIdx].process : -1;
    166 
    167  let roundTripPromises = [];
    168 
    169  let expectNestedEventLoop = false;
    170  let numBlockers = 0;
    171  let unloadPromises = [];
    172  let beforeUnloadPromises = [];
    173 
    174  for (let [i, frame] of frames.entries()) {
    175    let action = actions[i];
    176    if (action === ACTIONS.NONE) {
    177      continue;
    178    }
    179 
    180    let block = action === ACTIONS.LISTEN_AND_BLOCK;
    181    let promise = addListener(frame, block);
    182    if (startIdx <= i) {
    183      if (block || FRAMES[i].process !== process) {
    184        expectNestedEventLoop = true;
    185      }
    186      beforeUnloadPromises.push(promise);
    187      numBlockers += block;
    188    } else {
    189      unloadPromises.push(promise);
    190    }
    191 
    192    roundTripPromises.push(SpecialPowers.spawn(frame, [], () => {}));
    193  }
    194 
    195  // Wait for round trip messages to any processes with event listeners to
    196  // return so we're sure that all listeners are registered and their state
    197  // flags are propagated before we continue.
    198  await Promise.all(roundTripPromises);
    199 
    200  return {
    201    expectNestedEventLoop,
    202    expectPrompt: !!numBlockers,
    203    unloadPromises,
    204    beforeUnloadPromises,
    205  };
    206 }
    207 
    208 async function doTest(actions, startIdx, navigate) {
    209  let tab = await TabPool.getTab();
    210  let browser = tab.linkedBrowser;
    211 
    212  let frames = descendants(browser.browsingContext);
    213  let expected = await addListeners(frames, actions, startIdx);
    214 
    215  let awaitingPrompt = false;
    216  let promptPromise;
    217  if (expected.expectPrompt) {
    218    awaitingPrompt = true;
    219    promptPromise = promiseAllowUnloadPrompt(browser, false).then(() => {
    220      awaitingPrompt = false;
    221    });
    222  }
    223 
    224  let promptCount = expected.expectPrompt ? 1 : 0;
    225  await withTabModalPromptCount(promptCount, async () => {
    226    await navigate(tab, frames).then(result => {
    227      ok(
    228        !awaitingPrompt,
    229        "Navigation should not complete while we're still expecting a prompt"
    230      );
    231 
    232      is(
    233        result.eventLoopSpun,
    234        expected.expectNestedEventLoop,
    235        "Should have nested event loop?"
    236      );
    237    });
    238 
    239    for (let result of await Promise.all(expected.beforeUnloadPromises)) {
    240      is(
    241        result.event,
    242        "beforeunload",
    243        "Should have seen beforeunload event before unload"
    244      );
    245    }
    246    await promptPromise;
    247 
    248    await Promise.all(
    249      frames.map(frame =>
    250        SpecialPowers.spawn(frame, [], () => {
    251          if (content.unlisten) {
    252            content.unlisten();
    253          }
    254        }).catch(() => {})
    255      )
    256    );
    257 
    258    await BrowserTestUtils.removeTab(tab);
    259  });
    260 
    261  for (let result of await Promise.all(expected.unloadPromises)) {
    262    is(result.event, "unload", "Should have seen unload event");
    263  }
    264 }