ruby_raider 3.13.0 → 4.0.0

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.
Files changed (225) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/runtime.yml +38 -0
  3. data/.github/workflows/system_tests.yml +1 -23
  4. data/.reek.yml +1 -0
  5. data/.rspec +1 -0
  6. data/Gemfile +2 -0
  7. data/README.md +16 -49
  8. data/RELEASE.md +22 -0
  9. data/bin/raider +5 -0
  10. data/lib/commands/scaffolding_commands.rb +2 -25
  11. data/lib/commands/utility_commands.rb +23 -3
  12. data/lib/generators/automation/automation_generator.rb +0 -5
  13. data/lib/generators/automation/templates/component.tt +1 -0
  14. data/lib/generators/automation/templates/home.tt +2 -2
  15. data/lib/generators/automation/templates/login.tt +6 -26
  16. data/lib/generators/automation/templates/page.tt +23 -18
  17. data/lib/generators/automation/templates/partials/home_page_selector.tt +1 -2
  18. data/lib/generators/automation/templates/partials/initialize_selector.tt +0 -8
  19. data/lib/generators/automation/templates/partials/pdp_page_selector.tt +1 -2
  20. data/lib/generators/automation/templates/partials/url_methods.tt +8 -2
  21. data/lib/generators/automation/templates/partials/visit_method.tt +0 -5
  22. data/lib/generators/automation/templates/pdp.tt +1 -9
  23. data/lib/generators/common_generator.rb +0 -2
  24. data/lib/generators/generator.rb +12 -19
  25. data/lib/generators/helper_generator.rb +21 -19
  26. data/lib/generators/infrastructure/templates/azure_pipelines.tt +1 -1
  27. data/lib/generators/infrastructure/templates/github.tt +2 -3
  28. data/lib/generators/infrastructure/templates/github_appium.tt +0 -8
  29. data/lib/generators/invoke_generators.rb +4 -23
  30. data/lib/generators/menu_generator.rb +1 -9
  31. data/lib/generators/rspec/templates/accessibility_spec.tt +1 -9
  32. data/lib/generators/rspec/templates/spec.tt +8 -24
  33. data/lib/generators/template_renderer/partial_cache.rb +3 -1
  34. data/lib/generators/template_renderer.rb +1 -1
  35. data/lib/generators/templates/common/gemfile.tt +3 -10
  36. data/lib/generators/templates/common/git_ignore.tt +0 -5
  37. data/lib/generators/templates/common/partials/mobile_config.tt +3 -1
  38. data/lib/generators/templates/common/partials/web_config.tt +10 -1
  39. data/lib/generators/templates/common/rakefile.tt +0 -15
  40. data/lib/generators/templates/common/read_me.tt +357 -250
  41. data/lib/generators/templates/common/rubocop.tt +0 -2
  42. data/lib/generators/templates/helpers/driver_helper.tt +8 -0
  43. data/lib/generators/templates/helpers/gesture_helper.tt +78 -0
  44. data/lib/generators/templates/helpers/network_helper.tt +521 -0
  45. data/lib/generators/templates/helpers/partials/quit_driver.tt +0 -2
  46. data/lib/generators/templates/helpers/partials/screenshot.tt +0 -2
  47. data/lib/generators/templates/helpers/partials/selenium_driver.tt +5 -2
  48. data/lib/generators/templates/helpers/screenshot_helper.tt +490 -0
  49. data/lib/generators/templates/helpers/spec_helper.tt +39 -29
  50. data/lib/generators/templates/helpers/trace_helper.tt +240 -95
  51. data/lib/generators/templates/helpers/video_helper.tt +3 -8
  52. data/lib/ruby_raider/runtime/actionability.rb +129 -0
  53. data/lib/ruby_raider/runtime/actionable.rb +49 -0
  54. data/lib/ruby_raider/runtime/by_role.rb +55 -0
  55. data/lib/ruby_raider/runtime/config.rb +79 -0
  56. data/lib/ruby_raider/runtime/context.rb +38 -0
  57. data/lib/ruby_raider/runtime/errors.rb +69 -0
  58. data/lib/ruby_raider/runtime/finders.rb +73 -0
  59. data/lib/ruby_raider/runtime/js/actionability.js +134 -0
  60. data/lib/ruby_raider/runtime/js/role_engine.js +315 -0
  61. data/lib/ruby_raider/runtime/locator_converter.rb +72 -0
  62. data/lib/ruby_raider/runtime/matchers/base.rb +222 -0
  63. data/lib/ruby_raider/runtime/matchers/element.rb +183 -0
  64. data/lib/ruby_raider/runtime/matchers/page.rb +71 -0
  65. data/lib/ruby_raider/runtime/matchers/presence.rb +41 -0
  66. data/lib/ruby_raider/runtime/matchers/retrying_block.rb +104 -0
  67. data/lib/ruby_raider/runtime/observer.rb +170 -0
  68. data/lib/ruby_raider/runtime/role_finder.rb +83 -0
  69. data/lib/ruby_raider/runtime/rspec.rb +78 -0
  70. data/lib/ruby_raider/runtime/selectors.rb +60 -0
  71. data/lib/ruby_raider/runtime/strict.rb +142 -0
  72. data/lib/ruby_raider/runtime/wait.rb +84 -0
  73. data/lib/ruby_raider/runtime.rb +20 -0
  74. data/lib/scaffolding/name_normalizer.rb +1 -1
  75. data/lib/scaffolding/project_detector.rb +2 -28
  76. data/lib/scaffolding/scaffolding.rb +0 -20
  77. data/lib/scaffolding/templates/component.tt +0 -14
  78. data/lib/scaffolding/templates/page_object.tt +2 -14
  79. data/lib/scaffolding/templates/spec.tt +1 -9
  80. data/lib/utilities/utilities.rb +10 -8
  81. data/lib/version +1 -1
  82. data/ruby_raider.gemspec +2 -1
  83. data/sig/commands/scaffolding_commands.rbs +0 -2
  84. data/sig/commands/utility_commands.rbs +6 -0
  85. data/sig/generators/generator.rbs +0 -4
  86. data/sig/generators/helper_generator.rbs +0 -1
  87. data/sig/generators/menu_generator.rbs +0 -1
  88. data/sig/ruby_raider/runtime/actionability.rbs +25 -0
  89. data/sig/ruby_raider/runtime/actionable.rbs +13 -0
  90. data/sig/ruby_raider/runtime/by_role.rbs +15 -0
  91. data/sig/ruby_raider/runtime/config.rbs +26 -0
  92. data/sig/ruby_raider/runtime/context.rbs +12 -0
  93. data/sig/ruby_raider/runtime/errors.rbs +36 -0
  94. data/sig/ruby_raider/runtime/finders.rbs +26 -0
  95. data/sig/ruby_raider/runtime/locator_converter.rbs +21 -0
  96. data/sig/ruby_raider/runtime/matchers/base.rbs +64 -0
  97. data/sig/ruby_raider/runtime/matchers/element.rbs +78 -0
  98. data/sig/ruby_raider/runtime/matchers/page.rbs +38 -0
  99. data/sig/ruby_raider/runtime/matchers/presence.rbs +21 -0
  100. data/sig/ruby_raider/runtime/matchers/retrying_block.rbs +27 -0
  101. data/sig/ruby_raider/runtime/observer.rbs +46 -0
  102. data/sig/ruby_raider/runtime/role_finder.rbs +18 -0
  103. data/sig/ruby_raider/runtime/selectors.rbs +21 -0
  104. data/sig/ruby_raider/runtime/strict.rbs +35 -0
  105. data/sig/ruby_raider/runtime/wait.rbs +17 -0
  106. data/sig/ruby_raider/runtime.rbs +2 -0
  107. data/sig/scaffolding/project_detector.rbs +0 -4
  108. data/sig/scaffolding/scaffolding.rbs +0 -5
  109. data/sig/utilities/utilities.rbs +2 -1
  110. data/sig/vendor/selenium_webdriver.rbs +71 -0
  111. data/spec/commands/raider_commands_spec.rb +38 -3
  112. data/spec/commands/scaffolding_commands_spec.rb +27 -0
  113. data/spec/commands/utility_commands_spec.rb +53 -0
  114. data/spec/generators/gesture_helper_spec.rb +123 -0
  115. data/spec/generators/network_helper_spec.rb +634 -0
  116. data/spec/generators/screenshot_helper_spec.rb +776 -0
  117. data/spec/generators/template_renderer_spec.rb +0 -4
  118. data/spec/generators/trace_helper_spec.rb +489 -97
  119. data/spec/generators/video_helper_spec.rb +0 -9
  120. data/spec/integration/commands/browser_update_after_creation_spec.rb +0 -8
  121. data/spec/integration/commands/scaffolding_commands_spec.rb +12 -70
  122. data/spec/integration/commands/utility_commands_spec.rb +19 -41
  123. data/spec/integration/content/azure_content_spec.rb +2 -6
  124. data/spec/integration/content/ci_content_spec.rb +1 -55
  125. data/spec/integration/content/common_content_spec.rb +1 -118
  126. data/spec/integration/content/config_content_spec.rb +34 -31
  127. data/spec/integration/content/gemfile_content_spec.rb +7 -72
  128. data/spec/integration/content/helper_content_spec.rb +105 -99
  129. data/spec/integration/content/page_content_spec.rb +85 -87
  130. data/spec/integration/content/readme_content_spec.rb +134 -75
  131. data/spec/integration/content/rspec_reporter_content_spec.rb +2 -2
  132. data/spec/integration/content/rubocop_content_spec.rb +1 -1
  133. data/spec/integration/content/syntax_validation_spec.rb +2 -2
  134. data/spec/integration/content/test_content_spec.rb +23 -147
  135. data/spec/integration/end_to_end_features_spec.rb +1 -43
  136. data/spec/integration/end_to_end_spec.rb +319 -326
  137. data/spec/integration/generators/automation_generator_spec.rb +3 -37
  138. data/spec/integration/generators/axe_addon_spec.rb +2 -35
  139. data/spec/integration/generators/azure_generator_spec.rb +1 -1
  140. data/spec/integration/generators/common_generator_spec.rb +1 -40
  141. data/spec/integration/generators/config_features_spec.rb +1 -24
  142. data/spec/integration/generators/github_generator_spec.rb +0 -12
  143. data/spec/integration/generators/helpers_generator_spec.rb +74 -80
  144. data/spec/integration/generators/invoke_generators_spec.rb +54 -45
  145. data/spec/integration/generators/rspec_generator_spec.rb +0 -6
  146. data/spec/integration/scaffolding_e2e_spec.rb +17 -179
  147. data/spec/integration/spec_helper.rb +4 -6
  148. data/spec/integration/trace_format_checker.rb +11 -2
  149. data/spec/integration/trace_page_server.rb +72 -5
  150. data/spec/menus/menu_generator_spec.rb +14 -39
  151. data/spec/runtime/actionability_spec.rb +207 -0
  152. data/spec/runtime/actionable_spec.rb +125 -0
  153. data/spec/runtime/browser/actionability_script_browser_spec.rb +150 -0
  154. data/spec/runtime/browser/actionable_browser_spec.rb +98 -0
  155. data/spec/runtime/browser/finders_browser_spec.rb +148 -0
  156. data/spec/runtime/browser/matchers_browser_spec.rb +67 -0
  157. data/spec/runtime/browser/retrying_block_browser_spec.rb +37 -0
  158. data/spec/runtime/browser/role_engine_browser_spec.rb +184 -0
  159. data/spec/runtime/browser/strict_browser_spec.rb +38 -0
  160. data/spec/runtime/by_role_spec.rb +52 -0
  161. data/spec/runtime/config_spec.rb +79 -0
  162. data/spec/runtime/context_spec.rb +73 -0
  163. data/spec/runtime/errors_spec.rb +89 -0
  164. data/spec/runtime/finders_spec.rb +165 -0
  165. data/spec/runtime/fixtures/actionability.html +70 -0
  166. data/spec/runtime/fixtures/locators.html +46 -0
  167. data/spec/runtime/fixtures/matchers.html +33 -0
  168. data/spec/runtime/fixtures/roles.html +162 -0
  169. data/spec/runtime/matchers/base_spec.rb +165 -0
  170. data/spec/runtime/matchers/element_spec.rb +277 -0
  171. data/spec/runtime/matchers/page_spec.rb +102 -0
  172. data/spec/runtime/matchers/presence_spec.rb +59 -0
  173. data/spec/runtime/matchers/retrying_block_spec.rb +165 -0
  174. data/spec/runtime/observer_spec.rb +341 -0
  175. data/spec/runtime/role_finder_spec.rb +156 -0
  176. data/spec/runtime/role_query_spec.rb +75 -0
  177. data/spec/runtime/selectors_spec.rb +135 -0
  178. data/spec/runtime/strict_spec.rb +257 -0
  179. data/spec/runtime/support/browser.rb +48 -0
  180. data/spec/runtime/support/fake_bridge.rb +117 -0
  181. data/spec/runtime/wait_spec.rb +71 -0
  182. data/spec/scaffolding/name_normalizer_spec.rb +3 -2
  183. data/spec/scaffolding/scaffold_project_detector_spec.rb +4 -43
  184. data/spec/system/selenium_spec.rb +1 -5
  185. data/spec/system/support/system_test_helper.rb +2 -5
  186. data/spec/utilities/headless_config_spec.rb +0 -7
  187. data/spec/utilities/utilities_spec.rb +24 -0
  188. metadata +113 -48
  189. data/lib/generators/automation/templates/header.tt +0 -9
  190. data/lib/generators/cucumber/cucumber_generator.rb +0 -58
  191. data/lib/generators/cucumber/templates/accessibility_feature.tt +0 -5
  192. data/lib/generators/cucumber/templates/accessibility_steps.tt +0 -17
  193. data/lib/generators/cucumber/templates/cucumber.tt +0 -1
  194. data/lib/generators/cucumber/templates/env.tt +0 -7
  195. data/lib/generators/cucumber/templates/feature.tt +0 -21
  196. data/lib/generators/cucumber/templates/partials/appium_env.tt +0 -26
  197. data/lib/generators/cucumber/templates/partials/driver_world.tt +0 -18
  198. data/lib/generators/cucumber/templates/partials/mobile_steps.tt +0 -20
  199. data/lib/generators/cucumber/templates/partials/selenium_env.tt +0 -120
  200. data/lib/generators/cucumber/templates/partials/watir_env.tt +0 -48
  201. data/lib/generators/cucumber/templates/partials/watir_world.tt +0 -4
  202. data/lib/generators/cucumber/templates/partials/web_steps.tt +0 -28
  203. data/lib/generators/cucumber/templates/steps.tt +0 -5
  204. data/lib/generators/cucumber/templates/user_factory.tt +0 -15
  205. data/lib/generators/cucumber/templates/world.tt +0 -7
  206. data/lib/generators/templates/helpers/browser_helper.tt +0 -31
  207. data/lib/generators/templates/helpers/raider_element.tt +0 -159
  208. data/lib/generators/templates/helpers/wait_helper.tt +0 -611
  209. data/lib/scaffolding/templates/feature.tt +0 -6
  210. data/lib/scaffolding/templates/steps.tt +0 -19
  211. data/sig/generators/cucumber/cucumber_generator.rbs +0 -12
  212. data/spec/fixtures/watir_master/cucumber_watir/Readme.md.snapshot +0 -175
  213. data/spec/fixtures/watir_master/cucumber_watir/features/step_definitions/login_steps.rb.snapshot +0 -24
  214. data/spec/fixtures/watir_master/cucumber_watir/page_objects/abstract/page.rb.snapshot +0 -37
  215. data/spec/fixtures/watir_master/cucumber_watir/page_objects/pages/login.rb.snapshot +0 -33
  216. data/spec/fixtures/watir_master/rspec_watir/Readme.md.snapshot +0 -174
  217. data/spec/fixtures/watir_master/rspec_watir/page_objects/abstract/page.rb.snapshot +0 -37
  218. data/spec/fixtures/watir_master/rspec_watir/page_objects/pages/login.rb.snapshot +0 -33
  219. data/spec/fixtures/watir_master/rspec_watir/spec/login_page_spec.rb.snapshot +0 -49
  220. data/spec/integration/content/cucumber_reporter_content_spec.rb +0 -161
  221. data/spec/integration/generators/cucumber_generator_spec.rb +0 -100
  222. data/spec/integration/helpers/wait_helper_mobile_spec.rb +0 -281
  223. data/spec/integration/helpers/wait_helper_spec.rb +0 -679
  224. data/spec/integration/helpers/wait_helper_support.rb +0 -183
  225. data/spec/system/watir_spec.rb +0 -38
@@ -19,10 +19,6 @@
19
19
  <p align="center"> For more information and updates on releases, see <a href="https://ruby-raider.com">https://ruby-raider.com</a></p>
20
20
  </div>
21
21
 
22
- <%- if watir? -%>
23
- > **Deprecated:** Watir is deprecated and will be removed in the next major version of ruby_raider; new projects should use Selenium. Watir is only available through `-p automation:watir`. See [Migrating from Watir](https://github.com/RaiderHQ/ruby_raider#migrating-from-watir).
24
-
25
- <%- end -%>
26
22
  ## What is Ruby Raider?
27
23
 
28
24
  Ruby Raider is a generator and scaffolding gem to make UI test automation easier.
@@ -31,11 +27,8 @@ Ruby Raider is a generator and scaffolding gem to make UI test automation easier
31
27
 
32
28
  | Web Testing Framework | Mobile Testing Framework |
33
29
  |-----------------------|------------------------------------|
34
- | Cucumber and Selenium | Cucumber and Appium for iOS |
35
30
  | RSpec and Selenium | RSpec and Appium for iOS |
36
- | Cucumber and Watir | Cucumber and Appium for Android |
37
- | RSpec and Watir | RSpec and Appium for Android |
38
- | | Cucumber and Appium Cross-platform |
31
+ | | RSpec and Appium for Android |
39
32
  | | RSpec and Appium Cross-platform |
40
33
 
41
34
  **Optional add-on for web projects:**
@@ -81,13 +74,10 @@ Shortcuts: `g` (generate), `n` (new), `u` (utility), `v` (version)
81
74
  ```
82
75
  raider g page [NAME] # Create page object
83
76
  raider g spec [NAME] # Create RSpec test
84
- raider g feature [NAME] # Create Cucumber feature
85
- raider g steps [NAME] # Create step definitions
86
- raider g scaffold [NAME] # Create page + test + steps
77
+ raider g scaffold [NAME] # Create page + test
87
78
  ```
88
79
 
89
- <%- if wait_helper? -%>
90
- <%- test_cmd = rspec? ? 'rspec' : 'cucumber' -%>
80
+ <%- test_cmd = 'rspec' -%>
91
81
  ## How the framework works
92
82
 
93
83
  This section walks through the project: where things live, how a test finds an element and acts on it, and how to
@@ -98,7 +88,7 @@ run the tests.
98
88
 
99
89
  This project was generated without example tests, so its <%= mobile? ? 'pages' : 'pages, components' %> and tests folders are empty.
100
90
  Grow it with the scaffolding commands above: `raider g scaffold checkout` writes a `Checkout` page and its
101
- <%= rspec? ? 'spec' : 'feature and steps' %>, and `raider g page`<%= rspec? ? ' and `raider g spec`' : ', `raider g feature` and `raider g steps`' %> write one file at a time. The code examples
91
+ spec, and `raider g page` and `raider g spec` write one file at a time. The code examples
102
92
  below show what goes in them.
103
93
  <%- end -%>
104
94
 
@@ -118,58 +108,38 @@ languages call a function (it starts with `def` and ends with `end`).
118
108
  - `page_objects/pages/`: one class per screen of the app, empty for now. A page holds the locators of its elements
119
109
  and the actions a test can do on that screen.
120
110
  <%- end -%>
121
- - `page_objects/abstract/`: `page.rb`, the base class every page builds on. It gives each page the wait helper.
122
- - `helpers/`: `driver_helper.rb` starts the Appium session, `wait_helper.rb` and `raider_element.rb` are the wait
123
- helper, and `video_helper.rb` records the screen<%= cross_platform? ? '. `appium_helper.rb` has `element(ios: …, android: …)`, a lookup that does not wait' : '' %>.
124
- <%- if rspec? -%>
111
+ - `page_objects/abstract/`: `page.rb`, the base class every page builds on. It requires the Raider runtime and includes
112
+ `gesture_helper.rb`.
113
+ - `helpers/`: `driver_helper.rb` starts the Appium session, `gesture_helper.rb` has `swipe`, `scroll` and `scroll_to`,
114
+ and `video_helper.rb` records the screen<%= cross_platform? ? '. `appium_helper.rb` has `element(ios: …, android: …)`, a lookup for both platforms' : '' %>.
125
115
  `spec_helper.rb` sets up RSpec: it starts the app before each test and closes it after.
126
116
  <%- if examples? -%>
127
117
  - `spec/`: the tests. `pdp_page_spec.rb` opens a product and checks its "Add to Cart" button.
128
118
  <%- else -%>
129
119
  - `spec/`: the tests, empty for now.
130
120
  <%- end -%>
131
- <%- elsif examples? -%>
132
- - `features/`: the tests. `home.feature` describes them in plain English, `step_definitions/home_steps.rb` holds the
133
- Ruby code behind each step, and `support/` starts the app (`env.rb`) and makes the helpers available to the steps
134
- (`world.rb`).
135
- <%- else -%>
136
- - `features/`: the tests, empty for now. A `.feature` file describes them in plain English, `step_definitions/` holds
137
- the Ruby code behind each step, and `support/` starts the app (`env.rb`) and makes the helpers available to the
138
- steps (`world.rb`).
139
- <%- end -%>
140
121
  <%- else -%>
141
122
  `config.yml` is described under "Settings" below.
142
123
  <%- if examples? -%>
143
124
  - `page_objects/pages/`: one class per page of the site. `login.rb` is the login page and `account.rb` the account
144
125
  page. A page holds the locators of its elements and the actions a test can do on that page.
145
- - `page_objects/components/`: parts that appear on many pages. `header.rb` reads the welcome message at the top.
126
+ - `page_objects/components/`: parts that appear on many pages. Create one with `raider g component`.
146
127
  <%- else -%>
147
128
  - `page_objects/pages/`: one class per page of the site, empty for now. A page holds the locators of its elements
148
129
  and the actions a test can do on that page.
149
- - `page_objects/components/`: parts that appear on many pages, such as a header, empty for now.
130
+ - `page_objects/components/`: parts that appear on many pages. Create one with `raider g component`.
150
131
  <%- end -%>
151
132
  - `page_objects/abstract/`: `page.rb` and `component.rb`, the base classes every page and component builds on.
152
- `page.rb` gives each page the wait helper.
153
- - `helpers/`: `driver_helper.rb` starts the browser, `wait_helper.rb` and `raider_element.rb` are the wait helper,
154
- `video_helper.rb` records failure videos and `session_state.rb` reuses the login.
155
- <%- if rspec? -%>
156
- `spec_helper.rb` sets up RSpec: it opens the browser before each test and closes it after.
157
- <%- end -%>
133
+ `page.rb` requires the Raider runtime and gives each page `by_role`.
134
+ - `helpers/`: `driver_helper.rb` starts the browser, `video_helper.rb` records failure videos and `session_state.rb`
135
+ reuses the login. `spec_helper.rb` sets up RSpec: it opens the browser before each test and closes it after.
158
136
  <%- if examples? -%>
159
- - `models/`: test data. `data/users.yml` holds the users, and `model_factory.rb`<%= cucumber? ? ' and `user_factory.rb` load' : ' loads' %> them.
137
+ - `models/`: test data. `data/users.yml` holds the users, and `model_factory.rb` loads them.
160
138
  <%- end -%>
161
- <%- if rspec? && examples? -%>
139
+ <%- if examples? -%>
162
140
  - `spec/`: the tests. `login_page_spec.rb` logs in with the right password and with a wrong one.
163
- <%- elsif rspec? -%>
164
- - `spec/`: the tests, empty for now.
165
- <%- elsif examples? -%>
166
- - `features/`: the tests. `login.feature` describes them in plain English, `step_definitions/login_steps.rb` holds
167
- the Ruby code behind each step, and `support/` opens the browser (`env.rb`) and makes the helpers available to the
168
- steps (`world.rb`).
169
141
  <%- else -%>
170
- - `features/`: the tests, empty for now. A `.feature` file describes them in plain English, `step_definitions/`
171
- holds the Ruby code behind each step, and `support/` opens the browser (`env.rb`) and makes the helpers available
172
- to the steps (`world.rb`).
142
+ - `spec/`: the tests, empty for now.
173
143
  <%- end -%>
174
144
  <%- end -%>
175
145
  <%- if azure? && !mobile? -%>
@@ -197,15 +167,20 @@ The `browserstack` part is used instead when you run with `TEST_ENV=browserstack
197
167
  `config/config.yml`:
198
168
 
199
169
  - `platform`: which part of `capabilities.yml` to use, `ios` or `android`.
200
- - `timeout`: how many seconds the wait helper waits for an element before the test fails (10). To wait longer,
170
+ - `timeout`: how many seconds finds, actions and assertions wait before the test fails (10). To wait longer,
201
171
  change it, for example to `timeout: 15`.
172
+ - `strict` (`true`) and `actionability` (`true`): see [Waiting](#waiting). `raider u strict off` and
173
+ `raider u actionability off` set them. `RAIDER_TIMEOUT`, `RAIDER_STRICT` and `RAIDER_ACTIONABILITY` override the file for one run.
202
174
  - `video`: failure videos, see [Video replay](#video-replay).
203
175
  <%- else -%>
204
- This project has no `config/config.yml`, so the wait helper waits up to 10 seconds for an element before the test
205
- fails. To change that, pass `timeout:` to one call (see below), or create `config/config.yml` with this line:
176
+ This project has no `config/config.yml`. The runtime waits up to 10 seconds, with strict and actionability on. To change
177
+ that for one run, set `RAIDER_TIMEOUT`, `RAIDER_STRICT` or `RAIDER_ACTIONABILITY`, for example `RAIDER_TIMEOUT=15`. To
178
+ change it for the project, create `config/config.yml` with the keys you want:
206
179
 
207
180
  ```yaml
208
181
  timeout: 15
182
+ strict: true
183
+ actionability: true
209
184
  ```
210
185
  <%- end -%>
211
186
  <%- else -%>
@@ -218,8 +193,13 @@ timeout: 15
218
193
  <%- else -%>
219
194
  - `url`: the address of the site under test. Each page adds its own path to it. Change it to your site.
220
195
  <%- end -%>
221
- - `timeout`: how many seconds the wait helper waits for an element before the test fails (10).
222
- - `viewport`: the browser window's width and height.
196
+ - `timeout`: how many seconds finds, actions and assertions wait before the test fails (10).
197
+ - `strict`: `true` makes `find_element` fail when a locator matches more than one element. `false` returns the first.
198
+ `raider u strict off` sets it.
199
+ - `actionability`: `true` makes actions wait until the element is ready, see
200
+ [Waiting and actionability](#waiting-and-actionability). `raider u actionability off` sets it.
201
+ `RAIDER_TIMEOUT`, `RAIDER_STRICT` and `RAIDER_ACTIONABILITY` override the file for one run.
202
+ - `viewport`: the page viewport's width and height in CSS pixels.
223
203
  - `browser_arguments`: extra options passed to each browser when it starts.
224
204
  - `headless`: not in the file by default. Add `headless: true`, or set `HEADLESS=true` for one run, to run the
225
205
  browser without a window.
@@ -242,203 +222,243 @@ with it. For example, on a <%= mobile? ? 'product list screen' : 'login page' %>
242
222
  ```ruby
243
223
  <%- if mobile? -%>
244
224
  def go_to_product_detail
245
- tap(first_product)
225
+ first_product.click
246
226
  end
247
227
 
248
228
  def first_product
249
229
  <%- if cross_platform? -%>
250
- { ios: { accessibility_id: 'product-item-1' },
251
- android: { accessibility_id: 'product-item-1' } }
230
+ element({ ios: { accessibility_id: 'product-item-1' }, android: { accessibility_id: 'product-item-1' } })
252
231
  <%- else -%>
253
- { accessibility_id: 'product-item-1' }
232
+ driver.find_element(accessibility_id: 'product-item-1')
254
233
  <%- end -%>
255
234
  end
256
235
  <%- else -%>
257
236
  def login(username, password)
258
- send_keys(username_field, username)
259
- send_keys(password_field, password)
260
- click(login_button)
237
+ username_field.send_keys(username)
238
+ password_field.send_keys(password)
239
+ login_button.click
261
240
  end
262
241
 
263
242
  def username_field
264
- { id: 'loginFrm_loginname' }
243
+ driver.find_element(id: 'loginFrm_loginname')
265
244
  end
266
245
  <%- end -%>
267
246
  ```
268
247
 
248
+ Pages call <%= mobile? ? 'Appium' : 'Selenium' %> directly: `driver.find_element` finds an element and `click` and `send_keys` act on it.
249
+ The Raider runtime (`ruby_raider/runtime`) makes them wait. `helpers/driver_helper.rb` and `page_objects/abstract/page.rb`
250
+ require it, so it is loaded before the driver exists.
269
251
  <%- if mobile? -%>
270
- An element method returns a **locator**: a Ruby hash (a key and a value in curly braces) that tells Appium how to
271
- find the element. `{ accessibility_id: 'product-item-1' }` means "the element whose accessibility id is
272
- `product-item-1`". `xpath`, `id` and `class_name` work too.
273
252
  <%- if cross_platform? -%>
274
253
 
275
- Because this project runs on both iOS and Android, a locator can name one for each: `{ ios: { … }, android: { … } }`.
276
- The wait helper picks the one for the device the test runs on.
254
+ Because this project runs on both iOS and Android, a locator can name one for each: `element(ios: { … }, android: { … })`
255
+ from `helpers/appium_helper.rb` picks the one for the device the test runs on. It calls `driver.find_element`, so it waits
256
+ like any other find.
277
257
  <%- end -%>
278
-
279
- The action, `tap(first_product)`, comes from the wait helper: it waits until the product is ready, then taps it.
280
258
  <%- else -%>
281
- An element method returns a **locator**: a Ruby hash (a key and a value in curly braces) that tells Selenium how to
282
- find the element. `{ id: 'loginFrm_loginname' }` means "the element whose id is `loginFrm_loginname`". `css`,
283
- `xpath`, `name`, `class` and `link_text` work too.
284
-
285
- The actions, `send_keys` and `click`, come from the wait helper: each one waits until the element is ready, then
286
- types or clicks.
287
259
 
288
260
  <%- if examples? -%>
289
- A **component** is a part of the page that has its own methods. `page.rb` finds the welcome message and hands it to
290
- the `Header` component:
261
+ A **component** is a part of the page that has its own methods. It builds on `page_objects/abstract/component.rb`. This
262
+ project has none yet; add one with `raider g component`.
291
263
  <%- else -%>
292
- A **component** is a part of the page that has its own methods. It builds on `page_objects/abstract/component.rb`,
293
- and a page hands it the element it wraps. For a `Header` component in `page_objects/components/`:
264
+ A **component** is a part of the page that has its own methods. It builds on `page_objects/abstract/component.rb`, which
265
+ keeps the element it wraps as `component`, so `component.find_element(...)` searches inside it.
294
266
  <%- end -%>
267
+ <%- end -%>
268
+
269
+ ### Locators
270
+
271
+ <%- if mobile? -%>
272
+ An element method returns a **locator**: a Ruby hash (a key and a value in curly braces) that tells Appium how to find
273
+ the element. `{ accessibility_id: 'product-item-1' }` means "the element whose accessibility id is `product-item-1`".
274
+ These keys work on an app: `accessibility_id:`, `id:`, `xpath:` and `class_name:`.
295
275
 
296
276
  ```ruby
297
- def header
298
- Header.new(find_element(css: '.welcome-msg'))
299
- end
277
+ driver.find_element(accessibility_id: 'product-item-1')
278
+ driver.find_elements(accessibility_id: 'product-item-1')
300
279
  ```
301
280
 
302
- `Header#customer_name` then reads its text, so a test can write `account_page.header.customer_name`.
303
- <%- end -%>
281
+ `find_element` returns the first match. `find_elements` returns every match.
282
+ <%- else -%>
283
+ An element method returns a **locator**: a Ruby hash (a key and a value in curly braces) that tells Selenium how to find
284
+ the element. `{ id: 'loginFrm_loginname' }` means "the element whose id is `loginFrm_loginname`". Selenium's own keys
285
+ work: `id:`, `css:`, `xpath:`, `name:`, `class:` and `link_text:`. Raider adds the ones people see on the page:
304
286
 
305
- ### The wait helper and RaiderElement
287
+ | Key | Finds | Example |
288
+ |-----|-------|---------|
289
+ | `label:` | the field with that label text, `aria-label` or `aria-labelledby` | `driver.find_element(label: 'Email')` |
290
+ | `placeholder:` | the field with that placeholder | `driver.find_element(placeholder: 'Search')` |
291
+ | `alt:` | the element with that `alt` text, such as an image | `driver.find_element(alt: 'Logo')` |
292
+ | `title:` | the element with that `title` | `driver.find_element(title: 'Close')` |
293
+ | `text:` | the innermost element whose text contains it | `driver.find_element(text: 'Sign in')` |
294
+ | `test_id:` | the element whose `data-testid` equals it | `driver.find_element(test_id: 'submit')` |
306
295
 
307
- <%= mobile? ? 'Apps' : 'Pages' %> load at their own speed, so a test that acts too early fails. The wait helper,
308
- `helpers/wait_helper.rb`, waits for an element to be ready before it acts. It checks every 0.1 seconds, for up to
309
- `timeout` seconds. If the element is still not ready, the test fails and says what it saw last:
296
+ `label:`, `placeholder:`, `alt:`, `title:` and `text:` match a part of the text, ignoring extra spaces. For a match of the
297
+ whole text, pass the value and `{ exact: true }` in an array:
310
298
 
311
- ```
312
- <%- if mobile? -%>
313
- Waited 10s for {accessibility_id: "product-item-1"} to be visible, enabled and stable; last seen: not found
314
- <%- else -%>
315
- Waited 10s for {id: "loginFrm_loginname"} to be visible and enabled; last seen: present but not visible
316
- <%- end -%>
299
+ ```ruby
300
+ driver.find_element(label: ['Email', { exact: true }])
317
301
  ```
318
302
 
319
- You can use its methods in every page and in every <%= rspec? ? 'test' : 'step definition' %>:
303
+ `test_id:` reads `data-testid`. If your app uses another attribute, set `Raider.test_id_attribute = 'data-qa'` or
304
+ `RAIDER_TEST_ID_ATTRIBUTE=data-qa`.
320
305
 
321
- | Method | Waits until the element is | Then |
322
- |--------|----------------------------|------|
323
- | `find_element(locator)` | present and visible (`visible: false` skips visible) | returns it as a RaiderElement |
324
- | `find_elements(locator)` | present, at least one | returns a list of RaiderElements, or `[]` if none showed up |
325
- <%- if mobile? -%>
326
- | `click(locator)`, `tap(locator)` | visible, enabled and still | taps it (`tap` and `click` do the same) |
327
- | `send_keys(locator, 'text')` | visible and enabled | types the text |
328
- | `clear(locator)` | visible and enabled | empties the field |
329
- | `swipe('left', locator)` | visible | swipes on it: `'up'`, `'down'`, `'left'` or `'right'` |
330
- | `scroll('down', locator)` | visible | scrolls inside it |
331
- | `scroll_to(locator)` | visible, scrolling down up to 10 times | returns it as a RaiderElement |
306
+ `by_role` finds an element the way a person or a screen reader does, by its ARIA role and its name.
307
+ <%- if examples? -%>
308
+ `login.rb` uses it for the button:
332
309
  <%- else -%>
333
- | `click(locator)` | visible, enabled and still | clicks it |
334
- | `send_keys(locator, 'text')` | visible and enabled | types the text |
335
- | `clear(locator)` | visible and enabled | empties the field |
336
- | `submit(locator)` | visible and enabled | submits its form |
337
- | `select_by(locator, :text, 'Blue')` | visible and enabled | picks a dropdown option by `:text`, `:value` or `:index` |
338
- | `hover(locator)` | visible and still | moves the mouse over it |
339
- | `drag_and_drop(source, target)` | source visible, enabled and still; target visible | drags the source onto the target |
340
- | `scroll_into_view(locator)` | present | scrolls the page to it |
310
+ For example, a button:
341
311
  <%- end -%>
342
- | `text(locator)`, `attribute(locator, 'name')` | present | returns its text or the attribute's value |
343
- | `displayed?(locator)`, `enabled?(locator)`, `selected?(locator)` | present | returns `true` or `false` |
344
- | `eventually { … }` | (see below) | retries the block until it passes |
345
312
 
346
- Good to know:
313
+ ```ruby
314
+ driver.find_element(by_role('button', name: 'Login'))
315
+ ```
347
316
 
348
- <%- if mobile? -%>
349
- - Write the locator with or without braces: `tap(accessibility_id: 'login')` and `tap({ accessibility_id: 'login' })`
350
- are the same. When a method takes more than the locator, put the locator in braces:
351
- `send_keys({ accessibility_id: 'email' }, 'me@example.com')`, `swipe('left', { accessibility_id: 'card' })`.
352
- <%- else -%>
353
- - Write the locator with or without braces: `click(id: 'login')` and `click({ id: 'login' })` are the same. When a
354
- method takes more than the locator, put the locator in braces: `send_keys({ id: 'email' }, 'me@example.com')`.
355
- <%- end -%>
356
- - `timeout:` changes the wait for one call, in seconds: `<%= mobile? ? "tap({ accessibility_id: 'slow-button' }, timeout: 30)" : "click({ id: 'slow-button' }, timeout: 30)" %>`.
357
- - "Still" (`stable` in error messages) means the element was in the same place on two checks in a row, 0.1 seconds
358
- apart, so it is not in the middle of an animation. That is why <%= mobile? ? '`click` and `tap`' : '`click`, `hover` and `drag_and_drop`' %> need at least two checks, and `timeout: 0` never passes for them.
359
- - `text`, `displayed?` and the other readers fail when the element never shows up. To check that something is
360
- **not** on the <%= mobile? ? 'screen' : 'page' %>, ask for the list without waiting and check that it is empty:
361
-
362
- ```ruby
363
- eventually { expect(find_elements(<%= mobile? ? "accessibility_id: 'error'" : "css: '.error'" %>, timeout: 0)).to be_empty }
364
- ```
365
- <%- if mobile? -%>
366
- - `swipe` and `scroll` with a locator wait for that element to be visible and act on it. Without a locator
367
- (`swipe('up')`) they act on the whole screen and return `nil`.
368
- - `scroll_to(locator)` checks for the element, scrolls once if it is not visible, and checks again, up to
369
- `max_scrolls` times (10), then fails. Change the direction with `direction: 'up'`. An element that appears late
370
- near the top can be scrolled past, so first wait for something on the screen, for example with `find_element`.
371
- - `tap` with a block and no locator (`page.tap { |p| … }`) is still Ruby's own `tap`, which passes the object to the
372
- block.
373
- - `hide_keyboard`, `back` and the other Appium commands are on `driver`, and do not wait.
374
- <%- end -%>
317
+ `by_role(role, name:, exact:, include_hidden:, level:)` takes these options:
375
318
 
376
- `find_element` returns a **RaiderElement**: the element, plus the same waiting methods without the locator. So
377
- calls chain, and each step waits:
319
+ - `name:` is the accessible name, matched as a part of the text. `exact: true` matches all of it.
320
+ - `include_hidden: true` also finds elements that are not visible (by default they are skipped).
321
+ - `level:` picks a heading by its level, 1 to 6.
378
322
 
379
323
  ```ruby
380
- <%- if mobile? -%>
381
- find_element(accessibility_id: 'list').find_element(accessibility_id: 'row').tap
382
- find_element(accessibility_id: 'card').swipe('left')
383
- <%- else -%>
384
- find_element(css: '.form').find_element(id: 'email').send_keys('me@example.com')
385
- <%- end -%>
324
+ driver.find_element(by_role('button', name: 'Save', exact: true))
325
+ driver.find_element(by_role('heading', level: 1))
326
+ driver.find_element(by_role('dialog', include_hidden: true))
386
327
  ```
387
328
 
388
- A `find_element` on a RaiderElement searches only inside it. If the <%= mobile? ? 'app' : 'page' %> redraws the element, the RaiderElement
389
- finds it again by its locator. Any other <%= mobile? ? 'Appium' : 'Selenium' %> method (`tag_name`, `rect`, `location`, …) goes straight to the
390
- element, without waiting.
329
+ An unknown role raises an `ArgumentError`. The roles are the ARIA role names, such as `button`, `link` and `textbox`.
330
+ <%- end -%>
331
+
332
+ **Strict.** `find_element` fails with `Raider::AmbiguousMatch` when the locator matches more than one element. The message
333
+ lists the first three, so you can narrow the locator. When you want every match, use `driver.find_elements`: it never fails
334
+ on many matches. <%= mobile? ? 'Strict is off on native app screens, where `find_element` returns the first match.' : '`raider u strict off` turns strict off for the project.' %>
391
335
 
392
- A RaiderElement is not a <%= mobile? ? 'raw Appium' : 'Selenium' %> element, so the <%= mobile? ? 'Appium' : 'Selenium' %> calls that check what they are given need
393
- the real one, which `.native` returns:
336
+ <%- if mobile? -%>
337
+ ### Waiting
338
+
339
+ Apps load at their own speed, so the runtime waits for you. It checks every 0.1 seconds, for up to `timeout` seconds (10).
340
+
341
+ - `find_element` and `find_elements` wait for a match. `find_element` fails with a message such as
342
+ `no such element: { id: "x" } (waited 10s)`. `find_elements` returns `[]` at the deadline.
343
+ - On a native app, `click`, `send_keys` and `clear` wait until the element is displayed and enabled. `submit` is not
344
+ checked, and strict is off.
345
+ - `force: true` skips the wait before an action, for example `first_product.click(force: true)`. The element must still
346
+ exist.
347
+ - If the element is not ready in time, the action raises `Raider::NotActionable`, which names the check that failed and how
348
+ long it waited. On XCUITest, an element that the app reports as not displayed waits the whole `timeout` and then raises
349
+ it. If you know the element works, use `force: true` for that call, or `Raider.actionability = false` for the run.
350
+ - `timeout`, `strict` and `actionability` are set under [Settings](#settings) and in the environment:
351
+ `RAIDER_TIMEOUT=20`, `RAIDER_STRICT=0` and `RAIDER_ACTIONABILITY=0`. The first action in a run prints one line,
352
+ `[raider] actionability on`; `RAIDER_QUIET=1` silences it.
353
+ - Set no implicit wait on the driver. It makes the checks that something is **not** on the screen wait too.
354
+ <%- else -%>
355
+ ### Waiting and actionability
356
+
357
+ Pages load at their own speed, so the runtime waits for you. It checks every 0.1 seconds, for up to `timeout` seconds (10).
358
+
359
+ - `find_element` and `find_elements` wait for a match. `find_element` fails with a message such as
360
+ `no such element: { id: "x" } (waited 10s)`. `find_elements` returns `[]` at the deadline.
361
+ - Before an action, the runtime waits until the element is ready:
362
+
363
+ | Action | Waits until the element is |
364
+ |--------|----------------------------|
365
+ | `click` | visible, enabled, not inert, stable, receives events |
366
+ | `send_keys` | visible, enabled, editable, not inert |
367
+ | `clear` | visible, enabled, editable, not inert |
368
+ | `submit` | visible, enabled, not inert |
369
+
370
+ - "Stable" means the element was in the same place on two checks in a row, so it is not in the middle of an animation.
371
+ "Receives events" means nothing else covers the point where the click lands. "Inert" means the element sits inside an
372
+ `inert` part of the page. "Editable" means the field is not read-only.
373
+ - If the element is not ready in time, the action raises `Raider::NotActionable`, which names the check that failed and how
374
+ long it waited. If the locator matches several elements, `Raider::AmbiguousMatch` lists them.
375
+ - `force: true` skips the checks for one call: `login_button.click(force: true)`. The element must still be on the page.
376
+ - Third-party code that calls `find_elements` for an optional lookup waits `timeout` too, unless you wrap it in
377
+ `Raider::Wait.immediately { }`. Examples are axe-core's frame scan and the option lookup of
378
+ `Selenium::WebDriver::Support::Select`, so `select_by` waits `timeout` before it raises when the option never shows up.
379
+ - Set no implicit wait on the driver. It makes the checks that something is **not**
380
+ on the page wait too.
381
+ - `timeout`, `strict` and `actionability` are set under [Settings](#settings) and in the environment:
382
+ `RAIDER_TIMEOUT=20`, `RAIDER_STRICT=0` and `RAIDER_ACTIONABILITY=0`. `raider u strict off` and
383
+ `raider u actionability off` write them to the file. `Raider.actionability = false` turns the checks off for the run. The first action in a run prints one line, `[raider] actionability on`; `RAIDER_QUIET=1` silences it.
384
+ <%- end -%>
385
+
386
+ ### Assertions
387
+
388
+ The <%= mobile? ? 'screen' : 'page' %> changes after an action, so a check right after it can run too early. Raider's matchers retry until
389
+ the <%= mobile? ? 'screen' : 'page' %> matches or `timeout` runs out. Write them in a block, so the lookup runs again on every try:
394
390
 
395
391
  ```ruby
396
392
  <%- if mobile? -%>
397
- driver.action.click(find_element(accessibility_id: 'card').native).perform
393
+ expect { pdp_page.add_to_cart_button }.to have_text('Add to Cart')
398
394
  <%- else -%>
399
- driver.switch_to.frame(find_element(css: 'iframe').native)
400
- Selenium::WebDriver::Support::Select.new(find_element(id: 'color').native)
401
- driver.action.move_to(find_element(id: 'menu').native).perform
402
- driver.execute_script('arguments[0].click()', find_element(id: 'go').native)
395
+ expect { account_page.welcome_message }.to have_text("Welcome back #{user['name']}")
403
396
  <%- end -%>
404
397
  ```
405
- <%- if mobile? -%>
406
398
 
407
- Elements you pass to `execute_script` need `.native` too.
408
- <%- end -%>
399
+ A page-object method like `<%= mobile? ? 'add_to_cart_button' : 'welcome_message' %>` finds the element each time it is called. The block calls it on every try, so an element that was
400
+ redrawn is found again. `expect(<%= mobile? ? 'pdp_page.add_to_cart_button' : 'account_page.welcome_message' %>)` without a block finds it once.
409
401
 
410
- ### eventually
402
+ The shape is the same for every matcher: `expect { <%= mobile? ? 'pdp_page.add_to_cart_button' : 'account_page.welcome_message' %> }.to have_text(…)`, with another matcher in place of `have_text`.
411
403
 
412
- `eventually { … }` runs the block again and again until it passes, or until `timeout` seconds have gone by. It
413
- retries a failed `expect`, an element that was not found, an element that went stale (was redrawn), and
414
- a wait inside it that timed out. It returns what the block returns, and `eventually(timeout: 30) { … }` waits longer.
404
+ | Matcher | Passes when |
405
+ |---------|-------------|
406
+ | `have_text('Save')` | the text contains it (`exact: true` for all of it); a Regexp works too |
407
+ | `have_value('Ann')` | the field's `value` equals it |
408
+ | `have_attribute('type', 'text')` | the attribute equals it; with no value, the attribute is present |
409
+ | `be_visible`, `be_hidden` | the element is displayed, or is not displayed or not there |
410
+ | `be_enabled`, `be_disabled` | the element is enabled, or is not |
411
+ | `be_checked` | the checkbox or radio is selected |
412
+ | `have_count(3)` | a list has that many items: `expect { driver.find_elements(<%= mobile? ? "accessibility_id: 'row'" : "css: 'li'" %>) }.to have_count(3)` |
413
+ <%- if web? -%>
414
+ | `have_title('Done')` | the page title equals it (or matches a Regexp) |
415
+ | `have_url(/#done$/)` | the whole current URL equals it (or matches a Regexp) |
416
+ <%- end -%>
417
+ | `have_element(id: 'late-p')` | `driver` can find the element; <%= mobile? ? '`have_css` and `have_xpath` are shortcuts for web pages' : "`have_css('li')` and `have_xpath('//li')` are shortcuts" %> |
415
418
 
416
- Use it for checks on something that changes after an action. Before, the check runs once, and fails if the
417
- <%= mobile? ? 'screen' : 'page' %> has not updated yet:
419
+ Pass `wait:` to change the time for one matcher, in seconds: `have_text('Saved', wait: 5)`. A negative check passes as soon as
420
+ the element is gone, so `expect(driver).not_to have_element(id: 'gone')` does not wait for `timeout`.
421
+
422
+ The block form works with any value matcher. `expect { value }.to eq(...)` retries until the value matches or `timeout` runs
423
+ out:
418
424
 
419
425
  ```ruby
420
426
  <%- if mobile? -%>
421
- expect(pdp_page.add_to_cart_text).to eq 'Add to Cart'
427
+ expect { pdp_page.add_to_cart_button.text }.to eq('Add to Cart')
422
428
  <%- else -%>
423
- expect(account_page.header.customer_name).to eq "Welcome back #{user['name']}"
429
+ expect { driver.find_element(id: 'status').text }.to eq('answered by /api')
424
430
  <%- end -%>
425
431
  ```
426
432
 
427
- After, it is retried until the text matches:
433
+ - The failure message is RSpec's own for the last value, with the time waited in front of it, such as `(waited 10s)`. If the last
434
+ try raised a lookup error, such as no such element, that error is reported instead. Inside `aggregate_failures` the
435
+ `(waited Ns)` text is left out.
436
+ - RSpec's block matchers (`raise_error`, `change`, `output`, `throw_symbol` and `yield_*`) work as in RSpec: the block runs
437
+ once and does not retry.
438
+ - `expect(value)` without a block does not retry. The value is read once, so a plain string or number that is already
439
+ in hand cannot change.
440
+ - A block with side effects runs once per poll, so keep side effects out of it. Click first, then check.
441
+ <%- if mobile? -%>
442
+
443
+ ### Gestures
444
+
445
+ `helpers/gesture_helper.rb` holds `swipe`, `scroll` and `scroll_to`. Every page includes it. They use the platform's own
446
+ `mobile:` commands, so they work on iOS and Android, and they do not wait.
428
447
 
429
448
  ```ruby
430
- <%- if mobile? -%>
431
- eventually { expect(pdp_page.add_to_cart_text).to eq 'Add to Cart' }
432
- <%- else -%>
433
- eventually { expect(account_page.header.customer_name).to eq "Welcome back #{user['name']}" }
434
- <%- end -%>
449
+ swipe('left', element)
450
+ scroll('down')
451
+ scroll_to({ accessibility_id: 'footer' }).click
435
452
  ```
436
453
 
437
- If it still fails at the end, the error starts with `(still failing after 10s)`, followed by the last failure, with
438
- its own error class. The waits inside the block share its time limit, so the whole block never takes longer than
439
- `timeout`. A wait without its own `timeout:` gets whatever is left of the block's time, so inside
440
- `eventually(timeout: 30) { … }` it can wait up to 30 seconds, not just the default. A wait with its own
441
- `timeout: 2` gets 2 seconds per try, and the block is tried again until its own time is up.
454
+ - `swipe(direction, element = nil)` and `scroll(direction, element = nil)` take `'up'`, `'down'`, `'left'` or `'right'`. With
455
+ an element they act on it; without one, on the whole screen.
456
+ - `scroll_to(locator)` looks for the element without waiting, scrolls the screen and looks again, up to `max_scrolls`
457
+ times (10), then fails. Change the direction with `direction: 'up'`. It returns the element, so `scroll_to(...).click`
458
+ works. An element that appears late near the top can be scrolled past, so first wait for something on the screen.
459
+ - `hide_keyboard`, `back` and the other Appium commands are on `driver`, and do not wait.
460
+ <%- end -%>
461
+
442
462
 
443
463
  ### Running the tests
444
464
 
@@ -449,13 +469,8 @@ Install the gems once with `bundle install`. Then:
449
469
  <%- end -%>
450
470
 
451
471
  ```bash
452
- <%- if rspec? -%>
453
472
  bundle exec rspec # every test
454
473
  bundle exec rspec spec/<%= examples? ? (mobile? ? 'pdp_page_spec.rb' : 'login_page_spec.rb') : 'checkout_page_spec.rb' %> # one file
455
- <%- else -%>
456
- bundle exec cucumber # every scenario
457
- bundle exec cucumber features/<%= examples? ? (mobile? ? 'home.feature' : 'login.feature') : 'checkout.feature' %> # one feature
458
- <%- end -%>
459
474
  <%- unless mobile? -%>
460
475
  HEADLESS=true bundle exec <%= test_cmd %> # without a browser window
461
476
  <%- end -%>
@@ -465,16 +480,11 @@ HEADLESS=true bundle exec <%= test_cmd %> # without a browser
465
480
  - To run tests in parallel, each process needs its own device or simulator. This project does not set that up, so
466
481
  run the tests one at a time unless you add it.
467
482
  <%- else -%>
468
- - In parallel, with the `parallel_tests` gem in the Gemfile: `bundle exec <%= rspec? ? 'parallel_rspec spec/' : 'parallel_cucumber features/' %>`. Each process opens its own
483
+ - In parallel, with the `parallel_tests` gem in the Gemfile: `bundle exec parallel_rspec spec/`. Each process opens its own
469
484
  browser.
470
485
  <%- end -%>
471
- <%- if rspec? -%>
472
486
  - `RETRY_COUNT=2 bundle exec rspec` runs each test up to 2 times, so a failing test gets one more try before it
473
487
  counts as failed.
474
- <%- else -%>
475
- - To run a failing scenario again before it counts as failed, change `--retry 0` in `cucumber.yml` to, for
476
- example, `--retry 2`.
477
- <%- end -%>
478
488
 
479
489
  ### Running in CI
480
490
 
@@ -507,43 +517,154 @@ To log in once and reuse the session across tests, see [Reusing the login](#reus
507
517
  The tests connect to it at `http://127.0.0.1:4723`, Appium's default address.
508
518
  <%- end -%>
509
519
 
510
- ### Removing the wait helper
520
+ <%- if network_helper? -%>
521
+ ## Mocking network requests
511
522
 
512
- The wait helper is ordinary code in your project, so you can take it out. To do that:
523
+ Sometimes a test should not depend on a real server: you want an empty list, an error page, or no images at all.
524
+ `NetworkHelper` lets a test answer a request with your own response, or block it, before it leaves the browser.
525
+ It is available in every test and in the page objects, and it works on Chrome, Edge and Firefox. Safari raises
526
+ `NetworkHelper::Unavailable`, and so does a run with `BIDI=off` or a grid that cannot carry WebDriver BiDi.
513
527
 
514
- 1. Delete `helpers/wait_helper.rb` and `helpers/raider_element.rb`.
515
- 2. Remove `include WaitHelper` from `page_objects/abstract/page.rb`, <%= rspec? ? '`config.include(WaitHelper)` from `helpers/spec_helper.rb`' : '`World(WaitHelper)` from `features/support/world.rb`' %>, and the
516
- `require_relative` lines that load `wait_helper`.
517
- 3. Replace the calls:
518
- - `find_element(loc)` with `driver.find_element(**loc)`;
519
- - `click(loc)` and `send_keys(loc, …)` with `driver.find_element(**loc).click` and `.send_keys(…)`;
520
- - `eventually { … }` with a plain `expect`, or `Selenium::WebDriver::Wait.new(timeout: 10).until { condition }`
521
- with a block that returns true or false (`until` does not retry a failing `expect`).
522
- <%- if cross_platform? -%>
523
- - Per-OS locators (`{ ios: { … }, android: { … } }`) with `element(ios: { … }, android: { … })` from
524
- `helpers/appium_helper.rb`.
525
- <%- end -%>
526
- <%- if mobile? -%>
527
- 4. Optionally turn on Appium's implicit wait, so plain lookups wait for the element to be present: in
528
- `configure_driver` in `helpers/driver_helper.rb`, pass `appium_lib: { wait: 10 }` next to `caps:` in
529
- `Appium::Driver.new`.
530
- <%- else -%>
531
- 4. Optionally put back an implicit wait, `driver.manage.timeouts.implicit_wait = 10`, in `create_driver` in
532
- `helpers/driver_helper.rb`, so plain lookups wait for the element to be present.
533
- <%- end -%>
534
- 5. Scaffolding (`raider g page` and `raider g component`) then writes plain `driver.find_element` examples on its
535
- own, because it checks for `helpers/wait_helper.rb`.
536
- <%- if trace_helper? -%>
528
+ The methods:
529
+
530
+ - `mock_request(url, ...)` answers matching requests with your own response. It takes `status:` (default 200, any
531
+ number from 100 to 599), `json:` or `body:` (not both), `headers:`, `content_type:` and `method:` (for example
532
+ `method: 'POST'`). It returns a mock, so you can look at it later.
533
+ - `block_request(url)` makes matching requests fail with a network error, as if the server were unreachable.
534
+ - `remove_mock(mock)` stops one mock. `clear_mocks` stops all of them.
535
+ - `mock.calls` is the list of requests the mock handled, oldest first, and `mock.called?` says if there was one. Each
536
+ request has `method`, `url` and `headers` (header names are lowercase).
537
+
538
+ Set a mock up before the page makes the request, usually before you visit the page:
539
+
540
+ ```ruby
541
+ # An empty list instead of the real users
542
+ mock_request('**/api/users', json: { users: [] })
543
+
544
+ # A server error, to see how the page shows it
545
+ mock_request('**/api/users', status: 500, json: { error: 'down' })
546
+
547
+ # No images, so the page loads faster
548
+ block_request('**/*.png')
549
+ ```
550
+
551
+ To check that the page really made the request, keep the mock and look at its calls. Put the check in a block, because
552
+ the request may come a moment after the click, and the block is tried again until it passes:
553
+
554
+ ```ruby
555
+ users = mock_request('**/api/users', json: { users: [] })
556
+ # ...open the page that loads the users...
557
+ expect { users.called? }.to be(true)
558
+ expect { users.calls.first&.method }.to eq('GET')
559
+ ```
560
+
561
+ **Matching.** A URL is a glob that is matched against the whole URL, including the query string. `**` stands for any
562
+ characters, `*` for any characters except `/`, and everything else is literal. So `'**/api/users'` matches
563
+ `https://shop.test/api/users`, but not `https://shop.test/api/users?page=2`: write `'**/api/users?*'` for that. You can
564
+ also pass a Regexp. When several mocks match, the one you added last wins. Pass `method:` to match only one HTTP method.
565
+
566
+ **Name the host when you can.** A glob that starts with `scheme://host`, such as `'https://shop.test/api/users'`, tells
567
+ the browser which requests to pause. A Regexp, or a glob that starts with `**` or has no host, makes the browser pause
568
+ every request while the mock is registered, and the helper lets the ones that do not match go on. That works, but it
569
+ is slower, so prefer `'https://shop.test/api/users'` to `'**/api/users'` on pages that load a lot.
570
+
571
+ **CORS.** When the page and the mocked URL are on different origins, the helper answers the browser's preflight
572
+ request and adds the CORS headers for you, unless your mock sets them itself.
573
+
574
+ **Deciding per request.** Give `mock_request` a block to choose the answer for each request. The block gets the
575
+ request and returns a Hash with the same options as above (`status:`, `json:`, `body:`, `headers:`, `content_type:`),
576
+ `:fail` to make the request fail, or `:continue` to let it go on to the real server. To let it go on with a changed
577
+ header, return `{ continue: { headers: { 'x-test' => '1' } } }`.
578
+
579
+ ```ruby
580
+ mock_request('https://shop.test/api/users', method: 'POST') do |request|
581
+ request.headers['authorization'] ? { status: 201, json: { id: 1 } } : { status: 401 }
582
+ end
583
+ ```
584
+
585
+ The block runs on a background thread while the browser waits, so keep it quick and do not call the driver (or any
586
+ page method) inside it.
587
+
588
+ **Cleanup and errors.** Mocks are cleared after each test, so one test never affects the next. If a block raises, or
589
+ returns something that is not a response, the request fails and the test fails with `NetworkHelper::MockError`. A bad
590
+ `status:` or a pattern that is neither a String nor a Regexp raises right where you call `mock_request`.
591
+
592
+ A mocked request shows up as "Mocked" in the [Trace replay](#trace-replay), so you can see which requests the test
593
+ answered itself.
594
+
595
+ ### Removing the network helper
596
+
597
+ The network helper is ordinary code in your project, so you can take it out. To do that:
598
+
599
+ 1. Delete `helpers/network_helper.rb`.
600
+ 2. Remove `include NetworkHelper` from `page_objects/abstract/page.rb`, `config.include(NetworkHelper)` from `helpers/spec_helper.rb`, and the
601
+ `require_relative` lines that load `network_helper`.
602
+ 3. In the after hook (`helpers/spec_helper.rb`), remove the `NetworkHelper.finish` line and `raise mock_error`.
603
+ 4. In `helpers/driver_helper.rb`, remove the `NetworkHelper` half of the `bidi` condition, so it reads
604
+ `bidi = defined?(TraceHelper) && TraceHelper.bidi?(load_browser)`.
605
+ 5. Delete any `mock_request` and `block_request` calls in your tests.
537
606
 
538
- Note that without WaitHelper a trace records only navigation and the failure (see [Trace replay](#trace-replay)).
539
607
  <%- end -%>
608
+ <%- if screenshot_helper? -%>
609
+ ## Screenshot comparison
610
+
611
+ Use it when "does the page still look right?" is the question: a layout, a chart, a component. `match_screenshot` takes
612
+ a screenshot of the page and compares it with a picture you approved earlier (the baseline). If they differ, the test fails.
613
+
614
+ ```ruby
615
+ match_screenshot('home') # the viewport
616
+ match_screenshot('cart/summary', element: cart_summary) # one element
617
+ match_screenshot('home', max_diff_pixels: 50, timeout: 5) # allow a few pixels, wait up to 5 s
618
+ ```
619
+
620
+ A name is letters, digits, `-` and `_`, with `/` for folders, up to 100 characters, and can be used once per test.
621
+
622
+ **Where baselines live.** `screenshots/baselines/<browser>/<name>.png`, for example
623
+ `screenshots/baselines/chrome/home.png`. Commit them: they are part of your tests. Each browser has its own folder.
624
+
625
+ **First run.** With no baseline, the call saves the screenshot as the baseline and passes. Look at the new file, and
626
+ commit it if it is what you want. On CI (when `CI` is set or the reporter detects a CI system) a missing baseline fails the test
627
+ and writes nothing, so a forgotten file never goes unnoticed.
628
+
629
+ **After a change you wanted.** Run `SCREENSHOTS=update bundle exec rspec`. Every comparison that differs
630
+ rewrites its baseline and passes. Review the changed PNGs in your diff before you commit them.
631
+
632
+ **When it fails.** The failure message names the baseline and says how many pixels differ. The report gets three pictures,
633
+ `comparison:<name>:expected`, `:actual` and `:diff` (differences in red), and the trace has a "match_screenshot" step that shows the actual screenshot. The
634
+ comparison retries until it matches or `timeout:` (the wait timeout by default) runs out, so a page that is still settling does not fail it.
635
+
636
+ **Tolerances.** Set them for the project in `config/config.yml` under `screenshots:`, or for one call as arguments:
637
+
638
+ - `threshold` (default 0.2): how different a pixel must be to count, from 0 (exact) to 1.
639
+ - `max_diff_pixels` (default 0): how many pixels may differ.
640
+ - `max_diff_ratio` (default none): or the share of all pixels that may differ, for example `0.001`.
641
+
642
+ Anti-aliasing is not ignored (unlike Playwright's default), so for text-heavy areas raise `threshold` or `max_diff_pixels`.
643
+
644
+ A comparison passes when the differing pixels are within `max_diff_pixels` or `max_diff_ratio`. A picture with a different
645
+ size always fails, and the message gives both sizes.
646
+
647
+ **Generate baselines where CI runs.** Fonts and rendering differ between machines, so a baseline made on your laptop may not match CI.
648
+ A Retina screen is the usual case: it makes 2x pictures, and a 1x CI machine fails with a size change that names the
649
+ device pixel ratio. Create the baselines in the same environment that checks them (run the suite once with `SCREENSHOTS=update` there and commit the files).
650
+
651
+ ### Removing screenshot comparison
652
+
653
+ The helper is ordinary code in your project, so you can take it out. To do that:
654
+
655
+ 1. Delete `helpers/screenshot_helper.rb` and the `screenshots/` folder.
656
+ 2. Remove `include ScreenshotHelper` from `page_objects/abstract/page.rb`, `config.include(ScreenshotHelper)` from `helpers/spec_helper.rb`, and the
657
+ `require_relative` lines that load `screenshot_helper`.
658
+ 3. Remove the `ScreenshotHelper.reset` line from the before hook (`helpers/spec_helper.rb`).
659
+ 4. Remove the `screenshots:` block from `config/config.yml` and `gem 'chunky_png'` from the `Gemfile`.
660
+ 5. Delete any `match_screenshot` calls in your tests.
540
661
 
541
662
  <%- end -%>
542
663
  <%- if web? -%>
543
664
  ## Reusing the login
544
665
 
545
666
  <%- if examples? -%>
546
- Tag an example `:logged_in` (RSpec) or a scenario `@logged_in` (Cucumber) and the suite logs in once, saves the
667
+ Tag an example `:logged_in` and the suite logs in once, saves the
547
668
  session, and restores it for the next tagged example or run instead of filling in the login form again. The generated
548
669
  account example shows it. If the saved session no longer works, the real login runs again and the state is saved anew.
549
670
  <%- else -%>
@@ -552,21 +673,14 @@ filling in the login form again. If the saved session no longer works, the real
552
673
  saved anew.
553
674
 
554
675
  This project has no login page yet. Once you have one (`raider g page login`), add a hook so that every
555
- <%= rspec? ? 'example tagged `:logged_in`' : 'scenario tagged `@logged_in`' %> starts logged in. In `<%= rspec? ? 'helpers/spec_helper.rb' : 'features/support/env.rb' %>`, require your pages next to
556
- the other `require_relative` lines, then add the hook<%= rspec? ? ' inside `RSpec.configure`' : '' %>:
676
+ example tagged `:logged_in` starts logged in. In `helpers/spec_helper.rb`, require your pages next to
677
+ the other `require_relative` lines, then add the hook inside `RSpec.configure`:
557
678
 
558
679
  ```ruby
559
- <%- if rspec? -%>
560
680
  require_relative '../page_objects/pages/login'
561
681
  require_relative '../page_objects/pages/dashboard'
562
682
 
563
683
  config.before(:each, :logged_in) do
564
- <%- else -%>
565
- require_relative '../../page_objects/pages/login'
566
- require_relative '../../page_objects/pages/dashboard'
567
-
568
- Before('@logged_in') do
569
- <%- end -%>
570
684
  dashboard = Dashboard.new(driver)
571
685
  SessionState.new(driver).reuse(valid: -> { dashboard.tap(&:visit).logged_in? }) do
572
686
  login = Login.new(driver)
@@ -588,8 +702,6 @@ SessionState.new(driver, name: 'admin').reuse(valid: -> { dashboard.tap(&:visit)
588
702
  end
589
703
  ```
590
704
 
591
- With Watir, pass `browser.driver`, the Selenium driver underneath the browser.
592
-
593
705
  - `login_state: max_age:` in `config/config.yml` is how many seconds a saved login stays valid (default 3600).
594
706
  - `RAIDER_LOGIN_STATE=off` always does the real login and saves nothing; `RAIDER_LOGIN_STATE=refresh` ignores the saved
595
707
  file, logs in and saves again.
@@ -599,14 +711,8 @@ With Watir, pass `browser.driver`, the Selenium driver underneath the browser.
599
711
  <%- end -%>
600
712
  ## Reporting
601
713
 
602
- <%- if rspec? -%>
603
714
  Every run writes machine-readable results to `raider-results/` (including `run.json`) and a browsable report,
604
715
  `raider-results/index.html`. Open it in a browser after a run; `bundle exec raider-reporter report` rebuilds it.
605
- <%- else -%>
606
- Every run writes machine-readable results to `raider-results/` (including `run.json`) and a browsable report,
607
- `raider-results/index.html`. The reporter runs in plugin mode:
608
- `features/support/env.rb` requires `raider_reporter/cucumber`, so no extra Cucumber formatter flag is needed.
609
- <%- end -%>
610
716
 
611
717
  To upload runs to Raider, set these environment variables before running the suite.
612
718
  `RAIDER_TOKEN` and `RAIDER_ENDPOINT` come from your Raider Hideout server (its setup page, or `bin/rails hideout:setup` /
@@ -617,17 +723,12 @@ export RAIDER_TOKEN=your-token
617
723
  export RAIDER_ENDPOINT=https://hideout.example.com
618
724
  ```
619
725
 
620
- <%- if rspec? -%>
621
726
  Without `RAIDER_TOKEN`, nothing is uploaded and the suite output is unchanged. Failure screenshots are attached to the
622
727
  report and to Raider. See the [raider_reporter README](https://github.com/RaiderHQ/raider_reporter) for details.
623
- <%- else -%>
624
- Without `RAIDER_TOKEN`, nothing is uploaded and the suite output is unchanged. Failure screenshots are attached to the
625
- report and to Raider. See the [raider_reporter README](https://github.com/RaiderHQ/raider_reporter) for details.
626
- <%- end -%>
627
728
 
628
729
  ### Video replay
629
730
 
630
- Every test is recorded, and the video is kept only for failing tests<%= rspec? ? ' (on the last retry attempt)' : '' %>. It is attached
731
+ Every test is recorded, and the video is kept only for failing tests (on the last retry attempt). It is attached
631
732
  to the report as `video` and saved under `raider-results/attachments/`, next to the failure screenshot.
632
733
  <%- if mobile? -%>
633
734
  Appium records the device screen and the video is attached as `video.mp4`.
@@ -636,12 +737,12 @@ The video is a `video.webm` built from page screenshots with ffmpeg.
636
737
  <%- end -%>
637
738
 
638
739
  <%- if single_platform? -%>
639
- - Turn video off for one run with `VIDEO=off bundle exec <%= rspec? ? 'rspec' : 'cucumber' %>`, or for the project with
740
+ - Turn video off for one run with `VIDEO=off bundle exec rspec`, or for the project with
640
741
  `raider u video off`, which creates `config/config.yml` with a `video:` section (`raider u video on` turns it back
641
742
  on).
642
743
  <%- else -%>
643
744
  - Video is on by default: `video:` in `config/config.yml` (`enabled: true`, `fps: 5`, `max_width: 1280`).
644
- - Turn it off for one run with `VIDEO=off bundle exec <%= rspec? ? 'rspec' : 'cucumber' %>`, or for the project with
745
+ - Turn it off for one run with `VIDEO=off bundle exec rspec`, or for the project with
645
746
  `raider u video off` (`raider u video on` turns it back on).
646
747
  <%- end -%>
647
748
  <%- unless mobile? -%>
@@ -684,13 +785,19 @@ The video is a `video.webm` built from page screenshots with ffmpeg.
684
785
  <%- if trace_helper? -%>
685
786
  ### Trace replay
686
787
 
687
- Every test records a trace, and the trace is kept only for failing tests<%= rspec? ? ' (on the last retry attempt)' : '' %>. A trace is a step-by-step
788
+ Every test records a trace, and the trace is kept only for failing tests (on the last retry attempt). A trace is a step-by-step
688
789
  record of the test: each action and page load, with the locator, how long it waited, a screenshot and the page HTML
689
790
  after it, the network requests, and the browser console. It is saved as `raider-results/attachments/<n>-trace.zip`,
690
791
  next to the failure screenshot and video, and attached to the report as `trace`.
691
792
 
692
- - Keep the trace of passing tests too with `TRACE=on bundle exec <%= rspec? ? 'rspec' : 'cucumber' %>`. Capture nothing for one run with
693
- `TRACE=off bundle exec <%= rspec? ? 'rspec' : 'cucumber' %>`, or for the project with `raider u trace off` (`raider u trace on` turns it back on).
793
+ The trace records `click`, `send_keys`, `clear` and `submit` with their locators (`by_role` as
794
+ `button, name: Save`), and the time each one waited. Chained finds show the element they search in. Reads such as
795
+ `text`, `attribute` and `displayed?`, `find_element`, `hover`, `drag_and_drop` and matcher assertions are not steps.
796
+
797
+ - Keep the trace of passing tests too with `TRACE=on bundle exec rspec`. Capture nothing for one run with
798
+ `TRACE=off bundle exec rspec`, or for the project with `raider u trace off` (`raider u trace on` turns it back on).
799
+ `TRACE=off` turns tracing off but BiDi stays on, so [network mocking](#mocking-network-requests) still works;
800
+ `BIDI=off` turns BiDi off, which also drops the network and console entries from a trace.
694
801
  `raider u trace on` and `raider u trace off` write `enabled:` under `trace:` in `config/config.yml`.
695
802
  - Open a trace in Raider Desktop, or from the test's page on Hideout when the run is uploaded.
696
803
  - Cost: a trace takes one screenshot and one page HTML for every step that changes the page (a click, typing,
@@ -714,8 +821,8 @@ can contain typed text too, because an assertion message is kept as it is. Reque
714
821
  response bodies are never captured. Do not share or upload traces from runs that handle real credentials.
715
822
 
716
823
  **Removing the trace helper.** The trace helper is ordinary code in your project. Delete `helpers/trace_helper.rb` and
717
- the `require_relative` lines that load `trace_helper`: in <%= rspec? ? '`helpers/driver_helper.rb`' : '`helpers/driver_helper.rb` and `features/support/env.rb`' %>. The other lines that use it
718
- (in <%= rspec? ? '`helpers/spec_helper.rb`' : '`features/support/env.rb`' %> and the driver helper's listeners) only run when `TraceHelper` is defined,
824
+ the `require_relative` lines that load `trace_helper`: in `helpers/driver_helper.rb`. The other lines that use it
825
+ (in `helpers/spec_helper.rb` and the driver helper's listeners) only run when `TraceHelper` is defined,
719
826
  so they can stay. Remove the `trace:` block from `config/config.yml` if you like.
720
827
 
721
828
  <%- end -%>