cucu 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cucu might be problematic. Click here for more details.
- cucu/__init__.py +38 -0
- cucu/ansi_parser.py +58 -0
- cucu/behave_tweaks.py +196 -0
- cucu/browser/__init__.py +0 -0
- cucu/browser/core.py +80 -0
- cucu/browser/frames.py +106 -0
- cucu/browser/selenium.py +323 -0
- cucu/browser/selenium_tweaks.py +27 -0
- cucu/cli/__init__.py +3 -0
- cucu/cli/core.py +788 -0
- cucu/cli/run.py +207 -0
- cucu/cli/steps.py +137 -0
- cucu/cli/thread_dumper.py +55 -0
- cucu/config.py +440 -0
- cucu/edgedriver_autoinstaller/README.md +1 -0
- cucu/edgedriver_autoinstaller/__init__.py +37 -0
- cucu/edgedriver_autoinstaller/utils.py +231 -0
- cucu/environment.py +283 -0
- cucu/external/jquery/jquery-3.5.1.min.js +2 -0
- cucu/formatter/__init__.py +0 -0
- cucu/formatter/cucu.py +261 -0
- cucu/formatter/json.py +321 -0
- cucu/formatter/junit.py +289 -0
- cucu/fuzzy/__init__.py +3 -0
- cucu/fuzzy/core.py +107 -0
- cucu/fuzzy/fuzzy.js +253 -0
- cucu/helpers.py +875 -0
- cucu/hooks.py +205 -0
- cucu/language_server/__init__.py +3 -0
- cucu/language_server/core.py +114 -0
- cucu/lint/__init__.py +0 -0
- cucu/lint/linter.py +397 -0
- cucu/lint/rules/format.yaml +125 -0
- cucu/logger.py +113 -0
- cucu/matcher/__init__.py +0 -0
- cucu/matcher/core.py +30 -0
- cucu/page_checks.py +63 -0
- cucu/reporter/__init__.py +3 -0
- cucu/reporter/external/bootstrap.min.css +7 -0
- cucu/reporter/external/bootstrap.min.js +7 -0
- cucu/reporter/external/dataTables.bootstrap.min.css +1 -0
- cucu/reporter/external/dataTables.bootstrap.min.js +14 -0
- cucu/reporter/external/jquery-3.5.1.min.js +2 -0
- cucu/reporter/external/jquery.dataTables.min.js +192 -0
- cucu/reporter/external/popper.min.js +5 -0
- cucu/reporter/favicon.png +0 -0
- cucu/reporter/html.py +452 -0
- cucu/reporter/templates/feature.html +72 -0
- cucu/reporter/templates/flat.html +48 -0
- cucu/reporter/templates/index.html +49 -0
- cucu/reporter/templates/layout.html +109 -0
- cucu/reporter/templates/scenario.html +200 -0
- cucu/steps/__init__.py +27 -0
- cucu/steps/base_steps.py +88 -0
- cucu/steps/browser_steps.py +337 -0
- cucu/steps/button_steps.py +91 -0
- cucu/steps/checkbox_steps.py +111 -0
- cucu/steps/command_steps.py +181 -0
- cucu/steps/comment_steps.py +17 -0
- cucu/steps/draggable_steps.py +168 -0
- cucu/steps/dropdown_steps.py +467 -0
- cucu/steps/file_input_steps.py +80 -0
- cucu/steps/filesystem_steps.py +144 -0
- cucu/steps/flow_control_steps.py +198 -0
- cucu/steps/image_steps.py +37 -0
- cucu/steps/input_steps.py +301 -0
- cucu/steps/link_steps.py +63 -0
- cucu/steps/menuitem_steps.py +39 -0
- cucu/steps/platform_steps.py +29 -0
- cucu/steps/radio_steps.py +187 -0
- cucu/steps/step_utils.py +55 -0
- cucu/steps/tab_steps.py +68 -0
- cucu/steps/table_steps.py +437 -0
- cucu/steps/tables.js +28 -0
- cucu/steps/text_steps.py +78 -0
- cucu/steps/variable_steps.py +100 -0
- cucu/steps/webserver_steps.py +40 -0
- cucu/utils.py +269 -0
- cucu-1.0.0.dist-info/METADATA +424 -0
- cucu-1.0.0.dist-info/RECORD +83 -0
- cucu-1.0.0.dist-info/WHEEL +4 -0
- cucu-1.0.0.dist-info/entry_points.txt +2 -0
- cucu-1.0.0.dist-info/licenses/LICENSE +32 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import humanize
|
|
4
|
+
from selenium.common.exceptions import (
|
|
5
|
+
ElementClickInterceptedException,
|
|
6
|
+
ElementNotInteractableException,
|
|
7
|
+
)
|
|
8
|
+
from selenium.webdriver.common.by import By
|
|
9
|
+
from selenium.webdriver.common.keys import Keys
|
|
10
|
+
from selenium.webdriver.support.ui import Select
|
|
11
|
+
from tenacity import (
|
|
12
|
+
before_sleep_log,
|
|
13
|
+
retry_if_exception_type,
|
|
14
|
+
retry_if_result,
|
|
15
|
+
stop_after_attempt,
|
|
16
|
+
wait_fixed,
|
|
17
|
+
)
|
|
18
|
+
from tenacity import retry as retrying
|
|
19
|
+
|
|
20
|
+
from cucu import fuzzy, helpers, logger, retry, step
|
|
21
|
+
from cucu.steps.input_steps import find_input
|
|
22
|
+
from cucu.utils import take_saw_element_screenshot
|
|
23
|
+
|
|
24
|
+
from . import base_steps
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def find_dropdown(ctx, name, index=0):
|
|
28
|
+
"""
|
|
29
|
+
find a dropdown on screen by fuzzy matching on the name provided and the
|
|
30
|
+
target element:
|
|
31
|
+
|
|
32
|
+
* <select>
|
|
33
|
+
* <* role="combobox">
|
|
34
|
+
* <* role="listbox">
|
|
35
|
+
|
|
36
|
+
parameters:
|
|
37
|
+
ctx(object): behave context object used to share data between steps
|
|
38
|
+
name(str): name that identifies the desired dropdown on screen
|
|
39
|
+
index(str): the index of the dropdown if there are duplicates
|
|
40
|
+
|
|
41
|
+
returns:
|
|
42
|
+
the WebElement that matches the provided arguments.
|
|
43
|
+
"""
|
|
44
|
+
ctx.check_browser_initialized()
|
|
45
|
+
|
|
46
|
+
dropdown = fuzzy.find(
|
|
47
|
+
ctx.browser,
|
|
48
|
+
name,
|
|
49
|
+
[
|
|
50
|
+
"select",
|
|
51
|
+
'*[role="combobox"]',
|
|
52
|
+
'*[role="listbox"]',
|
|
53
|
+
],
|
|
54
|
+
index=index,
|
|
55
|
+
direction=fuzzy.Direction.LEFT_TO_RIGHT,
|
|
56
|
+
)
|
|
57
|
+
if not dropdown:
|
|
58
|
+
# In case the name is on the top of the dropdown element,
|
|
59
|
+
# the name is after the ting in DOM. Try the other direction.
|
|
60
|
+
dropdown = fuzzy.find(
|
|
61
|
+
ctx.browser,
|
|
62
|
+
name,
|
|
63
|
+
[
|
|
64
|
+
"select",
|
|
65
|
+
'*[role="combobox"]',
|
|
66
|
+
'*[role="listbox"]',
|
|
67
|
+
],
|
|
68
|
+
index=index,
|
|
69
|
+
direction=fuzzy.Direction.RIGHT_TO_LEFT,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
take_saw_element_screenshot(ctx, "dropdown", name, index, dropdown)
|
|
73
|
+
|
|
74
|
+
if dropdown:
|
|
75
|
+
outer_html = dropdown.get_attribute("outerHTML")
|
|
76
|
+
logger.debug(f'looked for dropdown "{name}", and found "{outer_html}"')
|
|
77
|
+
else:
|
|
78
|
+
logger.debug(f'looked for dropdown "{name}" but found none')
|
|
79
|
+
|
|
80
|
+
return dropdown
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@retrying(
|
|
84
|
+
retry=retry_if_result(lambda result: result is None),
|
|
85
|
+
stop=stop_after_attempt(10),
|
|
86
|
+
wait=wait_fixed(0.1),
|
|
87
|
+
before_sleep=before_sleep_log(logger, logging.DEBUG),
|
|
88
|
+
reraise=True,
|
|
89
|
+
retry_error_callback=lambda retry_state: retry_state.outcome.result(),
|
|
90
|
+
)
|
|
91
|
+
def find_dropdown_option(ctx, name, index=0):
|
|
92
|
+
"""
|
|
93
|
+
find a dropdown option with the provided name. It only considers
|
|
94
|
+
the web element with the name inside the element.
|
|
95
|
+
|
|
96
|
+
* <option>
|
|
97
|
+
* <* role="option">
|
|
98
|
+
|
|
99
|
+
parameters:
|
|
100
|
+
ctx(object): behave context object used to share data between steps
|
|
101
|
+
name(str): name that identifies the desired dropdown on screen
|
|
102
|
+
index(str): the index of the dropdown if there are duplicates
|
|
103
|
+
|
|
104
|
+
returns:
|
|
105
|
+
the WebElement that matches the provided arguments.
|
|
106
|
+
"""
|
|
107
|
+
ctx.check_browser_initialized()
|
|
108
|
+
|
|
109
|
+
option = fuzzy.find(
|
|
110
|
+
ctx.browser,
|
|
111
|
+
name,
|
|
112
|
+
[
|
|
113
|
+
"option",
|
|
114
|
+
'*[role="option"]',
|
|
115
|
+
'*[role="treeitem"]',
|
|
116
|
+
"*[aria-selected]",
|
|
117
|
+
],
|
|
118
|
+
index=index,
|
|
119
|
+
direction=fuzzy.Direction.LEFT_TO_RIGHT,
|
|
120
|
+
name_within_thing=True,
|
|
121
|
+
)
|
|
122
|
+
if option:
|
|
123
|
+
outer_html = option.get_attribute("outerHTML")
|
|
124
|
+
logger.debug(
|
|
125
|
+
f'looked for dropdown option "{name}", and found "{outer_html}"'
|
|
126
|
+
)
|
|
127
|
+
else:
|
|
128
|
+
logger.debug(f'looked for dropdown option "{name}" but found none')
|
|
129
|
+
|
|
130
|
+
return option
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def click_dropdown(ctx, dropdown):
|
|
134
|
+
"""
|
|
135
|
+
Internal method used to simply click a dropdown element
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
ctx(object): behave context object used to share data between steps
|
|
139
|
+
dropdown(WebElement): the dropdown element
|
|
140
|
+
"""
|
|
141
|
+
ctx.check_browser_initialized()
|
|
142
|
+
|
|
143
|
+
if base_steps.is_disabled(dropdown):
|
|
144
|
+
raise RuntimeError("unable to click the button, as it is disabled")
|
|
145
|
+
|
|
146
|
+
logger.debug("clicking dropdown")
|
|
147
|
+
try:
|
|
148
|
+
ctx.browser.click(dropdown)
|
|
149
|
+
except ElementClickInterceptedException:
|
|
150
|
+
clickable = dropdown
|
|
151
|
+
while True:
|
|
152
|
+
# In some cases, the dropdown is blocked by the selected item.
|
|
153
|
+
# It finds the ancestors of the dropdown that is clickable and click.
|
|
154
|
+
clickable = clickable.find_element(By.XPATH, "..")
|
|
155
|
+
try:
|
|
156
|
+
ctx.browser.click(clickable)
|
|
157
|
+
except ElementClickInterceptedException:
|
|
158
|
+
continue
|
|
159
|
+
break
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@retrying(
|
|
163
|
+
retry=retry_if_exception_type(ElementNotInteractableException),
|
|
164
|
+
stop=stop_after_attempt(10),
|
|
165
|
+
wait=wait_fixed(0.1),
|
|
166
|
+
before_sleep=before_sleep_log(logger, logging.DEBUG),
|
|
167
|
+
reraise=True,
|
|
168
|
+
)
|
|
169
|
+
def click_dynamic_dropdown_option(ctx, option_element):
|
|
170
|
+
ctx.browser.execute("arguments[0].scrollIntoView();", option_element)
|
|
171
|
+
ctx.browser.click(option_element)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def find_n_select_dropdown_option(ctx, dropdown, option, index=0):
|
|
175
|
+
"""
|
|
176
|
+
find and select dropdown option
|
|
177
|
+
|
|
178
|
+
parameters:
|
|
179
|
+
ctx(object): behave context object used to share data between steps
|
|
180
|
+
name(str): name that identifies the desired dropdown on screen
|
|
181
|
+
option(str): name of the option to select
|
|
182
|
+
index(str): the index of the dropdown if there are duplicates
|
|
183
|
+
"""
|
|
184
|
+
ctx.check_browser_initialized()
|
|
185
|
+
|
|
186
|
+
dropdown_element = find_dropdown(ctx, dropdown, index)
|
|
187
|
+
|
|
188
|
+
if dropdown_element is None:
|
|
189
|
+
prefix = "" if index == 0 else f"{humanize.ordinal(index)} "
|
|
190
|
+
raise RuntimeError(f"unable to find the {prefix}dropdown {dropdown}")
|
|
191
|
+
|
|
192
|
+
if base_steps.is_disabled(dropdown_element):
|
|
193
|
+
raise RuntimeError(
|
|
194
|
+
"unable to select from the dropdown, as it is disabled"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
if dropdown_element.tag_name == "select":
|
|
198
|
+
select_element = Select(dropdown_element)
|
|
199
|
+
select_element.select_by_visible_text(option)
|
|
200
|
+
|
|
201
|
+
else:
|
|
202
|
+
if dropdown_element.get_attribute("aria-expanded") != "true":
|
|
203
|
+
# open the dropdown
|
|
204
|
+
click_dropdown(ctx, dropdown_element)
|
|
205
|
+
|
|
206
|
+
option_element = find_dropdown_option(ctx, option)
|
|
207
|
+
|
|
208
|
+
if option_element is None:
|
|
209
|
+
raise RuntimeError(
|
|
210
|
+
f'unable to find option "{option}" in dropdown "{dropdown}"'
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
logger.debug("clicking dropdown option")
|
|
214
|
+
ctx.browser.execute("arguments[0].scrollIntoView();", option_element)
|
|
215
|
+
ctx.browser.click(option_element)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def find_n_select_dynamic_dropdown_option(ctx, dropdown, option, index=0):
|
|
219
|
+
"""
|
|
220
|
+
find and select dynamic dropdown option
|
|
221
|
+
|
|
222
|
+
parameters:
|
|
223
|
+
ctx(object): behave context object used to share data between steps
|
|
224
|
+
name(str): name that identifies the desired dropdown on screen
|
|
225
|
+
option(str): name of the option to select
|
|
226
|
+
index(str): the index of the dropdown if there are duplicates
|
|
227
|
+
"""
|
|
228
|
+
ctx.check_browser_initialized()
|
|
229
|
+
|
|
230
|
+
dropdown_element = find_dropdown(ctx, dropdown, index)
|
|
231
|
+
|
|
232
|
+
if dropdown_element is None:
|
|
233
|
+
prefix = "" if index == 0 else f"{humanize.ordinal(index)} "
|
|
234
|
+
raise RuntimeError(f"unable to find the {prefix}dropdown {dropdown}")
|
|
235
|
+
|
|
236
|
+
if base_steps.is_disabled(dropdown_element):
|
|
237
|
+
raise RuntimeError(
|
|
238
|
+
"unable to select from the dropdown, as it is disabled"
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if dropdown_element.get_attribute("aria-expanded") != "true":
|
|
242
|
+
# open the dropdown
|
|
243
|
+
click_dropdown(ctx, dropdown_element)
|
|
244
|
+
|
|
245
|
+
option_element = find_dropdown_option(ctx, option)
|
|
246
|
+
|
|
247
|
+
# Use the search feature to make the option visible so cucu can pick it up
|
|
248
|
+
if option_element is None:
|
|
249
|
+
dropdown_input = find_input(ctx, dropdown, index)
|
|
250
|
+
logger.debug(
|
|
251
|
+
f'option "{option}" is not found, trying to send keys "{option}".'
|
|
252
|
+
)
|
|
253
|
+
dropdown_value = dropdown_input.get_attribute("value")
|
|
254
|
+
if dropdown_value:
|
|
255
|
+
logger.debug(f"clear dropdown value: {dropdown_value}")
|
|
256
|
+
dropdown_input.send_keys(
|
|
257
|
+
Keys.ARROW_RIGHT * len(dropdown_value)
|
|
258
|
+
) # make sure the cursor is at the end
|
|
259
|
+
dropdown_input.send_keys(Keys.BACKSPACE * len(dropdown_value))
|
|
260
|
+
# After each key stroke there is a request and an update of the option list. To prevent stale element,
|
|
261
|
+
# we send keys one by one here and try to find the option after each key.
|
|
262
|
+
for key in option:
|
|
263
|
+
try:
|
|
264
|
+
dropdown_input = find_input(ctx, dropdown, index)
|
|
265
|
+
logger.debug(f'sending key "{key}"')
|
|
266
|
+
dropdown_input.send_keys(key)
|
|
267
|
+
ctx.browser.wait_for_page_to_load()
|
|
268
|
+
option_element = find_dropdown_option(ctx, option)
|
|
269
|
+
if option_element:
|
|
270
|
+
break
|
|
271
|
+
except Exception:
|
|
272
|
+
option_element = None
|
|
273
|
+
|
|
274
|
+
if option_element is None:
|
|
275
|
+
raise RuntimeError(
|
|
276
|
+
f'unable to find option "{option}" in dropdown "{dropdown}"'
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
logger.debug("clicking dropdown option")
|
|
280
|
+
click_dynamic_dropdown_option(ctx, option_element)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def assert_dropdown_option_selected(
|
|
284
|
+
ctx, dropdown, option, index=0, is_selected=True
|
|
285
|
+
):
|
|
286
|
+
"""
|
|
287
|
+
assert dropdown option is selected
|
|
288
|
+
|
|
289
|
+
parameters:
|
|
290
|
+
ctx(object): behave context object used to share data between steps
|
|
291
|
+
name(str): name that identifies the desired dropdown on screen
|
|
292
|
+
option(str): name of the option to select
|
|
293
|
+
index(str): the index of the dropdown if there are duplicates
|
|
294
|
+
"""
|
|
295
|
+
ctx.check_browser_initialized()
|
|
296
|
+
|
|
297
|
+
dropdown_element = find_dropdown(ctx, dropdown, index)
|
|
298
|
+
if dropdown_element is None:
|
|
299
|
+
raise RuntimeError(f'unable to find dropdown "{dropdown}"')
|
|
300
|
+
|
|
301
|
+
selected_option = None
|
|
302
|
+
if dropdown_element.tag_name == "select":
|
|
303
|
+
select_element = Select(dropdown_element)
|
|
304
|
+
selected_option = select_element.first_selected_option
|
|
305
|
+
|
|
306
|
+
if selected_option is None:
|
|
307
|
+
raise RuntimeError(
|
|
308
|
+
f"unable to find selected option in dropdown {dropdown}"
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
selected_name = selected_option.get_attribute("textContent")
|
|
312
|
+
|
|
313
|
+
# XXX: we're doing contains because a lot of our existing dropdowns
|
|
314
|
+
# do not use aria-label/aria-describedby to make them accessible
|
|
315
|
+
# and easier to find for automation by their name
|
|
316
|
+
if is_selected:
|
|
317
|
+
if selected_name.find(option) == -1:
|
|
318
|
+
raise RuntimeError(f"{option} is not selected")
|
|
319
|
+
else:
|
|
320
|
+
if selected_name.find(option) != -1:
|
|
321
|
+
raise RuntimeError(f"{option} is selected")
|
|
322
|
+
|
|
323
|
+
else:
|
|
324
|
+
if dropdown_element.get_attribute("aria-expanded") != "true":
|
|
325
|
+
# open the dropdown to see its options
|
|
326
|
+
click_dropdown(ctx, dropdown_element)
|
|
327
|
+
|
|
328
|
+
selected_option = find_dropdown_option(ctx, option)
|
|
329
|
+
|
|
330
|
+
if selected_option is None:
|
|
331
|
+
raise RuntimeError(
|
|
332
|
+
f'unable to find option "{option}" in dropdown "{dropdown}"'
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
if is_selected:
|
|
336
|
+
if selected_option.get_attribute("aria-selected") != "true":
|
|
337
|
+
raise RuntimeError(f"{option} is not selected")
|
|
338
|
+
else:
|
|
339
|
+
if selected_option.get_attribute("aria-selected") == "true":
|
|
340
|
+
raise RuntimeError(f"{option} is selected")
|
|
341
|
+
|
|
342
|
+
# close the dropdown
|
|
343
|
+
click_dropdown(ctx, dropdown_element)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
helpers.define_should_see_thing_with_name_steps("dropdown", find_dropdown)
|
|
347
|
+
helpers.define_thing_with_name_in_state_steps(
|
|
348
|
+
"dropdown", "disabled", find_dropdown, base_steps.is_disabled
|
|
349
|
+
)
|
|
350
|
+
helpers.define_thing_with_name_in_state_steps(
|
|
351
|
+
"dropdown", "not disabled", find_dropdown, base_steps.is_not_disabled
|
|
352
|
+
)
|
|
353
|
+
helpers.define_run_steps_if_I_can_see_element_with_name_steps(
|
|
354
|
+
"dropdown", find_dropdown
|
|
355
|
+
)
|
|
356
|
+
helpers.define_action_on_thing_with_name_steps(
|
|
357
|
+
"dropdown", "click", find_dropdown, click_dropdown, with_nth=True
|
|
358
|
+
)
|
|
359
|
+
helpers.define_thing_with_name_in_state_steps(
|
|
360
|
+
"dropdown option", "disabled", find_dropdown_option, base_steps.is_disabled
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
@step('I select the option "{option}" from the dropdown "{dropdown}"')
|
|
365
|
+
def select_option_from_dropdown(ctx, option, dropdown):
|
|
366
|
+
find_n_select_dropdown_option(ctx, dropdown, option)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
@step(
|
|
370
|
+
'I select the option "{option}" from the "{index:nth}" dropdown "{dropdown}"'
|
|
371
|
+
)
|
|
372
|
+
def select_option_from_nth_dropdown(ctx, option, dropdown, index):
|
|
373
|
+
find_n_select_dropdown_option(ctx, dropdown, option, index)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
@step(
|
|
377
|
+
'I wait to select the option "{option}" from the "{index:nth}" dropdown "{dropdown}"'
|
|
378
|
+
)
|
|
379
|
+
def wait_to_select_option_from_nth_dropdown(ctx, option, dropdown, index):
|
|
380
|
+
retry(find_n_select_dropdown_option)(ctx, dropdown, option, index)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
@step('I wait to select the option "{option}" from the dropdown "{dropdown}"')
|
|
384
|
+
def wait_to_select_option_from_dropdown(ctx, option, dropdown):
|
|
385
|
+
retry(find_n_select_dropdown_option)(ctx, dropdown, option)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
@step('I select the option "{option}" from the dynamic dropdown "{dropdown}"')
|
|
389
|
+
def select_option_from_dynamic_dropdown(ctx, option, dropdown):
|
|
390
|
+
find_n_select_dynamic_dropdown_option(ctx, dropdown, option)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
@step(
|
|
394
|
+
'I select the option "{option}" from the "{index:nth}" dynamic dropdown "{dropdown}"'
|
|
395
|
+
)
|
|
396
|
+
def select_option_from_nth_dynamic_dropdown(ctx, option, dropdown, index):
|
|
397
|
+
find_n_select_dynamic_dropdown_option(ctx, dropdown, option, index)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
@step(
|
|
401
|
+
'I wait to select the option "{option}" from the "{index:nth}" dynamic dropdown "{dropdown}"'
|
|
402
|
+
)
|
|
403
|
+
def wait_to_select_option_from_nth_dynamic_dropdown(
|
|
404
|
+
ctx, option, dropdown, index
|
|
405
|
+
):
|
|
406
|
+
retry(find_n_select_dynamic_dropdown_option)(ctx, dropdown, option, index)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
@step(
|
|
410
|
+
'I wait to select the option "{option}" from the dynamic dropdown "{dropdown}"'
|
|
411
|
+
)
|
|
412
|
+
def wait_to_select_option_from_dynamic_dropdown(ctx, option, dropdown):
|
|
413
|
+
retry(find_n_select_dynamic_dropdown_option)(ctx, dropdown, option)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
@step(
|
|
417
|
+
'I should see the option "{option}" is selected on the dropdown "{dropdown}"'
|
|
418
|
+
)
|
|
419
|
+
def should_see_option_is_selected_from_dropdown(ctx, option, dropdown):
|
|
420
|
+
assert_dropdown_option_selected(ctx, dropdown, option, is_selected=True)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
@step(
|
|
424
|
+
'I should see the option "{option}" is selected on the "{index:nth}" dropdown "{dropdown}"'
|
|
425
|
+
)
|
|
426
|
+
def should_see_option_is_selected_from_nth_dropdown(
|
|
427
|
+
ctx, option, dropdown, index
|
|
428
|
+
):
|
|
429
|
+
assert_dropdown_option_selected(
|
|
430
|
+
ctx, dropdown, option, index, is_selected=True
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
@step(
|
|
435
|
+
'I wait to see the option "{option}" is selected on the dropdown "{dropdown}"'
|
|
436
|
+
)
|
|
437
|
+
def wait_to_see_option_is_selected_from_dropdown(ctx, option, dropdown):
|
|
438
|
+
retry(assert_dropdown_option_selected)(
|
|
439
|
+
ctx, dropdown, option, is_selected=True
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
@step(
|
|
444
|
+
'I should see the option "{option}" is not selected on the "{index:nth}" dropdown "{dropdown}"'
|
|
445
|
+
)
|
|
446
|
+
def should_see_option_is_not_selected_from_nth_dropdown(
|
|
447
|
+
ctx, option, dropdown, index
|
|
448
|
+
):
|
|
449
|
+
assert_dropdown_option_selected(
|
|
450
|
+
ctx, dropdown, option, index, is_selected=False
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
@step(
|
|
455
|
+
'I should see the option "{option}" is not selected on the dropdown "{dropdown}"'
|
|
456
|
+
)
|
|
457
|
+
def should_see_option_is_not_selected_from_dropdown(ctx, option, dropdown):
|
|
458
|
+
assert_dropdown_option_selected(ctx, dropdown, option, is_selected=False)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
@step(
|
|
462
|
+
'I wait to see the option "{option}" is not selected on the dropdown "{dropdown}"'
|
|
463
|
+
)
|
|
464
|
+
def wait_to_see_option_is_not_selected_from_dropdown(
|
|
465
|
+
ctx, option, dropdown, is_selected=False
|
|
466
|
+
):
|
|
467
|
+
retry(assert_dropdown_option_selected)(ctx, dropdown, option)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import humanize
|
|
4
|
+
|
|
5
|
+
from cucu import fuzzy, logger, step
|
|
6
|
+
from cucu.utils import take_saw_element_screenshot
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def find_file_input(ctx, name, index=0):
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
* <input type="file">
|
|
13
|
+
|
|
14
|
+
parameters:
|
|
15
|
+
ctx(object): behave context object used to share data between steps
|
|
16
|
+
name(str): name that identifies the desired button on screen
|
|
17
|
+
index(str): the index of the button if there are duplicates
|
|
18
|
+
|
|
19
|
+
returns:
|
|
20
|
+
the WebElement that matches the provided arguments.
|
|
21
|
+
"""
|
|
22
|
+
ctx.check_browser_initialized()
|
|
23
|
+
element = fuzzy.find(
|
|
24
|
+
ctx.browser, name, ['input[type="file"]'], index=index
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
prefix = "" if index == 0 else f"{humanize.ordinal(index)} "
|
|
28
|
+
|
|
29
|
+
take_saw_element_screenshot(ctx, "file input", name, index, element)
|
|
30
|
+
|
|
31
|
+
if element is None:
|
|
32
|
+
raise RuntimeError(f'unable to find the {prefix}file input "{name}"')
|
|
33
|
+
|
|
34
|
+
return element
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@step('I upload the file "{filepath}" to the file input "{name}"')
|
|
38
|
+
def upload_file_to_input(ctx, filepath, name):
|
|
39
|
+
_input = find_file_input(ctx, name)
|
|
40
|
+
_input.send_keys(os.path.abspath(filepath))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
JS_DROP_FILE = """
|
|
44
|
+
var target = arguments[0],
|
|
45
|
+
offsetX = arguments[1],
|
|
46
|
+
offsetY = arguments[2],
|
|
47
|
+
document = target.ownerDocument || document,
|
|
48
|
+
window = document.defaultView || window;
|
|
49
|
+
|
|
50
|
+
var input = document.createElement('INPUT');
|
|
51
|
+
input.type = 'file';
|
|
52
|
+
input.onchange = function () {
|
|
53
|
+
var rect = target.getBoundingClientRect(),
|
|
54
|
+
x = rect.left + (offsetX || (rect.width >> 1)),
|
|
55
|
+
y = rect.top + (offsetY || (rect.height >> 1)),
|
|
56
|
+
dataTransfer = { files: this.files };
|
|
57
|
+
|
|
58
|
+
['dragenter', 'dragover', 'drop'].forEach(function (name) {
|
|
59
|
+
var evt = document.createEvent('MouseEvent');
|
|
60
|
+
evt.initMouseEvent(name, !0, !0, window, 0, 0, 0, x, y, !1, !1, !1, !1, 0, null);
|
|
61
|
+
evt.dataTransfer = dataTransfer;
|
|
62
|
+
target.dispatchEvent(evt);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
setTimeout(function () { document.body.removeChild(input); }, 25);
|
|
66
|
+
};
|
|
67
|
+
document.body.appendChild(input);
|
|
68
|
+
return input;
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@step('I drag and drop the file "{filepath}" to "{name}"')
|
|
73
|
+
def drag_and_drop_file(ctx, name, filepath):
|
|
74
|
+
drop_target = fuzzy.find(ctx.browser, name, ["*"])
|
|
75
|
+
drop_target_html = drop_target.get_attribute("outerHTML")
|
|
76
|
+
logger.debug(
|
|
77
|
+
f'looked for drag & drop target "{name}" and found "{drop_target_html}"'
|
|
78
|
+
)
|
|
79
|
+
file_input = ctx.browser.execute(JS_DROP_FILE, drop_target, 0, 0)
|
|
80
|
+
file_input.send_keys(os.path.abspath(filepath))
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
from cucu import retry, step
|
|
5
|
+
from cucu.config import CONFIG
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@step('I create a file at "{filepath}" with the following')
|
|
9
|
+
def create_file_with_the_following(ctx, filepath):
|
|
10
|
+
dirname = os.path.dirname(filepath)
|
|
11
|
+
if dirname and not os.path.exists(dirname):
|
|
12
|
+
os.makedirs(dirname)
|
|
13
|
+
|
|
14
|
+
with open(filepath, "wb") as output:
|
|
15
|
+
output.write(bytes(ctx.text, "utf8"))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@step('I create the directory at "{filepath}"')
|
|
19
|
+
def create_directory_at(ctx, filepath):
|
|
20
|
+
os.makedirs(filepath)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@step('I delete the file at "{filepath}"')
|
|
24
|
+
def delete_file_at(ctx, filepath):
|
|
25
|
+
os.remove(filepath)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@step('I delete the file at "{filepath}" if it exists')
|
|
29
|
+
def delete_file_at_if_it_exists(ctx, filepath):
|
|
30
|
+
if os.path.exists(filepath):
|
|
31
|
+
os.remove(filepath)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@step(
|
|
35
|
+
'I read the contents of the file at "{filepath}" and save to the variable "{variable}"'
|
|
36
|
+
)
|
|
37
|
+
def read_file_contents(ctx, filepath, variable):
|
|
38
|
+
with open(filepath, "r") as _input:
|
|
39
|
+
CONFIG[variable] = CONFIG.escape(_input.read())
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@step('I append to the file at "{filepath}" the following')
|
|
43
|
+
def append_to_file_the_following(ctx, filepath):
|
|
44
|
+
with open(filepath, "ab") as output:
|
|
45
|
+
output.write(bytes(ctx.text, "utf8"))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def assert_file(ctx, filepath):
|
|
49
|
+
if not (os.path.exists(filepath) and os.path.isfile(filepath)):
|
|
50
|
+
raise RuntimeError(f"unable to see file at {filepath}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@step('I should see a file at "{filepath}"')
|
|
54
|
+
def should_see_file(ctx, filepath):
|
|
55
|
+
assert_file(ctx, filepath)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@step('I should not see a file at "{filepath}"')
|
|
59
|
+
def should_not_see_file(ctx, filepath):
|
|
60
|
+
if os.path.exists(filepath) and os.path.isfile(filepath):
|
|
61
|
+
raise RuntimeError(f"able to see file at {filepath}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@step('I wait to see a file at "{filepath}"')
|
|
65
|
+
def wait_to_see_file(ctx, filepath):
|
|
66
|
+
retry(assert_file)(ctx, filepath)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@step('I wait up to "{seconds}" seconds to see a file at "{filepath}"')
|
|
70
|
+
def wait_up_to_see_file(ctx, seconds, filepath):
|
|
71
|
+
seconds = float(seconds)
|
|
72
|
+
retry(assert_file, wait_up_to_s=seconds)(ctx, filepath)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@step('I should see the directory at "{filepath}"')
|
|
76
|
+
def should_see_directory(ctx, filepath):
|
|
77
|
+
if not (os.path.exists(filepath) and os.path.isdir(filepath)):
|
|
78
|
+
raise RuntimeError(f"unable to see directory at {filepath}")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@step('I should not see the directory at "{filepath}"')
|
|
82
|
+
def should_not_see_directory(ctx, filepath):
|
|
83
|
+
if os.path.exists(filepath) and os.path.isdir(filepath):
|
|
84
|
+
raise RuntimeError(f"able to see directory at {filepath}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@step('I should see the file at "{filepath}" is equal to the following')
|
|
88
|
+
def should_see_file_is_equal_to_the_following(ctx, filepath):
|
|
89
|
+
with open(filepath, "rb") as input:
|
|
90
|
+
file_contents = input.read().decode("utf8")
|
|
91
|
+
|
|
92
|
+
if file_contents != ctx.text:
|
|
93
|
+
raise RuntimeError(
|
|
94
|
+
f"\n{file_contents}\nis not equal to\n{ctx.text}\n"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@step('I should see the file at "{filepath}" contains the following')
|
|
99
|
+
def should_see_file_contains_the_following(ctx, filepath):
|
|
100
|
+
with open(filepath, "rb") as input:
|
|
101
|
+
file_contents = input.read().decode("utf8")
|
|
102
|
+
|
|
103
|
+
if ctx.text not in file_contents:
|
|
104
|
+
raise RuntimeError(
|
|
105
|
+
f"\n{file_contents}\ndoes not contain\n{ctx.text}\n"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@step('I should see the file at "{filepath}" matches the following')
|
|
110
|
+
def should_see_file_matches_the_following(ctx, filepath):
|
|
111
|
+
with open(filepath, "rb") as input:
|
|
112
|
+
file_contents = input.read().decode("utf8")
|
|
113
|
+
|
|
114
|
+
if not re.match(ctx.text, file_contents):
|
|
115
|
+
raise RuntimeError(
|
|
116
|
+
f"\n{file_contents}\ndoes not match\n{ctx.text}\n"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@step('I should see the file at "{filepath}" is not equal to the following')
|
|
121
|
+
def should_see_file_is_not_equal_to_the_following(ctx, filepath):
|
|
122
|
+
with open(filepath, "rb") as input:
|
|
123
|
+
file_contents = input.read().decode("utf8")
|
|
124
|
+
|
|
125
|
+
if file_contents != ctx.text:
|
|
126
|
+
raise RuntimeError(f"\n{file_contents}\nis equal to\n{ctx.text}\n")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@step('I should see the file at "{filepath}" does not contain the following')
|
|
130
|
+
def should_see_file_does_not_contain_the_following(ctx, filepath):
|
|
131
|
+
with open(filepath, "rb") as input:
|
|
132
|
+
file_contents = input.read().decode("utf8")
|
|
133
|
+
|
|
134
|
+
if ctx.text in file_contents:
|
|
135
|
+
raise RuntimeError(f"\n{file_contents}\ncontains\n{ctx.text}\n")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@step('I should see the file at "{filepath}" does not match the following')
|
|
139
|
+
def should_see_file_does_not_match_the_following(ctx, filepath):
|
|
140
|
+
with open(filepath, "rb") as input:
|
|
141
|
+
file_contents = input.read().decode("utf8")
|
|
142
|
+
|
|
143
|
+
if re.match(ctx.text, file_contents):
|
|
144
|
+
raise RuntimeError(f"\n{file_contents}\nmatches\n{ctx.text}\n")
|