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/formatter/junit.py
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import os
|
|
3
|
+
import traceback
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from xml.sax.saxutils import escape
|
|
6
|
+
|
|
7
|
+
import bs4
|
|
8
|
+
from behave.formatter.base import Formatter
|
|
9
|
+
from behave.model_core import Status
|
|
10
|
+
from bs4.formatter import XMLFormatter
|
|
11
|
+
from tenacity import RetryError
|
|
12
|
+
|
|
13
|
+
from cucu.config import CONFIG
|
|
14
|
+
from cucu.utils import ellipsize_filename
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CucuJUnitFormatter(Formatter):
|
|
18
|
+
name = "cucu-junit"
|
|
19
|
+
description = "JUnit Formater"
|
|
20
|
+
|
|
21
|
+
def __init__(self, stream_opener, config):
|
|
22
|
+
super(CucuJUnitFormatter, self).__init__(stream_opener, config)
|
|
23
|
+
self.xml_root = None
|
|
24
|
+
self.current_feature = None
|
|
25
|
+
self.current_scenario = None
|
|
26
|
+
self.curent_step = None
|
|
27
|
+
self.current_scenario_traceback = None
|
|
28
|
+
self.current_scenario_duration = 0.0
|
|
29
|
+
self.current_scenario_results = {}
|
|
30
|
+
self.feature_timestamp = None
|
|
31
|
+
self.steps = []
|
|
32
|
+
|
|
33
|
+
# -- FORMATTER API:
|
|
34
|
+
def uri(self, uri):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
def feature(self, feature):
|
|
38
|
+
self.current_feature = feature
|
|
39
|
+
date_now = datetime.now()
|
|
40
|
+
self.feature_results = {
|
|
41
|
+
"name": escape(feature.name),
|
|
42
|
+
"foldername": escape(ellipsize_filename(feature.name)),
|
|
43
|
+
"tests": 0,
|
|
44
|
+
"errors": 0,
|
|
45
|
+
"failures": 0,
|
|
46
|
+
"skipped": 0,
|
|
47
|
+
"timestamp": date_now.strftime("%Y-%m-%dT%H:%M:%S.%f%z"),
|
|
48
|
+
"scenarios": {},
|
|
49
|
+
}
|
|
50
|
+
if feature.tags:
|
|
51
|
+
self.feature_results["tags"] = ", ".join(feature.tags)
|
|
52
|
+
|
|
53
|
+
def background(self, background):
|
|
54
|
+
# -- ADD BACKGROUND STEPS: Support *.feature file regeneration.
|
|
55
|
+
for step_ in background.steps:
|
|
56
|
+
self.step(step_)
|
|
57
|
+
|
|
58
|
+
def update_scenario(self):
|
|
59
|
+
if self.current_scenario is not None:
|
|
60
|
+
self.current_scenario_results["time"] = str(
|
|
61
|
+
round(self.current_scenario_duration, 3)
|
|
62
|
+
)
|
|
63
|
+
hook_failed = self.current_scenario.hook_failed
|
|
64
|
+
if hook_failed:
|
|
65
|
+
status = "errored"
|
|
66
|
+
else:
|
|
67
|
+
status = self.current_scenario.compute_status().name
|
|
68
|
+
|
|
69
|
+
self.current_scenario_results["status"] = status
|
|
70
|
+
failures = []
|
|
71
|
+
|
|
72
|
+
if status == "failed" and self.current_scenario_traceback:
|
|
73
|
+
failure_handlers = CONFIG["__CUCU_CUSTOM_FAILURE_HANDLERS"]
|
|
74
|
+
|
|
75
|
+
for failure_handler in failure_handlers:
|
|
76
|
+
failures.append(
|
|
77
|
+
failure_handler(
|
|
78
|
+
self.current_feature, self.current_scenario
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
failures += [
|
|
83
|
+
f"{self.current_step.keyword} {self.current_step.name} (after {round(self.current_step.duration, 3)}s)"
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
if error := self.current_step.exception:
|
|
87
|
+
if isinstance(error, RetryError):
|
|
88
|
+
error = error.last_attempt.exception()
|
|
89
|
+
|
|
90
|
+
if len(error.args) > 0 and isinstance(error.args[0], str):
|
|
91
|
+
error_class_name = error.__class__.__name__
|
|
92
|
+
error_lines = error.args[0].splitlines()
|
|
93
|
+
error_lines[0] = (
|
|
94
|
+
f"{error_class_name}: {error_lines[0]}"
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
error_lines = [repr(error)]
|
|
98
|
+
failures += error_lines
|
|
99
|
+
|
|
100
|
+
if CONFIG["CUCU_JUNIT_WITH_STACKTRACE"] == "true":
|
|
101
|
+
failures += traceback.format_tb(
|
|
102
|
+
self.current_scenario_traceback
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
self.current_scenario_results["failure"] = failures
|
|
106
|
+
|
|
107
|
+
if status == "skipped":
|
|
108
|
+
self.current_scenario_results["skipped"] = True
|
|
109
|
+
|
|
110
|
+
def scenario(self, scenario):
|
|
111
|
+
self.update_scenario()
|
|
112
|
+
|
|
113
|
+
self.current_scenario = scenario
|
|
114
|
+
self.current_scenario_traceback = None
|
|
115
|
+
self.current_scenario_duration = 0.0
|
|
116
|
+
self.current_scenario_results = {
|
|
117
|
+
"status": "pending",
|
|
118
|
+
"time": "n/a",
|
|
119
|
+
"failure": None,
|
|
120
|
+
"skipped": None,
|
|
121
|
+
}
|
|
122
|
+
self.current_scenario_results["foldername"] = escape(
|
|
123
|
+
ellipsize_filename(scenario.name)
|
|
124
|
+
)
|
|
125
|
+
if scenario.tags:
|
|
126
|
+
self.current_scenario_results["tags"] = ", ".join(scenario.tags)
|
|
127
|
+
|
|
128
|
+
scenario_name = escape(scenario.name)
|
|
129
|
+
self.feature_results["scenarios"][scenario_name] = (
|
|
130
|
+
self.current_scenario_results
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# we write out every new scenario into the JUnit results output
|
|
134
|
+
# which allows us to have a valid JUnit XML results file per feature
|
|
135
|
+
# file currently running even if the process crashes or is killed so we
|
|
136
|
+
# can still generate valid reports from every run.
|
|
137
|
+
self.write_results(self.feature_results)
|
|
138
|
+
|
|
139
|
+
def match(self, step):
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
def step(self, step):
|
|
143
|
+
self.steps.append(step)
|
|
144
|
+
|
|
145
|
+
def insert_step(self, step, index=-1):
|
|
146
|
+
if index == -1:
|
|
147
|
+
self.steps.append(step)
|
|
148
|
+
else:
|
|
149
|
+
self.steps.insert(index, step)
|
|
150
|
+
|
|
151
|
+
def result(self, step):
|
|
152
|
+
self.current_step = step
|
|
153
|
+
if step.status == Status.failed:
|
|
154
|
+
self.current_scenario_traceback = step.exc_traceback
|
|
155
|
+
|
|
156
|
+
self.current_scenario_duration += step.duration
|
|
157
|
+
|
|
158
|
+
def eof(self):
|
|
159
|
+
"""
|
|
160
|
+
end of file for the feature and this is when we write the updated
|
|
161
|
+
results to the file
|
|
162
|
+
"""
|
|
163
|
+
self.update_scenario()
|
|
164
|
+
self.write_results(self.feature_results)
|
|
165
|
+
|
|
166
|
+
def close(self):
|
|
167
|
+
pass
|
|
168
|
+
|
|
169
|
+
def write_results(self, results):
|
|
170
|
+
"""
|
|
171
|
+
given a feature results dictionary that looks like so:
|
|
172
|
+
{
|
|
173
|
+
"name": "name of the feature",
|
|
174
|
+
"foldername": "",
|
|
175
|
+
"tests": 0,
|
|
176
|
+
"errors": 0,
|
|
177
|
+
"failures": 0,
|
|
178
|
+
"skipped": 0,
|
|
179
|
+
"timestamp": "",
|
|
180
|
+
"scenarios": {
|
|
181
|
+
"scenario name": {
|
|
182
|
+
"foldername": "",
|
|
183
|
+
"tags": "DOM-3435, testrail(3366,45891)",
|
|
184
|
+
"status": "passed/failed/skipped",
|
|
185
|
+
"time": "0.0000":
|
|
186
|
+
"stdout": "",
|
|
187
|
+
"stderr": "",
|
|
188
|
+
},
|
|
189
|
+
...
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
# custom attribute ordering so attributes are printed in a consistent
|
|
195
|
+
# and desired order
|
|
196
|
+
class SortAttributes(XMLFormatter):
|
|
197
|
+
def attributes(self, tag):
|
|
198
|
+
ordered = [
|
|
199
|
+
"classname",
|
|
200
|
+
"name",
|
|
201
|
+
"foldername",
|
|
202
|
+
"tests",
|
|
203
|
+
"errors",
|
|
204
|
+
"failures",
|
|
205
|
+
"skipped",
|
|
206
|
+
"status",
|
|
207
|
+
"timestamp",
|
|
208
|
+
"time",
|
|
209
|
+
"tags",
|
|
210
|
+
]
|
|
211
|
+
|
|
212
|
+
return [
|
|
213
|
+
(attr, tag[attr]) for attr in ordered if attr in tag.attrs
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
soup = bs4.BeautifulSoup()
|
|
217
|
+
testsuite = bs4.Tag(name="testsuite")
|
|
218
|
+
testsuite["name"] = results["name"]
|
|
219
|
+
testsuite["foldername"] = results["foldername"]
|
|
220
|
+
testsuite["timestamp"] = results["timestamp"]
|
|
221
|
+
|
|
222
|
+
junit_dir = CONFIG["CUCU_JUNIT_DIR"]
|
|
223
|
+
os.makedirs(junit_dir, exist_ok=True)
|
|
224
|
+
|
|
225
|
+
feature_name = results["name"]
|
|
226
|
+
output_filepath = os.path.join(junit_dir, f"{feature_name}.xml")
|
|
227
|
+
|
|
228
|
+
scenarios = results["scenarios"]
|
|
229
|
+
|
|
230
|
+
if CONFIG["CUCU_SHOW_SKIPS"] != "true":
|
|
231
|
+
filtered_scenarios = {}
|
|
232
|
+
|
|
233
|
+
for name, scenario in scenarios.items():
|
|
234
|
+
if scenario["status"] != "skipped":
|
|
235
|
+
filtered_scenarios[name] = scenario
|
|
236
|
+
|
|
237
|
+
scenarios = filtered_scenarios
|
|
238
|
+
|
|
239
|
+
if len(scenarios) == 0:
|
|
240
|
+
# we had a suite of just skipped results
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
# calculate with the latest data
|
|
244
|
+
testsuite["tests"] = len(scenarios)
|
|
245
|
+
testsuite["failures"] = len(
|
|
246
|
+
[x for x in scenarios.values() if x["status"] == Status.failed]
|
|
247
|
+
)
|
|
248
|
+
testsuite["skipped"] = len(
|
|
249
|
+
[x for x in scenarios.values() if x["status"] == Status.skipped]
|
|
250
|
+
)
|
|
251
|
+
testsuite["errors"] = len(
|
|
252
|
+
[
|
|
253
|
+
x
|
|
254
|
+
for x in scenarios.values()
|
|
255
|
+
if x["status"]
|
|
256
|
+
not in (Status.failed, Status.skipped, Status.passed)
|
|
257
|
+
]
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
if "tags" in results:
|
|
261
|
+
testsuite["tags"] = results["tags"]
|
|
262
|
+
soup.append(testsuite)
|
|
263
|
+
|
|
264
|
+
for scenario_name in scenarios:
|
|
265
|
+
scenario = scenarios[scenario_name]
|
|
266
|
+
testcase = bs4.Tag(name="testcase")
|
|
267
|
+
testcase["classname"] = results["name"]
|
|
268
|
+
testcase["name"] = scenario_name
|
|
269
|
+
testcase["foldername"] = scenario["foldername"]
|
|
270
|
+
if "tags" in scenario:
|
|
271
|
+
testcase["tags"] = scenario["tags"]
|
|
272
|
+
testcase["status"] = scenario["status"]
|
|
273
|
+
testcase["time"] = scenario["time"]
|
|
274
|
+
|
|
275
|
+
if scenario["failure"] is not None:
|
|
276
|
+
failure_message = "\n".join(scenario["failure"])
|
|
277
|
+
failure = bs4.Tag(name="failure")
|
|
278
|
+
failure.append(bs4.CData(failure_message))
|
|
279
|
+
testcase.append(failure)
|
|
280
|
+
|
|
281
|
+
if scenario["skipped"] is not None:
|
|
282
|
+
testcase.append(bs4.Tag(name="skipped"))
|
|
283
|
+
|
|
284
|
+
testsuite.append(testcase)
|
|
285
|
+
|
|
286
|
+
feature_name = results["name"]
|
|
287
|
+
output_filepath = os.path.join(junit_dir, f"{feature_name}.xml")
|
|
288
|
+
with open(output_filepath, "w", encoding="utf-8") as output:
|
|
289
|
+
output.write(soup.prettify(formatter=SortAttributes()))
|
cucu/fuzzy/__init__.py
ADDED
cucu/fuzzy/core.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import pkgutil
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
from cucu import logger
|
|
5
|
+
from cucu.browser.frames import search_in_all_frames
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def load_jquery_lib():
|
|
9
|
+
"""
|
|
10
|
+
load jquery library
|
|
11
|
+
"""
|
|
12
|
+
jquery_lib = pkgutil.get_data(
|
|
13
|
+
"cucu", "external/jquery/jquery-3.5.1.min.js"
|
|
14
|
+
)
|
|
15
|
+
return jquery_lib.decode("utf8")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_fuzzy_lib():
|
|
19
|
+
"""
|
|
20
|
+
load the fuzzy javascript library
|
|
21
|
+
"""
|
|
22
|
+
return pkgutil.get_data("cucu", "fuzzy/fuzzy.js").decode("utf8")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def init(browser):
|
|
26
|
+
"""
|
|
27
|
+
initializes the fuzzy matching javascript library within the currently open
|
|
28
|
+
browsers execution engine
|
|
29
|
+
|
|
30
|
+
parameters:
|
|
31
|
+
browser - ...
|
|
32
|
+
"""
|
|
33
|
+
script = "return typeof cucu !== 'undefined' && typeof cucu.fuzzy_find === 'function';"
|
|
34
|
+
cucu_injected = browser.execute(script)
|
|
35
|
+
if cucu_injected:
|
|
36
|
+
# cucu fuzzy find already exists
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
logger.debug("inject cucu fuzzy find library to the browser")
|
|
40
|
+
jqCucu_script = "window.jqCucu = jQuery.noConflict(true);"
|
|
41
|
+
browser.execute(load_jquery_lib() + jqCucu_script + load_fuzzy_lib())
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Direction(Enum):
|
|
45
|
+
"""
|
|
46
|
+
simple Direction enum
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
LEFT_TO_RIGHT = 1
|
|
50
|
+
RIGHT_TO_LEFT = 2
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def find(
|
|
54
|
+
browser,
|
|
55
|
+
name,
|
|
56
|
+
things,
|
|
57
|
+
index=0,
|
|
58
|
+
direction=Direction.LEFT_TO_RIGHT,
|
|
59
|
+
name_within_thing=False,
|
|
60
|
+
):
|
|
61
|
+
"""
|
|
62
|
+
find an element by applying the fuzzy finding rules when given the name
|
|
63
|
+
that identifies the element on screen and a list of possible `things` that
|
|
64
|
+
are CSS expression fragments like so:
|
|
65
|
+
|
|
66
|
+
tag_name[attribute expression]
|
|
67
|
+
|
|
68
|
+
That identify the kind of element you're trying to find, such as a button,
|
|
69
|
+
input[type='button'], etc.
|
|
70
|
+
|
|
71
|
+
parameters:
|
|
72
|
+
browser - the cucu.browser.Browser object
|
|
73
|
+
name - name that identifies the element you are trying to find
|
|
74
|
+
things - array of CSS fragments that specify the kind of elements you
|
|
75
|
+
want to match on
|
|
76
|
+
index - which of the many matches to return
|
|
77
|
+
direction - the text to element direction to apply fuzzy in. Default we
|
|
78
|
+
apply right to left but for checkboxes or certain languages
|
|
79
|
+
this direction can be used to find things by prioritizing
|
|
80
|
+
matching from "left to right"
|
|
81
|
+
name_within_thing - to determine if the name has to be within the web element
|
|
82
|
+
|
|
83
|
+
returns:
|
|
84
|
+
the WebElement that matches the provided arguments.
|
|
85
|
+
"""
|
|
86
|
+
browser.switch_to_default_frame()
|
|
87
|
+
|
|
88
|
+
# always need to protect names in which double quotes are used as below
|
|
89
|
+
# we pass arguments to the fuzzy_find javascript function wrapped in double
|
|
90
|
+
# quotes
|
|
91
|
+
name = name.replace('"', '\\"')
|
|
92
|
+
name_within_thing = "true" if name_within_thing else "false"
|
|
93
|
+
|
|
94
|
+
args = [
|
|
95
|
+
f'"{name}"',
|
|
96
|
+
str(things),
|
|
97
|
+
str(index),
|
|
98
|
+
str(direction.value),
|
|
99
|
+
name_within_thing,
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
def execute_fuzzy_find():
|
|
103
|
+
init(browser)
|
|
104
|
+
script = f"return cucu.fuzzy_find({','.join(args)});"
|
|
105
|
+
return browser.execute(script)
|
|
106
|
+
|
|
107
|
+
return search_in_all_frames(browser, execute_fuzzy_find)
|
cucu/fuzzy/fuzzy.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
(function(){
|
|
2
|
+
window.cucu = {
|
|
3
|
+
debug: false
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* custom jquery matchers:
|
|
8
|
+
*
|
|
9
|
+
*
|
|
10
|
+
* has_text - matches an element with the exact text provided.
|
|
11
|
+
*
|
|
12
|
+
* vis - matches an element that is visible and has at least
|
|
13
|
+
* one visible parent.
|
|
14
|
+
*
|
|
15
|
+
*/
|
|
16
|
+
jqCucu.extend(
|
|
17
|
+
jqCucu.expr[ ":" ],
|
|
18
|
+
{
|
|
19
|
+
has_text: function(elem, index, match) {
|
|
20
|
+
return (elem.textContent || elem.innerText || jqCucu(elem).text() || '').trim() === match[3].trim();
|
|
21
|
+
},
|
|
22
|
+
vis: function (elem) {
|
|
23
|
+
return !(jqCucu(elem).is(":hidden") || jqCucu(elem).css("width") == "0px" || jqCucu(elem).css("height") == "0px" || jqCucu(elem).parents(":hidden").length);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// this matches the class Direction in cucu/fuzzy/core.py
|
|
29
|
+
const LEFT_TO_RIGHT = 1;
|
|
30
|
+
const RIGHT_TO_LEFT = 2;
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
* find an element by applying the fuzzy finding rules when given the name
|
|
34
|
+
* that identifies the element on screen and a list of possible `things` that
|
|
35
|
+
* are CSS expression fragments like so:
|
|
36
|
+
*
|
|
37
|
+
* tag_name[attribute expression]
|
|
38
|
+
*
|
|
39
|
+
* That identify the kind of element you're trying to find, such as a button,
|
|
40
|
+
* input[type='button'], etc.
|
|
41
|
+
*
|
|
42
|
+
* parameters:
|
|
43
|
+
* name - name that identifies the element you are trying to find
|
|
44
|
+
* things - array of CSS fragments that specify the kind of elements
|
|
45
|
+
* you want to match on
|
|
46
|
+
* index - which of the many matches to return
|
|
47
|
+
* direction - the text to element direction to apply fuzzy in. Default
|
|
48
|
+
* we apply right to left but for checkboxes or certain
|
|
49
|
+
* languages this direction can be used to find things by
|
|
50
|
+
* prioritizing matching from "left to right"
|
|
51
|
+
* name_within_thing - to determine if the name has to be within the web element
|
|
52
|
+
*/
|
|
53
|
+
cucu.fuzzy_find = function(name,
|
|
54
|
+
things,
|
|
55
|
+
index=0,
|
|
56
|
+
direction=LEFT_TO_RIGHT,
|
|
57
|
+
name_within_thing=false) {
|
|
58
|
+
var elements = [];
|
|
59
|
+
var results = null;
|
|
60
|
+
var attributes = ['aria-label', 'title', 'placeholder', 'value'];
|
|
61
|
+
var matchers = ['has_text', 'contains'];
|
|
62
|
+
|
|
63
|
+
name = name.replaceAll('"', '\\"')
|
|
64
|
+
|
|
65
|
+
/*
|
|
66
|
+
* try to match on exact text but ultimately fall back to matching on
|
|
67
|
+
* the elements text that contains the string we're searching for
|
|
68
|
+
*/
|
|
69
|
+
for(var mIndex=0; mIndex < matchers.length; mIndex++) {
|
|
70
|
+
var matcher = matchers[mIndex];
|
|
71
|
+
|
|
72
|
+
/*
|
|
73
|
+
* element labeled in itself or by its own attributes
|
|
74
|
+
*/
|
|
75
|
+
for(var tIndex = 0; tIndex < things.length; tIndex++) {
|
|
76
|
+
var thing = things[tIndex];
|
|
77
|
+
|
|
78
|
+
/*
|
|
79
|
+
* <thing>name</thing>
|
|
80
|
+
*/
|
|
81
|
+
results = jqCucu(thing + ':vis:' + matcher + '("' + name + '")', document.body).toArray();
|
|
82
|
+
if (cucu.debug) { console.log('<thing>name</thing>', results); }
|
|
83
|
+
elements = elements.concat(results);
|
|
84
|
+
|
|
85
|
+
// <thing attribute="name"></thing>
|
|
86
|
+
for(var aIndex=0; aIndex < attributes.length; aIndex++) {
|
|
87
|
+
var attribute_name = attributes[aIndex];
|
|
88
|
+
if (matcher == 'has_text') {
|
|
89
|
+
results = jqCucu(thing + '[' + attribute_name + '="' + name + '"]:vis', document.body).toArray();
|
|
90
|
+
if (cucu.debug) { console.log('<thing attribute="name"></thing>', results); }
|
|
91
|
+
} else if (matcher == 'contains') {
|
|
92
|
+
results = jqCucu(thing + '[' + attribute_name + '*="' + name + '"]:vis', document.body).toArray();
|
|
93
|
+
if (cucu.debug) { console.log('<thing attribute*="name"></thing>', results); }
|
|
94
|
+
}
|
|
95
|
+
elements = elements.concat(results);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/*
|
|
100
|
+
* validate against the `value` attribute of the actual DOM object
|
|
101
|
+
* as that has the value that may have changed from having written
|
|
102
|
+
* a new value into an input which doesn't get reflected in the DOM.
|
|
103
|
+
*
|
|
104
|
+
* TODO: I think there may be a cleaner way to handle this but for
|
|
105
|
+
* now lets just add another loop in here.
|
|
106
|
+
*/
|
|
107
|
+
for(var tIndex = 0; tIndex < things.length; tIndex++) {
|
|
108
|
+
var thing = things[tIndex];
|
|
109
|
+
|
|
110
|
+
// <thing value="name"></thing>
|
|
111
|
+
if (matcher == 'has_text') {
|
|
112
|
+
results = jqCucu(thing + ':vis', document.body).filter(function(){
|
|
113
|
+
return this.value == name;
|
|
114
|
+
}).toArray();
|
|
115
|
+
if (cucu.debug) { console.log('<thing value="name"></thing>', results); }
|
|
116
|
+
} else if (matcher == 'contains') {
|
|
117
|
+
results = jqCucu(thing + ':vis', document.body).filter(function(){
|
|
118
|
+
return this.value !== undefined && String(this.value).indexOf(name) != -1;
|
|
119
|
+
}).toArray();
|
|
120
|
+
if (cucu.debug) { console.log('<thing value*="name"></thing>', results); }
|
|
121
|
+
}
|
|
122
|
+
elements = elements.concat(results);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/*
|
|
126
|
+
* element labeled by another using the for/id attributes
|
|
127
|
+
*/
|
|
128
|
+
var labels = jqCucu('*[for]:vis:' + matcher + '("' + name + '")', document.body).toArray();
|
|
129
|
+
for(var tIndex = 0; tIndex < things.length; tIndex++) {
|
|
130
|
+
var thing = things[tIndex];
|
|
131
|
+
results = [];
|
|
132
|
+
|
|
133
|
+
/*
|
|
134
|
+
* <* for=...>name</*>...<thing id=...></thing>
|
|
135
|
+
*/
|
|
136
|
+
for(var lIndex=0; lIndex < labels.length; lIndex++) {
|
|
137
|
+
var label = labels[lIndex];
|
|
138
|
+
var id = label.getAttribute('for');
|
|
139
|
+
results = jqCucu(thing + '[id="' + id + '"]:vis', document.body).toArray();
|
|
140
|
+
if (cucu.debug) { console.log('<* for=...>name</*>...<thing id=...></thing>', results); }
|
|
141
|
+
elements = elements.concat(results);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/*
|
|
146
|
+
* element labeled by a nested child
|
|
147
|
+
*/
|
|
148
|
+
for(var tIndex = 0; tIndex < things.length; tIndex++) {
|
|
149
|
+
var thing = things[tIndex];
|
|
150
|
+
|
|
151
|
+
/*
|
|
152
|
+
* <thing><*>...name...</*></thing>
|
|
153
|
+
*/
|
|
154
|
+
results = jqCucu('*:vis:' + matcher + '("' + name + '")', document.body).parents(thing).toArray();
|
|
155
|
+
if (cucu.debug) { console.log('<thing><*>...name...</*></thing>', results); }
|
|
156
|
+
elements = elements.concat(results);
|
|
157
|
+
|
|
158
|
+
// <thing><* attribute="name"></*></thing>
|
|
159
|
+
for(var aIndex=0; aIndex < attributes.length; aIndex++) {
|
|
160
|
+
var attribute_name = attributes[aIndex];
|
|
161
|
+
results = jqCucu('*:vis[' + attribute_name + '="' + name + '"]', document.body).parents(thing).toArray();
|
|
162
|
+
if (cucu.debug) { console.log('<thing><* attibute="name"></*></thing>', results); }
|
|
163
|
+
elements = elements.concat(results);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// if the name has to be within the element, the following rules are not considered
|
|
168
|
+
if (name_within_thing) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// element labeled with direct previous sibling
|
|
173
|
+
if (direction === LEFT_TO_RIGHT) {
|
|
174
|
+
for(var tIndex = 0; tIndex < things.length; tIndex++) {
|
|
175
|
+
var thing = things[tIndex];
|
|
176
|
+
|
|
177
|
+
// <*>name</*><thing/>
|
|
178
|
+
results = jqCucu('*:vis:' + matcher + '("' + name + '")', document.body).next(thing + ':vis').toArray();
|
|
179
|
+
if (cucu.debug) { console.log('<*>name</*><thing/>', results); }
|
|
180
|
+
elements = elements.concat(results);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// element labeled with direct next sibling
|
|
185
|
+
if (direction === RIGHT_TO_LEFT) {
|
|
186
|
+
for(var tIndex = 0; tIndex < things.length; tIndex++) {
|
|
187
|
+
var thing = things[tIndex];
|
|
188
|
+
|
|
189
|
+
// <thing/><*>name</*>
|
|
190
|
+
results = jqCucu('*:vis:' + matcher + '("' + name + '")', document.body).prev(thing).toArray();
|
|
191
|
+
if (cucu.debug) { console.log('<thing/><*>name</*>', results); }
|
|
192
|
+
elements = elements.concat(results);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/*
|
|
197
|
+
* element labeled by a text sibling
|
|
198
|
+
*/
|
|
199
|
+
for(var tIndex = 0; tIndex < things.length; tIndex++) {
|
|
200
|
+
var thing = things[tIndex];
|
|
201
|
+
|
|
202
|
+
/*
|
|
203
|
+
* <*><thing></thing>name</*>
|
|
204
|
+
*/
|
|
205
|
+
results = jqCucu('*:vis:' + matcher + '("' + name + '")', document.body).children(thing + ':vis').toArray();
|
|
206
|
+
if (cucu.debug) { console.log('<*><thing></thing>name</*>', results); }
|
|
207
|
+
elements = elements.concat(results);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// element labeled with any previous sibling
|
|
211
|
+
if (direction === LEFT_TO_RIGHT) {
|
|
212
|
+
for(var tIndex = 0; tIndex < things.length; tIndex++) {
|
|
213
|
+
var thing = things[tIndex];
|
|
214
|
+
|
|
215
|
+
// <*>name</*>...<thing>...
|
|
216
|
+
results = jqCucu('*:vis:' + matcher + '("' + name + '")', document.body).nextAll(thing + ':vis').toArray();
|
|
217
|
+
if (cucu.debug) { console.log('<*>name</*>...<thing>...', results); }
|
|
218
|
+
elements = elements.concat(results);
|
|
219
|
+
|
|
220
|
+
// <...><*>name</*></...>...<...><thing></...>
|
|
221
|
+
// XXX: this rule is horribly complicated and I'd rather see it gone
|
|
222
|
+
// basically: common great grandpranet
|
|
223
|
+
results = jqCucu('*:vis:' + matcher + '("' + name + '")', document.body).nextAll().find(thing + ':vis').toArray();
|
|
224
|
+
if (cucu.debug) { console.log('<...><*>name</*></...>...<...><thing></...>', results); }
|
|
225
|
+
elements = elements.concat(results);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// element labeled with any next sibling
|
|
230
|
+
if (direction === RIGHT_TO_LEFT) {
|
|
231
|
+
for(var tIndex = 0; tIndex < things.length; tIndex++) {
|
|
232
|
+
var thing = things[tIndex];
|
|
233
|
+
|
|
234
|
+
// next siblings: <thing>...<*>name</*>...
|
|
235
|
+
results = jqCucu('*:vis:' + matcher + '("' + name + '")', document.body).prevAll(thing).toArray();
|
|
236
|
+
if (cucu.debug) { console.log('<thing>...<*>name</*>...', results); }
|
|
237
|
+
elements = elements.concat(results);
|
|
238
|
+
|
|
239
|
+
// <...><thing></...>...<...><*>name</*></...>
|
|
240
|
+
// XXX: this rule is horribly complicated and I'd rather see it gone
|
|
241
|
+
results = jqCucu('*:vis:' + matcher + '("' + name + '")', document.body).prevAll().find(thing + ':vis').toArray();
|
|
242
|
+
if (cucu.debug) { console.log('<...><thin></...>...<...><*>name</*></...>', results); }
|
|
243
|
+
elements = elements.concat(results);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (cucu.debug) {
|
|
249
|
+
console.log(elements);
|
|
250
|
+
}
|
|
251
|
+
return elements[index];
|
|
252
|
+
};
|
|
253
|
+
})();
|