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
cucu/utils.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""
|
|
2
|
+
various cucu utilities can be placed here and then exposed publicly through
|
|
3
|
+
the src/cucu/__init__.py
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import pkgutil
|
|
9
|
+
import shutil
|
|
10
|
+
|
|
11
|
+
import humanize
|
|
12
|
+
from tabulate import DataRow, TableFormat, tabulate
|
|
13
|
+
from tenacity import (
|
|
14
|
+
before_sleep_log,
|
|
15
|
+
retry_if_not_exception_type,
|
|
16
|
+
stop_after_delay,
|
|
17
|
+
wait_fixed,
|
|
18
|
+
)
|
|
19
|
+
from tenacity import retry as retrying
|
|
20
|
+
|
|
21
|
+
from cucu import logger
|
|
22
|
+
from cucu.browser.core import Browser
|
|
23
|
+
from cucu.config import CONFIG
|
|
24
|
+
|
|
25
|
+
GHERKIN_TABLEFORMAT = TableFormat(
|
|
26
|
+
lineabove=None,
|
|
27
|
+
linebelowheader=None,
|
|
28
|
+
linebetweenrows=None,
|
|
29
|
+
linebelow=None,
|
|
30
|
+
headerrow=DataRow("|", "|", "|"),
|
|
31
|
+
datarow=DataRow("|", "|", "|"),
|
|
32
|
+
padding=1,
|
|
33
|
+
with_header_hide=["lineabove"],
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class StopRetryException(Exception):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def format_gherkin_table(table, headings=[], prefix=""):
|
|
42
|
+
formatted = tabulate(table, headings, tablefmt=GHERKIN_TABLEFORMAT)
|
|
43
|
+
if prefix == "":
|
|
44
|
+
return formatted
|
|
45
|
+
|
|
46
|
+
return prefix + formatted.replace("\n", f"\n{prefix}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
#
|
|
50
|
+
# code below adapted from:
|
|
51
|
+
# https://github.com/behave/behave/blob/994dbfe30e2a372182ea613333e06f069ab97d4b/behave/runner.py#L385
|
|
52
|
+
# so we can have the sub steps printed in the console logs
|
|
53
|
+
#
|
|
54
|
+
def run_steps(ctx, steps_text):
|
|
55
|
+
"""
|
|
56
|
+
run sub steps within an existing step definition but also log their output
|
|
57
|
+
so that its easy to see what is happening.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
# -- PREPARE: Save original ctx data for current step.
|
|
61
|
+
# Needed if step definition that called this method uses .table/.text
|
|
62
|
+
original_table = getattr(ctx, "table", None)
|
|
63
|
+
original_text = getattr(ctx, "text", None)
|
|
64
|
+
|
|
65
|
+
# first time a given step calls substeps we want to move the step_index
|
|
66
|
+
# so it starts the followings steps the right point.
|
|
67
|
+
if not ctx.current_step.has_substeps:
|
|
68
|
+
ctx.step_index += 1
|
|
69
|
+
|
|
70
|
+
ctx.current_step.has_substeps = True
|
|
71
|
+
|
|
72
|
+
ctx.feature.parser.variant = "steps"
|
|
73
|
+
steps = ctx.feature.parser.parse_steps(steps_text)
|
|
74
|
+
|
|
75
|
+
current_step = ctx.current_step
|
|
76
|
+
current_step_start_time = ctx.start_time
|
|
77
|
+
|
|
78
|
+
# XXX: I want to get back to this and find a slightly better way to handle
|
|
79
|
+
# these substeps without mucking around with so much state in behave
|
|
80
|
+
# but for now this works correctly and existing tests work as expected.
|
|
81
|
+
try:
|
|
82
|
+
with ctx._use_with_behave_mode():
|
|
83
|
+
for step in steps:
|
|
84
|
+
for formatter in ctx._runner.formatters:
|
|
85
|
+
step.is_substep = True
|
|
86
|
+
formatter.insert_step(step, index=ctx.step_index)
|
|
87
|
+
|
|
88
|
+
passed = step.run(ctx._runner, quiet=False, capture=False)
|
|
89
|
+
|
|
90
|
+
if not passed:
|
|
91
|
+
if "StopRetryException" in step.error_message:
|
|
92
|
+
raise StopRetryException(step.error_message)
|
|
93
|
+
else:
|
|
94
|
+
raise RuntimeError(step.error_message)
|
|
95
|
+
|
|
96
|
+
# -- FINALLY: Restore original ctx data for current step.
|
|
97
|
+
ctx.table = original_table
|
|
98
|
+
ctx.text = original_text
|
|
99
|
+
finally:
|
|
100
|
+
ctx.current_step = current_step
|
|
101
|
+
ctx.start_time = current_step_start_time
|
|
102
|
+
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def retry(func, wait_up_to_s=None, retry_after_s=None):
|
|
107
|
+
"""
|
|
108
|
+
utility retry function that can retry the provided `func` for the maximum
|
|
109
|
+
amount of seconds specified by `wait_up_to_s` and wait the number of seconds
|
|
110
|
+
specified in `retry_after_s`
|
|
111
|
+
"""
|
|
112
|
+
if wait_up_to_s is None:
|
|
113
|
+
wait_up_to_s = float(CONFIG["CUCU_STEP_WAIT_TIMEOUT_S"])
|
|
114
|
+
|
|
115
|
+
if retry_after_s is None:
|
|
116
|
+
retry_after_s = float(CONFIG["CUCU_STEP_RETRY_AFTER_S"])
|
|
117
|
+
|
|
118
|
+
@retrying(
|
|
119
|
+
stop=stop_after_delay(wait_up_to_s),
|
|
120
|
+
wait=wait_fixed(retry_after_s),
|
|
121
|
+
retry=retry_if_not_exception_type(StopRetryException),
|
|
122
|
+
before_sleep=before_sleep_log(logger, logging.DEBUG),
|
|
123
|
+
)
|
|
124
|
+
def new_decorator(*args, **kwargs):
|
|
125
|
+
ctx = CONFIG["__CUCU_CTX"]
|
|
126
|
+
|
|
127
|
+
for hook in CONFIG["__CUCU_BEFORE_RETRY_HOOKS"]:
|
|
128
|
+
hook(ctx)
|
|
129
|
+
|
|
130
|
+
return func(*args, **kwargs)
|
|
131
|
+
|
|
132
|
+
return new_decorator
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def load_jquery_lib():
|
|
136
|
+
"""
|
|
137
|
+
load jquery library
|
|
138
|
+
"""
|
|
139
|
+
jquery_lib = pkgutil.get_data(
|
|
140
|
+
"cucu", "external/jquery/jquery-3.5.1.min.js"
|
|
141
|
+
)
|
|
142
|
+
return jquery_lib.decode("utf8")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def text_in_current_frame(browser: Browser) -> str:
|
|
146
|
+
"""
|
|
147
|
+
Utility to get all the visible text of the current frame.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
browser (Browser): the browser session switched to the desired frame
|
|
151
|
+
"""
|
|
152
|
+
script = "return window.jqCucu && jqCucu.fn.jquery;"
|
|
153
|
+
jquery_version = browser.execute(script)
|
|
154
|
+
if not jquery_version:
|
|
155
|
+
browser.execute(load_jquery_lib())
|
|
156
|
+
browser.execute("window.jqCucu = jQuery.noConflict(true);")
|
|
157
|
+
text = browser.execute(
|
|
158
|
+
'return jqCucu("body").children(":visible").text();'
|
|
159
|
+
)
|
|
160
|
+
return text
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def ellipsize_filename(raw_filename):
|
|
164
|
+
max_filename = 100
|
|
165
|
+
new_raw_filename = normalize_filename(raw_filename)
|
|
166
|
+
if len(new_raw_filename) < max_filename:
|
|
167
|
+
return new_raw_filename
|
|
168
|
+
|
|
169
|
+
ellipsis = "..."
|
|
170
|
+
# save the last chars, as the ending is often important
|
|
171
|
+
end_count = 40
|
|
172
|
+
front_count = max_filename - (len(ellipsis) + end_count)
|
|
173
|
+
ellipsized_filename = (
|
|
174
|
+
new_raw_filename[:front_count]
|
|
175
|
+
+ ellipsis
|
|
176
|
+
+ new_raw_filename[-1 * end_count :]
|
|
177
|
+
)
|
|
178
|
+
return ellipsized_filename
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def normalize_filename(raw_filename):
|
|
182
|
+
normalized_filename = (
|
|
183
|
+
raw_filename.replace('"', "")
|
|
184
|
+
.replace("{", "")
|
|
185
|
+
.replace("}", "")
|
|
186
|
+
.replace("#", "")
|
|
187
|
+
.replace("&", "")
|
|
188
|
+
)
|
|
189
|
+
return normalized_filename
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def get_step_image_dir(step_index, step_name):
|
|
193
|
+
"""
|
|
194
|
+
generate .png image file name that meets these criteria:
|
|
195
|
+
- hides secrets
|
|
196
|
+
- escaped
|
|
197
|
+
- filename does not exceed 255 chars (OS limitation)
|
|
198
|
+
- uniqueness comes from step number
|
|
199
|
+
"""
|
|
200
|
+
escaped_step_name = CONFIG.hide_secrets(step_name).replace("/", "_")
|
|
201
|
+
unabridged_dirname = f"{step_index:0>4} - {escaped_step_name}"
|
|
202
|
+
dirname = ellipsize_filename(unabridged_dirname)
|
|
203
|
+
|
|
204
|
+
return dirname
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def take_saw_element_screenshot(ctx, thing, name, index, element=None):
|
|
208
|
+
observed = "saw" if element else "did not see"
|
|
209
|
+
prefix = "" if index == 0 else f"{humanize.ordinal(index)} "
|
|
210
|
+
|
|
211
|
+
take_screenshot(
|
|
212
|
+
ctx,
|
|
213
|
+
ctx.current_step.name,
|
|
214
|
+
label=f'{observed} {prefix}{thing} "{name}"',
|
|
215
|
+
element=element,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def take_screenshot(ctx, step_name, label="", element=None):
|
|
220
|
+
screenshot_dir = os.path.join(
|
|
221
|
+
ctx.scenario_dir, get_step_image_dir(ctx.step_index, step_name)
|
|
222
|
+
)
|
|
223
|
+
if not os.path.exists(screenshot_dir):
|
|
224
|
+
os.mkdir(screenshot_dir)
|
|
225
|
+
|
|
226
|
+
if len(label) > 0:
|
|
227
|
+
label = f" - {CONFIG.hide_secrets(label).replace('/', '_')}"
|
|
228
|
+
filename = f"{CONFIG['__STEP_SCREENSHOT_COUNT']:0>4}{label}.png"
|
|
229
|
+
filename = ellipsize_filename(filename)
|
|
230
|
+
filepath = os.path.join(screenshot_dir, filename)
|
|
231
|
+
|
|
232
|
+
if CONFIG["CUCU_SKIP_HIGHLIGHT_BORDER"] or not element:
|
|
233
|
+
ctx.browser.screenshot(filepath)
|
|
234
|
+
else:
|
|
235
|
+
location = element.location
|
|
236
|
+
border_width = 4
|
|
237
|
+
x, y = location["x"] - border_width, location["y"] - border_width
|
|
238
|
+
size = element.size
|
|
239
|
+
width, height = size["width"], size["height"]
|
|
240
|
+
|
|
241
|
+
position_css = f"position: absolute; top: {y}px; left: {x}px; width: {width}px; height: {height}px; z-index: 9001;"
|
|
242
|
+
visual_css = "border-radius: 4px; border: 4px solid #ff00ff1c; background: #ff00ff05; filter: drop-shadow(magenta 0 0 10px);"
|
|
243
|
+
|
|
244
|
+
script = f"""
|
|
245
|
+
(function() {{ // double curly-brace to escape python f-string
|
|
246
|
+
var body = document.querySelector('body');
|
|
247
|
+
var cucu_border = document.createElement('div');
|
|
248
|
+
cucu_border.setAttribute('id', 'cucu_border');
|
|
249
|
+
cucu_border.setAttribute('style', '{position_css} {visual_css}');
|
|
250
|
+
body.append(cucu_border);
|
|
251
|
+
}})();
|
|
252
|
+
"""
|
|
253
|
+
ctx.browser.execute(script)
|
|
254
|
+
|
|
255
|
+
ctx.browser.screenshot(filepath)
|
|
256
|
+
|
|
257
|
+
clear_highlight = """
|
|
258
|
+
(function() {
|
|
259
|
+
var body = document.querySelector('body');
|
|
260
|
+
var cucu_border = document.getElementById('cucu_border');
|
|
261
|
+
body.removeChild(cucu_border);
|
|
262
|
+
})();
|
|
263
|
+
"""
|
|
264
|
+
ctx.browser.execute(clear_highlight, element)
|
|
265
|
+
|
|
266
|
+
if CONFIG["CUCU_MONITOR_PNG"]:
|
|
267
|
+
shutil.copyfile(filepath, CONFIG["CUCU_MONITOR_PNG"])
|
|
268
|
+
|
|
269
|
+
CONFIG["__STEP_SCREENSHOT_COUNT"] += 1
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: cucu
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Easy BDD web testing
|
|
5
|
+
Author-email: Domino Data Lab <open-source@dominodatalab.com>
|
|
6
|
+
License: The Clear BSD License
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: behave,cucumber,selenium
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
13
|
+
Classifier: Natural Language :: English
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Software Development :: Testing :: BDD
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Requires-Dist: beautifulsoup4~=4.12.2
|
|
25
|
+
Requires-Dist: behave~=1.2.6
|
|
26
|
+
Requires-Dist: chromedriver-autoinstaller~=0.6.2
|
|
27
|
+
Requires-Dist: click~=8.1.7
|
|
28
|
+
Requires-Dist: coverage[toml]~=7.4.3
|
|
29
|
+
Requires-Dist: geckodriver-autoinstaller~=0.1.0
|
|
30
|
+
Requires-Dist: humanize~=4.8.0
|
|
31
|
+
Requires-Dist: importlib-metadata~=8.0.0
|
|
32
|
+
Requires-Dist: ipdb~=0.13.13
|
|
33
|
+
Requires-Dist: jellyfish~=1.0.1
|
|
34
|
+
Requires-Dist: jinja2~=3.1.3
|
|
35
|
+
Requires-Dist: lsprotocol~=2023.0.1
|
|
36
|
+
Requires-Dist: mpire~=2.10.2
|
|
37
|
+
Requires-Dist: psutil~=6.0.0
|
|
38
|
+
Requires-Dist: pygls~=1.3.1
|
|
39
|
+
Requires-Dist: pyyaml~=6.0.1
|
|
40
|
+
Requires-Dist: requests~=2.31.0
|
|
41
|
+
Requires-Dist: selenium~=4.15
|
|
42
|
+
Requires-Dist: tabulate~=0.9.0
|
|
43
|
+
Requires-Dist: tenacity~=9.0.0
|
|
44
|
+
Description-Content-Type: text/markdown
|
|
45
|
+
|
|
46
|
+
#  **CUCU** - Easy BDD web testing
|
|
47
|
+
|
|
48
|
+
End-to-end testing framework that uses [gherkin](https://cucumber.io/docs/gherkin/)
|
|
49
|
+
to drive various underlying tools/frameworks to create real world testing scenarios.
|
|
50
|
+
|
|
51
|
+
[](https://dl.circleci.com/status-badge/redirect/gh/dominodatalab/cucu/tree/main)
|
|
52
|
+
|
|
53
|
+
## Why cucu?
|
|
54
|
+
1. Cucu avoids unnecessary abstractions (i.e. no Page Objects!) while keeping scenarios readable.
|
|
55
|
+
```gherkin
|
|
56
|
+
Feature: My First Cucu Test
|
|
57
|
+
We want to be sure the user get search results using the landing page
|
|
58
|
+
|
|
59
|
+
Scenario: User can get search results
|
|
60
|
+
Given I open a browser at the url "https://www.google.com/search"
|
|
61
|
+
When I wait to write "google" into the input "Search"
|
|
62
|
+
And I click the button "Google Search"
|
|
63
|
+
Then I wait to see the text "results"
|
|
64
|
+
```
|
|
65
|
+
2. Designed to be run **locally** and in **CI**
|
|
66
|
+
3. Runs a selenium container for you OR you can bring your own browser / container
|
|
67
|
+
4. Does fuzzy matching to approximate actions of a real user
|
|
68
|
+
5. Provides many steps out of the box
|
|
69
|
+
6. Makes it easy to create **customized** steps
|
|
70
|
+
7. Enables hierarchical configuration and env var and **CLI arg overrides**
|
|
71
|
+
8. Comes with a linter that is **customizable**
|
|
72
|
+
|
|
73
|
+
## Supporting docs
|
|
74
|
+
1. [CHANGELOG.md](CHANGELOG.md) - for latest news
|
|
75
|
+
2. [CONTRIBUTING.md](CONTRIBUTING.md) - how we develop and test the library
|
|
76
|
+
3. [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
|
|
77
|
+
4. [CONTRIBUTORS.md](CONTRIBUTORS.md)
|
|
78
|
+
5. [LICENSE](LICENSE)
|
|
79
|
+
|
|
80
|
+
# Table of Contents
|
|
81
|
+
|
|
82
|
+
- [ **CUCU** - Easy BDD web testing](#-cucu---easy-bdd-web-testing)
|
|
83
|
+
- [Why cucu?](#why-cucu)
|
|
84
|
+
- [Supporting docs](#supporting-docs)
|
|
85
|
+
- [Table of Contents](#table-of-contents)
|
|
86
|
+
- [Installation](#installation)
|
|
87
|
+
- [Requirements](#requirements)
|
|
88
|
+
- [Install Walkthrough](#install-walkthrough)
|
|
89
|
+
- [Usage](#usage)
|
|
90
|
+
- [Cucu Run](#cucu-run)
|
|
91
|
+
- [Run specific browser version with docker](#run-specific-browser-version-with-docker)
|
|
92
|
+
- [Extending Cucu](#extending-cucu)
|
|
93
|
+
- [Fuzzy matching](#fuzzy-matching)
|
|
94
|
+
- [Custom steps](#custom-steps)
|
|
95
|
+
- [Before / After hooks](#before--after-hooks)
|
|
96
|
+
- [Custom lint rules](#custom-lint-rules)
|
|
97
|
+
- [More Ways To Install Cucu](#more-ways-to-install-cucu)
|
|
98
|
+
- [Install From Build](#install-from-build)
|
|
99
|
+
|
|
100
|
+
# Installation
|
|
101
|
+
## Requirements
|
|
102
|
+
Cucu requires
|
|
103
|
+
1. python 3.9+
|
|
104
|
+
2. docker (to do UI testing)
|
|
105
|
+
|
|
106
|
+
## Install Walkthrough
|
|
107
|
+
_Get your repo setup using cucu as a test framework_
|
|
108
|
+
|
|
109
|
+
1. install and start Docker if you haven't already
|
|
110
|
+
2. install [cucu](https://pypi.org/project/cucu/)
|
|
111
|
+
```
|
|
112
|
+
pip install cucu
|
|
113
|
+
```
|
|
114
|
+
3. create the folder structure and files with content:
|
|
115
|
+
_Cucu uses the [behave framework](https://github.com/behave/behave) which expects the `features/steps` directories_
|
|
116
|
+
- features/
|
|
117
|
+
- steps/
|
|
118
|
+
- `__init__.py` # enables cucu and custom steps
|
|
119
|
+
```python
|
|
120
|
+
# import all of the steps from cucu
|
|
121
|
+
from cucu.steps import * # noqa: F403, F401
|
|
122
|
+
|
|
123
|
+
# import individual sub-modules here (i.e. module names of your custom step py files)
|
|
124
|
+
# Example: For file features/steps/ui/login.py
|
|
125
|
+
# import steps.ui.login_steps
|
|
126
|
+
```
|
|
127
|
+
- environment.py - enables before/after hooks
|
|
128
|
+
```python
|
|
129
|
+
# flake8: noqa
|
|
130
|
+
from cucu.environment import *
|
|
131
|
+
|
|
132
|
+
# Define custom before/after hooks here
|
|
133
|
+
```
|
|
134
|
+
4. list available cucu steps
|
|
135
|
+
```bash
|
|
136
|
+
cucu steps
|
|
137
|
+
```
|
|
138
|
+
- if you have `brew install fzf` then you can fuzzy find steps
|
|
139
|
+
```bash
|
|
140
|
+
cucu steps | fzf
|
|
141
|
+
# start typing for search
|
|
142
|
+
```
|
|
143
|
+
5. **create your first cucu test**
|
|
144
|
+
- features/my_first_test.feature
|
|
145
|
+
```gherkin
|
|
146
|
+
Feature: My First Cucu Test
|
|
147
|
+
We want to be sure the user get search results using the landing page
|
|
148
|
+
|
|
149
|
+
Scenario: User can get search results
|
|
150
|
+
Given I open a browser at the url "https://www.google.com/search"
|
|
151
|
+
When I wait to write "google" into the input "Search"
|
|
152
|
+
And I click the button "Google Search"
|
|
153
|
+
Then I wait to see the text "results"
|
|
154
|
+
```
|
|
155
|
+
6. **run it**
|
|
156
|
+
```bash
|
|
157
|
+
cucu run features/my_first_test.feature
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
# Usage
|
|
161
|
+
## Cucu Run
|
|
162
|
+
The command `cucu run` is used to run a given test or set of tests and in its
|
|
163
|
+
simplest form you can use it like so:
|
|
164
|
+
```bash
|
|
165
|
+
cucu run features/my_first_test.feature
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
That would simply run the "google search for the word google" and once it's
|
|
169
|
+
finished executing you can use the `cucu report` command to generate an easy
|
|
170
|
+
to navigate and read HTML test report which includes the steps and screenshots
|
|
171
|
+
from that previous test run.
|
|
172
|
+
|
|
173
|
+
*NOTE:*
|
|
174
|
+
By default we'll simply use the `Google Chrome` you have installed and there's
|
|
175
|
+
a python package that'll handle downloading chromedriver that matches your
|
|
176
|
+
specific local Google Chrome version.
|
|
177
|
+
|
|
178
|
+
## Run specific browser version with docker
|
|
179
|
+
|
|
180
|
+
[docker hub](https://hub.docker.com/) has easy to use docker containers for
|
|
181
|
+
running specific versions of chrome, edge and firefox browsers for testing that
|
|
182
|
+
you can spin up manually in standalone mode like so:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
docker run -d -p 4444:4444 selenium/standalone-chrome:latest
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
If you are using ARM64 CPU architecture (Mac M1 or M2), you must use seleniarm
|
|
189
|
+
container.
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
docker run -d -p 4444:4444 seleniarm/standalone-chromium:latest
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
And can choose a specific version replacing the `latest` with any tag from
|
|
196
|
+
[here](https://hub.docker.com/r/selenium/standalone-chrome/tags). You can find
|
|
197
|
+
browser tags for `standalone-edge` and `standalone-firefox` the same way. Once
|
|
198
|
+
you run the command you will see with `docker ps -a` that the container
|
|
199
|
+
is running and listening on port `4444`:
|
|
200
|
+
|
|
201
|
+
Specific tags for seleniarm:
|
|
202
|
+
[here](https://hub.docker.com/r/seleniarm/standalone-chromium/tags)
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
> docker ps -a
|
|
206
|
+
CONTAINER ID ... PORTS NAMES
|
|
207
|
+
7c719f4bee29 ... 0.0.0.0:4444->4444/tcp, :::4444->4444/tcp, 5900/tcp wizardly_haslett
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
*NOTE:* For seleniarm containers, the available browsers are chromium and firefox.
|
|
211
|
+
The reason for this is because Google and Microsoft have not released binaries
|
|
212
|
+
for their respective browsers (Chrome and Edge).
|
|
213
|
+
|
|
214
|
+
Now when running `cucu run some.feature` you can provide
|
|
215
|
+
`--selenium-remote-url http://localhost:4444` and this way you'll run a very
|
|
216
|
+
specific version of chrome on any setup you run this on.
|
|
217
|
+
|
|
218
|
+
You can also create a docker hub setup with all 3 browser nodes connected using
|
|
219
|
+
the utilty script at `./bin/start_selenium_hub.sh` and you can point your tests
|
|
220
|
+
at `http://localhost:4444` and then specify the `--browser` to be `chrome`,
|
|
221
|
+
`firefox` or `edge` and use that specific browser for testing.
|
|
222
|
+
|
|
223
|
+
The docker hub setup for seleniarm: `./bin/start_seleniarm_hub.sh`
|
|
224
|
+
*NOTE:* `edge` cannot be selected as a specific browser for testing
|
|
225
|
+
|
|
226
|
+
To ease using various custom settings you can also set most of the command line
|
|
227
|
+
options in a local `cucurc.yml` or in a more global place at `~/.cucurc.yml`
|
|
228
|
+
the same settings. For the remote url above you'd simply have the following
|
|
229
|
+
in your `cucurc.yml`:
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
CUCU_SELENIUM_REMOTE_URL: http://localhost:4444
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Then you can simply run `cucu run path/to/some.feature` and `cucu` would load
|
|
236
|
+
the local `cucurc.yml` or `~/.cucurc.yml` settings and use those.
|
|
237
|
+
|
|
238
|
+
# Extending Cucu
|
|
239
|
+
|
|
240
|
+
## Fuzzy matching
|
|
241
|
+
|
|
242
|
+
`cucu` uses selenium to interact with the browser but on top of that we've
|
|
243
|
+
developed a fuzzy matching set of rules that allow the framework to find
|
|
244
|
+
elements on the page by having a label and a type of element we're searching for.
|
|
245
|
+
|
|
246
|
+
The principal is simple you want to `click the button "Foo"` so we know you want
|
|
247
|
+
to find a button which can be one of a few different kind of HTML elements:
|
|
248
|
+
|
|
249
|
+
* `<a>`
|
|
250
|
+
* `<button>`
|
|
251
|
+
* `<input type="button">`
|
|
252
|
+
* `<* role="button">`
|
|
253
|
+
* etc
|
|
254
|
+
|
|
255
|
+
We also know that it has the name you provided labeling it and that can be
|
|
256
|
+
done using any of the following rules:
|
|
257
|
+
|
|
258
|
+
* `<thing>name</thing>`
|
|
259
|
+
* `<*>name</*><thing></thing>`
|
|
260
|
+
* `<thing attribute="name"></thing>`
|
|
261
|
+
* `<*>name</*>...<thing>...`
|
|
262
|
+
|
|
263
|
+
Where `thing` is any of the previously identified element types. With the above
|
|
264
|
+
rules we created a simple method method that uses the those rules to find a set
|
|
265
|
+
of elements labeled with the name you provide and type of elements you're
|
|
266
|
+
looking for. We currently use [swizzle](https://github.com/jquery/sizzle) as
|
|
267
|
+
the underlying element query language as its highly portable and has a bit
|
|
268
|
+
useful features than basic CSS gives us.
|
|
269
|
+
|
|
270
|
+
## Custom steps
|
|
271
|
+
It's easy to create custom steps, for example:
|
|
272
|
+
1. create a new python file in your repo `features/steps/ui/weird_button_steps.py`
|
|
273
|
+
```python
|
|
274
|
+
from cucu import fuzzy, retry, step
|
|
275
|
+
|
|
276
|
+
# make this step available for scenarios and listed in `cucu steps`
|
|
277
|
+
@step('I open the wierd menu item "{menu_item}"')
|
|
278
|
+
def open_jupyter_menu(ctx, menu_item):
|
|
279
|
+
# using fuzzy.find
|
|
280
|
+
dropdown_item = fuzzy.find(ctx.browser, menu_item, ["li a"])
|
|
281
|
+
dropdown_item.click()
|
|
282
|
+
|
|
283
|
+
# example using retry
|
|
284
|
+
def click_that_weird_button(ctx):
|
|
285
|
+
# using selenium's css_find_elements
|
|
286
|
+
ctx.browser.css_find_elements("button[custom_thing='painful-id']")[0].click()
|
|
287
|
+
|
|
288
|
+
@step("I wait to click this button that isn't aria compliant on my page")
|
|
289
|
+
def wait_to_click_that_weird_button(ctx):
|
|
290
|
+
# makes this retry with the default wait timeout
|
|
291
|
+
retry(click_that_weird_button)(ctx) # remember to call the returned function `(ctx)` at the end
|
|
292
|
+
```
|
|
293
|
+
2. then update the magic `features/steps/__init__.py` file (this one file only!)
|
|
294
|
+
|
|
295
|
+
_Yeah I know that this is kind of odd, but work with me here😅_
|
|
296
|
+
```python
|
|
297
|
+
# import all of the steps from cucu
|
|
298
|
+
from cucu.steps import * # noqa: F403, F401
|
|
299
|
+
|
|
300
|
+
# import individual sub-modules here (i.e. module names of your custom step py files)
|
|
301
|
+
# Example: For file features/steps/ui/login.py
|
|
302
|
+
# import steps.ui.login_steps
|
|
303
|
+
import steps.ui.weird_button_steps
|
|
304
|
+
```
|
|
305
|
+
3. profit!
|
|
306
|
+
|
|
307
|
+
## Before / After hooks
|
|
308
|
+
|
|
309
|
+
There are several hooks you can access, here's a few:
|
|
310
|
+
```python
|
|
311
|
+
register_before_retry_hook,
|
|
312
|
+
register_before_scenario_hook,
|
|
313
|
+
register_custom_junit_failure_handler,
|
|
314
|
+
register_custom_tags_in_report_handling,
|
|
315
|
+
register_custom_scenario_subheader_in_report_handling,
|
|
316
|
+
register_custom_variable_handling,
|
|
317
|
+
register_page_check_hook,
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
And here's an example:
|
|
321
|
+
1. add your function def to `features/environment.py`
|
|
322
|
+
```python
|
|
323
|
+
import logging
|
|
324
|
+
|
|
325
|
+
from cucu import (
|
|
326
|
+
fuzzy,
|
|
327
|
+
logger,
|
|
328
|
+
register_page_check_hook,
|
|
329
|
+
retry,
|
|
330
|
+
)
|
|
331
|
+
from cucu.config import CONFIG
|
|
332
|
+
from cucu.environment import *
|
|
333
|
+
|
|
334
|
+
def print_elements(elements):
|
|
335
|
+
"""
|
|
336
|
+
given a list of selenium web elements we print their outerHTML
|
|
337
|
+
representation to the logs
|
|
338
|
+
"""
|
|
339
|
+
for element in elements:
|
|
340
|
+
logger.debug(f"found element: {element.get_attribute('outerHTML')}")
|
|
341
|
+
|
|
342
|
+
def wait_for_my_loading_indicators(browser):
|
|
343
|
+
# aria-label="loading"
|
|
344
|
+
def should_not_see_aria_label_equals_loading():
|
|
345
|
+
# ignore the checks on the my-page page as there are these silly
|
|
346
|
+
# spinners that have aria-label=loading and probably shouldn't
|
|
347
|
+
if "my-page" not in browser.get_current_url():
|
|
348
|
+
elements = browser.css_find_elements("[aria-label='loading'")
|
|
349
|
+
if elements:
|
|
350
|
+
print_elements(elements)
|
|
351
|
+
raise RuntimeError("aria-label='loading', see above for details")
|
|
352
|
+
|
|
353
|
+
retry(should_not_see_aria_label_equals_loading)()
|
|
354
|
+
|
|
355
|
+
# my-attr contains "loading"
|
|
356
|
+
def should_not_see_data_test_contains_loading():
|
|
357
|
+
elements = browser.css_find_elements("[my-attr*='loading'")
|
|
358
|
+
if elements:
|
|
359
|
+
print_elements(elements)
|
|
360
|
+
raise RuntimeError("my-attr*='loading', see above for details")
|
|
361
|
+
|
|
362
|
+
retry(should_not_see_data_test_contains_loading)()
|
|
363
|
+
|
|
364
|
+
# class contains "my-spinner"
|
|
365
|
+
def should_not_see_class_contains_my_spinner():
|
|
366
|
+
elements = browser.css_find_elements("[class*='my-spinner'")
|
|
367
|
+
if elements:
|
|
368
|
+
print_elements(elements)
|
|
369
|
+
raise RuntimeError("class*='my-spinner', see above for details")
|
|
370
|
+
|
|
371
|
+
retry(should_not_see_class_contains_my_spinner)()
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
register_page_check_hook("my loading indicators", wait_for_my_loading_indicators)
|
|
375
|
+
```
|
|
376
|
+
2. done!
|
|
377
|
+
|
|
378
|
+
## Custom lint rules
|
|
379
|
+
|
|
380
|
+
You can easily extend the `cucu lint` linting rules by setting the variable
|
|
381
|
+
`CUCU_LINT_RULES_PATH` and pointing it to a directory in your features source
|
|
382
|
+
that has `.yaml` files that are structured like so:
|
|
383
|
+
|
|
384
|
+
```yaml
|
|
385
|
+
[unique_rule_identifier]:
|
|
386
|
+
message: [the message to provide the end user explaining the violation]
|
|
387
|
+
type: [warning|error] # I or W will be printed when reporting the violation
|
|
388
|
+
current_line:
|
|
389
|
+
match: [regex]
|
|
390
|
+
previous_line:
|
|
391
|
+
match: [regex]
|
|
392
|
+
next_line:
|
|
393
|
+
match: [regex]
|
|
394
|
+
fix:
|
|
395
|
+
match: [regex]
|
|
396
|
+
replace: [regex]
|
|
397
|
+
-- or --
|
|
398
|
+
delete: true
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
The `current_line`, `previous_line` and `next_line` sections are used to match
|
|
402
|
+
on a specific set of lines so that you can then "fix" the current line a way
|
|
403
|
+
specified by the `fix` block. When there is no `fix` block provided then
|
|
404
|
+
`cucu lint` will notify the end user it can not fix the violation.
|
|
405
|
+
|
|
406
|
+
In the `fix` section one can choose to do `match` and `replace` or to simply
|
|
407
|
+
`delete` the violating line.
|
|
408
|
+
|
|
409
|
+
# More Ways To Install Cucu
|
|
410
|
+
|
|
411
|
+
## Install From Build
|
|
412
|
+
|
|
413
|
+
Within the cucu directory you can run `uv build` and that will produce some
|
|
414
|
+
output like so:
|
|
415
|
+
|
|
416
|
+
```bash
|
|
417
|
+
Building source distribution...
|
|
418
|
+
Building wheel from source distribution...
|
|
419
|
+
Successfully built dist/cucu-0.207.0.tar.gz and dist/cucu-0.207.0-py3-none-any.whl
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
At this point you can install the file `dist/cucu-0.1.0.tar.gz` using
|
|
423
|
+
`pip install .../cucu/dist/cucu-*.tar.gz` anywhere you'd like and have the `cucu` tool ready to
|
|
424
|
+
run.
|