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.

Files changed (83) hide show
  1. cucu/__init__.py +38 -0
  2. cucu/ansi_parser.py +58 -0
  3. cucu/behave_tweaks.py +196 -0
  4. cucu/browser/__init__.py +0 -0
  5. cucu/browser/core.py +80 -0
  6. cucu/browser/frames.py +106 -0
  7. cucu/browser/selenium.py +323 -0
  8. cucu/browser/selenium_tweaks.py +27 -0
  9. cucu/cli/__init__.py +3 -0
  10. cucu/cli/core.py +788 -0
  11. cucu/cli/run.py +207 -0
  12. cucu/cli/steps.py +137 -0
  13. cucu/cli/thread_dumper.py +55 -0
  14. cucu/config.py +440 -0
  15. cucu/edgedriver_autoinstaller/README.md +1 -0
  16. cucu/edgedriver_autoinstaller/__init__.py +37 -0
  17. cucu/edgedriver_autoinstaller/utils.py +231 -0
  18. cucu/environment.py +283 -0
  19. cucu/external/jquery/jquery-3.5.1.min.js +2 -0
  20. cucu/formatter/__init__.py +0 -0
  21. cucu/formatter/cucu.py +261 -0
  22. cucu/formatter/json.py +321 -0
  23. cucu/formatter/junit.py +289 -0
  24. cucu/fuzzy/__init__.py +3 -0
  25. cucu/fuzzy/core.py +107 -0
  26. cucu/fuzzy/fuzzy.js +253 -0
  27. cucu/helpers.py +875 -0
  28. cucu/hooks.py +205 -0
  29. cucu/language_server/__init__.py +3 -0
  30. cucu/language_server/core.py +114 -0
  31. cucu/lint/__init__.py +0 -0
  32. cucu/lint/linter.py +397 -0
  33. cucu/lint/rules/format.yaml +125 -0
  34. cucu/logger.py +113 -0
  35. cucu/matcher/__init__.py +0 -0
  36. cucu/matcher/core.py +30 -0
  37. cucu/page_checks.py +63 -0
  38. cucu/reporter/__init__.py +3 -0
  39. cucu/reporter/external/bootstrap.min.css +7 -0
  40. cucu/reporter/external/bootstrap.min.js +7 -0
  41. cucu/reporter/external/dataTables.bootstrap.min.css +1 -0
  42. cucu/reporter/external/dataTables.bootstrap.min.js +14 -0
  43. cucu/reporter/external/jquery-3.5.1.min.js +2 -0
  44. cucu/reporter/external/jquery.dataTables.min.js +192 -0
  45. cucu/reporter/external/popper.min.js +5 -0
  46. cucu/reporter/favicon.png +0 -0
  47. cucu/reporter/html.py +452 -0
  48. cucu/reporter/templates/feature.html +72 -0
  49. cucu/reporter/templates/flat.html +48 -0
  50. cucu/reporter/templates/index.html +49 -0
  51. cucu/reporter/templates/layout.html +109 -0
  52. cucu/reporter/templates/scenario.html +200 -0
  53. cucu/steps/__init__.py +27 -0
  54. cucu/steps/base_steps.py +88 -0
  55. cucu/steps/browser_steps.py +337 -0
  56. cucu/steps/button_steps.py +91 -0
  57. cucu/steps/checkbox_steps.py +111 -0
  58. cucu/steps/command_steps.py +181 -0
  59. cucu/steps/comment_steps.py +17 -0
  60. cucu/steps/draggable_steps.py +168 -0
  61. cucu/steps/dropdown_steps.py +467 -0
  62. cucu/steps/file_input_steps.py +80 -0
  63. cucu/steps/filesystem_steps.py +144 -0
  64. cucu/steps/flow_control_steps.py +198 -0
  65. cucu/steps/image_steps.py +37 -0
  66. cucu/steps/input_steps.py +301 -0
  67. cucu/steps/link_steps.py +63 -0
  68. cucu/steps/menuitem_steps.py +39 -0
  69. cucu/steps/platform_steps.py +29 -0
  70. cucu/steps/radio_steps.py +187 -0
  71. cucu/steps/step_utils.py +55 -0
  72. cucu/steps/tab_steps.py +68 -0
  73. cucu/steps/table_steps.py +437 -0
  74. cucu/steps/tables.js +28 -0
  75. cucu/steps/text_steps.py +78 -0
  76. cucu/steps/variable_steps.py +100 -0
  77. cucu/steps/webserver_steps.py +40 -0
  78. cucu/utils.py +269 -0
  79. cucu-1.0.0.dist-info/METADATA +424 -0
  80. cucu-1.0.0.dist-info/RECORD +83 -0
  81. cucu-1.0.0.dist-info/WHEEL +4 -0
  82. cucu-1.0.0.dist-info/entry_points.txt +2 -0
  83. 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 Logo](logo.png) **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
+ [![CircleCI](https://dl.circleci.com/status-badge/img/gh/dominodatalab/cucu/tree/main.svg?style=svg&circle-token=CCIPRJ_FnyZPtQ9odT5vmGW3CmZNU_bf0cfd776a09729ca4225a2860d9b59c4dae88af)](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.