-
Notifications
You must be signed in to change notification settings - Fork 0
/
findtext.py
294 lines (240 loc) · 8.94 KB
/
findtext.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
"""Utilities functions for API tests."""
import time
from selenium.common.exceptions import (
InvalidElementStateException,
NoSuchElementException,
StaleElementReferenceException,
WebDriverException,
)
class wait_for_page_text(object):
"""Selenium Wait helper to wait until specific text appears."""
def __init__(self, value):
"""Initilaize and save expected text to check for later."""
self.value = value
def __call__(self, driver):
"""Check if the expected check appears in the page yet."""
body = driver.find_element_by_tag_name('body')
return self.value in body.get_attribute('innerText')
# TODO: combine this, wait_for_page_text, and find_elements_by_text
class wait_for_result(object):
"""Selenium Wait helper to wait until specific text appears."""
def __init__(self, timeout, func, *args, **kwargs):
"""Initilaize and save expected text to check for later."""
self.timeout = timeout
self.func = func
self.args = args
self.kwargs = kwargs
def __call__(self, driver):
"""Check if the expected check appears in the page yet."""
start = time.time()
end = start + self.timeout
while time.time() < end:
retval = self.func(*self.args, **self.kwargs)
if retval:
return retval
return None
def get_element_depth(element):
"""Determine the depth of the element in the page, counting from body.
Used to find the most specific element out of a set of matching
elements for a condition where an element and its ancestors might match.
"""
depth = 1
if element.tag_name.lower() in ('html', 'body'):
return depth
next_element = element.find_element_by_xpath('..')
while next_element.tag_name.lower() not in ('html', 'body'):
depth += 1
assert depth < 100
next_element = next_element.find_element_by_xpath('..')
return depth
def get_el_text(e):
"""Get the inner text of a WebElement and cache for quicker re-lookup."""
try:
try:
return e.__innerText
except AttributeError:
text = ' '.join(e.get_attribute('innerText').split())
e.__innerText = text
return e.__innerText
except StaleElementReferenceException:
return ''
def find_element_by_text(driver, text, *args, **kwargs):
"""Find an element which contains the given text.
If multiple results are found, return them ordered
counting from the most specific to least.
"Specific" is defined by the length and depth of the
matched element. The shorter the overall text of the
element and the greater depth in the DOM the more
specific it is considered. (length is only applicable
when exact=False, otherwise only depth defines the
specificity)
parameters:
- driver Selenium or Element instance to locate under
- text Text to search for on the page
- fail_hard=False If True, raise exception on failure to locate
- exact=True Only locate elements that exactly match the text. If
False, locate elements which contain the text somewhere
in their content. (See n parameter description for the
affects on specificity)
- timeout=0.1 Time to spend re-checking for text to appear on page.
Larger values behave as a "wait for..." search for the
text.
"""
fail_hard = kwargs.pop('fail_hard', False)
elements = find_elements_by_text(driver, text, *args, **kwargs)
# elements.sort(key=lambda e: (len(get_el_text(e)), -get_element_depth(e)))
if fail_hard and not elements:
raise ValueError(
'Did not find in page: %r\n%s' %
(text, driver.page_source)
)
elif elements:
return elements[0]
def find_elements_by_text(driver, text,
n=0,
exact=True,
selector='*',
timeout=None):
"""Find an element which contains the given text."""
if driver.__class__.__name__ == 'WebElement':
element = driver
while driver.__class__.__name__ == 'WebElement':
driver = driver._parent
else:
element = None
start = time.time()
end = start + (5 if timeout is None else timeout)
while time.time() < end:
elements = driver.execute_script("""
var ctx = arguments[0]
var text = arguments[1]
var exact = arguments[2]
var selector = arguments[3]
var txtattr = 'textContent'
// TODO: Add a flag to use innerText for visible-only text
// txtattr = 'innerText'
function depth(top, el) {
var n = el.parentNode
var d = 0
while (n && n != top) {
d++
n = n.parentNode
}
return d
}
var all = (ctx || document).querySelectorAll(selector)
var check = (el) => el[txtattr].indexOf(text) > -1
if (exact) {
check = (el) => el[txtattr].trim() == text
}
var elements = Array.prototype.filter.call(all, check)
// Cull non-visible elements
elements = Array.prototype.filter.call(elements,
(el) => el.offsetParent !== null
)
elements.sort((a, b) => {
if (a[txtattr].length < b[txtattr].length) {
return true
} else if (a[txtattr].length > b[txtattr].length) {
return false
} else {
return depth(document.body, a) < depth(document.body, b)
}
})
// Cull parents
var indices = []
var precull = new Array(...elements)
elements.forEach((a) => {
elements.forEach((b, i) => {
if (a !== b && b.contains(a)) {
indices.push(i)
}
})
})
indices = new Array(...new Set(indices))
indices.sort((a,b) => a - b)
var culled = new Array(...indices)
while (indices.length > 0) {
var i = indices.pop();
elements.splice(i, 1)
}
/* uncomment for debugging */
window.last_results = {
target: ctx,
all: all,
culled: culled,
precull: precull,
elements: elements,
} /* uncomment for debugging */
return elements
""", element, text, exact, selector)
if elements:
break
return elements
def fill_input_by_label(driver, element, label, value, timeout=None):
"""Click on a field label and enter text to the associated input."""
input = None
@retry_w_timeout(1)
def _():
nonlocal input
el = find_element_by_text(element or driver, label,
timeout=0.2, selector='label')
el.click()
input = driver.execute_script('return document.activeElement')
try:
input.clear()
except InvalidElementStateException:
return False
else:
input.send_keys(value)
return True
return input
def fill_input_by_placeholder(driver, element, label, value, timeout=1):
"""Click on a field label and enter text to the associated input."""
input = None
@retry_w_timeout(1)
def _():
nonlocal input
css_sel = '[placeholder="%s"]' % label
try:
input = (element or driver).find_element_by_css_selector(css_sel)
except NoSuchElementException:
return False
input.clear()
input.send_keys(value)
return True
return input
def read_input_by_label(driver, element, label):
"""Click on a field label and read text from the associated input."""
value = None
@retry_w_timeout(1)
def _():
nonlocal value
el = find_element_by_text(element or driver, label, selector='label')
try:
el.click()
except WebDriverException as e:
return e
input = driver.execute_script('return document.activeElement')
value = input.get_attribute('value')
return value
return value
def retry_w_timeout(t, func=None, *args, **kwargs):
"""Retry a function until it returns truthy or a timeout occures."""
if func is None:
def dec(func, *args, **kwargs):
return retry_w_timeout(t, func, *args, **kwargs)
return dec
else:
start = time.time()
end = start
while end - start < t:
retval = func(*args, **kwargs)
if retval and not isinstance(retval, BaseException):
return retval
end = time.time()
if isinstance(retval, BaseException):
raise retval
def elem_parent(elem):
"""Return the parent of a given element."""
return elem.find_element_by_xpath('..')