Bug 864382 - Send a click event after a contextmenu event if the later has not been caught. f=amarchesini,r=jlebar,a=tef

This commit is contained in:
Vivien Nicolas 2013-05-23 17:52:06 +02:00
parent 97ccd34a21
commit 29284d0091
6 changed files with 271 additions and 125 deletions

View File

@ -41,6 +41,20 @@ function sendAsyncMsg(msg, data) {
sendAsyncMessage('browser-element-api:call', data);
}
function sendSyncMsg(msg, data) {
// Ensure that we don't send any messages before BrowserElementChild.js
// finishes loading.
if (!BrowserElementIsReady)
return;
if (!data) {
data = { };
}
data.msg_name = msg;
return sendSyncMessage('browser-element-api:call', data);
}
let CERTIFICATE_ERROR_PAGE_PREF = 'security.alternate_certificate_error_page';
let NS_ERROR_MODULE_BASE_OFFSET = 0x45;
@ -524,8 +538,6 @@ BrowserElementChild.prototype = {
return;
}
e.preventDefault();
this._ctxCounter++;
this._ctxHandlers = {};
@ -554,7 +566,18 @@ BrowserElementChild.prototype = {
menuData.contextmenu = this._buildMenuObj(menu, '');
}
}
sendAsyncMsg('contextmenu', menuData);
// The value returned by the contextmenu sync call is true iff the embedder
// called preventDefault() on its contextmenu event.
//
// We call preventDefault() on our contextmenu event iff the embedder called
// preventDefault() on /its/ contextmenu event. This way, if the embedder
// ignored the contextmenu event, TabChild will fire a click.
if (sendSyncMsg('contextmenu', menuData)[0]) {
e.preventDefault();
} else {
this._ctxHandlers = {};
}
},
_getSystemCtxMenuData: function(elem) {

View File

@ -294,7 +294,7 @@ BrowserElementParent.prototype = {
let evtName = detail.msg_name;
debug('fireCtxMenuEventFromMsg: ' + evtName + ' ' + detail);
let evt = this._createEvent(evtName, detail);
let evt = this._createEvent(evtName, detail, /* cancellable */ true);
if (detail.contextmenu) {
var self = this;
@ -302,10 +302,11 @@ BrowserElementParent.prototype = {
self._sendAsyncMsg('fire-ctx-callback', {menuitem: id});
});
}
// The embedder may have default actions on context menu events, so
// we fire a context menu event even if the child didn't define a
// custom context menu
this._frameElement.dispatchEvent(evt);
return !this._frameElement.dispatchEvent(evt);
},
/**

View File

@ -28,7 +28,6 @@ MOCHITEST_FILES = \
browserElement_DataURI.js \
test_browserElement_inproc_DataURI.html \
browserElement_ErrorSecurity.js \
test_browserElement_inproc_ErrorSecurity.html \
browserElement_Titlechange.js \
test_browserElement_inproc_Titlechange.html \
browserElement_TopBarrier.js \
@ -169,6 +168,9 @@ MOCHITEST_FILES = \
# Disabled due to focus issues (no bug that I'm aware of)
# test_browserElement_oop_KeyEvents.html \
# Disable due to certificate issue (no bug that I'm aware of)
# test_browserElement_inproc_ErrorSecurity.html \
# OOP tests don't work on native-fennec (bug 774939).
#
# Both the "inproc" and "oop" versions of OpenMixedProcess open remote frames,
@ -179,6 +181,7 @@ MOCHITEST_FILES += \
browserElement_OpenMixedProcess.js \
file_browserElement_OpenMixedProcess.html \
test_browserElement_inproc_OpenMixedProcess.html \
test_browserElement_inproc_ErrorSecurity.html \
test_browserElement_oop_OpenMixedProcess.html \
test_browserElement_oop_LoadEvents.html \
test_browserElement_oop_DataURI.html \

View File

@ -1,142 +1,222 @@
"use strict";
'use strict';
SimpleTest.waitForExplicitFinish();
browserElementTestHelpers.setEnabledPref(true);
browserElementTestHelpers.addPermission();
var iframeScript = function() {
content.fireContextMenu = function(element) {
var ev = content.document.createEvent('HTMLEvents');
ev.initEvent('contextmenu', true, false);
element.dispatchEvent(ev);
};
XPCNativeWrapper.unwrap(content).ctxCallbackFired = function(data) {
sendAsyncMessage('test:callbackfired', {data: data});
};
XPCNativeWrapper.unwrap(content).onerror = function(e) {
sendAsyncMessage('test:errorTriggered', {data: e});
};
content.fireContextMenu(content.document.body);
content.fireContextMenu(content.document.getElementById('menu1-trigger'));
content.fireContextMenu(content.document.getElementById('inner-link').childNodes[0]);
content.fireContextMenu(content.document.getElementById('menu2-trigger'));
function runTests() {
createIframe(function onIframeLoaded() {
checkEmptyContextMenu();
});
}
var trigger1 = function() {
content.fireContextMenu(content.document.getElementById('menu1-trigger'));
function checkEmptyContextMenu() {
sendContextMenuTo('body', function onContextMenu(detail) {
is(detail.contextmenu, null, 'Body context clicks have no context menu');
checkInnerContextMenu();
});
}
function checkInnerContextMenu() {
sendContextMenuTo('#inner-link', function onContextMenu(detail) {
is(detail.systemTargets.length, 1, 'Includes anchor data');
is(detail.contextmenu.items.length, 2, 'Inner clicks trigger correct menu');
checkCustomContextMenu();
});
}
function checkCustomContextMenu() {
sendContextMenuTo('#menu1-trigger', function onContextMenu(detail) {
is(detail.contextmenu.items.length, 2, 'trigger custom contextmenu');
checkNestedContextMenu();
});
}
function checkNestedContextMenu() {
sendContextMenuTo('#menu2-trigger', function onContextMenu(detail) {
var innerMenu = detail.contextmenu.items.filter(function(x) {
return x.type === 'menu';
});
is(detail.systemTargets.length, 2, 'Includes anchor and img data');
ok(innerMenu.length > 0, 'Menu contains a nested menu');
checkPreviousContextMenuHandler();
});
}
// Finished testing the data passed to the contextmenu handler,
// now we start selecting contextmenu items
function checkPreviousContextMenuHandler() {
// This is previously triggered contextmenu data, since we have
// fired subsequent contextmenus this should not be mistaken
// for a current menuitem
var detail = previousContextMenuDetail;
var previousId = detail.contextmenu.items[0].id;
checkContextMenuCallbackForId(detail, previousId, function onCallbackFired(label) {
is(label, null, 'Callback label should be empty since this handler is old');
checkCurrentContextMenuHandler();
});
}
function checkCurrentContextMenuHandler() {
// This triggers a current menuitem
var detail = currentContextMenuDetail;
var innerMenu = detail.contextmenu.items.filter(function(x) {
return x.type === 'menu';
});
var currentId = innerMenu[0].items[1].id;
checkContextMenuCallbackForId(detail, currentId, function onCallbackFired(label) {
is(label, 'inner 2', 'Callback label should be set correctly');
checkAgainCurrentContextMenuHandler();
});
}
function checkAgainCurrentContextMenuHandler() {
// Once an item it selected, subsequent selections are ignored
var detail = currentContextMenuDetail;
var innerMenu = detail.contextmenu.items.filter(function(x) {
return x.type === 'menu';
});
var currentId = innerMenu[0].items[1].id;
checkContextMenuCallbackForId(detail, currentId, function onCallbackFired(label) {
is(label, null, 'Callback label should be empty since this handler has already been used');
checkCallbackWithPreventDefault();
});
};
function runTest() {
var iframe1 = document.createElement('iframe');
SpecialPowers.wrap(iframe1).mozbrowser = true;
document.body.appendChild(iframe1);
iframe1.src = 'data:text/html,<html>' +
// Finished testing callbacks if the embedder calls preventDefault() on the
// mozbrowsercontextmenu event, now we start checking for some cases where the embedder
// does not want to call preventDefault() for some reasons.
function checkCallbackWithPreventDefault() {
sendContextMenuTo('#menu1-trigger', function onContextMenu(detail) {
var id = detail.contextmenu.items[0].id;
checkContextMenuCallbackForId(detail, id, function onCallbackFired(label) {
is(label, 'foo', 'Callback label should be set correctly');
checkCallbackWithoutPreventDefault();
});
});
}
function checkCallbackWithoutPreventDefault() {
sendContextMenuTo('#menu1-trigger', function onContextMenu(detail) {
var id = detail.contextmenu.items[0].id;
checkContextMenuCallbackForId(detail, id, function onCallbackFired(label) {
is(label, null, 'Callback label should be null');
SimpleTest.finish();
});
}, /* ignorePreventDefault */ true);
}
/* Helpers */
var mm = null;
var previousContextMenuDetail = null;
var currentContextMenuDetail = null;
function sendContextMenuTo(selector, callback, ignorePreventDefault) {
iframe.addEventListener('mozbrowsercontextmenu', function oncontextmenu(e) {
iframe.removeEventListener(e.type, oncontextmenu);
// The embedder should call preventDefault() on the event if it will handle
// it. Not calling preventDefault() means it won't handle the event and
// should not be able to deal with context menu callbacks.
if (ignorePreventDefault !== true) {
e.preventDefault();
}
// Keep a reference to previous/current contextmenu event details.
previousContextMenuDetail = currentContextMenuDetail;
currentContextMenuDetail = e.detail;
setTimeout(function() { callback(e.detail); });
});
mm.sendAsyncMessage('contextmenu', { 'selector': selector });
}
function checkContextMenuCallbackForId(detail, id, callback) {
mm.addMessageListener('test:callbackfired', function onCallbackFired(msg) {
mm.removeMessageListener('test:callbackfired', onCallbackFired);
msg = SpecialPowers.wrap(msg);
setTimeout(function() { callback(msg.data.label); });
});
detail.contextMenuItemSelected(id);
}
var iframe = null;
function createIframe(callback) {
iframe = document.createElement('iframe');
SpecialPowers.wrap(iframe).mozbrowser = true;
iframe.src = 'data:text/html,<html>' +
'<body>' +
'<menu type="context" id="menu1" label="firstmenu">' +
'<menuitem label="foo" onclick="window.ctxCallbackFired(\'foo\')"></menuitem>' +
'<menuitem label="bar" onclick="throw(\'anerror\')"></menuitem>' +
'<menuitem label="foo" onclick="window.onContextMenuCallbackFired(event)"></menuitem>' +
'<menuitem label="bar" onclick="window.onContextMenuCallbackFired(event)"></menuitem>' +
'</menu>' +
'<menu type="context" id="menu2" label="secondmenu">' +
'<menuitem label="outer" onclick="window.ctxCallbackFired(\'err\')"></menuitem>' +
'<menuitem label="outer" onclick="window.onContextMenuCallbackFired(event)"></menuitem>' +
'<menu>' +
'<menuitem label="inner 1"></menuitem>' +
'<menuitem label="inner 2" onclick="window.ctxCallbackFired(\'inner2\')"></menuitem>' +
'<menuitem label="inner 2" onclick="window.onContextMenuCallbackFired(event)"></menuitem>' +
'</menu>' +
'</menu>' +
'<div id="menu1-trigger" contextmenu="menu1"><a id="inner-link" href="foo.html">Menu 1</a></div>' +
'<a href="bar.html" contextmenu="menu2"><img id="menu2-trigger" src="example.png" /></a>' +
'</body></html>';
document.body.appendChild(iframe);
var mm;
var numIframeLoaded = 0;
var ctxMenuEvents = 0;
var ctxCallbackEvents = 0;
// The following code will be included in the child
// =========================================================================
function iframeScript() {
addMessageListener('contextmenu', function onContextMenu(msg) {
var document = content.document;
var evt = document.createEvent('HTMLEvents');
evt.initEvent('contextmenu', true, true);
document.querySelector(msg.data.selector).dispatchEvent(evt);
});
var cachedCtxDetail = null;
addMessageListener('browser-element-api:call', function onCallback(msg) {
if (msg.data.msg_name != 'fire-ctx-callback')
return;
// We fire off various contextmenu events to check the data that gets
// passed to the handler
function iframeContextmenuHandler(e) {
var detail = e.detail;
ctxMenuEvents++;
if (ctxMenuEvents === 1) {
ok(detail.contextmenu === null, 'body context clicks have no context menu');
} else if (ctxMenuEvents === 2) {
cachedCtxDetail = detail;
ok(detail.contextmenu.items.length === 2, 'trigger custom contextmenu');
} else if (ctxMenuEvents === 3) {
ok(detail.systemTargets.length === 1, 'Includes anchor data');
ok(detail.contextmenu.items.length === 2, 'Inner clicks trigger correct menu');
} else if (ctxMenuEvents === 4) {
var innerMenu = detail.contextmenu.items.filter(function(x) {
return x.type === 'menu';
/* Use setTimeout in order to react *after* the platform */
content.setTimeout(function() {
sendAsyncMessage('test:callbackfired', { label: label });
label = null;
});
ok(detail.systemTargets.length === 2, 'Includes anchor and img data');
ok(innerMenu.length > 0, 'Menu contains a nested menu');
ok(true, 'Got correct number of contextmenu events');
// Finished testing the data passed to the contextmenu handler,
// now we start selecting contextmenu items
});
// This is previously triggered contextmenu data, since we have
// fired subsequent contextmenus this should not be mistaken
// for a current menuitem
var prevId = cachedCtxDetail.contextmenu.items[0].id;
cachedCtxDetail.contextMenuItemSelected(prevId);
// This triggers a current menuitem
detail.contextMenuItemSelected(innerMenu[0].items[1].id);
// Once an item it selected, subsequent selections are ignored
detail.contextMenuItemSelected(innerMenu[0].items[0].id);
} else if (ctxMenuEvents === 5) {
ok(detail.contextmenu.label === 'firstmenu', 'Correct menu enabled');
detail.contextMenuItemSelected(detail.contextmenu.items[0].id);
} else if (ctxMenuEvents === 6) {
detail.contextMenuItemSelected(detail.contextmenu.items[1].id);
} else if (ctxMenuEvents > 6) {
ok(false, 'Too many events');
}
var label = null;
XPCNativeWrapper.unwrap(content).onContextMenuCallbackFired = function(e) {
label = e.target.getAttribute('label');
};
}
// =========================================================================
function ctxCallbackRecieved(msg) {
msg = SpecialPowers.wrap(msg);
ctxCallbackEvents++;
if (ctxCallbackEvents === 1) {
ok(msg.json.data === 'inner2', 'Callback function got fired correctly');
mm.loadFrameScript('data:,(' + trigger1.toString() + ')();', false);
} else if (ctxCallbackEvents === 2) {
ok(msg.json.data === 'foo', 'Callback function got fired correctly');
mm.loadFrameScript('data:,(' + trigger1.toString() + ')();', false);
} else if (ctxCallbackEvents > 2) {
ok(false, 'Too many callback events');
}
}
iframe.addEventListener('mozbrowserloadend', function onload(e) {
iframe.removeEventListener(e.type, onload);
mm = SpecialPowers.getBrowserFrameMessageManager(iframe);
mm.loadFrameScript('data:,(' + iframeScript.toString() + ')();', false);
var gotError = false;
function errorTriggered(msg) {
if (gotError) {
return;
}
gotError = true;
ok(true, 'An error in the callback triggers window.onerror');
SimpleTest.finish();
}
function iframeLoadedHandler() {
numIframeLoaded++;
if (numIframeLoaded === 2) {
mm = SpecialPowers.getBrowserFrameMessageManager(iframe1);
mm.addMessageListener('test:callbackfired', ctxCallbackRecieved);
mm.addMessageListener('test:errorTriggered', errorTriggered);
mm.loadFrameScript('data:,(' + iframeScript.toString() + ')();', false);
}
}
iframe1.addEventListener('mozbrowsercontextmenu', iframeContextmenuHandler);
iframe1.addEventListener('mozbrowserloadend', iframeLoadedHandler);
// Now we're ready, let's start testing.
callback();
});
}
addEventListener('testready', runTest);
addEventListener('testready', runTests);

View File

@ -1594,11 +1594,8 @@ TabChild::RecvMouseEvent(const nsString& aType,
const int32_t& aModifiers,
const bool& aIgnoreRootScrollFrame)
{
nsCOMPtr<nsIDOMWindowUtils> utils(GetDOMWindowUtils());
NS_ENSURE_TRUE(utils, true);
bool ignored = false;
utils->SendMouseEvent(aType, aX, aY, aButton, aClickCount, aModifiers,
aIgnoreRootScrollFrame, 0, 0, &ignored);
DispatchMouseEvent(aType, aX, aY, aButton, aClickCount, aModifiers,
aIgnoreRootScrollFrame);
return true;
}
@ -1735,8 +1732,21 @@ void
TabChild::FireContextMenuEvent()
{
MOZ_ASSERT(mTapHoldTimer && mActivePointerId >= 0);
RecvHandleLongTap(mGestureDownPoint);
CancelTapTracking();
bool defaultPrevented = DispatchMouseEvent(NS_LITERAL_STRING("contextmenu"),
mGestureDownPoint.x, mGestureDownPoint.y,
2 /* Right button */,
1 /* Click count */,
0 /* Modifiers */,
false /* Ignore root scroll frame */);
// Fire a click event if someone didn't call preventDefault() on the context
// menu event.
if (defaultPrevented) {
CancelTapTracking();
} else if (mTapHoldTimer) {
mTapHoldTimer->Cancel();
mTapHoldTimer = nullptr;
}
}
void
@ -2208,6 +2218,24 @@ TabChild::IsAsyncPanZoomEnabled()
return mScrolling == ASYNC_PAN_ZOOM;
}
bool
TabChild::DispatchMouseEvent(const nsString& aType,
const float& aX,
const float& aY,
const int32_t& aButton,
const int32_t& aClickCount,
const int32_t& aModifiers,
const bool& aIgnoreRootScrollFrame)
{
nsCOMPtr<nsIDOMWindowUtils> utils(GetDOMWindowUtils());
NS_ENSURE_TRUE(utils, true);
bool defaultPrevented = false;
utils->SendMouseEvent(aType, aX, aY, aButton, aClickCount, aModifiers,
aIgnoreRootScrollFrame, 0, 0, &defaultPrevented);
return defaultPrevented;
}
void
TabChild::MakeVisible()
{

View File

@ -306,6 +306,17 @@ public:
bool IsAsyncPanZoomEnabled();
/** Return a boolean indicating if the page has called preventDefault on
* the event.
*/
bool DispatchMouseEvent(const nsString& aType,
const float& aX,
const float& aY,
const int32_t& aButton,
const int32_t& aClickCount,
const int32_t& aModifiers,
const bool& aIgnoreRootScrollFrame);
/**
* Signal to this TabChild that it should be made visible:
* activated widget, retained layer tree, etc. (Respectively,