Bug 1873025 - Fix the displayed relative time for open tabs listing to match the recency sorting.r=jsudiaman,fxview-reviewers,tabbrowser-reviewers,dao

* Carry over lastAccessed times from tabs that haven't been seen/active this session.
* Use the lastSeenActive rather than lastAccessed timestamp for labeling open tabs in firefox view.

Differential Revision: https://phabricator.services.mozilla.com/D198892
This commit is contained in:
Sam Foster 2024-04-04 16:27:32 +00:00
parent 12228dff42
commit 4765d125ef
4 changed files with 281 additions and 3 deletions

View File

@ -259,6 +259,14 @@
return this._lastAccessed == Infinity ? Date.now() : this._lastAccessed;
}
/**
* Returns a timestamp which attempts to represent the last time the user saw this tab.
* If the tab has not been active in this session, any lastAccessed is used. We
* differentiate between selected and explicitly visible; a selected tab in a hidden
* window is last seen when that window and tab were last visible.
* We use the application start time as a fallback value when no other suitable value
* is available.
*/
get lastSeenActive() {
const isForegroundWindow =
this.ownerGlobal ==
@ -270,8 +278,16 @@
if (this._lastSeenActive) {
return this._lastSeenActive;
}
// Use the application start time as the fallback value
return Services.startup.getStartupInfo().start.getTime();
const appStartTime = Services.startup.getStartupInfo().start.getTime();
if (!this._lastAccessed || this._lastAccessed >= appStartTime) {
// When the tab was created this session but hasn't been seen by the user,
// default to the application start time.
return appStartTime;
}
// The tab was restored from a previous session but never seen.
// Use the lastAccessed as the best proxy for when the user might have seen it.
return this._lastAccessed;
}
get _overPlayingIcon() {

View File

@ -76,6 +76,8 @@ support-files = ["tab_that_closes.html"]
["browser_hiddentab_contextmenu.js"]
["browser_lastSeenActive.js"]
["browser_lazy_tab_browser_events.js"]
["browser_link_in_tab_title_and_url_prefilled_blank_page.js"]

View File

@ -0,0 +1,260 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
const { SessionStoreTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/SessionStoreTestUtils.sys.mjs"
);
const triggeringPrincipal_base64 = E10SUtils.SERIALIZED_SYSTEMPRINCIPAL;
SessionStoreTestUtils.init(this, window);
// take a state snapshot we can restore to after each test
const ORIG_STATE = SessionStore.getBrowserState();
const SECOND_MS = 1000;
const DAY_MS = 24 * 60 * 60 * 1000;
const today = new Date().getTime();
const yesterday = new Date(Date.now() - DAY_MS).getTime();
function tabEntry(url, lastAccessed) {
return {
entries: [{ url, triggeringPrincipal_base64 }],
lastAccessed,
};
}
/**
* Make the given window focused and active
*/
async function switchToWindow(win) {
info("switchToWindow, waiting for promiseFocus");
await SimpleTest.promiseFocus(win);
info("switchToWindow, waiting for correct Services.focus.activeWindow");
await BrowserTestUtils.waitForCondition(
() => Services.focus.activeWindow == win
);
}
async function changeSizeMode(win, mode) {
let promise = BrowserTestUtils.waitForEvent(win, "sizemodechange");
win[mode]();
await promise;
}
async function cleanup() {
await switchToWindow(window);
await SessionStoreTestUtils.promiseBrowserState(ORIG_STATE);
is(
BrowserWindowTracker.orderedWindows.length,
1,
"One window at the end of test cleanup"
);
info("cleanup, browser state restored");
}
function deltaTime(time, expectedTime) {
return Math.abs(expectedTime - time);
}
function getWindowUrl(win) {
return win.gBrowser.selectedBrowser?.currentURI?.spec;
}
function getWindowByTabUrl(url) {
return BrowserWindowTracker.orderedWindows.find(
win => getWindowUrl(win) == url
);
}
add_task(async function restoredTabs() {
const now = Date.now();
await SessionStoreTestUtils.promiseBrowserState({
windows: [
{
tabs: [
tabEntry("data:,Window0-Tab0", yesterday),
tabEntry("data:,Window0-Tab1", yesterday),
],
selected: 2,
},
],
});
is(
gBrowser.visibleTabs[1],
gBrowser.selectedTab,
"The selected tab is the 2nd visible tab"
);
is(
getWindowUrl(window),
"data:,Window0-Tab1",
"The expected tab is selected"
);
Assert.greaterOrEqual(
gBrowser.selectedTab.lastSeenActive,
now,
"The selected tab's lastSeenActive is now"
);
Assert.greaterOrEqual(
gBrowser.selectedTab.lastAccessed,
now,
"The selected tab's lastAccessed is now"
);
// tab restored from session but never seen or active
is(
gBrowser.visibleTabs[0].lastSeenActive,
yesterday,
"The restored tab's lastSeenActive is yesterday"
);
await cleanup();
});
add_task(async function switchingTabs() {
let now = Date.now();
let initialTab = gBrowser.selectedTab;
let applicationStart = Services.startup.getStartupInfo().start.getTime();
let openedTab = BrowserTestUtils.addTab(gBrowser, "data:,Tab1");
await BrowserTestUtils.browserLoaded(openedTab.linkedBrowser);
ok(!openedTab.selected, "The background tab we opened isn't selected");
Assert.greaterOrEqual(
initialTab.selected && initialTab.lastSeenActive,
now,
"The initial tab is selected and last seen now"
);
is(
openedTab.lastSeenActive,
applicationStart,
`Background tab got default lastSeenActive value, delta: ${deltaTime(
openedTab.lastSeenActive,
applicationStart
)}`
);
now = Date.now();
await BrowserTestUtils.switchTab(gBrowser, openedTab);
Assert.greaterOrEqual(
openedTab.lastSeenActive,
now,
"The tab we switched to is last seen now"
);
await cleanup();
});
add_task(async function switchingWindows() {
info("Restoring to the test browser state");
await SessionStoreTestUtils.promiseBrowserState({
windows: [
{
tabs: [tabEntry("data:,Window1-Tab0", yesterday)],
selected: 1,
sizemodeBeforeMinimized: "normal",
sizemode: "maximized",
zIndex: 1, // this will be the selected window
},
{
tabs: [tabEntry("data:,Window2-Tab0", yesterday)],
selected: 1,
sizemodeBeforeMinimized: "normal",
sizemode: "maximized",
zIndex: 2,
},
],
});
info("promiseBrowserState resolved");
info(
`BrowserWindowTracker.pendingWindows: ${BrowserWindowTracker.pendingWindows.size}`
);
await Promise.all(
Array.from(BrowserWindowTracker.pendingWindows.values()).map(
win => win.deferred.promise
)
);
info("All the pending windows are resolved");
info("Waiting for the firstBrowserLoaded in each of the windows");
await Promise.all(
BrowserWindowTracker.orderedWindows.map(win => {
const selectedUrl = getWindowUrl(win);
if (selectedUrl && selectedUrl !== "about:blank") {
return Promise.resolve();
}
return BrowserTestUtils.firstBrowserLoaded(win, false);
})
);
let expectedTabURLs = ["data:,Window1-Tab0", "data:,Window2-Tab0"];
let [win1, win2] = expectedTabURLs.map(url => getWindowByTabUrl(url));
if (BrowserWindowTracker.getTopWindow() !== win1) {
info("Switch to win1 which isn't active/top after restoring session");
// In theory the zIndex values in the session state should make win1 active
// But in practice that isn't always true. To ensure we're testing from a known state,
// ensure the first window is active before proceeding with the test
await switchToWindow(win1);
[win1, win2] = expectedTabURLs.map(url => getWindowByTabUrl(url));
}
let actualTabURLs = Array.from(BrowserWindowTracker.orderedWindows).map(win =>
getWindowUrl(win)
);
Assert.deepEqual(
actualTabURLs,
expectedTabURLs,
"Both windows are open with selected tab URLs in the expected order"
);
let lastSeenTimes = [win1, win2].map(
win => win.gBrowser.selectedTab.lastSeenActive
);
info("Focusing the other window");
await switchToWindow(win2);
// wait a little so the timestamps will differ and then check again
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(res => setTimeout(res, 100));
Assert.greater(
win2.gBrowser.selectedTab.lastSeenActive,
lastSeenTimes[1],
"The foreground window selected tab is last seen more recently than it was before being focused"
);
Assert.greater(
win2.gBrowser.selectedTab.lastSeenActive,
win1.gBrowser.selectedTab.lastSeenActive,
"The foreground window selected tab is last seen more recently than the backgrounded one"
);
lastSeenTimes = [win1, win2].map(
win => win.gBrowser.selectedTab.lastSeenActive
);
// minimize the foreground window and focus the other
let promiseSizeModeChange = BrowserTestUtils.waitForEvent(
win2,
"sizemodechange"
);
win2.minimize();
info("Waiting for the sizemodechange on minimized window");
await promiseSizeModeChange;
await switchToWindow(win1);
ok(
!win2.gBrowser.selectedTab.linkedBrowser.docShellIsActive,
"Docshell should be Inactive"
);
ok(win2.document.hidden, "Minimized windows's document should be hidden");
// wait a little so the timestamps will differ and then check again
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(res => setTimeout(res, 100));
Assert.greater(
win1.gBrowser.selectedTab.lastSeenActive,
win2.gBrowser.selectedTab.lastSeenActive,
"The foreground window selected tab is last seen more recently than the minimized one"
);
Assert.greater(
win1.gBrowser.selectedTab.lastSeenActive,
lastSeenTimes[0],
"The foreground window selected tab is last seen more recently than it was before being focused"
);
await cleanup();
});

View File

@ -1024,7 +1024,7 @@ function getTabListItems(tabs, isRecentBrowsing) {
? JSON.stringify({ tabTitle: tab.label })
: null,
tabElement: tab,
time: tab.lastAccessed,
time: tab.lastSeenActive,
title: tab.label,
url,
};