Bug 1348911 - Add lastAccessed to tabs.Tab; r?zombie,mixedpuppy draft
authorThomas Wisniewski <wisniewskit@gmail.com>
Fri, 07 Jul 2017 20:17:23 -0400
changeset 609433 5a9bac31f4fde51b56be24b3e0cba48285934fae
parent 605571 c45f1aa05c4d8ee5e3ed6a707172e710152fce38
child 637570 90923375738b3404d90082d75930c03625bf6183
push id68576
push userwisniewskit@gmail.com
push dateSun, 16 Jul 2017 15:17:55 +0000
reviewerszombie, mixedpuppy
bugs1348911
milestone56.0a1
Bug 1348911 - Add lastAccessed to tabs.Tab; r?zombie,mixedpuppy MozReview-Commit-ID: 4ulhseGDQ4P
browser/components/extensions/ext-utils.js
browser/components/extensions/schemas/tabs.json
browser/components/extensions/test/browser/browser-common.ini
browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_tabs.js
browser/components/extensions/test/browser/browser_ext_tabs_lastAccessed.js
mobile/android/components/extensions/ext-utils.js
mobile/android/components/extensions/schemas/tabs.json
mobile/android/components/extensions/test/mochitest/mochitest.ini
mobile/android/components/extensions/test/mochitest/test_ext_tabs_lastAccessed.html
toolkit/components/extensions/ExtensionTabs.jsm
toolkit/components/extensions/test/mochitest/mochitest-common.ini
--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -517,16 +517,20 @@ class Tab extends TabBase {
     } else if (nativeTab.muteReason) {
       mutedInfo.reason = "extension";
       mutedInfo.extensionId = nativeTab.muteReason;
     }
 
     return mutedInfo;
   }
 
+  get lastAccessed() {
+    return this.nativeTab.lastAccessed;
+  }
+
   get pinned() {
     return this.nativeTab.pinned;
   }
 
   get active() {
     return this.nativeTab.selected;
   }
 
@@ -573,16 +577,17 @@ class Tab extends TabBase {
     let result = {
       sessionId: String(tabData.closedId),
       index: tabData.pos ? tabData.pos : 0,
       windowId: window && windowTracker.getId(window),
       highlighted: false,
       active: false,
       pinned: false,
       incognito: Boolean(tabData.state && tabData.state.isPrivate),
+      lastAccessed: tabData.state ? tabData.state.lastAccessed : tabData.lastAccessed,
     };
 
     if (extension.tabManager.hasTabPermission(tabData)) {
       let entries = tabData.state ? tabData.state.entries : tabData.entries;
       let entry = entries[entries.length - 1];
       result.url = entry.url;
       result.title = entry.title;
       if (tabData.image) {
--- a/browser/components/extensions/schemas/tabs.json
+++ b/browser/components/extensions/schemas/tabs.json
@@ -59,16 +59,17 @@
           "id": {"type": "integer", "minimum": -1, "optional": true, "description": "The ID of the tab. Tab IDs are unique within a browser session. Under some circumstances a Tab may not be assigned an ID, for example when querying foreign tabs using the $(ref:sessions) API, in which case a session ID may be present. Tab ID can also be set to $(ref:tabs.TAB_ID_NONE) for apps and devtools windows."},
           "index": {"type": "integer", "minimum": -1, "description": "The zero-based index of the tab within its window."},
           "windowId": {"type": "integer", "minimum": 0, "description": "The ID of the window the tab is contained within."},
           "openerTabId": {"unsupported": true, "type": "integer", "minimum": 0, "optional": true, "description": "The ID of the tab that opened this tab, if any. This property is only present if the opener tab still exists."},
           "selected": {"type": "boolean", "description": "Whether the tab is selected.", "deprecated": "Please use $(ref:tabs.Tab.highlighted).", "unsupported": true},
           "highlighted": {"type": "boolean", "description": "Whether the tab is highlighted. Works as an alias of active"},
           "active": {"type": "boolean", "description": "Whether the tab is active in its window. (Does not necessarily mean the window is focused.)"},
           "pinned": {"type": "boolean", "description": "Whether the tab is pinned."},
+          "lastAccessed": {"type": "integer", "optional": true, "description": "The last time the tab was accessed as the number of milliseconds since epoch."},
           "audible": {"type": "boolean", "optional": true, "description": "Whether the tab has produced sound over the past couple of seconds (but it might not be heard if also muted). Equivalent to whether the speaker audio indicator is showing."},
           "mutedInfo": {"$ref": "MutedInfo", "optional": true, "description": "Current tab muted state and the reason for the last state change."},
           "url": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The URL the tab is displaying. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission."},
           "title": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The title of the tab. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission."},
           "favIconUrl": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The URL of the tab's favicon. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission. It may also be an empty string if the tab is loading."},
           "status": {"type": "string", "optional": true, "description": "Either <em>loading</em> or <em>complete</em>."},
           "incognito": {"type": "boolean", "description": "Whether the tab is in an incognito window."},
           "width": {"type": "integer", "optional": true, "description": "The width of the tab in pixels."},
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -118,16 +118,17 @@ skip-if = debug || asan # Bug 1354681
 [browser_ext_tabs_executeScript.js]
 [browser_ext_tabs_executeScript_good.js]
 [browser_ext_tabs_executeScript_bad.js]
 [browser_ext_tabs_executeScript_multiple.js]
 [browser_ext_tabs_executeScript_no_create.js]
 [browser_ext_tabs_executeScript_runAt.js]
 [browser_ext_tabs_getCurrent.js]
 [browser_ext_tabs_insertCSS.js]
+[browser_ext_tabs_lastAccessed.js]
 [browser_ext_tabs_removeCSS.js]
 [browser_ext_tabs_move_array.js]
 [browser_ext_tabs_move_window.js]
 [browser_ext_tabs_move_window_multiple.js]
 [browser_ext_tabs_move_window_pinned.js]
 [browser_ext_tabs_onHighlighted.js]
 [browser_ext_tabs_onUpdated.js]
 [browser_ext_tabs_query.js]
--- a/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_tabs.js
+++ b/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_tabs.js
@@ -38,42 +38,49 @@ add_task(async function test_sessions_ge
   });
 
   let win = await BrowserTestUtils.openNewBrowserWindow();
   await BrowserTestUtils.loadURI(win.gBrowser.selectedBrowser, "about:mozilla");
   await BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
   let expectedTabs = [];
   let tab = win.gBrowser.selectedTab;
   expectedTabs.push(expectedTabInfo(tab, win));
+  let lastAccessedTimes = new Map();
+  lastAccessedTimes.set("about:mozilla", tab.lastAccessed);
 
   for (let url of ["about:robots", "about:buildconfig"]) {
     tab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser, url);
     expectedTabs.push(expectedTabInfo(tab, win));
+    lastAccessedTimes.set(url, tab.lastAccessed);
   }
 
   await extension.startup();
 
   // Test with a closed tab.
   await BrowserTestUtils.removeTab(tab);
 
   extension.sendMessage("check-sessions");
   let recentlyClosed = await extension.awaitMessage("recentlyClosed");
   let tabInfo = recentlyClosed[0].tab;
   let expectedTab = expectedTabs.pop();
   checkTabInfo(expectedTab, tabInfo);
+  ok(tabInfo.lastAccessed > lastAccessedTimes.get(tabInfo.url),
+     "lastAccessed has been updated");
 
   // Test with a closed window containing tabs.
   await BrowserTestUtils.closeWindow(win);
 
   extension.sendMessage("check-sessions");
   recentlyClosed = await extension.awaitMessage("recentlyClosed");
   let tabInfos = recentlyClosed[0].window.tabs;
   is(tabInfos.length, 2, "Expected number of tabs in closed window.");
   for (let x = 0; x < tabInfos.length; x++) {
     checkTabInfo(expectedTabs[x], tabInfos[x]);
+    ok(tabInfos[x].lastAccessed > lastAccessedTimes.get(tabInfos[x].url),
+       "lastAccessed has been updated");
   }
 
   await extension.unload();
 
   // Test without tabs permission.
   extension = ExtensionTestUtils.loadExtension({
     manifest: {
       permissions: ["sessions"],
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_lastAccessed.js
@@ -0,0 +1,42 @@
+"use strict";
+
+add_task(async function testLastAccessed() {
+  let past = Date.now();
+
+  await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/?1");
+  await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/?2");
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["tabs"],
+    },
+    async background() {
+      browser.test.onMessage.addListener(async function(msg, past) {
+        if (msg !== "past") {
+          return;
+        }
+
+        let [tab1] = await browser.tabs.query({url: "https://example.com/?1"});
+        let [tab2] = await browser.tabs.query({url: "https://example.com/?2"});
+
+        browser.test.assertTrue(tab1 && tab2, "Expected tabs were found");
+
+        let now = Date.now();
+
+        browser.test.assertTrue(past < tab1.lastAccessed &&
+                                tab1.lastAccessed < tab2.lastAccessed &&
+                                tab2.lastAccessed <= now,
+                                "lastAccessed timestamps are recent and in the right order");
+
+        await browser.tabs.remove([tab1.id, tab2.id]);
+
+        browser.test.notifyPass("tabs.lastAccessed");
+      });
+    },
+  });
+
+  await extension.startup();
+  await extension.sendMessage("past", past);
+  await extension.awaitFinish("tabs.lastAccessed");
+  await extension.unload();
+});
--- a/mobile/android/components/extensions/ext-utils.js
+++ b/mobile/android/components/extensions/ext-utils.js
@@ -393,16 +393,20 @@ class Tab extends TabBase {
   get index() {
     return this.window.BrowserApp.tabs.indexOf(this.nativeTab);
   }
 
   get mutedInfo() {
     return {muted: false};
   }
 
+  get lastAccessed() {
+    return this.nativeTab.lastTouchedAt;
+  }
+
   get pinned() {
     return false;
   }
 
   get active() {
     return this.nativeTab.getActive();
   }
 
--- a/mobile/android/components/extensions/schemas/tabs.json
+++ b/mobile/android/components/extensions/schemas/tabs.json
@@ -59,16 +59,17 @@
           "id": {"type": "integer", "minimum": -1, "optional": true, "description": "The ID of the tab. Tab IDs are unique within a browser session. Under some circumstances a Tab may not be assigned an ID, for example when querying foreign tabs using the $(ref:sessions) API, in which case a session ID may be present. Tab ID can also be set to $(ref:tabs.TAB_ID_NONE) for apps and devtools windows."},
           "index": {"type": "integer", "minimum": -1, "description": "The zero-based index of the tab within its window."},
           "windowId": {"type": "integer", "minimum": 0, "description": "The ID of the window the tab is contained within."},
           "openerTabId": {"unsupported": true, "type": "integer", "minimum": 0, "optional": true, "description": "The ID of the tab that opened this tab, if any. This property is only present if the opener tab still exists."},
           "selected": {"type": "boolean", "description": "Whether the tab is selected.", "deprecated": "Please use $(ref:tabs.Tab.highlighted).", "unsupported": true},
           "highlighted": {"type": "boolean", "description": "Whether the tab is highlighted. Works as an alias of active."},
           "active": {"type": "boolean", "description": "Whether the tab is active in its window. (Does not necessarily mean the window is focused.)"},
           "pinned": {"type": "boolean", "description": "Whether the tab is pinned."},
+          "lastAccessed": {"type": "integer", "optional": true, "description": "The last time the tab was accessed as the number of milliseconds since epoch."},
           "audible": {"type": "boolean", "optional": true, "description": "Whether the tab has produced sound over the past couple of seconds (but it might not be heard if also muted). Equivalent to whether the speaker audio indicator is showing."},
           "mutedInfo": {"$ref": "MutedInfo", "optional": true, "description": "Current tab muted state and the reason for the last state change."},
           "url": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The URL the tab is displaying. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission."},
           "title": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The title of the tab. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission."},
           "favIconUrl": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The URL of the tab's favicon. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission. It may also be an empty string if the tab is loading."},
           "status": {"type": "string", "optional": true, "description": "Either <em>loading</em> or <em>complete</em>."},
           "incognito": {"type": "boolean", "description": "Whether the tab is in an incognito window."},
           "width": {"type": "integer", "optional": true, "description": "The width of the tab in pixels."},
--- a/mobile/android/components/extensions/test/mochitest/mochitest.ini
+++ b/mobile/android/components/extensions/test/mochitest/mochitest.ini
@@ -21,13 +21,14 @@ tags = webextensions
 [test_ext_tabs_executeScript.html]
 [test_ext_tabs_executeScript_bad.html]
 skip-if = true # Currently fails in emulator runs
 [test_ext_tabs_executeScript_good.html]
 [test_ext_tabs_executeScript_no_create.html]
 [test_ext_tabs_executeScript_runAt.html]
 [test_ext_tabs_getCurrent.html]
 [test_ext_tabs_insertCSS.html]
+[test_ext_tabs_lastAccessed.html]
 [test_ext_tabs_reload.html]
 [test_ext_tabs_reload_bypass_cache.html]
 [test_ext_tabs_onUpdated.html]
 [test_ext_tabs_sendMessage.html]
 [test_ext_tabs_update_url.html]
new file mode 100644
--- /dev/null
+++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_lastAccessed.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Tabs lastAccessed Test</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="head.js"></script>
+  <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(async function testLastAccessed() {
+  let past = Date.now();
+
+  window.open("https://example.com/?1");
+  window.open("https://example.com/?2");
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["tabs"],
+    },
+    async background() {
+      browser.test.onMessage.addListener(async function(msg, past) {
+        if (msg !== "past") {
+          return;
+        }
+
+        let [tab1] = await browser.tabs.query({url: "https://example.com/?1"});
+        let [tab2] = await browser.tabs.query({url: "https://example.com/?2"});
+
+        browser.test.assertTrue(tab1 && tab2, "Expected tabs were found");
+
+        let now = Date.now();
+
+        browser.test.assertTrue(past < tab1.lastAccessed &&
+                                tab1.lastAccessed < tab2.lastAccessed &&
+                                tab2.lastAccessed <= now,
+                                "lastAccessed timestamps are recent and in the right order");
+
+        await browser.tabs.remove([tab1.id, tab2.id]);
+
+        browser.test.notifyPass("tabs.lastAccessed");
+      });
+    },
+  });
+
+  await extension.startup();
+  await extension.sendMessage("past", past);
+  await extension.awaitFinish("tabs.lastAccessed");
+  await extension.unload();
+});
+</script>
+
+</body>
--- a/toolkit/components/extensions/ExtensionTabs.jsm
+++ b/toolkit/components/extensions/ExtensionTabs.jsm
@@ -261,16 +261,27 @@ class TabBase {
    */
   get favIconUrl() {
     if (this.hasTabPermission) {
       return this._favIconUrl;
     }
   }
 
   /**
+   * @property {integer} lastAccessed
+   *        Returns the last time the tab was accessed as the number of
+   *        milliseconds since epoch.
+   *        @readonly
+   *        @abstract
+   */
+  get lastAccessed() {
+    throw new Error("Not implemented");
+  }
+
+  /**
    * @property {boolean} audible
    *        Returns true if the tab is currently playing audio, false otherwise.
    *        @readonly
    *        @abstract
    */
   get audible() {
     throw new Error("Not implemented");
   }
@@ -476,16 +487,17 @@ class TabBase {
       windowId: this.windowId,
       highlighted: this.selected,
       active: this.selected,
       pinned: this.pinned,
       status: this.status,
       incognito: this.incognito,
       width: this.width,
       height: this.height,
+      lastAccessed: this.lastAccessed,
       audible: this.audible,
       mutedInfo: this.mutedInfo,
     };
 
     // If the tab has not been fully layed-out yet, fallback to the geometry
     // from a different tab (usually the currently active tab).
     if (fallbackTab && (!result.width || !result.height)) {
       result.width = fallbackTab.width;
--- a/toolkit/components/extensions/test/mochitest/mochitest-common.ini
+++ b/toolkit/components/extensions/test/mochitest/mochitest-common.ini
@@ -122,9 +122,9 @@ skip-if = os == 'android'
 [test_ext_webrequest_upload.html]
 skip-if = os == 'android' # Currently fails in emulator tests
 [test_ext_webrequest_permission.html]
 [test_ext_webrequest_websocket.html]
 [test_ext_webnavigation.html]
 [test_ext_webnavigation_filters.html]
 [test_ext_window_postMessage.html]
 [test_ext_subframes_privileges.html]
-[test_ext_xhr_capabilities.html]
\ No newline at end of file
+[test_ext_xhr_capabilities.html]