Backed out 4 changesets (bug 1669172) for marionette failures on test_navigation.py . CLOSED TREE

Backed out changeset 226b7b7e0ee8 (bug 1669172)
Backed out changeset 07e433cbf1d8 (bug 1669172)
Backed out changeset 74add9dbce91 (bug 1669172)
Backed out changeset 8781218cfac4 (bug 1669172)
This commit is contained in:
Narcis Beleuzu 2021-02-14 01:06:49 +02:00
Родитель 537c2f8aa6
Коммит 891e453d2f
33 изменённых файлов: 2728 добавлений и 130 удалений

Просмотреть файл

@ -43,6 +43,21 @@ marionette:
instance-size: default
chunks: 1
marionette-framescript:
description: "Marionette unittest run with its JSWindowActors disabled"
treeherder-symbol: MnFr
max-run-time: 5400
instance-size: default
tier: 2
run-on-projects:
# Platforms for which we release geckodriver
by-test-platform:
(linux|windows|macos)(?!.*shippable).*: built-projects
default: []
mozharness:
extra-options:
- --disable-actors
marionette-headless:
description: "Marionette headless unittest run"
treeherder-symbol: MnH

Просмотреть файл

@ -31,6 +31,7 @@ linux1804-64/opt:
- awsy
- desktop-screenshot-capture
- linux1804-tests
- marionette-framescript
- marionette-headless
- mochitest-headless
- mochitest-webgpu
@ -51,6 +52,7 @@ linux1804-64-shippable/opt:
- awsy
- desktop-screenshot-capture
- linux1804-tests
- marionette-framescript
- marionette-headless
- mochitest-headless
- mochitest-webgpu
@ -181,6 +183,7 @@ windows7-32/opt:
test-sets:
- awsy
- desktop-screenshot-capture
- marionette-framescript
- windows-reftest-gpu
- windows-talos
- windows-tests
@ -195,6 +198,7 @@ windows7-32-shippable/opt:
test-sets:
- awsy
- desktop-screenshot-capture
- marionette-framescript
- windows-reftest-gpu
- windows-talos
- windows-tests
@ -251,6 +255,7 @@ windows10-64/opt:
- awsy
- desktop-screenshot-capture
- windows-talos
- marionette-framescript
- windows-tests
- windows10-tests
- web-platform-tests
@ -278,6 +283,7 @@ windows10-64-shippable/opt:
- awsy
- desktop-screenshot-capture
- windows-talos
- marionette-framescript
- windows-tests
- windows10-tests
- web-platform-tests
@ -375,6 +381,7 @@ macosx1014-64-qr/opt:
test-sets:
- macosx1014-64-qr-tests
- desktop-screenshot-capture
- marionette-framescript
- web-platform-tests
- web-platform-tests-backlog
- web-platform-tests-wdspec-headless
@ -387,6 +394,7 @@ macosx1014-64-shippable-qr/opt:
- awsy
- browsertime
- desktop-screenshot-capture
- marionette-framescript
- raptor-chrome
- raptor-chromium
- raptor-firefox

Просмотреть файл

@ -193,6 +193,9 @@ mochitest-headless:
mochitest-valgrind:
- mochitest-valgrind
marionette-framescript:
- marionette-framescript
marionette-headless:
- marionette-headless

Просмотреть файл

@ -407,6 +407,8 @@ action.PointerType.get = function(str) {
* Input state associated with current session. This is a map between
* input ID and the device state for that input source, with one entry
* for each active input source.
*
* Re-initialized in listener.js.
*/
action.inputStateMap = new Map();
@ -414,6 +416,8 @@ action.inputStateMap = new Map();
* List of {@link action.Action} associated with current session. Used to
* manage dispatching events when resetting the state of the input sources.
* Reset operations are assumed to be idempotent.
*
* Re-initialized in listener.js
*/
action.inputsToCancel = [];

Просмотреть файл

@ -83,6 +83,7 @@ class MarionetteCommandsParent extends JSWindowActorParent {
}
// Proxying methods for WebDriver commands
// TODO: Maybe using a proxy class instead similar to proxy.js
clearElement(webEl) {
return this.sendQuery("MarionetteCommandsParent:clearElement", {

Просмотреть файл

@ -165,6 +165,8 @@ browser.Context = class {
// Used to set curFrameId upon new session
this.newSession = true;
this.seenEls = new element.Store();
// A reference to the tab corresponding to the current window handle,
// if any. Specifically, this.tab refers to the last tab that Marionette
// switched to in this browser window. Note that this may not equal the

Просмотреть файл

@ -704,7 +704,7 @@ function isObjectEmpty(obj) {
return isObject(obj) && Object.keys(obj).length === 0;
}
// Services.dirsvc is not accessible from JSWindowActor child,
// Services.dirsvc is not accessible from content frame scripts,
// but we should not panic about that.
function maybeProfile() {
try {

Просмотреть файл

@ -69,3 +69,12 @@ disabling auto-updates, Telemetry, and first-run UX.
The user preference file takes presedence over the recommended
preferences, meaning any user-defined preference value will not be
overridden.
`marionette.contentListener`
----------------------------
Used internally in Marionette for determining whether content scripts
can safely be reused. Should not be tweaked manually.
This preference is scheduled for removal.

Просмотреть файл

@ -1,9 +1,9 @@
element module
==============
element.ReferenceStore
----------------------
.. js:autoclass:: element.ReferenceStore
element.Store
-------------
.. js:autoclass:: element.Store
:members:
element.find

Просмотреть файл

@ -0,0 +1,2 @@
listener module
===============

Просмотреть файл

@ -0,0 +1,4 @@
proxy module
============
.. js:autoclass:: proxy
:members:

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -68,10 +68,9 @@ const XUL_SELECTED_ELS = new Set([
* web element reference for every element representing the same element
* is the same.
*
* The {@link element.ReferenceStore} provides a mapping between web element
* references and the ContentDOMReference of DOM elements for each browsing
* context. It also provides functionality for looking up and retrieving
* elements.
* The {@link element.Store} provides a mapping between web element
* references and DOM elements for each browsing context. It also provides
* functionality for looking up and retrieving elements.
*
* @namespace
*/
@ -88,6 +87,167 @@ element.Strategy = {
XPath: "xpath",
};
/**
* Stores known/seen elements and their associated web element
* references.
*
* Elements are added by calling {@link #add()} or {@link addAll()},
* and may be queried by their web element reference using {@link get()}.
*
* @class
* @memberof element
*/
element.Store = class {
constructor() {
this.els = {};
}
clear() {
this.els = {};
}
/**
* Make a collection of elements seen.
*
* The order of the returned web element references is guaranteed to
* match that of the collection passed in.
*
* @param {NodeList} els
* Sequence of elements to add to set of seen elements.
*
* @return {Array.<WebElement>}
* List of the web element references associated with each element
* from <var>els</var>.
*/
addAll(els) {
let add = this.add.bind(this);
return [...els].map(add);
}
/**
* Make an element seen.
*
* @param {(Element|WindowProxy|XULElement)} el
* Element to add to set of seen elements.
*
* @return {WebElement}
* Web element reference associated with element.
*
* @throws {TypeError}
* If <var>el</var> is not an {@link Element} or a {@link XULElement}.
*/
add(el) {
const isDOMElement = element.isDOMElement(el);
const isDOMWindow = element.isDOMWindow(el);
const isXULElement = element.isXULElement(el);
const context = element.isInXULDocument(el) ? "chrome" : "content";
if (!(isDOMElement || isDOMWindow || isXULElement)) {
throw new TypeError(
"Expected an element or WindowProxy, " + pprint`got: ${el}`
);
}
for (let i in this.els) {
let foundEl;
try {
foundEl = this.els[i].get();
} catch (e) {}
if (foundEl) {
if (new XPCNativeWrapper(foundEl) == new XPCNativeWrapper(el)) {
return WebElement.fromUUID(i, context);
}
// cleanup reference to gc'd element
} else {
delete this.els[i];
}
}
let webEl = WebElement.from(el);
this.els[webEl.uuid] = Cu.getWeakReference(el);
return webEl;
}
/**
* Determine if the provided web element reference has been seen
* before/is in the element store.
*
* Unlike when getting the element, a staleness check is not
* performed.
*
* @param {WebElement} webEl
* Element's associated web element reference.
*
* @return {boolean}
* True if element is in the store, false otherwise.
*
* @throws {TypeError}
* If <var>webEl</var> is not a {@link WebElement}.
*/
has(webEl) {
if (!(webEl instanceof WebElement)) {
throw new TypeError(pprint`Expected web element, got: ${webEl}`);
}
return Object.keys(this.els).includes(webEl.uuid);
}
/**
* Retrieve a DOM {@link Element} or a {@link XULElement} by its
* unique {@link WebElement} reference.
*
* @param {WebElement} webEl
* Web element reference to find the associated {@link Element}
* of.
* @param {WindowProxy} win
* Current window global, which may differ from the associated
* window global of <var>el</var>.
*
* @returns {(Element|XULElement)}
* Element associated with reference.
*
* @throws {TypeError}
* If <var>webEl</var> is not a {@link WebElement}.
* @throws {NoSuchElementError}
* If the web element reference <var>uuid</var> has not been
* seen before.
* @throws {StaleElementReferenceError}
* If the element has gone stale, indicating it is no longer
* attached to the DOM, or its node document is no longer the
* active document.
*/
get(webEl, win) {
if (!(webEl instanceof WebElement)) {
throw new TypeError(pprint`Expected web element, got: ${webEl}`);
}
if (!this.has(webEl)) {
throw new error.NoSuchElementError(
"Web element reference not seen before: " + webEl.uuid
);
}
let el;
let ref = this.els[webEl.uuid];
try {
el = ref.get();
} catch (e) {
delete this.els[webEl.uuid];
}
if (element.isStale(el, win)) {
throw new error.StaleElementReferenceError(
pprint`The element reference of ${el || webEl.uuid} is stale; ` +
"either the element is no longer attached to the DOM, " +
"it is not in the current frame context, " +
"or the document has been refreshed"
);
}
return el;
}
};
/**
* Stores known/seen web element references and their associated
* ContentDOMReference ElementIdentifiers.
@ -100,6 +260,7 @@ element.Strategy = {
* webElRef: {element-6066-11e4-a52e-4f735466cecf: <uuid>} }
*
* For use in parent process in conjunction with ContentDOMReference in content.
* Implements all `element.Store` methods for duck typing.
*
* @class
* @memberof element

Просмотреть файл

@ -177,17 +177,18 @@ evaluate.sandbox = function(
};
/**
* Convert any web elements in arbitrary objects to a ContentDOMReference by
* looking them up in the seen element reference store. For ElementIdentifiers a
* new entry in the seen element reference store gets added when running in the
* Convert any web elements in arbitrary objects to DOM elements by
* looking them up in the seen element store. For ElementIdentifiers a new
* entry in the seen element reference store gets added when running in the
* parent process, otherwise ContentDOMReference is used to retrieve the DOM
* node.
*
* @param {Object} obj
* Arbitrary object containing web elements or ElementIdentifiers.
* @param {element.ReferenceStore=} seenEls
* @param {(element.Store|element.ReferenceStore)=} seenEls
* Known element store to look up web elements from. If `seenEls` is an
* instance of `element.ReferenceStore`, return WebElement. If `seenEls` is
* instance of `element.ReferenceStore`, return WebElement. If `seenEls`
* is an instance of `element.Store`, return Element. If `seenEls` is
* `undefined` the Element from the ContentDOMReference cache is returned
* when executed in the child process, in the parent process the WebElement
* is passed-through.
@ -199,12 +200,12 @@ evaluate.sandbox = function(
* replaced by DOM elements.
*
* @throws {NoSuchElementError}
* If `seenEls` is an `element.ReferenceStore` and the web element reference
* has not been seen before.
* If `seenEls` is an `element.Store` and the web element reference has not
* been seen before.
* @throws {StaleElementReferenceError}
* If `seenEls` is an `element.ReferenceStore` and the element has gone
* stale, indicating it is no longer attached to the DOM, or its node
* document is no longer the active document.
* If `seenEls` is an `element.ReferenceStore` or `element.Store` and the
* element has gone stale, indicating it is no longer attached to the DOM,
* or its node document is no longer the active document.
*/
evaluate.fromJSON = function(obj, seenEls = undefined, win = undefined) {
switch (typeof obj) {
@ -232,6 +233,18 @@ evaluate.fromJSON = function(obj, seenEls = undefined, win = undefined) {
return element.resolveElement(obj, win);
}
throw new TypeError("seenEls is not an instance of ReferenceStore");
// WebElement and Store (used by framescript)
} else if (WebElement.isReference(obj)) {
const webEl = WebElement.fromJSON(obj);
if (seenEls instanceof element.Store) {
// Child: Get web element from the store
return seenEls.get(webEl, win);
} else if (!seenEls) {
// Parent: No conversion. Just return the web element
return webEl;
}
throw new TypeError("seenEls is not an instance of Store");
}
// arbitrary objects
@ -254,8 +267,8 @@ evaluate.fromJSON = function(obj, seenEls = undefined, win = undefined) {
* - Collections, such as `Array<`, `NodeList`, `HTMLCollection`
* et al. are expanded to arrays and then recursed.
*
* - Elements that are not known web elements are added to the
* ContentDOMReference registry. Once known, the elements'
* - Elements that are not known web elements are added to the `seenEls` element
* store, or the ContentDOMReference registry. Once known, the elements'
* associated web element representation is returned.
*
* - WebElements are transformed to the corresponding ElementIdentifier
@ -271,7 +284,7 @@ evaluate.fromJSON = function(obj, seenEls = undefined, win = undefined) {
* @param {Object} obj
* Object to be marshaled.
*
* @param {element.ReferenceStore=} seenEls
* @param {(element.Store|element.ReferenceStore)=} seenEls
* Element store to use for lookup of web element references.
*
* @return {Object}
@ -304,18 +317,31 @@ evaluate.toJSON = function(obj, seenEls) {
// WebElement
} else if (WebElement.isReference(obj)) {
// Parent: Convert to ElementIdentifier for use in child actor
return seenEls.get(WebElement.fromJSON(obj));
if (seenEls instanceof element.ReferenceStore) {
return seenEls.get(WebElement.fromJSON(obj));
}
return obj;
// ElementIdentifier
} else if (WebElement.isReference(obj.webElRef)) {
// Parent: Pass-through ElementIdentifiers to the child
return obj;
if (seenEls instanceof element.ReferenceStore) {
return obj;
}
// Parent: Otherwise return the web element
return WebElement.fromJSON(obj.webElRef);
// Element (HTMLElement, SVGElement, XULElement, et al.)
} else if (element.isElement(obj)) {
// Parent
if (seenEls instanceof element.ReferenceStore) {
throw new TypeError(`ReferenceStore can't be used with Element`);
// Child: Add element to the Store, return as WebElement
} else if (seenEls instanceof element.Store) {
return seenEls.add(obj);
}
// If no storage has been specified assume we are in a child process.

Просмотреть файл

@ -15,6 +15,7 @@ from .marionette_test import (
skip,
skip_if_chrome,
skip_if_desktop,
skip_if_framescript,
SkipTest,
skip_unless_browser_pref,
skip_unless_protocol,

Просмотреть файл

@ -17,6 +17,7 @@ from .decorators import (
run_if_manage_instance,
skip_if_chrome,
skip_if_desktop,
skip_if_framescript,
skip_unless_browser_pref,
skip_unless_protocol,
with_parameters,

Просмотреть файл

@ -102,6 +102,24 @@ def skip_if_desktop(reason):
return decorator
def skip_if_framescript(reason):
"""Decorator which skips a test if the framescript implementation is used."""
def decorator(test_item):
if not isinstance(test_item, types.FunctionType):
raise Exception("Decorator only supported for functions")
@functools.wraps(test_item)
def skip_wrapper(self, *args, **kwargs):
if self.marionette.get_pref("marionette.actors.enabled") is False:
raise SkipTest(reason)
return test_item(self, *args, **kwargs)
return skip_wrapper
return decorator
def skip_unless_browser_pref(reason, pref, predicate=bool):
"""Decorator which skips a test based on the value of a browser preference.

Просмотреть файл

@ -421,6 +421,13 @@ class BaseMarionetteArguments(ArgumentParser):
help="Enable python post-mortem debugger when a test fails."
" Pass in the debugger you want to use, eg pdb or ipdb.",
)
self.add_argument(
"--disable-actors",
action="store_true",
dest="disable_actors",
default=False,
help="Disable the usage of JSWindowActors in Marionette.",
)
self.add_argument(
"--enable-fission",
action="store_true",
@ -650,6 +657,7 @@ class BaseMarionetteTestRunner(object):
verbose=0,
emulator=False,
headless=False,
disable_actors=False,
enable_fission=False,
enable_webrender=False,
**kwargs
@ -697,6 +705,14 @@ class BaseMarionetteTestRunner(object):
self.headless = headless
self.enable_webrender = enable_webrender
self.disable_actors = disable_actors
if self.disable_actors:
self.prefs.update(
{
"marionette.actors.enabled": False,
}
)
self.enable_fission = enable_fission
if self.enable_fission:
self.prefs.update(
@ -1138,6 +1154,7 @@ class BaseMarionetteTestRunner(object):
"appname": self.appName,
"manage_instance": self.marionette.instance is not None,
"headless": self.headless,
"actors": not self.disable_actors,
"webrender": self.enable_webrender,
}
)

Просмотреть файл

@ -378,9 +378,17 @@ def test_manifest_basic_args(mock_runner, manifest, monkeypatch):
assert kwargs["exists"] is False
assert kwargs["disabled"] is True
assert kwargs["appname"] == "fake_app"
assert kwargs["actors"] is True
assert "mozinfo_key" in kwargs and kwargs["mozinfo_key"] == "mozinfo_val"
def test_manifest_actors_disabled(mock_runner, manifest, monkeypatch):
kwargs = get_kwargs_passed_to_manifest(
mock_runner, manifest, monkeypatch, disable_actors=True
)
assert kwargs["actors"] is False
@pytest.mark.parametrize("test_tags", (None, ["tag", "tag2"]))
def test_manifest_with_test_tags(mock_runner, manifest, monkeypatch, test_tags):
kwargs = get_kwargs_passed_to_manifest(

Просмотреть файл

@ -4,7 +4,7 @@
from __future__ import absolute_import
from marionette_driver import By
from marionette_driver import By, errors
from marionette_driver.keys import Keys
from marionette_harness import MarionetteTestCase, WindowManagerMixin
@ -14,6 +14,10 @@ class TestPointerActions(WindowManagerMixin, MarionetteTestCase):
def setUp(self):
super(TestPointerActions, self).setUp()
self.actors_enabled = self.marionette.get_pref("marionette.actors.enabled")
if self.actors_enabled is None:
self.actors_enabled = True
self.mouse_chain = self.marionette.actions.sequence(
"pointer", "pointer_id", {"pointerType": "mouse"}
)
@ -30,7 +34,8 @@ class TestPointerActions(WindowManagerMixin, MarionetteTestCase):
self.marionette.switch_to_window(self.win)
def tearDown(self):
self.marionette.actions.release()
if self.actors_enabled:
self.marionette.actions.release()
self.close_all_windows()
super(TestPointerActions, self).tearDown()
@ -38,24 +43,32 @@ class TestPointerActions(WindowManagerMixin, MarionetteTestCase):
def test_click_action(self):
box = self.marionette.find_element(By.ID, "testBox")
box.get_property("localName")
self.assertFalse(
self.marionette.execute_script(
"return document.getElementById('testBox').checked"
if self.actors_enabled:
self.assertFalse(
self.marionette.execute_script(
"return document.getElementById('testBox').checked"
)
)
)
self.mouse_chain.click(element=box).perform()
self.assertTrue(
self.marionette.execute_script(
"return document.getElementById('testBox').checked"
self.mouse_chain.click(element=box).perform()
self.assertTrue(
self.marionette.execute_script(
"return document.getElementById('testBox').checked"
)
)
)
else:
with self.assertRaises(errors.UnsupportedOperationException):
self.mouse_chain.click(element=box).perform()
def test_key_action(self):
self.marionette.find_element(By.ID, "textInput").click()
self.key_chain.send_keys("x").perform()
self.assertEqual(
self.marionette.execute_script(
"return document.getElementById('textInput').value"
),
"testx",
)
if self.actors_enabled:
self.key_chain.send_keys("x").perform()
self.assertEqual(
self.marionette.execute_script(
"return document.getElementById('textInput').value"
),
"testx",
)
else:
with self.assertRaises(errors.UnsupportedOperationException):
self.key_chain.send_keys("x").perform()

Просмотреть файл

@ -191,7 +191,7 @@ arguments[0](4);
)
# Functions defined in higher privilege scopes, such as the privileged
# JSWindowActor child runs in, cannot be accessed from
# content frame script listener.js runs in, cannot be accessed from
# content. This tests that it is possible to introspect the objects on
# `arguments` without getting permission defined errors. This is made
# possible because the last argument is always the callback/complete

Просмотреть файл

@ -15,6 +15,7 @@ from marionette_driver.marionette import Alert
from marionette_harness import (
MarionetteTestCase,
run_if_manage_instance,
skip_if_framescript,
skip_unless_browser_pref,
WindowManagerMixin,
)
@ -911,6 +912,7 @@ class TestPageLoadStrategy(BaseNavigationTestCase):
super(TestPageLoadStrategy, self).tearDown()
@skip_if_framescript("Bug 1675173: Won't be fixed for framescript mode")
def test_none(self):
self.marionette.delete_session()
self.marionette.start_session({"pageLoadStrategy": "none"})

Просмотреть файл

@ -11,6 +11,7 @@ from marionette_driver.keys import Keys
from marionette_harness import (
MarionetteTestCase,
skip_if_framescript,
WindowManagerMixin,
)
@ -87,6 +88,9 @@ class TestSendkeysMenupopup(WindowManagerMixin, MarionetteTestCase):
self.testwindow_el.send_keys(Keys.ESCAPE)
self.wait_for_context_menu_closed()
@skip_if_framescript(
"Bug 1675173: Interactability is only checked with actors enabled"
)
def test_sendkeys_closed_menu(self):
# send_keys should throw for the menupopup if the contextmenu is closed.
with self.assertRaises(errors.ElementNotInteractableException):
@ -96,6 +100,9 @@ class TestSendkeysMenupopup(WindowManagerMixin, MarionetteTestCase):
with self.assertRaises(errors.ElementNotInteractableException):
self.menuitem_el.send_keys(Keys.ESCAPE)
@skip_if_framescript(
"Bug 1675173: Interactability is only checked with actors enabled"
)
def test_sendkeys_hidden_disabled_menuitem(self):
self.open_context_menu()

Просмотреть файл

@ -198,7 +198,7 @@ async function webdriverClickElement(el, a11y) {
// step 10
// if the click causes navigation, the post-navigation checks are
// handled by navigate.js
// handled by the load listener in listener.js
}
async function chromeClick(el, a11y) {

Просмотреть файл

@ -30,6 +30,7 @@ marionette.jar:
content/interaction.js (interaction.js)
content/l10n.js (l10n.js)
content/legacyaction.js (legacyaction.js)
content/listener.js (listener.js)
content/log.js (log.js)
content/message.js (message.js)
content/modal.js (modal.js)
@ -37,6 +38,7 @@ marionette.jar:
content/packets.js (packets.js)
content/prefs.js (prefs.js)
content/print.js (print.js)
content/proxy.js (proxy.js)
content/reftest.js (reftest.js)
content/reftest.xhtml (reftest.xhtml)
content/reftest-content.js (reftest-content.js)

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -16,6 +16,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
EventDispatcher:
"chrome://marionette/content/actors/MarionetteEventsParent.jsm",
Log: "chrome://marionette/content/log.js",
MarionettePrefs: "chrome://marionette/content/prefs.js",
modal: "chrome://marionette/content/modal.js",
PageLoadStrategy: "chrome://marionette/content/capabilities.js",
TimedPromise: "chrome://marionette/content/sync.js",
@ -251,7 +252,7 @@ navigate.waitForNavigationCompleted = async function waitForNavigationCompleted(
const onTimer = timer => {
// In the case when a document has a beforeunload handler
// registered, the currently active command will return immediately
// due to the modal dialog observer.
// due to the modal dialog observer in proxy.js.
//
// Otherwise the timeout waiting for the document to start
// navigating is increased by 5000 ms to ensure a possible load
@ -276,11 +277,19 @@ navigate.waitForNavigationCompleted = async function waitForNavigationCompleted(
}
};
const onNavigation = (eventName, data) => {
// Only care about navigation events from the actor of the current frame.
// Bug 1674329: Always use the currently active browsing context,
// and not the original one to not cause hangs for remoteness changes.
if (data.browsingContext != browsingContextFn()) {
const onNavigation = ({ json }, message) => {
let data = MarionettePrefs.useActors ? message : json;
if (MarionettePrefs.useActors) {
// Only care about navigation events from the actor of the current frame.
// Bug 1674329: Always use the currently active browsing context,
// and not the original one to not cause hangs for remoteness changes.
if (data.browsingContext != browsingContextFn()) {
return;
}
} else if (
data.browsingContext.browserId != browsingContextFn().browserId
) {
return;
}
@ -341,7 +350,15 @@ navigate.waitForNavigationCompleted = async function waitForNavigationCompleted(
"browsing-context-discarded"
);
EventDispatcher.on("page-load", onNavigation);
if (MarionettePrefs.useActors) {
EventDispatcher.on("page-load", onNavigation);
} else {
driver.mm.addMessageListener(
"Marionette:NavigationEvent",
onNavigation,
true
);
}
return new TimedPromise(
async (resolve, reject) => {
@ -384,6 +401,14 @@ navigate.waitForNavigationCompleted = async function waitForNavigationCompleted(
driver.dialogObserver?.remove(onDialogOpened);
unloadTimer?.cancel();
EventDispatcher.off("page-load", onNavigation);
if (MarionettePrefs.useActors) {
EventDispatcher.off("page-load", onNavigation);
} else {
driver.mm.removeMessageListener(
"Marionette:NavigationEvent",
onNavigation,
true
);
}
});
};

Просмотреть файл

@ -231,6 +231,14 @@ class MarionetteBranch extends Branch {
get recommendedPrefs() {
return this.get("prefs.recommended", true);
}
/**
* Temporary preference to enable the usage of the JSWindowActor
* implementation for commands that already support Fission.
*/
get useActors() {
return this.get("actors.enabled", true);
}
}
/** Reads a JSON serialised blob stored in the environment. */

340
testing/marionette/proxy.js Normal file
Просмотреть файл

@ -0,0 +1,340 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const EXPORTED_SYMBOLS = ["proxy"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyModuleGetters(this, {
Log: "chrome://marionette/content/log.js",
error: "chrome://marionette/content/error.js",
evaluate: "chrome://marionette/content/evaluate.js",
MessageManagerDestroyedPromise: "chrome://marionette/content/sync.js",
modal: "chrome://marionette/content/modal.js",
});
XPCOMUtils.defineLazyGetter(this, "logger", () => Log.get());
XPCOMUtils.defineLazyServiceGetter(
this,
"uuidgen",
"@mozilla.org/uuid-generator;1",
"nsIUUIDGenerator"
);
// Proxy handler that traps requests to get a property. Will prioritise
// properties that exist on the object's own prototype.
const ownPriorityGetterTrap = {
get: (obj, prop) => {
if (obj.hasOwnProperty(prop)) {
return obj[prop];
}
return (...args) => obj.send(prop, args);
},
};
/** @namespace */
this.proxy = {};
/**
* Creates a transparent interface between the chrome- and content
* contexts.
*
* Calls to this object will be proxied via the message manager to a
* content frame script, and responses are returend as promises.
*
* The argument sequence is serialised and passed as an array, unless it
* consists of a single object type that isn't null, in which case it's
* passed literally. The latter specialisation is temporary to achieve
* backwards compatibility with listener.js.
*
* @param {function(string, Object, number)} sendAsyncFn
* Callback for sending async messages.
* @param {function(): browser.Context} browserFn
* Closure that returns the current browsing context.
*/
proxy.toListener = function(sendAsyncFn, browserFn) {
let sender = new proxy.AsyncMessageChannel(sendAsyncFn, browserFn);
return new Proxy(sender, ownPriorityGetterTrap);
};
/**
* Provides a transparent interface between chrome- and content space.
*
* The AsyncMessageChannel is an abstraction of the message manager
* IPC architecture allowing calls to be made to any registered message
* listener in Marionette. The <code>#send(...)</code> method
* returns a promise that gets resolved when the message handler calls
* <code>.reply(...)</code>.
*/
proxy.AsyncMessageChannel = class {
constructor(sendAsyncFn, browserFn) {
this.sendAsync = sendAsyncFn;
this.browserFn_ = browserFn;
// TODO(ato): Bug 1242595
this.activeMessageId = null;
this.listeners_ = new Map();
this.dialogHandler = null;
this.closeHandler = null;
}
get browser() {
return this.browserFn_();
}
/**
* Send a message across the channel. The name of the function to
* call must be registered as a message listener.
*
* Usage:
*
* <pre><code>
* let channel = new AsyncMessageChannel(
* messageManager, sendAsyncMessage.bind(this));
* let rv = await channel.send("remoteFunction", ["argument"]);
* </code></pre>
*
* @param {string} name
* Function to call in the listener, e.g. for the message listener
* <tt>Marionette:foo8</tt>, use <tt>foo</tt>.
* @param {Array.<?>=} args
* Argument list to pass the function. If args has a single entry
* that is an object, we assume it's an old style dispatch, and
* the object will passed literally.
*
* @return {Promise}
* A promise that resolves to the result of the command.
* @throws {TypeError}
* If an unsupported reply type is received.
* @throws {WebDriverError}
* If an error is returned over the channel.
*/
send(name, args = []) {
let uuid = uuidgen.generateUUID().toString();
// TODO(ato): Bug 1242595
this.activeMessageId = uuid;
return new Promise((resolve, reject) => {
let path = proxy.AsyncMessageChannel.makePath(uuid);
let cb = msg => {
this.activeMessageId = null;
let { data, type } = msg.json;
switch (msg.json.type) {
case proxy.AsyncMessageChannel.ReplyType.Ok:
case proxy.AsyncMessageChannel.ReplyType.Value:
let payload = evaluate.fromJSON(data);
resolve(payload);
break;
case proxy.AsyncMessageChannel.ReplyType.Error:
let err = error.WebDriverError.fromJSON(data);
reject(err);
break;
default:
throw new TypeError(`Unknown async response type: ${type}`);
}
};
// The currently selected tab or window is closing. Make sure to wait
// until it's fully gone.
this.closeHandler = async ({ type, target }) => {
logger.trace(`Received DOM event ${type} for ${target}`);
let messageManager;
switch (type) {
case "unload":
messageManager = this.browser.window.messageManager;
break;
case "TabClose":
messageManager = this.browser.messageManager;
break;
}
await new MessageManagerDestroyedPromise(messageManager);
this.removeHandlers();
resolve();
};
// A modal or tab modal dialog has been opened. To be able to handle it,
// the active command has to be aborted. Therefore remove all handlers,
// and cancel any ongoing requests in the listener.
this.dialogHandler = (action, dialogRef, win) => {
// Only care about modals of the currently selected window.
if (win !== this.browser.window) {
return;
}
this.removeAllListeners_();
// TODO(ato): It's not ideal to have listener specific behaviour here:
this.sendAsync("cancelRequest");
this.removeHandlers();
resolve();
};
// start content message listener, and install handlers for
// modal dialogues, and window/tab state changes.
this.addListener_(path, cb);
this.addHandlers();
// sendAsync is GeckoDriver#sendAsync
this.sendAsync(name, marshal(args), uuid);
});
}
/**
* Add all necessary handlers for events and observer notifications.
*/
addHandlers() {
this.browser.driver.dialogObserver.add(this.dialogHandler.bind(this));
// Register event handlers in case the command closes the current
// tab or window, and the promise has to be escaped.
if (this.browser) {
this.browser.window.addEventListener("unload", this.closeHandler);
if (this.browser.tab) {
let node = this.browser.tab.addEventListener
? this.browser.tab
: this.browser.contentBrowser;
node.addEventListener("TabClose", this.closeHandler);
}
}
}
/**
* Remove all registered handlers for events and observer notifications.
*/
removeHandlers() {
this.browser.driver.dialogObserver.remove(this.dialogHandler.bind(this));
if (this.browser) {
this.browser.window.removeEventListener("unload", this.closeHandler);
if (this.browser.tab) {
let node = this.browser.tab.addEventListener
? this.browser.tab
: this.browser.contentBrowser;
if (node) {
node.removeEventListener("TabClose", this.closeHandler);
}
}
}
}
/**
* Reply to an asynchronous request.
*
* Passing an {@link WebDriverError} prototype will cause the receiving
* channel to throw this error.
*
* Usage:
*
* <pre><code>
* let channel = proxy.AsyncMessageChannel(
* messageManager, sendAsyncMessage.bind(this));
*
* // throws in requester:
* channel.reply(uuid, new error.WebDriverError());
*
* // returns with value:
* channel.reply(uuid, "hello world!");
*
* // returns with undefined:
* channel.reply(uuid);
* </pre></code>
*
* @param {UUID} uuid
* Unique identifier of the request.
* @param {*} obj
* Message data to reply with.
*/
reply(uuid, obj = undefined) {
// TODO(ato): Eventually the uuid will be hidden in the dispatcher
// in listener, and passing it explicitly to this function will be
// unnecessary.
if (typeof obj == "undefined") {
this.sendReply_(uuid, proxy.AsyncMessageChannel.ReplyType.Ok);
} else if (error.isError(obj)) {
let err = error.wrap(obj);
this.sendReply_(uuid, proxy.AsyncMessageChannel.ReplyType.Error, err);
} else {
this.sendReply_(uuid, proxy.AsyncMessageChannel.ReplyType.Value, obj);
}
}
sendReply_(uuid, type, payload = undefined) {
const path = proxy.AsyncMessageChannel.makePath(uuid);
let data = evaluate.toJSON(payload);
const msg = { type, data };
// here sendAsync is actually the content frame's
// sendAsyncMessage(path, message) global
this.sendAsync(path, msg);
}
/**
* Produces a path, or a name, for the message listener handler that
* listens for a reply.
*
* @param {UUID} uuid
* Unique identifier of the channel request.
*
* @return {string}
* Path to be used for nsIMessageListener.addMessageListener.
*/
static makePath(uuid) {
return "Marionette:asyncReply:" + uuid;
}
addListener_(path, callback) {
let autoRemover = msg => {
this.removeListener_(path);
this.removeHandlers();
callback(msg);
};
Services.mm.addMessageListener(path, autoRemover);
this.listeners_.set(path, autoRemover);
}
removeListener_(path) {
if (!this.listeners_.has(path)) {
return true;
}
let l = this.listeners_.get(path);
Services.mm.removeMessageListener(path, l);
return this.listeners_.delete(path);
}
removeAllListeners_() {
let ok = true;
for (let [p] of this.listeners_) {
ok |= this.removeListener_(p);
}
return ok;
}
};
proxy.AsyncMessageChannel.ReplyType = {
Ok: 0,
Value: 1,
Error: 2,
};
function marshal(args) {
if (args.length == 1 && typeof args[0] == "object") {
return args[0];
}
return args;
}

Просмотреть файл

@ -637,6 +637,11 @@ max-width: ${width}px; max-height: ${height}px`;
browser.changeRemoteness({ remoteType });
browser.construct();
// XXX: This appears to be working fine as is, should we be reinitializing
// something here? If so, what? The listener.js framescript is registered
// on the reftest.xhtml chrome window (which shouldn't be changing?), and
// driver.js uses the global message manager to listen for messages.
}
}

Просмотреть файл

@ -185,6 +185,7 @@ class TCPConnection {
this.lastID = 0;
this.driver = driverFactory();
this.driver.init();
}
/**
@ -193,6 +194,7 @@ class TCPConnection {
*/
onClosed() {
this.driver.deleteSession();
this.driver.uninit();
if (this.onclose) {
this.onclose(this);
}

Просмотреть файл

@ -65,6 +65,7 @@ const domElId = { id: 1, browsingContextId: 4, webElRef: domWebEl.toJSON() };
const svgElId = { id: 2, browsingContextId: 5, webElRef: svgWebEl.toJSON() };
const xulElId = { id: 3, browsingContextId: 6, webElRef: xulWebEl.toJSON() };
const seenEls = new element.Store();
const elementIdCache = new element.ReferenceStore();
add_test(function test_toJSON_types() {
@ -80,6 +81,11 @@ add_test(function test_toJSON_types() {
// collections
deepEqual([], evaluate.toJSON([]));
// elements
ok(evaluate.toJSON(domEl, seenEls) instanceof WebElement);
ok(evaluate.toJSON(svgEl, seenEls) instanceof WebElement);
ok(evaluate.toJSON(xulEl, seenEls) instanceof WebElement);
// toJSON
equal(
"foo",
@ -119,6 +125,31 @@ add_test(function test_toJSON_types_ReferenceStore() {
});
add_test(function test_toJSON_sequences() {
const input = [
null,
true,
[],
domEl,
{
toJSON() {
return "foo";
},
},
{ bar: "baz" },
];
const actual = evaluate.toJSON(input, seenEls);
equal(null, actual[0]);
equal(true, actual[1]);
deepEqual([], actual[2]);
ok(actual[3] instanceof WebElement);
equal("foo", actual[4]);
deepEqual({ bar: "baz" }, actual[5]);
run_next_test();
});
add_test(function test_toJSON_sequences_ReferenceStore() {
const input = [
null,
true,
@ -155,6 +186,33 @@ add_test(function test_toJSON_sequences() {
});
add_test(function test_toJSON_objects() {
const input = {
null: null,
boolean: true,
array: [],
webElement: domEl,
elementId: domElId,
toJSON: {
toJSON() {
return "foo";
},
},
object: { bar: "baz" },
};
const actual = evaluate.toJSON(input, seenEls);
equal(null, actual.null);
equal(true, actual.boolean);
deepEqual([], actual.array);
ok(actual.webElement instanceof WebElement);
ok(actual.elementId instanceof WebElement);
equal("foo", actual.toJSON);
deepEqual({ bar: "baz" }, actual.object);
run_next_test();
});
add_test(function test_toJSON_objects_ReferenceStore() {
const input = {
null: null,
boolean: true,
@ -208,11 +266,41 @@ add_test(function test_fromJSON_ReferenceStore() {
deepEqual(webEl, domWebEl);
deepEqual(elementIdCache.get(webEl), domElId);
// Store doesn't contain ElementIdentifiers
Assert.throws(
() => evaluate.fromJSON(domElId, seenEls),
/TypeError/,
"Expected element.ReferenceStore"
);
elementIdCache.clear();
run_next_test();
});
add_test(function test_fromJSON_Store() {
// Pass-through WebElements without adding it to the element store
let webEl = evaluate.fromJSON(domWebEl.toJSON());
deepEqual(webEl, domWebEl);
ok(!seenEls.has(domWebEl));
// Find element in the element store
webEl = seenEls.add(domEl);
const el = evaluate.fromJSON(webEl.toJSON(), seenEls);
deepEqual(el, domEl);
// Reference store doesn't contain web elements
Assert.throws(
() => evaluate.fromJSON(domWebEl.toJSON(), elementIdCache),
/TypeError/,
"Expected element.Store"
);
seenEls.clear();
run_next_test();
});
add_test(function test_isCyclic_noncyclic() {
for (let type of [true, 42, "foo", [], {}, null, undefined]) {
ok(!evaluate.isCyclic(type));

Просмотреть файл

@ -145,6 +145,15 @@ class MarionetteTest(TestingMixin, MercurialScript, TransferMixin, CodeCoverageM
"help": "Permits a software GL implementation (such as LLVMPipe) to use the GL compositor.", # NOQA: E501
},
],
[
["--disable-actors"],
{
"action": "store_true",
"dest": "disable_actors",
"default": False,
"help": "Disable the usage of JSWindowActors in Marionette.",
},
],
[
["--enable-webrender"],
{
@ -342,6 +351,9 @@ class MarionetteTest(TestingMixin, MercurialScript, TransferMixin, CodeCoverageM
if self.config.get("app_arg"):
config_fmt_args["app_arg"] = self.config["app_arg"]
if self.config["disable_actors"]:
cmd.append("--disable-actors")
if self.config["enable_webrender"]:
cmd.append("--enable-webrender")