mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-23 02:05:42 +00:00
Bug 894595 - part 1.6 - Move session history collection into a module; r=yoric
This commit is contained in:
parent
1f68f49d38
commit
548fded61a
272
browser/components/sessionstore/src/SessionHistory.jsm
Normal file
272
browser/components/sessionstore/src/SessionHistory.jsm
Normal file
@ -0,0 +1,272 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["SessionHistory"];
|
||||
|
||||
const Cu = Components.utils;
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel",
|
||||
"resource:///modules/sessionstore/PrivacyLevel.jsm");
|
||||
|
||||
function debug(msg) {
|
||||
Services.console.logStringMessage("SessionHistory: " + msg);
|
||||
}
|
||||
|
||||
// The preference value that determines how much post data to save.
|
||||
XPCOMUtils.defineLazyGetter(this, "gPostData", function () {
|
||||
const PREF = "browser.sessionstore.postdata";
|
||||
|
||||
// Observer that updates the cached value when the preference changes.
|
||||
Services.prefs.addObserver(PREF, () => {
|
||||
this.gPostData = Services.prefs.getIntPref(PREF);
|
||||
}, false);
|
||||
|
||||
return Services.prefs.getIntPref(PREF);
|
||||
});
|
||||
|
||||
/**
|
||||
* The external API exported by this module.
|
||||
*/
|
||||
this.SessionHistory = Object.freeze({
|
||||
read: function (docShell, includePrivateData) {
|
||||
return SessionHistoryInternal.read(docShell, includePrivateData);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* The internal API for the SessionHistory module.
|
||||
*/
|
||||
let SessionHistoryInternal = {
|
||||
/**
|
||||
* Collects session history data for a given docShell.
|
||||
*
|
||||
* @param docShell
|
||||
* The docShell that owns the session history.
|
||||
* @param includePrivateData (optional)
|
||||
* True to always include private data and skip any privacy checks.
|
||||
*/
|
||||
read: function (docShell, includePrivateData = false) {
|
||||
let data = {entries: []};
|
||||
let isPinned = docShell.isAppTab;
|
||||
let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
|
||||
let history = webNavigation.sessionHistory;
|
||||
|
||||
if (history && history.count > 0) {
|
||||
try {
|
||||
for (let i = 0; i < history.count; i++) {
|
||||
let shEntry = history.getEntryAtIndex(i, false);
|
||||
let entry = this._serializeEntry(shEntry, includePrivateData, isPinned);
|
||||
data.entries.push(entry);
|
||||
}
|
||||
} catch (ex) {
|
||||
// In some cases, getEntryAtIndex will throw. This seems to be due to
|
||||
// history.count being higher than it should be. By doing this in a
|
||||
// try-catch, we'll update history to where it breaks, print an error
|
||||
// message, and still save sessionstore.js.
|
||||
debug("SessionStore failed gathering complete history " +
|
||||
"for the focused window/tab. See bug 669196.");
|
||||
}
|
||||
data.index = history.index + 1;
|
||||
} else {
|
||||
let uri = webNavigation.currentURI.spec;
|
||||
// We landed here because the history is inaccessible or there are no
|
||||
// history entries. In that case we should at least record the docShell's
|
||||
// current URL as a single history entry. If the URL is not about:blank
|
||||
// or it's a blank tab that was modified (like a custom newtab page),
|
||||
// record it. For about:blank we explicitly want an empty array without
|
||||
// an 'index' property to denote that there are no history entries.
|
||||
if (uri != "about:blank" || webNavigation.document.body.hasChildNodes()) {
|
||||
data.entries.push({ url: uri });
|
||||
data.index = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get an object that is a serialized representation of a History entry.
|
||||
*
|
||||
* @param shEntry
|
||||
* nsISHEntry instance
|
||||
* @param includePrivateData
|
||||
* Always return privacy sensitive data (use with care).
|
||||
* @param isPinned
|
||||
* The tab is pinned and should be treated differently for privacy.
|
||||
* @return object
|
||||
*/
|
||||
_serializeEntry: function (shEntry, includePrivateData, isPinned) {
|
||||
let entry = { url: shEntry.URI.spec };
|
||||
|
||||
// Save some bytes and don't include the title property
|
||||
// if that's identical to the current entry's URL.
|
||||
if (shEntry.title && shEntry.title != entry.url) {
|
||||
entry.title = shEntry.title;
|
||||
}
|
||||
if (shEntry.isSubFrame) {
|
||||
entry.subframe = true;
|
||||
}
|
||||
|
||||
let cacheKey = shEntry.cacheKey;
|
||||
if (cacheKey && cacheKey instanceof Ci.nsISupportsPRUint32 &&
|
||||
cacheKey.data != 0) {
|
||||
// XXXbz would be better to have cache keys implement
|
||||
// nsISerializable or something.
|
||||
entry.cacheKey = cacheKey.data;
|
||||
}
|
||||
entry.ID = shEntry.ID;
|
||||
entry.docshellID = shEntry.docshellID;
|
||||
|
||||
// We will include the property only if it's truthy to save a couple of
|
||||
// bytes when the resulting object is stringified and saved to disk.
|
||||
if (shEntry.referrerURI)
|
||||
entry.referrer = shEntry.referrerURI.spec;
|
||||
|
||||
if (shEntry.srcdocData)
|
||||
entry.srcdocData = shEntry.srcdocData;
|
||||
|
||||
if (shEntry.isSrcdocEntry)
|
||||
entry.isSrcdocEntry = shEntry.isSrcdocEntry;
|
||||
|
||||
if (shEntry.contentType)
|
||||
entry.contentType = shEntry.contentType;
|
||||
|
||||
let x = {}, y = {};
|
||||
shEntry.getScrollPosition(x, y);
|
||||
if (x.value != 0 || y.value != 0)
|
||||
entry.scroll = x.value + "," + y.value;
|
||||
|
||||
// Collect post data for the current history entry.
|
||||
try {
|
||||
let postdata = this._serializePostData(shEntry, isPinned);
|
||||
if (postdata) {
|
||||
entry.postdata_b64 = postdata;
|
||||
}
|
||||
} catch (ex) {
|
||||
// POSTDATA is tricky - especially since some extensions don't get it right
|
||||
debug("Failed serializing post data: " + ex);
|
||||
}
|
||||
|
||||
// Collect owner data for the current history entry.
|
||||
try {
|
||||
let owner = this._serializeOwner(shEntry);
|
||||
if (owner) {
|
||||
entry.owner_b64 = owner;
|
||||
}
|
||||
} catch (ex) {
|
||||
// Not catching anything specific here, just possible errors
|
||||
// from writeCompoundObject() and the like.
|
||||
debug("Failed serializing owner data: " + ex);
|
||||
}
|
||||
|
||||
entry.docIdentifier = shEntry.BFCacheEntry.ID;
|
||||
|
||||
if (shEntry.stateData != null) {
|
||||
entry.structuredCloneState = shEntry.stateData.getDataAsBase64();
|
||||
entry.structuredCloneVersion = shEntry.stateData.formatVersion;
|
||||
}
|
||||
|
||||
if (!(shEntry instanceof Ci.nsISHContainer)) {
|
||||
return entry;
|
||||
}
|
||||
|
||||
if (shEntry.childCount > 0) {
|
||||
let children = [];
|
||||
for (let i = 0; i < shEntry.childCount; i++) {
|
||||
let child = shEntry.GetChildAt(i);
|
||||
|
||||
if (child) {
|
||||
// Don't try to restore framesets containing wyciwyg URLs.
|
||||
// (cf. bug 424689 and bug 450595)
|
||||
if (child.URI.schemeIs("wyciwyg")) {
|
||||
children.length = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
children.push(this._serializeEntry(child, includePrivateData, isPinned));
|
||||
}
|
||||
}
|
||||
|
||||
if (children.length) {
|
||||
entry.children = children;
|
||||
}
|
||||
}
|
||||
|
||||
return entry;
|
||||
},
|
||||
|
||||
/**
|
||||
* Serialize post data contained in the given session history entry.
|
||||
*
|
||||
* @param shEntry
|
||||
* The session history entry.
|
||||
* @param isPinned
|
||||
* Whether the docShell is owned by a pinned tab.
|
||||
* @return The base64 encoded post data.
|
||||
*/
|
||||
_serializePostData: function (shEntry, isPinned) {
|
||||
let isHttps = shEntry.URI.schemeIs("https");
|
||||
if (!shEntry.postData || !gPostData ||
|
||||
!PrivacyLevel.canSave({isHttps: isHttps, isPinned: isPinned})) {
|
||||
return null;
|
||||
}
|
||||
|
||||
shEntry.postData.QueryInterface(Ci.nsISeekableStream)
|
||||
.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
|
||||
let stream = Cc["@mozilla.org/binaryinputstream;1"]
|
||||
.createInstance(Ci.nsIBinaryInputStream);
|
||||
stream.setInputStream(shEntry.postData);
|
||||
let postBytes = stream.readByteArray(stream.available());
|
||||
let postdata = String.fromCharCode.apply(null, postBytes);
|
||||
if (gPostData != -1 &&
|
||||
postdata.replace(/^(Content-.*\r\n)+(\r\n)*/, "").length > gPostData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// We can stop doing base64 encoding once our serialization into JSON
|
||||
// is guaranteed to handle all chars in strings, including embedded
|
||||
// nulls.
|
||||
return btoa(postdata);
|
||||
},
|
||||
|
||||
/**
|
||||
* Serialize owner data contained in the given session history entry.
|
||||
*
|
||||
* @param shEntry
|
||||
* The session history entry.
|
||||
* @return The base64 encoded owner data.
|
||||
*/
|
||||
_serializeOwner: function (shEntry) {
|
||||
if (!shEntry.owner) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let binaryStream = Cc["@mozilla.org/binaryoutputstream;1"].
|
||||
createInstance(Ci.nsIObjectOutputStream);
|
||||
let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
|
||||
pipe.init(false, false, 0, 0xffffffff, null);
|
||||
binaryStream.setOutputStream(pipe.outputStream);
|
||||
binaryStream.writeCompoundObject(shEntry.owner, Ci.nsISupports, true);
|
||||
binaryStream.close();
|
||||
|
||||
// Now we want to read the data from the pipe's input end and encode it.
|
||||
let scriptableStream = Cc["@mozilla.org/binaryinputstream;1"].
|
||||
createInstance(Ci.nsIBinaryInputStream);
|
||||
scriptableStream.setInputStream(pipe.inputStream);
|
||||
let ownerBytes =
|
||||
scriptableStream.readByteArray(scriptableStream.available());
|
||||
|
||||
// We can stop doing base64 encoding once our serialization into JSON
|
||||
// is guaranteed to handle all chars in strings, including embedded
|
||||
// nulls.
|
||||
return btoa(String.fromCharCode.apply(null, ownerBytes));
|
||||
}
|
||||
};
|
@ -132,6 +132,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage",
|
||||
"resource:///modules/sessionstore/SessionStorage.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "SessionCookies",
|
||||
"resource:///modules/sessionstore/SessionCookies.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory",
|
||||
"resource:///modules/sessionstore/SessionHistory.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "_SessionFile",
|
||||
"resource:///modules/sessionstore/_SessionFile.jsm");
|
||||
|
||||
@ -393,10 +395,6 @@ let SessionStoreInternal = {
|
||||
this._initPrefs();
|
||||
this._initialized = true;
|
||||
|
||||
// this pref is only read at startup, so no need to observe it
|
||||
this._sessionhistory_max_entries =
|
||||
this._prefBranch.getIntPref("sessionhistory.max_entries");
|
||||
|
||||
// Wait until nsISessionStartup has finished reading the session data.
|
||||
gSessionStartup.onceInitialized.then(() => {
|
||||
// Parse session data and start restoring.
|
||||
@ -4377,60 +4375,17 @@ let TabState = {
|
||||
*/
|
||||
_collectTabHistory: function (tab, tabData, options = {}) {
|
||||
let includePrivateData = options && options.includePrivateData;
|
||||
let browser = tab.linkedBrowser;
|
||||
let history = null;
|
||||
try {
|
||||
history = browser.sessionHistory;
|
||||
} catch (ex) {
|
||||
// this could happen if we catch a tab during (de)initialization
|
||||
}
|
||||
let docShell = tab.linkedBrowser.docShell;
|
||||
|
||||
// XXXzeniko anchor navigation doesn't reset __SS_data, so we could reuse
|
||||
// data even when we shouldn't (e.g. Back, different anchor)
|
||||
if (history && browser.__SS_data &&
|
||||
browser.__SS_data.entries[history.index] &&
|
||||
browser.__SS_data.entries[history.index].url == browser.currentURI.spec &&
|
||||
history.index < this._sessionhistory_max_entries - 1 && !includePrivateData) {
|
||||
tabData = browser.__SS_data;
|
||||
tabData.index = history.index + 1;
|
||||
}
|
||||
else if (history && history.count > 0) {
|
||||
try {
|
||||
for (let j = 0; j < history.count; j++) {
|
||||
let entry = this._serializeHistoryEntry(history.getEntryAtIndex(j, false),
|
||||
includePrivateData, tab.pinned);
|
||||
tabData.entries.push(entry);
|
||||
}
|
||||
// If we make it through the for loop, then we're ok and we should clear
|
||||
// any indicator of brokenness.
|
||||
delete tab.__SS_broken_history;
|
||||
}
|
||||
catch (ex) {
|
||||
// In some cases, getEntryAtIndex will throw. This seems to be due to
|
||||
// history.count being higher than it should be. By doing this in a
|
||||
// try-catch, we'll update history to where it breaks, assert for
|
||||
// non-release builds, and still save sessionstore.js. We'll track if
|
||||
// we've shown the assert for this tab so we only show it once.
|
||||
// cf. bug 669196.
|
||||
if (!tab.__SS_broken_history) {
|
||||
// First Focus the window & tab we're having trouble with.
|
||||
tab.ownerDocument.defaultView.focus();
|
||||
tab.ownerDocument.defaultView.gBrowser.selectedTab = tab;
|
||||
debug("SessionStore failed gathering complete history " +
|
||||
"for the focused window/tab. See bug 669196.");
|
||||
tab.__SS_broken_history = true;
|
||||
}
|
||||
}
|
||||
tabData.index = history.index + 1;
|
||||
if (docShell instanceof Ci.nsIDocShell) {
|
||||
let history = SessionHistory.read(docShell, includePrivateData);
|
||||
tabData.entries = history.entries;
|
||||
|
||||
// make sure not to cache privacy sensitive data which shouldn't get out
|
||||
if (!includePrivateData)
|
||||
browser.__SS_data = tabData;
|
||||
}
|
||||
else if (browser.currentURI.spec != "about:blank" ||
|
||||
browser.contentDocument.body.hasChildNodes()) {
|
||||
tabData.entries[0] = { url: browser.currentURI.spec };
|
||||
tabData.index = 1;
|
||||
// For blank tabs without any history entries,
|
||||
// there will not be an 'index' property.
|
||||
if ("index" in history) {
|
||||
tabData.index = history.index;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -4446,160 +4401,16 @@ let TabState = {
|
||||
*/
|
||||
_collectTabSessionStorage: function (tab, tabData, options = {}) {
|
||||
let includePrivateData = options && options.includePrivateData;
|
||||
let browser = tab.linkedBrowser;
|
||||
let history = null;
|
||||
try {
|
||||
history = browser.sessionHistory;
|
||||
} catch (ex) {
|
||||
// this could happen if we catch a tab during (de)initialization
|
||||
}
|
||||
let docShell = tab.linkedBrowser.docShell;
|
||||
|
||||
if (history && browser.docShell instanceof Ci.nsIDocShell) {
|
||||
let storageData = SessionStorage.serialize(browser.docShell, includePrivateData)
|
||||
if (docShell instanceof Ci.nsIDocShell) {
|
||||
let storageData = SessionStorage.serialize(docShell, includePrivateData)
|
||||
if (Object.keys(storageData).length) {
|
||||
tabData.storage = storageData;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get an object that is a serialized representation of a History entry.
|
||||
*
|
||||
* @param shEntry
|
||||
* nsISHEntry instance
|
||||
* @param includePrivateData
|
||||
* Always return privacy sensitive data (use with care).
|
||||
* @param isPinned
|
||||
* The tab is pinned and should be treated differently for privacy.
|
||||
* @return object
|
||||
*/
|
||||
_serializeHistoryEntry: function (shEntry, includePrivateData, isPinned) {
|
||||
let entry = { url: shEntry.URI.spec };
|
||||
|
||||
if (shEntry.title && shEntry.title != entry.url) {
|
||||
entry.title = shEntry.title;
|
||||
}
|
||||
if (shEntry.isSubFrame) {
|
||||
entry.subframe = true;
|
||||
}
|
||||
if (!(shEntry instanceof Ci.nsISHEntry)) {
|
||||
return entry;
|
||||
}
|
||||
|
||||
let cacheKey = shEntry.cacheKey;
|
||||
if (cacheKey && cacheKey instanceof Ci.nsISupportsPRUint32 &&
|
||||
cacheKey.data != 0) {
|
||||
// XXXbz would be better to have cache keys implement
|
||||
// nsISerializable or something.
|
||||
entry.cacheKey = cacheKey.data;
|
||||
}
|
||||
entry.ID = shEntry.ID;
|
||||
entry.docshellID = shEntry.docshellID;
|
||||
|
||||
// We will include the property only if it's truthy to save a couple of
|
||||
// bytes when the resulting object is stringified and saved to disk.
|
||||
if (shEntry.referrerURI)
|
||||
entry.referrer = shEntry.referrerURI.spec;
|
||||
|
||||
if (shEntry.srcdocData)
|
||||
entry.srcdocData = shEntry.srcdocData;
|
||||
|
||||
if (shEntry.isSrcdocEntry)
|
||||
entry.isSrcdocEntry = shEntry.isSrcdocEntry;
|
||||
|
||||
if (shEntry.contentType)
|
||||
entry.contentType = shEntry.contentType;
|
||||
|
||||
let x = {}, y = {};
|
||||
shEntry.getScrollPosition(x, y);
|
||||
if (x.value != 0 || y.value != 0)
|
||||
entry.scroll = x.value + "," + y.value;
|
||||
|
||||
try {
|
||||
let isHttps = shEntry.URI.schemeIs("https");
|
||||
let prefPostdata = Services.prefs.getIntPref("browser.sessionstore.postdata");
|
||||
if (shEntry.postData && (includePrivateData || prefPostdata &&
|
||||
PrivacyLevel.canSave({isHttps: isHttps, isPinned: isPinned}))) {
|
||||
shEntry.postData.QueryInterface(Ci.nsISeekableStream).
|
||||
seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
|
||||
let stream = Cc["@mozilla.org/binaryinputstream;1"].
|
||||
createInstance(Ci.nsIBinaryInputStream);
|
||||
stream.setInputStream(shEntry.postData);
|
||||
let postBytes = stream.readByteArray(stream.available());
|
||||
let postdata = String.fromCharCode.apply(null, postBytes);
|
||||
if (includePrivateData || prefPostdata == -1 ||
|
||||
postdata.replace(/^(Content-.*\r\n)+(\r\n)*/, "").length <=
|
||||
prefPostdata) {
|
||||
// We can stop doing base64 encoding once our serialization into JSON
|
||||
// is guaranteed to handle all chars in strings, including embedded
|
||||
// nulls.
|
||||
entry.postdata_b64 = btoa(postdata);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (ex) { debug(ex); } // POSTDATA is tricky - especially since some extensions don't get it right
|
||||
|
||||
if (shEntry.owner) {
|
||||
// Not catching anything specific here, just possible errors
|
||||
// from writeCompoundObject and the like.
|
||||
try {
|
||||
let binaryStream = Cc["@mozilla.org/binaryoutputstream;1"].
|
||||
createInstance(Ci.nsIObjectOutputStream);
|
||||
let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
|
||||
pipe.init(false, false, 0, 0xffffffff, null);
|
||||
binaryStream.setOutputStream(pipe.outputStream);
|
||||
binaryStream.writeCompoundObject(shEntry.owner, Ci.nsISupports, true);
|
||||
binaryStream.close();
|
||||
|
||||
// Now we want to read the data from the pipe's input end and encode it.
|
||||
let scriptableStream = Cc["@mozilla.org/binaryinputstream;1"].
|
||||
createInstance(Ci.nsIBinaryInputStream);
|
||||
scriptableStream.setInputStream(pipe.inputStream);
|
||||
let ownerBytes =
|
||||
scriptableStream.readByteArray(scriptableStream.available());
|
||||
// We can stop doing base64 encoding once our serialization into JSON
|
||||
// is guaranteed to handle all chars in strings, including embedded
|
||||
// nulls.
|
||||
entry.owner_b64 = btoa(String.fromCharCode.apply(null, ownerBytes));
|
||||
}
|
||||
catch (ex) { debug(ex); }
|
||||
}
|
||||
|
||||
entry.docIdentifier = shEntry.BFCacheEntry.ID;
|
||||
|
||||
if (shEntry.stateData != null) {
|
||||
entry.structuredCloneState = shEntry.stateData.getDataAsBase64();
|
||||
entry.structuredCloneVersion = shEntry.stateData.formatVersion;
|
||||
}
|
||||
|
||||
if (!(shEntry instanceof Ci.nsISHContainer)) {
|
||||
return entry;
|
||||
}
|
||||
|
||||
if (shEntry.childCount > 0) {
|
||||
let children = [];
|
||||
for (let i = 0; i < shEntry.childCount; i++) {
|
||||
let child = shEntry.GetChildAt(i);
|
||||
|
||||
if (child) {
|
||||
// don't try to restore framesets containing wyciwyg URLs (cf. bug 424689 and bug 450595)
|
||||
if (child.URI.schemeIs("wyciwyg")) {
|
||||
children = [];
|
||||
break;
|
||||
}
|
||||
|
||||
children.push(this._serializeHistoryEntry(child, includePrivateData,
|
||||
isPinned));
|
||||
}
|
||||
}
|
||||
|
||||
if (children.length)
|
||||
entry.children = children;
|
||||
}
|
||||
|
||||
return entry;
|
||||
},
|
||||
|
||||
/**
|
||||
* Go through all frames and store the current scroll positions
|
||||
* and innerHTML content of WYSIWYG editors
|
||||
|
@ -16,6 +16,7 @@ EXTRA_JS_MODULES = [
|
||||
'DocumentUtils.jsm',
|
||||
'PrivacyLevel.jsm',
|
||||
'SessionCookies.jsm',
|
||||
'SessionHistory.jsm',
|
||||
'SessionMigration.jsm',
|
||||
'SessionStorage.jsm',
|
||||
'SessionWorker.js',
|
||||
|
@ -58,7 +58,6 @@ function test() {
|
||||
ss.getBrowserState();
|
||||
|
||||
is(gBrowser.tabs[1], tab, "newly created tab should exist by now");
|
||||
ok(tab.linkedBrowser.__SS_data, "newly created tab should be in save state");
|
||||
|
||||
// Start a load and interrupt it by closing the tab
|
||||
tab.linkedBrowser.loadURI(URI_TO_LOAD);
|
||||
|
Loading…
Reference in New Issue
Block a user