const { NonPrivateTabs, getTabsTargetForWindow } = ChromeUtils.importESModule(
"resource:///modules/OpenTabs.sys.mjs"
);
let privateTabsChanges;
const tabURL1 = "data:text/html,
Tab1Tab1";
const tabURL2 = "data:text/html,Tab2Tab2";
const tabURL3 = "data:text/html,Tab3Tab3";
const tabURL4 = "data:text/html,Tab4Tab4";
const nonPrivateListener = sinon.stub();
const privateListener = sinon.stub();
function tabUrl(tab) {
return tab.linkedBrowser.currentURI?.spec;
}
function getWindowId(win) {
return win.windowGlobalChild.innerWindowId;
}
async function setup(tabChangeEventName) {
nonPrivateListener.resetHistory();
privateListener.resetHistory();
NonPrivateTabs.addEventListener(tabChangeEventName, nonPrivateListener);
await TestUtils.waitForTick();
is(
NonPrivateTabs.currentWindows.length,
1,
"NonPrivateTabs has 1 window a tick after adding the event listener"
);
info("Opening new windows");
let win0 = window,
win1 = await BrowserTestUtils.openNewBrowserWindow(),
privateWin = await BrowserTestUtils.openNewBrowserWindow({
private: true,
});
BrowserTestUtils.startLoadingURIString(
win1.gBrowser.selectedBrowser,
tabURL1
);
await BrowserTestUtils.browserLoaded(win1.gBrowser.selectedBrowser);
// load a tab with a title/label we can easily verify
BrowserTestUtils.startLoadingURIString(
privateWin.gBrowser.selectedBrowser,
tabURL2
);
await BrowserTestUtils.browserLoaded(privateWin.gBrowser.selectedBrowser);
is(
win1.gBrowser.selectedTab.label,
"Tab1",
"Check the tab label in the new non-private window"
);
is(
privateWin.gBrowser.selectedTab.label,
"Tab2",
"Check the tab label in the new private window"
);
privateTabsChanges = getTabsTargetForWindow(privateWin);
privateTabsChanges.addEventListener(tabChangeEventName, privateListener);
is(
privateTabsChanges,
getTabsTargetForWindow(privateWin),
"getTabsTargetForWindow reuses a single instance per exclusive window"
);
await TestUtils.waitForTick();
is(
NonPrivateTabs.currentWindows.length,
2,
"NonPrivateTabs has 2 windows once openNewBrowserWindow resolves"
);
is(
privateTabsChanges.currentWindows.length,
1,
"privateTabsChanges has 1 window once openNewBrowserWindow resolves"
);
await SimpleTest.promiseFocus(win0);
info("setup, win0 has id: " + getWindowId(win0));
info("setup, win1 has id: " + getWindowId(win1));
info("setup, privateWin has id: " + getWindowId(privateWin));
info("setup,waiting for both private and nonPrivateListener to be called");
await TestUtils.waitForCondition(() => {
return nonPrivateListener.called && privateListener.called;
});
nonPrivateListener.resetHistory();
privateListener.resetHistory();
const cleanup = async eventName => {
NonPrivateTabs.removeEventListener(eventName, nonPrivateListener);
privateTabsChanges.removeEventListener(eventName, privateListener);
await SimpleTest.promiseFocus(window);
await promiseAllButPrimaryWindowClosed();
};
return { windows: [win0, win1, privateWin], cleanup };
}
add_task(async function test_TabChanges() {
const { windows, cleanup } = await setup("TabChange");
const [win0, win1, privateWin] = windows;
let tabChangeRaised;
let changeEvent;
info(
"Verify that manipulating tabs in a non-private window dispatches events on the correct target"
);
for (let win of [win0, win1]) {
tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabChange"
);
let newTab = await BrowserTestUtils.openNewForegroundTab(
win.gBrowser,
tabURL1
);
changeEvent = await tabChangeRaised;
Assert.deepEqual(
changeEvent.detail.windowIds,
[getWindowId(win)],
"The event had the correct window id"
);
tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabChange"
);
const navigateUrl = "https://example.org/";
BrowserTestUtils.startLoadingURIString(newTab.linkedBrowser, navigateUrl);
await BrowserTestUtils.browserLoaded(
newTab.linkedBrowser,
null,
navigateUrl
);
// navigation in a tab changes the label which should produce a change event
changeEvent = await tabChangeRaised;
Assert.deepEqual(
changeEvent.detail.windowIds,
[getWindowId(win)],
"The event had the correct window id"
);
tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabChange"
);
BrowserTestUtils.removeTab(newTab);
// navigation in a tab changes the label which should produce a change event
changeEvent = await tabChangeRaised;
Assert.deepEqual(
changeEvent.detail.windowIds,
[getWindowId(win)],
"The event had the correct window id"
);
}
info(
"make sure a change to a private window doesnt dispatch on a nonprivate target"
);
nonPrivateListener.resetHistory();
privateListener.resetHistory();
tabChangeRaised = BrowserTestUtils.waitForEvent(
privateTabsChanges,
"TabChange"
);
BrowserTestUtils.addTab(privateWin.gBrowser, tabURL1);
changeEvent = await tabChangeRaised;
info(
`Check windowIds adding tab to private window: ${getWindowId(
privateWin
)}: ${JSON.stringify(changeEvent.detail.windowIds)}`
);
Assert.deepEqual(
changeEvent.detail.windowIds,
[getWindowId(privateWin)],
"The event had the correct window id"
);
await TestUtils.waitForTick();
Assert.ok(
nonPrivateListener.notCalled,
"A private tab change shouldnt raise a tab change event on the non-private target"
);
info("testTabChanges complete");
await cleanup("TabChange");
});
add_task(async function test_TabRecencyChange() {
const { windows, cleanup } = await setup("TabRecencyChange");
const [win0, win1, privateWin] = windows;
let tabChangeRaised;
let changeEvent;
let sortedTabs;
info("Open some tabs in the non-private windows");
for (let win of [win0, win1]) {
for (let url of [tabURL1, tabURL2]) {
let tab = BrowserTestUtils.addTab(win.gBrowser, url);
tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabChange"
);
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
await tabChangeRaised;
}
}
info("Verify switching tabs produces the expected event and result");
nonPrivateListener.resetHistory();
privateListener.resetHistory();
tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabRecencyChange"
);
BrowserTestUtils.switchTab(win0.gBrowser, win0.gBrowser.tabs.at(-1));
changeEvent = await tabChangeRaised;
Assert.deepEqual(
changeEvent.detail.windowIds,
[getWindowId(win0)],
"The recency change event had the correct window id"
);
Assert.ok(
nonPrivateListener.called,
"Sanity check that the non-private tabs listener was called"
);
Assert.ok(
privateListener.notCalled,
"The private tabs listener was not called"
);
sortedTabs = NonPrivateTabs.getRecentTabs();
is(
sortedTabs[0],
win0.gBrowser.selectedTab,
"The most-recent tab is the selected tab"
);
info("Verify switching window produces the expected event and result");
nonPrivateListener.resetHistory();
privateListener.resetHistory();
tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabRecencyChange"
);
await SimpleTest.promiseFocus(win1);
changeEvent = await tabChangeRaised;
Assert.deepEqual(
changeEvent.detail.windowIds,
[getWindowId(win1)],
"The recency change event had the correct window id"
);
Assert.ok(
nonPrivateListener.called,
"Sanity check that the non-private tabs listener was called"
);
Assert.ok(
privateListener.notCalled,
"The private tabs listener was not called"
);
sortedTabs = NonPrivateTabs.getRecentTabs();
is(
sortedTabs[0],
win1.gBrowser.selectedTab,
"The most-recent tab is the selected tab in the current window"
);
info("Verify behavior with private window changes");
nonPrivateListener.resetHistory();
privateListener.resetHistory();
tabChangeRaised = BrowserTestUtils.waitForEvent(
privateTabsChanges,
"TabRecencyChange"
);
await SimpleTest.promiseFocus(privateWin);
changeEvent = await tabChangeRaised;
Assert.deepEqual(
changeEvent.detail.windowIds,
[getWindowId(privateWin)],
"The recency change event had the correct window id"
);
Assert.ok(
nonPrivateListener.notCalled,
"The non-private listener got no recency-change events from the private window"
);
Assert.ok(
privateListener.called,
"Sanity check the private tabs listener was called"
);
sortedTabs = privateTabsChanges.getRecentTabs();
is(
sortedTabs[0],
privateWin.gBrowser.selectedTab,
"The most-recent tab is the selected tab in the current window"
);
sortedTabs = NonPrivateTabs.getRecentTabs();
is(
sortedTabs[0],
win1.gBrowser.selectedTab,
"The most-recent non-private tab is still the selected tab in the previous non-private window"
);
info("Verify adding a tab to a private window does the right thing");
nonPrivateListener.resetHistory();
privateListener.resetHistory();
tabChangeRaised = BrowserTestUtils.waitForEvent(
privateTabsChanges,
"TabRecencyChange"
);
await BrowserTestUtils.openNewForegroundTab(privateWin.gBrowser, tabURL3);
changeEvent = await tabChangeRaised;
Assert.deepEqual(
changeEvent.detail.windowIds,
[getWindowId(privateWin)],
"The event had the correct window id"
);
Assert.ok(
nonPrivateListener.notCalled,
"The non-private listener got no recency-change events from the private window"
);
sortedTabs = privateTabsChanges.getRecentTabs();
is(
tabUrl(sortedTabs[0]),
tabURL3,
"The most-recent tab is the tab we just opened in the private window"
);
nonPrivateListener.resetHistory();
privateListener.resetHistory();
tabChangeRaised = BrowserTestUtils.waitForEvent(
privateTabsChanges,
"TabRecencyChange"
);
BrowserTestUtils.switchTab(privateWin.gBrowser, privateWin.gBrowser.tabs[0]);
changeEvent = await tabChangeRaised;
Assert.deepEqual(
changeEvent.detail.windowIds,
[getWindowId(privateWin)],
"The event had the correct window id"
);
Assert.ok(
nonPrivateListener.notCalled,
"The non-private listener got no recency-change events from the private window"
);
sortedTabs = privateTabsChanges.getRecentTabs();
is(
sortedTabs[0],
privateWin.gBrowser.selectedTab,
"The most-recent tab is the selected tab in the private window"
);
info("Verify switching back to a non-private does the right thing");
nonPrivateListener.resetHistory();
privateListener.resetHistory();
tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabRecencyChange"
);
await SimpleTest.promiseFocus(win1);
await tabChangeRaised;
if (privateListener.called) {
info(`The private listener was called ${privateListener.callCount} times`);
}
Assert.ok(
privateListener.notCalled,
"The private listener got no recency-change events for the non-private window"
);
Assert.ok(
nonPrivateListener.called,
"Sanity-check the non-private listener got a recency-change event for the non-private window"
);
sortedTabs = privateTabsChanges.getRecentTabs();
is(
sortedTabs[0],
privateWin.gBrowser.selectedTab,
"The most-recent private tab is unchanged"
);
sortedTabs = NonPrivateTabs.getRecentTabs();
is(
sortedTabs[0],
win1.gBrowser.selectedTab,
"The most-recent non-private tab is the selected tab in the current window"
);
await cleanup("TabRecencyChange");
while (win0.gBrowser.tabs.length > 1) {
info(
"Removing last tab:" +
win0.gBrowser.tabs.at(-1).linkedBrowser.currentURI.spec
);
BrowserTestUtils.removeTab(win0.gBrowser.tabs.at(-1));
info("Removed, tabs.length:" + win0.gBrowser.tabs.length);
}
});
add_task(async function test_tabNavigations() {
const { windows, cleanup } = await setup("TabChange");
const [, win1, privateWin] = windows;
// also listen for TabRecencyChange events
const nonPrivateRecencyListener = sinon.stub();
const privateRecencyListener = sinon.stub();
privateTabsChanges.addEventListener(
"TabRecencyChange",
privateRecencyListener
);
NonPrivateTabs.addEventListener(
"TabRecencyChange",
nonPrivateRecencyListener
);
info(
`Verify navigating in tab generates TabChange & TabRecencyChange events`
);
let loaded = BrowserTestUtils.browserLoaded(win1.gBrowser.selectedBrowser);
win1.gBrowser.selectedBrowser.loadURI(Services.io.newURI(tabURL4), {
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
});
info("waiting for the load into win1 tab to complete");
await loaded;
info("waiting for listeners to be called");
await BrowserTestUtils.waitForCondition(() => {
return nonPrivateListener.called && nonPrivateRecencyListener.called;
});
ok(!privateListener.called, "The private TabChange listener was not called");
ok(
!privateRecencyListener.called,
"The private TabRecencyChange listener was not called"
);
nonPrivateListener.resetHistory();
privateListener.resetHistory();
nonPrivateRecencyListener.resetHistory();
privateRecencyListener.resetHistory();
// Now verify the same with a private window
info(
`Verify navigating in private tab generates TabChange & TabRecencyChange events`
);
ok(
!nonPrivateListener.called,
"The non-private TabChange listener is not yet called"
);
loaded = BrowserTestUtils.browserLoaded(privateWin.gBrowser.selectedBrowser);
privateWin.gBrowser.selectedBrowser.loadURI(
Services.io.newURI("about:robots"),
{
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
}
);
info("waiting for the load into privateWin tab to complete");
await loaded;
info("waiting for the privateListeners to be called");
await BrowserTestUtils.waitForCondition(() => {
return privateListener.called && privateRecencyListener.called;
});
ok(
!nonPrivateListener.called,
"The non-private TabChange listener was not called"
);
ok(
!nonPrivateRecencyListener.called,
"The non-private TabRecencyChange listener was not called"
);
// cleanup
privateTabsChanges.removeEventListener(
"TabRecencyChange",
privateRecencyListener
);
NonPrivateTabs.removeEventListener(
"TabRecencyChange",
nonPrivateRecencyListener
);
await cleanup();
});
add_task(async function test_tabsFromPrivateWindows() {
const { cleanup } = await setup("TabChange");
const private2Listener = sinon.stub();
const private2Win = await BrowserTestUtils.openNewBrowserWindow({
private: true,
waitForTabURL: "about:privatebrowsing",
});
const private2TabsChanges = getTabsTargetForWindow(private2Win);
private2TabsChanges.addEventListener("TabChange", private2Listener);
ok(
privateTabsChanges !== getTabsTargetForWindow(private2Win),
"getTabsTargetForWindow creates a distinct instance for a different private window"
);
await BrowserTestUtils.waitForCondition(() => private2Listener.called);
ok(
!privateListener.called,
"No TabChange event was raised by opening a different private window"
);
privateListener.resetHistory();
private2Listener.resetHistory();
BrowserTestUtils.addTab(private2Win.gBrowser, tabURL1);
await BrowserTestUtils.waitForCondition(() => private2Listener.called);
ok(
!privateListener.called,
"No TabChange event was raised by adding tab to a different private window"
);
is(
privateTabsChanges.getRecentTabs().length,
1,
"The recent tab count for the first private window tab target only reports the tabs for its associated windodw"
);
is(
private2TabsChanges.getRecentTabs().length,
2,
"The recent tab count for a 2nd private window tab target only reports the tabs for its associated windodw"
);
await cleanup("TabChange");
});