merge m-c to fx-team

This commit is contained in:
Tim Taubert 2012-09-28 18:59:01 +02:00
commit c6a07311e8
29 changed files with 1082 additions and 982 deletions

View File

@ -1207,7 +1207,7 @@ BrowserGlue.prototype = {
},
_migrateUI: function BG__migrateUI() {
const UI_VERSION = 7;
const UI_VERSION = 8;
const BROWSER_DOCURL = "chrome://browser/content/browser.xul#";
let currentUIVersion = 0;
try {
@ -1342,6 +1342,15 @@ BrowserGlue.prototype = {
}
}
if (currentUIVersion < 8) {
// Reset homepage pref for users who have it set to google.com/firefox
let uri = Services.prefs.getComplexValue("browser.startup.homepage",
Ci.nsIPrefLocalizedString).data;
if (uri && /^https?:\/\/(www\.)?google(\.\w{2,3}){1,2}\/firefox\/?$/.test(uri)) {
Services.prefs.clearUserPref("browser.startup.homepage");
}
}
if (this._dirty)
this._dataSource.QueryInterface(Ci.nsIRDFRemoteDataSource).Flush();

View File

@ -88,7 +88,15 @@ MOCHITEST_BROWSER_FILES = \
browser_581593.js \
browser_581937.js \
browser_586147.js \
browser_586068-cascaded_restore.js \
browser_586068-apptabs.js \
browser_586068-apptabs_ondemand.js \
browser_586068-browser_state_interrupted.js \
browser_586068-cascade.js \
browser_586068-multi_window.js \
browser_586068-reload.js \
browser_586068-select.js \
browser_586068-window_state.js \
browser_586068-window_state_override.js \
browser_588426.js \
browser_590268.js \
browser_590563.js \

View File

@ -0,0 +1,55 @@
/* 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/. */
const PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
let stateBackup = ss.getBrowserState();
function test() {
waitForExplicitFinish();
Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, true);
registerCleanupFunction(function () {
Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
});
let state = { windows: [{ tabs: [
{ entries: [{ url: "http://example.org/#1" }], extData: { "uniq": r() }, pinned: true },
{ entries: [{ url: "http://example.org/#2" }], extData: { "uniq": r() }, pinned: true },
{ entries: [{ url: "http://example.org/#3" }], extData: { "uniq": r() }, pinned: true },
{ entries: [{ url: "http://example.org/#4" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#5" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#6" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#7" }], extData: { "uniq": r() } },
], selected: 5 }] };
let loadCount = 0;
gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
loadCount++;
// We'll make sure that the loads we get come from pinned tabs or the
// the selected tab.
// get the tab
let tab;
for (let i = 0; i < window.gBrowser.tabs.length; i++) {
if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser)
tab = window.gBrowser.tabs[i];
}
ok(tab.pinned || gBrowser.selectedTab == tab,
"load came from pinned or selected tab");
// We should get 4 loads: 3 app tabs + 1 normal selected tab
if (loadCount < 4)
return;
gProgressListener.unsetCallback();
executeSoon(function () {
waitForBrowserState(JSON.parse(stateBackup), finish);
});
});
ss.setBrowserState(JSON.stringify(state));
}

View File

@ -0,0 +1,52 @@
/* 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/. */
const PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
const PREF_RESTORE_PINNED_TABS_ON_DEMAND = "browser.sessionstore.restore_pinned_tabs_on_demand";
let stateBackup = ss.getBrowserState();
function test() {
waitForExplicitFinish();
Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, true);
Services.prefs.setBoolPref(PREF_RESTORE_PINNED_TABS_ON_DEMAND, true);
registerCleanupFunction(function () {
Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
Services.prefs.clearUserPref(PREF_RESTORE_PINNED_TABS_ON_DEMAND);
});
let state = { windows: [{ tabs: [
{ entries: [{ url: "http://example.org/#1" }], extData: { "uniq": r() }, pinned: true },
{ entries: [{ url: "http://example.org/#2" }], extData: { "uniq": r() }, pinned: true },
{ entries: [{ url: "http://example.org/#3" }], extData: { "uniq": r() }, pinned: true },
{ entries: [{ url: "http://example.org/#4" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#5" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#6" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#7" }], extData: { "uniq": r() } },
], selected: 5 }] };
gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
// get the tab
let tab;
for (let i = 0; i < window.gBrowser.tabs.length; i++) {
if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser)
tab = window.gBrowser.tabs[i];
}
// Check that the load only comes from the selected tab.
ok(gBrowser.selectedTab == tab, "load came from selected tab");
is(aNeedRestore, 6, "six tabs left to restore");
is(aRestoring, 1, "one tab is restoring");
is(aRestored, 0, "no tabs have been restored, yet");
gProgressListener.unsetCallback();
executeSoon(function () {
waitForBrowserState(JSON.parse(stateBackup), finish);
});
});
ss.setBrowserState(JSON.stringify(state));
}

View File

@ -0,0 +1,111 @@
/* 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/. */
const PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
let stateBackup = ss.getBrowserState();
function test() {
waitForExplicitFinish();
Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, false);
registerCleanupFunction(function () {
Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
});
// The first state will be loaded using setBrowserState, followed by the 2nd
// state also being loaded using setBrowserState, interrupting the first restore.
let state1 = { windows: [
{
tabs: [
{ entries: [{ url: "http://example.org#1" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org#2" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org#3" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org#4" }], extData: { "uniq": r() } }
],
selected: 1
},
{
tabs: [
{ entries: [{ url: "http://example.com#1" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com#2" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com#3" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com#4" }], extData: { "uniq": r() } },
],
selected: 3
}
] };
let state2 = { windows: [
{
tabs: [
{ entries: [{ url: "http://example.org#5" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org#6" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org#7" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org#8" }], extData: { "uniq": r() } }
],
selected: 3
},
{
tabs: [
{ entries: [{ url: "http://example.com#5" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com#6" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com#7" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com#8" }], extData: { "uniq": r() } },
],
selected: 1
}
] };
// interruptedAfter will be set after the selected tab from each window have loaded.
let interruptedAfter = 0;
let loadedWindow1 = false;
let loadedWindow2 = false;
let numTabs = state2.windows[0].tabs.length + state2.windows[1].tabs.length;
let loadCount = 0;
gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
loadCount++;
if (aBrowser.currentURI.spec == state1.windows[0].tabs[2].entries[0].url)
loadedWindow1 = true;
if (aBrowser.currentURI.spec == state1.windows[1].tabs[0].entries[0].url)
loadedWindow2 = true;
if (!interruptedAfter && loadedWindow1 && loadedWindow2) {
interruptedAfter = loadCount;
ss.setBrowserState(JSON.stringify(state2));
return;
}
if (loadCount < numTabs + interruptedAfter)
return;
// We don't actually care about load order in this test, just that they all
// do load.
is(loadCount, numTabs + interruptedAfter, "all tabs were restored");
is(aNeedRestore, 0, "there are no tabs left needing restore");
// Remove the progress listener from this window, it will be removed from
// theWin when that window is closed (in setBrowserState).
gProgressListener.unsetCallback();
executeSoon(function () {
closeAllButPrimaryWindow();
waitForBrowserState(JSON.parse(stateBackup), finish);
});
});
// We also want to catch the extra windows (there should be 2), so we need to observe domwindowopened
Services.ww.registerNotification(function observer(aSubject, aTopic, aData) {
if (aTopic == "domwindowopened") {
let win = aSubject.QueryInterface(Ci.nsIDOMWindow);
win.addEventListener("load", function onLoad() {
win.removeEventListener("load", onLoad);
Services.ww.unregisterNotification(observer);
win.gBrowser.addTabsProgressListener(gProgressListener);
});
}
});
ss.setBrowserState(JSON.stringify(state1));
}

View File

@ -0,0 +1,53 @@
/* 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/. */
const PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
let stateBackup = ss.getBrowserState();
function test() {
waitForExplicitFinish();
Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, false);
registerCleanupFunction(function () {
Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
});
let state = { windows: [{ tabs: [
{ entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com" }], extData: { "uniq": r() } }
] }] };
let expectedCounts = [
[3, 3, 0],
[2, 3, 1],
[1, 3, 2],
[0, 3, 3],
[0, 2, 4],
[0, 1, 5]
];
let loadCount = 0;
gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
loadCount++;
let expected = expectedCounts[loadCount - 1];
is(aNeedRestore, expected[0], "load " + loadCount + " - # tabs that need to be restored");
is(aRestoring, expected[1], "load " + loadCount + " - # tabs that are restoring");
is(aRestored, expected[2], "load " + loadCount + " - # tabs that has been restored");
if (loadCount == state.windows[0].tabs.length) {
gProgressListener.unsetCallback();
executeSoon(function () {
waitForBrowserState(JSON.parse(stateBackup), finish);
});
}
});
ss.setBrowserState(JSON.stringify(state));
}

View File

@ -1,848 +0,0 @@
/* 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/. */
let stateBackup = ss.getBrowserState();
const TAB_STATE_NEEDS_RESTORE = 1;
const TAB_STATE_RESTORING = 2;
function test() {
/** Test for Bug 586068 - Cascade page loads when restoring **/
waitForExplicitFinish();
// This test does a lot of window opening / closing and waiting for loads.
// In order to prevent timeouts, we'll extend the default that mochitest uses.
requestLongerTimeout(4);
runNextTest();
}
// test_reloadCascade, test_reloadReload are generated tests that are run out
// of cycle (since they depend on current state). They're listed in [tests] here
// so that it is obvious when they run in respect to the other tests.
let tests = [test_cascade, test_select, test_multiWindowState,
test_setWindowStateNoOverwrite, test_setWindowStateOverwrite,
test_setBrowserStateInterrupted, test_reload,
/* test_reloadReload, */ test_reloadCascadeSetup,
/* test_reloadCascade, */ test_apptabs_only,
test_restore_apptabs_ondemand];
function runNextTest() {
// Reset the pref
try {
Services.prefs.clearUserPref("browser.sessionstore.restore_on_demand");
Services.prefs.clearUserPref("browser.sessionstore.restore_pinned_tabs_on_demand");
} catch (e) {}
// set an empty state & run the next test, or finish
if (tests.length) {
// Enumerate windows and close everything but our primary window. We can't
// use waitForFocus() because apparently it's buggy. See bug 599253.
var windowsEnum = Services.wm.getEnumerator("navigator:browser");
while (windowsEnum.hasMoreElements()) {
var currentWindow = windowsEnum.getNext();
if (currentWindow != window) {
currentWindow.close();
}
}
ss.setBrowserState(JSON.stringify({ windows: [{ tabs: [{ url: 'about:blank' }] }] }));
let currentTest = tests.shift();
info("running " + currentTest.name);
executeSoon(currentTest);
}
else {
ss.setBrowserState(stateBackup);
executeSoon(finish);
}
}
function test_cascade() {
Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", false);
// We have our own progress listener for this test, which we'll attach before our state is set
let progressListener = {
onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
dump("\n\nload: " + aBrowser.currentURI.spec + "\n" + JSON.stringify(countTabs()) + "\n\n");
if (aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
test_cascade_progressCallback();
}
}
let state = { windows: [{ tabs: [
{ entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com" }], extData: { "uniq": r() } }
] }] };
let loadCount = 0;
// Since our progress listener is fired before the one in sessionstore, our
// expected counts look a little weird. This is because we inspect the state
// before sessionstore has marked the tab as finished restoring and before it
// starts restoring the next tab
let expectedCounts = [
[3, 3, 0],
[2, 3, 1],
[1, 3, 2],
[0, 3, 3],
[0, 2, 4],
[0, 1, 5]
];
function test_cascade_progressCallback() {
loadCount++;
let counts = countTabs();
let expected = expectedCounts[loadCount - 1];
is(counts[0], expected[0], "test_cascade: load " + loadCount + " - # tabs that need to be restored");
is(counts[1], expected[1], "test_cascade: load " + loadCount + " - # tabs that are restoring");
is(counts[2], expected[2], "test_cascade: load " + loadCount + " - # tabs that has been restored");
if (loadCount < state.windows[0].tabs.length)
return;
window.gBrowser.removeTabsProgressListener(progressListener);
runNextTest();
}
// This progress listener will get attached before the listener in session store.
window.gBrowser.addTabsProgressListener(progressListener);
ss.setBrowserState(JSON.stringify(state));
}
function test_select() {
// Set the pref to true so we know exactly how many tabs should be restoring at
// any given time. This guarantees that a finishing load won't start another.
Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", true);
// We have our own progress listener for this test, which we'll attach before our state is set
let progressListener = {
onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
if (aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
test_select_progressCallback(aBrowser);
}
}
let state = { windows: [{ tabs: [
{ entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org" }], extData: { "uniq": r() } }
], selected: 1 }] };
let loadCount = 0;
// expectedCounts looks a little wierd for the test case, but it works. See
// comment in test_cascade for an explanation
let expectedCounts = [
[5, 1, 0],
[4, 1, 1],
[3, 1, 2],
[2, 1, 3],
[1, 1, 4],
[0, 1, 5]
];
let tabOrder = [0, 5, 1, 4, 3, 2];
function test_select_progressCallback(aBrowser) {
loadCount++;
let counts = countTabs();
let expected = expectedCounts[loadCount - 1];
is(counts[0], expected[0], "test_select: load " + loadCount + " - # tabs that need to be restored");
is(counts[1], expected[1], "test_select: load " + loadCount + " - # tabs that are restoring");
is(counts[2], expected[2], "test_select: load " + loadCount + " - # tabs that has been restored");
if (loadCount < state.windows[0].tabs.length) {
// double check that this tab was the right one
let expectedData = state.windows[0].tabs[tabOrder[loadCount - 1]].extData.uniq;
let tab;
for (let i = 0; i < window.gBrowser.tabs.length; i++) {
if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser)
tab = window.gBrowser.tabs[i];
}
is(ss.getTabValue(tab, "uniq"), expectedData, "test_select: load " + loadCount + " - correct tab was restored");
// select the next tab
window.gBrowser.selectTabAtIndex(tabOrder[loadCount]);
return;
}
window.gBrowser.removeTabsProgressListener(progressListener);
runNextTest();
}
window.gBrowser.addTabsProgressListener(progressListener);
ss.setBrowserState(JSON.stringify(state));
}
function test_multiWindowState() {
Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", false);
// We have our own progress listener for this test, which we'll attach before our state is set
let progressListener = {
onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
// We only care about load events when the tab still has
// __SS_restoreState == TAB_STATE_RESTORING on it.
// Since our listener is attached before the sessionstore one, this works out.
if (aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
test_multiWindowState_progressCallback(aBrowser);
}
}
// The first window will be put into the already open window and the second
// window will be opened with _openWindowWithState, which is the source of the problem.
let state = { windows: [
{
tabs: [
{ entries: [{ url: "http://example.org#0" }], extData: { "uniq": r() } }
],
selected: 1
},
{
tabs: [
{ entries: [{ url: "http://example.com#1" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com#2" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com#3" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com#4" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com#5" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com#6" }], extData: { "uniq": r() } }
],
selected: 4
}
] };
let numTabs = state.windows[0].tabs.length + state.windows[1].tabs.length;
let loadCount = 0;
function test_multiWindowState_progressCallback(aBrowser) {
loadCount++;
if (loadCount < numTabs)
return;
// We don't actually care about load order in this test, just that they all
// do load.
is(loadCount, numTabs, "test_multiWindowState: all tabs were restored");
let count = countTabs();
is(count[0], 0,
"test_multiWindowState: there are no tabs left needing restore");
// Remove the progress listener from this window, it will be removed from
// theWin when that window is closed (in setBrowserState).
window.gBrowser.removeTabsProgressListener(progressListener);
runNextTest();
}
// We also want to catch the 2nd window, so we need to observe domwindowopened
function windowObserver(aSubject, aTopic, aData) {
let theWin = aSubject.QueryInterface(Ci.nsIDOMWindow);
if (aTopic == "domwindowopened") {
theWin.addEventListener("load", function() {
theWin.removeEventListener("load", arguments.callee, false);
Services.ww.unregisterNotification(windowObserver);
theWin.gBrowser.addTabsProgressListener(progressListener);
}, false);
}
}
Services.ww.registerNotification(windowObserver);
window.gBrowser.addTabsProgressListener(progressListener);
ss.setBrowserState(JSON.stringify(state));
}
function test_setWindowStateNoOverwrite() {
Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", false);
// We have our own progress listener for this test, which we'll attach before our state is set
let progressListener = {
onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
// We only care about load events when the tab still has
// __SS_restoreState == TAB_STATE_RESTORING on it.
// Since our listener is attached before the sessionstore one, this works out.
if (aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
test_setWindowStateNoOverwrite_progressCallback(aBrowser);
}
}
// We'll use 2 states so that we can make sure calling setWindowState doesn't
// wipe out currently restoring data.
let state1 = { windows: [{ tabs: [
{ entries: [{ url: "http://example.com#1" }] },
{ entries: [{ url: "http://example.com#2" }] },
{ entries: [{ url: "http://example.com#3" }] },
{ entries: [{ url: "http://example.com#4" }] },
{ entries: [{ url: "http://example.com#5" }] },
] }] };
let state2 = { windows: [{ tabs: [
{ entries: [{ url: "http://example.org#1" }] },
{ entries: [{ url: "http://example.org#2" }] },
{ entries: [{ url: "http://example.org#3" }] },
{ entries: [{ url: "http://example.org#4" }] },
{ entries: [{ url: "http://example.org#5" }] }
] }] };
let numTabs = state1.windows[0].tabs.length + state2.windows[0].tabs.length;
let loadCount = 0;
function test_setWindowStateNoOverwrite_progressCallback(aBrowser) {
loadCount++;
// When loadCount == 2, we'll also restore state2 into the window
if (loadCount == 2)
ss.setWindowState(window, JSON.stringify(state2), false);
if (loadCount < numTabs)
return;
// We don't actually care about load order in this test, just that they all
// do load.
is(loadCount, numTabs, "test_setWindowStateNoOverwrite: all tabs were restored");
// window.__SS_tabsToRestore isn't decremented until after the progress
// listener is called. Since we get in here before that, we still expect
// the count to be 1.
is(window.__SS_tabsToRestore, 1,
"test_setWindowStateNoOverwrite: window doesn't think there are more tabs to restore");
let count = countTabs();
is(count[0], 0,
"test_setWindowStateNoOverwrite: there are no tabs left needing restore");
// Remove the progress listener from this window, it will be removed from
// theWin when that window is closed (in setBrowserState).
window.gBrowser.removeTabsProgressListener(progressListener);
runNextTest();
}
window.gBrowser.addTabsProgressListener(progressListener);
ss.setWindowState(window, JSON.stringify(state1), true);
}
function test_setWindowStateOverwrite() {
Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", false);
// We have our own progress listener for this test, which we'll attach before our state is set
let progressListener = {
onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
// We only care about load events when the tab still has
// __SS_restoreState == TAB_STATE_RESTORING on it.
// Since our listener is attached before the sessionstore one, this works out.
if (aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
test_setWindowStateOverwrite_progressCallback(aBrowser);
}
}
// We'll use 2 states so that we can make sure calling setWindowState doesn't
// wipe out currently restoring data.
let state1 = { windows: [{ tabs: [
{ entries: [{ url: "http://example.com#1" }] },
{ entries: [{ url: "http://example.com#2" }] },
{ entries: [{ url: "http://example.com#3" }] },
{ entries: [{ url: "http://example.com#4" }] },
{ entries: [{ url: "http://example.com#5" }] },
] }] };
let state2 = { windows: [{ tabs: [
{ entries: [{ url: "http://example.org#1" }] },
{ entries: [{ url: "http://example.org#2" }] },
{ entries: [{ url: "http://example.org#3" }] },
{ entries: [{ url: "http://example.org#4" }] },
{ entries: [{ url: "http://example.org#5" }] }
] }] };
let numTabs = 2 + state2.windows[0].tabs.length;
let loadCount = 0;
function test_setWindowStateOverwrite_progressCallback(aBrowser) {
loadCount++;
// When loadCount == 2, we'll also restore state2 into the window
if (loadCount == 2)
ss.setWindowState(window, JSON.stringify(state2), true);
if (loadCount < numTabs)
return;
// We don't actually care about load order in this test, just that they all
// do load.
is(loadCount, numTabs, "test_setWindowStateOverwrite: all tabs were restored");
// window.__SS_tabsToRestore isn't decremented until after the progress
// listener is called. Since we get in here before that, we still expect
// the count to be 1.
is(window.__SS_tabsToRestore, 1,
"test_setWindowStateOverwrite: window doesn't think there are more tabs to restore");
let count = countTabs();
is(count[0], 0,
"test_setWindowStateOverwrite: there are no tabs left needing restore");
// Remove the progress listener from this window, it will be removed from
// theWin when that window is closed (in setBrowserState).
window.gBrowser.removeTabsProgressListener(progressListener);
runNextTest();
}
window.gBrowser.addTabsProgressListener(progressListener);
ss.setWindowState(window, JSON.stringify(state1), true);
}
function test_setBrowserStateInterrupted() {
Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", false);
// We have our own progress listener for this test, which we'll attach before our state is set
let progressListener = {
onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
// We only care about load events when the tab still has
// __SS_restoreState == TAB_STATE_RESTORING on it.
// Since our listener is attached before the sessionstore one, this works out.
if (aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
test_setBrowserStateInterrupted_progressCallback(aBrowser);
}
}
// The first state will be loaded using setBrowserState, followed by the 2nd
// state also being loaded using setBrowserState, interrupting the first restore.
let state1 = { windows: [
{
tabs: [
{ entries: [{ url: "http://example.org#1" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org#2" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org#3" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org#4" }], extData: { "uniq": r() } }
],
selected: 1
},
{
tabs: [
{ entries: [{ url: "http://example.com#1" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com#2" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com#3" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com#4" }], extData: { "uniq": r() } },
],
selected: 3
}
] };
let state2 = { windows: [
{
tabs: [
{ entries: [{ url: "http://example.org#5" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org#6" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org#7" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org#8" }], extData: { "uniq": r() } }
],
selected: 3
},
{
tabs: [
{ entries: [{ url: "http://example.com#5" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com#6" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com#7" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com#8" }], extData: { "uniq": r() } },
],
selected: 1
}
] };
// interruptedAfter will be set after the selected tab from each window have loaded.
let interruptedAfter = 0;
let loadedWindow1 = false;
let loadedWindow2 = false;
let numTabs = state2.windows[0].tabs.length + state2.windows[1].tabs.length;
let loadCount = 0;
function test_setBrowserStateInterrupted_progressCallback(aBrowser) {
loadCount++;
if (aBrowser.currentURI.spec == state1.windows[0].tabs[2].entries[0].url)
loadedWindow1 = true;
if (aBrowser.currentURI.spec == state1.windows[1].tabs[0].entries[0].url)
loadedWindow2 = true;
if (!interruptedAfter && loadedWindow1 && loadedWindow2) {
interruptedAfter = loadCount;
ss.setBrowserState(JSON.stringify(state2));
return;
}
if (loadCount < numTabs + interruptedAfter)
return;
// We don't actually care about load order in this test, just that they all
// do load.
is(loadCount, numTabs + interruptedAfter,
"test_setBrowserStateInterrupted: all tabs were restored");
let count = countTabs();
is(count[0], 0,
"test_setBrowserStateInterrupted: there are no tabs left needing restore");
// Remove the progress listener from this window, it will be removed from
// theWin when that window is closed (in setBrowserState).
window.gBrowser.removeTabsProgressListener(progressListener);
Services.ww.unregisterNotification(windowObserver);
runNextTest();
}
// We also want to catch the extra windows (there should be 2), so we need to observe domwindowopened
function windowObserver(aSubject, aTopic, aData) {
let theWin = aSubject.QueryInterface(Ci.nsIDOMWindow);
if (aTopic == "domwindowopened") {
theWin.addEventListener("load", function() {
theWin.removeEventListener("load", arguments.callee, false);
Services.ww.unregisterNotification(windowObserver);
theWin.gBrowser.addTabsProgressListener(progressListener);
}, false);
}
}
Services.ww.registerNotification(windowObserver);
window.gBrowser.addTabsProgressListener(progressListener);
ss.setBrowserState(JSON.stringify(state1));
}
function test_reload() {
// Set the pref to true so we know exactly how many tabs should be restoring at
// any given time. This guarantees that a finishing load won't start another.
Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", true);
// We have our own progress listener for this test, which we'll attach before our state is set
let progressListener = {
onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
if (aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
test_reload_progressCallback(aBrowser);
}
}
let state = { windows: [{ tabs: [
{ entries: [{ url: "http://example.org/#1" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#2" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#3" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#4" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#5" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#6" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#7" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#8" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#9" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#10" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#11" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#12" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#13" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#14" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#15" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#16" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#17" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#18" }], extData: { "uniq": r() } }
], selected: 1 }] };
let loadCount = 0;
function test_reload_progressCallback(aBrowser) {
loadCount++;
is(aBrowser.currentURI.spec, state.windows[0].tabs[loadCount - 1].entries[0].url,
"test_reload: load " + loadCount + " - browser loaded correct url");
if (loadCount <= state.windows[0].tabs.length) {
// double check that this tab was the right one
let expectedData = state.windows[0].tabs[loadCount - 1].extData.uniq;
let tab;
for (let i = 0; i < window.gBrowser.tabs.length; i++) {
if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser)
tab = window.gBrowser.tabs[i];
}
is(ss.getTabValue(tab, "uniq"), expectedData,
"test_reload: load " + loadCount + " - correct tab was restored");
if (loadCount == state.windows[0].tabs.length) {
window.gBrowser.removeTabsProgressListener(progressListener);
executeSoon(function() {
_test_reloadAfter("test_reloadReload", state, runNextTest);
});
}
else {
// reload the next tab
window.gBrowser.reloadTab(window.gBrowser.tabs[loadCount]);
}
}
}
window.gBrowser.addTabsProgressListener(progressListener);
ss.setBrowserState(JSON.stringify(state));
}
// This doesn't actually test anything, just does a cascaded restore with default
// settings. This really just sets up to test that reloads work.
function test_reloadCascadeSetup() {
Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", false);
// We have our own progress listener for this test, which we'll attach before our state is set
let progressListener = {
onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
if (aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
test_cascadeReloadSetup_progressCallback();
}
}
let state = { windows: [{ tabs: [
{ entries: [{ url: "http://example.com/#1" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com/#2" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com/#3" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com/#4" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com/#5" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com/#6" }], extData: { "uniq": r() } }
] }] };
let loadCount = 0;
function test_cascadeReloadSetup_progressCallback() {
loadCount++;
if (loadCount < state.windows[0].tabs.length)
return;
window.gBrowser.removeTabsProgressListener(progressListener);
executeSoon(function() {
_test_reloadAfter("test_reloadCascade", state, runNextTest);
});
}
// This progress listener will get attached before the listener in session store.
window.gBrowser.addTabsProgressListener(progressListener);
ss.setBrowserState(JSON.stringify(state));
}
// This is a generic function that will attempt to reload each test. We do this
// a couple times, so make it utilitarian.
// This test expects that aState contains a single window and that each tab has
// a unique extData value eg. { "uniq": value }.
function _test_reloadAfter(aTestName, aState, aCallback) {
info("starting " + aTestName);
let progressListener = {
onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
test_reloadAfter_progressCallback(aBrowser);
}
}
// Simulate a left mouse button click with no modifiers, which is what
// Command-R, or clicking reload does.
let fakeEvent = {
button: 0,
metaKey: false,
altKey: false,
ctrlKey: false,
shiftKey: false,
}
let loadCount = 0;
function test_reloadAfter_progressCallback(aBrowser) {
loadCount++;
if (loadCount <= aState.windows[0].tabs.length) {
// double check that this tab was the right one
let expectedData = aState.windows[0].tabs[loadCount - 1].extData.uniq;
let tab;
for (let i = 0; i < window.gBrowser.tabs.length; i++) {
if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser)
tab = window.gBrowser.tabs[i];
}
is(ss.getTabValue(tab, "uniq"), expectedData,
aTestName + ": load " + loadCount + " - correct tab was reloaded");
if (loadCount == aState.windows[0].tabs.length) {
window.gBrowser.removeTabsProgressListener(progressListener);
aCallback();
}
else {
// reload the next tab
window.gBrowser.selectTabAtIndex(loadCount);
BrowserReloadOrDuplicate(fakeEvent);
}
}
}
window.gBrowser.addTabsProgressListener(progressListener);
BrowserReloadOrDuplicate(fakeEvent);
}
// This test ensures that app tabs are restored regardless of restore_on_demand
function test_apptabs_only() {
// Set the pref to true so we know exactly how many tabs should be restoring at
// any given time. This guarantees that a finishing load won't start another.
Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", true);
// We have our own progress listener for this test, which we'll attach before our state is set
let progressListener = {
onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
if (aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
test_apptabs_only_progressCallback(aBrowser);
}
}
let state = { windows: [{ tabs: [
{ entries: [{ url: "http://example.org/#1" }], extData: { "uniq": r() }, pinned: true },
{ entries: [{ url: "http://example.org/#2" }], extData: { "uniq": r() }, pinned: true },
{ entries: [{ url: "http://example.org/#3" }], extData: { "uniq": r() }, pinned: true },
{ entries: [{ url: "http://example.org/#4" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#5" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#6" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#7" }], extData: { "uniq": r() } },
], selected: 5 }] };
let loadCount = 0;
function test_apptabs_only_progressCallback(aBrowser) {
loadCount++;
// We'll make sure that the loads we get come from pinned tabs or the
// the selected tab.
// get the tab
let tab;
for (let i = 0; i < window.gBrowser.tabs.length; i++) {
if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser)
tab = window.gBrowser.tabs[i];
}
ok(tab.pinned || gBrowser.selectedTab == tab,
"test_apptabs_only: load came from pinned or selected tab");
// We should get 4 loads: 3 app tabs + 1 normal selected tab
if (loadCount < 4)
return;
window.gBrowser.removeTabsProgressListener(progressListener);
runNextTest();
}
window.gBrowser.addTabsProgressListener(progressListener);
ss.setBrowserState(JSON.stringify(state));
}
// This test ensures that app tabs are not restored when restore_pinned_tabs_on_demand is set
function test_restore_apptabs_ondemand() {
Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", true);
Services.prefs.setBoolPref("browser.sessionstore.restore_pinned_tabs_on_demand", true);
// We have our own progress listener for this test, which we'll attach before our state is set
let progressListener = {
onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
if (aBrowser.__SS_restoreState == TAB_STATE_RESTORING &&
aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
test_restore_apptabs_ondemand_progressCallback(aBrowser);
}
}
let state = { windows: [{ tabs: [
{ entries: [{ url: "http://example.org/#1" }], extData: { "uniq": r() }, pinned: true },
{ entries: [{ url: "http://example.org/#2" }], extData: { "uniq": r() }, pinned: true },
{ entries: [{ url: "http://example.org/#3" }], extData: { "uniq": r() }, pinned: true },
{ entries: [{ url: "http://example.org/#4" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#5" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#6" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#7" }], extData: { "uniq": r() } },
], selected: 5 }] };
let loadCount = 0;
let nextTestTimer;
function test_restore_apptabs_ondemand_progressCallback(aBrowser) {
loadCount++;
// get the tab
let tab;
for (let i = 0; i < window.gBrowser.tabs.length; i++) {
if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser)
tab = window.gBrowser.tabs[i];
}
// Check that the load only comes from the selected tab.
ok(gBrowser.selectedTab == tab,
"test_restore_apptabs_ondemand: load came from selected tab");
// We should get only 1 load: the selected tab
if (loadCount == 1) {
nextTestTimer = setTimeout(nextTest, 1000);
return;
}
else if (loadCount > 1) {
clearTimeout(nextTestTimer);
}
function nextTest() {
window.gBrowser.removeTabsProgressListener(progressListener);
runNextTest();
}
nextTest();
}
window.gBrowser.addTabsProgressListener(progressListener);
ss.setBrowserState(JSON.stringify(state));
}
function countTabs() {
let needsRestore = 0,
isRestoring = 0,
wasRestored = 0;
let windowsEnum = Services.wm.getEnumerator("navigator:browser");
while (windowsEnum.hasMoreElements()) {
let window = windowsEnum.getNext();
if (window.closed)
continue;
for (let i = 0; i < window.gBrowser.tabs.length; i++) {
let browser = window.gBrowser.tabs[i].linkedBrowser;
if (browser.__SS_restoreState == TAB_STATE_RESTORING)
isRestoring++;
else if (browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE)
needsRestore++;
else
wasRestored++;
}
}
return [needsRestore, isRestoring, wasRestored];
}

View File

@ -0,0 +1,69 @@
/* 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/. */
const PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
let stateBackup = ss.getBrowserState();
function test() {
waitForExplicitFinish();
Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, false);
registerCleanupFunction(function () {
Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
});
// The first window will be put into the already open window and the second
// window will be opened with _openWindowWithState, which is the source of the problem.
let state = { windows: [
{
tabs: [
{ entries: [{ url: "http://example.org#0" }], extData: { "uniq": r() } }
],
selected: 1
},
{
tabs: [
{ entries: [{ url: "http://example.com#1" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com#2" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com#3" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com#4" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com#5" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com#6" }], extData: { "uniq": r() } }
],
selected: 4
}
] };
let numTabs = state.windows[0].tabs.length + state.windows[1].tabs.length;
let loadCount = 0;
gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
if (++loadCount == numTabs) {
// We don't actually care about load order in this test, just that they all
// do load.
is(loadCount, numTabs, "all tabs were restored");
is(aNeedRestore, 0, "there are no tabs left needing restore");
gProgressListener.unsetCallback();
executeSoon(function () {
closeAllButPrimaryWindow();
waitForBrowserState(JSON.parse(stateBackup), finish);
});
}
});
// We also want to catch the 2nd window, so we need to observe domwindowopened
Services.ww.registerNotification(function observer(aSubject, aTopic, aData) {
if (aTopic == "domwindowopened") {
let win = aSubject.QueryInterface(Ci.nsIDOMWindow);
win.addEventListener("load", function onLoad() {
win.removeEventListener("load", onLoad);
Services.ww.unregisterNotification(observer);
win.gBrowser.addTabsProgressListener(gProgressListener);
});
}
});
ss.setBrowserState(JSON.stringify(state));
}

View File

@ -0,0 +1,137 @@
/* 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/. */
const PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
let stateBackup = ss.getBrowserState();
function test() {
waitForExplicitFinish();
Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, true);
registerCleanupFunction(function () {
Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
});
let state = { windows: [{ tabs: [
{ entries: [{ url: "http://example.org/#1" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#2" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#3" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#4" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#5" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#6" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#7" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#8" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#9" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#10" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#11" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#12" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#13" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#14" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#15" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#16" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#17" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org/#18" }], extData: { "uniq": r() } }
], selected: 1 }] };
let loadCount = 0;
gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
loadCount++;
is(aBrowser.currentURI.spec, state.windows[0].tabs[loadCount - 1].entries[0].url,
"load " + loadCount + " - browser loaded correct url");
if (loadCount <= state.windows[0].tabs.length) {
// double check that this tab was the right one
let expectedData = state.windows[0].tabs[loadCount - 1].extData.uniq;
let tab;
for (let i = 0; i < window.gBrowser.tabs.length; i++) {
if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser)
tab = window.gBrowser.tabs[i];
}
is(ss.getTabValue(tab, "uniq"), expectedData,
"load " + loadCount + " - correct tab was restored");
if (loadCount == state.windows[0].tabs.length) {
gProgressListener.unsetCallback();
executeSoon(function () {
reloadAllTabs(state, function () {
waitForBrowserState(JSON.parse(stateBackup), testCascade);
});
});
} else {
// reload the next tab
window.gBrowser.reloadTab(window.gBrowser.tabs[loadCount]);
}
}
});
ss.setBrowserState(JSON.stringify(state));
}
function testCascade() {
Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, false);
let state = { windows: [{ tabs: [
{ entries: [{ url: "http://example.com/#1" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com/#2" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com/#3" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com/#4" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com/#5" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com/#6" }], extData: { "uniq": r() } }
] }] };
let loadCount = 0;
gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
if (++loadCount < state.windows[0].tabs.length) {
return;
}
gProgressListener.unsetCallback();
executeSoon(function () {
reloadAllTabs(state, function () {
waitForBrowserState(JSON.parse(stateBackup), finish);
});
});
});
ss.setBrowserState(JSON.stringify(state));
}
function reloadAllTabs(aState, aCallback) {
// Simulate a left mouse button click with no modifiers, which is what
// Command-R, or clicking reload does.
let fakeEvent = {
button: 0,
metaKey: false,
altKey: false,
ctrlKey: false,
shiftKey: false
};
let loadCount = 0;
gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
if (++loadCount <= aState.windows[0].tabs.length) {
// double check that this tab was the right one
let expectedData = aState.windows[0].tabs[loadCount - 1].extData.uniq;
let tab;
for (let i = 0; i < window.gBrowser.tabs.length; i++) {
if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser)
tab = window.gBrowser.tabs[i];
}
is(ss.getTabValue(tab, "uniq"), expectedData,
"load " + loadCount + " - correct tab was reloaded");
if (loadCount == aState.windows[0].tabs.length) {
gProgressListener.unsetCallback();
executeSoon(aCallback);
} else {
// reload the next tab
window.gBrowser.selectTabAtIndex(loadCount);
BrowserReloadOrDuplicate(fakeEvent);
}
}
}, false);
BrowserReloadOrDuplicate(fakeEvent);
}

View File

@ -0,0 +1,68 @@
/* 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/. */
const PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
let stateBackup = ss.getBrowserState();
function test() {
waitForExplicitFinish();
Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, true);
registerCleanupFunction(function () {
Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
});
let state = { windows: [{ tabs: [
{ entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org" }], extData: { "uniq": r() } }
], selected: 1 }] };
let expectedCounts = [
[5, 1, 0],
[4, 1, 1],
[3, 1, 2],
[2, 1, 3],
[1, 1, 4],
[0, 1, 5]
];
let tabOrder = [0, 5, 1, 4, 3, 2];
let loadCount = 0;
gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
loadCount++;
let expected = expectedCounts[loadCount - 1];
is(aNeedRestore, expected[0], "load " + loadCount + " - # tabs that need to be restored");
is(aRestoring, expected[1], "load " + loadCount + " - # tabs that are restoring");
is(aRestored, expected[2], "load " + loadCount + " - # tabs that has been restored");
if (loadCount < state.windows[0].tabs.length) {
// double check that this tab was the right one
let expectedData = state.windows[0].tabs[tabOrder[loadCount - 1]].extData.uniq;
let tab;
for (let i = 0; i < window.gBrowser.tabs.length; i++) {
if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser)
tab = window.gBrowser.tabs[i];
}
is(ss.getTabValue(tab, "uniq"), expectedData,
"load " + loadCount + " - correct tab was restored");
// select the next tab
window.gBrowser.selectTabAtIndex(tabOrder[loadCount]);
} else {
gProgressListener.unsetCallback();
executeSoon(function () {
waitForBrowserState(JSON.parse(stateBackup), finish);
});
}
});
ss.setBrowserState(JSON.stringify(state));
}

View File

@ -0,0 +1,62 @@
/* 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/. */
const PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
let stateBackup = ss.getBrowserState();
function test() {
waitForExplicitFinish();
Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, false);
registerCleanupFunction(function () {
Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
});
// We'll use 2 states so that we can make sure calling setWindowState doesn't
// wipe out currently restoring data.
let state1 = { windows: [{ tabs: [
{ entries: [{ url: "http://example.com#1" }] },
{ entries: [{ url: "http://example.com#2" }] },
{ entries: [{ url: "http://example.com#3" }] },
{ entries: [{ url: "http://example.com#4" }] },
{ entries: [{ url: "http://example.com#5" }] },
] }] };
let state2 = { windows: [{ tabs: [
{ entries: [{ url: "http://example.org#1" }] },
{ entries: [{ url: "http://example.org#2" }] },
{ entries: [{ url: "http://example.org#3" }] },
{ entries: [{ url: "http://example.org#4" }] },
{ entries: [{ url: "http://example.org#5" }] }
] }] };
let numTabs = state1.windows[0].tabs.length + state2.windows[0].tabs.length;
let loadCount = 0;
gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
// When loadCount == 2, we'll also restore state2 into the window
if (++loadCount == 2) {
ss.setWindowState(window, JSON.stringify(state2), false);
}
if (loadCount < numTabs) {
return;
}
// We don't actually care about load order in this test, just that they all
// do load.
is(loadCount, numTabs, "test_setWindowStateNoOverwrite: all tabs were restored");
// window.__SS_tabsToRestore isn't decremented until after the progress
// listener is called. Since we get in here before that, we still expect
// the count to be 1.
is(window.__SS_tabsToRestore, 1, "window doesn't think there are more tabs to restore");
is(aNeedRestore, 0, "there are no tabs left needing restore");
gProgressListener.unsetCallback();
executeSoon(function () {
waitForBrowserState(JSON.parse(stateBackup), finish);
});
});
ss.setWindowState(window, JSON.stringify(state1), true);
}

View File

@ -0,0 +1,62 @@
/* 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/. */
const PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand";
let stateBackup = ss.getBrowserState();
function test() {
waitForExplicitFinish();
Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, false);
registerCleanupFunction(function () {
Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND);
});
// We'll use 2 states so that we can make sure calling setWindowState doesn't
// wipe out currently restoring data.
let state1 = { windows: [{ tabs: [
{ entries: [{ url: "http://example.com#1" }] },
{ entries: [{ url: "http://example.com#2" }] },
{ entries: [{ url: "http://example.com#3" }] },
{ entries: [{ url: "http://example.com#4" }] },
{ entries: [{ url: "http://example.com#5" }] },
] }] };
let state2 = { windows: [{ tabs: [
{ entries: [{ url: "http://example.org#1" }] },
{ entries: [{ url: "http://example.org#2" }] },
{ entries: [{ url: "http://example.org#3" }] },
{ entries: [{ url: "http://example.org#4" }] },
{ entries: [{ url: "http://example.org#5" }] }
] }] };
let numTabs = 2 + state2.windows[0].tabs.length;
let loadCount = 0;
gProgressListener.setCallback(function (aBrowser, aNeedRestore, aRestoring, aRestored) {
// When loadCount == 2, we'll also restore state2 into the window
if (++loadCount == 2) {
ss.setWindowState(window, JSON.stringify(state2), true);
}
if (loadCount < numTabs) {
return;
}
// We don't actually care about load order in this test, just that they all
// do load.
is(loadCount, numTabs, "all tabs were restored");
// window.__SS_tabsToRestore isn't decremented until after the progress
// listener is called. Since we get in here before that, we still expect
// the count to be 1.
is(window.__SS_tabsToRestore, 1, "window doesn't think there are more tabs to restore");
is(aNeedRestore, 0, "there are no tabs left needing restore");
gProgressListener.unsetCallback();
executeSoon(function () {
waitForBrowserState(JSON.parse(stateBackup), finish);
});
});
ss.setWindowState(window, JSON.stringify(state1), true);
}

View File

@ -1,9 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const TAB_STATE_NEEDS_RESTORE = 1;
const TAB_STATE_RESTORING = 2;
let state = {windows:[{tabs:[
{entries:[{url:"http://example.com#1"}]},
{entries:[{url:"http://example.com#2"}]},

View File

@ -2,9 +2,6 @@
* 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/. */
const TAB_STATE_NEEDS_RESTORE = 1;
const TAB_STATE_RESTORING = 2;
let stateBackup = ss.getBrowserState();
function cleanup() {

View File

@ -2,9 +2,6 @@
* 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/. */
const TAB_STATE_NEEDS_RESTORE = 1;
const TAB_STATE_RESTORING = 2;
let stateBackup = ss.getBrowserState();
function cleanup() {

View File

@ -1,9 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const TAB_STATE_NEEDS_RESTORE = 1;
const TAB_STATE_RESTORING = 2;
let stateBackup = ss.getBrowserState();
let statePinned = {windows:[{tabs:[

View File

@ -1,8 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const TAB_STATE_NEEDS_RESTORE = 1;
let tabState = {
entries: [{url: "data:text/html,<input%20id='foo'>", formdata: { id: { "foo": "bar" } } }]
};

View File

@ -2,6 +2,9 @@
* 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/. */
const TAB_STATE_NEEDS_RESTORE = 1;
const TAB_STATE_RESTORING = 2;
let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
// Some tests here assume that all restored tabs are loaded without waiting for
@ -200,3 +203,77 @@ var gUniqueCounter = 0;
function r() {
return Date.now() + "-" + (++gUniqueCounter);
}
function BrowserWindowIterator() {
let windowsEnum = Services.wm.getEnumerator("navigator:browser");
while (windowsEnum.hasMoreElements()) {
let currentWindow = windowsEnum.getNext();
if (!currentWindow.closed) {
yield currentWindow;
}
}
}
let gProgressListener = {
_callback: null,
_checkRestoreState: true,
setCallback: function gProgressListener_setCallback(aCallback, aCheckRestoreState = true) {
if (!this._callback) {
window.gBrowser.addTabsProgressListener(this);
}
this._callback = aCallback;
this._checkRestoreState = aCheckRestoreState;
},
unsetCallback: function gProgressListener_unsetCallback() {
if (this._callback) {
this._callback = null;
window.gBrowser.removeTabsProgressListener(this);
}
},
onStateChange:
function gProgressListener_onStateChange(aBrowser, aWebProgress, aRequest,
aStateFlags, aStatus) {
if ((!this._checkRestoreState ||
aBrowser.__SS_restoreState == TAB_STATE_RESTORING) &&
aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) {
let args = [aBrowser].concat(this._countTabs());
this._callback.apply(this, args);
}
},
_countTabs: function gProgressListener_countTabs() {
let needsRestore = 0, isRestoring = 0, wasRestored = 0;
for (let win in BrowserWindowIterator()) {
for (let i = 0; i < win.gBrowser.tabs.length; i++) {
let browser = win.gBrowser.tabs[i].linkedBrowser;
if (browser.__SS_restoreState == TAB_STATE_RESTORING)
isRestoring++;
else if (browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE)
needsRestore++;
else
wasRestored++;
}
}
return [needsRestore, isRestoring, wasRestored];
}
};
registerCleanupFunction(function () {
gProgressListener.unsetCallback();
});
// Close everything but our primary window. We can't use waitForFocus()
// because apparently it's buggy. See bug 599253.
function closeAllButPrimaryWindow() {
for (let win in BrowserWindowIterator()) {
if (win != window) {
win.close();
}
}
}

View File

@ -445,27 +445,13 @@ let PageThumbsExpiration = {
}
},
expireThumbnails: function Expiration_expireThumbnails(aURLsToKeep) {
let keep = {};
// Transform all these URLs into file names.
for (let url of aURLsToKeep) {
keep[PageThumbsStorage.getLeafNameForURL(url)] = true;
}
let numFilesRemoved = 0;
let dir = PageThumbsStorage.getDirectory().path;
let msg = {type: "getFilesInDirectory", path: dir};
PageThumbsWorker.postMessage(msg, function (aData) {
let files = [file for (file of aData.result) if (!(file in keep))];
let maxFilesToRemove = Math.max(EXPIRATION_MIN_CHUNK_SIZE,
Math.round(files.length / 2));
let fileNames = files.slice(0, maxFilesToRemove);
let filePaths = [dir + "/" + fileName for (fileName of fileNames)];
PageThumbsWorker.postMessage({type: "removeFiles", paths: filePaths});
});
expireThumbnails: function Expiration_expireThumbnails(aURLsToKeep, aCallback) {
PageThumbsWorker.postMessage({
type: "expireFilesInDirectory",
minChunkSize: EXPIRATION_MIN_CHUNK_SIZE,
path: PageThumbsStorage.getDirectory().path,
filesToKeep: [PageThumbsStorage.getLeafNameForURL(url) for (url of aURLsToKeep)]
}, aCallback);
}
};

View File

@ -22,11 +22,8 @@ let PageThumbsWorker = {
case "removeFile":
data.result = this.removeFile(msg);
break;
case "removeFiles":
data.result = this.removeFiles(msg);
break;
case "getFilesInDirectory":
data.result = this.getFilesInDirectory(msg);
case "expireFilesInDirectory":
data.result = this.expireFilesInDirectory(msg);
break;
default:
data.result = false;
@ -37,20 +34,6 @@ let PageThumbsWorker = {
self.postMessage(data);
},
getFilesInDirectory: function Worker_getFilesInDirectory(msg) {
let iter = new OS.File.DirectoryIterator(msg.path);
let entries = [];
for (let entry in iter) {
if (!entry.isDir && !entry.isSymLink) {
entries.push(entry.name);
}
}
iter.close();
return entries;
},
removeFile: function Worker_removeFile(msg) {
try {
OS.File.remove(msg.path);
@ -60,16 +43,30 @@ let PageThumbsWorker = {
}
},
removeFiles: function Worker_removeFiles(msg) {
for (let file of msg.paths) {
try {
OS.File.remove(file);
} catch (e) {
// We couldn't remove the file for some reason.
// Let's just continue with the next one.
expireFilesInDirectory: function Worker_expireFilesInDirectory(msg) {
let entries = this.getFileEntriesInDirectory(msg.path, msg.filesToKeep);
let limit = Math.max(msg.minChunkSize, Math.round(entries.length / 2));
for (let entry of entries) {
this.removeFile(entry);
// Check if we reached the limit of files to remove.
if (--limit <= 0) {
break;
}
}
return true;
},
getFileEntriesInDirectory:
function Worker_getFileEntriesInDirectory(aPath, aSkipFiles) {
let skip = new Set(aSkipFiles);
let iter = new OS.File.DirectoryIterator(aPath);
return [entry
for (entry in iter)
if (!entry.isDir && !entry.isSymLink && !skip.has(entry.name))];
}
};

View File

@ -13,6 +13,7 @@ include $(topsrcdir)/config/rules.mk
_BROWSER_FILES = \
browser_thumbnails_capture.js \
browser_thumbnails_expiration.js \
browser_thumbnails_privacy.js \
browser_thumbnails_redirect.js \
browser_thumbnails_storage.js \

View File

@ -0,0 +1,78 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const URL = "http://mochi.test:8888/?t=" + Date.now();
const URL1 = URL + "#1";
const URL2 = URL + "#2";
const URL3 = URL + "#3";
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
let tmp = {};
Cc["@mozilla.org/moz/jssubscript-loader;1"]
.getService(Ci.mozIJSSubScriptLoader)
.loadSubScript("resource:///modules/PageThumbs.jsm", tmp);
const {EXPIRATION_MIN_CHUNK_SIZE, PageThumbsExpiration} = tmp;
function runTests() {
// Create three thumbnails.
createDummyThumbnail(URL1);
ok(thumbnailExists(URL1), "first thumbnail created");
createDummyThumbnail(URL2);
ok(thumbnailExists(URL2), "second thumbnail created");
createDummyThumbnail(URL3);
ok(thumbnailExists(URL3), "third thumbnail created");
// Remove the third thumbnail.
yield expireThumbnails([URL1, URL2]);
ok(thumbnailExists(URL1), "first thumbnail still exists");
ok(thumbnailExists(URL2), "second thumbnail still exists");
ok(!thumbnailExists(URL3), "third thumbnail has been removed");
// Remove the second thumbnail.
yield expireThumbnails([URL1]);
ok(thumbnailExists(URL1), "first thumbnail still exists");
ok(!thumbnailExists(URL2), "second thumbnail has been removed");
// Remove all thumbnails.
yield expireThumbnails([]);
ok(!thumbnailExists(URL1), "all thumbnails have been removed");
// Create some more files than the min chunk size.
let urls = [];
for (let i = 0; i < EXPIRATION_MIN_CHUNK_SIZE + 10; i++) {
urls.push(URL + "#dummy" + i);
}
urls.forEach(createDummyThumbnail);
ok(urls.every(thumbnailExists), "all dummy thumbnails created");
// Expire thumbnails and expect 10 remaining.
yield expireThumbnails([]);
let remainingURLs = [u for (u of urls) if (thumbnailExists(u))];
is(remainingURLs.length, 10, "10 dummy thumbnails remaining");
// Expire thumbnails again. All should be gone by now.
yield expireThumbnails([]);
remainingURLs = [u for (u of remainingURLs) if (thumbnailExists(u))];
is(remainingURLs.length, 0, "no dummy thumbnails remaining");
}
function createDummyThumbnail(aURL) {
let file = PageThumbsStorage.getFileForURL(aURL);
let fos = FileUtils.openSafeFileOutputStream(file);
let data = "dummy";
fos.write(data, data.length);
FileUtils.closeSafeFileOutputStream(fos);
}
function expireThumbnails(aKeep) {
PageThumbsExpiration.expireThumbnails(aKeep, function () {
executeSoon(next);
});
}

View File

@ -96,8 +96,7 @@ function createThumbnail() {
function whenFileExists(aCallback) {
let callback;
let file = PageThumbsStorage.getFileForURL(URL);
if (file.exists() && file.fileSize) {
if (thumbnailExists(URL)) {
callback = aCallback;
} else {
callback = function () whenFileExists(aCallback);

View File

@ -146,3 +146,12 @@ function checkCanvasColor(aContext, aRed, aGreen, aBlue, aMessage) {
let [r, g, b] = aContext.getImageData(0, 0, 1, 1).data;
ok(r == aRed && g == aGreen && b == aBlue, aMessage);
}
/**
* Checks if a thumbnail for the given URL exists.
* @param aURL The url associated to the thumbnail.
*/
function thumbnailExists(aURL) {
let file = PageThumbsStorage.getFileForURL(aURL);
return file.exists() && file.fileSize;
}

View File

@ -31,15 +31,6 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sour
var ArraySet = require('source-map/array-set').ArraySet;
var base64VLQ = require('source-map/base64-vlq');
// TODO: bug 673487
//
// Sometime in the future, if we decide we need to be able to query where in
// the generated source a piece of the original code came from, we may want to
// add a slot `_originalMappings` which would be an object keyed by the
// original source and whose value would be an array of mappings ordered by
// original line/col rather than generated (which is what we have now in
// `_generatedMappings`).
/**
* A SourceMapConsumer instance represents a parsed source map which we can
* query for information about the original file positions by giving it a file
@ -72,7 +63,7 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sour
function SourceMapConsumer(aSourceMap) {
var sourceMap = aSourceMap;
if (typeof aSourceMap === 'string') {
sourceMap = JSON.parse(aSourceMap);
sourceMap = JSON.parse(aSourceMap.replace(/^\)\]\}'/, ''));
}
var version = util.getArg(sourceMap, 'version');
@ -89,9 +80,9 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sour
this._names = ArraySet.fromArray(names);
this._sources = ArraySet.fromArray(sources);
// `this._generatedMappings` hold the parsed mapping coordinates from the
// source map's "mappings" attribute. Each object in the array is of the
// form
// `this._generatedMappings` and `this._originalMappings` hold the parsed
// mapping coordinates from the source map's "mappings" attribute. Each
// object in the array is of the form
//
// {
// generatedLine: The line number in the generated code,
@ -108,7 +99,12 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sour
//
// All properties except for `generatedLine` and `generatedColumn` can be
// `null`.
//
// `this._generatedMappings` is ordered by the generated positions.
//
// `this._originalMappings` is ordered by the original positions.
this._generatedMappings = [];
this._originalMappings = [];
this._parseMappings(mappings, sourceRoot);
}
@ -195,8 +191,65 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sour
}
this._generatedMappings.push(mapping);
this._originalMappings.push(mapping);
}
}
this._originalMappings.sort(this._compareOriginalPositions);
};
/**
* Comparator between two mappings where the original positions are compared.
*/
SourceMapConsumer.prototype._compareOriginalPositions =
function SourceMapConsumer_compareOriginalPositions(mappingA, mappingB) {
if (mappingA.source > mappingB.source) {
return 1;
}
else if (mappingA.source < mappingB.source) {
return -1;
}
else {
var cmp = mappingA.originalLine - mappingB.originalLine;
return cmp === 0
? mappingA.originalColumn - mappingB.originalColumn
: cmp;
}
};
/**
* Comparator between two mappings where the generated positions are compared.
*/
SourceMapConsumer.prototype._compareGeneratedPositions =
function SourceMapConsumer_compareGeneratedPositions(mappingA, mappingB) {
var cmp = mappingA.generatedLine - mappingB.generatedLine;
return cmp === 0
? mappingA.generatedColumn - mappingB.generatedColumn
: cmp;
};
/**
* Find the mapping that best matches the hypothetical "needle" mapping that
* we are searching for in the given "haystack" of mappings.
*/
SourceMapConsumer.prototype._findMapping =
function SourceMapConsumer_findMapping(aNeedle, aMappings, aLineName,
aColumnName, aComparator) {
// To return the position we are searching for, we must first find the
// mapping for the given position and then return the opposite position it
// points to. Because the mappings are sorted, we can use binary search to
// find the best mapping.
if (aNeedle[aLineName] <= 0) {
throw new TypeError('Line must be greater than or equal to 1, got '
+ aNeedle[aLineName]);
}
if (aNeedle[aColumnName] < 0) {
throw new TypeError('Column must be greater than or equal to 0, got '
+ aNeedle[aColumnName]);
}
return binarySearch.search(aNeedle, aMappings, aComparator);
};
/**
@ -216,35 +269,16 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sour
*/
SourceMapConsumer.prototype.originalPositionFor =
function SourceMapConsumer_originalPositionFor(aArgs) {
// To return the original position, we must first find the mapping for the
// given generated position and then return the original position it
// points to. Because the mappings are sorted by generated line/column, we
// can use binary search to find the best mapping.
// To perform a binary search on the mappings, we must be able to compare
// two mappings.
function compare(mappingA, mappingB) {
var cmp = mappingA.generatedLine - mappingB.generatedLine;
return cmp === 0
? mappingA.generatedColumn - mappingB.generatedColumn
: cmp;
}
// This is the mock of the mapping we are looking for: the needle in the
// haystack of mappings.
var needle = {
generatedLine: util.getArg(aArgs, 'line'),
generatedColumn: util.getArg(aArgs, 'column')
};
if (needle.generatedLine <= 0) {
throw new TypeError('Line must be greater than or equal to 1.');
}
if (needle.generatedColumn < 0) {
throw new TypeError('Column must be greater than or equal to 0.');
}
var mapping = binarySearch.search(needle, this._generatedMappings, compare);
var mapping = this._findMapping(needle,
this._generatedMappings,
"generatedLine",
"generatedColumn",
this._compareGeneratedPositions)
if (mapping) {
return {
@ -261,7 +295,47 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sour
column: null,
name: null
};
};
/**
* Returns the generated line and column information for the original source,
* line, and column positions provided. The only argument is an object with
* the following properties:
*
* - source: The filename of the original source.
* - line: The line number in the original source.
* - column: The column number in the original source.
*
* and an object is returned with the following properties:
*
* - line: The line number in the generated source, or null.
* - column: The column number in the generated source, or null.
*/
SourceMapConsumer.prototype.generatedPositionFor =
function SourceMapConsumer_generatedPositionFor(aArgs) {
var needle = {
source: util.getArg(aArgs, 'source'),
originalLine: util.getArg(aArgs, 'line'),
originalColumn: util.getArg(aArgs, 'column')
};
var mapping = this._findMapping(needle,
this._originalMappings,
"originalLine",
"originalColumn",
this._compareOriginalPositions)
if (mapping) {
return {
line: util.getArg(mapping, 'generatedLine', null),
column: util.getArg(mapping, 'generatedColumn', null)
};
}
return {
line: null,
column: null
};
};
exports.SourceMapConsumer = SourceMapConsumer;
@ -885,7 +959,9 @@ define('source-map/source-node', ['require', 'exports', 'module' , 'source-map/s
}, this);
}
else if (aChunk instanceof SourceNode || typeof aChunk === "string") {
this.children.push(aChunk);
if (aChunk) {
this.children.push(aChunk);
}
}
else {
throw new TypeError(
@ -973,7 +1049,7 @@ define('source-map/source-node', ['require', 'exports', 'module' , 'source-map/s
lastChild.replaceRight(aPattern, aReplacement);
}
else if (typeof lastChild === 'string') {
this.children[this.children.lenth - 1] = lastChild.replace(aPattern, aReplacement);
this.children[this.children.length - 1] = lastChild.replace(aPattern, aReplacement);
}
else {
this.children.push(''.replace(aPattern, aReplacement));

View File

@ -13,7 +13,7 @@
*/
Components.utils.import('resource://gre/modules/devtools/Require.jsm');
Components.utils.import('resource:///modules/devtools/SourceMap.jsm');
Components.utils.import('resource://gre/modules/devtools/SourceMap.jsm');
let EXPORTED_SYMBOLS = [ "define", "runSourceMapTests" ];
/* -*- Mode: js; js-indent-level: 2; -*- */
@ -109,23 +109,40 @@ define('test/source-map/util', ['require', 'exports', 'module' ], function(requi
};
function assertMapping(generatedLine, generatedColumn, originalSource,
originalLine, originalColumn, name, map, assert) {
var mapping = map.originalPositionFor({
line: generatedLine,
column: generatedColumn
});
assert.equal(mapping.name, name,
'Incorrect name, expected ' + JSON.stringify(name)
+ ', got ' + JSON.stringify(mapping.name));
assert.equal(mapping.line, originalLine,
'Incorrect line, expected ' + JSON.stringify(originalLine)
+ ', got ' + JSON.stringify(mapping.line));
assert.equal(mapping.column, originalColumn,
'Incorrect column, expected ' + JSON.stringify(originalColumn)
+ ', got ' + JSON.stringify(mapping.column));
assert.equal(mapping.source, originalSource,
'Incorrect source, expected ' + JSON.stringify(originalSource)
+ ', got ' + JSON.stringify(mapping.source));
originalLine, originalColumn, name, map, assert,
dontTestGenerated, dontTestOriginal) {
if (!dontTestOriginal) {
var origMapping = map.originalPositionFor({
line: generatedLine,
column: generatedColumn
});
assert.equal(origMapping.name, name,
'Incorrect name, expected ' + JSON.stringify(name)
+ ', got ' + JSON.stringify(origMapping.name));
assert.equal(origMapping.line, originalLine,
'Incorrect line, expected ' + JSON.stringify(originalLine)
+ ', got ' + JSON.stringify(origMapping.line));
assert.equal(origMapping.column, originalColumn,
'Incorrect column, expected ' + JSON.stringify(originalColumn)
+ ', got ' + JSON.stringify(origMapping.column));
assert.equal(origMapping.source, originalSource,
'Incorrect source, expected ' + JSON.stringify(originalSource)
+ ', got ' + JSON.stringify(origMapping.source));
}
if (!dontTestGenerated) {
var genMapping = map.generatedPositionFor({
source: originalSource,
line: originalLine,
column: originalColumn
});
assert.equal(genMapping.line, generatedLine,
'Incorrect line, expected ' + JSON.stringify(generatedLine)
+ ', got ' + JSON.stringify(genMapping.line));
assert.equal(genMapping.column, generatedColumn,
'Incorrect column, expected ' + JSON.stringify(generatedColumn)
+ ', got ' + JSON.stringify(genMapping.column));
}
}
exports.assertMapping = assertMapping;

View File

@ -56,14 +56,22 @@ define("test/source-map/test-dog-fooding", ["require", "exports", "module"], fun
util.assertMapping(5, 2, '/wu/tang/gza.coffee', 4, 0, null, smc, assert);
// Fuzzy
util.assertMapping(2, 0, null, null, null, null, smc, assert);
util.assertMapping(2, 9, '/wu/tang/gza.coffee', 1, 0, null, smc, assert);
util.assertMapping(3, 0, '/wu/tang/gza.coffee', 1, 0, null, smc, assert);
util.assertMapping(3, 9, '/wu/tang/gza.coffee', 2, 0, null, smc, assert);
util.assertMapping(4, 0, '/wu/tang/gza.coffee', 2, 0, null, smc, assert);
util.assertMapping(4, 9, '/wu/tang/gza.coffee', 3, 0, null, smc, assert);
util.assertMapping(5, 0, '/wu/tang/gza.coffee', 3, 0, null, smc, assert);
util.assertMapping(5, 9, '/wu/tang/gza.coffee', 4, 0, null, smc, assert);
// Original to generated
util.assertMapping(2, 0, null, null, null, null, smc, assert, true);
util.assertMapping(2, 9, '/wu/tang/gza.coffee', 1, 0, null, smc, assert, true);
util.assertMapping(3, 0, '/wu/tang/gza.coffee', 1, 0, null, smc, assert, true);
util.assertMapping(3, 9, '/wu/tang/gza.coffee', 2, 0, null, smc, assert, true);
util.assertMapping(4, 0, '/wu/tang/gza.coffee', 2, 0, null, smc, assert, true);
util.assertMapping(4, 9, '/wu/tang/gza.coffee', 3, 0, null, smc, assert, true);
util.assertMapping(5, 0, '/wu/tang/gza.coffee', 3, 0, null, smc, assert, true);
util.assertMapping(5, 9, '/wu/tang/gza.coffee', 4, 0, null, smc, assert, true);
// Generated to original
util.assertMapping(2, 2, '/wu/tang/gza.coffee', 1, 1, null, smc, assert, null, true);
util.assertMapping(3, 2, '/wu/tang/gza.coffee', 2, 3, null, smc, assert, null, true);
util.assertMapping(4, 2, '/wu/tang/gza.coffee', 3, 6, null, smc, assert, null, true);
util.assertMapping(5, 2, '/wu/tang/gza.coffee', 4, 9, null, smc, assert, null, true);
};
});

View File

@ -61,12 +61,24 @@ define("test/source-map/test-source-map-consumer", ["require", "exports", "modul
util.assertMapping(2, 28, '/the/root/two.js', 2, 10, 'n', map, assert);
};
exports['test mapping tokens back fuzzy'] = function (assert, util) {
exports['test mapping tokens fuzzy'] = function (assert, util) {
var map = new SourceMapConsumer(util.testMap);
util.assertMapping(1, 20, '/the/root/one.js', 1, 21, 'bar', map, assert);
util.assertMapping(1, 30, '/the/root/one.js', 2, 10, 'baz', map, assert);
util.assertMapping(2, 12, '/the/root/two.js', 1, 11, null, map, assert);
// Finding original positions
util.assertMapping(1, 20, '/the/root/one.js', 1, 21, 'bar', map, assert, true);
util.assertMapping(1, 30, '/the/root/one.js', 2, 10, 'baz', map, assert, true);
util.assertMapping(2, 12, '/the/root/two.js', 1, 11, null, map, assert, true);
// Finding generated positions
util.assertMapping(1, 18, '/the/root/one.js', 1, 22, 'bar', map, assert, null, true);
util.assertMapping(1, 28, '/the/root/one.js', 2, 13, 'baz', map, assert, null, true);
util.assertMapping(2, 9, '/the/root/two.js', 1, 16, null, map, assert, null, true);
};
exports['test creating source map consumers with )]}\' prefix'] = function (assert, util) {
assert.doesNotThrow(function () {
var map = new SourceMapConsumer(")]}'" + JSON.stringify(util.testMap));
});
};
});

View File

@ -117,6 +117,22 @@ define("test/source-map/test-source-node", ["require", "exports", "module"], fun
});
};
exports['test .replaceRight'] = function (assert, util) {
var node;
// Not nested
node = new SourceNode(null, null, null, 'hello world');
node.replaceRight(/world/, 'universe');
assert.equal(node.toString(), 'hello universe');
// Nested
node = new SourceNode(null, null, null,
[new SourceNode(null, null, null, 'hey sexy mama, '),
new SourceNode(null, null, null, 'want to kill all humans?')]);
node.replaceRight(/kill all humans/, 'watch Futurama');
assert.equal(node.toString(), 'hey sexy mama, want to watch Futurama?');
};
exports['test .toStringWithSourceMap()'] = function (assert, util) {
var node = new SourceNode(null, null, null,
['(function () {\n',