cucu 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cucu might be problematic. Click here for more details.
- cucu/__init__.py +38 -0
- cucu/ansi_parser.py +58 -0
- cucu/behave_tweaks.py +196 -0
- cucu/browser/__init__.py +0 -0
- cucu/browser/core.py +80 -0
- cucu/browser/frames.py +106 -0
- cucu/browser/selenium.py +323 -0
- cucu/browser/selenium_tweaks.py +27 -0
- cucu/cli/__init__.py +3 -0
- cucu/cli/core.py +788 -0
- cucu/cli/run.py +207 -0
- cucu/cli/steps.py +137 -0
- cucu/cli/thread_dumper.py +55 -0
- cucu/config.py +440 -0
- cucu/edgedriver_autoinstaller/README.md +1 -0
- cucu/edgedriver_autoinstaller/__init__.py +37 -0
- cucu/edgedriver_autoinstaller/utils.py +231 -0
- cucu/environment.py +283 -0
- cucu/external/jquery/jquery-3.5.1.min.js +2 -0
- cucu/formatter/__init__.py +0 -0
- cucu/formatter/cucu.py +261 -0
- cucu/formatter/json.py +321 -0
- cucu/formatter/junit.py +289 -0
- cucu/fuzzy/__init__.py +3 -0
- cucu/fuzzy/core.py +107 -0
- cucu/fuzzy/fuzzy.js +253 -0
- cucu/helpers.py +875 -0
- cucu/hooks.py +205 -0
- cucu/language_server/__init__.py +3 -0
- cucu/language_server/core.py +114 -0
- cucu/lint/__init__.py +0 -0
- cucu/lint/linter.py +397 -0
- cucu/lint/rules/format.yaml +125 -0
- cucu/logger.py +113 -0
- cucu/matcher/__init__.py +0 -0
- cucu/matcher/core.py +30 -0
- cucu/page_checks.py +63 -0
- cucu/reporter/__init__.py +3 -0
- cucu/reporter/external/bootstrap.min.css +7 -0
- cucu/reporter/external/bootstrap.min.js +7 -0
- cucu/reporter/external/dataTables.bootstrap.min.css +1 -0
- cucu/reporter/external/dataTables.bootstrap.min.js +14 -0
- cucu/reporter/external/jquery-3.5.1.min.js +2 -0
- cucu/reporter/external/jquery.dataTables.min.js +192 -0
- cucu/reporter/external/popper.min.js +5 -0
- cucu/reporter/favicon.png +0 -0
- cucu/reporter/html.py +452 -0
- cucu/reporter/templates/feature.html +72 -0
- cucu/reporter/templates/flat.html +48 -0
- cucu/reporter/templates/index.html +49 -0
- cucu/reporter/templates/layout.html +109 -0
- cucu/reporter/templates/scenario.html +200 -0
- cucu/steps/__init__.py +27 -0
- cucu/steps/base_steps.py +88 -0
- cucu/steps/browser_steps.py +337 -0
- cucu/steps/button_steps.py +91 -0
- cucu/steps/checkbox_steps.py +111 -0
- cucu/steps/command_steps.py +181 -0
- cucu/steps/comment_steps.py +17 -0
- cucu/steps/draggable_steps.py +168 -0
- cucu/steps/dropdown_steps.py +467 -0
- cucu/steps/file_input_steps.py +80 -0
- cucu/steps/filesystem_steps.py +144 -0
- cucu/steps/flow_control_steps.py +198 -0
- cucu/steps/image_steps.py +37 -0
- cucu/steps/input_steps.py +301 -0
- cucu/steps/link_steps.py +63 -0
- cucu/steps/menuitem_steps.py +39 -0
- cucu/steps/platform_steps.py +29 -0
- cucu/steps/radio_steps.py +187 -0
- cucu/steps/step_utils.py +55 -0
- cucu/steps/tab_steps.py +68 -0
- cucu/steps/table_steps.py +437 -0
- cucu/steps/tables.js +28 -0
- cucu/steps/text_steps.py +78 -0
- cucu/steps/variable_steps.py +100 -0
- cucu/steps/webserver_steps.py +40 -0
- cucu/utils.py +269 -0
- cucu-1.0.0.dist-info/METADATA +424 -0
- cucu-1.0.0.dist-info/RECORD +83 -0
- cucu-1.0.0.dist-info/WHEEL +4 -0
- cucu-1.0.0.dist-info/entry_points.txt +2 -0
- cucu-1.0.0.dist-info/licenses/LICENSE +32 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import pkgutil
|
|
2
|
+
import re
|
|
3
|
+
from io import StringIO
|
|
4
|
+
|
|
5
|
+
from selenium.webdriver.common.by import By
|
|
6
|
+
|
|
7
|
+
from cucu import (
|
|
8
|
+
config,
|
|
9
|
+
format_gherkin_table,
|
|
10
|
+
fuzzy,
|
|
11
|
+
helpers,
|
|
12
|
+
logger,
|
|
13
|
+
retry,
|
|
14
|
+
step,
|
|
15
|
+
)
|
|
16
|
+
from cucu.browser.frames import run_in_all_frames
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def find_tables(ctx):
|
|
20
|
+
"""
|
|
21
|
+
find all the tables currently present on the page
|
|
22
|
+
|
|
23
|
+
parameters:
|
|
24
|
+
ctx(object): behave context object used to share data between steps
|
|
25
|
+
|
|
26
|
+
returns:
|
|
27
|
+
an array of arrays containing the HTML tables currently displayed
|
|
28
|
+
"""
|
|
29
|
+
ctx.check_browser_initialized()
|
|
30
|
+
tables_lib = pkgutil.get_data("cucu", "steps/tables.js")
|
|
31
|
+
tables_lib = tables_lib.decode("utf8")
|
|
32
|
+
|
|
33
|
+
def search_for_tables():
|
|
34
|
+
ctx.browser.execute(tables_lib)
|
|
35
|
+
return ctx.browser.execute("return findAllTables();")
|
|
36
|
+
|
|
37
|
+
return run_in_all_frames(ctx.browser, search_for_tables)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def behave_table_to_array(table):
|
|
41
|
+
"""
|
|
42
|
+
given a behave.model.Table object convert it to an array of rows
|
|
43
|
+
|
|
44
|
+
parameters:
|
|
45
|
+
table(behave.model.Table): the behave table to convert into an array
|
|
46
|
+
|
|
47
|
+
returns:
|
|
48
|
+
array of rows representing the behave table provided.
|
|
49
|
+
"""
|
|
50
|
+
result = [table.headings]
|
|
51
|
+
|
|
52
|
+
for row in table.rows:
|
|
53
|
+
values = []
|
|
54
|
+
for value in row:
|
|
55
|
+
values.append(value)
|
|
56
|
+
result.append(values)
|
|
57
|
+
|
|
58
|
+
return result
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def check_table_equals_table(table, expected_table):
|
|
62
|
+
"""
|
|
63
|
+
check that table is equal to expected_table
|
|
64
|
+
"""
|
|
65
|
+
return table == expected_table
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def check_table_matches_table(table, expected_table):
|
|
69
|
+
"""
|
|
70
|
+
check if table matches the regex patterns in expected table
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
if len(table) == len(expected_table):
|
|
74
|
+
table_matched = True
|
|
75
|
+
|
|
76
|
+
for expected_row, row in zip(expected_table, table):
|
|
77
|
+
for expected_value, value in zip(expected_row, row):
|
|
78
|
+
if not re.match(expected_value, value):
|
|
79
|
+
table_matched = False
|
|
80
|
+
|
|
81
|
+
if table_matched:
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def check_table_contains_table(table, expected_table):
|
|
88
|
+
"""
|
|
89
|
+
check that table contains the rows in expected_table
|
|
90
|
+
"""
|
|
91
|
+
return all(row in table for row in expected_table)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def check_table_contains_matching_rows_in_table(table, expected_table):
|
|
95
|
+
"""
|
|
96
|
+
check that table contains the matching rows in expected_table
|
|
97
|
+
"""
|
|
98
|
+
table_matched = True
|
|
99
|
+
|
|
100
|
+
for expected_row in expected_table:
|
|
101
|
+
for row in table:
|
|
102
|
+
found_row = True
|
|
103
|
+
for expected_value, value in zip(expected_row, row):
|
|
104
|
+
if not re.match(expected_value, value):
|
|
105
|
+
found_row = False
|
|
106
|
+
if found_row:
|
|
107
|
+
break
|
|
108
|
+
|
|
109
|
+
if not found_row:
|
|
110
|
+
table_matched = False
|
|
111
|
+
break
|
|
112
|
+
|
|
113
|
+
if table_matched:
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def report_unable_to_find_table(expected_table, found_tables):
|
|
120
|
+
stream = StringIO()
|
|
121
|
+
stream.write("\n")
|
|
122
|
+
for index, table in enumerate(found_tables):
|
|
123
|
+
print_index = helpers.nth_to_ordinal(index) or '"1st" '
|
|
124
|
+
stream.write(
|
|
125
|
+
f"{print_index}table:\n{format_gherkin_table(table, [], ' ')}\n"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
stream.seek(0)
|
|
129
|
+
raise RuntimeError(
|
|
130
|
+
f"unable to find desired table\nexpected:\n{format_gherkin_table(expected_table, [], ' ')}\n\nfound:{stream.read()}"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def report_found_undesired_table(unexpected_tables, found_tables):
|
|
135
|
+
stream = StringIO()
|
|
136
|
+
stream.write("\n")
|
|
137
|
+
for index, table in enumerate(found_tables):
|
|
138
|
+
print_index = helpers.nth_to_ordinal(index) or '"1st" '
|
|
139
|
+
stream.write(
|
|
140
|
+
f"{print_index}table:\n{format_gherkin_table(table, [], ' ')}\n"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
stream.seek(0)
|
|
144
|
+
error_message = ""
|
|
145
|
+
for table in unexpected_tables:
|
|
146
|
+
error_message += (
|
|
147
|
+
f"found undesired table\n\nundesired table:\n{format_gherkin_table(table, [], ' ')}\n\n"
|
|
148
|
+
f"all tables found:{stream.read()}\n"
|
|
149
|
+
)
|
|
150
|
+
raise RuntimeError(error_message)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def find_table(ctx, assert_func, nth=None):
|
|
154
|
+
"""
|
|
155
|
+
validate we can find the table passed in the ctx object and assert it
|
|
156
|
+
matches anyone of the tables on the current web page. If `nth` is set to
|
|
157
|
+
something then we only check against the nth table of the available tables.
|
|
158
|
+
|
|
159
|
+
paramters:
|
|
160
|
+
ctx(object): behave context object
|
|
161
|
+
assert_func(function): function used to assert two tables "match"
|
|
162
|
+
nth(int): when set to an int specifies the exact table within the list
|
|
163
|
+
of available tables to match against.
|
|
164
|
+
|
|
165
|
+
raises:
|
|
166
|
+
RuntimeError when the desired table was not found
|
|
167
|
+
"""
|
|
168
|
+
expected = behave_table_to_array(ctx.table)
|
|
169
|
+
found_tables = find_tables(ctx)
|
|
170
|
+
|
|
171
|
+
if nth is not None:
|
|
172
|
+
if assert_func(found_tables[nth], expected):
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
else:
|
|
176
|
+
for table in found_tables:
|
|
177
|
+
if assert_func(table, expected):
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
report_unable_to_find_table(expected, found_tables)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def do_not_find_table(ctx, assert_func, nth=None):
|
|
184
|
+
"""
|
|
185
|
+
validate we can not find the table passed in the ctx object and assert it
|
|
186
|
+
matches anyone of the tables on the current web page. If `nth` is set to
|
|
187
|
+
something then we only check against the nth table of the available tables.
|
|
188
|
+
|
|
189
|
+
paramters:
|
|
190
|
+
ctx(object): behave context object
|
|
191
|
+
assert_func(function): function used to assert two tables "match"
|
|
192
|
+
nth(int): when set to an int specifies the exact table within the list
|
|
193
|
+
of available tables to match against.
|
|
194
|
+
|
|
195
|
+
raises:
|
|
196
|
+
RuntimeError when the desired table was not found
|
|
197
|
+
"""
|
|
198
|
+
expected = behave_table_to_array(ctx.table)
|
|
199
|
+
tables = find_tables(ctx)
|
|
200
|
+
matching_tables = []
|
|
201
|
+
|
|
202
|
+
if nth is not None:
|
|
203
|
+
if assert_func(tables[nth], expected):
|
|
204
|
+
matching_tables.append(tables[nth])
|
|
205
|
+
|
|
206
|
+
else:
|
|
207
|
+
for table in tables:
|
|
208
|
+
if assert_func(table, expected):
|
|
209
|
+
matching_tables.append(table)
|
|
210
|
+
|
|
211
|
+
# If none of the tables match the pattern, then return
|
|
212
|
+
if len(matching_tables) == 0:
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
report_found_undesired_table(matching_tables, tables)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
for thing, check_func in {
|
|
219
|
+
"is": check_table_equals_table,
|
|
220
|
+
"matches": check_table_matches_table,
|
|
221
|
+
"contains": check_table_contains_table,
|
|
222
|
+
"contains rows matching": check_table_contains_matching_rows_in_table,
|
|
223
|
+
}.items():
|
|
224
|
+
|
|
225
|
+
@step(f"I should see a table that {thing} the following")
|
|
226
|
+
def should_see_the_table(ctx, check_func=check_func):
|
|
227
|
+
find_table(ctx, check_func)
|
|
228
|
+
|
|
229
|
+
@step(f"I should not see a table that {thing} the following")
|
|
230
|
+
def should_not_see_the_table(ctx, check_func=check_func):
|
|
231
|
+
do_not_find_table(ctx, check_func)
|
|
232
|
+
|
|
233
|
+
@step(f"I wait to see a table that {thing} the following")
|
|
234
|
+
def wait_to_see_the_table(ctx, check_func=check_func):
|
|
235
|
+
retry(find_table)(ctx, check_func)
|
|
236
|
+
|
|
237
|
+
@step(f"I wait to not see a table that {thing} the following")
|
|
238
|
+
def wait_to_not_see_the_table(ctx, check_func=check_func):
|
|
239
|
+
retry(do_not_find_table)(ctx, check_func)
|
|
240
|
+
|
|
241
|
+
@step(
|
|
242
|
+
f'I wait up to "{{seconds}}" seconds to see a table that {thing} the following'
|
|
243
|
+
)
|
|
244
|
+
def wait_up_to_seconds_to_see_the_table(
|
|
245
|
+
ctx, seconds, check_func=check_func
|
|
246
|
+
):
|
|
247
|
+
seconds = float(seconds)
|
|
248
|
+
retry(find_table, wait_up_to_s=seconds)(ctx, check_func)
|
|
249
|
+
|
|
250
|
+
@step(
|
|
251
|
+
f'I wait up to "{{seconds}}" seconds to not see a table that {thing} the following'
|
|
252
|
+
)
|
|
253
|
+
def wait_up_to_seconds_to_not_see_the_table(
|
|
254
|
+
ctx, seconds, check_func=check_func
|
|
255
|
+
):
|
|
256
|
+
seconds = float(seconds)
|
|
257
|
+
retry(do_not_find_table, wait_up_to_s=seconds)(ctx, check_func)
|
|
258
|
+
|
|
259
|
+
@step(f'I should see the "{{nth}}" table {thing} the following')
|
|
260
|
+
def should_see_the_nth_table(ctx, nth, check_func=check_func):
|
|
261
|
+
find_table(ctx, check_func, nth=nth)
|
|
262
|
+
|
|
263
|
+
@step(f'I wait to see the "{{nth}}" table {thing} the following')
|
|
264
|
+
def wait_to_see_the_nth_table(ctx, nth, check_func=check_func):
|
|
265
|
+
retry(find_table)(ctx, check_func, nth=nth)
|
|
266
|
+
|
|
267
|
+
@step(
|
|
268
|
+
f'I wait up to "{{seconds}}" seconds to see the "{{nth}}" table {thing} the following'
|
|
269
|
+
)
|
|
270
|
+
def wait_up_to_seconds_to_see_the_nth_table(
|
|
271
|
+
ctx, seconds, nth, check_func=check_func
|
|
272
|
+
):
|
|
273
|
+
seconds = float(seconds)
|
|
274
|
+
retry(find_table, wait_up_to_s=seconds)(ctx, check_func, nth=nth)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def find_table_header(ctx, name, index=0):
|
|
278
|
+
"""
|
|
279
|
+
find a table header with the provided name
|
|
280
|
+
"""
|
|
281
|
+
ctx.check_browser_initialized()
|
|
282
|
+
return fuzzy.find(ctx.browser, name, ["th"], index=index)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def click_table_header(ctx, header):
|
|
286
|
+
"""
|
|
287
|
+
internal method used to simply click a table header element
|
|
288
|
+
"""
|
|
289
|
+
ctx.check_browser_initialized()
|
|
290
|
+
ctx.browser.click(header)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
helpers.define_action_on_thing_with_name_steps(
|
|
294
|
+
"table header",
|
|
295
|
+
"click",
|
|
296
|
+
find_table_header,
|
|
297
|
+
click_table_header,
|
|
298
|
+
with_nth=True,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def get_table_cell_value(ctx, table, row, column, variable_name):
|
|
303
|
+
tables = find_tables(ctx)
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
cell_value = tables[table][row][column]
|
|
307
|
+
except IndexError:
|
|
308
|
+
raise RuntimeError(
|
|
309
|
+
f"Cannot find table:{table+1},row:{row+1},column:{column+1}. Please check your table data."
|
|
310
|
+
)
|
|
311
|
+
config.CONFIG[variable_name] = cell_value
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@step(
|
|
315
|
+
'I save "{table:nth}" table, "{row:nth}" row, "{column:nth}" column value to a variable "{variable_name}"'
|
|
316
|
+
)
|
|
317
|
+
def step_get_table_cell_value(ctx, table, row, column, variable_name):
|
|
318
|
+
get_table_cell_value(ctx, table, row, column, variable_name)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@step(
|
|
322
|
+
'I wait to save "{table:nth}" table, "{row:nth}" row, "{column:nth}" column value to a variable "{variable_name}"'
|
|
323
|
+
)
|
|
324
|
+
def wait_to_get_table_cell_value(ctx, table, row, column, variable_name):
|
|
325
|
+
retry(get_table_cell_value)(ctx, table, row, column, variable_name)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def find_table_element(ctx, nth=1):
|
|
329
|
+
"""
|
|
330
|
+
Return the nth table as a WebElement
|
|
331
|
+
|
|
332
|
+
parameters:
|
|
333
|
+
ctx(object): behave context object used to share data between steps
|
|
334
|
+
nth(int): specifies the exact table within the list of available tables to match against.
|
|
335
|
+
Defaults to 1st table.
|
|
336
|
+
|
|
337
|
+
returns:
|
|
338
|
+
A selenium WebElement associated with the table that was specified
|
|
339
|
+
"""
|
|
340
|
+
ctx.check_browser_initialized()
|
|
341
|
+
|
|
342
|
+
try:
|
|
343
|
+
return ctx.browser.css_find_elements("table")[nth]
|
|
344
|
+
except IndexError:
|
|
345
|
+
raise RuntimeError(
|
|
346
|
+
f"Cannot find table:{nth+1}. Please check your table data."
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def click_table_cell(ctx, row, column, table):
|
|
351
|
+
"""
|
|
352
|
+
Clicks the cell corresponding to the given row and column
|
|
353
|
+
|
|
354
|
+
parameters:
|
|
355
|
+
ctx(object): behave context object used to share data between steps
|
|
356
|
+
row(int): the row of the table to click
|
|
357
|
+
column(int): the column of the table to click
|
|
358
|
+
table(int): specifies the exact table within the list of available tables to match against.
|
|
359
|
+
"""
|
|
360
|
+
table_element = find_table_element(ctx, table)
|
|
361
|
+
|
|
362
|
+
try:
|
|
363
|
+
row = table_element.find_elements(By.CSS_SELECTOR, "tbody tr")[row]
|
|
364
|
+
cell = row.find_elements(By.CSS_SELECTOR, "td")[column]
|
|
365
|
+
except IndexError:
|
|
366
|
+
raise RuntimeError(
|
|
367
|
+
f"Cannot find table:{table+1},row:{row+1},column:{column+1}. Please check your table data."
|
|
368
|
+
)
|
|
369
|
+
ctx.browser.click(cell)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
@step('I wait to click the "{row:nth}" row in the "{table:nth}" table')
|
|
373
|
+
def wait_click_table_row(ctx, row, table):
|
|
374
|
+
"""
|
|
375
|
+
Add 1 to the row number if the table has a header row.
|
|
376
|
+
|
|
377
|
+
Note: Firefox is unable to click directly on a row <tr> if it has child columns <td>.
|
|
378
|
+
In order to workaround this, the step just clicks the first column <td> of the row <tr>.
|
|
379
|
+
Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1448825
|
|
380
|
+
"""
|
|
381
|
+
retry(click_table_cell)(ctx, row, 1, table)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
@step(
|
|
385
|
+
'I wait to click the cell corresponding to the "{row:nth}" row and "{column:nth}" column in the "{table:nth}" table'
|
|
386
|
+
)
|
|
387
|
+
def wait_click_table_cell(ctx, row, column, table):
|
|
388
|
+
"""
|
|
389
|
+
Add 1 to the row number if the table has a header row.
|
|
390
|
+
"""
|
|
391
|
+
retry(click_table_cell)(ctx, row, column, table)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@step(
|
|
395
|
+
'I wait to click the "{column:nth}" column within a row that contains the text "{match_text}" in the "{table:nth}" table'
|
|
396
|
+
)
|
|
397
|
+
def wait_click_table_cell_matching_text(ctx, column, match_text, table):
|
|
398
|
+
def click_table_cell_matching_text(ctx, column, match_text, table):
|
|
399
|
+
table_element = find_table_element(ctx, table)
|
|
400
|
+
|
|
401
|
+
try:
|
|
402
|
+
row = table_element.find_elements(
|
|
403
|
+
By.XPATH, f'//td[.="{match_text}"]/parent::tr'
|
|
404
|
+
)
|
|
405
|
+
if len(row) > 1:
|
|
406
|
+
logger.warn(
|
|
407
|
+
f'Found {len(row)} rows with matching text "{match_text}", using the first row.'
|
|
408
|
+
)
|
|
409
|
+
cell = row[0].find_elements(By.CSS_SELECTOR, "td")[column]
|
|
410
|
+
except IndexError:
|
|
411
|
+
raise RuntimeError(
|
|
412
|
+
f"Cannot find table:{table+1},column:{column+1},text:{match_text}. Please check your table data."
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
ctx.browser.click(cell)
|
|
416
|
+
|
|
417
|
+
retry(click_table_cell_matching_text)(ctx, column, match_text, table)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
@step('I wait to see there are "{row_count}" rows in the "{table:nth}" table')
|
|
421
|
+
def wait_table_row_count(ctx, row_count, table):
|
|
422
|
+
"""
|
|
423
|
+
Add 1 to the row number if the table has a header row.
|
|
424
|
+
"""
|
|
425
|
+
|
|
426
|
+
def find_table_row_count(ctx, row_count, table):
|
|
427
|
+
table_element = find_table_element(ctx, table)
|
|
428
|
+
table_rows = len(table_element.find_elements(By.CSS_SELECTOR, "tr"))
|
|
429
|
+
|
|
430
|
+
if int(row_count) == table_rows:
|
|
431
|
+
return
|
|
432
|
+
else:
|
|
433
|
+
raise RuntimeError(
|
|
434
|
+
f"Unable to find {row_count} rows in table {table+1}. Please check your table data."
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
retry(find_table_row_count)(ctx, row_count, table)
|
cucu/steps/tables.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
(function(){
|
|
2
|
+
window.findAllTables = function() {
|
|
3
|
+
var tables = [];
|
|
4
|
+
function tableToJSON(table) {
|
|
5
|
+
var data = [];
|
|
6
|
+
for (var rIndex=0; rIndex < table.rows.length; rIndex++) {
|
|
7
|
+
var row = table.rows[rIndex];
|
|
8
|
+
var values = [];
|
|
9
|
+
for (var vIndex=0; vIndex < row.cells.length; vIndex++) {
|
|
10
|
+
var value = row.cells[vIndex].innerText.trim();
|
|
11
|
+
value = value.replace(/[\r\n\s]+/g, " ");
|
|
12
|
+
values.push(value);
|
|
13
|
+
}
|
|
14
|
+
if (values.length != 0) {
|
|
15
|
+
data.push(values);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return data;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
var table_elements = document.querySelectorAll('table');
|
|
22
|
+
for(var index=0; index < table_elements.length; index++) {
|
|
23
|
+
tables.push(tableToJSON(table_elements[index]));
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return tables;
|
|
27
|
+
};
|
|
28
|
+
})();
|
cucu/steps/text_steps.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from cucu import fuzzy, helpers, step
|
|
2
|
+
from cucu.browser.frames import try_in_frames_until_success
|
|
3
|
+
from cucu.steps import step_utils
|
|
4
|
+
from cucu.utils import take_saw_element_screenshot, text_in_current_frame
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def find_text(ctx, name, index=0):
|
|
8
|
+
"""
|
|
9
|
+
find any element containing the text provide.
|
|
10
|
+
|
|
11
|
+
parameters:
|
|
12
|
+
ctx(object): behave context object used to share data between steps
|
|
13
|
+
name(str): name that identifies the desired radio text on screen
|
|
14
|
+
index(str): the index of the radio text if there are duplicates
|
|
15
|
+
|
|
16
|
+
returns:
|
|
17
|
+
the WebElement that matches the provided arguments or None if none found
|
|
18
|
+
"""
|
|
19
|
+
ctx.check_browser_initialized()
|
|
20
|
+
element = fuzzy.find(
|
|
21
|
+
ctx.browser,
|
|
22
|
+
name,
|
|
23
|
+
["*"],
|
|
24
|
+
index=index,
|
|
25
|
+
direction=fuzzy.Direction.LEFT_TO_RIGHT,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
take_saw_element_screenshot(ctx, "text", name, index, element)
|
|
29
|
+
|
|
30
|
+
return element
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Also update the line number in the scenario: `User gets the right stacktrace for steps using step helpers` when changing the code below.
|
|
34
|
+
helpers.define_should_see_thing_with_name_steps("text", find_text)
|
|
35
|
+
helpers.define_run_steps_if_I_can_see_element_with_name_steps(
|
|
36
|
+
"text", find_text
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@step(
|
|
41
|
+
'I search for the regex "{regex}" on the current page and save the group "{name}" to the variable "{variable}"'
|
|
42
|
+
)
|
|
43
|
+
def search_for_regex_to_page_and_save(ctx, regex, name, variable):
|
|
44
|
+
ctx.check_browser_initialized()
|
|
45
|
+
|
|
46
|
+
def search_for_regex_in_frame():
|
|
47
|
+
text = text_in_current_frame(ctx.browser)
|
|
48
|
+
step_utils.search_and_save(
|
|
49
|
+
regex=regex, value=text, name=name, variable=variable
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
try_in_frames_until_success(ctx.browser, search_for_regex_in_frame)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@step(
|
|
56
|
+
'I match the regex "{regex}" on the current page and save the group "{name}" to the variable "{variable}"'
|
|
57
|
+
)
|
|
58
|
+
def match_for_regex_to_page_and_save(ctx, regex, name, variable):
|
|
59
|
+
ctx.check_browser_initialized()
|
|
60
|
+
|
|
61
|
+
def match_for_regex_in_frame():
|
|
62
|
+
text = text_in_current_frame(ctx.browser)
|
|
63
|
+
step_utils.match_and_save(
|
|
64
|
+
regex=regex, value=text, name=name, variable=variable
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
try_in_frames_until_success(ctx.browser, match_for_regex_in_frame)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@step('I should see text matching the regex "{regex}" on the current page')
|
|
71
|
+
def search_for_regex_on_page(ctx, regex):
|
|
72
|
+
ctx.check_browser_initialized()
|
|
73
|
+
|
|
74
|
+
def search_for_regex_in_frame():
|
|
75
|
+
text = text_in_current_frame(ctx.browser)
|
|
76
|
+
step_utils.search(regex=regex, value=text)
|
|
77
|
+
|
|
78
|
+
try_in_frames_until_success(ctx.browser, search_for_regex_in_frame)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from cucu import config, step
|
|
4
|
+
from cucu.steps import step_utils
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@step('I set the variable "{variable}" to "{value}"')
|
|
8
|
+
def set_variable_to(_, variable, value):
|
|
9
|
+
config.CONFIG[variable] = value
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@step('I set the variable "{variable}" to the following')
|
|
13
|
+
def set_variable_to_the_following(ctx, variable):
|
|
14
|
+
config.CONFIG[variable] = ctx.text
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@step('I should see "{this}" is empty')
|
|
18
|
+
def should_see_is_empty(_, this):
|
|
19
|
+
if this or len(this) != 0:
|
|
20
|
+
raise RuntimeError(f"{this} is not empty")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@step('I should see "{this}" is equal to "{that}"')
|
|
24
|
+
def should_see_is_equal(_, this, that):
|
|
25
|
+
if this != that:
|
|
26
|
+
raise RuntimeError(f"{this} is not equal to {that}")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@step('I should see "{this}" is not equal to "{that}"')
|
|
30
|
+
def should_see_is_not_equal(_, this, that):
|
|
31
|
+
if this == that:
|
|
32
|
+
raise RuntimeError(f"{this} is equal to {that}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@step('I should see "{this}" contains "{that}"')
|
|
36
|
+
def should_see_it_contains(_, this, that):
|
|
37
|
+
if that not in this:
|
|
38
|
+
raise RuntimeError(f"{this} does not contain {that}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@step('I should see "{this}" contains the following')
|
|
42
|
+
def should_see_it_contains_the_following(ctx, this):
|
|
43
|
+
if ctx.text not in this:
|
|
44
|
+
raise RuntimeError(f"{this} does not contain {ctx.text}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@step('I should see "{this}" does not contain "{that}"')
|
|
48
|
+
def should_see_it_doest_not_contain(_, this, that):
|
|
49
|
+
if that in this:
|
|
50
|
+
raise RuntimeError(f"{this} contains {that}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@step('I should see "{this}" does not contain the following')
|
|
54
|
+
def should_see_it_does_not_contain(ctx, this):
|
|
55
|
+
if ctx.text in this:
|
|
56
|
+
raise RuntimeError(f"{this} contain {ctx.text}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@step('I should see "{this}" is equal to the following')
|
|
60
|
+
def should_see_is_equal_to_the_following(ctx, this):
|
|
61
|
+
that = ctx.text
|
|
62
|
+
|
|
63
|
+
if this != that:
|
|
64
|
+
raise RuntimeError(f"{this} is not equal to {that}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@step('I should see "{this}" matches "{that}"')
|
|
68
|
+
def should_see_matches(_, this, that):
|
|
69
|
+
if re.match(that, this) is None:
|
|
70
|
+
raise RuntimeError(f"{this} does not match {that}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@step('I should see "{this}" matches the following')
|
|
74
|
+
def should_see_matches_the_following(ctx, this):
|
|
75
|
+
that = ctx.text
|
|
76
|
+
|
|
77
|
+
if re.match(that, this) is None:
|
|
78
|
+
raise RuntimeError(f"{this}\ndoes not match:\n{that}")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@step('I should see "{this}" does not match the following')
|
|
82
|
+
def should_does_not_see_matches_the_following(ctx, this):
|
|
83
|
+
that = ctx.text
|
|
84
|
+
|
|
85
|
+
if re.match(that, this) is not None:
|
|
86
|
+
raise RuntimeError(f"{this}\nmatches:\n{that}")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@step(
|
|
90
|
+
'I search for the regex "{regex}" in "{value}" and save the group "{name}" to the variable "{variable}"'
|
|
91
|
+
)
|
|
92
|
+
def search_and_save(ctx, regex, value, name, variable):
|
|
93
|
+
step_utils.search_and_save(regex, value, name, variable)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@step(
|
|
97
|
+
'I match the regex "{regex}" in "{value}" and save the group "{name}" to the variable "{variable}"'
|
|
98
|
+
)
|
|
99
|
+
def match_and_save(ctx, regex, value, name, variable):
|
|
100
|
+
step_utils.match_and_save(regex, value, name, variable)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from functools import partial
|
|
2
|
+
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
|
3
|
+
from threading import Thread
|
|
4
|
+
|
|
5
|
+
from behave import step
|
|
6
|
+
|
|
7
|
+
from cucu import register_after_this_scenario_hook
|
|
8
|
+
from cucu.config import CONFIG
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class QuietHTTPRequestHandler(SimpleHTTPRequestHandler):
|
|
12
|
+
def log_message(self, format, *args):
|
|
13
|
+
return
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@step(
|
|
17
|
+
'I start a webserver at directory "{directory}" and save the port to the variable "{variable}"'
|
|
18
|
+
)
|
|
19
|
+
def run_webserver_for_scenario(ctx, directory, variable):
|
|
20
|
+
"""
|
|
21
|
+
start a webserver with the root at the directory provided and save the
|
|
22
|
+
port that the server is listening at to the variable name provided
|
|
23
|
+
|
|
24
|
+
examples:
|
|
25
|
+
Given I start webserver at directory "/some/path" and save the port to the variable "PORT"
|
|
26
|
+
And I open a browser at the url "http://{HOST_ADDRESS}:{PORT}/somefile.html"
|
|
27
|
+
"""
|
|
28
|
+
handler = partial(QuietHTTPRequestHandler, directory=directory)
|
|
29
|
+
httpd = HTTPServer(("", 0), handler)
|
|
30
|
+
thread = Thread(target=httpd.serve_forever)
|
|
31
|
+
thread.start()
|
|
32
|
+
|
|
33
|
+
_, port = httpd.server_address
|
|
34
|
+
CONFIG[variable] = str(port)
|
|
35
|
+
|
|
36
|
+
def shutdown_webserver(_):
|
|
37
|
+
httpd.shutdown()
|
|
38
|
+
thread.join()
|
|
39
|
+
|
|
40
|
+
register_after_this_scenario_hook(shutdown_webserver)
|