forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			264 lines
		
	
	
	
		
			6.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			264 lines
		
	
	
	
		
			6.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
"use strict";
 | 
						|
 | 
						|
const BASE_URL = "http://mochi.test:8888/browser/docshell/test/browser/";
 | 
						|
 | 
						|
const TEST_PAGE = BASE_URL + "file_onbeforeunload_0.html";
 | 
						|
 | 
						|
const { PromptTestUtils } = ChromeUtils.importESModule(
 | 
						|
  "resource://testing-common/PromptTestUtils.sys.mjs"
 | 
						|
);
 | 
						|
 | 
						|
async function withTabModalPromptCount(expected, task) {
 | 
						|
  const DIALOG_TOPIC = "common-dialog-loaded";
 | 
						|
 | 
						|
  let count = 0;
 | 
						|
  function observer() {
 | 
						|
    count++;
 | 
						|
  }
 | 
						|
 | 
						|
  Services.obs.addObserver(observer, DIALOG_TOPIC);
 | 
						|
  try {
 | 
						|
    return await task();
 | 
						|
  } finally {
 | 
						|
    Services.obs.removeObserver(observer, DIALOG_TOPIC);
 | 
						|
    is(count, expected, "Should see expected number of tab modal prompts");
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function promiseAllowUnloadPrompt(browser, allowNavigation) {
 | 
						|
  return PromptTestUtils.handleNextPrompt(
 | 
						|
    browser,
 | 
						|
    { modalType: Services.prompt.MODAL_TYPE_CONTENT, promptType: "confirmEx" },
 | 
						|
    { buttonNumClick: allowNavigation ? 0 : 1 }
 | 
						|
  );
 | 
						|
}
 | 
						|
 | 
						|
// Maintain a pool of background tabs with our test document loaded so
 | 
						|
// we don't have to wait for a load prior to each test step (potentially
 | 
						|
// tearing down and recreating content processes in the process).
 | 
						|
const TabPool = {
 | 
						|
  poolSize: 5,
 | 
						|
 | 
						|
  pendingCount: 0,
 | 
						|
 | 
						|
  readyTabs: [],
 | 
						|
 | 
						|
  readyPromise: null,
 | 
						|
  resolveReadyPromise: null,
 | 
						|
 | 
						|
  spawnTabs() {
 | 
						|
    while (this.pendingCount + this.readyTabs.length < this.poolSize) {
 | 
						|
      this.pendingCount++;
 | 
						|
      let tab = BrowserTestUtils.addTab(gBrowser, TEST_PAGE);
 | 
						|
      BrowserTestUtils.browserLoaded(tab.linkedBrowser).then(() => {
 | 
						|
        this.readyTabs.push(tab);
 | 
						|
        this.pendingCount--;
 | 
						|
 | 
						|
        if (this.resolveReadyPromise) {
 | 
						|
          this.readyPromise = null;
 | 
						|
          this.resolveReadyPromise();
 | 
						|
          this.resolveReadyPromise = null;
 | 
						|
        }
 | 
						|
 | 
						|
        this.spawnTabs();
 | 
						|
      });
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  getReadyPromise() {
 | 
						|
    if (!this.readyPromise) {
 | 
						|
      this.readyPromise = new Promise(resolve => {
 | 
						|
        this.resolveReadyPromise = resolve;
 | 
						|
      });
 | 
						|
    }
 | 
						|
    return this.readyPromise;
 | 
						|
  },
 | 
						|
 | 
						|
  async getTab() {
 | 
						|
    while (!this.readyTabs.length) {
 | 
						|
      this.spawnTabs();
 | 
						|
      await this.getReadyPromise();
 | 
						|
    }
 | 
						|
 | 
						|
    let tab = this.readyTabs.shift();
 | 
						|
    this.spawnTabs();
 | 
						|
 | 
						|
    gBrowser.selectedTab = tab;
 | 
						|
    return tab;
 | 
						|
  },
 | 
						|
 | 
						|
  async cleanup() {
 | 
						|
    this.poolSize = 0;
 | 
						|
 | 
						|
    while (this.pendingCount) {
 | 
						|
      await this.getReadyPromise();
 | 
						|
    }
 | 
						|
 | 
						|
    while (this.readyTabs.length) {
 | 
						|
      await BrowserTestUtils.removeTab(this.readyTabs.shift());
 | 
						|
    }
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
const ACTIONS = {
 | 
						|
  NONE: 0,
 | 
						|
  LISTEN_AND_ALLOW: 1,
 | 
						|
  LISTEN_AND_BLOCK: 2,
 | 
						|
};
 | 
						|
 | 
						|
const ACTION_NAMES = new Map(Object.entries(ACTIONS).map(([k, v]) => [v, k]));
 | 
						|
 | 
						|
function* generatePermutations(depth) {
 | 
						|
  if (depth == 0) {
 | 
						|
    yield [];
 | 
						|
    return;
 | 
						|
  }
 | 
						|
  for (let subActions of generatePermutations(depth - 1)) {
 | 
						|
    for (let action of Object.values(ACTIONS)) {
 | 
						|
      yield [action, ...subActions];
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
const PERMUTATIONS = Array.from(generatePermutations(4));
 | 
						|
 | 
						|
const FRAMES = [
 | 
						|
  { process: 0 },
 | 
						|
  { process: SpecialPowers.useRemoteSubframes ? 1 : 0 },
 | 
						|
  { process: 0 },
 | 
						|
  { process: SpecialPowers.useRemoteSubframes ? 1 : 0 },
 | 
						|
];
 | 
						|
 | 
						|
function addListener(bc, block) {
 | 
						|
  return SpecialPowers.spawn(bc, [block], block => {
 | 
						|
    return new Promise(resolve => {
 | 
						|
      function onbeforeunload(event) {
 | 
						|
        if (block) {
 | 
						|
          event.preventDefault();
 | 
						|
        }
 | 
						|
        resolve({ event: "beforeunload" });
 | 
						|
      }
 | 
						|
      content.addEventListener("beforeunload", onbeforeunload, { once: true });
 | 
						|
      content.unlisten = () => {
 | 
						|
        content.removeEventListener("beforeunload", onbeforeunload);
 | 
						|
      };
 | 
						|
 | 
						|
      content.addEventListener(
 | 
						|
        "unload",
 | 
						|
        () => {
 | 
						|
          resolve({ event: "unload" });
 | 
						|
        },
 | 
						|
        { once: true }
 | 
						|
      );
 | 
						|
    });
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
function descendants(bc) {
 | 
						|
  if (bc) {
 | 
						|
    return [bc, ...descendants(bc.children[0])];
 | 
						|
  }
 | 
						|
  return [];
 | 
						|
}
 | 
						|
 | 
						|
async function addListeners(frames, actions, startIdx) {
 | 
						|
  let process = startIdx >= 0 ? FRAMES[startIdx].process : -1;
 | 
						|
 | 
						|
  let roundTripPromises = [];
 | 
						|
 | 
						|
  let expectNestedEventLoop = false;
 | 
						|
  let numBlockers = 0;
 | 
						|
  let unloadPromises = [];
 | 
						|
  let beforeUnloadPromises = [];
 | 
						|
 | 
						|
  for (let [i, frame] of frames.entries()) {
 | 
						|
    let action = actions[i];
 | 
						|
    if (action === ACTIONS.NONE) {
 | 
						|
      continue;
 | 
						|
    }
 | 
						|
 | 
						|
    let block = action === ACTIONS.LISTEN_AND_BLOCK;
 | 
						|
    let promise = addListener(frame, block);
 | 
						|
    if (startIdx <= i) {
 | 
						|
      if (block || FRAMES[i].process !== process) {
 | 
						|
        expectNestedEventLoop = true;
 | 
						|
      }
 | 
						|
      beforeUnloadPromises.push(promise);
 | 
						|
      numBlockers += block;
 | 
						|
    } else {
 | 
						|
      unloadPromises.push(promise);
 | 
						|
    }
 | 
						|
 | 
						|
    roundTripPromises.push(SpecialPowers.spawn(frame, [], () => {}));
 | 
						|
  }
 | 
						|
 | 
						|
  // Wait for round trip messages to any processes with event listeners to
 | 
						|
  // return so we're sure that all listeners are registered and their state
 | 
						|
  // flags are propagated before we continue.
 | 
						|
  await Promise.all(roundTripPromises);
 | 
						|
 | 
						|
  return {
 | 
						|
    expectNestedEventLoop,
 | 
						|
    expectPrompt: !!numBlockers,
 | 
						|
    unloadPromises,
 | 
						|
    beforeUnloadPromises,
 | 
						|
  };
 | 
						|
}
 | 
						|
 | 
						|
async function doTest(actions, startIdx, navigate) {
 | 
						|
  let tab = await TabPool.getTab();
 | 
						|
  let browser = tab.linkedBrowser;
 | 
						|
 | 
						|
  let frames = descendants(browser.browsingContext);
 | 
						|
  let expected = await addListeners(frames, actions, startIdx);
 | 
						|
 | 
						|
  let awaitingPrompt = false;
 | 
						|
  let promptPromise;
 | 
						|
  if (expected.expectPrompt) {
 | 
						|
    awaitingPrompt = true;
 | 
						|
    promptPromise = promiseAllowUnloadPrompt(browser, false).then(() => {
 | 
						|
      awaitingPrompt = false;
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  let promptCount = expected.expectPrompt ? 1 : 0;
 | 
						|
  await withTabModalPromptCount(promptCount, async () => {
 | 
						|
    await navigate(tab, frames).then(result => {
 | 
						|
      ok(
 | 
						|
        !awaitingPrompt,
 | 
						|
        "Navigation should not complete while we're still expecting a prompt"
 | 
						|
      );
 | 
						|
 | 
						|
      is(
 | 
						|
        result.eventLoopSpun,
 | 
						|
        expected.expectNestedEventLoop,
 | 
						|
        "Should have nested event loop?"
 | 
						|
      );
 | 
						|
    });
 | 
						|
 | 
						|
    for (let result of await Promise.all(expected.beforeUnloadPromises)) {
 | 
						|
      is(
 | 
						|
        result.event,
 | 
						|
        "beforeunload",
 | 
						|
        "Should have seen beforeunload event before unload"
 | 
						|
      );
 | 
						|
    }
 | 
						|
    await promptPromise;
 | 
						|
 | 
						|
    await Promise.all(
 | 
						|
      frames.map(frame =>
 | 
						|
        SpecialPowers.spawn(frame, [], () => {
 | 
						|
          if (content.unlisten) {
 | 
						|
            content.unlisten();
 | 
						|
          }
 | 
						|
        }).catch(() => {})
 | 
						|
      )
 | 
						|
    );
 | 
						|
 | 
						|
    await BrowserTestUtils.removeTab(tab);
 | 
						|
  });
 | 
						|
 | 
						|
  for (let result of await Promise.all(expected.unloadPromises)) {
 | 
						|
    is(result.event, "unload", "Should have seen unload event");
 | 
						|
  }
 | 
						|
}
 |