Bug 1852622 - Track a lastSetActive property on each tab and use that when sorting open tabs for recency in firefox view. r=jsudiaman,Gijs,fxview-reviewers,tabbrowser-reviewers,mak,sclements

Differential Revision: https://phabricator.services.mozilla.com/D189444
This commit is contained in:
Sam Foster 2023-10-05 23:44:57 +00:00
parent e3a5f85385
commit 6e6ac6197e
7 changed files with 403 additions and 2 deletions

View file

@ -259,6 +259,21 @@
return this._lastAccessed == Infinity ? Date.now() : this._lastAccessed; return this._lastAccessed == Infinity ? Date.now() : this._lastAccessed;
} }
get lastSeenActive() {
const isForegroundWindow =
this.ownerGlobal ==
BrowserWindowTracker.getTopWindow({ allowPopups: true });
// the timestamp for the selected tab in the active window is always now
if (isForegroundWindow && this.selected) {
return Date.now();
}
if (this._lastSeenActive) {
return this._lastSeenActive;
}
// Use the application start time as the fallback value
return Services.startup.getStartupInfo().start.getTime();
}
get _overPlayingIcon() { get _overPlayingIcon() {
return this.overlayIcon?.matches(":hover"); return this.overlayIcon?.matches(":hover");
} }
@ -291,6 +306,10 @@
this._lastAccessed = this.selected ? Infinity : aDate || Date.now(); this._lastAccessed = this.selected ? Infinity : aDate || Date.now();
} }
updateLastSeenActive() {
this._lastSeenActive = Date.now();
}
updateLastUnloadedByTabUnloader() { updateLastUnloadedByTabUnloader() {
this._lastUnloaded = Date.now(); this._lastUnloaded = Date.now();
Services.telemetry.scalarAdd("browser.engagement.tab_unload_count", 1); Services.telemetry.scalarAdd("browser.engagement.tab_unload_count", 1);

View file

@ -111,6 +111,8 @@
Services.els.addSystemEventListener(document, "keypress", this, false); Services.els.addSystemEventListener(document, "keypress", this, false);
document.addEventListener("visibilitychange", this); document.addEventListener("visibilitychange", this);
window.addEventListener("framefocusrequested", this); window.addEventListener("framefocusrequested", this);
window.addEventListener("activate", this);
window.addEventListener("deactivate", this);
this.tabContainer.init(); this.tabContainer.init();
this._setupInitialBrowserAndTab(); this._setupInitialBrowserAndTab();
@ -1201,6 +1203,11 @@
newTab.recordTimeFromUnloadToReload(); newTab.recordTimeFromUnloadToReload();
newTab.updateLastAccessed(); newTab.updateLastAccessed();
oldTab.updateLastAccessed(); oldTab.updateLastAccessed();
// if this is the foreground window, update the last-seen timestamps.
if (this.ownerGlobal == BrowserWindowTracker.getTopWindow()) {
newTab.updateLastSeenActive();
oldTab.updateLastSeenActive();
}
let oldFindBar = oldTab._findBar; let oldFindBar = oldTab._findBar;
if ( if (
@ -5762,6 +5769,11 @@
this.selectedBrowser.docShellIsActive = !inactive; this.selectedBrowser.docShellIsActive = !inactive;
} }
break; break;
case "activate":
// Intentional fallthrough
case "deactivate":
this.selectedTab.updateLastSeenActive();
break;
} }
}, },
@ -5875,6 +5887,8 @@
} }
document.removeEventListener("visibilitychange", this); document.removeEventListener("visibilitychange", this);
window.removeEventListener("framefocusrequested", this); window.removeEventListener("framefocusrequested", this);
window.removeEventListener("activate", this);
window.removeEventListener("deactivate", this);
if (gMultiProcessBrowser) { if (gMultiProcessBrowser) {
if (this._switcher) { if (this._switcher) {

View file

@ -27,6 +27,7 @@ ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => {
}); });
const TOPIC_DEVICELIST_UPDATED = "fxaccounts:devicelist_updated"; const TOPIC_DEVICELIST_UPDATED = "fxaccounts:devicelist_updated";
const TOPIC_CURRENT_BROWSER_CHANGED = "net:current-browser-id";
/** /**
* A collection of open tabs grouped by window. * A collection of open tabs grouped by window.
@ -65,6 +66,9 @@ class OpenTabsInView extends ViewPage {
tabContainer.addEventListener("TabOpen", this); tabContainer.addEventListener("TabOpen", this);
tabContainer.addEventListener("TabPinned", this); tabContainer.addEventListener("TabPinned", this);
tabContainer.addEventListener("TabUnpinned", this); tabContainer.addEventListener("TabUnpinned", this);
// BrowserWindowWatcher doesnt always notify "net:current-browser-id" when
// restoring a window, so we need to listen for "activate" events here as well.
win.addEventListener("activate", this);
this._updateOpenTabsList(); this._updateOpenTabsList();
} }
}, },
@ -77,6 +81,7 @@ class OpenTabsInView extends ViewPage {
tabContainer.removeEventListener("TabOpen", this); tabContainer.removeEventListener("TabOpen", this);
tabContainer.removeEventListener("TabPinned", this); tabContainer.removeEventListener("TabPinned", this);
tabContainer.removeEventListener("TabUnpinned", this); tabContainer.removeEventListener("TabUnpinned", this);
win.removeEventListener("activate", this);
this._updateOpenTabsList(); this._updateOpenTabsList();
} }
} }
@ -98,6 +103,10 @@ class OpenTabsInView extends ViewPage {
if (!this.observerAdded) { if (!this.observerAdded) {
Services.obs.addObserver(this.boundObserve, lazy.UIState.ON_UPDATE); Services.obs.addObserver(this.boundObserve, lazy.UIState.ON_UPDATE);
Services.obs.addObserver(this.boundObserve, TOPIC_DEVICELIST_UPDATED); Services.obs.addObserver(this.boundObserve, TOPIC_DEVICELIST_UPDATED);
Services.obs.addObserver(
this.boundObserve,
TOPIC_CURRENT_BROWSER_CHANGED
);
this.observerAdded = true; this.observerAdded = true;
} }
} }
@ -106,6 +115,10 @@ class OpenTabsInView extends ViewPage {
if (this.observerAdded) { if (this.observerAdded) {
Services.obs.removeObserver(this.boundObserve, lazy.UIState.ON_UPDATE); Services.obs.removeObserver(this.boundObserve, lazy.UIState.ON_UPDATE);
Services.obs.removeObserver(this.boundObserve, TOPIC_DEVICELIST_UPDATED); Services.obs.removeObserver(this.boundObserve, TOPIC_DEVICELIST_UPDATED);
Services.obs.removeObserver(
this.boundObserve,
TOPIC_CURRENT_BROWSER_CHANGED
);
this.observerAdded = false; this.observerAdded = false;
} }
} }
@ -123,6 +136,10 @@ class OpenTabsInView extends ViewPage {
if (deviceListUpdated) { if (deviceListUpdated) {
this.devices = this.currentWindow.gSync.getSendTabTargets(); this.devices = this.currentWindow.gSync.getSendTabTargets();
} }
break;
case TOPIC_CURRENT_BROWSER_CHANGED:
this.requestUpdate();
break;
} }
} }
@ -220,7 +237,17 @@ class OpenTabsInView extends ViewPage {
getRecentBrowsingTemplate() { getRecentBrowsingTemplate() {
const tabs = Array.from(this.windows.values()) const tabs = Array.from(this.windows.values())
.flat() .flat()
.sort((a, b) => b.lastAccessed - a.lastAccessed); .sort((a, b) => {
let dt = b.lastSeenActive - a.lastSeenActive;
if (dt) {
return dt;
}
// try to break a deadlock by sorting the selected tab higher
if (!(a.selected || b.selected)) {
return 0;
}
return a.selected ? -1 : 1;
});
return html`<view-opentabs-card return html`<view-opentabs-card
.tabs=${tabs} .tabs=${tabs}
.recentBrowsing=${true} .recentBrowsing=${true}

View file

@ -56,7 +56,11 @@ async function openFirefoxViewTab(win) {
const alreadyLoaded = const alreadyLoaded =
fxViewTab?.linkedBrowser?.currentURI.spec.split("#")[0] == fxViewTab?.linkedBrowser?.currentURI.spec.split("#")[0] ==
getFirefoxViewURL(); getFirefoxViewURL();
const enteredPromise = TestUtils.topicObserved("firefoxview-entered"); const enteredPromise =
alreadyLoaded &&
fxViewTab.linkedBrowser.contentDocument.visibilityState == "visible"
? Promise.resolve()
: TestUtils.topicObserved("firefoxview-entered");
await BrowserTestUtils.synthesizeMouseAtCenter( await BrowserTestUtils.synthesizeMouseAtCenter(
"#firefox-view-button", "#firefox-view-button",
{ type: "mousedown" }, { type: "mousedown" },

View file

@ -9,6 +9,9 @@ support-files = [ "../head.js"]
["browser_opentabs_firefoxview_next.js"] ["browser_opentabs_firefoxview_next.js"]
["browser_opentabs_recency_next.js"]
skip-if = ["os == 'mac' && verify"] # macos times out, see bug 1857293
["browser_recentlyclosed_firefoxview_next.js"] ["browser_recentlyclosed_firefoxview_next.js"]
["browser_syncedtabs_errors_firefoxview_next.js"] ["browser_syncedtabs_errors_firefoxview_next.js"]

View file

@ -0,0 +1,333 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* import-globals-from ../head.js */
const tabURL1 = "data:,Tab1";
const tabURL2 = "data:,Tab2";
const tabURL3 = "data:,Tab3";
const tabURL4 = "data:,Tab4";
let gInitialTab;
let gInitialTabURL;
add_setup(function () {
gInitialTab = gBrowser.selectedTab;
gInitialTabURL = tabUrl(gInitialTab);
});
function tabUrl(tab) {
return tab.linkedBrowser.currentURI?.spec;
}
async function minimizeWindow(win) {
let promiseSizeModeChange = BrowserTestUtils.waitForEvent(
win,
"sizemodechange"
);
win.minimize();
await promiseSizeModeChange;
ok(
!win.gBrowser.selectedTab.linkedBrowser.docShellIsActive,
"Docshell should be Inactive"
);
ok(win.document.hidden, "Top level window should be hidden");
}
async function restoreWindow(win) {
ok(win.document.hidden, "Top level window should be hidden");
let promiseSizeModeChange = BrowserTestUtils.waitForEvent(
win,
"sizemodechange"
);
info("Calling window.restore");
win.restore();
// From browser/base/content/test/general/browser_minimize.js:
// On Ubuntu `window.restore` doesn't seem to work, use a timer to make the
// test fail faster and more cleanly than with a test timeout.
info("Waiting for sizemodechange event");
let timer;
await Promise.race([
promiseSizeModeChange,
new Promise((resolve, reject) => {
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
timer = setTimeout(() => {
reject("timed out waiting for sizemodechange event");
}, 5000);
}),
]);
clearTimeout(timer);
info(
"Waiting occlusionstatechange if win.isFullyOccluded: " +
win.isFullyOccluded
);
// From browser/base/content/test/general/browser_minimize.js:
// The sizemodechange event can sometimes be fired before the
// occlusionstatechange event, especially in chaos mode.
if (win.isFullyOccluded) {
await BrowserTestUtils.waitForEvent(win, "occlusionstatechange");
}
ok(
win.gBrowser.selectedTab.linkedBrowser.docShellIsActive,
"Docshell should be active again"
);
ok(!win.document.hidden, "Top level window should be visible");
}
async function prepareOpenTabs(urls, win = window) {
const reusableTabURLs = ["about:newtab", "about:blank"];
const gBrowser = win.gBrowser;
for (let url of urls) {
if (
gBrowser.visibleTabs.length == 1 &&
reusableTabURLs.includes(gBrowser.selectedBrowser.currentURI.spec)
) {
// we'll load into this tab rather than opening a new one
info(
`Loading ${url} into blank tab: ${gBrowser.selectedBrowser.currentURI.spec}`
);
BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, url);
await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, null, url);
} else {
info(`Loading ${url} into new tab`);
await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
}
await new Promise(res => win.requestAnimationFrame(res));
}
Assert.equal(
gBrowser.visibleTabs.length,
urls.length,
`Prepared ${urls.length} tabs as expected`
);
Assert.equal(
tabUrl(gBrowser.selectedTab),
urls[urls.length - 1],
"The selectedTab is the last of the URLs given as expected"
);
}
async function cleanup(...windowsToClose) {
await Promise.all(
windowsToClose.map(win => BrowserTestUtils.closeWindow(win))
);
while (gBrowser.visibleTabs.length > 1) {
await SessionStoreTestUtils.closeTab(gBrowser.tabs.at(-1));
}
if (gBrowser.selectedBrowser.currentURI.spec !== gInitialTabURL) {
BrowserTestUtils.startLoadingURIString(
gBrowser.selectedBrowser,
gInitialTabURL
);
await BrowserTestUtils.browserLoaded(
gBrowser.selectedBrowser,
null,
gInitialTabURL
);
}
}
async function checkTabList(document, expected) {
const tabsView = document.querySelector("view-opentabs");
const openTabsCard = tabsView.shadowRoot.querySelector("view-opentabs-card");
await openTabsCard.updateCompleted;
const tabList = openTabsCard.shadowRoot.querySelector("fxview-tab-list");
Assert.ok(tabList, "Found the tab list element");
let actual = Array.from(tabList.rowEls).map(row => row.url);
Assert.deepEqual(
actual,
expected,
"Tab list has items with URLs in the expected order"
);
}
add_task(async function test_single_window_tabs() {
await prepareOpenTabs([tabURL1, tabURL2]);
await openFirefoxViewTab(window).then(async viewTab => {
const browser = viewTab.linkedBrowser;
await checkTabList(browser.contentDocument, [tabURL2, tabURL1]);
});
// switch to the first tab
await BrowserTestUtils.switchTab(gBrowser, gBrowser.visibleTabs[0]);
// and check the results in the open tabs section of Recent Browsing
await openFirefoxViewTab(window).then(async viewTab => {
const browser = viewTab.linkedBrowser;
await checkTabList(browser.contentDocument, [tabURL1, tabURL2]);
});
await cleanup();
});
add_task(async function test_multiple_window_tabs() {
const fxViewURL = getFirefoxViewURL();
const win1 = window;
await prepareOpenTabs([tabURL1, tabURL2]);
const win2 = await BrowserTestUtils.openNewBrowserWindow();
await prepareOpenTabs([tabURL3, tabURL4], win2);
// to avoid confusing the results by activating different windows,
// check fxview in the current window - which is win2
info("Switching to fxview tab in win2");
await openFirefoxViewTab(win2).then(async viewTab => {
const browser = viewTab.linkedBrowser;
await checkTabList(browser.contentDocument, [
tabURL4,
tabURL3,
tabURL2,
tabURL1,
]);
});
Assert.equal(
tabUrl(win2.gBrowser.selectedTab),
fxViewURL,
`The selected tab in window 2 is ${fxViewURL}`
);
info("Switching to first tab (tab3) in win2");
await BrowserTestUtils.switchTab(win2.gBrowser, win2.gBrowser.visibleTabs[0]);
Assert.equal(
tabUrl(win2.gBrowser.selectedTab),
tabURL3,
`The selected tab in window 2 is ${tabURL3}`
);
info("Opening fxview in win2 to confirm tab3 is most recent");
await openFirefoxViewTab(win2).then(async viewTab => {
const browser = viewTab.linkedBrowser;
info("Check result of selecting 1ist tab in window 2");
await checkTabList(browser.contentDocument, [
tabURL3,
tabURL4,
tabURL2,
tabURL1,
]);
});
info("Focusing win1, where tab2 should be selected");
await SimpleTest.promiseFocus(win1);
Assert.equal(
tabUrl(win1.gBrowser.selectedTab),
tabURL2,
`The selected tab in window 1 is ${tabURL2}`
);
info("Opening fxview in win1 to confirm tab2 is most recent");
await openFirefoxViewTab(win1).then(async viewTab => {
const browser = viewTab.linkedBrowser;
info(
"In fxview, check result of activating window 1, where tab 2 is selected"
);
await checkTabList(browser.contentDocument, [
tabURL2,
tabURL3,
tabURL4,
tabURL1,
]);
});
info("Switching to first visible tab (tab1) in win1");
await BrowserTestUtils.switchTab(win1.gBrowser, win1.gBrowser.visibleTabs[0]);
// check result in the fxview in the 1st window
info("Opening fxview in win1 to confirm tab1 is most recent");
await openFirefoxViewTab(win1).then(async viewTab => {
const browser = viewTab.linkedBrowser;
info("Check result of selecting 1st tab in win1");
await checkTabList(browser.contentDocument, [
tabURL1,
tabURL2,
tabURL3,
tabURL4,
]);
});
await cleanup(win2);
});
add_task(async function test_windows_activation() {
const win1 = window;
await prepareOpenTabs([tabURL1], win1);
let fxViewTab;
info("switch to firefox-view and leave it selected");
await openFirefoxViewTab(win1).then(tab => (fxViewTab = tab));
const win2 = await BrowserTestUtils.openNewBrowserWindow();
await prepareOpenTabs([tabURL2], win2);
const win3 = await BrowserTestUtils.openNewBrowserWindow();
await prepareOpenTabs([tabURL3], win3);
await SimpleTest.promiseFocus(win1);
const browser = fxViewTab.linkedBrowser;
await checkTabList(browser.contentDocument, [tabURL3, tabURL2, tabURL1]);
info("switch to win2 and confirm its selected tab becomes most recent");
await SimpleTest.promiseFocus(win2);
await checkTabList(browser.contentDocument, [tabURL2, tabURL3, tabURL1]);
await cleanup(win2, win3);
});
add_task(async function test_minimize_restore_windows() {
const win1 = window;
await prepareOpenTabs([tabURL1, tabURL2]);
const win2 = await BrowserTestUtils.openNewBrowserWindow();
await prepareOpenTabs([tabURL3, tabURL4], win2);
// to avoid confusing the results by activating different windows,
// check fxview in the current window - which is win2
info("Opening fxview in win2 to confirm tab4 is most recent");
await openFirefoxViewTab(win2).then(async viewTab => {
const browser = viewTab.linkedBrowser;
await checkTabList(browser.contentDocument, [
tabURL4,
tabURL3,
tabURL2,
tabURL1,
]);
});
//
info("Switching to the first tab (tab3) in 2nd window");
await BrowserTestUtils.switchTab(win2.gBrowser, win2.gBrowser.visibleTabs[0]);
// then minimize the window, focusing the 1st window
info("Minimizing win2, leaving tab 3 selected");
await minimizeWindow(win2);
info("Focusing win1, where tab2 is selected - making it most recent");
await SimpleTest.promiseFocus(win1);
Assert.equal(
tabUrl(win1.gBrowser.selectedTab),
tabURL2,
`The selected tab in window 1 is ${tabURL2}`
);
info("Opening fxview in win1 to confirm tab2 is most recent");
await openFirefoxViewTab(win1).then(async viewTab => {
const browser = viewTab.linkedBrowser;
await checkTabList(browser.contentDocument, [
tabURL2,
tabURL3,
tabURL4,
tabURL1,
]);
info(
"Restoring win2 and focusing it - which should make its selected tab most recent"
);
await restoreWindow(win2);
await SimpleTest.promiseFocus(win2);
info(
"Checking tab order in fxview in win1, to confirm tab3 is most recent"
);
await checkTabList(browser.contentDocument, [
tabURL3,
tabURL2,
tabURL4,
tabURL1,
]);
});
await cleanup(win2);
});

View file

@ -2,6 +2,7 @@
http://creativecommons.org/publicdomain/zero/1.0/ */ http://creativecommons.org/publicdomain/zero/1.0/ */
const { const {
getFirefoxViewURL,
withFirefoxView, withFirefoxView,
assertFirefoxViewTab, assertFirefoxViewTab,
assertFirefoxViewTabSelected, assertFirefoxViewTabSelected,