1
+ from time import gmtime
2
+ from time import strftime
3
+ import json
1
4
import re
2
5
3
6
from selenium import webdriver
4
7
from selenium .common .exceptions import NoSuchElementException
8
+ from selenium .common .exceptions import StaleElementReferenceException
9
+ from selenium .common .exceptions import WebDriverException
5
10
from selenium .webdriver .common .by import By
6
11
from selenium .webdriver .common .keys import Keys
7
12
from selenium .webdriver .support import expected_conditions
15
20
from codebender_testing .config import WEBDRIVERS
16
21
17
22
23
+ # Time to wait until we give up on a DOM property becoming available.
24
+ DOM_PROPERTY_DEFINED_TIMEOUT = 10
25
+
26
+ # JavaScript snippet to extract all the links to sketches on the current page.
27
+ # `selector` is a CSS selector selecting these links.
28
+ _GET_SKETCHES_SCRIPT = \
29
+ "return $('{selector}').map(function() {{ return this.href; }}).toArray();"
30
+
31
+ # JavaScript snippet to verify the code on the current page.
32
+ _VERIFY_SCRIPT = """
33
+ if (window.compilerflasher !== undefined) {
34
+ compilerflasher.verify();
35
+ } else {
36
+ // BACHELOR
37
+ verify();
38
+ }
39
+ """
40
+
41
+ # How long (in seconds) to wait before assuming that an example
42
+ # has failed to compile
43
+ VERIFY_TIMEOUT = 15
44
+
45
+ # Messages displayed to the user after verifying a sketch.
46
+ VERIFICATION_SUCCESSFUL_MESSAGE = "Verification Successful"
47
+ VERIFICATION_FAILED_MESSAGE = "Verification failed."
48
+
49
+
18
50
class SeleniumTestCase (object ):
19
51
"""Base class for all Selenium tests."""
20
52
53
+ # This can be configured on a per-test case basis to use a different
54
+ # URL for testing; e.g., http://localhost, or http://codebender.cc.
55
+ # It is set via command line option in _testcase_attrs (below)
56
+ site_url = None
57
+
21
58
@classmethod
22
59
@pytest .fixture (scope = "class" , autouse = True )
23
- def _testcase_attrs (cls , webdriver ):
60
+ def _testcase_attrs (cls , webdriver , testing_url ):
24
61
"""Sets up any class attributes to be used by any SeleniumTestCase.
25
62
Here, we just store fixtures as class attributes. This allows us to avoid
26
63
the pytest boilerplate of getting a fixture value, and instead just
27
64
refer to the fixture as `self.<fixture>`.
28
65
"""
29
66
cls .driver = webdriver
67
+ cls .site_url = testing_url
30
68
31
69
@pytest .fixture (scope = "class" )
32
70
def tester_login (self ):
@@ -40,12 +78,12 @@ def open(self, url=None):
40
78
"""
41
79
if url is None :
42
80
url = ''
43
- if re .match (".+?://^ " , url ):
81
+ if re .match (".+?://" , url ):
44
82
# url specifies an absolute path.
45
83
return self .driver .get (url )
46
84
else :
47
85
url = url .lstrip ('/' )
48
- return self .driver .get ("%s/%s" % (BASE_URL , url ))
86
+ return self .driver .get ("%s/%s" % (self . site_url , url ))
49
87
50
88
def open_project (self , project_name = None ):
51
89
"""Opens the project specified by `name`, bringing the driver to the
@@ -91,3 +129,107 @@ def delete_project(self, project_name):
91
129
delete_button .click ()
92
130
popup_delete_button = self .get_element (By .ID , 'deleteProjectButton' )
93
131
popup_delete_button .click ()
132
+
133
+ def compile_sketch (self , url , iframe = False ):
134
+ """Compiles the sketch located at `url`, or an iframe within the page
135
+ referred to by `url`. Raises an exception if it does not compile.
136
+ """
137
+ self .open (url )
138
+ if iframe :
139
+ # Note: here, we simply get the first iframe on the page.
140
+ # There's a slightly less awkward way to do this that involves
141
+ # giving this iframe a meaningful name in the HTML (TODO?)
142
+ self .driver .switch_to_frame (0 )
143
+ self .execute_script (_VERIFY_SCRIPT )
144
+ # In the BACHELOR site the id is 'operation_output', but in the live
145
+ # site the id is 'cb_cf_operation_output'. The [id$=operation_output]
146
+ # here selects an id that _ends_ with 'operation_output'.
147
+ compile_result = WebDriverWait (self .driver , VERIFY_TIMEOUT ).until (
148
+ any_text_to_be_present_in_element ((By .CSS_SELECTOR , "[id$=operation_output]" ),
149
+ VERIFICATION_SUCCESSFUL_MESSAGE , VERIFICATION_FAILED_MESSAGE ))
150
+ if compile_result != VERIFICATION_SUCCESSFUL_MESSAGE :
151
+ raise VerificationError (compile_result )
152
+
153
+
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
156
+ that should select all relevant <a> tags containing links to sketches.
157
+ `logfile` specifies a path to a file to which test results will be
158
+ logged. If it is not `None`, compile errors will not cause the test
159
+ to halt, but rather be logged to the given file. `logfile` may be a time
160
+ format string, which will be formatted appropriately.
161
+ `iframe` specifies whether the urls pointed to by `selector` are contained
162
+ within an iframe.
163
+ """
164
+ self .open (url )
165
+ sketches = self .execute_script (_GET_SKETCHES_SCRIPT .format (selector = selector ))
166
+ assert len (sketches ) > 0
167
+
168
+ if logfile is None :
169
+ for sketch in sketches :
170
+ self .compile_sketch (sketch , iframe = iframe )
171
+ else :
172
+ log_entry = {'url' : self .site_url , 'succeeded' : [], 'failed' : []}
173
+ for sketch in sketches :
174
+ try :
175
+ self .compile_sketch (sketch , iframe = iframe )
176
+ log_entry ['succeeded' ].append (sketch )
177
+ except (VerificationError , WebDriverException ) as e :
178
+ log_entry ['failed' ].append ({
179
+ 'sketch' : sketch ,
180
+ 'exception' : "%s; %s" % (type (e ).__name__ , str (e ))
181
+ # TODO?: is it possible to get the actual compiler error?
182
+ })
183
+ # Dump the test results to `logfile`.
184
+ f = open (strftime (logfile , gmtime ()), 'w' )
185
+ json .dump (log_entry , f )
186
+ f .close ()
187
+
188
+
189
+ def execute_script (self , script , * deps ):
190
+ """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."""
193
+ if len (deps ) > 0 :
194
+ WebDriverWait (self .driver , DOM_PROPERTY_DEFINED_TIMEOUT ).until (
195
+ dom_properties_defined (* deps ))
196
+ return self .driver .execute_script (script )
197
+
198
+
199
+ class VerificationError (Exception ):
200
+ """An exception representing a failed verification of a sketch."""
201
+ pass
202
+
203
+
204
+ class dom_properties_defined (object ):
205
+ """An expectation for the given DOM properties to be defined.
206
+ See selenium.webdriver.support.expected_conditions for more on how this
207
+ type of class works.
208
+ """
209
+
210
+ def __init__ (self , * properties ):
211
+ self ._properties = properties
212
+
213
+ def __call__ (self , driver ):
214
+ return all (
215
+ driver .execute_script ("return window.%s !== undefined" % prop )
216
+ for prop in self ._properties )
217
+
218
+
219
+ class any_text_to_be_present_in_element (object ):
220
+ """An expectation for checking if any of the given strings are present in
221
+ the specified element. Returns the string that was present.
222
+ """
223
+ def __init__ (self , locator , * texts ):
224
+ self .locator = locator
225
+ self .texts = texts
226
+
227
+ def __call__ (self , driver ):
228
+ try :
229
+ element_text = expected_conditions ._find_element (driver , self .locator ).text
230
+ for text in self .texts :
231
+ if text in element_text :
232
+ return text
233
+ return False
234
+ except StaleElementReferenceException :
235
+ return False
0 commit comments