1
+ from contextlib import contextmanager
1
2
from time import gmtime
2
3
from time import strftime
3
4
import json
5
+ import os
4
6
import re
7
+ import tempfile
5
8
6
- from selenium import webdriver
7
9
from selenium .common .exceptions import NoSuchElementException
8
10
from selenium .common .exceptions import StaleElementReferenceException
9
11
from selenium .common .exceptions import WebDriverException
38
40
}
39
41
"""
40
42
43
+ _TEST_INPUT_ID = "_cb_test_input"
44
+
45
+ # Creates an input into which we can upload files using Selenium.
46
+ _CREATE_INPUT_SCRIPT = """
47
+ var input = window.$('<input id="{input_id}" type="file" style="position: fixed">');
48
+ window.$('body').append(input);
49
+ """ .format (input_id = _TEST_INPUT_ID )
50
+
51
+ # After the file is chosen via Selenium, this script moves the file object
52
+ # (in the DOM) to the Dropzone.
53
+ def _move_file_to_dropzone_script (dropzone_selector ):
54
+ return """
55
+ var fileInput = document.getElementById('{input_id}');
56
+ var file = fileInput.files[0];
57
+ var dropzone = Dropzone.forElement('{selector}');
58
+ dropzone.drop({{ dataTransfer: {{ files: [file] }} }});
59
+ """ .format (input_id = _TEST_INPUT_ID , selector = dropzone_selector )
60
+
41
61
# How long (in seconds) to wait before assuming that an example
42
62
# has failed to compile
43
63
VERIFY_TIMEOUT = 15
47
67
VERIFICATION_FAILED_MESSAGE = "Verification failed."
48
68
49
69
50
- class SeleniumTestCase (object ):
51
- """Base class for all Selenium tests."""
70
+ @contextmanager
71
+ def temp_copy (fname ):
72
+ """Creates a temporary copy of the file `fname`.
73
+ This is useful for testing features that derive certain properties
74
+ from the filename, and we want a unique filename each time we run the
75
+ test (in case, for example, there is leftover garbage from previous
76
+ tests with the same name).
77
+ """
78
+ extension = fname .split ('.' )[- 1 ]
79
+ with tempfile .NamedTemporaryFile (mode = 'w+b' , suffix = '.%s' % extension ) as copy :
80
+ with open (fname , 'r' ) as original :
81
+ for line in original :
82
+ copy .write (line )
83
+ copy .flush ()
84
+ yield copy
85
+
86
+
87
+ class CodebenderSeleniumBot (object ):
88
+ """Contains various utilities for navigating the Codebender website."""
52
89
53
90
# This can be configured on a per-test case basis to use a different
54
91
# URL for testing; e.g., http://localhost, or http://codebender.cc.
55
92
# It is set via command line option in _testcase_attrs (below)
56
93
site_url = None
57
94
58
- @classmethod
59
- @pytest .fixture (scope = "class" , autouse = True )
60
- def _testcase_attrs (cls , webdriver , testing_url ):
61
- """Sets up any class attributes to be used by any SeleniumTestCase.
62
- Here, we just store fixtures as class attributes. This allows us to avoid
63
- the pytest boilerplate of getting a fixture value, and instead just
64
- refer to the fixture as `self.<fixture>`.
95
+ def start (url = None , webdriver = None ):
96
+ """Create the selenium webdriver, operating on `url`. We can't do this
97
+ in an __init__ method, otherwise py.test complains about
98
+ SeleniumTestCase having an init method.
99
+ The webdriver that is created is specified as a key into the WEBDRIVERS
100
+ dict (in codebender_testing.config)
65
101
"""
66
- cls .driver = webdriver
67
- cls .site_url = testing_url
102
+ if webdriver is None :
103
+ webdriver = WEBDRIVERS .keys ()[0 ]
104
+ self .driver = WEBDRIVERS [webdriver ]
68
105
69
- @pytest .fixture (scope = "class" )
70
- def tester_login (self ):
71
- self .login ()
106
+ if url is None :
107
+ url = BASE_URL
108
+ self .site_url = url
109
+
110
+ @classmethod
111
+ @contextmanager
112
+ def session (cls , ** kwargs ):
113
+ """Start a new session with a new webdriver. Regardless of whether an
114
+ exception is raised, the webdriver is guaranteed to quit.
115
+ The keyword arguments should be interpreted as in `start`.
116
+
117
+ Sample usage:
118
+
119
+ ```
120
+ with CodebenderSeleniumBot.session(url="localhost",
121
+ webdriver="firefox") as bot:
122
+ # The browser is now open
123
+ bot.open("/")
124
+ assert "Codebender" in bot.driver.title
125
+ # The browser is now closed
126
+ ```
127
+
128
+ Test cases shouldn't need to use this method; it's mostly useful for
129
+ scripts, automation, etc.
130
+ """
131
+ try :
132
+ bot = cls ()
133
+ bot .start (** kwargs )
134
+ yield bot
135
+ bot .driver .quit ()
136
+ except :
137
+ bot .driver .quit ()
138
+ raise
72
139
73
140
def open (self , url = None ):
74
141
"""Open the resource specified by `url`.
@@ -95,6 +162,30 @@ def open_project(self, project_name=None):
95
162
project_link = self .driver .find_element_by_link_text (project_name )
96
163
project_link .send_keys (Keys .ENTER )
97
164
165
+ def upload_project (self , test_fname , project_name = None ):
166
+ """Tests that we can successfully upload `test_fname`.
167
+ `project_name` is the expected name of the project; by
168
+ default it is inferred from the file name.
169
+ Returns a pair of (the name of the project, the url of the project sketch)
170
+ """
171
+ # A tempfile is used here since we want the name to be
172
+ # unique; if the file has already been successfully uploaded
173
+ # then the test might give a false-positive.
174
+ with temp_copy (test_fname ) as test_file :
175
+ self .dropzone_upload ("#dropzoneForm" , test_file .name )
176
+ if project_name is None :
177
+ project_name = os .path .split (test_file .name )[- 1 ].split ('.' )[0 ]
178
+
179
+ # The upload was successful <==> we get a green "check" on its
180
+ # Dropzone upload indicator
181
+ self .get_element (By .CSS_SELECTOR , '#dropzoneForm .dz-success' )
182
+
183
+ # Make sure the project shows up in the Projects list
184
+ last_project = self .get_element (By .CSS_SELECTOR ,
185
+ '#sidebar-list-main li:last-child .project_link' )
186
+
187
+ return last_project .text , last_project .get_attribute ('href' )
188
+
98
189
def login (self ):
99
190
"""Performs a login."""
100
191
try :
@@ -119,6 +210,21 @@ def get_element(self, *locator):
119
210
expected_conditions .visibility_of_element_located (locator ))
120
211
return self .driver .find_element (* locator )
121
212
213
+ def get_elements (self , * locator ):
214
+ """Like `get_element`, but returns a list of all elements matching
215
+ the selector."""
216
+ WebDriverWait (self .driver , ELEMENT_FIND_TIMEOUT ).until (
217
+ expected_conditions .visibility_of_all_elements_located_by (locator ))
218
+ return self .driver .find_elements (* locator )
219
+
220
+ def get (self , selector ):
221
+ """Alias for `self.get_element(By.CSS_SELECTOR, selector)`."""
222
+ return self .get_element (By .CSS_SELECTOR , selector )
223
+
224
+ def get_all (self , selector ):
225
+ """Alias for `self.get_elements(By.CSS_SELECTOR, selector)`."""
226
+ return self .get_elements (By .CSS_SELECTOR , selector )
227
+
122
228
def delete_project (self , project_name ):
123
229
"""Deletes the project specified by `project_name`. Note that this will
124
230
navigate to the user's homepage."""
@@ -151,20 +257,35 @@ def compile_sketch(self, url, iframe=False):
151
257
raise VerificationError (compile_result )
152
258
153
259
154
- def compile_all_sketches (self , url , selector , iframe = False , logfile = None ):
155
- """Compiles all projects on the page at `url`. `selector` is a CSS selector
260
+ def dropzone_upload (self , selector , fname ):
261
+ """Uploads a file specified by `fname` via the Dropzone within the
262
+ element specified by `selector`. (Dropzone refers to Dropzone.js)
263
+ """
264
+ # Create an artificial file input.
265
+ self .execute_script (_CREATE_INPUT_SCRIPT )
266
+ test_input = self .get_element (By .ID , _TEST_INPUT_ID )
267
+ test_input .send_keys (fname )
268
+ self .execute_script (_move_file_to_dropzone_script (selector ))
269
+
270
+ def compile_all_sketches (self , url , selector , ** kwargs ):
271
+ """Compiles all sketches on the page at `url`. `selector` is a CSS selector
156
272
that should select all relevant <a> tags containing links to sketches.
273
+ See `compile_sketches` for the possible keyword arguments that can be specified.
274
+ """
275
+ self .open (url )
276
+ sketches = self .execute_script (_GET_SKETCHES_SCRIPT .format (selector = selector ))
277
+ assert len (sketches ) > 0
278
+ self .compile_sketches (sketches , ** kwargs )
279
+
280
+ def compile_sketches (self , sketches , iframe = False , logfile = None ):
281
+ """Compiles the sketches with URLs given by the `sketches` list.
157
282
`logfile` specifies a path to a file to which test results will be
158
283
logged. If it is not `None`, compile errors will not cause the test
159
284
to halt, but rather be logged to the given file. `logfile` may be a time
160
285
format string, which will be formatted appropriately.
161
286
`iframe` specifies whether the urls pointed to by `selector` are contained
162
287
within an iframe.
163
288
"""
164
- self .open (url )
165
- sketches = self .execute_script (_GET_SKETCHES_SCRIPT .format (selector = selector ))
166
- assert len (sketches ) > 0
167
-
168
289
if logfile is None :
169
290
for sketch in sketches :
170
291
self .compile_sketch (sketch , iframe = iframe )
@@ -185,17 +306,34 @@ def compile_all_sketches(self, url, selector, iframe=False, logfile=None):
185
306
json .dump (log_entry , f )
186
307
f .close ()
187
308
188
-
189
309
def execute_script (self , script , * deps ):
190
310
"""Waits for all JavaScript variables in `deps` to be defined, then
191
- executes the given script. Especially useful for waiting for things like
192
- jQuery to become available for use."""
311
+ executes the given script."""
193
312
if len (deps ) > 0 :
194
313
WebDriverWait (self .driver , DOM_PROPERTY_DEFINED_TIMEOUT ).until (
195
314
dom_properties_defined (* deps ))
196
315
return self .driver .execute_script (script )
197
316
198
317
318
+ class SeleniumTestCase (CodebenderSeleniumBot ):
319
+ """Base class for all Selenium tests."""
320
+
321
+ @classmethod
322
+ @pytest .fixture (scope = "class" , autouse = True )
323
+ def _testcase_attrs (cls , webdriver , testing_url ):
324
+ """Sets up any class attributes to be used by any SeleniumTestCase.
325
+ Here, we just store fixtures as class attributes. This allows us to avoid
326
+ the pytest boilerplate of getting a fixture value, and instead just
327
+ refer to the fixture as `self.<fixture>`.
328
+ """
329
+ cls .driver = webdriver
330
+ cls .site_url = testing_url
331
+
332
+ @pytest .fixture (scope = "class" )
333
+ def tester_login (self ):
334
+ self .login ()
335
+
336
+
199
337
class VerificationError (Exception ):
200
338
"""An exception representing a failed verification of a sketch."""
201
339
pass
0 commit comments