Commit cf46007a authored by rniwa@webkit.org's avatar rniwa@webkit.org

2011-01-06 Ryosuke Niwa <rniwa@webkit.org>

        Reviewed by Adam Barth.

        onbeforeunload is broken for framesets
        https://bugs.webkit.org/show_bug.cgi?id=19418

        Added beforeunload event support for sub frames. WebKit's implementation tries to match
        that of Internet Explorer as much as possible. beforeunload event is fired for each and
        every descendent of a frame that is about to navigate.

        When a value other than null is returned by a beforeunload handler, a confirmation dialog
        is shown for each handler (calls chrome's runBeforeUnloadConfirmPanel) just like it is done
        for main frames.

        In addition, navigation is forbidden while beforeunload handlers are being called.
        Setting values to location.href, location.reload, and other means of navigations are thus
        ignored while beforeunload event handler is being ran, matching Internet Explorer's behavior.

        Because navigation needs to prevented globally, NavigationDisablerForBeforeUnload is added to
        NavigationScheduler.h, which is instantiated as a RAII object in FrameLoader::shouldClose.

        Tests: fast/events/before-unload-adopt-subframe-to-outside.html
               fast/events/before-unload-adopt-within-subframes.html
               fast/events/before-unload-forbidden-navigation.html
               fast/events/before-unload-in-multiple-subframes.html
               fast/events/before-unload-in-subframe.html
               fast/events/before-unload-javascript-navigation.html
               fast/events/before-unload-remove-and-add-subframe.html
               fact/events/before-unload-remove-itself.html
               fast/events/before-unload-with-subframes.html

       * loader/FrameLoader.cpp:
       (WebCore::FrameLoader::shouldClose): Calls fireBeforeUnloadEvent on m_frame and m_frame's
       descendents. Returns true only if every call to fireBeforeUnloadEvent returned true.
       (WebCore::FrameLoader::fireBeforeUnloadEvent): Fires a beforeunload event and calls
       chrome's runBeforeUnloadConfirmPanel as needed.
       (WebCore::FrameLoader::continueLoadAfterNavigationPolicy): Calls shouldClose for all frames.
       * loader/FrameLoader.h:
       * loader/NavigationScheduler.cpp:
       (WebCore::NavigationScheduler::shouldScheduleNavigation): Checks the nullity of Page and calls
       NavigationDisablerForBeforeUnload::isNavigationAllowed when url is not javascript scheme.
       (WebCore::NavigationScheduler::scheduleRedirect): Calls shouldScheduleNavigation.
       (WebCore::NavigationScheduler::scheduleLocationChange): Ditto.
       (WebCore::NavigationScheduler::scheduleRefresh): Ditto.
       (WebCore::NavigationScheduler::scheduleHistoryNavigation): Ditto.
       * loader/NavigationScheduler.h:
       (WebCore::NavigationDisablerForBeforeUnload::NavigationDisablerForBeforeUnload): Disables navigation.
       (WebCore::NavigationDisablerForBeforeUnload::~NavigationDisablerForBeforeUnload): Enables navigation
       when called on the last instance of NavigationDisablerForBeforeUnload.
       (WebCore::NavigationDisablerForBeforeUnload::isNavigationAllowed): Returns true if there are no instance
       of NavigationDisablerForBeforeUnload left on the stack.
2011-01-06  Ryosuke Niwa  <rniwa@webkit.org>

        Reviewed by Adam Barth.

        onbeforeunload is broken for framesets
        https://bugs.webkit.org/show_bug.cgi?id=19418

        Added tests to ensure WebKit fires beforeunload events for subframes,
        and disallows navigation except that of javascript scheme while beforeunload event
        handlers are being called.

        Also added a test to ensure WebKit fires beforeunload event for subframes exactly
        once even if a subframe was moved around within a beforeunload event handler.

        A test that ensures beforeunload event is not fired for an iframe if the iframe
        was added or removed within a beforeunload event handler is also added.

        Furthermore, a test to ensure WebKit does not fire a beforeunload event to an iframe
        that has been adopted by a document outside of the unloading document is added.

        * fast/events/before-unload-adopt-subframe-to-outside-expected.txt: Added.
        * fast/events/before-unload-adopt-subframe-to-outside.html: Added.
        * fast/events/before-unload-adopt-within-subframes-expected.txt: Added.
        * fast/events/before-unload-adopt-within-subframes.html: Added.
        * fast/events/before-unload-forbidden-navigation-expected.txt: Added.
        * fast/events/before-unload-forbidden-navigation.html: Added.
        * fast/events/before-unload-in-multiple-subframes-expected.txt: Added.
        * fast/events/before-unload-in-multiple-subframes.html: Added.
        * fast/events/before-unload-in-subframe-expected.txt: Added.
        * fast/events/before-unload-in-subframe.html: Added.
        * fast/events/before-unload-javascript-navigation-expected.txt: Added.
        * fast/events/before-unload-javascript-navigation.html: Added.
        * fast/events/before-unload-remove-and-add-subframe-expected.txt: Added.
        * fast/events/before-unload-remove-and-add-subframe.html: Added.
        * fact/events/before-unload-remove-itself-expected.txt: Added.
        * fact/events/before-unload-remove-itself.html: Added.
        * fast/events/before-unload-with-subframes-expected.txt: Added.
        * fast/events/before-unload-with-subframes.html: Added.
        * fast/events/resources/before-unload-in-subframe-child.html: Added.
        * fast/events/resources/before-unload-in-subframe-destination.html: Added.
        * fast/events/resources/before-unload-in-subframe-fail.html: Added.
        * fast/events/resources/before-unload-with-subframes-parent.html: Added.


git-svn-id: http://svn.webkit.org/repository/webkit/trunk@75305 268f45cc-cd09-0410-ab3c-d52691b4dbfc
parent e7f0902b
2011-01-06 Ryosuke Niwa <rniwa@webkit.org>
Reviewed by Adam Barth.
onbeforeunload is broken for framesets
https://bugs.webkit.org/show_bug.cgi?id=19418
Added tests to ensure WebKit fires beforeunload events for subframes,
and disallows navigation except that of javascript scheme while beforeunload event
handlers are being called.
Also added a test to ensure WebKit fires beforeunload event for subframes exactly
once even if a subframe was moved around within a beforeunload event handler.
A test that ensures beforeunload event is not fired for an iframe if the iframe
was added or removed within a beforeunload event handler is also added.
Furthermore, a test to ensure WebKit does not fire a beforeunload event to an iframe
that has been adopted by a document outside of the unloading document is added.
* fast/events/before-unload-adopt-subframe-to-outside-expected.txt: Added.
* fast/events/before-unload-adopt-subframe-to-outside.html: Added.
* fast/events/before-unload-adopt-within-subframes-expected.txt: Added.
* fast/events/before-unload-adopt-within-subframes.html: Added.
* fast/events/before-unload-forbidden-navigation-expected.txt: Added.
* fast/events/before-unload-forbidden-navigation.html: Added.
* fast/events/before-unload-in-multiple-subframes-expected.txt: Added.
* fast/events/before-unload-in-multiple-subframes.html: Added.
* fast/events/before-unload-in-subframe-expected.txt: Added.
* fast/events/before-unload-in-subframe.html: Added.
* fast/events/before-unload-javascript-navigation-expected.txt: Added.
* fast/events/before-unload-javascript-navigation.html: Added.
* fast/events/before-unload-remove-and-add-subframe-expected.txt: Added.
* fast/events/before-unload-remove-and-add-subframe.html: Added.
* fact/events/before-unload-remove-itself-expected.txt: Added.
* fact/events/before-unload-remove-itself.html: Added.
* fast/events/before-unload-with-subframes-expected.txt: Added.
* fast/events/before-unload-with-subframes.html: Added.
* fast/events/resources/before-unload-in-subframe-child.html: Added.
* fast/events/resources/before-unload-in-subframe-destination.html: Added.
* fast/events/resources/before-unload-in-subframe-fail.html: Added.
* fast/events/resources/before-unload-with-subframes-parent.html: Added.
2011-01-07 Martin Robinson <mrobinson@igalia.com>
Reviewed by Mihai Parparita.
......
This test ensures beforeunload event does NOT fire for a subframe that has been adopted by a document outside of the frame hierarchy that is about to unload.
PASS: fired on parent
PASS: fired on a
adopting
adopted
PASS: fired on b
DONE
<!DOCTYPE html>
<html>
<body>
<p>This test ensures beforeunload event does NOT fire for a subframe that has been adopted by a document outside of the frame hierarchy that is about to unload.</p>
<pre id="log"></pre>
<script>
if (window.layoutTestController) {
layoutTestController.dumpAsText();
layoutTestController.waitUntilDone();
}
function createFrame(id, parent) {
var iframe = document.createElement('iframe');
if (parent)
parent.contentDocument.body.appendChild(iframe);
else
document.body.appendChild(iframe);
if (!iframe.contentDocument.body)
iframe.contentDocument.appendChild(iframe.contentDocument.createElement('body'));
iframe.contentDocument.body.appendChild(iframe.contentDocument.createTextNode(id));
iframe.contentDocument.body.appendChild(iframe.contentDocument.createElement('br'));
iframe.contentWindow.onbeforeunload = function () { fired(iframe.contentWindow, id); return null; }
iframe.style.width = '70%';
iframe.style.height = '40%';
return iframe;
}
function log(message) {
var log = document.getElementById('log');
log.innerHTML += message + '\n';
}
var expectedOrder = ['parent', 'a', 'b'];
var i = 0;
function fired(contentWindow, id) {
if (expectedOrder[i] == id)
log('PASS: fired on ' + id);
else
log('FAIL: fired on ' + id + ' but expected on ' + expectedOrder[i]);
i++;
if (contentWindow == a.contentWindow) {
log('adopting');
document.body.appendChild(document.adoptNode(adoptee));
log('adopted');
}
}
var container = createFrame('parent');
var a = createFrame('a', container);
var adoptee = createFrame('adoptee', a);
var b = createFrame('b', container);
container.onload = function () {
if (i == expectedOrder.length)
log('DONE');
else
log('Received ' + i + ' events but expected ' + expectedOrder.length);
if (window.layoutTestController)
layoutTestController.notifyDone();
}
container.src = 'resources/before-unload-in-subframe-destination.html';
</script>
</body>
</html>
This test ensures beforeunload event fires exactly once in a subframe even if the frame was adopted to a frame that appears later in the tree.
PASS: fired on parent
PASS: fired on a
PASS: fired on adoptee
adopting
adopted
PASS: fired on b
DONE
<!DOCTYPE html>
<html>
<body>
<p>This test ensures beforeunload event fires exactly once in a subframe even if the frame was adopted to a frame that appears later in the tree.</p>
<pre id="log"></pre>
<script>
if (window.layoutTestController) {
layoutTestController.dumpAsText();
layoutTestController.waitUntilDone();
}
function createFrame(id, parent) {
var iframe = document.createElement('iframe');
if (parent)
parent.contentDocument.body.appendChild(iframe);
else
document.body.appendChild(iframe);
iframe.contentDocument.body.appendChild(iframe.contentDocument.createTextNode(id));
iframe.contentDocument.body.appendChild(iframe.contentDocument.createElement('br'));
iframe.contentWindow.onbeforeunload = function () { fired(iframe.contentWindow, id); return null; }
iframe.style.width = '70%';
iframe.style.height = '40%';
return iframe;
}
function log(message) {
var log = document.getElementById('log');
log.innerHTML += message + '\n';
}
var expectedOrder = ['parent', 'a', 'adoptee', 'b'];
var i = 0;
function fired(contentWindow, id) {
if (expectedOrder[i] == id)
log('PASS: fired on ' + id);
else
log('FAIL: fired on ' + id + ' but expected on ' + expectedOrder[i]);
i++;
if (contentWindow == adoptee.contentWindow) {
log('adopting');
b.contentDocument.body.appendChild(b.contentDocument.adoptNode(adoptee));
log('adopted');
}
}
var container = createFrame('parent');
var a = createFrame('a', container);
var adoptee = createFrame('adoptee', a);
var b = createFrame('b', container);
container.onload = function () {
if (i == expectedOrder.length)
log('DONE');
else
log('Received ' + i + ' events but expected ' + expectedOrder.length);
if (window.layoutTestController)
layoutTestController.notifyDone();
}
container.src = 'resources/before-unload-in-subframe-destination.html';
</script>
</body>
</html>
This test ensures navigation is forbidden while beforeunload event is being fired. You should see PASS 1/2 and PASS 2/2 below:
PASS 1/2
--------
Frame: '<!--framePath //<!--frame0-->-->'
--------
PASS: 2/2
<!DOCTYPE html>
<html>
<body>
<p>This test ensures navigation is forbidden while beforeunload event is being fired. You should see PASS 1/2 and PASS 2/2 below:</p>
<pre id="log">FAIL</pre>
<script>
if (window.layoutTestController) {
layoutTestController.dumpAsText();
layoutTestController.dumpChildFramesAsText();
layoutTestController.waitUntilDone();
}
var log = document.getElementById('log');
function test(iframe) {
if (iframe.done) {
if (iframe.halfPassed) {
iframe.contentWindow.location.href = 'resources/before-unload-in-subframe-destination.html';
iframe.halfPassed = false;
}
return;
}
iframe.done = true;
iframe.contentWindow.location.href = 'resources/before-unload-in-subframe-child.html';
}
function fired(contentWindow) {
location.href = 'resources/before-unload-in-subframe-fail.html';
contentWindow.location.href = 'resources/before-unload-in-subframe-fail.html';
log.innerHTML = 'PASS 1/2';
contentWindow.frameElement.halfPassed = true;
}
</script>
<iframe onload="test(this);" src="resources/before-unload-in-subframe-child.html"></iframe>
</body>
</html>
This tests beforeunload event in multiple subframes. You should see PASS below:
PASS
<!DOCTYPE html>
<html>
<body>
<p>This tests beforeunload event in multiple subframes. You should see PASS below:</p>
<pre id="log"></pre>
<script>
if (window.layoutTestController) {
layoutTestController.dumpAsText();
layoutTestController.waitUntilDone();
}
var log = document.getElementById('log');
function test(iframe) {
if (iframe.done)
return;
iframe.done = true;
iframe.contentWindow.location.href = 'resources/before-unload-in-subframe-child.html';
}
var numberOfBeforeUnloadInSubframes = 0;
var numberOfFrames = 3;
function logError() {
log.innerHTML = 'FAIL: ' + numberOfBeforeUnloadInSubframes + ' beforeunload events are fired but expected ' + numberOfFrames + ' events';
}
logError();
function fired(contentWindow) {
numberOfBeforeUnloadInSubframes++;
if (numberOfBeforeUnloadInSubframes == numberOfFrames) {
log.innerHTML = 'PASS';
if (window.layoutTestController)
layoutTestController.notifyDone();
}
else
logError();
contentWindow.frameElement.fired = true;
}
</script>
<iframe onload="test(this)" src="resources/before-unload-in-subframe-child.html"></iframe>
<iframe onload="test(this)" src="resources/before-unload-in-subframe-child.html"></iframe>
<iframe onload="test(this)" src="resources/before-unload-in-subframe-child.html"></iframe>
</body>
</html>
This tests beforeunload event in subframes. You should see PASS below:
PASS
--------
Frame: '<!--framePath //<!--frame0-->-->'
--------
Child loaded.
<!DOCTYPE html>
<html>
<body>
<p>This tests beforeunload event in subframes. You should see PASS below:</p>
<pre id="log"></pre>
<script>
if (window.layoutTestController) {
layoutTestController.dumpAsText();
layoutTestController.dumpChildFramesAsText();
layoutTestController.waitUntilDone();
}
var log = document.getElementById('log');
var done = false;
function test() {
if (done) {
// log's not having any content implies that load event was dispatched for the second time before beforeunload is dispatched.
if (!log.innerHTML.length)
log.innerHTML = 'FAIL: beforeunload event was never dispatched.\n';
if (window.layoutTestController)
layoutTestController.notifyDone();
return;
}
done = true;
document.getElementsByTagName('iframe')[0].contentWindow.location.href = 'resources/before-unload-in-subframe-child.html';
}
function fired() {
if (!log.innerHTML.length)
log.innerHTML = 'PASS\n';
else
log.innerHTML = 'FAIL: beforeunload event was dispatched after the second load event.'
if (window.layoutTestController)
layoutTestController.notifyDone();
}
</script>
<iframe onload="test()" src="resources/before-unload-in-subframe-child.html"></iframe>
</body>
</html>
This test ensures setting location.href with javascript scheme properly executes the script even while beforeunload event is being fired. You should see PASS 1/2 and PASS 2/2 below:
PASS
<!DOCTYPE html>
<html>
<body>
<p>This test ensures setting location.href with javascript scheme properly executes the script even while beforeunload event is being fired. You should see PASS 1/2 and PASS 2/2 below:</p>
<pre id="log">FAIL</pre>
<script>
if (window.layoutTestController) {
layoutTestController.dumpAsText();
layoutTestController.waitUntilDone();
}
var log = document.getElementById('log');
function test(iframe) {
if (iframe.done)
return;
iframe.done = true;
iframe.contentWindow.location.href = 'resources/before-unload-in-subframe-child.html';
}
function done() {
log.innerHTML = 'PASS';
if (window.layoutTestController)
layoutTestController.notifyDone();
}
function fired(contentWindow) {
location.href = 'javascript:top.done()';
}
</script>
<iframe onload="test(this);" src="resources/before-unload-in-subframe-child.html"></iframe>
</body>
</html>
This test ensures beforeunload event does not fire for subframes that has been removed from the DOM within a beforeunload event handler. Also ensures the event doesn't fire for subframes added within a beforeunload event handler. The latter behavior matches MSIE.
PASS: fired on parent
PASS: fired on a
PASS: fired on c
DONE
<!DOCTYPE html>
<html>
<body>
<p>This test ensures beforeunload event does not fire for subframes that has been removed from the DOM within a beforeunload event handler. Also ensures the event doesn't fire for subframes added within a beforeunload event handler. The latter behavior matches MSIE.</p>
<pre id="log"></pre>
<script>
if (window.layoutTestController) {
layoutTestController.dumpAsText();
layoutTestController.waitUntilDone();
}
function createFrame(id, parent) {
var iframe = document.createElement('iframe');
iframe.id = id;
if (parent)
parent.contentDocument.body.appendChild(iframe);
else
document.body.appendChild(iframe);
if (!iframe.contentDocument.body)
iframe.contentDocument.appendChild(iframe.contentDocument.createElement('body'));
iframe.contentDocument.body.appendChild(iframe.contentDocument.createTextNode(id));
iframe.contentDocument.body.appendChild(iframe.contentDocument.createElement('br'));
iframe.contentWindow.onbeforeunload = function () { fired(iframe.contentWindow, id); return null; }
iframe.style.width = '70%';
iframe.style.height = '40%';
return iframe;
}
function log(message) {
var log = document.getElementById('log');
log.innerHTML += message + '\n';
}
var expectedOrder = ['parent', 'a', 'c'];
var i = 0;
function fired(contentWindow, id) {
if (expectedOrder[i] == id)
log('PASS: fired on ' + id);
else
log('FAIL: fired on ' + id + ' but expected on ' + expectedOrder[i]);
i++;
if (contentWindow == a.contentWindow) {
b.parentNode.removeChild(b);
createFrame('d', container);
}
}
var container = createFrame('parent');
var a = createFrame('a', container);
var b = createFrame('b', container);
var c = createFrame('c', container);
container.onload = function () {
if (i == expectedOrder.length)
log('DONE');
else
log('Received ' + i + ' events but expected ' + expectedOrder.length);
if (window.layoutTestController)
layoutTestController.notifyDone();
}
container.src = 'resources/before-unload-in-subframe-destination.html';
</script>
</body>
</html>
This test ensures a beforeunload event handler can safely remove the frame to which the event is fired. You should see PASS below:
PASS
<!DOCTYPE html>
<html>
<body>
<p>This test ensures a beforeunload event handler can safely remove the frame to which the event is fired. You should see PASS below:</p>
<pre id="log"></pre>
<script>
if (window.layoutTestController) {
layoutTestController.dumpAsText();
layoutTestController.dumpChildFramesAsText();
layoutTestController.waitUntilDone();
}
var log = document.getElementById('log');
var done = false;
function test() {
if (done) {
// log's not having any content implies that load event was dispatched for the second time before beforeunload is dispatched.
if (!log.innerHTML.length)
log.innerHTML = 'FAIL: beforeunload event was never dispatched.\n';
else
log.innerHTML = 'FAIL: beforeunload event handler did not remove the frame.\n';
if (window.layoutTestController)
layoutTestController.notifyDone();
return;
}
done = true;
document.getElementsByTagName('iframe')[0].contentWindow.location.href = 'resources/before-unload-in-subframe-child.html';
}
function fired() {
document.body.removeChild(document.body.getElementsByTagName('iframe')[0]);
log.innerHTML = 'PASS\n';
if (window.layoutTestController)
layoutTestController.notifyDone();
}
</script>
<iframe onload="test()" src="resources/before-unload-in-subframe-child.html"></iframe>
</body>
</html>
This test ensures beforeunload event fires in all subframes when a parent frame is navigated. You should see PASS 1/2 and PASS 2/2 below:
PASS 1/2
--------
Frame: '<!--framePath //<!--frame0-->-->'
--------
PASS: 2/2
<!DOCTYPE html>
<html>
<body>
<p>This test ensures beforeunload event fires in all subframes when a parent frame is navigated. You should see PASS 1/2 and PASS 2/2 below:</p>
<pre id="log"></pre>
<script>
if (window.layoutTestController) {
layoutTestController.dumpAsText();
layoutTestController.dumpChildFramesAsText();
layoutTestController.waitUntilDone();
}
var log = document.getElementById('log');
var numberOfBeforeUnloadInSubframes = 0;
var numberOfFrames = 3;
function test(iframe) {
if (iframe.done) {
if (numberOfBeforeUnloadInSubframes == numberOfFrames)
layoutTestController.notifyDone();
return;
}
iframe.done = true;
iframe.contentWindow.location.href = 'resources/before-unload-in-subframe-destination.html';
}
function logError() {
log.innerHTML = 'FAIL: ' + numberOfBeforeUnloadInSubframes + ' beforeunload events are fired but expected ' + numberOfFrames + ' events';
}
logError();
function fired(contentWindow) {
numberOfBeforeUnloadInSubframes++;
if (numberOfBeforeUnloadInSubframes == numberOfFrames)
log.innerHTML = 'PASS 1/2\n';
else
logError();
contentWindow.frameElement.fired = true;
}
</script>
<iframe onload="test(this)" src="resources/before-unload-with-subframes-parent.html"></iframe>
</body>
</html>
<!DOCTYPE html>
<html>
<body onbeforeunload="top.fired(window); return null;">
Child loaded.
<script>
if (top.loaded)
top.loaded();