--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -10345,22 +10345,30 @@ nsDocShell::InternalLoad(nsIURI* aURI,
// The event dispatch below can cause us to re-enter script and
// destroy the docshell, nulling out mScriptGlobal. Hold a stack
// reference to avoid null derefs. See bug 914521.
if (win) {
// Fire a hashchange event URIs differ, and only in their hashes.
bool doHashchange = sameExceptHashes &&
(curURIHasRef != newURIHasRef || !curHash.Equals(newHash));
- if (historyNavBetweenSameDoc || doHashchange) {
- win->DispatchSyncPopState();
+ function<void ()> scrollCallback;
+ if (needsScrollPosUpdate && win->AsInner()->HasActiveDocument()) {
+ RefPtr<nsDocShell> thisHolder = this;
+ scrollCallback = [=] () {
+ thisHolder->SetCurScrollPosEx(bx, by);
+ };
}
- if (needsScrollPosUpdate && win->AsInner()->HasActiveDocument()) {
- SetCurScrollPosEx(bx, by);
+ if (historyNavBetweenSameDoc || doHashchange) {
+ win->DispatchAsyncPopState(Move(scrollCallback));
+ }
+
+ if (scrollCallback) {
+ scrollCallback();
}
if (doHashchange) {
// Note that currentURI hasn't changed because it's on the
// stack, so we can just use it directly as the old URI.
win->DispatchAsyncHashchange(currentURI, aURI);
}
}
new file mode 100644
--- /dev/null
+++ b/docshell/test/navigation/file_popstate_sync.html
@@ -0,0 +1,44 @@
+<html>
+ <head>
+ <script>
+ let correctOrder = ["start-onload", "end-onload", "onpopstate", "onhashchange"];
+ let order = [];
+ let done = false;
+
+ window.onload = function () {
+ setTimeout(function () {
+ order.push("start-onload");
+ history.pushState({page: 1}, "Hi", "#fragment");
+ history.back();
+ order.push("end-onload");
+ opener.ok(!done, "onhash should run last, not onload");
+ }, 0);
+ }
+
+ window.onpopstate = function () {
+ order.push("onpopstate");
+ opener.ok(!done, "onhash should run last, not onpopstate");
+ }
+
+ window.onhashchange = function () {
+ order.push("onhashchange");
+ done = true;
+ opener.is(correctOrder.length, order.length,
+ "should have the right number of total states");
+ let i = 0;
+ for (let e of correctOrder) {
+ opener.is(e, order[i], "state " + i.toString() + " should match");
+ ++i;
+ }
+ opener.nextTest();
+ window.close();
+ }
+ </script>
+ </head>
+ <body>
+ <div style="border: 1px solid black; height: 5000px;">
+ </div>
+ <div id="bottom">Hello world</div>
+ <a href="#hash" name="hash">hash</a>
+ </body>
+</html>
--- a/docshell/test/navigation/file_scrollRestoration.html
+++ b/docshell/test/navigation/file_scrollRestoration.html
@@ -65,41 +65,17 @@
opener.is(window.scrollY, 0, "Shouldn't have kept the old scroll position.");
opener.is(history.scrollRestoration, "manual", "Should have the same scrollRestoration mode as before fragment navigation.");
history.scrollRestoration = "auto";
document.getElementById("bottom").scrollIntoView();
history.pushState({ state: "state1" }, "state1");
history.pushState({ state: "state2" }, "state2");
window.scrollTo(0, 0);
history.back();
- opener.isnot(window.scrollY, 0, "Should have scrolled back to the state1's position");
- opener.is(history.state.state, "state1", "Unexpected state.");
-
- history.scrollRestoration = "manual";
- document.getElementById("bottom").scrollIntoView();
- history.pushState({ state: "state3" }, "state3");
- history.pushState({ state: "state4" }, "state4");
- window.scrollTo(0, 0);
- history.back();
- opener.is(window.scrollY, 0, "Shouldn't have scrolled back to the state3's position");
- opener.is(history.state.state, "state3", "Unexpected state.");
-
- history.pushState({ state: "state5" }, "state5");
- history.scrollRestoration = "auto";
- document.getElementById("bottom").scrollIntoView();
- opener.isnot(window.scrollY, 0, "Should have scrolled to 'bottom'.");
- history.back();
- window.scrollTo(0, 0);
- history.forward();
- opener.isnot(window.scrollY, 0, "Should have scrolled back to the state5's position");
-
- var ifr = document.createElement("iframe");
- ifr.src = "data:text/html,";
- document.body.appendChild(ifr);
- ifr.onload = test;
+ // continued in testPopState() below.
break;
}
case 7: {
oldHistoryObject = event.target.contentWindow.history;
event.target.src = "about:blank";
break;
}
case 8: {
@@ -117,20 +93,76 @@
}
opener.nextTest();
window.close();
break;
}
}
}
+ function testPopState() {
+ if (opener.scrollRestorationTest != 6) {
+ return;
+ }
+
+ if (!opener.scrollRestorationTestPopState) {
+ opener.scrollRestorationTestPopState = 0;
+ }
+ ++opener.scrollRestorationTestPopState;
+
+ switch (opener.scrollRestorationTestPopState) {
+ case 1: {
+ opener.isnot(window.scrollY, 0, "Should have scrolled back to the state1's position");
+ opener.is(history.state.state, "state1", "Unexpected state.");
+
+ history.scrollRestoration = "manual";
+ document.getElementById("bottom").scrollIntoView();
+ history.pushState({ state: "state3" }, "state3");
+ history.pushState({ state: "state4" }, "state4");
+ window.scrollTo(0, 0);
+ history.back();
+ break;
+ }
+ case 2: {
+ opener.is(window.scrollY, 0, "Shouldn't have scrolled back to the state3's position");
+ opener.is(history.state.state, "state3", "Unexpected state.");
+
+ history.pushState({ state: "state5" }, "state5");
+ history.scrollRestoration = "auto";
+ document.getElementById("bottom").scrollIntoView();
+ opener.isnot(window.scrollY, 0, "Should have scrolled to 'bottom'.");
+ history.back();
+ break;
+ }
+ case 3: {
+ window.scrollTo(0, 0);
+ history.forward();
+ break;
+ }
+ case 4: {
+ opener.isnot(window.scrollY, 0, "Should have scrolled back to the state5's position");
+
+ var ifr = document.createElement("iframe");
+ ifr.src = "data:text/html,";
+ document.body.appendChild(ifr);
+ ifr.onload = test;
+ break;
+ }
+ }
+ };
+
window.addEventListener("pageshow",
function(e) {
setTimeout(test, 0, e);
});
+
+ window.addEventListener("popstate",
+ function(e) {
+ setTimeout(testPopState, 0, e);
+ });
</script>
</head>
<body>
<div style="border: 1px solid black; height: 5000px;">
</div>
<div id="bottom">Hello world</div>
<a href="#hash" name="hash">hash</a>
</body>
--- a/docshell/test/navigation/mochitest.ini
+++ b/docshell/test/navigation/mochitest.ini
@@ -5,16 +5,17 @@ support-files =
file_bug462076_1.html
file_bug462076_2.html
file_bug462076_3.html
file_bug508537_1.html
file_bug534178.html
file_document_write_1.html
file_fragment_handling_during_load.html
file_nested_frames.html
+ file_popstate_sync.html
file_scrollRestoration.html
file_shiftReload_and_pushState.html
file_static_and_dynamic_1.html
frame0.html
frame1.html
frame2.html
frame3.html
goback.html
@@ -27,17 +28,17 @@ support-files =
file_triggeringprincipal_subframe.html
file_triggeringprincipal_subframe_nav.html
file_triggeringprincipal_window_open.html
file_triggeringprincipal_parent_iframe_window_open_base.html
file_triggeringprincipal_parent_iframe_window_open_nav.html
file_triggeringprincipal_iframe_iframe_window_open_frame_a.html
file_triggeringprincipal_iframe_iframe_window_open_frame_b.html
file_triggeringprincipal_iframe_iframe_window_open_frame_a_nav.html
-
+
[test_bug13871.html]
skip-if = buildapp == 'b2g' || buildapp == 'mulet' || toolkit == 'android' #RANDOM # Bug 1136180 disabled on B2G Desktop and Mulet for intermittent failures
[test_bug270414.html]
skip-if = buildapp == 'b2g' || buildapp == 'mulet' || toolkit == "android" # Bug 1136180 disabled on B2G Desktop and Mulet for intermittent failures
[test_bug278916.html]
[test_bug279495.html]
[test_bug344861.html]
skip-if = buildapp == 'b2g' || buildapp == 'mulet' || toolkit == "android" || toolkit == "windows" # disabled on Windows because of bug 1234520
--- a/docshell/test/navigation/test_sessionhistory.html
+++ b/docshell/test/navigation/test_sessionhistory.html
@@ -25,17 +25,18 @@ var testFiles =
"file_bug462076_3.html", // Dynamic frames after onload
"file_bug508537_1.html", // Dynamic frames and forward-back
"file_document_write_1.html", // Session history + document.write
//"file_static_and_dynamic_1.html",// Static and dynamic frames and forward-back
"file_bug534178.html", // Session history transaction clean-up.
"file_fragment_handling_during_load.html",
"file_nested_frames.html",
"file_shiftReload_and_pushState.html",
- "file_scrollRestoration.html"
+ "file_scrollRestoration.html",
+ "file_popstate_sync.html",
];
var testCount = 0; // Used by the test files.
SimpleTest.waitForExplicitFinish();
SimpleTest.requestFlakyTimeout("untriaged");
var testWindow;
function nextTest_() {
--- a/docshell/test/test_bug590573.html
+++ b/docshell/test/test_bug590573.html
@@ -30,17 +30,17 @@ function page1Load()
}
var page1PopstateCallbackEnabled = false;
function page1Popstate()
{
if (page1PopstateCallbackEnabled) {
page1PopstateCallbackEnabled = false;
dump('Got page1 popstate.\n');
- pageLoad();
+ setTimeout(pageLoad, 0);
}
else {
dump('Ignoring page1 popstate.\n');
}
}
var page1PageShowCallbackEnabled = false;
function page1PageShow()
@@ -118,16 +118,30 @@ function dumpSHistory(theWindow)
}
}
return sh;
}
var popup = window.open('file_bug590573_1.html');
+function* popupForward()
+{
+ page1PopstateCallbackEnabled = 1;
+ popup.history.forward();
+ yield;
+}
+
+function* popupBack()
+{
+ page1PopstateCallbackEnabled = 1;
+ popup.history.back();
+ yield;
+}
+
var gTestContinuation = null;
var loads = 0;
function pageLoad()
{
loads++;
dump('pageLoad(loads=' + loads + ', page location=' + popup.location + ')\n');
dumpSHistory(window);
@@ -145,27 +159,30 @@ function* testBody()
{
is(popup.scrollY, 0, "test 1");
popup.scroll(0, 100);
popup.history.pushState('', '', '?pushed');
is(popup.scrollY, 100, "test 2");
popup.scroll(0, 200); // set state-2's position to 200
- popup.history.back();
+ yield* popupBack();
+
is(popup.scrollY, 100, "test 3");
popup.scroll(0, 150); // set original page's position to 150
- popup.history.forward();
+ yield* popupForward();
+
is(popup.scrollY, 200, "test 4");
- popup.history.back();
+ yield* popupBack();
+
is(popup.scrollY, 150, "test 5");
- popup.history.forward();
+ yield* popupForward();
is(popup.scrollY, 200, "test 6");
// At this point, the history looks like:
// PATH POSITION
// file_bug590573_1.html 150 <-- oldest
// file_bug590573_1.html?pushed 200 <-- newest, current
// Now test that the scroll position is persisted when we have real
@@ -199,20 +216,20 @@ function* testBody()
setTimeout(pageLoad, 0);
yield;
is(popup.location.search, "?pushed");
ok(popup.document.getElementById('div1'), 'page should have div1.');
is(popup.scrollY, 200, "test 8");
- popup.history.back();
+ yield* popupBack();
is(popup.scrollY, 150, "test 9");
- popup.history.forward();
+ yield* popupForward();
is(popup.scrollY, 200, "test 10");
// Spin one last time...
setTimeout(pageLoad, 0);
yield;
page2PageShowCallbackEnabled = true;
popup.history.forward();
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -10203,18 +10203,71 @@ nsGlobalWindow::FireHashchange(const nsA
init);
event->SetTrusted(true);
bool dummy;
return DispatchEvent(event, &dummy);
}
+class PopStateCallback : public Runnable
+{
+public:
+ PopStateCallback(JSContext* aCx,
+ JS::Handle<JS::Value> aStateJSValue,
+ nsGlobalWindow* aWindow,
+ mozilla::function<void ()>&& aRunAfter)
+ : mStateJSValue(aCx, aStateJSValue)
+ , mWindow(aWindow)
+ , mRunAfter(Move(aRunAfter))
+ {
+ MOZ_ASSERT(mWindow);
+ MOZ_ASSERT(mWindow->IsInnerWindow());
+ }
+
+ NS_IMETHOD Run()
+ {
+ MOZ_ASSERT(NS_IsMainThread(), "Should be called on the main thread.");
+
+ bool result = true;
+ AutoJSAPI jsapi;
+ result = jsapi.Init(mWindow->AsInner());
+ NS_ENSURE_TRUE(result, NS_ERROR_FAILURE);
+ JSContext* cx = jsapi.cx();
+
+ RootedDictionary<PopStateEventInit> init(cx);
+ init.mBubbles = true;
+ init.mCancelable = false;
+ init.mState = mStateJSValue;
+
+ RefPtr<PopStateEvent> event =
+ PopStateEvent::Constructor(mWindow, NS_LITERAL_STRING("popstate"),
+ init);
+ event->SetTrusted(true);
+ event->SetTarget(mWindow);
+
+ bool dummy; // default action
+ nsresult rv = mWindow->DispatchEvent(event, &dummy);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mRunAfter && mWindow->AsInner()->HasActiveDocument()) {
+ mRunAfter();
+ }
+
+ return NS_OK;
+ }
+
+private:
+ JS::PersistentRooted<JS::Value> mStateJSValue;
+ RefPtr<nsGlobalWindow> mWindow;
+ mozilla::function<void ()> mRunAfter;
+};
+
nsresult
-nsGlobalWindow::DispatchSyncPopState()
+nsGlobalWindow::DispatchAsyncPopState(mozilla::function<void ()>&& aRunAfter)
{
MOZ_RELEASE_ASSERT(IsInnerWindow());
NS_ASSERTION(nsContentUtils::IsSafeToRunScript(),
"Must be safe to run script here.");
nsresult rv = NS_OK;
// Bail if the window is frozen.
@@ -10241,29 +10294,19 @@ nsGlobalWindow::DispatchSyncPopState()
result = jsapi.Init(AsInner());
NS_ENSURE_TRUE(result, NS_ERROR_FAILURE);
JSContext* cx = jsapi.cx();
JS::Rooted<JS::Value> stateJSValue(cx, JS::NullValue());
result = stateObj ? VariantToJsval(cx, stateObj, &stateJSValue) : true;
NS_ENSURE_TRUE(result, NS_ERROR_FAILURE);
- RootedDictionary<PopStateEventInit> init(cx);
- init.mBubbles = true;
- init.mCancelable = false;
- init.mState = stateJSValue;
-
- RefPtr<PopStateEvent> event =
- PopStateEvent::Constructor(this, NS_LITERAL_STRING("popstate"),
- init);
- event->SetTrusted(true);
- event->SetTarget(this);
-
- bool dummy; // default action
- return DispatchEvent(event, &dummy);
+ nsCOMPtr<nsIRunnable> callback =
+ new PopStateCallback(cx, stateJSValue, this, Move(aRunAfter));
+ return NS_DispatchToMainThread(callback);
}
// Find an nsICanvasFrame under aFrame. Only search the principal
// child lists. aFrame must be non-null.
static nsCanvasFrame* FindCanvasFrame(nsIFrame* aFrame)
{
nsCanvasFrame* canvasFrame = do_QueryFrame(aFrame);
if (canvasFrame) {
--- a/dom/base/nsGlobalWindow.h
+++ b/dom/base/nsGlobalWindow.h
@@ -672,17 +672,17 @@ public:
virtual void
CacheXBLPrototypeHandler(nsXBLPrototypeHandler* aKey,
JS::Handle<JSObject*> aHandler) override;
virtual bool TakeFocus(bool aFocus, uint32_t aFocusMethod) override;
virtual void SetReadyForFocus() override;
virtual void PageHidden() override;
virtual nsresult DispatchAsyncHashchange(nsIURI *aOldURI, nsIURI *aNewURI) override;
- virtual nsresult DispatchSyncPopState() override;
+ virtual nsresult DispatchAsyncPopState(mozilla::function<void ()>&& aRunAfter) override;
// Inner windows only.
virtual void EnableDeviceSensor(uint32_t aType) override;
virtual void DisableDeviceSensor(uint32_t aType) override;
#if defined(MOZ_WIDGET_ANDROID) || defined(MOZ_WIDGET_GONK)
virtual void EnableOrientationChangeListener() override;
virtual void DisableOrientationChangeListener() override;
--- a/dom/base/nsPIDOMWindow.h
+++ b/dom/base/nsPIDOMWindow.h
@@ -8,16 +8,17 @@
#define nsPIDOMWindow_h__
#include "nsIDOMWindow.h"
#include "mozIDOMWindow.h"
#include "nsCOMPtr.h"
#include "nsTArray.h"
#include "mozilla/dom/EventTarget.h"
+#include "mozilla/Function.h"
#include "js/TypeDecls.h"
#include "nsRefPtrHashtable.h"
// Only fired for inner windows.
#define DOM_WINDOW_DESTROYED_TOPIC "dom-window-destroyed"
#define DOM_WINDOW_FROZEN_TOPIC "dom-window-frozen"
#define DOM_WINDOW_THAWED_TOPIC "dom-window-thawed"
@@ -413,19 +414,19 @@ public:
/**
* Instructs this window to asynchronously dispatch a hashchange event. This
* method must be called on an inner window.
*/
virtual nsresult DispatchAsyncHashchange(nsIURI *aOldURI,
nsIURI *aNewURI) = 0;
/**
- * Instructs this window to synchronously dispatch a popState event.
+ * Instructs this window to asynchronously dispatch a popState event.
*/
- virtual nsresult DispatchSyncPopState() = 0;
+ virtual nsresult DispatchAsyncPopState(mozilla::function<void ()>&& aRunAfter) = 0;
/**
* Tell this window that it should listen for sensor changes of the given
* type.
*
* Inner windows only.
*/
virtual void EnableDeviceSensor(uint32_t aType) = 0;
--- a/dom/tests/mochitest/general/historyframes.html
+++ b/dom/tests/mochitest/general/historyframes.html
@@ -20,20 +20,30 @@ var testWin = window.opener ? window.ope
var SimpleTest = testWin.SimpleTest;
function is() { testWin.is.apply(testWin, arguments); }
var gFrame = null;
var gState = null;
+var waitingForPopState = 0;
+
window.addEventListener("popstate", function(aEvent) {
gState = aEvent.state;
+ if (waitingForPopState > 0) {
+ --waitingForPopState;
+ SimpleTest.executeSoon(continue_test);
+ }
}, false);
+function waitForPopState() {
+ ++waitingForPopState;
+}
+
function waitForLoad() {
function listener() {
gFrame.removeEventListener("load", listener, false);
SimpleTest.executeSoon(continue_test);
}
gFrame.addEventListener("load", listener, false);
}
@@ -112,29 +122,32 @@ function* test_state_navigation() {
is(getContent(), "Test2", "Page should be correct");
window.history.pushState("STATE2", window.location);
is(getURL(), URL2, "URL should be correct");
is(getContent(), "Test2", "Page should be correct");
window.history.back();
+ yield waitForPopState();
is(gState, "STATE1", "State should be correct");
is(getURL(), URL2, "URL should be correct");
is(getContent(), "Test2", "Page should be correct");
window.history.forward();
+ yield waitForPopState();
is(gState, "STATE2", "State should be correct");
is(getURL(), URL2, "URL should be correct");
is(getContent(), "Test2", "Page should be correct");
window.history.back();
window.history.back();
+ yield waitForPopState();
is(gState, "START", "State should be correct");
is(getURL(), URL2, "URL should be correct");
is(getContent(), "Test2", "Page should be correct");
window.history.back();
is(gState, "START", "State should be correct");
yield waitForLoad();
deleted file mode 100644
--- a/testing/web-platform/meta/html/browsers/browsing-the-web/history-traversal/popstate_event.html.ini
+++ /dev/null
@@ -1,5 +0,0 @@
-[popstate_event.html]
- type: testharness
- [Queue a task to fire popstate event]
- expected: FAIL
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/browsers/history/the-history-interface/combination_history_004.html.ini
+++ /dev/null
@@ -1,6 +0,0 @@
-[combination_history_004.html]
- type: testharness
- expected: TIMEOUT
- [After calling of back method, check length]
- expected: TIMEOUT
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/browsers/history/the-history-interface/combination_history_005.html.ini
+++ /dev/null
@@ -1,6 +0,0 @@
-[combination_history_005.html]
- type: testharness
- expected: TIMEOUT
- [After calling of forward method, check length]
- expected: TIMEOUT
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/browsers/history/the-history-interface/combination_history_006.html.ini
+++ /dev/null
@@ -1,6 +0,0 @@
-[combination_history_006.html]
- type: testharness
- expected: TIMEOUT
- [After calling of go method, check length]
- expected: TIMEOUT
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/browsers/history/the-history-interface/combination_history_007.html.ini
+++ /dev/null
@@ -1,6 +0,0 @@
-[combination_history_007.html]
- type: testharness
- expected: TIMEOUT
- [After calling of back and pushState method, check length]
- expected: TIMEOUT
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/browsers/history/the-history-interface/history_back.html.ini
+++ /dev/null
@@ -1,6 +0,0 @@
-[history_back.html]
- type: testharness
- expected: TIMEOUT
- [history back]
- expected: TIMEOUT
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/browsers/history/the-history-interface/history_forward.html.ini
+++ /dev/null
@@ -1,6 +0,0 @@
-[history_forward.html]
- type: testharness
- expected: TIMEOUT
- [history forward]
- expected: TIMEOUT
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/browsers/history/the-history-interface/history_go_minus.html.ini
+++ /dev/null
@@ -1,6 +0,0 @@
-[history_go_minus.html]
- type: testharness
- expected: TIMEOUT
- [history go minus]
- expected: TIMEOUT
-
deleted file mode 100644
--- a/testing/web-platform/meta/html/browsers/history/the-history-interface/history_go_plus.html.ini
+++ /dev/null
@@ -1,6 +0,0 @@
-[history_go_plus.html]
- type: testharness
- expected: TIMEOUT
- [history go plus]
- expected: TIMEOUT
-