Bug 941085: File uploads support in Marionette

Adds support for W3C WebDriver compatible file uploads, where additional
calls to sendKeys on <input type=file multiple> will append files,
rather than reset the field.

r=dburns

--HG--
extra : rebase_source : 5f058fd1fcf767a5b45ebb6ba4c32994eb52212e
This commit is contained in:
Andreas Tolfsen 2015-03-31 16:45:27 +01:00
Родитель 5abff40de2
Коммит f785cf403f
9 изменённых файлов: 300 добавлений и 30 удалений

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

@ -18,7 +18,6 @@ from runner import (
MarionetteTestResult, MarionetteTestResult,
MarionetteTextTestRunner, MarionetteTextTestRunner,
MemoryEnduranceTestCaseMixin, MemoryEnduranceTestCaseMixin,
MozHttpd,
OptionParser, OptionParser,
TestManifest, TestManifest,
TestResult, TestResult,

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

@ -5,7 +5,7 @@
from base import ( from base import (
B2GTestResultMixin, BaseMarionetteOptions, BaseMarionetteTestRunner, B2GTestResultMixin, BaseMarionetteOptions, BaseMarionetteTestRunner,
Marionette, MarionetteTest, MarionetteTestResult, MarionetteTextTestRunner, Marionette, MarionetteTest, MarionetteTestResult, MarionetteTextTestRunner,
MozHttpd, OptionParser, TestManifest, TestResult, TestResultCollection OptionParser, TestManifest, TestResult, TestResultCollection
) )
from mixins import ( from mixins import (
B2GTestCaseMixin, B2GTestResultMixin, EnduranceOptionsMixin, B2GTestCaseMixin, B2GTestResultMixin, EnduranceOptionsMixin,

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

@ -20,12 +20,13 @@ from manifestparser import TestManifest
from manifestparser.filters import tags from manifestparser.filters import tags
from marionette_driver.marionette import Marionette from marionette_driver.marionette import Marionette
from mixins.b2g import B2GTestResultMixin, get_b2g_pid, get_dm from mixins.b2g import B2GTestResultMixin, get_b2g_pid, get_dm
from mozhttpd import MozHttpd
from mozlog.structured.structuredlog import get_default_logger from mozlog.structured.structuredlog import get_default_logger
from moztest.adapters.unit import StructuredTestRunner, StructuredTestResult from moztest.adapters.unit import StructuredTestRunner, StructuredTestResult
from moztest.results import TestResultCollection, TestResult, relevant_line from moztest.results import TestResultCollection, TestResult, relevant_line
import mozversion import mozversion
import httpd
here = os.path.abspath(os.path.dirname(__file__)) here = os.path.abspath(os.path.dirname(__file__))
@ -638,24 +639,6 @@ class BaseMarionetteTestRunner(object):
self.skipped = 0 self.skipped = 0
self.failures = [] self.failures = []
def start_httpd(self, need_external_ip):
if self.server_root is None or os.path.isdir(self.server_root):
host = '127.0.0.1'
if need_external_ip:
host = moznetwork.get_ip()
docroot = self.server_root or os.path.join(os.path.dirname(here), 'www')
if not os.path.isdir(docroot):
raise Exception('Server root %s is not a valid path' % docroot)
self.httpd = MozHttpd(host=host,
port=0,
docroot=docroot)
self.httpd.start()
self.marionette.baseurl = 'http://%s:%d/' % (host, self.httpd.httpd.server_port)
self.logger.info('running webserver on %s' % self.marionette.baseurl)
else:
self.marionette.baseurl = self.server_root
self.logger.info('using content from %s' % self.marionette.baseurl)
def _build_kwargs(self): def _build_kwargs(self):
kwargs = { kwargs = {
'device_serial': self.device_serial, 'device_serial': self.device_serial,
@ -780,7 +763,9 @@ setReq.onerror = function() {
if not self.httpd: if not self.httpd:
self.logger.info("starting httpd") self.logger.info("starting httpd")
self.start_httpd(need_external_ip) self.httpd = self.create_httpd(need_external_ip)
self.marionette.baseurl = self.httpd.get_url()
self.logger.info("running httpd on %s" % self.marionette.baseurl)
for test in tests: for test in tests:
self.add_test(test) self.add_test(test)
@ -861,6 +846,15 @@ setReq.onerror = function() {
self.logger.suite_end() self.logger.suite_end()
def create_httpd(self, need_external_ip):
ip = "127.0.0.1"
if need_external_ip:
ip = moznetwork.get_ip()
root = self.server_root or os.path.join(os.path.dirname(here), "www")
rv = httpd.FixtureServer(root, host=ip)
rv.start()
return rv
def add_test(self, test, expected='pass', test_container=None): def add_test(self, test, expected='pass', test_container=None):
filepath = os.path.abspath(test) filepath = os.path.abspath(test)

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

@ -0,0 +1,57 @@
# 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/.
import os
from mozhttpd import MozHttpd
class FixtureServer(object):
def __init__(self, root, host="127.0.0.1", port=0):
if not os.path.isdir(root):
raise Exception("Server root is not a valid path: %s" % root)
self.root = root
self.host = host
self.port = port
self._server = None
def start(self, block=False):
if self.alive:
return
self._server = MozHttpd(host=self.host, port=self.port, docroot=self.root, urlhandlers=[
{"method": "POST", "path": "/file_upload", "function": upload_handler}])
self._server.start(block=block)
self.port = self._server.httpd.server_port
self.base_url = self.get_url()
def stop(self):
if not self.alive:
return
self._server.stop()
self._server = None
@property
def alive(self):
return self._server is not None
def get_url(self, path="/"):
if not self.alive:
raise "Server not started"
return self._server.get_url(path)
def upload_handler(query, postdata=None):
return (200, {}, query.headers.getheader("Content-Type"))
if __name__ == "__main__":
here = os.path.abspath(os.path.dirname(__file__))
root = os.path.join(os.path.dirname(here), "www")
httpd = FixtureServer(root, port=2829)
print "Started fixture server on http://%s:%d/" % (httpd.host, httpd.port)
try:
httpd.start(True)
except KeyboardInterrupt:
pass

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

@ -0,0 +1,135 @@
# 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/.
import contextlib
import urllib
from tempfile import NamedTemporaryFile as tempfile
from marionette import MarionetteTestCase, skip
from marionette_driver import By, errors, expected
from marionette_driver.wait import Wait
single = "data:text/html,%s" % urllib.quote("<input type=file>")
multiple = "data:text/html,%s" % urllib.quote("<input type=file multiple>")
upload = lambda url: "data:text/html,%s" % urllib.quote("""
<form action='%s' method=post enctype='multipart/form-data'>
<input type=file>
<input type=submit>
</form>""" % url)
class TestFileUpload(MarionetteTestCase):
def test_sets_one_file(self):
self.marionette.navigate(single)
input = self.input
exp = None
with tempfile() as f:
input.send_keys(f.name)
exp = [f.name]
files = self.get_files(input)
self.assertEqual(len(files), 1)
self.assertFilesEqual(files, exp)
def test_sets_multiple_files(self):
self.marionette.navigate(multiple)
input = self.input
exp = None
with contextlib.nested(tempfile(), tempfile()) as (a, b):
input.send_keys(a.name)
input.send_keys(b.name)
exp = [a.name, b.name]
files = self.get_files(input)
self.assertEqual(len(files), 2)
self.assertFilesEqual(files, exp)
def test_sets_multiple_indentical_files(self):
self.marionette.navigate(multiple)
input = self.input
exp = []
with tempfile() as f:
input.send_keys(f.name)
input.send_keys(f.name)
exp = f.name
files = self.get_files(input)
self.assertEqual(len(files), 2)
self.assertFilesEqual(files, exp)
def test_clear_file(self):
self.marionette.navigate(single)
input = self.input
with tempfile() as f:
input.send_keys(f.name)
self.assertEqual(len(self.get_files(input)), 1)
input.clear()
self.assertEqual(len(self.get_files(input)), 0)
def test_clear_files(self):
self.marionette.navigate(multiple)
input = self.input
with contextlib.nested(tempfile(), tempfile()) as (a, b):
input.send_keys(a.name)
input.send_keys(b.name)
self.assertEqual(len(self.get_files(input)), 2)
input.clear()
self.assertEqual(len(self.get_files(input)), 0)
def test_illegal_file(self):
self.marionette.navigate(single)
with self.assertRaisesRegexp(errors.MarionetteException, "File not found"):
self.input.send_keys("rochefort")
def test_upload(self):
self.marionette.navigate(
upload(self.marionette.absolute_url("file_upload")))
with tempfile() as f:
f.write("camembert")
f.flush()
self.input.send_keys(f.name)
self.submit.click()
self.assertIn("multipart/form-data", self.body.text)
def find_inputs(self):
return self.marionette.find_elements("tag name", "input")
@property
def input(self):
return self.find_inputs()[0]
@property
def submit(self):
return self.find_inputs()[1]
@property
def body(self):
return Wait(self.marionette).until(
expected.element_present(By.TAG_NAME, "body"))
def get_files(self, el):
# This is horribly complex because (1) Marionette doesn't serialise arrays properly,
# and (2) accessing File.name in the content JS throws a permissions
# error.
fl = self.marionette.execute_script(
"return arguments[0].files", script_args=[el])
return [f["name"] for f in [v for k, v in fl.iteritems() if k.isdigit()]]
def assertFilesEqual(self, act, exp):
# File array returned from browser doesn't contain full path names,
# this cuts off the path of the expected files.
filenames = [f.rsplit("/", 0)[-1] for f in act]
self.assertListEqual(filenames, act)

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

@ -154,3 +154,6 @@ b2g = false
b2g = false b2g = false
[test_teardown_context_preserved.py] [test_teardown_context_preserved.py]
b2g = false b2g = false
[test_file_upload.py]
b2g = false
skip-if = os == "win" # http://bugs.python.org/issue14574

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

@ -884,8 +884,9 @@ GeckoDriver.prototype.executeScriptInSandbox = function(
directInject, directInject,
async, async,
timeout) { timeout) {
if (directInject && async && (timeout == null || timeout == 0)) if (directInject && async && (timeout == null || timeout == 0)) {
throw new TimeoutError("Please set a timeout"); throw new TimeoutError("Please set a timeout");
}
if (this.importedScripts.exists()) { if (this.importedScripts.exists()) {
let stream = Cc["@mozilla.org/network/file-input-stream;1"] let stream = Cc["@mozilla.org/network/file-input-stream;1"]
@ -2307,6 +2308,10 @@ GeckoDriver.prototype.getElementRect = function(cmd, resp) {
GeckoDriver.prototype.sendKeysToElement = function(cmd, resp) { GeckoDriver.prototype.sendKeysToElement = function(cmd, resp) {
let {id, value} = cmd.parameters; let {id, value} = cmd.parameters;
if (!value) {
throw new IllegalArgumentError(`Expected character sequence: ${value}`);
}
switch (this.context) { switch (this.context) {
case Context.CHROME: case Context.CHROME:
let win = this.getCurrentWindow(); let win = this.getCurrentWindow();
@ -2322,7 +2327,36 @@ GeckoDriver.prototype.sendKeysToElement = function(cmd, resp) {
break; break;
case Context.CONTENT: case Context.CONTENT:
let err;
let listener = function(msg) {
this.mm.removeMessageListener("Marionette:setElementValue", listener);
let val = msg.data.value;
let el = msg.objects.element;
let win = this.getCurrentWindow();
if (el.type == "file") {
Cu.importGlobalProperties(["File"]);
let fs = Array.prototype.slice.call(el.files);
let file;
try {
file = new File(val);
} catch (e) {
err = new IllegalArgumentError(`File not found: ${val}`);
}
fs.push(file);
el.mozSetFileArray(fs);
} else {
el.value = val;
}
}.bind(this);
this.mm.addMessageListener("Marionette:setElementValue", listener);
yield this.listener.sendKeysToElement({id: id, value: value}); yield this.listener.sendKeysToElement({id: id, value: value});
this.mm.removeMessageListener("Marionette:setElementValue", listener);
if (err) {
throw err;
}
break; break;
} }
}; };

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

@ -10,7 +10,11 @@ const errors = [
"ElementNotVisibleError", "ElementNotVisibleError",
"FrameSendFailureError", "FrameSendFailureError",
"FrameSendNotInitializedError", "FrameSendNotInitializedError",
<<<<<<< dest
"InvalidElementStateError", "InvalidElementStateError",
=======
"IllegalArgumentError",
>>>>>>> source
"JavaScriptError", "JavaScriptError",
"NoAlertOpenError", "NoAlertOpenError",
"NoSuchElementError", "NoSuchElementError",
@ -295,11 +299,23 @@ this.UnsupportedOperationError = function(msg) {
}; };
UnsupportedOperationError.prototype = Object.create(WebDriverError.prototype); UnsupportedOperationError.prototype = Object.create(WebDriverError.prototype);
this.IllegalArgumentError = function(msg) {
WebDriverError.call(this, msg);
this.name = "IllegalArgumentError";
this.status = "illegal argument";
this.code = 13; // unknown error
};
IllegalArgumentError.prototype = Object.create(WebDriverError.prototype);
const errorObjs = [ const errorObjs = [
this.ElementNotVisibleError, this.ElementNotVisibleError,
this.FrameSendFailureError, this.FrameSendFailureError,
this.FrameSendNotInitializedError, this.FrameSendNotInitializedError,
<<<<<<< dest
this.InvalidElementStateError, this.InvalidElementStateError,
=======
this.IllegalArgumentError,
>>>>>>> source
this.JavaScriptError, this.JavaScriptError,
this.NoAlertOpenError, this.NoAlertOpenError,
this.NoSuchElementError, this.NoSuchElementError,

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

@ -35,9 +35,10 @@ let isB2G = false;
let marionetteTestName; let marionetteTestName;
let winUtil = content.QueryInterface(Ci.nsIInterfaceRequestor) let winUtil = content.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils); .getInterface(Ci.nsIDOMWindowUtils);
let listenerId = null; //unique ID of this listener let listenerId = null; // unique ID of this listener
let curFrame = content; let curFrame = content;
let isRemoteBrowser = () => curFrame.contentWindow !== null;
let previousFrame = null; let previousFrame = null;
let elementManager = new ElementManager([]); let elementManager = new ElementManager([]);
let accessibility = new Accessibility(); let accessibility = new Accessibility();
@ -1548,11 +1549,39 @@ function isElementSelected(msg) {
*/ */
function sendKeysToElement(msg) { function sendKeysToElement(msg) {
let command_id = msg.json.command_id; let command_id = msg.json.command_id;
let val = msg.json.value;
let el = elementManager.getKnownElement(msg.json.id, curFrame); let el = elementManager.getKnownElement(msg.json.id, curFrame);
let keysToSend = msg.json.value; if (el.type == "file") {
let p = val.join("");
utils.sendKeysToElement(curFrame, el, keysToSend, sendOk, sendError, command_id); // for some reason using mozSetFileArray doesn't work with e10s
// enabled (probably a bug), but a workaround is to elevate the element's
// privileges with SpecialPowers
//
// this extra branch can be removed when the e10s bug 1149998 is fixed
if (isRemoteBrowser()) {
let fs = Array.prototype.slice.call(el.files);
let file;
try {
file = new File(p);
} catch (e) {
let err = new IllegalArgumentError(`File not found: ${val}`);
sendError(err.message, err.code, err.stack, command_id);
return;
}
fs.push(file);
let wel = new SpecialPowers(utils.window).wrap(el);
wel.mozSetFileArray(fs);
} else {
sendSyncMessage("Marionette:setElementValue", {value: p}, {element: el});
}
sendOk(command_id);
} else {
utils.sendKeysToElement(curFrame, el, val, sendOk, sendError, command_id);
}
} }
/** /**
@ -1582,10 +1611,13 @@ function clearElement(msg) {
let command_id = msg.json.command_id; let command_id = msg.json.command_id;
try { try {
let el = elementManager.getKnownElement(msg.json.id, curFrame); let el = elementManager.getKnownElement(msg.json.id, curFrame);
utils.clearElement(el); if (el.type == "file") {
el.value = null;
} else {
utils.clearElement(el);
}
sendOk(command_id); sendOk(command_id);
} } catch (e) {
catch (e) {
sendError(e.message, e.code, e.stack, command_id); sendError(e.message, e.code, e.stack, command_id);
} }
} }