mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-09 03:15:11 +00:00
Bug 1308058 - Implement sessions.getRecentlyClosed WebExtensions API, r=aswan
MozReview-Commit-ID: 7AKfMil3Dr4 --HG-- extra : rebase_source : 075f74835aff7f9937d3f456714c64b9a31168db
This commit is contained in:
parent
c9fdc270b6
commit
7083cbf0d9
44
browser/components/extensions/ext-sessions.js
Normal file
44
browser/components/extensions/ext-sessions.js
Normal file
@ -0,0 +1,44 @@
|
||||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "SessionStore",
|
||||
"resource:///modules/sessionstore/SessionStore.jsm");
|
||||
|
||||
function getRecentlyClosed(maxResults, extension) {
|
||||
let recentlyClosed = [];
|
||||
|
||||
// Get closed windows
|
||||
let closedWindowData = SessionStore.getClosedWindowData(false);
|
||||
for (let window of closedWindowData) {
|
||||
recentlyClosed.push({
|
||||
lastModified: window.closedAt,
|
||||
window: WindowManager.convertFromSessionStoreClosedData(window, extension)});
|
||||
}
|
||||
|
||||
// Get closed tabs
|
||||
for (let window of WindowListManager.browserWindows()) {
|
||||
let closedTabData = SessionStore.getClosedTabData(window, false);
|
||||
for (let tab of closedTabData) {
|
||||
recentlyClosed.push({
|
||||
lastModified: tab.closedAt,
|
||||
tab: TabManager.for(extension).convertFromSessionStoreClosedData(tab, window)});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort windows and tabs
|
||||
recentlyClosed.sort((a, b) => b.lastModified - a.lastModified);
|
||||
return recentlyClosed.slice(0, maxResults);
|
||||
}
|
||||
|
||||
extensions.registerSchemaAPI("sessions", "addon_parent", context => {
|
||||
let {extension} = context;
|
||||
return {
|
||||
sessions: {
|
||||
getRecentlyClosed: function(filter) {
|
||||
let maxResults = filter.maxResults == undefined ? this.MAX_SESSION_RESULTS : filter.maxResults;
|
||||
return Promise.resolve(getRecentlyClosed(maxResults, extension));
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
@ -661,6 +661,23 @@ ExtensionTabManager.prototype = {
|
||||
return result;
|
||||
},
|
||||
|
||||
// Converts tabs returned from SessionStore.getClosedTabData and
|
||||
// SessionStore.getClosedWindowData into API tab objects
|
||||
convertFromSessionStoreClosedData(tab, window) {
|
||||
let result = {
|
||||
sessionId: String(tab.closedId),
|
||||
index: tab.pos ? tab.pos : 0,
|
||||
windowId: WindowManager.getId(window),
|
||||
selected: false,
|
||||
highlighted: false,
|
||||
active: false,
|
||||
pinned: false,
|
||||
incognito: Boolean(tab.state && tab.state.isPrivate),
|
||||
};
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
getTabs(window) {
|
||||
return Array.from(window.gBrowser.tabs)
|
||||
.filter(tab => !tab.closing)
|
||||
@ -908,6 +925,19 @@ global.WindowManager = {
|
||||
return null;
|
||||
},
|
||||
|
||||
getState(window) {
|
||||
const STATES = {
|
||||
[window.STATE_MAXIMIZED]: "maximized",
|
||||
[window.STATE_MINIMIZED]: "minimized",
|
||||
[window.STATE_NORMAL]: "normal",
|
||||
};
|
||||
let state = STATES[window.windowState];
|
||||
if (window.fullScreen) {
|
||||
state = "fullscreen";
|
||||
}
|
||||
return state;
|
||||
},
|
||||
|
||||
setState(window, state) {
|
||||
if (state != "fullscreen" && window.fullScreen) {
|
||||
window.fullScreen = false;
|
||||
@ -948,16 +978,6 @@ global.WindowManager = {
|
||||
},
|
||||
|
||||
convert(extension, window, getInfo) {
|
||||
const STATES = {
|
||||
[window.STATE_MAXIMIZED]: "maximized",
|
||||
[window.STATE_MINIMIZED]: "minimized",
|
||||
[window.STATE_NORMAL]: "normal",
|
||||
};
|
||||
let state = STATES[window.windowState];
|
||||
if (window.fullScreen) {
|
||||
state = "fullscreen";
|
||||
}
|
||||
|
||||
let xulWindow = window.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDocShell)
|
||||
.treeOwner.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
@ -972,7 +992,7 @@ global.WindowManager = {
|
||||
height: window.outerHeight,
|
||||
incognito: PrivateBrowsingUtils.isWindowPrivate(window),
|
||||
type: this.windowType(window),
|
||||
state,
|
||||
state: this.getState(window),
|
||||
alwaysOnTop: xulWindow.zLevel >= Ci.nsIXULWindow.raisedZ,
|
||||
};
|
||||
|
||||
@ -982,6 +1002,28 @@ global.WindowManager = {
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
// Converts windows returned from SessionStore.getClosedWindowData
|
||||
// into API window objects
|
||||
convertFromSessionStoreClosedData(window, extension) {
|
||||
let result = {
|
||||
sessionId: String(window.closedId),
|
||||
focused: false,
|
||||
incognito: false,
|
||||
type: "normal", // this is always "normal" for a closed window
|
||||
state: this.getState(window),
|
||||
alwaysOnTop: false,
|
||||
};
|
||||
|
||||
if (window.tabs.length) {
|
||||
result.tabs = [];
|
||||
window.tabs.forEach((tab, index) => {
|
||||
result.tabs.push(TabManager.for(extension).convertFromSessionStoreClosedData(tab, window, index));
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
||||
|
||||
// Manages listeners for window opening and closing. A window is
|
||||
|
@ -6,6 +6,7 @@ category webextension-scripts contextMenus chrome://browser/content/ext-contextM
|
||||
category webextension-scripts desktop-runtime chrome://browser/content/ext-desktop-runtime.js
|
||||
category webextension-scripts history chrome://browser/content/ext-history.js
|
||||
category webextension-scripts pageAction chrome://browser/content/ext-pageAction.js
|
||||
category webextension-scripts sessions chrome://browser/content/ext-sessions.js
|
||||
category webextension-scripts tabs chrome://browser/content/ext-tabs.js
|
||||
category webextension-scripts utils chrome://browser/content/ext-utils.js
|
||||
category webextension-scripts windows chrome://browser/content/ext-windows.js
|
||||
@ -24,5 +25,6 @@ category webextension-schemas context_menus chrome://browser/content/schemas/con
|
||||
category webextension-schemas context_menus_internal chrome://browser/content/schemas/context_menus_internal.json
|
||||
category webextension-schemas history chrome://browser/content/schemas/history.json
|
||||
category webextension-schemas page_action chrome://browser/content/schemas/page_action.json
|
||||
category webextension-schemas sessions chrome://browser/content/schemas/sessions.json
|
||||
category webextension-schemas tabs chrome://browser/content/schemas/tabs.json
|
||||
category webextension-schemas windows chrome://browser/content/schemas/windows.json
|
||||
|
@ -19,6 +19,7 @@ browser.jar:
|
||||
content/browser/ext-desktop-runtime.js
|
||||
content/browser/ext-history.js
|
||||
content/browser/ext-pageAction.js
|
||||
content/browser/ext-sessions.js
|
||||
content/browser/ext-tabs.js
|
||||
content/browser/ext-utils.js
|
||||
content/browser/ext-windows.js
|
||||
|
@ -10,5 +10,6 @@ browser.jar:
|
||||
content/browser/schemas/context_menus_internal.json
|
||||
content/browser/schemas/history.json
|
||||
content/browser/schemas/page_action.json
|
||||
content/browser/schemas/sessions.json
|
||||
content/browser/schemas/tabs.json
|
||||
content/browser/schemas/windows.json
|
||||
|
147
browser/components/extensions/schemas/sessions.json
Normal file
147
browser/components/extensions/schemas/sessions.json
Normal file
@ -0,0 +1,147 @@
|
||||
// Copyright 2013 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
[
|
||||
{
|
||||
"namespace": "manifest",
|
||||
"types": [
|
||||
{
|
||||
"$extend": "Permission",
|
||||
"choices": [{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"sessions"
|
||||
]
|
||||
}]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"namespace": "sessions",
|
||||
"description": "Use the <code>chrome.sessions</code> API to query and restore tabs and windows from a browsing session.",
|
||||
"permissions": ["sessions"],
|
||||
"types": [
|
||||
{
|
||||
"id": "Filter",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"maxResults": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 25,
|
||||
"optional": true,
|
||||
"description": "The maximum number of entries to be fetched in the requested list. Omit this parameter to fetch the maximum number of entries ($(ref:sessions.MAX_SESSION_RESULTS))."
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Session",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"lastModified": {"type": "integer", "description": "The time when the window or tab was closed or modified, represented in milliseconds since the epoch."},
|
||||
"tab": {"$ref": "tabs.Tab", "optional": true, "description": "The $(ref:tabs.Tab), if this entry describes a tab. Either this or $(ref:sessions.Session.window) will be set."},
|
||||
"window": {"$ref": "windows.Window", "optional": true, "description": "The $(ref:windows.Window), if this entry describes a window. Either this or $(ref:sessions.Session.tab) will be set."}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Device",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"info": {"type": "string"},
|
||||
"deviceName": {"type": "string", "description": "The name of the foreign device."},
|
||||
"sessions": {"type": "array", "items": {"$ref": "Session"}, "description": "A list of open window sessions for the foreign device, sorted from most recently to least recently modified session."}
|
||||
}
|
||||
}
|
||||
],
|
||||
"functions": [
|
||||
{
|
||||
"name": "getRecentlyClosed",
|
||||
"type": "function",
|
||||
"description": "Gets the list of recently closed tabs and/or windows.",
|
||||
"async": "callback",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "Filter",
|
||||
"name": "filter",
|
||||
"optional": true,
|
||||
"default": {}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "callback",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "sessions", "type": "array", "items": { "$ref": "Session" }, "description": "The list of closed entries in reverse order that they were closed (the most recently closed tab or window will be at index <code>0</code>). The entries may contain either tabs or windows."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "getDevices",
|
||||
"unsupported": true,
|
||||
"type": "function",
|
||||
"description": "Retrieves all devices with synced sessions.",
|
||||
"async": "callback",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "Filter",
|
||||
"name": "filter",
|
||||
"optional": true
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "callback",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "devices", "type": "array", "items": { "$ref": "Device" }, "description": "The list of $(ref:sessions.Device) objects for each synced session, sorted in order from device with most recently modified session to device with least recently modified session. $(ref:tabs.Tab) objects are sorted by recency in the $(ref:windows.Window) of the $(ref:sessions.Session) objects."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "restore",
|
||||
"unsupported": true,
|
||||
"type": "function",
|
||||
"description": "Reopens a $(ref:windows.Window) or $(ref:tabs.Tab), with an optional callback to run when the entry has been restored.",
|
||||
"async": "callback",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "sessionId",
|
||||
"optional": true,
|
||||
"description": "The $(ref:windows.Window.sessionId), or $(ref:tabs.Tab.sessionId) to restore. If this parameter is not specified, the most recently closed session is restored."
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "callback",
|
||||
"optional": true,
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "Session",
|
||||
"name": "restoredSession",
|
||||
"description": "A $(ref:sessions.Session) containing the restored $(ref:windows.Window) or $(ref:tabs.Tab) object."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"name": "onChanged",
|
||||
"unsupported": true,
|
||||
"description": "Fired when recently closed tabs and/or windows are changed. This event does not monitor synced sessions changes.",
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"MAX_SESSION_RESULTS": {
|
||||
"value": 25,
|
||||
"description": "The maximum number of $(ref:sessions.Session) that will be included in a requested list."
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
@ -78,7 +78,6 @@
|
||||
"description": "Whether the window is set to be always on top."
|
||||
},
|
||||
"sessionId": {
|
||||
"unsupported": true,
|
||||
"type": "string",
|
||||
"optional": true,
|
||||
"description": "The session ID used to uniquely identify a Window obtained from the $(ref:sessions) API."
|
||||
|
@ -58,6 +58,7 @@ tags = webextensions
|
||||
[browser_ext_runtime_openOptionsPage.js]
|
||||
[browser_ext_runtime_openOptionsPage_uninstall.js]
|
||||
[browser_ext_runtime_setUninstallURL.js]
|
||||
[browser_ext_sessions.js]
|
||||
[browser_ext_simple.js]
|
||||
[browser_ext_tab_runtimeConnect.js]
|
||||
[browser_ext_tabs_audio.js]
|
||||
|
@ -0,0 +1,182 @@
|
||||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
SimpleTest.requestCompleteLog();
|
||||
|
||||
let initialTimestamps = [];
|
||||
|
||||
function onlyNewItemsFilter(item) {
|
||||
return !initialTimestamps.includes(item.lastModified);
|
||||
}
|
||||
|
||||
function checkWindow(window) {
|
||||
for (let prop of ["focused", "incognito", "alwaysOnTop"]) {
|
||||
is(window[prop], false, `closed window has the expected value for ${prop}`);
|
||||
}
|
||||
for (let prop of ["state", "type"]) {
|
||||
is(window[prop], "normal", `closed window has the expected value for ${prop}`);
|
||||
}
|
||||
}
|
||||
|
||||
function checkTab(tab, windowId, incognito) {
|
||||
for (let prop of ["selected", "highlighted", "active", "pinned"]) {
|
||||
is(tab[prop], false, `closed tab has the expected value for ${prop}`);
|
||||
}
|
||||
is(tab.windowId, windowId, "closed tab has the expected value for windowId");
|
||||
is(tab.incognito, incognito, "closed tab has the expected value for incognito");
|
||||
}
|
||||
|
||||
function checkRecentlyClosed(recentlyClosed, expectedCount, windowId, incognito = false) {
|
||||
let sessionIds = new Set();
|
||||
is(recentlyClosed.length, expectedCount, "the expected number of closed tabs/windows was found");
|
||||
for (let item of recentlyClosed) {
|
||||
if (item.window) {
|
||||
sessionIds.add(item.window.sessionId);
|
||||
checkWindow(item.window);
|
||||
} else if (item.tab) {
|
||||
sessionIds.add(item.tab.sessionId);
|
||||
checkTab(item.tab, windowId, incognito);
|
||||
}
|
||||
}
|
||||
is(sessionIds.size, expectedCount, "each item has a unique sessionId");
|
||||
}
|
||||
|
||||
add_task(function* test_sessions_get_recently_closed() {
|
||||
function* openAndCloseWindow(url = "http://example.com", tabUrls) {
|
||||
let win = yield BrowserTestUtils.openNewBrowserWindow();
|
||||
yield BrowserTestUtils.loadURI(win.gBrowser.selectedBrowser, url);
|
||||
yield BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
|
||||
if (tabUrls) {
|
||||
for (let url of tabUrls) {
|
||||
yield BrowserTestUtils.openNewForegroundTab(win.gBrowser, url);
|
||||
}
|
||||
}
|
||||
yield BrowserTestUtils.closeWindow(win);
|
||||
}
|
||||
|
||||
function background() {
|
||||
Promise.all([
|
||||
browser.sessions.getRecentlyClosed(),
|
||||
browser.tabs.query({active: true, currentWindow: true}),
|
||||
]).then(([recentlyClosed, tabs]) => {
|
||||
browser.test.sendMessage("initialData", {recentlyClosed, currentWindowId: tabs[0].windowId});
|
||||
});
|
||||
|
||||
browser.test.onMessage.addListener((msg, filter) => {
|
||||
if (msg == "check-sessions") {
|
||||
browser.sessions.getRecentlyClosed(filter).then(recentlyClosed => {
|
||||
browser.test.sendMessage("recentlyClosed", recentlyClosed);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
permissions: ["sessions", "tabs"],
|
||||
},
|
||||
background,
|
||||
});
|
||||
|
||||
// Open and close a window that will be ignored, to prove that we are removing previous entries
|
||||
yield openAndCloseWindow();
|
||||
|
||||
yield extension.startup();
|
||||
|
||||
let {recentlyClosed, currentWindowId} = yield extension.awaitMessage("initialData");
|
||||
initialTimestamps = recentlyClosed.map(item => item.lastModified);
|
||||
|
||||
yield openAndCloseWindow();
|
||||
extension.sendMessage("check-sessions");
|
||||
recentlyClosed = yield extension.awaitMessage("recentlyClosed");
|
||||
checkRecentlyClosed(recentlyClosed.filter(onlyNewItemsFilter), 1, currentWindowId);
|
||||
|
||||
yield openAndCloseWindow("about:config", ["about:robots", "about:mozilla"]);
|
||||
extension.sendMessage("check-sessions");
|
||||
recentlyClosed = yield extension.awaitMessage("recentlyClosed");
|
||||
// Check for multiple tabs in most recently closed window
|
||||
is(recentlyClosed[0].window.tabs.length, 3, "most recently closed window has the expected number of tabs");
|
||||
|
||||
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com");
|
||||
yield BrowserTestUtils.removeTab(tab);
|
||||
|
||||
tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com");
|
||||
yield BrowserTestUtils.removeTab(tab);
|
||||
|
||||
yield openAndCloseWindow();
|
||||
extension.sendMessage("check-sessions");
|
||||
recentlyClosed = yield extension.awaitMessage("recentlyClosed");
|
||||
let finalResult = recentlyClosed.filter(onlyNewItemsFilter);
|
||||
checkRecentlyClosed(finalResult, 5, currentWindowId);
|
||||
|
||||
isnot(finalResult[0].window, undefined, "first item is a window");
|
||||
is(finalResult[0].tab, undefined, "first item is not a tab");
|
||||
isnot(finalResult[1].tab, undefined, "second item is a tab");
|
||||
is(finalResult[1].window, undefined, "second item is not a window");
|
||||
isnot(finalResult[2].tab, undefined, "third item is a tab");
|
||||
is(finalResult[2].window, undefined, "third item is not a window");
|
||||
isnot(finalResult[3].window, undefined, "fourth item is a window");
|
||||
is(finalResult[3].tab, undefined, "fourth item is not a tab");
|
||||
isnot(finalResult[4].window, undefined, "fifth item is a window");
|
||||
is(finalResult[4].tab, undefined, "fifth item is not a tab");
|
||||
|
||||
// test with filter
|
||||
extension.sendMessage("check-sessions", {maxResults: 2});
|
||||
recentlyClosed = yield extension.awaitMessage("recentlyClosed");
|
||||
checkRecentlyClosed(recentlyClosed.filter(onlyNewItemsFilter), 2, currentWindowId);
|
||||
|
||||
yield extension.unload();
|
||||
});
|
||||
|
||||
add_task(function* test_sessions_get_recently_closed_private() {
|
||||
function background() {
|
||||
browser.test.onMessage.addListener((msg, filter) => {
|
||||
if (msg == "check-sessions") {
|
||||
browser.sessions.getRecentlyClosed(filter).then(recentlyClosed => {
|
||||
browser.test.sendMessage("recentlyClosed", recentlyClosed);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
permissions: ["sessions", "tabs"],
|
||||
},
|
||||
background,
|
||||
});
|
||||
|
||||
// Open a private browsing window.
|
||||
let privateWin = yield BrowserTestUtils.openNewBrowserWindow({private: true});
|
||||
|
||||
let {Management: {global: {WindowManager}}} = Cu.import("resource://gre/modules/Extension.jsm", {});
|
||||
|
||||
let privateWinId = WindowManager.getId(privateWin);
|
||||
|
||||
yield extension.startup();
|
||||
|
||||
extension.sendMessage("check-sessions");
|
||||
let recentlyClosed = yield extension.awaitMessage("recentlyClosed");
|
||||
initialTimestamps = recentlyClosed.map(item => item.lastModified);
|
||||
|
||||
// Open and close two tabs in the private window
|
||||
let tab = yield BrowserTestUtils.openNewForegroundTab(privateWin.gBrowser, "http://example.com");
|
||||
yield BrowserTestUtils.removeTab(tab);
|
||||
|
||||
tab = yield BrowserTestUtils.openNewForegroundTab(privateWin.gBrowser, "http://example.com");
|
||||
yield BrowserTestUtils.removeTab(tab);
|
||||
|
||||
extension.sendMessage("check-sessions");
|
||||
recentlyClosed = yield extension.awaitMessage("recentlyClosed");
|
||||
checkRecentlyClosed(recentlyClosed.filter(onlyNewItemsFilter), 2, privateWinId, true);
|
||||
|
||||
// Close the private window.
|
||||
yield BrowserTestUtils.closeWindow(privateWin);
|
||||
|
||||
extension.sendMessage("check-sessions");
|
||||
recentlyClosed = yield extension.awaitMessage("recentlyClosed");
|
||||
is(recentlyClosed.filter(onlyNewItemsFilter).length, 0, "the closed private window info was not found in recently closed data");
|
||||
|
||||
yield extension.unload();
|
||||
});
|
Loading…
Reference in New Issue
Block a user