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,198 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import subprocess
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from cucu import logger, retry, run_steps, step
|
|
7
|
+
from cucu.config import CONFIG
|
|
8
|
+
from cucu.hooks import register_after_this_scenario_hook
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@step('I skip this scenario if the file at "{filepath}" exists')
|
|
12
|
+
def skip_scenario_if_file_exists(ctx, filepath):
|
|
13
|
+
if os.path.exists(filepath):
|
|
14
|
+
ctx.scenario.skip(
|
|
15
|
+
reason='skipping scenario since file at "{filepath}" exists'
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@step('I expect the following step to fail with "{message}"')
|
|
20
|
+
def expect_the_following_step_to_fail(ctx, message):
|
|
21
|
+
try:
|
|
22
|
+
run_steps(ctx, ctx.text)
|
|
23
|
+
except Exception as exception:
|
|
24
|
+
if str(exception).find(message) == -1:
|
|
25
|
+
raise RuntimeError(
|
|
26
|
+
f'expected failure message was "{str(exception)}" not "{message}"'
|
|
27
|
+
)
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
raise RuntimeError("previous steps did not fail!")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@step('I should see the previous step took less than "{seconds}" seconds')
|
|
34
|
+
def should_see_previous_step_took_less_than(ctx, seconds):
|
|
35
|
+
if ctx.previous_step_duration > float(seconds):
|
|
36
|
+
raise RuntimeError(
|
|
37
|
+
f"previous step took {ctx.previous_step_duration}, which is more than {seconds}"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@step('I should see the previous step took more than "{seconds}" seconds')
|
|
42
|
+
def should_see_previous_step_took_more_than(ctx, seconds):
|
|
43
|
+
if ctx.previous_step_duration < float(seconds):
|
|
44
|
+
raise RuntimeError(
|
|
45
|
+
f"previous step took {ctx.previous_step_duration}, which is less than {seconds}"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@step("I wait to see the following steps succeed")
|
|
50
|
+
def wait_to_see_the_following_steps_suceed(ctx):
|
|
51
|
+
retry(run_steps)(ctx, ctx.text)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@step('I wait up to "{seconds}" seconds to see the following steps succeed')
|
|
55
|
+
def wait_up_to_seconds_to_see_the_following_steps_suceed(ctx, seconds):
|
|
56
|
+
retry(run_steps, wait_up_to_s=float(seconds))(ctx, ctx.text)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def wait_for_steps_to_fail(ctx, steps, timeout=None):
|
|
60
|
+
def steps_should_fail():
|
|
61
|
+
try:
|
|
62
|
+
run_steps(ctx, steps)
|
|
63
|
+
except: # noqa: E722
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
raise RuntimeError("underlying steps did not fail")
|
|
67
|
+
|
|
68
|
+
retry(steps_should_fail, wait_up_to_s=timeout)()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@step("I wait to see the following steps fail")
|
|
72
|
+
def wait_to_see_the_following_steps_fail(ctx):
|
|
73
|
+
wait_for_steps_to_fail(ctx, ctx.text)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@step('I wait up to "{seconds}" seconds to see the following steps fail')
|
|
77
|
+
def wait_up_to_seconds_to_see_the_following_steps_fail(ctx, seconds):
|
|
78
|
+
wait_for_steps_to_fail(ctx, ctx.text, timeout=float(seconds))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def repeat_steps(ctx, steps, repeat, variable=None):
|
|
82
|
+
for index in range(0, repeat):
|
|
83
|
+
if variable is not None:
|
|
84
|
+
CONFIG[variable] = index + 1
|
|
85
|
+
|
|
86
|
+
run_steps(ctx, steps)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@step('I repeat "{repeat}" times the following steps')
|
|
90
|
+
def repeat_n_times_the_following_steps(ctx, repeat):
|
|
91
|
+
repeat_steps(ctx, ctx.text, int(repeat))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@step(
|
|
95
|
+
'I repeat "{repeat}" times the following steps with iteration variable "{variable}"'
|
|
96
|
+
)
|
|
97
|
+
def repeat_n_times_the_following_steps_with_variable(ctx, repeat, variable):
|
|
98
|
+
repeat_steps(ctx, ctx.text, int(repeat), variable=variable)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@step('I run the following steps if the file at "{filepath}" does not exist')
|
|
102
|
+
def run_steps_if_file_does_not_exist(ctx, filepath):
|
|
103
|
+
if not os.path.exists(filepath):
|
|
104
|
+
run_steps(ctx, ctx.text)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@step('I run the following steps if the file at "{filepath}" exists')
|
|
108
|
+
def run_steps_if_file_exists(ctx, filepath):
|
|
109
|
+
if os.path.exists(filepath):
|
|
110
|
+
run_steps(ctx, ctx.text)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@step('I run the following timed steps as "{name}"')
|
|
114
|
+
def run_and_measure_the_following_steps(ctx, name):
|
|
115
|
+
start = time.time()
|
|
116
|
+
run_steps(ctx, ctx.text)
|
|
117
|
+
duration = round(time.time() - start, 3)
|
|
118
|
+
logger.info(f'"{name}" timer took {duration}s')
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@step('I start the timer "{name}"')
|
|
122
|
+
def start_the_timer(ctx, name):
|
|
123
|
+
ctx.step_timers[name] = time.time()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@step('I stop the timer "{name}"')
|
|
127
|
+
def stop_the_timer(ctx, name):
|
|
128
|
+
if name not in ctx.step_timers:
|
|
129
|
+
raise RuntimeError(f'no previously started timer with name "{name}"')
|
|
130
|
+
start = ctx.step_timers[name]
|
|
131
|
+
del ctx.step_timers[name]
|
|
132
|
+
|
|
133
|
+
duration = round(time.time() - start, 3)
|
|
134
|
+
logger.info(f'"{name}" timer took {duration}s')
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def run_feature(ctx, filepath, results):
|
|
138
|
+
command = f"cucu run {filepath} --results {results}"
|
|
139
|
+
process = subprocess.run(command, shell=True) # nosec
|
|
140
|
+
|
|
141
|
+
return_code = process.returncode
|
|
142
|
+
if return_code != 0:
|
|
143
|
+
raise RuntimeError(
|
|
144
|
+
f'"{command}" exited with {return_code}, see above for details'
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@step(
|
|
149
|
+
'I run the feature at "{feature_filepath}" with results at "{results_filepath}"'
|
|
150
|
+
)
|
|
151
|
+
def run_feature_at(ctx, feature_filepath, results_filepath):
|
|
152
|
+
run_feature(ctx, feature_filepath, results_filepath)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@step(
|
|
156
|
+
'I run the feature at "{feature_filepath}" with results at "{results_filepath}" if the file at "{filepath}" does not exist'
|
|
157
|
+
)
|
|
158
|
+
def run_feature_if_file_does_not_exist(
|
|
159
|
+
ctx, feature_filepath, results_filepath, filepath
|
|
160
|
+
):
|
|
161
|
+
if not os.path.exists(filepath):
|
|
162
|
+
run_feature(ctx, feature_filepath, results_filepath)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@step(
|
|
166
|
+
'I run the feature at "{feature_filepath}" with results at "{results_filepath}" if the file at "{filepath}" exists'
|
|
167
|
+
)
|
|
168
|
+
def run_feature_if_file_exists(
|
|
169
|
+
ctx, feature_filepath, results_filepath, filepath
|
|
170
|
+
):
|
|
171
|
+
if os.path.exists(filepath):
|
|
172
|
+
run_feature(ctx, feature_filepath, results_filepath)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@step("I run the following steps at the end of the current scenario")
|
|
176
|
+
def run_the_following_steps_at_end_of_scenario(ctx):
|
|
177
|
+
steps = ctx.text
|
|
178
|
+
|
|
179
|
+
def run_final_steps(ctx):
|
|
180
|
+
run_steps(ctx, steps)
|
|
181
|
+
|
|
182
|
+
register_after_this_scenario_hook(run_final_steps)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
for operation, operator in {
|
|
186
|
+
"is equal to": lambda x, y: x == y,
|
|
187
|
+
"is not equal to": lambda x, y: x != y,
|
|
188
|
+
"contains": lambda x, y: y in x,
|
|
189
|
+
"does not contain": lambda x, y: y not in x,
|
|
190
|
+
"matches": lambda x, y: re.match(y, x),
|
|
191
|
+
"does not match": lambda x, y: not (re.match(y, x)),
|
|
192
|
+
}.items():
|
|
193
|
+
# the operator keyword below is used to scope the value of the operator
|
|
194
|
+
# value to the correct value at runtime when creating the various steps
|
|
195
|
+
@step('I run the following steps if "{this}" ' + operation + ' "{that}"')
|
|
196
|
+
def run_steps_if_this_operator_that(ctx, this, that, operator=operator):
|
|
197
|
+
if operator(this, that):
|
|
198
|
+
run_steps(ctx, ctx.text)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from cucu import helpers
|
|
2
|
+
from cucu.utils import take_saw_element_screenshot
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def find_image(ctx, name, index=0):
|
|
6
|
+
"""
|
|
7
|
+
find an input on screen by fuzzy matching on the name provided and the
|
|
8
|
+
target element:
|
|
9
|
+
|
|
10
|
+
* <img>
|
|
11
|
+
|
|
12
|
+
parameters:
|
|
13
|
+
ctx(object): behave context object used to share data between steps
|
|
14
|
+
name(str): name that identifies the desired image on screen
|
|
15
|
+
index(str): the index of the image if there are duplicates
|
|
16
|
+
|
|
17
|
+
returns:
|
|
18
|
+
the WebElement that matches the provided arguments.
|
|
19
|
+
"""
|
|
20
|
+
ctx.check_browser_initialized()
|
|
21
|
+
|
|
22
|
+
name = name.replace('"', '\\"')
|
|
23
|
+
images = ctx.browser.css_find_elements(f'img[alt="{name}"')
|
|
24
|
+
|
|
25
|
+
if index >= len(images):
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
element = images[index]
|
|
29
|
+
|
|
30
|
+
take_saw_element_screenshot(ctx, "image", name, index, element)
|
|
31
|
+
|
|
32
|
+
return element
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
helpers.define_should_see_thing_with_name_steps(
|
|
36
|
+
"image with the alt text", find_image
|
|
37
|
+
)
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
import humanize
|
|
4
|
+
|
|
5
|
+
# XXX: this would have to be generalized to other browser abstractions
|
|
6
|
+
from selenium.webdriver.common.keys import Keys
|
|
7
|
+
|
|
8
|
+
from cucu import fuzzy, helpers, retry, step
|
|
9
|
+
from cucu.utils import take_saw_element_screenshot
|
|
10
|
+
|
|
11
|
+
from . import base_steps
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def find_input(ctx, name, index=0):
|
|
15
|
+
"""
|
|
16
|
+
find an input on screen by fuzzy matching on the name and index provided.
|
|
17
|
+
|
|
18
|
+
* <input>
|
|
19
|
+
* <textarea>
|
|
20
|
+
|
|
21
|
+
parameters:
|
|
22
|
+
ctx(object): behave context object used to share data between steps
|
|
23
|
+
name(str): name that identifies the desired input on screen
|
|
24
|
+
index(str): the index of the input if there are a few with the same name.
|
|
25
|
+
|
|
26
|
+
returns:
|
|
27
|
+
the WebElement that matches the provided arguments.
|
|
28
|
+
|
|
29
|
+
raises:
|
|
30
|
+
a RuntimeError if the input isn't found
|
|
31
|
+
"""
|
|
32
|
+
ctx.check_browser_initialized()
|
|
33
|
+
|
|
34
|
+
element = fuzzy.find(
|
|
35
|
+
ctx.browser,
|
|
36
|
+
name,
|
|
37
|
+
[
|
|
38
|
+
# we can only write into things that do not have the type:
|
|
39
|
+
# button, checkbox, radio, color, hidden, range, reset
|
|
40
|
+
"input[type!=button][type!=checkbox][type!=radio][button!=color][button!=hidden][button!=range][button!=reset]",
|
|
41
|
+
"textarea",
|
|
42
|
+
],
|
|
43
|
+
index=index,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
prefix = "" if index == 0 else f"{humanize.ordinal(index)} "
|
|
47
|
+
|
|
48
|
+
take_saw_element_screenshot(ctx, "input", name, index, element)
|
|
49
|
+
|
|
50
|
+
if element is None:
|
|
51
|
+
raise RuntimeError(f'unable to find the {prefix}input "{name}"')
|
|
52
|
+
|
|
53
|
+
return element
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def click_input(ctx, input_):
|
|
57
|
+
"""
|
|
58
|
+
internal method used to simply click a input element
|
|
59
|
+
"""
|
|
60
|
+
ctx.check_browser_initialized()
|
|
61
|
+
|
|
62
|
+
if base_steps.is_disabled(input_):
|
|
63
|
+
raise RuntimeError("unable to click the input, as it is disabled")
|
|
64
|
+
|
|
65
|
+
ctx.browser.click(input_)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def clear_input(input_):
|
|
69
|
+
"""
|
|
70
|
+
internal method used to clear inputs and make sure they clear correctly
|
|
71
|
+
"""
|
|
72
|
+
# Keys.CONTROL works on the Selenium grid (when running in CI)
|
|
73
|
+
# - it stopped working on laptop after the upgrade in antd version (Feb 2023)
|
|
74
|
+
# Keys.COMMAND works on laptop, but not on Selenium grid,
|
|
75
|
+
# and actually causes an active session on the grid to hang.
|
|
76
|
+
if "darwin" in sys.platform:
|
|
77
|
+
input_.send_keys(Keys.COMMAND, "a")
|
|
78
|
+
else:
|
|
79
|
+
input_.send_keys(Keys.CONTROL, "a")
|
|
80
|
+
input_.send_keys(Keys.BACKSPACE)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def find_n_write(ctx, name, value, index=0, clear_existing=True):
|
|
84
|
+
"""
|
|
85
|
+
find the input with the name provided and write the value provided into it.
|
|
86
|
+
|
|
87
|
+
parameters:
|
|
88
|
+
ctx(object): behave context object used to share data between steps
|
|
89
|
+
name(str): name that identifies the desired input on screen
|
|
90
|
+
index(str): the index of the input if there are a few with the same name.
|
|
91
|
+
clear_existing(bool): if clearing the existing content before writing
|
|
92
|
+
|
|
93
|
+
raises:
|
|
94
|
+
an error if the desired input is not found
|
|
95
|
+
"""
|
|
96
|
+
ctx.check_browser_initialized()
|
|
97
|
+
|
|
98
|
+
input_ = find_input(ctx, name, index=index)
|
|
99
|
+
|
|
100
|
+
if base_steps.is_disabled(input_):
|
|
101
|
+
raise RuntimeError("unable to write into the input, as it is disabled")
|
|
102
|
+
|
|
103
|
+
if clear_existing:
|
|
104
|
+
clear_input(input_)
|
|
105
|
+
|
|
106
|
+
if len(value) > 512:
|
|
107
|
+
#
|
|
108
|
+
# to avoid various bugs with writing large chunks of text into
|
|
109
|
+
# inputs/textareas using send_keys we can just put the text into the
|
|
110
|
+
# @value attribute and simulate a keystroke so other UI related events
|
|
111
|
+
# fire, list of some bugs:
|
|
112
|
+
#
|
|
113
|
+
# * https://github.com/seleniumhq/selenium-google-code-issue-archive/issues/4469
|
|
114
|
+
# * various stackoverflow articles on this matter
|
|
115
|
+
#
|
|
116
|
+
ctx.browser.execute(
|
|
117
|
+
"arguments[0].value = arguments[1];", input_, value
|
|
118
|
+
)
|
|
119
|
+
input_.send_keys(" ")
|
|
120
|
+
input_.send_keys(Keys.BACKSPACE)
|
|
121
|
+
|
|
122
|
+
else:
|
|
123
|
+
input_.send_keys(value)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def find_n_clear(ctx, name, index=0):
|
|
127
|
+
"""
|
|
128
|
+
find the input with the name provided and clear whatever value it has.
|
|
129
|
+
|
|
130
|
+
parameters:
|
|
131
|
+
ctx(object): behave context object used to share data between steps
|
|
132
|
+
name(str): name that identifies the desired input on screen
|
|
133
|
+
index(str): the index of the input if there are a few with the same name.
|
|
134
|
+
|
|
135
|
+
raises:
|
|
136
|
+
an error if the desired input is not found
|
|
137
|
+
"""
|
|
138
|
+
ctx.check_browser_initialized()
|
|
139
|
+
|
|
140
|
+
input_ = find_input(ctx, name, index=index)
|
|
141
|
+
|
|
142
|
+
if base_steps.is_disabled(input_):
|
|
143
|
+
raise RuntimeError("unable to clear the input, as it is disabled")
|
|
144
|
+
|
|
145
|
+
clear_input(input_)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def assert_input_value(ctx, name, value, index=0):
|
|
149
|
+
"""
|
|
150
|
+
assert the input with the name provided has the value specified, unless the
|
|
151
|
+
value is None which then means to verify there is not value set.
|
|
152
|
+
|
|
153
|
+
Params:
|
|
154
|
+
ctx(object): behave context used to share information between steps
|
|
155
|
+
name(string): name of the input to find and assert is visible
|
|
156
|
+
value(string): value to assert the input has, when set to None we verify
|
|
157
|
+
the input currently has no value.
|
|
158
|
+
index(int): index of the input to assert when there are multiple
|
|
159
|
+
inputs have the same name.
|
|
160
|
+
"""
|
|
161
|
+
input_ = find_input(ctx, name, index=index)
|
|
162
|
+
actual = input_.get_attribute("value")
|
|
163
|
+
prefix = "" if index == 0 else f"{humanize.ordinal(index)} "
|
|
164
|
+
|
|
165
|
+
if value is None:
|
|
166
|
+
if actual != "":
|
|
167
|
+
raise RuntimeError(
|
|
168
|
+
f'the {prefix}input "{name}" has value "{actual}"'
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
elif actual != value:
|
|
172
|
+
raise RuntimeError(f'the {prefix}input "{name}" has value "{actual}"')
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
helpers.define_should_see_thing_with_name_steps("input", find_input)
|
|
176
|
+
helpers.define_thing_with_name_in_state_steps(
|
|
177
|
+
"input", "disabled", find_input, base_steps.is_disabled
|
|
178
|
+
)
|
|
179
|
+
helpers.define_thing_with_name_in_state_steps(
|
|
180
|
+
"input", "not disabled", find_input, base_steps.is_not_disabled
|
|
181
|
+
)
|
|
182
|
+
helpers.define_run_steps_if_I_can_see_element_with_name_steps(
|
|
183
|
+
"input", find_input
|
|
184
|
+
)
|
|
185
|
+
helpers.define_action_on_thing_with_name_steps(
|
|
186
|
+
"input", "click", find_input, click_input, with_nth=True
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@step('I write "{value}" into the input "{name}"')
|
|
191
|
+
def writes_into_input(ctx, value, name):
|
|
192
|
+
find_n_write(ctx, name, value)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@step('I wait to write "{value}" into the input "{name}"')
|
|
196
|
+
def wait_to_write_into_input(ctx, value, name):
|
|
197
|
+
retry(find_n_write)(ctx, name, value)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@step(
|
|
201
|
+
'I wait up to "{seconds}" seconds to write "{value}" into the input "{name}"'
|
|
202
|
+
)
|
|
203
|
+
def wait_up_to_write_into_input(ctx, seconds, value, name):
|
|
204
|
+
retry(find_n_write, wait_up_to_s=float(seconds))(ctx, name, value)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@step('I write the following into the input "{name}"')
|
|
208
|
+
def writes_multi_lines_into_input(ctx, name):
|
|
209
|
+
find_n_write(ctx, name, ctx.text)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@step('I wait to write the following into the input "{name}"')
|
|
213
|
+
def wait_to_write_multi_lines_into_input(ctx, name):
|
|
214
|
+
retry(find_n_write)(ctx, name, ctx.text)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@step(
|
|
218
|
+
'I wait up to "{seconds}" to write the following into the input "{name}"'
|
|
219
|
+
)
|
|
220
|
+
def wait_up_to_write_multi_lines_into_input(ctx, seconds, name):
|
|
221
|
+
retry(find_n_write, wait_up_to_s=float(seconds))(ctx, name, ctx.text)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@step('I send the "{key}" key to the input "{name}"')
|
|
225
|
+
def send_keys_to_input(ctx, key, name):
|
|
226
|
+
find_n_write(ctx, name, Keys.__dict__[key.upper()], clear_existing=False)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@step('I clear the input "{name}"')
|
|
230
|
+
def clear_the_input(ctx, name):
|
|
231
|
+
find_n_clear(ctx, name)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@step('I wait to clear the input "{name}"')
|
|
235
|
+
def wait_to_clear_input(ctx, name):
|
|
236
|
+
retry(find_n_clear)(ctx, name)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@step('I wait up to "{seconds}" seconds to clear the input "{name}"')
|
|
240
|
+
def wait_up_to_clear_input(ctx, seconds, name):
|
|
241
|
+
retry(find_n_clear, wait_up_to_s=float(seconds))(ctx, name)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@step('I should see the input "{name}" is empty')
|
|
245
|
+
def should_see_the_input_is_empty(ctx, name):
|
|
246
|
+
assert_input_value(ctx, name, None)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@step('I should see "{value}" in the input "{name}"')
|
|
250
|
+
def should_see_the_input_with_value(ctx, value, name):
|
|
251
|
+
assert_input_value(ctx, name, value)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@step('I wait to see the value "{value}" in the input "{name}"')
|
|
255
|
+
def wait_to_see_the_input_with_value(ctx, value, name):
|
|
256
|
+
retry(assert_input_value)(ctx, name, value)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@step('I should see the following in the input "{name}"')
|
|
260
|
+
def should_see_the_following_input_with_value(ctx, name):
|
|
261
|
+
assert_input_value(ctx, name, ctx.text)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@step('I wait to see the following in the input "{name}"')
|
|
265
|
+
def wait_to_see_the_following_input_with_value(ctx, name):
|
|
266
|
+
retry(assert_input_value)(ctx, name, ctx.text)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@step('I should see no value in the input "{name}"')
|
|
270
|
+
def should_to_see_the_input_with_no_value(ctx, name):
|
|
271
|
+
assert_input_value(ctx, name, None)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@step('I write "{value}" into the "{nth:nth}" input "{name}"')
|
|
275
|
+
def write_into_the_nth_input(ctx, value, nth, name):
|
|
276
|
+
find_n_write(ctx, name, value, index=nth)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@step('I wait to write "{value}" into the "{nth:nth}" input "{name}"')
|
|
280
|
+
def wait_to_write_into_the_nth_input(ctx, value, nth, name):
|
|
281
|
+
retry(find_n_write)(ctx, name, value, index=nth)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@step('I wait to see the "{nth:nth}" input "{name}"')
|
|
285
|
+
def wait_to_see_the_nth_input(ctx, nth, name):
|
|
286
|
+
retry(find_input)(ctx, name, index=nth)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@step('I should see "{value}" in the "{nth:nth}" input "{name}"')
|
|
290
|
+
def should_see_the_nth_input_with_value(ctx, value, nth, name):
|
|
291
|
+
assert_input_value(ctx, name, value, index=nth)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@step('I wait to see the value "{value}" in the "{nth:nth}" input "{name}"')
|
|
295
|
+
def wait_to_see_the_nth_input_with_value(ctx, value, nth, name):
|
|
296
|
+
retry(assert_input_value)(ctx, name, value, index=nth)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
@step('I should see no value in the "{nth:nth}: input "{name}"')
|
|
300
|
+
def should_see_the_nth_input_with_no_value(ctx, nth, name):
|
|
301
|
+
assert_input_value(ctx, name, None, index=nth)
|
cucu/steps/link_steps.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from cucu import fuzzy, helpers
|
|
2
|
+
from cucu.utils import take_saw_element_screenshot
|
|
3
|
+
|
|
4
|
+
from . import base_steps
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def find_link(ctx, name, index=0):
|
|
8
|
+
"""
|
|
9
|
+
find a link on screen by fuzzy matching on the name provided and the target
|
|
10
|
+
element:
|
|
11
|
+
|
|
12
|
+
* <a>
|
|
13
|
+
* <* role="link">
|
|
14
|
+
|
|
15
|
+
parameters:
|
|
16
|
+
ctx(object): behave context object used to share data between steps
|
|
17
|
+
name(str): name that identifies the desired link on screen
|
|
18
|
+
index(str): the index of the link if there are duplicates
|
|
19
|
+
|
|
20
|
+
returns:
|
|
21
|
+
the WebElement that matches the provided arguments.
|
|
22
|
+
"""
|
|
23
|
+
ctx.check_browser_initialized()
|
|
24
|
+
element = fuzzy.find(
|
|
25
|
+
ctx.browser, name, ["a", '*[role="link"]'], index=index
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
take_saw_element_screenshot(ctx, "link", name, index, element)
|
|
29
|
+
|
|
30
|
+
return element
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def click_link(ctx, link):
|
|
34
|
+
"""
|
|
35
|
+
internal method to click a link
|
|
36
|
+
"""
|
|
37
|
+
ctx.check_browser_initialized()
|
|
38
|
+
|
|
39
|
+
if base_steps.is_disabled(link):
|
|
40
|
+
raise RuntimeError("unable to click the link, as it is disabled")
|
|
41
|
+
|
|
42
|
+
ctx.browser.click(link)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
helpers.define_should_see_thing_with_name_steps(
|
|
46
|
+
"link", find_link, with_nth=True
|
|
47
|
+
)
|
|
48
|
+
helpers.define_action_on_thing_with_name_steps(
|
|
49
|
+
"link", "click", find_link, click_link, with_nth=True
|
|
50
|
+
)
|
|
51
|
+
helpers.define_thing_with_name_in_state_steps(
|
|
52
|
+
"link", "disabled", find_link, base_steps.is_disabled, with_nth=True
|
|
53
|
+
)
|
|
54
|
+
helpers.define_thing_with_name_in_state_steps(
|
|
55
|
+
"link",
|
|
56
|
+
"not disabled",
|
|
57
|
+
find_link,
|
|
58
|
+
base_steps.is_not_disabled,
|
|
59
|
+
with_nth=True,
|
|
60
|
+
)
|
|
61
|
+
helpers.define_run_steps_if_I_can_see_element_with_name_steps(
|
|
62
|
+
"link", find_link
|
|
63
|
+
)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#
|
|
2
|
+
# menuitem steps
|
|
3
|
+
# https://www.w3.org/TR/wai-aria-1.1/#menuitem
|
|
4
|
+
#
|
|
5
|
+
from cucu import fuzzy, helpers
|
|
6
|
+
from cucu.utils import take_saw_element_screenshot
|
|
7
|
+
|
|
8
|
+
from . import base_steps
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def find_menuitem(ctx, name, index=0):
|
|
12
|
+
"""
|
|
13
|
+
find a menuitem containing the menuitem provide.
|
|
14
|
+
|
|
15
|
+
parameters:
|
|
16
|
+
ctx(object): behave context object used to share data between steps
|
|
17
|
+
name(str): name that identifies the desired menuitem on screen
|
|
18
|
+
index(str): the index of the menuitem if there are duplicates
|
|
19
|
+
|
|
20
|
+
returns:
|
|
21
|
+
the WebElement that matches the provided arguments.
|
|
22
|
+
"""
|
|
23
|
+
ctx.check_browser_initialized()
|
|
24
|
+
element = fuzzy.find(
|
|
25
|
+
ctx.browser, name, ['*[role="menuitem"]'], index=index
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
take_saw_element_screenshot(ctx, "menuitem", name, index, element)
|
|
29
|
+
|
|
30
|
+
return element
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
helpers.define_should_see_thing_with_name_steps("menu item", find_menuitem)
|
|
34
|
+
helpers.define_thing_with_name_in_state_steps(
|
|
35
|
+
"menu item", "disabled", find_menuitem, base_steps.is_disabled
|
|
36
|
+
)
|
|
37
|
+
helpers.define_thing_with_name_in_state_steps(
|
|
38
|
+
"menu item", "not disabled", find_menuitem, base_steps.is_not_disabled
|
|
39
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from cucu import run_steps, step
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@step("I skip this scenario if not on mac")
|
|
7
|
+
def skip_scenario_if_on_mac(ctx):
|
|
8
|
+
if "darwin" not in sys.platform:
|
|
9
|
+
ctx.scenario.skip(reason="skipping scenario since we're not on a mac")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@step("I skip this scenario if not on linux")
|
|
13
|
+
def skip_scenario_if_on_linuxc(ctx):
|
|
14
|
+
if "linux" not in sys.platform:
|
|
15
|
+
ctx.scenario.skip(
|
|
16
|
+
reason="skipping scenario since we're not on a linux"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@step("I run the following steps if on mac")
|
|
21
|
+
def run_steps_if_on_mac(ctx):
|
|
22
|
+
if "darwin" in sys.platform:
|
|
23
|
+
run_steps(ctx, ctx.text)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@step("I run the following steps if on linux")
|
|
27
|
+
def run_steps_if_on_linux(ctx):
|
|
28
|
+
if "linux" in sys.platform:
|
|
29
|
+
run_steps(ctx, ctx.text)
|