cucu 1.2.1__tar.gz → 1.2.3__tar.gz
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-1.2.1 → cucu-1.2.3}/CHANGELOG.md +10 -1
- {cucu-1.2.1 → cucu-1.2.3}/PKG-INFO +1 -1
- {cucu-1.2.1 → cucu-1.2.3}/pyproject.toml +1 -1
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/browser/core.py +13 -1
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/browser/selenium.py +46 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/cli/core.py +54 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/environment.py +6 -6
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/formatter/cucu.py +3 -3
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/formatter/json.py +1 -1
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/reporter/html.py +3 -3
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/steps/browser_steps.py +64 -2
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/steps/button_steps.py +9 -1
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/steps/checkbox_steps.py +13 -1
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/steps/radio_steps.py +13 -1
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/steps/table_steps.py +5 -5
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/utils.py +24 -11
- {cucu-1.2.1 → cucu-1.2.3}/.gitignore +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/LICENSE +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/README.md +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/__init__.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/ansi_parser.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/behave_tweaks.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/browser/__init__.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/browser/frames.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/browser/selenium_tweaks.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/cli/__init__.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/cli/run.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/cli/steps.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/cli/thread_dumper.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/config.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/edgedriver_autoinstaller/README.md +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/edgedriver_autoinstaller/__init__.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/edgedriver_autoinstaller/utils.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/external/jquery/jquery-3.5.1.min.js +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/formatter/__init__.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/formatter/junit.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/fuzzy/__init__.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/fuzzy/core.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/fuzzy/fuzzy.js +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/helpers.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/hooks.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/init_data/.gitignore +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/init_data/README.md +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/init_data/cucurc.yml +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/init_data/data/www/example.html +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/init_data/features/cucurc.yml +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/init_data/features/environment.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/init_data/features/example.feature +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/init_data/features/lint_rules/sid.yaml +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/init_data/features/steps/__init__.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/init_data/features/steps/my_steps.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/language_server/__init__.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/language_server/core.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/lint/__init__.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/lint/linter.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/lint/rules/format.yaml +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/logger.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/matcher/__init__.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/matcher/core.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/page_checks.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/reporter/__init__.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/reporter/external/bootstrap.min.css +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/reporter/external/bootstrap.min.js +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/reporter/external/dataTables.bootstrap.min.css +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/reporter/external/dataTables.bootstrap.min.js +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/reporter/external/jquery-3.5.1.min.js +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/reporter/external/jquery.dataTables.min.js +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/reporter/external/popper.min.js +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/reporter/favicon.png +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/reporter/templates/feature.html +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/reporter/templates/flat.html +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/reporter/templates/index.html +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/reporter/templates/layout.html +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/reporter/templates/scenario.html +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/steps/__init__.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/steps/base_steps.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/steps/command_steps.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/steps/draggable_steps.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/steps/dropdown_steps.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/steps/file_input_steps.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/steps/filesystem_steps.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/steps/flow_control_steps.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/steps/image_steps.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/steps/input_steps.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/steps/link_steps.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/steps/menuitem_steps.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/steps/platform_steps.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/steps/section_steps.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/steps/step_utils.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/steps/tab_steps.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/steps/tables.js +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/steps/text_steps.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/steps/variable_steps.py +0 -0
- {cucu-1.2.1 → cucu-1.2.3}/src/cucu/steps/webserver_steps.py +0 -0
|
@@ -5,8 +5,17 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project closely adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## 1.2.3
|
|
9
|
+
- Add - additional tab information steps
|
|
10
|
+
- Change - retry open a browser
|
|
11
|
+
|
|
12
|
+
## 1.2.2
|
|
13
|
+
- Chore - make lint also run ruff format --check
|
|
14
|
+
- Add - tags command to list tags and affected scenario counts
|
|
15
|
+
- Add - click parent label of an input element when the size of the input element is zero
|
|
16
|
+
|
|
8
17
|
## 1.2.1
|
|
9
|
-
- Add -
|
|
18
|
+
- Add - tab information to html report and cucu debug console log
|
|
10
19
|
|
|
11
20
|
## 1.2.0
|
|
12
21
|
- Add - levels 2-4 to section step
|
|
@@ -55,6 +55,18 @@ class Browser:
|
|
|
55
55
|
def close_window(self):
|
|
56
56
|
raise RuntimeError("implement me")
|
|
57
57
|
|
|
58
|
+
def get_tab_info(self):
|
|
59
|
+
raise RuntimeError("implement me")
|
|
60
|
+
|
|
61
|
+
def get_all_tabs_info(self):
|
|
62
|
+
raise RuntimeError("implement me")
|
|
63
|
+
|
|
64
|
+
def switch_to_nth_tab(self, tab_index):
|
|
65
|
+
raise RuntimeError("implement me")
|
|
66
|
+
|
|
67
|
+
def switch_to_tab_that_matches_regex(self, text):
|
|
68
|
+
raise RuntimeError("implement me")
|
|
69
|
+
|
|
58
70
|
def quit(self):
|
|
59
71
|
raise RuntimeError("implement me")
|
|
60
72
|
|
|
@@ -76,5 +88,5 @@ class Browser:
|
|
|
76
88
|
start = time.time()
|
|
77
89
|
hook(self)
|
|
78
90
|
logger.debug(
|
|
79
|
-
f'executed page check "{name}" in {round(time.time()-start, 3)}s'
|
|
91
|
+
f'executed page check "{name}" in {round(time.time() - start, 3)}s'
|
|
80
92
|
)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
|
+
import re
|
|
3
4
|
|
|
4
5
|
import chromedriver_autoinstaller
|
|
5
6
|
import geckodriver_autoinstaller
|
|
@@ -226,6 +227,51 @@ class Selenium(Browser):
|
|
|
226
227
|
raise RuntimeError("no previous browser tab available")
|
|
227
228
|
self.driver.switch_to.window(window_handles[window_handle_index - 1])
|
|
228
229
|
|
|
230
|
+
def switch_to_nth_tab(self, tab_number):
|
|
231
|
+
print(f"tab_number, {tab_number}")
|
|
232
|
+
window_handles = self.driver.window_handles
|
|
233
|
+
total_tabs = len(window_handles)
|
|
234
|
+
if tab_number > total_tabs:
|
|
235
|
+
raise RuntimeError(f"no {tab_number} browser tab available")
|
|
236
|
+
self.driver.switch_to.window(window_handles[tab_number])
|
|
237
|
+
|
|
238
|
+
def switch_to_tab_that_matches_regex(self, title_pattern):
|
|
239
|
+
window_handles = self.driver.window_handles
|
|
240
|
+
|
|
241
|
+
for handle in window_handles:
|
|
242
|
+
self.driver.switch_to.window(handle)
|
|
243
|
+
if re.search(title_pattern, self.driver.title):
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
raise Exception(f"No tab title matches pattern: {title_pattern}")
|
|
247
|
+
|
|
248
|
+
def get_tab_info(self):
|
|
249
|
+
window_handles = self.driver.window_handles
|
|
250
|
+
current_window = self.driver.current_window_handle
|
|
251
|
+
window_handle_index = window_handles.index(current_window)
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
"tab_count": len(window_handles),
|
|
255
|
+
"index": window_handle_index,
|
|
256
|
+
"title": self.driver.title,
|
|
257
|
+
"url": self.driver.current_url,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
def get_all_tabs_info(self):
|
|
261
|
+
tabs_info = []
|
|
262
|
+
handles = self.driver.window_handles
|
|
263
|
+
for idx, handle in enumerate(handles):
|
|
264
|
+
self.driver.switch_to.window(handle)
|
|
265
|
+
tabs_info.append(
|
|
266
|
+
{
|
|
267
|
+
"index": idx,
|
|
268
|
+
"title": self.driver.title,
|
|
269
|
+
"url": self.driver.current_url,
|
|
270
|
+
}
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
return tabs_info
|
|
274
|
+
|
|
229
275
|
def back(self):
|
|
230
276
|
self.driver.back()
|
|
231
277
|
self.wait_for_page_to_load()
|
|
@@ -7,6 +7,7 @@ import signal
|
|
|
7
7
|
import sys
|
|
8
8
|
import time
|
|
9
9
|
import xml.etree.ElementTree as ET
|
|
10
|
+
from collections import Counter
|
|
10
11
|
from importlib.metadata import version
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
from threading import Timer
|
|
@@ -14,6 +15,8 @@ from threading import Timer
|
|
|
14
15
|
import click
|
|
15
16
|
import coverage
|
|
16
17
|
import psutil
|
|
18
|
+
from behave.model_core import FileLocation
|
|
19
|
+
from behave.runner_util import parse_features
|
|
17
20
|
from click import ClickException
|
|
18
21
|
from mpire import WorkerPool
|
|
19
22
|
from tabulate import tabulate
|
|
@@ -851,5 +854,56 @@ def debug(browser, url, detach, logging_level):
|
|
|
851
854
|
time.sleep(5)
|
|
852
855
|
|
|
853
856
|
|
|
857
|
+
@main.command()
|
|
858
|
+
@click.option(
|
|
859
|
+
"-l",
|
|
860
|
+
"--logging-level",
|
|
861
|
+
default="INFO",
|
|
862
|
+
help="set logging level to one of debug, warn or info (default)",
|
|
863
|
+
)
|
|
864
|
+
@click.argument(
|
|
865
|
+
"filepath", default="features", type=click.Path(path_type=Path)
|
|
866
|
+
)
|
|
867
|
+
def tags(filepath, logging_level):
|
|
868
|
+
"""
|
|
869
|
+
print a table of tags and affected scenario counts
|
|
870
|
+
"""
|
|
871
|
+
init_global_hook_variables()
|
|
872
|
+
os.environ["CUCU_LOGGING_LEVEL"] = logging_level.upper()
|
|
873
|
+
logger.init_logging(logging_level.upper())
|
|
874
|
+
|
|
875
|
+
if filepath.is_file():
|
|
876
|
+
feature_files = [filepath]
|
|
877
|
+
else:
|
|
878
|
+
feature_files = list(filepath.rglob("*.feature"))
|
|
879
|
+
|
|
880
|
+
if not filepath.exists() or not feature_files:
|
|
881
|
+
raise ClickException("No feature files found.")
|
|
882
|
+
|
|
883
|
+
file_locations = [
|
|
884
|
+
FileLocation(os.path.abspath(str(f))) for f in feature_files
|
|
885
|
+
]
|
|
886
|
+
features = parse_features(file_locations)
|
|
887
|
+
tag_scenarios = Counter()
|
|
888
|
+
|
|
889
|
+
for feature in features:
|
|
890
|
+
for scenario in feature.scenarios:
|
|
891
|
+
affecting_tags = set(feature.tags + scenario.tags)
|
|
892
|
+
tag_scenarios.update(affecting_tags)
|
|
893
|
+
|
|
894
|
+
if not tag_scenarios:
|
|
895
|
+
print("No tags found in feature files.")
|
|
896
|
+
return
|
|
897
|
+
|
|
898
|
+
table_data = [["Tag", "Scenarios Affected"]] + [
|
|
899
|
+
[tag_name, str(count)]
|
|
900
|
+
for tag_name, count in sorted(
|
|
901
|
+
tag_scenarios.items(), key=lambda x: x[0].lower()
|
|
902
|
+
)
|
|
903
|
+
]
|
|
904
|
+
|
|
905
|
+
print(tabulate(table_data, headers="firstrow", tablefmt="fancy_grid"))
|
|
906
|
+
|
|
907
|
+
|
|
854
908
|
if __name__ == "__main__":
|
|
855
909
|
main()
|
|
@@ -10,7 +10,7 @@ from functools import partial
|
|
|
10
10
|
from cucu import config, init_scenario_hook_variables, logger
|
|
11
11
|
from cucu.config import CONFIG
|
|
12
12
|
from cucu.page_checks import init_page_checks
|
|
13
|
-
from cucu.utils import ellipsize_filename,
|
|
13
|
+
from cucu.utils import ellipsize_filename, take_screenshot
|
|
14
14
|
|
|
15
15
|
CONFIG.define(
|
|
16
16
|
"FEATURE_RESULTS_DIR",
|
|
@@ -264,11 +264,11 @@ def after_step(ctx, step):
|
|
|
264
264
|
if ctx.browser is not None and ctx.current_step.has_substeps is False:
|
|
265
265
|
take_screenshot(ctx, step.name, label=f"After {step.name}")
|
|
266
266
|
|
|
267
|
-
tab_info =
|
|
268
|
-
total_tabs = tab_info["
|
|
269
|
-
current_tab = tab_info["
|
|
270
|
-
title = tab_info["
|
|
271
|
-
url = tab_info["
|
|
267
|
+
tab_info = ctx.browser.get_tab_info()
|
|
268
|
+
total_tabs = tab_info["tab_count"]
|
|
269
|
+
current_tab = tab_info["index"] + 1
|
|
270
|
+
title = tab_info["title"]
|
|
271
|
+
url = tab_info["url"]
|
|
272
272
|
log_message = (
|
|
273
273
|
f"\ntab({current_tab} of {total_tabs}): {title}\nurl: {url}\n"
|
|
274
274
|
)
|
|
@@ -66,7 +66,7 @@ class CucuFormatter(Formatter):
|
|
|
66
66
|
# -- IMPLEMENT-INTERFACE FOR: Formatter
|
|
67
67
|
def feature(self, feature):
|
|
68
68
|
self.write_tags(feature.tags, for_feature=True)
|
|
69
|
-
text = f
|
|
69
|
+
text = f"{self.colorize(feature.keyword, 'magenta')}: {feature.name}\n"
|
|
70
70
|
self.stream.write(text)
|
|
71
71
|
|
|
72
72
|
def colorize(self, text, color):
|
|
@@ -193,7 +193,7 @@ class CucuFormatter(Formatter):
|
|
|
193
193
|
status_text_padding = (
|
|
194
194
|
max_line_length - len(current_step_text) - len(prefix)
|
|
195
195
|
)
|
|
196
|
-
status_text = f'
|
|
196
|
+
status_text = f"{' ' * status_text_padding}{status_text}"
|
|
197
197
|
status_text = self.colorize(status_text, "yellow")
|
|
198
198
|
|
|
199
199
|
self.stream.write(f"{status_text}\n")
|
|
@@ -227,7 +227,7 @@ class CucuFormatter(Formatter):
|
|
|
227
227
|
]
|
|
228
228
|
)
|
|
229
229
|
|
|
230
|
-
padding = f" {' '*(len('Given')-len(step.keyword))}"
|
|
230
|
+
padding = f" {' ' * (len('Given') - len(step.keyword))}"
|
|
231
231
|
variable_line = f"{padding}# {expanded}\n"
|
|
232
232
|
# hide secrets before we do anything to add color which could
|
|
233
233
|
# modify the output and result in not being able to correctly
|
|
@@ -176,7 +176,7 @@ class CucuJSONFormatter(Formatter):
|
|
|
176
176
|
for (key, value) in step_variables.items()
|
|
177
177
|
]
|
|
178
178
|
)
|
|
179
|
-
padding = f" {' '*(len('Given')-len(step.keyword))}"
|
|
179
|
+
padding = f" {' ' * (len('Given') - len(step.keyword))}"
|
|
180
180
|
step.stdout.insert(
|
|
181
181
|
0, f"{padding}# {CONFIG.hide_secrets(expanded)}\n"
|
|
182
182
|
)
|
|
@@ -171,7 +171,7 @@ def generate(results, basepath, only_failures=False):
|
|
|
171
171
|
sub_headers.append(handler(scenario))
|
|
172
172
|
except Exception:
|
|
173
173
|
logger.warning(
|
|
174
|
-
f
|
|
174
|
+
f'Exception while trying to run sub_headers hook for scenario: "{scenario["name"]}"\n{traceback.format_exc()}'
|
|
175
175
|
)
|
|
176
176
|
scenario["sub_headers"] = "<br/>".join(sub_headers)
|
|
177
177
|
|
|
@@ -204,7 +204,7 @@ def generate(results, basepath, only_failures=False):
|
|
|
204
204
|
# Map the count to the appropriate HTML heading (h2-h5)
|
|
205
205
|
# We use h2-h5 instead of h1-h4 so h1 can be reserved for scenario/feature titles
|
|
206
206
|
step["heading_level"] = (
|
|
207
|
-
f"h{step[
|
|
207
|
+
f"h{step['name'][:4].count('#') + 1}"
|
|
208
208
|
)
|
|
209
209
|
|
|
210
210
|
if os.path.exists(image_dirpath):
|
|
@@ -422,7 +422,7 @@ def generate(results, basepath, only_failures=False):
|
|
|
422
422
|
)
|
|
423
423
|
|
|
424
424
|
feature_output_filepath = os.path.join(
|
|
425
|
-
basepath, f
|
|
425
|
+
basepath, f"{feature['name']}.html"
|
|
426
426
|
)
|
|
427
427
|
|
|
428
428
|
with open(feature_output_filepath, "wb") as output:
|
|
@@ -32,7 +32,7 @@ def open_a_browser(ctx, url):
|
|
|
32
32
|
Given I open a browser at the url "https://www.google.com"
|
|
33
33
|
"""
|
|
34
34
|
if ctx.browser is None:
|
|
35
|
-
ctx.browser = open_browser(ctx)
|
|
35
|
+
ctx.browser = retry(open_browser)(ctx)
|
|
36
36
|
ctx.browsers.append(ctx.browser)
|
|
37
37
|
else:
|
|
38
38
|
logger.debug("browser already open so using existing instance")
|
|
@@ -43,7 +43,7 @@ def open_a_browser(ctx, url):
|
|
|
43
43
|
|
|
44
44
|
@step('I open a new browser at the url "{url}"')
|
|
45
45
|
def open_a_new_browser(ctx, url):
|
|
46
|
-
ctx.browser = open_browser(ctx)
|
|
46
|
+
ctx.browser = retry(open_browser)(ctx)
|
|
47
47
|
ctx.browsers.append(ctx.browser)
|
|
48
48
|
logger.debug(f"navigating to url #{url}")
|
|
49
49
|
ctx.browser.navigate(url)
|
|
@@ -219,6 +219,68 @@ def wait_to_switch_to_previous_browser_tab(ctx):
|
|
|
219
219
|
retry(switch_to_previous_tab)(ctx)
|
|
220
220
|
|
|
221
221
|
|
|
222
|
+
def switch_to_nth_tab(ctx, tab_number):
|
|
223
|
+
ctx.check_browser_initialized()
|
|
224
|
+
ctx.browser.switch_to_nth_tab(tab_number)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@step('I switch to the "{nth:nth}" browser tab')
|
|
228
|
+
def switch_to_nth_browser_tab(ctx, nth):
|
|
229
|
+
switch_to_nth_tab(ctx, nth)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@step('I wait to switch to the "{nth:nth}" browser tab')
|
|
233
|
+
def wait_to_switch_to_nth_browser_tab(ctx, nth):
|
|
234
|
+
retry(switch_to_nth_tab)(ctx, nth)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def switch_to_tab_matching_regex(ctx, regex):
|
|
238
|
+
ctx.check_browser_initialized()
|
|
239
|
+
ctx.browser.switch_to_tab_that_matches_regex(regex)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@step('I switch to the browser tab that matches "{regex}"')
|
|
243
|
+
def switch_to_browser_tab_matching_regex(ctx, regex):
|
|
244
|
+
switch_to_tab_matching_regex(ctx, regex)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@step('I wait to switch to the browser tab that matches "{regex}"')
|
|
248
|
+
def wait_to_switch_to_browser_tab_matching_regex(ctx, regex):
|
|
249
|
+
retry(switch_to_tab_matching_regex)(ctx, regex)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@step('I save the browser tabs info to the variable "{variable}"')
|
|
253
|
+
def save_browser_tabs_info_to_variable(ctx, variable):
|
|
254
|
+
ctx.check_browser_initialized()
|
|
255
|
+
tabs_info = ctx.browser.get_all_tabs_info()
|
|
256
|
+
lst_tab_info = [
|
|
257
|
+
f"tab({tab['index'] + 1}): {tab['title']}, url: {tab['url']}"
|
|
258
|
+
for tab in tabs_info
|
|
259
|
+
]
|
|
260
|
+
config.CONFIG[variable] = lst_tab_info
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@step("I list the current browser tab info")
|
|
264
|
+
def list_current_browser_tab_info(ctx):
|
|
265
|
+
ctx.check_browser_initialized()
|
|
266
|
+
tab_info = ctx.browser.get_tab_info()
|
|
267
|
+
current_tab = tab_info["index"] + 1
|
|
268
|
+
title = tab_info["title"]
|
|
269
|
+
url = tab_info["url"]
|
|
270
|
+
print(f"\ntab({current_tab}): {title}\nurl: {url}\n")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@step("I list all browser tabs info")
|
|
274
|
+
def list_browser_tabs_info(ctx):
|
|
275
|
+
ctx.check_browser_initialized()
|
|
276
|
+
all_tabs_info = ctx.browser.get_all_tabs_info()
|
|
277
|
+
for tab in all_tabs_info:
|
|
278
|
+
tab_index = tab["index"] + 1
|
|
279
|
+
title = tab["title"]
|
|
280
|
+
url = tab["url"]
|
|
281
|
+
print(f"\ntab({tab_index}): {title}\nurl: {url}\n")
|
|
282
|
+
|
|
283
|
+
|
|
222
284
|
def save_downloaded_file(ctx, filename):
|
|
223
285
|
ctx.check_browser_initialized()
|
|
224
286
|
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
from cucu import fuzzy, helpers
|
|
2
|
-
from cucu.utils import
|
|
2
|
+
from cucu.utils import (
|
|
3
|
+
find_n_click_input_parent_label,
|
|
4
|
+
is_element_size_zero,
|
|
5
|
+
take_saw_element_screenshot,
|
|
6
|
+
)
|
|
3
7
|
|
|
4
8
|
from . import base_steps
|
|
5
9
|
|
|
@@ -67,6 +71,10 @@ def click_button(ctx, button):
|
|
|
67
71
|
if base_steps.is_disabled(button):
|
|
68
72
|
raise RuntimeError("unable to click the button, as it is disabled")
|
|
69
73
|
|
|
74
|
+
if is_element_size_zero(button):
|
|
75
|
+
find_n_click_input_parent_label(ctx, button)
|
|
76
|
+
return
|
|
77
|
+
|
|
70
78
|
ctx.browser.click(button)
|
|
71
79
|
|
|
72
80
|
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
from cucu import fuzzy, helpers
|
|
2
|
-
from cucu.utils import
|
|
2
|
+
from cucu.utils import (
|
|
3
|
+
find_n_click_input_parent_label,
|
|
4
|
+
is_element_size_zero,
|
|
5
|
+
take_saw_element_screenshot,
|
|
6
|
+
)
|
|
3
7
|
|
|
4
8
|
from . import base_steps
|
|
5
9
|
|
|
@@ -66,6 +70,10 @@ def check_checkbox(ctx, checkbox):
|
|
|
66
70
|
if base_steps.is_disabled(checkbox):
|
|
67
71
|
raise RuntimeError("unable to check the checkbox, as it is disabled")
|
|
68
72
|
|
|
73
|
+
if is_element_size_zero(checkbox):
|
|
74
|
+
find_n_click_input_parent_label(ctx, checkbox)
|
|
75
|
+
return
|
|
76
|
+
|
|
69
77
|
ctx.browser.click(checkbox)
|
|
70
78
|
|
|
71
79
|
|
|
@@ -78,6 +86,10 @@ def uncheck_checkbox(ctx, checkbox):
|
|
|
78
86
|
if is_not_checked(checkbox):
|
|
79
87
|
raise Exception("checkbox already unchecked")
|
|
80
88
|
|
|
89
|
+
if is_element_size_zero(checkbox):
|
|
90
|
+
find_n_click_input_parent_label(ctx, checkbox)
|
|
91
|
+
return
|
|
92
|
+
|
|
81
93
|
ctx.browser.click(checkbox)
|
|
82
94
|
|
|
83
95
|
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
from cucu import fuzzy, helpers, retry, step
|
|
2
2
|
from cucu.config import CONFIG
|
|
3
|
-
from cucu.utils import
|
|
3
|
+
from cucu.utils import (
|
|
4
|
+
find_n_click_input_parent_label,
|
|
5
|
+
is_element_size_zero,
|
|
6
|
+
take_saw_element_screenshot,
|
|
7
|
+
)
|
|
4
8
|
|
|
5
9
|
from . import base_steps
|
|
6
10
|
|
|
@@ -90,6 +94,10 @@ def find_n_select_radio_button(ctx, name, index=0, ignore_if_selected=False):
|
|
|
90
94
|
|
|
91
95
|
raise Exception(f'radio button "{name}" already selected')
|
|
92
96
|
|
|
97
|
+
if is_element_size_zero(radio):
|
|
98
|
+
find_n_click_input_parent_label(ctx, radio)
|
|
99
|
+
return
|
|
100
|
+
|
|
93
101
|
ctx.browser.click(radio)
|
|
94
102
|
|
|
95
103
|
|
|
@@ -123,6 +131,10 @@ def select_radio_button(ctx, radiobox):
|
|
|
123
131
|
if selected:
|
|
124
132
|
raise Exception("radiobox already selected")
|
|
125
133
|
|
|
134
|
+
if is_element_size_zero(radiobox):
|
|
135
|
+
find_n_click_input_parent_label(ctx, radiobox)
|
|
136
|
+
return
|
|
137
|
+
|
|
126
138
|
ctx.browser.click(radiobox)
|
|
127
139
|
|
|
128
140
|
|
|
@@ -306,7 +306,7 @@ def get_table_cell_value(ctx, table, row, column, variable_name):
|
|
|
306
306
|
cell_value = tables[table][row][column]
|
|
307
307
|
except IndexError:
|
|
308
308
|
raise RuntimeError(
|
|
309
|
-
f"Cannot find table:{table+1},row:{row+1},column:{column+1}. Please check your table data."
|
|
309
|
+
f"Cannot find table:{table + 1},row:{row + 1},column:{column + 1}. Please check your table data."
|
|
310
310
|
)
|
|
311
311
|
config.CONFIG[variable_name] = cell_value
|
|
312
312
|
|
|
@@ -343,7 +343,7 @@ def find_table_element(ctx, nth=1):
|
|
|
343
343
|
return ctx.browser.css_find_elements("table")[nth]
|
|
344
344
|
except IndexError:
|
|
345
345
|
raise RuntimeError(
|
|
346
|
-
f"Cannot find table:{nth+1}. Please check your table data."
|
|
346
|
+
f"Cannot find table:{nth + 1}. Please check your table data."
|
|
347
347
|
)
|
|
348
348
|
|
|
349
349
|
|
|
@@ -364,7 +364,7 @@ def click_table_cell(ctx, row, column, table):
|
|
|
364
364
|
cell = row.find_elements(By.CSS_SELECTOR, "td")[column]
|
|
365
365
|
except IndexError:
|
|
366
366
|
raise RuntimeError(
|
|
367
|
-
f"Cannot find table:{table+1},row:{row+1},column:{column+1}. Please check your table data."
|
|
367
|
+
f"Cannot find table:{table + 1},row:{row + 1},column:{column + 1}. Please check your table data."
|
|
368
368
|
)
|
|
369
369
|
ctx.browser.click(cell)
|
|
370
370
|
|
|
@@ -409,7 +409,7 @@ def wait_click_table_cell_matching_text(ctx, column, match_text, table):
|
|
|
409
409
|
cell = row[0].find_elements(By.CSS_SELECTOR, "td")[column]
|
|
410
410
|
except IndexError:
|
|
411
411
|
raise RuntimeError(
|
|
412
|
-
f"Cannot find table:{table+1},column:{column+1},text:{match_text}. Please check your table data."
|
|
412
|
+
f"Cannot find table:{table + 1},column:{column + 1},text:{match_text}. Please check your table data."
|
|
413
413
|
)
|
|
414
414
|
|
|
415
415
|
ctx.browser.click(cell)
|
|
@@ -431,7 +431,7 @@ def wait_table_row_count(ctx, row_count, table):
|
|
|
431
431
|
return
|
|
432
432
|
else:
|
|
433
433
|
raise RuntimeError(
|
|
434
|
-
f"Unable to find {row_count} rows in table {table+1}. Please check your table data."
|
|
434
|
+
f"Unable to find {row_count} rows in table {table + 1}. Please check your table data."
|
|
435
435
|
)
|
|
436
436
|
|
|
437
437
|
retry(find_table_row_count)(ctx, row_count, table)
|
|
@@ -9,6 +9,7 @@ import pkgutil
|
|
|
9
9
|
import shutil
|
|
10
10
|
|
|
11
11
|
import humanize
|
|
12
|
+
from selenium.webdriver.common.by import By
|
|
12
13
|
from tabulate import DataRow, TableFormat, tabulate
|
|
13
14
|
from tenacity import (
|
|
14
15
|
after_log,
|
|
@@ -273,14 +274,26 @@ def take_screenshot(ctx, step_name, label="", element=None):
|
|
|
273
274
|
CONFIG["__STEP_SCREENSHOT_COUNT"] += 1
|
|
274
275
|
|
|
275
276
|
|
|
276
|
-
def
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
"
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
277
|
+
def find_n_click_input_parent_label(ctx, input_element):
|
|
278
|
+
"""
|
|
279
|
+
Clicks the nearest parent <label> of an input elemnt (if input is visually hidden or size is zero).
|
|
280
|
+
"""
|
|
281
|
+
try:
|
|
282
|
+
# Find the closest ancestor <label> element
|
|
283
|
+
label = input_element.find_element(By.XPATH, "ancestor::label[1]")
|
|
284
|
+
|
|
285
|
+
if label and label.is_displayed():
|
|
286
|
+
ctx.browser.click(label)
|
|
287
|
+
logger.debug("Successfully clicked the parent label.")
|
|
288
|
+
else:
|
|
289
|
+
logger.warning("Parent label is not displayed or not found.")
|
|
290
|
+
|
|
291
|
+
except Exception as e:
|
|
292
|
+
logger.error(
|
|
293
|
+
f"Click on parent label failed (possibly missing label ancestor): {e}"
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def is_element_size_zero(element):
|
|
298
|
+
size = element.size
|
|
299
|
+
return size["width"] == 0 and size["height"] == 0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|