├── .coveragerc ├── .gitignore ├── .travis.yml ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── NOTICE ├── README.rst ├── dev-requirements.txt ├── docs ├── Makefile ├── _themes │ ├── .gitignore │ ├── LICENSE │ ├── flask_theme_support.py │ ├── kr │ │ ├── layout.html │ │ ├── relations.html │ │ ├── static │ │ │ ├── flasky.css_t │ │ │ └── small_flask.css │ │ └── theme.conf │ └── kr_small │ │ ├── layout.html │ │ ├── static │ │ └── flasky.css_t │ │ └── theme.conf ├── api.rst ├── authors.rst ├── conf.py ├── contributing.rst ├── history.rst ├── index.rst ├── installation.rst ├── make.bat ├── modules.rst ├── readme.rst ├── robobrowser.forms.rst ├── robobrowser.rst └── usage.rst ├── robobrowser ├── __init__.py ├── browser.py ├── cache.py ├── compat.py ├── exceptions.py ├── forms │ ├── __init__.py │ ├── fields.py │ └── form.py ├── helpers.py ├── ordereddict.py └── responses.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── fixtures.py ├── test_browser.py ├── test_cache.py ├── test_forms.py ├── test_helpers.py └── utils.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | omit = 3 | robobrowser/ordereddict.py 4 | robobrowser/responses.py 5 | */python?.?/* 6 | */lib-python/?.?/*.py 7 | */pypy/* 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | .idea/ 3 | 4 | # C extensions 5 | *.so 6 | 7 | # Packages 8 | *.egg 9 | *.egg-info 10 | dist 11 | build 12 | eggs 13 | parts 14 | bin 15 | var 16 | sdist 17 | develop-eggs 18 | .installed.cfg 19 | lib 20 | lib64 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | 38 | # Complexity 39 | output/*.html 40 | output/*/index.html 41 | 42 | # Sphinx 43 | docs/_build 44 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | 4 | python: 5 | - "3.4" 6 | - "3.3" 7 | - "2.7" 8 | - "2.6" 9 | - "pypy" 10 | 11 | install: 12 | - python setup.py install 13 | - pip install -U -r dev-requirements.txt 14 | - pip install coverage coveralls nose responses 15 | 16 | before_script: flake8 robobrowser 17 | 18 | script: nosetests --with-coverage --cover-package=robobrowser 19 | 20 | after_success: coveralls 21 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | History 4 | ------- 5 | 6 | 0.5.3 7 | ++++++++++++++++++ 8 | * Improve documentation. Thanks tpugsley and rcutmore for improvements! 9 | * Improve messages in error handling. Thanks again rcutmore! 10 | * Fix default values for 27 | 28 |
' 29 | 30 |
31 | ''' 32 | ), 33 | utils.ArgCatcher( 34 | responses.GET, 'http://robobrowser.com/multi_submit_form/', 35 | body=b''' 36 |
' 37 | 38 | 39 | 40 |
41 | ''' 42 | ), 43 | utils.ArgCatcher( 44 | responses.GET, 'http://robobrowser.com/post_form/', 45 | body=b''' 46 |
' 47 | 48 |
49 |
' 50 | 51 |
52 | ''' 53 | ), 54 | utils.ArgCatcher( 55 | responses.GET, 'http://robobrowser.com/noname/', 56 | body=b''' 57 |
58 | 59 | I have a bike
60 | I have a car 61 |

62 | 63 |
64 | ''' 65 | ), 66 | utils.ArgCatcher( 67 | responses.POST, 'http://robobrowser.com/submit/', 68 | ), 69 | ] 70 | ) 71 | 72 | mock_urls = utils.mock_responses( 73 | [ 74 | utils.ArgCatcher(responses.GET, 'http://robobrowser.com/page1/'), 75 | utils.ArgCatcher(responses.GET, 'http://robobrowser.com/page2/'), 76 | utils.ArgCatcher(responses.GET, 'http://robobrowser.com/page3/'), 77 | utils.ArgCatcher(responses.GET, 'http://robobrowser.com/page4/'), 78 | ] 79 | ) 80 | -------------------------------------------------------------------------------- /tests/test_browser.py: -------------------------------------------------------------------------------- 1 | import mock 2 | import unittest 3 | from nose.tools import * # noqa 4 | 5 | import re 6 | import requests 7 | from bs4 import BeautifulSoup 8 | 9 | from robobrowser.browser import RoboBrowser 10 | from robobrowser import exceptions 11 | 12 | from tests.fixtures import mock_links, mock_urls, mock_forms 13 | 14 | 15 | class TestHeaders(unittest.TestCase): 16 | 17 | @mock_links 18 | def test_user_agent(self): 19 | browser = RoboBrowser(user_agent='freddie') 20 | browser.open('http://robobrowser.com/links/') 21 | assert_true('User-Agent' in browser.session.headers) 22 | assert_equal( 23 | browser.session.headers['User-Agent'], 'freddie' 24 | ) 25 | 26 | def test_default_headers(self): 27 | browser = RoboBrowser() 28 | assert_equal(browser.session.headers, requests.Session().headers) 29 | 30 | 31 | class TestOpen(unittest.TestCase): 32 | 33 | def setUp(self): 34 | self.browser = RoboBrowser() 35 | 36 | @mock.patch('requests.Session.request') 37 | def test_open_default_method(self, mock_request): 38 | url = 'http://robobrowser.com' 39 | self.browser.open(url) 40 | assert_true(mock_request.called) 41 | args = mock_request.mock_calls[0][1] 42 | assert_equal(args, ('get', url)) 43 | 44 | @mock.patch('requests.Session.request') 45 | def test_open_custom_method(self, mock_request): 46 | url = 'http://robobrowser.com' 47 | self.browser.open(url, method='post') 48 | assert_true(mock_request.called) 49 | args = mock_request.mock_calls[0][1] 50 | assert_equal(args, ('post', url)) 51 | 52 | 53 | class TestLinks(unittest.TestCase): 54 | 55 | @mock_links 56 | def setUp(self): 57 | self.browser = RoboBrowser() 58 | self.browser.open('http://robobrowser.com/links/') 59 | 60 | @mock_links 61 | def test_get_link(self): 62 | link = self.browser.get_link() 63 | assert_equal(link.get('href'), '/link1/') 64 | 65 | @mock_links 66 | def test_get_links(self): 67 | links = self.browser.get_links() 68 | assert_equal(len(links), 3) 69 | 70 | @mock_links 71 | def test_follow_link_tag(self): 72 | link = self.browser.get_link(text=re.compile('sheer')) 73 | self.browser.follow_link(link) 74 | assert_equal(self.browser.url, 'http://robobrowser.com/link1/') 75 | 76 | @mock_links 77 | def test_follow_link_no_href(self): 78 | link = BeautifulSoup('nohref').find('a') 79 | assert_raises( 80 | exceptions.RoboError, 81 | lambda: self.browser.follow_link(link) 82 | ) 83 | 84 | 85 | class TestForms(unittest.TestCase): 86 | 87 | def setUp(self): 88 | self.browser = RoboBrowser() 89 | 90 | @mock_forms 91 | def test_get_forms(self): 92 | self.browser.open('http://robobrowser.com/get_form/') 93 | forms = self.browser.get_forms() 94 | assert_equal(len(forms), 2) 95 | 96 | @mock_forms 97 | def test_get_form_by_id(self): 98 | self.browser.open('http://robobrowser.com/get_form/') 99 | form = self.browser.get_form('bass') 100 | assert_equal(form.parsed.get('id'), 'bass') 101 | 102 | @mock_forms 103 | def test_submit_form_get(self): 104 | self.browser.open('http://robobrowser.com/get_form/') 105 | form = self.browser.get_form() 106 | self.browser.submit_form(form) 107 | assert_equal( 108 | self.browser.url, 109 | 'http://robobrowser.com/get_form/?deacon=john' 110 | ) 111 | assert_true(self.browser.state.response.request.body is None) 112 | 113 | @mock_forms 114 | def test_submit_form_multi_submit(self): 115 | self.browser.open('http://robobrowser.com/multi_submit_form/') 116 | form = self.browser.get_form() 117 | submit = form.submit_fields['submit2'] 118 | self.browser.submit_form(form, submit=submit) 119 | assert_equal( 120 | self.browser.url, 121 | 'http://robobrowser.com/multi_submit_form/' 122 | '?deacon=john&submit2=value2' 123 | ) 124 | 125 | @mock_forms 126 | def test_submit_form_post(self): 127 | self.browser.open('http://robobrowser.com/post_form/') 128 | form = self.browser.get_form() 129 | self.browser.submit_form(form) 130 | assert_equal( 131 | self.browser.url, 132 | 'http://robobrowser.com/submit/' 133 | ) 134 | assert_equal( 135 | self.browser.state.response.request.body, 136 | 'deacon=john' 137 | ) 138 | 139 | 140 | class TestFormsInputNoName(unittest.TestCase): 141 | 142 | @mock_forms 143 | def setUp(self): 144 | self.browser = RoboBrowser() 145 | self.browser.open('http://robobrowser.com/noname/') 146 | 147 | @mock_forms 148 | def test_get_forms(self): 149 | forms = self.browser.get_forms() 150 | assert_equal(len(forms), 1) 151 | 152 | 153 | class TestHistoryInternals(unittest.TestCase): 154 | 155 | def setUp(self): 156 | self.browser = RoboBrowser(history=True) 157 | 158 | @mock_urls 159 | def test_open_appends_to_history(self): 160 | assert_equal(len(self.browser._states), 0) 161 | assert_equal(self.browser._cursor, -1) 162 | self.browser.open('http://robobrowser.com/page1/') 163 | assert_equal(len(self.browser._states), 1) 164 | assert_equal(self.browser._cursor, 0) 165 | 166 | @mock_forms 167 | def test_submit_appends_to_history(self): 168 | self.browser.open('http://robobrowser.com/get_form/') 169 | form = self.browser.get_form() 170 | self.browser.submit_form(form) 171 | 172 | assert_equal(len(self.browser._states), 2) 173 | assert_equal(self.browser._cursor, 1) 174 | 175 | @mock_urls 176 | def test_open_clears_history_after_back(self): 177 | self.browser.open('http://robobrowser.com/page1/') 178 | self.browser.open('http://robobrowser.com/page2/') 179 | self.browser.back() 180 | self.browser.open('http://robobrowser.com/page3/') 181 | assert_equal(len(self.browser._states), 2) 182 | assert_equal(self.browser._cursor, 1) 183 | 184 | @mock_urls 185 | def test_state_deque_max_length(self): 186 | browser = RoboBrowser(history=5) 187 | for _ in range(5): 188 | browser.open('http://robobrowser.com/page1/') 189 | assert_equal(len(browser._states), 5) 190 | browser.open('http://robobrowser.com/page2/') 191 | assert_equal(len(browser._states), 5) 192 | 193 | @mock_urls 194 | def test_state_deque_no_history(self): 195 | browser = RoboBrowser(history=False) 196 | for _ in range(5): 197 | browser.open('http://robobrowser.com/page1/') 198 | assert_equal(len(browser._states), 1) 199 | assert_equal(browser._cursor, 0) 200 | 201 | 202 | class TestHistory(unittest.TestCase): 203 | 204 | @mock_urls 205 | def setUp(self): 206 | self.browser = RoboBrowser(history=True) 207 | self.browser.open('http://robobrowser.com/page1/') 208 | self.browser.open('http://robobrowser.com/page2/') 209 | self.browser.open('http://robobrowser.com/page3/') 210 | 211 | def test_back(self): 212 | self.browser.back() 213 | assert_equal( 214 | self.browser.url, 215 | 'http://robobrowser.com/page2/' 216 | ) 217 | 218 | def test_back_n(self): 219 | self.browser.back(n=2) 220 | assert_equal( 221 | self.browser.url, 222 | 'http://robobrowser.com/page1/' 223 | ) 224 | 225 | def test_forward(self): 226 | self.browser.back() 227 | self.browser.forward() 228 | assert_equal( 229 | self.browser.url, 230 | 'http://robobrowser.com/page3/' 231 | ) 232 | 233 | def test_forward_n(self): 234 | self.browser.back(n=2) 235 | self.browser.forward(n=2) 236 | assert_equal( 237 | self.browser.url, 238 | 'http://robobrowser.com/page3/' 239 | ) 240 | 241 | @mock_urls 242 | def test_open_clears_forward(self): 243 | self.browser.back(n=2) 244 | self.browser.open('http://robobrowser.com/page4/') 245 | assert_equal( 246 | self.browser._cursor, 247 | len(self.browser._states) - 1 248 | ) 249 | assert_raises( 250 | exceptions.RoboError, 251 | self.browser.forward 252 | ) 253 | 254 | def test_back_error(self): 255 | assert_raises( 256 | exceptions.RoboError, 257 | self.browser.back, 258 | 5 259 | ) 260 | 261 | 262 | class TestCustomSession(unittest.TestCase): 263 | 264 | @mock_links 265 | def test_custom_headers(self): 266 | session = requests.Session() 267 | session.headers.update({ 268 | 'Content-Encoding': 'gzip', 269 | }) 270 | browser = RoboBrowser(session=session) 271 | browser.open('http://robobrowser.com/links/') 272 | assert_equal( 273 | browser.response.request.headers.get('Content-Encoding'), 274 | 'gzip' 275 | ) 276 | 277 | @mock_links 278 | def test_custom_headers_override(self): 279 | session = requests.Session() 280 | session.headers.update({ 281 | 'Content-Encoding': 'gzip', 282 | }) 283 | browser = RoboBrowser(session=session) 284 | browser.open( 285 | 'http://robobrowser.com/links/', 286 | headers={'Content-Encoding': 'identity'} 287 | ) 288 | assert_equal( 289 | browser.response.request.headers.get('Content-Encoding'), 290 | 'identity' 291 | ) 292 | 293 | 294 | class TestTimeout(unittest.TestCase): 295 | 296 | @mock.patch('requests.Session.request') 297 | def test_no_timeout(self, mock_request): 298 | browser = RoboBrowser() 299 | browser.open('http://robobrowser.com/') 300 | assert_true(mock_request.called) 301 | kwargs = mock_request.mock_calls[0][2] 302 | assert_true(kwargs.get('timeout') is None) 303 | 304 | @mock.patch('requests.Session.request') 305 | def test_instance_timeout(self, mock_request): 306 | browser = RoboBrowser(timeout=5) 307 | browser.open('http://robobrowser.com/') 308 | assert_true(mock_request.called) 309 | kwargs = mock_request.mock_calls[0][2] 310 | assert_equal(kwargs.get('timeout'), 5) 311 | 312 | @mock.patch('requests.Session.request') 313 | def test_call_timeout(self, mock_request): 314 | browser = RoboBrowser(timeout=5) 315 | browser.open('http://robobrowser.com/', timeout=10) 316 | assert_true(mock_request.called) 317 | kwargs = mock_request.mock_calls[0][2] 318 | assert_equal(kwargs.get('timeout'), 10) 319 | 320 | 321 | class TestAllowRedirects(unittest.TestCase): 322 | 323 | @mock.patch('requests.Session.request') 324 | def test_no_allow_redirects(self, mock_request): 325 | browser = RoboBrowser() 326 | browser.open('http://robobrowser.com/') 327 | assert_true(mock_request.called) 328 | kwargs = mock_request.mock_calls[0][2] 329 | assert_true(kwargs.get('allow_redirects') is True) 330 | 331 | @mock.patch('requests.Session.request') 332 | def test_instance_allow_redirects(self, mock_request): 333 | browser = RoboBrowser(allow_redirects=False) 334 | browser.open('http://robobrowser.com/') 335 | assert_true(mock_request.called) 336 | kwargs = mock_request.mock_calls[0][2] 337 | assert_true(kwargs.get('allow_redirects') is False) 338 | 339 | @mock.patch('requests.Session.request') 340 | def test_call_allow_redirects(self, mock_request): 341 | browser = RoboBrowser(allow_redirects=True) 342 | browser.open('http://robobrowser.com/', allow_redirects=False) 343 | assert_true(mock_request.called) 344 | kwargs = mock_request.mock_calls[0][2] 345 | assert_true(kwargs.get('allow_redirects') is False) 346 | -------------------------------------------------------------------------------- /tests/test_cache.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from nose.tools import * 3 | 4 | import datetime 5 | 6 | from robobrowser.browser import RoboBrowser 7 | from robobrowser.cache import RoboCache 8 | from tests.utils import KwargSetter 9 | 10 | 11 | class TestAdapter(unittest.TestCase): 12 | 13 | def test_cache_on(self): 14 | self.browser = RoboBrowser(cache=True) 15 | self.browser.open('http://httpbin.org/') 16 | resp1 = self.browser.state.response 17 | self.browser.open('http://httpbin.org/') 18 | resp2 = self.browser.state.response 19 | assert_true(resp1 is resp2) 20 | 21 | def test_cache_off(self): 22 | self.browser = RoboBrowser(cache=False) 23 | self.browser.open('http://httpbin.org/') 24 | resp1 = self.browser.state.response 25 | self.browser.open('http://httpbin.org/') 26 | resp2 = self.browser.state.response 27 | assert_true(resp1 is not resp2) 28 | 29 | 30 | class TestCache(unittest.TestCase): 31 | 32 | def setUp(self): 33 | self.cache = RoboCache() 34 | 35 | def test_store(self): 36 | url = 'http://robobrowser.com/' 37 | response = KwargSetter(url=url, status_code=200) 38 | now = datetime.datetime.now() 39 | self.cache.store(response) 40 | assert_true(url in self.cache.data) 41 | assert_equal(self.cache.data[url]['response'], response) 42 | date_diff = self.cache.data[url]['date'] - now 43 | assert_true(date_diff < datetime.timedelta(seconds=0.1)) 44 | 45 | def test_store_invalid_verb(self): 46 | url = 'http://robobrowser.com/' 47 | response = KwargSetter(url=url, status_code=400) 48 | self.cache.store(response) 49 | assert_false(url in self.cache.data) 50 | 51 | def test_retrieve_not_stored(self): 52 | request = KwargSetter(url='http://robobrowser.com/', method='GET') 53 | retrieved = self.cache.retrieve(request) 54 | assert_equal(retrieved, None) 55 | 56 | def test_retrieve_stored(self): 57 | request = KwargSetter(url='http://robobrowser.com/', method='GET') 58 | response = KwargSetter(url='http://robobrowser.com/', status_code=200) 59 | self.cache.store(response) 60 | retrieved = self.cache.retrieve(request) 61 | assert_equal(retrieved, response) 62 | 63 | def test_retrieve_invalid_code(self): 64 | request = KwargSetter(url='http://robobrowser.com/', method='GET') 65 | response = KwargSetter(url='http://robobrowser.com/', status_code=400) 66 | self.cache.store(response) 67 | retrieved = self.cache.retrieve(request) 68 | assert_equal(retrieved, None) 69 | 70 | def test_reduce_age(self): 71 | for idx in range(5): 72 | response = KwargSetter(url=idx, status_code=200) 73 | self.cache.store(response) 74 | # time.sleep(0.1) 75 | assert_equal(len(self.cache.data), 5) 76 | now = datetime.datetime.now() 77 | self.cache.max_age = now - self.cache.data[2]['date'] 78 | self.cache._reduce_age(now) 79 | assert_equal(len(self.cache.data), 3) 80 | # Cast keys to list for 3.3 compatibility 81 | assert_equal(list(self.cache.data.keys()), [2, 3, 4]) 82 | 83 | def test_reduce_count(self): 84 | for idx in range(5): 85 | response = KwargSetter(url=idx, status_code=200) 86 | self.cache.store(response) 87 | assert_equal(len(self.cache.data), 5) 88 | self.cache.max_count = 3 89 | self.cache._reduce_count() 90 | assert_equal(len(self.cache.data), 3) 91 | # Cast keys to list for 3.3 compatibility 92 | assert_equal(list(self.cache.data.keys()), [2, 3, 4]) 93 | -------------------------------------------------------------------------------- /tests/test_forms.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import mock 4 | import unittest 5 | from nose.tools import * # noqa 6 | 7 | import tempfile 8 | from bs4 import BeautifulSoup 9 | 10 | from robobrowser.compat import builtin_name 11 | from robobrowser.forms.form import Form, Payload, fields, _parse_fields 12 | from robobrowser import exceptions 13 | 14 | 15 | class TestPayload(unittest.TestCase): 16 | 17 | def setUp(self): 18 | self.payload = Payload() 19 | self.payload.add({'red': 'special'}) 20 | 21 | def test_add_payload(self): 22 | self.payload.add({'lazing': 'sunday'}) 23 | assert_true('lazing' in self.payload.data) 24 | assert_equal(self.payload.data['lazing'], 'sunday') 25 | 26 | def test_add_by_key(self): 27 | self.payload.add({'lazing': 'sunday'}, 'afternoon') 28 | assert_false('lazing' in self.payload.data) 29 | assert_true('afternoon' in self.payload.options) 30 | assert_true('lazing' in self.payload.options['afternoon']) 31 | assert_equal( 32 | self.payload.options['afternoon']['lazing'], 33 | 'sunday' 34 | ) 35 | 36 | def test_requests_get(self): 37 | out = self.payload.to_requests('get') 38 | assert_true('params' in out) 39 | assert_equal(list(out['params']), [('red', 'special')]) 40 | 41 | def test_requests_post(self): 42 | out = self.payload.to_requests('post') 43 | assert_true('data' in out) 44 | assert_equal(list(out['data']), [('red', 'special')]) 45 | 46 | 47 | class TestForm(unittest.TestCase): 48 | 49 | def setUp(self): 50 | self.html = ''' 51 |
52 | 53 | 54 | 58 | Roger
59 | John
60 | 61 | 62 | 63 |
64 | ''' 65 | self.form = Form(self.html) 66 | 67 | def test_fields(self): 68 | keys = set(('vocals', 'guitar', 'drums', 'bass', 'multi', 'submit')) 69 | assert_equal(set(self.form.fields.keys()), keys) 70 | assert_equal(set(self.form.keys()), keys) 71 | 72 | def test_add_field(self): 73 | html = '' 74 | field = fields.Input(html) 75 | self.form.add_field(field) 76 | assert_true('instrument' in self.form.fields) 77 | 78 | def test_add_field_wrong_type(self): 79 | assert_raises(ValueError, lambda: self.form.add_field('freddie')) 80 | 81 | def test_repr(self): 82 | assert_equal( 83 | repr(self.form), 84 | '' 86 | ) 87 | 88 | def test_repr_empty(self): 89 | assert_equal( 90 | repr(Form('
')), 91 | '' 92 | ) 93 | 94 | def test_repr_unicode(self): 95 | form = Form(u'
') 96 | assert_equal( 97 | repr(form), 98 | '' 99 | ) 100 | 101 | def test_serialize(self): 102 | serialized = self.form.serialize() 103 | assert_equal(serialized.data.getlist('multi'), ['multi1', 'multi2']) 104 | assert_equal(serialized.data['submit'], 'submit') 105 | 106 | def test_serialize_skips_disabled(self): 107 | html = ''' 108 |
109 | 110 | 111 | 112 |
113 | ''' 114 | form = Form(html) 115 | serialized = form.serialize() 116 | assert_false('guitar' in serialized.data) 117 | 118 | 119 | class TestFormMultiSubmit(unittest.TestCase): 120 | 121 | def setUp(self): 122 | self.html = ''' 123 |
124 | 125 | 126 |
127 | ''' 128 | self.form = Form(self.html) 129 | 130 | def test_serialize_multi_no_submit_specified(self): 131 | assert_raises( 132 | exceptions.InvalidSubmitError, 133 | lambda: self.form.serialize() 134 | ) 135 | 136 | def test_serialize_multi_wrong_submit_specified(self): 137 | fake_submit = fields.Submit('') 138 | assert_raises( 139 | exceptions.InvalidSubmitError, 140 | lambda: self.form.serialize(submit=fake_submit) 141 | ) 142 | 143 | def test_serialize_multi(self): 144 | submit = self.form.submit_fields['submit1'] 145 | serialized = self.form.serialize(submit) 146 | assert_equal(serialized.data['submit1'], 'value1') 147 | assert_false('submit2' in serialized.data) 148 | 149 | 150 | class TestParser(unittest.TestCase): 151 | 152 | def setUp(self): 153 | self.form = Form('
') 154 | 155 | def test_method_default(self): 156 | assert_equal(self.form.method, 'get') 157 | 158 | def test_method(self): 159 | form = Form('
') 160 | assert_equal(form.method, 'put') 161 | 162 | def test_action(self): 163 | form = Form('
') 164 | assert_equal(form.action, '/') 165 | 166 | def test_parse_input(self): 167 | html = '' 168 | _fields = _parse_fields(BeautifulSoup(html)) 169 | assert_equal(len(_fields), 1) 170 | assert_true(isinstance(_fields[0], fields.Input)) 171 | 172 | def test_parse_file_input(self): 173 | html = '' 174 | _fields = _parse_fields(BeautifulSoup(html)) 175 | assert_equal(len(_fields), 1) 176 | assert_true(isinstance(_fields[0], fields.FileInput)) 177 | 178 | def test_parse_textarea(self): 179 | html = '' 180 | _fields = _parse_fields(BeautifulSoup(html)) 181 | assert_equal(len(_fields), 1) 182 | assert_true(isinstance(_fields[0], fields.Textarea)) 183 | 184 | def test_parse_radio(self): 185 | html = ''' 186 | freddie
187 | brian
188 | roger
189 | john
190 | rhapsody
191 | killer
192 | ''' 193 | _fields = _parse_fields(BeautifulSoup(html)) 194 | assert_equal(len(_fields), 2) 195 | assert_true(isinstance(_fields[0], fields.Radio)) 196 | assert_true(isinstance(_fields[0], fields.Radio)) 197 | assert_equal( 198 | len(_fields[0]._parsed), 4 199 | ) 200 | assert_equal( 201 | len(_fields[1]._parsed), 2 202 | ) 203 | 204 | def test_parse_checkbox(self): 205 | html = ''' 206 | freddie
207 | brian
208 | roger
209 | john
210 | rhapsody
211 | killer
212 | ''' 213 | _fields = _parse_fields(BeautifulSoup(html)) 214 | assert_equal(len(_fields), 2) 215 | assert_true(isinstance(_fields[0], fields.Checkbox)) 216 | assert_true(isinstance(_fields[1], fields.Checkbox)) 217 | assert_equal(len(_fields[0]._parsed), 4) 218 | assert_equal(len(_fields[1]._parsed), 2) 219 | 220 | def test_parse_select(self): 221 | html = ''' 222 | 228 | ''' 229 | _fields = _parse_fields(BeautifulSoup(html)) 230 | assert_equal(len(_fields), 1) 231 | assert_true(isinstance(_fields[0], fields.Select)) 232 | 233 | def test_parse_empty_select(self): 234 | html = ''' 235 | 236 | ''' 237 | _fields = _parse_fields(BeautifulSoup(html)) 238 | assert_equal(len(_fields), 1) 239 | assert_true(isinstance(_fields[0], fields.Select)) 240 | assert_equal(_fields[0].value, '') 241 | assert_equal(_fields[0].options, []) 242 | 243 | def test_parse_select_multi(self): 244 | html = ''' 245 | 251 | ''' 252 | _fields = _parse_fields(BeautifulSoup(html)) 253 | assert_equal(len(_fields), 1) 254 | assert_true(isinstance(_fields[0], fields.MultiSelect)) 255 | 256 | 257 | class TestInput(unittest.TestCase): 258 | 259 | def setUp(self): 260 | self.html = '' 261 | self.input = fields.Input(BeautifulSoup(self.html).find('input')) 262 | 263 | def test_name(self): 264 | assert_equal(self.input.name, 'brian') 265 | 266 | def test_initial(self): 267 | assert_equal(self.input._value, 'may') 268 | assert_equal(self.input.value, 'may') 269 | 270 | def test_value(self): 271 | self.input.value = 'red special' 272 | assert_equal(self.input._value, 'red special') 273 | assert_equal(self.input.value, 'red special') 274 | 275 | def test_serialize(self): 276 | assert_equal( 277 | self.input.serialize(), 278 | {'brian': 'may'} 279 | ) 280 | 281 | def test_invalid_name(self): 282 | html = '' 283 | assert_raises(exceptions.InvalidNameError, lambda: fields.Input(html)) 284 | 285 | 286 | class TestInputBlank(unittest.TestCase): 287 | 288 | def setUp(self): 289 | self.html = '' 290 | self.input = fields.Input(BeautifulSoup(self.html).find('input')) 291 | 292 | def test_initial(self): 293 | assert_equal(self.input._value, None) 294 | assert_equal(self.input.value, '') 295 | 296 | def test_serialize(self): 297 | assert_equal( 298 | self.input.serialize(), 299 | {'blank': ''} 300 | ) 301 | 302 | 303 | class TestTextarea(unittest.TestCase): 304 | 305 | def setUp(self): 306 | self.html = '' 307 | self.input = fields.Textarea(BeautifulSoup(self.html).find('textarea')) 308 | 309 | def test_name(self): 310 | assert_equal(self.input.name, 'roger') 311 | 312 | def test_initial(self): 313 | assert_equal(self.input._value, 'taylor') 314 | assert_equal(self.input.value, 'taylor') 315 | 316 | def test_value(self): 317 | self.input.value = 'the drums' 318 | assert_equal(self.input._value, 'the drums') 319 | assert_equal(self.input.value, 'the drums') 320 | 321 | def test_serialize(self): 322 | assert_equal( 323 | self.input.serialize(), 324 | {'roger': 'taylor'} 325 | ) 326 | 327 | 328 | class TestTextareaBlank(unittest.TestCase): 329 | 330 | def setUp(self): 331 | self.html = '' 332 | self.input = fields.Textarea(BeautifulSoup(self.html).find('textarea')) 333 | 334 | def test_initial(self): 335 | assert_equal(self.input._value, '') 336 | assert_equal(self.input.value, '') 337 | 338 | def test_serialize(self): 339 | assert_equal( 340 | self.input.serialize(), 341 | {'blank': ''} 342 | ) 343 | 344 | 345 | class TestSelect(unittest.TestCase): 346 | 347 | def setUp(self): 348 | self.html = ''' 349 | 354 | ''' 355 | self.input = fields.Select(BeautifulSoup(self.html).find('select')) 356 | 357 | def test_name(self): 358 | assert_equal(self.input.name, 'john') 359 | 360 | def test_options(self): 361 | assert_equal( 362 | self.input.options, 363 | ['tie', "you're", 'the'] 364 | ) 365 | 366 | def test_initial(self): 367 | assert_equal(self.input._value, 1) 368 | assert_equal(self.input.value, "you're") 369 | 370 | def test_value(self): 371 | self.input.value = 'the' 372 | assert_equal(self.input._value, 2) 373 | assert_equal(self.input.value, 'the') 374 | 375 | def test_value_label(self): 376 | self.input.value = 'millionaire waltz' 377 | assert_equal(self.input._value, 2) 378 | assert_equal(self.input.value, 'the') 379 | 380 | def test_serialize(self): 381 | assert_equal( 382 | self.input.serialize(), 383 | {'john': "you're"} 384 | ) 385 | 386 | 387 | class TestSelectBlank(unittest.TestCase): 388 | 389 | def setUp(self): 390 | self.html = ''' 391 | 396 | ''' 397 | self.input = fields.Select(BeautifulSoup(self.html).find('select')) 398 | 399 | def test_name(self): 400 | assert_equal(self.input.name, 'john') 401 | 402 | def test_initial(self): 403 | assert_equal(self.input._value, 0) 404 | assert_equal(self.input.value, 'tie') 405 | 406 | def test_serialize(self): 407 | assert_equal( 408 | self.input.serialize(), 409 | {'john': 'tie'} 410 | ) 411 | 412 | 413 | class TestMultiSelect(unittest.TestCase): 414 | 415 | def setUp(self): 416 | self.html = ''' 417 | 422 | ''' 423 | self.input = fields.MultiSelect(BeautifulSoup(self.html).find('select')) 424 | 425 | 426 | class TestMixedCase(unittest.TestCase): 427 | 428 | def test_upper_type(self): 429 | html = ''' 430 | vocals
431 | ''' 432 | input = fields.Radio(BeautifulSoup(html).find_all('input')) 433 | assert_equal(input.name, 'members') 434 | 435 | def test_upper_name(self): 436 | html = ''' 437 | vocals
438 | ''' 439 | input = fields.Radio(BeautifulSoup(html).find_all('input')) 440 | assert_equal(input.name, 'members') 441 | 442 | def test_mixed_radio_names(self): 443 | html = ''' 444 | vocals
445 | guitar
446 | ''' 447 | input = fields.Radio(BeautifulSoup(html).find_all('input')) 448 | assert_equal(input.name, 'members') 449 | assert_equal( 450 | input.options, 451 | ['mercury', 'may'] 452 | ) 453 | 454 | 455 | class TestRadio(unittest.TestCase): 456 | 457 | def setUp(self): 458 | self.html = ''' 459 | vocals
460 | guitar
461 | drums
462 | bass
463 | ''' 464 | self.input = fields.Radio(BeautifulSoup(self.html).find_all('input')) 465 | 466 | def test_name(self): 467 | assert_equal(self.input.name, 'members') 468 | 469 | def test_options(self): 470 | assert_equal( 471 | self.input.options, 472 | ['mercury', 'may', 'taylor', 'deacon'] 473 | ) 474 | 475 | def test_initial(self): 476 | assert_equal(self.input.value, 'mercury') 477 | 478 | def test_value(self): 479 | self.input.value = 'taylor' 480 | assert_equal(self.input._value, 2) 481 | assert_equal(self.input.value, 'taylor') 482 | 483 | def test_value_label(self): 484 | self.input.value = 'drums' 485 | assert_equal(self.input._value, 2) 486 | assert_equal(self.input.value, 'taylor') 487 | 488 | def test_serialize(self): 489 | assert_equal( 490 | self.input.serialize(), 491 | {'members': 'mercury'} 492 | ) 493 | 494 | 495 | class TestRadioBlank(unittest.TestCase): 496 | 497 | def setUp(self): 498 | self.html = ''' 499 | vocals
500 | guitar
501 | drums
502 | bass
503 | ''' 504 | self.input = fields.Radio(BeautifulSoup(self.html).find_all('input')) 505 | 506 | def test_initial(self): 507 | assert_equal(self.input.value, '') 508 | 509 | def test_serialize(self): 510 | assert_equal( 511 | self.input.serialize(), 512 | {'member': ''} 513 | ) 514 | 515 | 516 | class TestCheckbox(unittest.TestCase): 517 | 518 | def setUp(self): 519 | self.html = ''' 520 | vocals
521 | guitar
522 | drums
523 | bass
524 | ''' 525 | self.input = fields.Checkbox(BeautifulSoup(self.html).find_all('input')) 526 | 527 | def test_name(self): 528 | assert_equal(self.input.name, 'member') 529 | 530 | def test_options(self): 531 | assert_equal( 532 | self.input.options, 533 | ['mercury', 'may', 'taylor', 'deacon'] 534 | ) 535 | 536 | def test_initial(self): 537 | assert_equal( 538 | self.input.value, 539 | ['mercury', 'deacon'] 540 | ) 541 | 542 | def test_value(self): 543 | self.input.value = 'taylor' 544 | assert_equal(self.input._value, [2]) 545 | assert_equal(self.input.value, ['taylor']) 546 | 547 | def test_values(self): 548 | self.input.value = ['taylor', 'deacon'] 549 | assert_equal(self.input._value, [2, 3]) 550 | assert_equal(self.input.value, ['taylor', 'deacon']) 551 | 552 | def test_value_label(self): 553 | self.input.value = 'drums' 554 | assert_equal(self.input._value, [2]) 555 | assert_equal(self.input.value, ['taylor']) 556 | 557 | def test_serialize(self): 558 | assert_equal( 559 | self.input.serialize(), 560 | {'member': ['mercury', 'deacon']} 561 | ) 562 | 563 | 564 | class TestCheckboxBlank(unittest.TestCase): 565 | 566 | def setUp(self): 567 | self.html = ''' 568 | vocals
569 | guitar
570 | drums
571 | bass
572 | ''' 573 | self.input = fields.Checkbox(BeautifulSoup(self.html).find_all('input')) 574 | 575 | def test_initial(self): 576 | assert_equal( 577 | self.input.value, [] 578 | ) 579 | 580 | def test_serialize(self): 581 | assert_equal( 582 | self.input.serialize(), 583 | {'member': []} 584 | ) 585 | 586 | 587 | class TestFileInput(unittest.TestCase): 588 | 589 | def setUp(self): 590 | self.html = '' 591 | self.input = fields.FileInput(BeautifulSoup(self.html).find('input')) 592 | 593 | def test_name(self): 594 | assert_equal(self.input.name, 'song') 595 | 596 | def test_value_file(self): 597 | file = tempfile.TemporaryFile('r') 598 | self.input.value = file 599 | assert_equal(self.input._value, file) 600 | assert_equal(self.input.value, file) 601 | 602 | @mock.patch('{0}.open'.format(builtin_name)) 603 | def test_value_name(self, mock_open): 604 | file = tempfile.TemporaryFile('r') 605 | mock_open.return_value = file 606 | self.input.value = 'temp' 607 | assert_equal(self.input._value, file) 608 | assert_equal(self.input.value, file) 609 | 610 | def test_serialize(self): 611 | file = tempfile.TemporaryFile('r') 612 | self.input.value = file 613 | assert_equal( 614 | self.input.serialize(), 615 | {'song': file} 616 | ) 617 | 618 | 619 | class TestDisabledValues(unittest.TestCase): 620 | 621 | def test_input_enabled(self): 622 | html = '' 623 | input = fields.Input(BeautifulSoup(html).find('input')) 624 | assert_false(input.disabled) 625 | 626 | def test_input_disabled(self): 627 | html = '' 628 | input = fields.Input(BeautifulSoup(html).find('input')) 629 | assert_true(input.disabled) 630 | 631 | def test_checkbox_enabled(self): 632 | html = ''' 633 | vocals
634 | guitar
635 | drums
636 | bass
637 | ''' 638 | input = fields.Checkbox(BeautifulSoup(html).find_all('input')) 639 | assert_false(input.disabled) 640 | 641 | def test_checkbox_disabled(self): 642 | html = ''' 643 | vocals
644 | guitar
645 | drums
646 | bass
647 | ''' 648 | input = fields.Checkbox(BeautifulSoup(html).find_all('input')) 649 | assert_true(input.disabled) 650 | 651 | def test_select_enabled(self): 652 | html = ''' 653 | 658 | ''' 659 | input = fields.Select(BeautifulSoup(html).find('select')) 660 | assert_false(input.disabled) 661 | 662 | def test_select_disabled_root(self): 663 | html = ''' 664 | 669 | ''' 670 | input = fields.Select(BeautifulSoup(html).find('select')) 671 | assert_true(input.disabled) 672 | 673 | def test_select_disabled_options(self): 674 | html = ''' 675 | 680 | ''' 681 | input = fields.Select(BeautifulSoup(html).find('select')) 682 | assert_true(input.disabled) 683 | 684 | 685 | class TestDefaultValues(unittest.TestCase): 686 | 687 | def test_checkbox_default(self): 688 | inputs = BeautifulSoup(''' 689 | 690 | ''').find_all('input') 691 | checkbox = fields.Checkbox(inputs) 692 | assert_equal(checkbox.options, ['on']) 693 | 694 | def test_radio_default(self): 695 | inputs = BeautifulSoup(''' 696 | 697 | ''').find_all('input') 698 | radio = fields.Radio(inputs) 699 | assert_equal(radio.options, ['on']) 700 | 701 | def test_select_default(self): 702 | parsed = BeautifulSoup(''' 703 | 706 | ''', 'html.parser') 707 | select = fields.Select(parsed) 708 | assert_equal(select.options, ['opt']) 709 | 710 | def test_multi_select_default(self): 711 | parsed = BeautifulSoup(''' 712 | 715 | ''', 'html.parser') 716 | select = fields.Select(parsed) 717 | assert_equal(select.options, ['opt']) 718 | -------------------------------------------------------------------------------- /tests/test_helpers.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from nose.tools import * 3 | 4 | from bs4 import BeautifulSoup 5 | 6 | from robobrowser import helpers 7 | 8 | 9 | class TestEnsureSoup(unittest.TestCase): 10 | 11 | def setUp(self): 12 | self.html = '
' 13 | self.tag = BeautifulSoup(self.html).find() 14 | self.htmls = [ 15 | '
', 16 | '', 17 | ] 18 | self.tags = [ 19 | BeautifulSoup(html).find() 20 | for html in self.htmls 21 | ] 22 | 23 | def test_handle_string(self): 24 | ensured = helpers.ensure_soup(self.html) 25 | assert_equal(ensured, self.tag) 26 | 27 | def test_handle_soup(self): 28 | ensured = helpers.ensure_soup(BeautifulSoup(self.html)) 29 | assert_equal(ensured, self.tag) 30 | 31 | def test_handle_tag(self): 32 | ensured = helpers.ensure_soup(BeautifulSoup(self.html).find()) 33 | assert_equal(ensured, self.tag) 34 | 35 | def test_handle_string_list(self): 36 | ensured = helpers.ensure_soup(self.htmls) 37 | assert_equal(ensured, self.tags) 38 | 39 | def test_handle_soup_list(self): 40 | ensured = helpers.ensure_soup([ 41 | BeautifulSoup(html) 42 | for html in self.htmls 43 | ]) 44 | assert_equal(ensured, self.tags) 45 | 46 | def test_handle_tag_list(self): 47 | ensured = helpers.ensure_soup([ 48 | BeautifulSoup(html).find() 49 | for html in self.htmls 50 | ]) 51 | assert_equal(ensured, self.tags) 52 | 53 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | from robobrowser import responses 4 | from robobrowser.compat import iteritems 5 | 6 | 7 | class ArgCatcher(object): 8 | """Simple class for memorizing positional and keyword arguments. Used to 9 | capture responses for mock_responses. 10 | 11 | """ 12 | def __init__(self, *args, **kwargs): 13 | self.args = args 14 | self.kwargs = kwargs 15 | 16 | 17 | class KwargSetter(object): 18 | """Simple class for memorizing keyword arguments as instance attributes. 19 | Used to mock requests and responses for testing. 20 | 21 | """ 22 | def __init__(self, **kwargs): 23 | for key, value in iteritems(kwargs): 24 | setattr(self, key, value) 25 | 26 | 27 | def mock_responses(resps): 28 | """Decorator factory to make tests more DRY. Bundles responses.activate 29 | with a collection of response rules. 30 | 31 | :param list resps: List of response-formatted ArgCatcher arguments. 32 | 33 | """ 34 | def wrapper(func): 35 | @responses.activate 36 | @functools.wraps(func) 37 | def wrapped(*args, **kwargs): 38 | for resp in resps: 39 | responses.add(*resp.args, **resp.kwargs) 40 | return func(*args, **kwargs) 41 | return wrapped 42 | return wrapper 43 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py26,py27,py33,py34,pypy 3 | 4 | [testenv] 5 | deps = 6 | -rdev-requirements.txt 7 | commands = 8 | {envpython} setup.py test 9 | 10 | --------------------------------------------------------------------------------