Repository: scrapy/scrapy
Pull request #1469 (2015-08-31 21:00:01)

Form submit button plain
Allow `<button type="submit">` controls to submit forms

[Forms - HTML5](http://www.w3.org/TR/html5/forms.html#the-button-element) and [Forms - HTML4](http://www.w3.org/TR/html401/interact/forms.html#h-17.5) both allow `button` controls as form submitter.

This patch extends `_get_clickable()` to allow submit buttons too.
The change from `el.name` and `el.value` to `el.get(...)` is needed,
because lxml parses buttons as `HtmlElement` and not `InputElement`.


Patch:
diff --git a/scrapy/http/request/form.py b/scrapy/http/request/form.py
index 0b1d3b926..1920cefe1 100644
--- a/scrapy/http/request/form.py
+++ b/scrapy/http/request/form.py
@@ -150,14 +150,16 @@ def _get_clickable(clickdata, form):
     if the latter is given. If not, it returns the first
     clickable element found
     """
-    clickables = [el for el in form.xpath('.//input[@type="submit"]')]
+    clickables = [el for el in form.xpath('descendant::input[@type="submit"]'
+                                          '|descendant::button[@type="submit"]'
+                                          '|descendant::button[not(@type)]')]
     if not clickables:
         return
 
     # If we don't have clickdata, we just use the first clickable element
     if clickdata is None:
         el = clickables[0]
-        return (el.name, el.value)
+        return (el.get('name'), el.get('value'))
 
     # If clickdata is given, we compare it to the clickable elements to find a
     # match. We first look to see if the number is specified in clickdata,
@@ -169,7 +171,7 @@ def _get_clickable(clickdata, form):
         except IndexError:
             pass
         else:
-            return (el.name, el.value)
+            return (el.get('name'), el.get('value'))
 
     # We didn't find it, so now we build an XPath expression out of the other
     # arguments, because they can be used as such
@@ -177,7 +179,7 @@ def _get_clickable(clickdata, form):
             u''.join(u'[@%s="%s"]' % c for c in six.iteritems(clickdata))
     el = form.xpath(xpath)
     if len(el) == 1:
-        return (el[0].name, el[0].value)
+        return (el[0].get('name'), el[0].get('value'))
     elif len(el) > 1:
         raise ValueError("Multiple elements found (%r) matching the criteria "
                          "in clickdata: %r" % (el, clickdata))


Test patch:
diff --git a/tests/test_http_request.py b/tests/test_http_request.py
index 02111f075..b87045325 100644
--- a/tests/test_http_request.py
+++ b/tests/test_http_request.py
@@ -733,6 +733,40 @@ class FormRequestTest(RequestTest):
         self.assertRaises(ValueError, self.request_class.from_response,
                           response, formxpath="//form/input[@name='abc']")
 
+    def test_from_response_button_submit(self):
+        response = _buildresponse(
+            """<form action="post.php" method="POST">
+            <input type="hidden" name="test1" value="val1">
+            <input type="hidden" name="test2" value="val2">
+            <button type="submit" name="button1" value="submit1">Submit</button>
+            </form>""",
+            url="http://www.example.com/this/list.html")
+        req = self.request_class.from_response(response)
+        self.assertEqual(req.method, 'POST')
+        self.assertEqual(req.headers['Content-type'], b'application/x-www-form-urlencoded')
+        self.assertEqual(req.url, "http://www.example.com/this/post.php")
+        fs = _qs(req)
+        self.assertEqual(fs[b'test1'], [b'val1'])
+        self.assertEqual(fs[b'test2'], [b'val2'])
+        self.assertEqual(fs[b'button1'], [b'submit1'])
+
+    def test_from_response_button_notype(self):
+        response = _buildresponse(
+            """<form action="post.php" method="POST">
+            <input type="hidden" name="test1" value="val1">
+            <input type="hidden" name="test2" value="val2">
+            <button name="button1" value="submit1">Submit</button>
+            </form>""",
+            url="http://www.example.com/this/list.html")
+        req = self.request_class.from_response(response)
+        self.assertEqual(req.method, 'POST')
+        self.assertEqual(req.headers['Content-type'], b'application/x-www-form-urlencoded')
+        self.assertEqual(req.url, "http://www.example.com/this/post.php")
+        fs = _qs(req)
+        self.assertEqual(fs[b'test1'], [b'val1'])
+        self.assertEqual(fs[b'test2'], [b'val2'])
+        self.assertEqual(fs[b'button1'], [b'submit1'])
+
 def _buildresponse(body, **kwargs):
     kwargs.setdefault('body', body)
     kwargs.setdefault('url', 'http://example.com')