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
@@ -0,0 +1,337 @@
1
+ import base64
2
+ import os
3
+
4
+ from selenium.webdriver.common.keys import Keys
5
+
6
+ from cucu import config, logger, retry, run_steps, step
7
+ from cucu.browser.selenium import Selenium
8
+
9
+
10
+ def open_browser(ctx):
11
+ browser_name = config.CONFIG["CUCU_BROWSER"]
12
+ headless = config.CONFIG["CUCU_BROWSER_HEADLESS"]
13
+ selenium_remote_url = config.CONFIG["CUCU_SELENIUM_REMOTE_URL"]
14
+
15
+ browser = Selenium()
16
+ logger.debug(f"opening browser {browser_name}")
17
+ browser.open(
18
+ browser_name,
19
+ headless=headless,
20
+ selenium_remote_url=selenium_remote_url,
21
+ )
22
+
23
+ return browser
24
+
25
+
26
+ @step('I open a browser at the url "{url}"')
27
+ def open_a_browser(ctx, url):
28
+ """
29
+ open a browser at the url provided
30
+
31
+ example:
32
+ Given I open a browser at the url "https://www.google.com"
33
+ """
34
+ if ctx.browser is None:
35
+ ctx.browser = open_browser(ctx)
36
+ ctx.browsers.append(ctx.browser)
37
+ else:
38
+ logger.debug("browser already open so using existing instance")
39
+
40
+ logger.debug(f"navigating to url #{url}")
41
+ ctx.browser.navigate(url)
42
+
43
+
44
+ @step('I open a new browser at the url "{url}"')
45
+ def open_a_new_browser(ctx, url):
46
+ ctx.browser = open_browser(ctx)
47
+ ctx.browsers.append(ctx.browser)
48
+ logger.debug(f"navigating to url #{url}")
49
+ ctx.browser.navigate(url)
50
+
51
+
52
+ @step("I execute in the current browser the following javascript")
53
+ def execute_javascript(ctx):
54
+ ctx.check_browser_initialized()
55
+ ctx.browser.execute(ctx.text)
56
+
57
+
58
+ @step(
59
+ 'I execute in the current browser the following javascript and save the result to the variable "{variable}"'
60
+ )
61
+ def execute_javascript_and_save(ctx, variable):
62
+ ctx.check_browser_initialized()
63
+ result = ctx.browser.execute(ctx.text)
64
+ config.CONFIG[variable] = result
65
+
66
+
67
+ def assert_url_is(ctx, value):
68
+ ctx.check_browser_initialized()
69
+ url = ctx.browser.get_current_url()
70
+ if value == url:
71
+ raise RuntimeError(f"current url is {url}, not {value}")
72
+
73
+
74
+ @step('I should see the current url is "{value}"')
75
+ def should_see_the_current_url_is(ctx, value):
76
+ assert_url_is(ctx, value)
77
+
78
+
79
+ @step('I wait to see the current url is "{value}"')
80
+ def wait_to_see_the_current_url_is(ctx, value):
81
+ retry(assert_url_is)(ctx, value)
82
+
83
+
84
+ @step('I save the current url to the variable "{variable}"')
85
+ def save_current_url_to_variable(ctx, variable):
86
+ ctx.check_browser_initialized()
87
+ config.CONFIG[variable] = ctx.browser.get_current_url()
88
+
89
+
90
+ @step("I refresh the browser")
91
+ def refresh_browser(ctx):
92
+ ctx.check_browser_initialized()
93
+ ctx.browser.refresh()
94
+
95
+
96
+ @step("I go back on the browser")
97
+ def go_back_on_browser(ctx):
98
+ ctx.check_browser_initialized()
99
+ ctx.browser.back()
100
+
101
+
102
+ @step('I save the contents of the clipboard to the variable "{variable}"')
103
+ def save_clipboard_value_to_variable(ctx, variable):
104
+ ctx.check_browser_initialized()
105
+
106
+ # use default frame when adding elements to the document to avoid this errro on access:
107
+ # "selenium.common.exceptions.ElementNotInteractableException: Message: element not interactable"
108
+ ctx.browser.switch_to_default_frame()
109
+
110
+ # create the hidden textarea so we can paste clipboard contents in
111
+ textarea = ctx.browser.execute(
112
+ """
113
+ var textarea = document.getElementById('cucu-copy-n-paste')
114
+ if (!textarea) {
115
+ textarea = document.createElement('textarea');
116
+ textarea.setAttribute('id', 'cucu-copy-n-paste');
117
+ textarea.style.display = 'hidden';
118
+ textarea.style.height = '0px';
119
+ textarea.style.width = '0px';
120
+ document.body.insertBefore(textarea, document.body.firstChild);
121
+ }
122
+ return textarea;
123
+ """
124
+ )
125
+
126
+ # send ctrl+v or cmd+v to that element
127
+ if "mac" in ctx.browser.execute("return navigator.platform").lower():
128
+ textarea.send_keys(Keys.COMMAND, "v")
129
+ else:
130
+ textarea.send_keys(Keys.CONTROL, "v")
131
+
132
+ clipboard_contents = textarea.get_attribute("value")
133
+ config.CONFIG[variable] = clipboard_contents
134
+
135
+
136
+ @step('I should see the browser title is "{title}"')
137
+ def should_see_browser_title(ctx, title):
138
+ ctx.check_browser_initialized()
139
+ current_title = ctx.browser.title()
140
+
141
+ if current_title != title:
142
+ raise RuntimeError(f'unexpected browser title, got "{current_title}"')
143
+
144
+
145
+ @step("I close the current browser")
146
+ def close_browser(ctx):
147
+ ctx.check_browser_initialized()
148
+ browser_index = ctx.browsers.index(ctx.browser)
149
+
150
+ if browser_index > 0:
151
+ ctx.browser = ctx.browsers[browser_index - 1]
152
+ else:
153
+ ctx.browser = None
154
+
155
+ ctx.browsers[browser_index].quit()
156
+ del ctx.browsers[browser_index]
157
+
158
+
159
+ @step('I navigate to the url "{url}"')
160
+ def navigate_to_the_url(ctx, url):
161
+ ctx.check_browser_initialized()
162
+ logger.debug(f"navigating to url #{url}")
163
+ ctx.browser.navigate(url)
164
+
165
+
166
+ @step("I switch to the previous browser")
167
+ def switch_to_previous_browser(ctx):
168
+ browser_index = ctx.browsers.index(ctx.browser)
169
+
170
+ if browser_index > 0:
171
+ ctx.browser = ctx.browsers[browser_index - 1]
172
+ else:
173
+ raise RuntimeError("no previous browser window available")
174
+
175
+
176
+ @step("I switch to the next browser")
177
+ def switch_to_next_browser(ctx):
178
+ browser_index = ctx.browsers.index(ctx.browser)
179
+
180
+ if browser_index < len(ctx.browsers) - 1:
181
+ ctx.browser = ctx.browsers[browser_index + 1]
182
+ else:
183
+ raise RuntimeError("no next browser window available")
184
+
185
+
186
+ @step("I close the current browser tab")
187
+ def close_browser_tab(ctx):
188
+ ctx.check_browser_initialized()
189
+ ctx.browser.close_window()
190
+
191
+
192
+ def switch_to_next_tab(ctx):
193
+ ctx.check_browser_initialized()
194
+ ctx.browser.switch_to_next_tab()
195
+
196
+
197
+ @step("I switch to the next browser tab")
198
+ def switch_to_next_browser_tab(ctx):
199
+ switch_to_next_tab(ctx)
200
+
201
+
202
+ @step("I wait to switch to the next browser tab")
203
+ def wait_to_switch_to_next_browser_tab(ctx):
204
+ retry(switch_to_next_tab)(ctx)
205
+
206
+
207
+ def switch_to_previous_tab(ctx):
208
+ ctx.check_browser_initialized()
209
+ ctx.browser.switch_to_previous_tab()
210
+
211
+
212
+ @step("I switch to the previous browser tab")
213
+ def switch_to_previous_browser_tab(ctx):
214
+ switch_to_previous_tab(ctx)
215
+
216
+
217
+ @step("I wait to switch to the previous browser tab")
218
+ def wait_to_switch_to_previous_browser_tab(ctx):
219
+ retry(switch_to_previous_tab)(ctx)
220
+
221
+
222
+ def save_downloaded_file(ctx, filename):
223
+ ctx.check_browser_initialized()
224
+
225
+ # use default frame when adding elements to the document to avoid this errro on access:
226
+ # "selenium.common.exceptions.ElementNotInteractableException: Message: element not interactable"
227
+ ctx.browser.switch_to_default_frame()
228
+
229
+ elem = ctx.browser.execute(
230
+ """
231
+ var input = window.document.createElement('INPUT');
232
+ input.setAttribute('type', 'file');
233
+ input.hidden = true;
234
+ input.onchange = function (e) { e.stopPropagation() };
235
+ return window.document.documentElement.appendChild(input);
236
+ """
237
+ )
238
+ cucu_downloads_dir = config.CONFIG["CUCU_BROWSER_DOWNLOADS_DIR"]
239
+ elem.send_keys(f"{cucu_downloads_dir}/{filename}")
240
+ ctx.browser.execute(
241
+ """
242
+ var input = arguments[0];
243
+ window.__cucu_downloaded_file = null;
244
+ var reader = new FileReader();
245
+ reader.onload = function (ev) {
246
+ window.__cucu_downloaded_file = reader.result;
247
+ };
248
+ reader.onerror = function (ex) {
249
+ window.__cucu_downloaded_file = ex.message;
250
+ };
251
+ reader.readAsDataURL(input.files[0]);
252
+ input.remove();
253
+ """,
254
+ elem,
255
+ )
256
+
257
+ def wait_for_file():
258
+ if (
259
+ ctx.browser.execute("return window.__cucu_downloaded_file;")
260
+ is None
261
+ ):
262
+ raise RuntimeError(f"waiting on file {filename}")
263
+
264
+ retry(wait_for_file)()
265
+
266
+ result = ctx.browser.execute("return window.__cucu_downloaded_file;")
267
+ if not result.startswith("data:"):
268
+ raise Exception("Failed to get file content: %s" % result)
269
+
270
+ filedata = base64.b64decode(result[result.find("base64,") + 7 :])
271
+ scenario_downloads_dir = config.CONFIG["SCENARIO_DOWNLOADS_DIR"]
272
+ download_filepath = os.path.join(scenario_downloads_dir, filename)
273
+ open(download_filepath, "wb").write(filedata)
274
+
275
+
276
+ @step('I wait to see the downloaded file "{filename}"')
277
+ def wait_to_see_downloaded_file(ctx, filename):
278
+ """
279
+ wait to see the expected downloaded filename appears in the current
280
+ browsers download directory and internally we then copy the contents of
281
+ that file to the SCENARIO_DOWNLOADS_DIR so the test can continue to
282
+ use the file as it deems necessary.
283
+ """
284
+ retry(save_downloaded_file)(ctx, filename)
285
+
286
+
287
+ @step(
288
+ 'I wait up to "{seconds}" seconds to see the downloaded file "{filename}"'
289
+ )
290
+ def wait_up_to_seconds_to_see_downloaded_file(ctx, seconds, filename):
291
+ seconds = float(seconds)
292
+ retry(save_downloaded_file, wait_up_to_s=seconds)(ctx, filename)
293
+
294
+
295
+ @step('I download an mht archive of the current page to "{file_path}"')
296
+ def download_mht_archive(ctx, file_path):
297
+ ctx.browser.download_mht(file_path)
298
+
299
+
300
+ @step('I run the following steps if the current browser is "{name}"')
301
+ def run_if_browser(ctx, name):
302
+ if config.CONFIG["CUCU_BROWSER"].lower() == name.lower():
303
+ run_steps(ctx, ctx.text)
304
+
305
+
306
+ @step('I do not run the following steps if the current browser is "{name}"')
307
+ def run_if_not_browser(ctx, name):
308
+ if config.CONFIG["CUCU_BROWSER"].lower() != name.lower():
309
+ run_steps(ctx, ctx.text)
310
+
311
+
312
+ @step('I skip this scenario if the current browser is "{name}"')
313
+ def skip_if_browser(ctx, name):
314
+ if config.CONFIG["CUCU_BROWSER"].lower() == name.lower():
315
+ ctx.scenario.skip(reason=f"skipping scenario since we're on {name}")
316
+
317
+
318
+ @step('I skip this scenario if the current browser is not "{name}"')
319
+ def skip_if_not_browser(ctx, name):
320
+ if config.CONFIG["CUCU_BROWSER"].lower() != name.lower():
321
+ ctx.scenario.skip(
322
+ reason=f"skipping scenario since we're not on {name}"
323
+ )
324
+
325
+
326
+ @step('I save the browser cookie "{cookie_name}" to the variable "{variable}"')
327
+ def save_browser_cookie(ctx, cookie_name, variable):
328
+ ctx.check_browser_initialized()
329
+ config.CONFIG[variable] = ctx.browser.driver.get_cookie(cookie_name)[
330
+ "value"
331
+ ]
332
+
333
+
334
+ @step('I set the browser cookie "{name}" a value of "{value}"')
335
+ def add_browser_cookie(ctx, name, value):
336
+ ctx.check_browser_initialized()
337
+ ctx.browser.driver.add_cookie({"name": name, "value": value})
@@ -0,0 +1,91 @@
1
+ from cucu import fuzzy, helpers
2
+ from cucu.utils import take_saw_element_screenshot
3
+
4
+ from . import base_steps
5
+
6
+
7
+ def find_button(ctx, name, index=0):
8
+ """
9
+ find a button on screen by fuzzy matching on the name and index provided.
10
+
11
+ * <button>
12
+ * <input type="button">
13
+ * <input type="submit">
14
+ * <a>
15
+ * <* role="button">
16
+ * <* role="link">
17
+ * <* role="menuitem">
18
+ * <* role="treetem">
19
+ * <* role="option">
20
+ * <* role="radio">
21
+
22
+ note: the reason we're allowing other items such as menuitem, option, etc
23
+ is that on screen they can present themselves like "buttons". When
24
+ searching for more things to include use the following image
25
+ reference:
26
+
27
+ https://www.w3.org/TR/2009/WD-wai-aria-20091215/rdf_model.png
28
+
29
+ parameters:
30
+ ctx(object): behave context object used to share data between steps
31
+ name(str): name that identifies the desired button on screen
32
+ index(str): the index of the button if there are duplicates
33
+
34
+ returns:
35
+ the WebElement that matches the provided arguments.
36
+ """
37
+ ctx.check_browser_initialized()
38
+ element = fuzzy.find(
39
+ ctx.browser,
40
+ name,
41
+ [
42
+ "button",
43
+ 'input[type="button"]',
44
+ 'input[type="submit"]',
45
+ "a",
46
+ '*[role="button"]',
47
+ '*[role="link"]',
48
+ '*[role="menuitem"]',
49
+ '*[role="treeitem"]',
50
+ '*[role="option"]',
51
+ '*[role="radio"]',
52
+ ],
53
+ index=index,
54
+ )
55
+
56
+ take_saw_element_screenshot(ctx, "button", name, index, element)
57
+
58
+ return element
59
+
60
+
61
+ def click_button(ctx, button):
62
+ """
63
+ internal method used to simply click a button element
64
+ """
65
+ ctx.check_browser_initialized()
66
+
67
+ if base_steps.is_disabled(button):
68
+ raise RuntimeError("unable to click the button, as it is disabled")
69
+
70
+ ctx.browser.click(button)
71
+
72
+
73
+ helpers.define_should_see_thing_with_name_steps(
74
+ "button", find_button, with_nth=True
75
+ )
76
+ helpers.define_action_on_thing_with_name_steps(
77
+ "button", "click", find_button, click_button, with_nth=True
78
+ )
79
+ helpers.define_thing_with_name_in_state_steps(
80
+ "button", "disabled", find_button, base_steps.is_disabled, with_nth=True
81
+ )
82
+ helpers.define_thing_with_name_in_state_steps(
83
+ "button",
84
+ "not disabled",
85
+ find_button,
86
+ base_steps.is_not_disabled,
87
+ with_nth=True,
88
+ )
89
+ helpers.define_run_steps_if_I_can_see_element_with_name_steps(
90
+ "button", find_button
91
+ )
@@ -0,0 +1,111 @@
1
+ from cucu import fuzzy, helpers
2
+ from cucu.utils import take_saw_element_screenshot
3
+
4
+ from . import base_steps
5
+
6
+
7
+ def find_checkbox(ctx, name, index=0):
8
+ """
9
+ find a checkbox on screen using name and index provided
10
+
11
+ * <input type="checkbox">
12
+ * <* role="checkbox">
13
+
14
+ parameters:
15
+ ctx(object): behave context object used to share data between steps
16
+ name(str): name that identifies the desired checkbox on screen
17
+ index(str): the index of the checkbox if there are duplicates
18
+
19
+ returns:
20
+ the WebElement that matches the provided arguments.
21
+ """
22
+ ctx.check_browser_initialized()
23
+
24
+ element = fuzzy.find(
25
+ ctx.browser,
26
+ name,
27
+ [
28
+ 'input[type="checkbox"]',
29
+ '*[role="checkbox"]',
30
+ ],
31
+ index=index,
32
+ direction=fuzzy.Direction.RIGHT_TO_LEFT,
33
+ )
34
+
35
+ take_saw_element_screenshot(ctx, "checkbox", name, index, element)
36
+
37
+ return element
38
+
39
+
40
+ def is_checked(checkbox):
41
+ """
42
+ internal method to check a checkbox is checked
43
+ """
44
+ return (
45
+ checkbox.get_attribute("checked")
46
+ or checkbox.get_attribute("aria-checked") == "true"
47
+ )
48
+
49
+
50
+ def is_not_checked(checkbox):
51
+ """
52
+ internal method to check a checkbox is not checked
53
+ """
54
+ return not is_checked(checkbox)
55
+
56
+
57
+ def check_checkbox(ctx, checkbox):
58
+ """
59
+ internal method used to check a checkbox if it is not already checked
60
+ """
61
+ ctx.check_browser_initialized()
62
+
63
+ if is_checked(checkbox):
64
+ raise Exception("checkbox already checked")
65
+
66
+ if base_steps.is_disabled(checkbox):
67
+ raise RuntimeError("unable to check the checkbox, as it is disabled")
68
+
69
+ ctx.browser.click(checkbox)
70
+
71
+
72
+ def uncheck_checkbox(ctx, checkbox):
73
+ """
74
+ internal method used to uncheck a checkbox if it is not already unchecked
75
+ """
76
+ ctx.check_browser_initialized()
77
+
78
+ if is_not_checked(checkbox):
79
+ raise Exception("checkbox already unchecked")
80
+
81
+ ctx.browser.click(checkbox)
82
+
83
+
84
+ helpers.define_should_see_thing_with_name_steps("checkbox", find_checkbox)
85
+ helpers.define_action_on_thing_with_name_steps(
86
+ "checkbox", "check", find_checkbox, check_checkbox
87
+ )
88
+ helpers.define_action_on_thing_with_name_steps(
89
+ "checkbox", "uncheck", find_checkbox, uncheck_checkbox
90
+ )
91
+ helpers.define_thing_with_name_in_state_steps(
92
+ "checkbox", "checked", find_checkbox, is_checked
93
+ )
94
+ helpers.define_thing_with_name_in_state_steps(
95
+ "checkbox", "not checked", find_checkbox, is_not_checked
96
+ )
97
+ helpers.define_thing_with_name_in_state_steps(
98
+ "checkbox", "disabled", find_checkbox, base_steps.is_disabled
99
+ )
100
+ helpers.define_thing_with_name_in_state_steps(
101
+ "checkbox", "not disabled", find_checkbox, base_steps.is_not_disabled
102
+ )
103
+ helpers.define_run_steps_if_I_can_see_element_with_name_steps(
104
+ "checkbox", find_checkbox
105
+ )
106
+ helpers.define_ensure_state_on_thing_with_name_steps(
107
+ "checkbox", "checked", find_checkbox, is_checked, check_checkbox
108
+ )
109
+ helpers.define_ensure_state_on_thing_with_name_steps(
110
+ "checkbox", "not checked", find_checkbox, is_not_checked, uncheck_checkbox
111
+ )