ruby_raider 3.11.0 → 3.12.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.
- checksums.yaml +4 -4
- data/.github/workflows/reek.yml +5 -1
- data/.github/workflows/release.yml +2 -1
- data/.github/workflows/system_tests.yml +1 -1
- data/.reek.yml +13 -0
- data/README.md +43 -4
- data/lib/commands/utility_commands.rb +1 -1
- data/lib/generators/automation/automation_generator.rb +15 -10
- data/lib/generators/automation/templates/appium_caps.tt +2 -4
- data/lib/generators/automation/templates/home.tt +1 -2
- data/lib/generators/automation/templates/login.tt +6 -6
- data/lib/generators/automation/templates/page.tt +10 -3
- data/lib/generators/automation/templates/partials/home_page_selector.tt +4 -6
- data/lib/generators/automation/templates/partials/pdp_page_selector.tt +4 -6
- data/lib/generators/automation/templates/pdp.tt +3 -2
- data/lib/generators/cucumber/cucumber_generator.rb +15 -2
- data/lib/generators/cucumber/templates/partials/driver_world.tt +9 -0
- data/lib/generators/cucumber/templates/partials/mobile_steps.tt +1 -1
- data/lib/generators/cucumber/templates/partials/selenium_env.tt +4 -0
- data/lib/generators/cucumber/templates/partials/web_steps.tt +4 -0
- data/lib/generators/generator.rb +15 -4
- data/lib/generators/helper_generator.rb +8 -0
- data/lib/generators/invoke_generators.rb +23 -0
- data/lib/generators/menu_generator.rb +26 -14
- data/lib/generators/rspec/rspec_generator.rb +10 -3
- data/lib/generators/rspec/templates/spec.tt +15 -1
- data/lib/generators/template_renderer/template_error.rb +8 -8
- data/lib/generators/templates/common/partials/mobile_config.tt +1 -0
- data/lib/generators/templates/common/partials/web_config.tt +1 -1
- data/lib/generators/templates/common/read_me.tt +508 -24
- data/lib/generators/templates/helpers/partials/selenium_driver.tt +1 -3
- data/lib/generators/templates/helpers/raider_element.tt +159 -0
- data/lib/generators/templates/helpers/session_state.tt +45 -5
- data/lib/generators/templates/helpers/spec_helper.tt +9 -1
- data/lib/generators/templates/helpers/wait_helper.tt +470 -0
- data/lib/ruby_raider/ffmpeg.rb +7 -7
- data/lib/ruby_raider.rb +2 -0
- data/lib/scaffolding/project_detector.rb +6 -0
- data/lib/scaffolding/scaffolding.rb +4 -0
- data/lib/scaffolding/templates/component.tt +4 -0
- data/lib/scaffolding/templates/page_object.tt +8 -0
- data/lib/version +1 -1
- data/sig/generators/generator.rbs +2 -0
- data/sig/scaffolding/project_detector.rbs +1 -0
- data/sig/scaffolding/scaffolding.rbs +1 -0
- data/spec/commands/raider_commands_spec.rb +24 -0
- data/spec/fixtures/watir_master/cucumber_watir/Readme.md.snapshot +175 -0
- data/spec/fixtures/watir_master/cucumber_watir/features/step_definitions/login_steps.rb.snapshot +24 -0
- data/spec/fixtures/watir_master/cucumber_watir/page_objects/abstract/page.rb.snapshot +39 -0
- data/spec/fixtures/watir_master/cucumber_watir/page_objects/pages/login.rb.snapshot +33 -0
- data/spec/fixtures/watir_master/rspec_watir/Readme.md.snapshot +174 -0
- data/spec/fixtures/watir_master/rspec_watir/page_objects/abstract/page.rb.snapshot +39 -0
- data/spec/fixtures/watir_master/rspec_watir/page_objects/pages/login.rb.snapshot +33 -0
- data/spec/fixtures/watir_master/rspec_watir/spec/login_page_spec.rb.snapshot +49 -0
- data/spec/integration/commands/scaffolding_commands_spec.rb +49 -0
- data/spec/integration/commands/utility_commands_spec.rb +8 -0
- data/spec/integration/content/config_content_spec.rb +44 -0
- data/spec/integration/content/helper_content_spec.rb +79 -0
- data/spec/integration/content/page_content_spec.rb +75 -4
- data/spec/integration/content/readme_content_spec.rb +126 -5
- data/spec/integration/content/syntax_validation_spec.rb +23 -0
- data/spec/integration/content/test_content_spec.rb +57 -0
- data/spec/integration/end_to_end_spec.rb +50 -28
- data/spec/integration/generators/automation_generator_spec.rb +50 -0
- data/spec/integration/generators/common_generator_spec.rb +20 -0
- data/spec/integration/generators/config_features_spec.rb +2 -2
- data/spec/integration/generators/cucumber_generator_spec.rb +36 -0
- data/spec/integration/generators/helpers_generator_spec.rb +38 -0
- data/spec/integration/generators/invoke_generators_spec.rb +70 -0
- data/spec/integration/generators/rspec_generator_spec.rb +20 -0
- data/spec/integration/helpers/session_state_spec.rb +46 -0
- data/spec/integration/helpers/wait_helper_mobile_spec.rb +281 -0
- data/spec/integration/helpers/wait_helper_spec.rb +496 -0
- data/spec/integration/helpers/wait_helper_support.rb +183 -0
- data/spec/integration/settings_helper.rb +4 -1
- data/spec/integration/spec_helper.rb +2 -2
- data/spec/menus/menu_generator_spec.rb +64 -2
- data/spec/scaffolding/scaffold_project_detector_spec.rb +12 -0
- metadata +15 -2
|
@@ -19,6 +19,10 @@
|
|
|
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 -%>
|
|
22
26
|
## What is Ruby Raider?
|
|
23
27
|
|
|
24
28
|
Ruby Raider is a generator and scaffolding gem to make UI test automation easier.
|
|
@@ -82,6 +86,510 @@ raider g steps [NAME] # Create step definitions
|
|
|
82
86
|
raider g scaffold [NAME] # Create page + test + steps
|
|
83
87
|
```
|
|
84
88
|
|
|
89
|
+
<%- if wait_helper? -%>
|
|
90
|
+
<%- test_cmd = rspec? ? 'rspec' : 'cucumber' -%>
|
|
91
|
+
## How the framework works
|
|
92
|
+
|
|
93
|
+
This section walks through the project: where things live, how a test finds an element and acts on it, and how to
|
|
94
|
+
<%- if examples? -%>
|
|
95
|
+
run the tests. Most examples come from the files in this project.
|
|
96
|
+
<%- else -%>
|
|
97
|
+
run the tests.
|
|
98
|
+
|
|
99
|
+
This project was generated without example tests, so its <%= mobile? ? 'pages' : 'pages, components' %> and tests folders are empty.
|
|
100
|
+
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
|
|
102
|
+
below show what goes in them.
|
|
103
|
+
<%- end -%>
|
|
104
|
+
|
|
105
|
+
Two Ruby words you will see below: a **class** is a named group of methods, and a **method** is what other
|
|
106
|
+
languages call a function (it starts with `def` and ends with `end`).
|
|
107
|
+
|
|
108
|
+
### Folder layout
|
|
109
|
+
|
|
110
|
+
- `config/`: the settings the tests read.
|
|
111
|
+
<%- if mobile? -%>
|
|
112
|
+
`capabilities.yml` describes the device and the app<%= cross_platform? ? ', and `config.yml` holds the other settings' : '' %>.
|
|
113
|
+
See "Settings" below.
|
|
114
|
+
<%- if examples? -%>
|
|
115
|
+
- `page_objects/pages/`: one class per screen of the app. `home.rb` is the product list and `pdp.rb` the product
|
|
116
|
+
details page. A page holds the locators of its elements and the actions a test can do on that screen.
|
|
117
|
+
<%- else -%>
|
|
118
|
+
- `page_objects/pages/`: one class per screen of the app, empty for now. A page holds the locators of its elements
|
|
119
|
+
and the actions a test can do on that screen.
|
|
120
|
+
<%- 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? -%>
|
|
125
|
+
`spec_helper.rb` sets up RSpec: it starts the app before each test and closes it after.
|
|
126
|
+
<%- if examples? -%>
|
|
127
|
+
- `spec/`: the tests. `pdp_page_spec.rb` opens a product and checks its "Add to Cart" button.
|
|
128
|
+
<%- else -%>
|
|
129
|
+
- `spec/`: the tests, empty for now.
|
|
130
|
+
<%- 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
|
+
<%- else -%>
|
|
141
|
+
`config.yml` is described under "Settings" below.
|
|
142
|
+
<%- if examples? -%>
|
|
143
|
+
- `page_objects/pages/`: one class per page of the site. `login.rb` is the login page and `account.rb` the account
|
|
144
|
+
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.
|
|
146
|
+
<%- else -%>
|
|
147
|
+
- `page_objects/pages/`: one class per page of the site, empty for now. A page holds the locators of its elements
|
|
148
|
+
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.
|
|
150
|
+
<%- end -%>
|
|
151
|
+
- `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 -%>
|
|
158
|
+
<%- if examples? -%>
|
|
159
|
+
- `models/`: test data. `data/users.yml` holds the users, and `model_factory.rb`<%= cucumber? ? ' and `user_factory.rb` load' : ' loads' %> them.
|
|
160
|
+
<%- end -%>
|
|
161
|
+
<%- if rspec? && examples? -%>
|
|
162
|
+
- `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
|
+
<%- 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`).
|
|
173
|
+
<%- end -%>
|
|
174
|
+
<%- end -%>
|
|
175
|
+
<%- if azure? && !mobile? -%>
|
|
176
|
+
- `azure-pipelines.yml`: the CI pipeline.
|
|
177
|
+
<%- elsif !azure? -%>
|
|
178
|
+
- `.github/workflows/`: the CI pipeline.
|
|
179
|
+
<%- end -%>
|
|
180
|
+
|
|
181
|
+
### Settings
|
|
182
|
+
|
|
183
|
+
<%- if mobile? -%>
|
|
184
|
+
<%- if cross_platform? -%>
|
|
185
|
+
`config/capabilities.yml` tells Appium which device to use and which app to install. The capabilities for each
|
|
186
|
+
platform sit under the `ios:` and `android:` parts. In each part, `platformName` sits at the top, and the rest sits
|
|
187
|
+
under `appium:options`: `platformVersion`, `deviceName`, `automationName` (`XCUITest` on iOS, `UiAutomator2` on
|
|
188
|
+
Android) and `app`, the path to the app file.
|
|
189
|
+
<%- else -%>
|
|
190
|
+
`config/capabilities.yml` tells Appium which device to use and which app to install. `platformName` sits at the
|
|
191
|
+
top, and the rest sits under `appium:options`: `platformVersion`, `deviceName`, `automationName` (`XCUITest` on
|
|
192
|
+
iOS, `UiAutomator2` on Android) and `app`, the path to the app file.
|
|
193
|
+
<%- end -%>
|
|
194
|
+
The `browserstack` part is used instead when you run with `TEST_ENV=browserstack`.
|
|
195
|
+
|
|
196
|
+
<%- if cross_platform? -%>
|
|
197
|
+
`config/config.yml`:
|
|
198
|
+
|
|
199
|
+
- `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,
|
|
201
|
+
change it, for example to `timeout: 15`.
|
|
202
|
+
- `video`: failure videos, see [Video replay](#video-replay).
|
|
203
|
+
<%- 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:
|
|
206
|
+
|
|
207
|
+
```yaml
|
|
208
|
+
timeout: 15
|
|
209
|
+
```
|
|
210
|
+
<%- end -%>
|
|
211
|
+
<%- else -%>
|
|
212
|
+
`config/config.yml`:
|
|
213
|
+
|
|
214
|
+
- `browser`: the browser the tests run in, `chrome` or `firefox`.
|
|
215
|
+
<%- if examples? -%>
|
|
216
|
+
- `url`: the address of the site under test. Each page adds its own path to it (`Login` adds
|
|
217
|
+
`index.php?rt=account/login`).
|
|
218
|
+
<%- else -%>
|
|
219
|
+
- `url`: the address of the site under test. Each page adds its own path to it. Change it to your site.
|
|
220
|
+
<%- 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.
|
|
223
|
+
- `browser_arguments`: extra options passed to each browser when it starts.
|
|
224
|
+
- `headless`: not in the file by default. Add `headless: true`, or set `HEADLESS=true` for one run, to run the
|
|
225
|
+
browser without a window.
|
|
226
|
+
- `video`: failure videos, see [Video replay](#video-replay).
|
|
227
|
+
- `login_state`: how long a saved login is reused, see [Reusing the login](#reusing-the-login).
|
|
228
|
+
<%- end -%>
|
|
229
|
+
|
|
230
|
+
### Pages and components
|
|
231
|
+
|
|
232
|
+
A page keeps two kinds of methods: **elements**, which say where something is, and **actions**, which do something
|
|
233
|
+
<%- if examples? -%>
|
|
234
|
+
with it. From `page_objects/pages/<%= mobile? ? 'home.rb' : 'login.rb' %>`:
|
|
235
|
+
<%- else -%>
|
|
236
|
+
with it. For example, on a <%= mobile? ? 'product list screen' : 'login page' %>:
|
|
237
|
+
<%- end -%>
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
<%- if mobile? -%>
|
|
241
|
+
def go_to_product_detail
|
|
242
|
+
tap(first_product)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def first_product
|
|
246
|
+
<%- if cross_platform? -%>
|
|
247
|
+
{ ios: { accessibility_id: 'product-item-1' },
|
|
248
|
+
android: { accessibility_id: 'product-item-1' } }
|
|
249
|
+
<%- else -%>
|
|
250
|
+
{ accessibility_id: 'product-item-1' }
|
|
251
|
+
<%- end -%>
|
|
252
|
+
end
|
|
253
|
+
<%- else -%>
|
|
254
|
+
def login(username, password)
|
|
255
|
+
send_keys(username_field, username)
|
|
256
|
+
send_keys(password_field, password)
|
|
257
|
+
click(login_button)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def username_field
|
|
261
|
+
{ id: 'loginFrm_loginname' }
|
|
262
|
+
end
|
|
263
|
+
<%- end -%>
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
<%- if mobile? -%>
|
|
267
|
+
An element method returns a **locator**: a Ruby hash (a key and a value in curly braces) that tells Appium how to
|
|
268
|
+
find the element. `{ accessibility_id: 'product-item-1' }` means "the element whose accessibility id is
|
|
269
|
+
`product-item-1`". `xpath`, `id` and `class_name` work too.
|
|
270
|
+
<%- if cross_platform? -%>
|
|
271
|
+
|
|
272
|
+
Because this project runs on both iOS and Android, a locator can name one for each: `{ ios: { … }, android: { … } }`.
|
|
273
|
+
The wait helper picks the one for the device the test runs on.
|
|
274
|
+
<%- end -%>
|
|
275
|
+
|
|
276
|
+
The action, `tap(first_product)`, comes from the wait helper: it waits until the product is ready, then taps it.
|
|
277
|
+
<%- else -%>
|
|
278
|
+
An element method returns a **locator**: a Ruby hash (a key and a value in curly braces) that tells Selenium how to
|
|
279
|
+
find the element. `{ id: 'loginFrm_loginname' }` means "the element whose id is `loginFrm_loginname`". `css`,
|
|
280
|
+
`xpath`, `name`, `class` and `link_text` work too.
|
|
281
|
+
|
|
282
|
+
The actions, `send_keys` and `click`, come from the wait helper: each one waits until the element is ready, then
|
|
283
|
+
types or clicks.
|
|
284
|
+
|
|
285
|
+
<%- if examples? -%>
|
|
286
|
+
A **component** is a part of the page that has its own methods. `page.rb` finds the welcome message and hands it to
|
|
287
|
+
the `Header` component:
|
|
288
|
+
<%- else -%>
|
|
289
|
+
A **component** is a part of the page that has its own methods. It builds on `page_objects/abstract/component.rb`,
|
|
290
|
+
and a page hands it the element it wraps. For a `Header` component in `page_objects/components/`:
|
|
291
|
+
<%- end -%>
|
|
292
|
+
|
|
293
|
+
```ruby
|
|
294
|
+
def header
|
|
295
|
+
Header.new(find_element(css: '.welcome-msg'))
|
|
296
|
+
end
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
`Header#customer_name` then reads its text, so a test can write `account_page.header.customer_name`.
|
|
300
|
+
<%- end -%>
|
|
301
|
+
|
|
302
|
+
### The wait helper and RaiderElement
|
|
303
|
+
|
|
304
|
+
<%= mobile? ? 'Apps' : 'Pages' %> load at their own speed, so a test that acts too early fails. The wait helper,
|
|
305
|
+
`helpers/wait_helper.rb`, waits for an element to be ready before it acts. It checks every 0.1 seconds, for up to
|
|
306
|
+
`timeout` seconds. If the element is still not ready, the test fails and says what it saw last:
|
|
307
|
+
|
|
308
|
+
```
|
|
309
|
+
<%- if mobile? -%>
|
|
310
|
+
Waited 10s for {accessibility_id: "product-item-1"} to be visible, enabled and stable; last seen: not found
|
|
311
|
+
<%- else -%>
|
|
312
|
+
Waited 10s for {id: "loginFrm_loginname"} to be visible and enabled; last seen: present but not visible
|
|
313
|
+
<%- end -%>
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
You can use its methods in every page and in every <%= rspec? ? 'test' : 'step definition' %>:
|
|
317
|
+
|
|
318
|
+
| Method | Waits until the element is | Then |
|
|
319
|
+
|--------|----------------------------|------|
|
|
320
|
+
| `find_element(locator)` | present and visible (`visible: false` skips visible) | returns it as a RaiderElement |
|
|
321
|
+
| `find_elements(locator)` | present, at least one | returns a list of RaiderElements, or `[]` if none showed up |
|
|
322
|
+
<%- if mobile? -%>
|
|
323
|
+
| `click(locator)`, `tap(locator)` | visible, enabled and still | taps it (`tap` and `click` do the same) |
|
|
324
|
+
| `send_keys(locator, 'text')` | visible and enabled | types the text |
|
|
325
|
+
| `clear(locator)` | visible and enabled | empties the field |
|
|
326
|
+
| `swipe('left', locator)` | visible | swipes on it: `'up'`, `'down'`, `'left'` or `'right'` |
|
|
327
|
+
| `scroll('down', locator)` | visible | scrolls inside it |
|
|
328
|
+
| `scroll_to(locator)` | visible, scrolling down up to 10 times | returns it as a RaiderElement |
|
|
329
|
+
<%- else -%>
|
|
330
|
+
| `click(locator)` | visible, enabled and still | clicks it |
|
|
331
|
+
| `send_keys(locator, 'text')` | visible and enabled | types the text |
|
|
332
|
+
| `clear(locator)` | visible and enabled | empties the field |
|
|
333
|
+
| `submit(locator)` | visible and enabled | submits its form |
|
|
334
|
+
| `select_by(locator, :text, 'Blue')` | visible and enabled | picks a dropdown option by `:text`, `:value` or `:index` |
|
|
335
|
+
| `hover(locator)` | visible and still | moves the mouse over it |
|
|
336
|
+
| `drag_and_drop(source, target)` | source visible, enabled and still; target visible | drags the source onto the target |
|
|
337
|
+
| `scroll_into_view(locator)` | present | scrolls the page to it |
|
|
338
|
+
<%- end -%>
|
|
339
|
+
| `text(locator)`, `attribute(locator, 'name')` | present | returns its text or the attribute's value |
|
|
340
|
+
| `displayed?(locator)`, `enabled?(locator)`, `selected?(locator)` | present | returns `true` or `false` |
|
|
341
|
+
| `eventually { … }` | (see below) | retries the block until it passes |
|
|
342
|
+
|
|
343
|
+
Good to know:
|
|
344
|
+
|
|
345
|
+
<%- if mobile? -%>
|
|
346
|
+
- Write the locator with or without braces: `tap(accessibility_id: 'login')` and `tap({ accessibility_id: 'login' })`
|
|
347
|
+
are the same. When a method takes more than the locator, put the locator in braces:
|
|
348
|
+
`send_keys({ accessibility_id: 'email' }, 'me@example.com')`, `swipe('left', { accessibility_id: 'card' })`.
|
|
349
|
+
<%- else -%>
|
|
350
|
+
- Write the locator with or without braces: `click(id: 'login')` and `click({ id: 'login' })` are the same. When a
|
|
351
|
+
method takes more than the locator, put the locator in braces: `send_keys({ id: 'email' }, 'me@example.com')`.
|
|
352
|
+
<%- end -%>
|
|
353
|
+
- `timeout:` changes the wait for one call, in seconds: `<%= mobile? ? "tap({ accessibility_id: 'slow-button' }, timeout: 30)" : "click({ id: 'slow-button' }, timeout: 30)" %>`.
|
|
354
|
+
- "Still" (`stable` in error messages) means the element was in the same place on two checks in a row, 0.1 seconds
|
|
355
|
+
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.
|
|
356
|
+
- `text`, `displayed?` and the other readers fail when the element never shows up. To check that something is
|
|
357
|
+
**not** on the <%= mobile? ? 'screen' : 'page' %>, ask for the list without waiting and check that it is empty:
|
|
358
|
+
|
|
359
|
+
```ruby
|
|
360
|
+
eventually { expect(find_elements(<%= mobile? ? "accessibility_id: 'error'" : "css: '.error'" %>, timeout: 0)).to be_empty }
|
|
361
|
+
```
|
|
362
|
+
<%- if mobile? -%>
|
|
363
|
+
- `swipe` and `scroll` with a locator wait for that element to be visible and act on it. Without a locator
|
|
364
|
+
(`swipe('up')`) they act on the whole screen and return `nil`.
|
|
365
|
+
- `scroll_to(locator)` checks for the element, scrolls once if it is not visible, and checks again, up to
|
|
366
|
+
`max_scrolls` times (10), then fails. Change the direction with `direction: 'up'`. An element that appears late
|
|
367
|
+
near the top can be scrolled past, so first wait for something on the screen, for example with `find_element`.
|
|
368
|
+
- `tap` with a block and no locator (`page.tap { |p| … }`) is still Ruby's own `tap`, which passes the object to the
|
|
369
|
+
block.
|
|
370
|
+
- `hide_keyboard`, `back` and the other Appium commands are on `driver`, and do not wait.
|
|
371
|
+
<%- end -%>
|
|
372
|
+
|
|
373
|
+
`find_element` returns a **RaiderElement**: the element, plus the same waiting methods without the locator. So
|
|
374
|
+
calls chain, and each step waits:
|
|
375
|
+
|
|
376
|
+
```ruby
|
|
377
|
+
<%- if mobile? -%>
|
|
378
|
+
find_element(accessibility_id: 'list').find_element(accessibility_id: 'row').tap
|
|
379
|
+
find_element(accessibility_id: 'card').swipe('left')
|
|
380
|
+
<%- else -%>
|
|
381
|
+
find_element(css: '.form').find_element(id: 'email').send_keys('me@example.com')
|
|
382
|
+
<%- end -%>
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
A `find_element` on a RaiderElement searches only inside it. If the <%= mobile? ? 'app' : 'page' %> redraws the element, the RaiderElement
|
|
386
|
+
finds it again by its locator. Any other <%= mobile? ? 'Appium' : 'Selenium' %> method (`tag_name`, `rect`, `location`, …) goes straight to the
|
|
387
|
+
element, without waiting.
|
|
388
|
+
|
|
389
|
+
A RaiderElement is not a <%= mobile? ? 'raw Appium' : 'Selenium' %> element, so the <%= mobile? ? 'Appium' : 'Selenium' %> calls that check what they are given need
|
|
390
|
+
the real one, which `.native` returns:
|
|
391
|
+
|
|
392
|
+
```ruby
|
|
393
|
+
<%- if mobile? -%>
|
|
394
|
+
driver.action.click(find_element(accessibility_id: 'card').native).perform
|
|
395
|
+
<%- else -%>
|
|
396
|
+
driver.switch_to.frame(find_element(css: 'iframe').native)
|
|
397
|
+
Selenium::WebDriver::Support::Select.new(find_element(id: 'color').native)
|
|
398
|
+
driver.action.move_to(find_element(id: 'menu').native).perform
|
|
399
|
+
driver.execute_script('arguments[0].click()', find_element(id: 'go').native)
|
|
400
|
+
<%- end -%>
|
|
401
|
+
```
|
|
402
|
+
<%- if mobile? -%>
|
|
403
|
+
|
|
404
|
+
Elements you pass to `execute_script` need `.native` too.
|
|
405
|
+
<%- end -%>
|
|
406
|
+
|
|
407
|
+
### eventually
|
|
408
|
+
|
|
409
|
+
`eventually { … }` runs the block again and again until it passes, or until `timeout` seconds have gone by. It
|
|
410
|
+
retries a failed `expect`, an element that was not found, an element that went stale (was redrawn), and
|
|
411
|
+
a wait inside it that timed out. It returns what the block returns, and `eventually(timeout: 30) { … }` waits longer.
|
|
412
|
+
|
|
413
|
+
Use it for checks on something that changes after an action. Before, the check runs once, and fails if the
|
|
414
|
+
<%= mobile? ? 'screen' : 'page' %> has not updated yet:
|
|
415
|
+
|
|
416
|
+
```ruby
|
|
417
|
+
<%- if mobile? -%>
|
|
418
|
+
expect(pdp_page.add_to_cart_text).to eq 'Add to Cart'
|
|
419
|
+
<%- else -%>
|
|
420
|
+
expect(account_page.header.customer_name).to eq "Welcome back #{user['name']}"
|
|
421
|
+
<%- end -%>
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
After, it is retried until the text matches:
|
|
425
|
+
|
|
426
|
+
```ruby
|
|
427
|
+
<%- if mobile? -%>
|
|
428
|
+
eventually { expect(pdp_page.add_to_cart_text).to eq 'Add to Cart' }
|
|
429
|
+
<%- else -%>
|
|
430
|
+
eventually { expect(account_page.header.customer_name).to eq "Welcome back #{user['name']}" }
|
|
431
|
+
<%- end -%>
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
If it still fails at the end, the error starts with `(still failing after 10s)`, followed by the last failure, with
|
|
435
|
+
its own error class. The waits inside the block share its time limit, so the whole block never takes longer than
|
|
436
|
+
`timeout`. A wait without its own `timeout:` gets whatever is left of the block's time, so inside
|
|
437
|
+
`eventually(timeout: 30) { … }` it can wait up to 30 seconds, not just the default. A wait with its own
|
|
438
|
+
`timeout: 2` gets 2 seconds per try, and the block is tried again until its own time is up.
|
|
439
|
+
|
|
440
|
+
### Running the tests
|
|
441
|
+
|
|
442
|
+
<%- if mobile? -%>
|
|
443
|
+
Start Appium and a simulator or emulator first (see "Setting up Appium" below). Then:
|
|
444
|
+
<%- else -%>
|
|
445
|
+
Install the gems once with `bundle install`. Then:
|
|
446
|
+
<%- end -%>
|
|
447
|
+
|
|
448
|
+
```bash
|
|
449
|
+
<%- if rspec? -%>
|
|
450
|
+
bundle exec rspec # every test
|
|
451
|
+
bundle exec rspec spec/<%= examples? ? (mobile? ? 'pdp_page_spec.rb' : 'login_page_spec.rb') : 'checkout_page_spec.rb' %> # one file
|
|
452
|
+
<%- else -%>
|
|
453
|
+
bundle exec cucumber # every scenario
|
|
454
|
+
bundle exec cucumber features/<%= examples? ? (mobile? ? 'home.feature' : 'login.feature') : 'checkout.feature' %> # one feature
|
|
455
|
+
<%- end -%>
|
|
456
|
+
<%- unless mobile? -%>
|
|
457
|
+
HEADLESS=true bundle exec <%= test_cmd %> # without a browser window
|
|
458
|
+
<%- end -%>
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
<%- if mobile? -%>
|
|
462
|
+
- To run tests in parallel, each process needs its own device or simulator. This project does not set that up, so
|
|
463
|
+
run the tests one at a time unless you add it.
|
|
464
|
+
<%- else -%>
|
|
465
|
+
- In parallel, with the `parallel_tests` gem in the Gemfile: `bundle exec <%= rspec? ? 'parallel_rspec spec/' : 'parallel_cucumber features/' %>`. Each process opens its own
|
|
466
|
+
browser.
|
|
467
|
+
<%- end -%>
|
|
468
|
+
<%- if rspec? -%>
|
|
469
|
+
- `RETRY_COUNT=2 bundle exec rspec` runs each test up to 2 times, so a failing test gets one more try before it
|
|
470
|
+
counts as failed.
|
|
471
|
+
<%- else -%>
|
|
472
|
+
- To run a failing scenario again before it counts as failed, change `--retry 0` in `cucumber.yml` to, for
|
|
473
|
+
example, `--retry 2`.
|
|
474
|
+
<%- end -%>
|
|
475
|
+
|
|
476
|
+
### Running in CI
|
|
477
|
+
|
|
478
|
+
<%- if azure? && mobile? -%>
|
|
479
|
+
There is no Azure pipeline for mobile projects yet; see [CI](#ci).
|
|
480
|
+
<%- elsif azure? -%>
|
|
481
|
+
`azure-pipelines.yml` runs the suite headless on Azure Pipelines; see [Azure Pipelines](#azure-pipelines) to set it up.
|
|
482
|
+
<%- elsif mobile? -%>
|
|
483
|
+
`.github/workflows/appium_pipeline.yml` runs the suite on GitHub Actions on every push to `main` or `master` and on
|
|
484
|
+
every pull request. It downloads the test app and starts a simulator or emulator and Appium.
|
|
485
|
+
<%- else -%>
|
|
486
|
+
`.github/workflows/test_pipeline.yml` runs the suite headless on GitHub Actions on every push to `main` or `master`
|
|
487
|
+
and on every pull request, and uploads `raider-results/`.
|
|
488
|
+
<%- end -%>
|
|
489
|
+
|
|
490
|
+
Every run writes a report; see [Reporting](#reporting).
|
|
491
|
+
<%- if web? -%>
|
|
492
|
+
To log in once and reuse the session across tests, see [Reusing the login](#reusing-the-login).
|
|
493
|
+
<%- end -%>
|
|
494
|
+
<%- if mobile? -%>
|
|
495
|
+
|
|
496
|
+
### Setting up Appium
|
|
497
|
+
|
|
498
|
+
1. Install Appium and its driver: `npm install -g appium`, then
|
|
499
|
+
`appium driver install <%= android? ? 'uiautomator2' : 'xcuitest' %>`<%= cross_platform? ? ' and `appium driver install uiautomator2`' : '' %>.
|
|
500
|
+
2. Download the example [app](https://github.com/RaiderHQ/raider_test_app) and put it in the project folder.
|
|
501
|
+
`app` in `config/capabilities.yml` is its path<%= cross_platform? ? ' (`RaiderTestApp.app` for iOS, `app-release.apk` for Android)' : (ios? ? ' (`RaiderTestApp.app`)' : ' (`app-release.apk`)') %>.
|
|
502
|
+
3. Start a<%= android? ? 'n Android emulator' : (ios? ? 'n iOS simulator' : ' simulator or an emulator') %> that matches `deviceName` and `platformVersion`.
|
|
503
|
+
4. Start the Appium server with `raider u start_appium` (plain `appium`), and keep it running while the tests run.
|
|
504
|
+
The tests connect to it at `http://127.0.0.1:4723`, Appium's default address.
|
|
505
|
+
<%- end -%>
|
|
506
|
+
|
|
507
|
+
### Removing the wait helper
|
|
508
|
+
|
|
509
|
+
The wait helper is ordinary code in your project, so you can take it out. To do that:
|
|
510
|
+
|
|
511
|
+
1. Delete `helpers/wait_helper.rb` and `helpers/raider_element.rb`.
|
|
512
|
+
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
|
|
513
|
+
`require_relative` lines that load `wait_helper`.
|
|
514
|
+
3. Replace the calls:
|
|
515
|
+
- `find_element(loc)` with `driver.find_element(**loc)`;
|
|
516
|
+
- `click(loc)` and `send_keys(loc, …)` with `driver.find_element(**loc).click` and `.send_keys(…)`;
|
|
517
|
+
- `eventually { … }` with a plain `expect`, or `Selenium::WebDriver::Wait.new(timeout: 10).until { condition }`
|
|
518
|
+
with a block that returns true or false (`until` does not retry a failing `expect`).
|
|
519
|
+
<%- if cross_platform? -%>
|
|
520
|
+
- Per-OS locators (`{ ios: { … }, android: { … } }`) with `element(ios: { … }, android: { … })` from
|
|
521
|
+
`helpers/appium_helper.rb`.
|
|
522
|
+
<%- end -%>
|
|
523
|
+
<%- if mobile? -%>
|
|
524
|
+
4. Optionally turn on Appium's implicit wait, so plain lookups wait for the element to be present: in
|
|
525
|
+
`configure_driver` in `helpers/driver_helper.rb`, pass `appium_lib: { wait: 10 }` next to `caps:` in
|
|
526
|
+
`Appium::Driver.new`.
|
|
527
|
+
<%- else -%>
|
|
528
|
+
4. Optionally put back an implicit wait, `driver.manage.timeouts.implicit_wait = 10`, in `create_driver` in
|
|
529
|
+
`helpers/driver_helper.rb`, so plain lookups wait for the element to be present.
|
|
530
|
+
<%- end -%>
|
|
531
|
+
5. Scaffolding (`raider g page` and `raider g component`) then writes plain `driver.find_element` examples on its
|
|
532
|
+
own, because it checks for `helpers/wait_helper.rb`.
|
|
533
|
+
|
|
534
|
+
<%- end -%>
|
|
535
|
+
<%- if web? -%>
|
|
536
|
+
## Reusing the login
|
|
537
|
+
|
|
538
|
+
<%- if examples? -%>
|
|
539
|
+
Tag an example `:logged_in` (RSpec) or a scenario `@logged_in` (Cucumber) and the suite logs in once, saves the
|
|
540
|
+
session, and restores it for the next tagged example or run instead of filling in the login form again. The generated
|
|
541
|
+
account example shows it. If the saved session no longer works, the real login runs again and the state is saved anew.
|
|
542
|
+
<%- else -%>
|
|
543
|
+
`helpers/session_state.rb` logs in once, saves the session, and restores it for the next test or run instead of
|
|
544
|
+
filling in the login form again. If the saved session no longer works, the real login runs again and the state is
|
|
545
|
+
saved anew.
|
|
546
|
+
|
|
547
|
+
This project has no login page yet. Once you have one (`raider g page login`), add a hook so that every
|
|
548
|
+
<%= 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
|
|
549
|
+
the other `require_relative` lines, then add the hook<%= rspec? ? ' inside `RSpec.configure`' : '' %>:
|
|
550
|
+
|
|
551
|
+
```ruby
|
|
552
|
+
<%- if rspec? -%>
|
|
553
|
+
require_relative '../page_objects/pages/login'
|
|
554
|
+
require_relative '../page_objects/pages/dashboard'
|
|
555
|
+
|
|
556
|
+
config.before(:each, :logged_in) do
|
|
557
|
+
<%- else -%>
|
|
558
|
+
require_relative '../../page_objects/pages/login'
|
|
559
|
+
require_relative '../../page_objects/pages/dashboard'
|
|
560
|
+
|
|
561
|
+
Before('@logged_in') do
|
|
562
|
+
<%- end -%>
|
|
563
|
+
dashboard = Dashboard.new(driver)
|
|
564
|
+
SessionState.new(driver).reuse(valid: -> { dashboard.tap(&:visit).logged_in? }) do
|
|
565
|
+
login = Login.new(driver)
|
|
566
|
+
login.visit
|
|
567
|
+
login.login(ENV.fetch('TEST_USER'), ENV.fetch('TEST_PASSWORD'))
|
|
568
|
+
end
|
|
569
|
+
end
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
`Login` and `Dashboard` stand for your own pages: `valid:` checks that a restored session is still logged in (here a
|
|
573
|
+
`logged_in?` method you write on the page), and the block does the real login.
|
|
574
|
+
<%- end -%>
|
|
575
|
+
|
|
576
|
+
For another role, call it yourself with its own name:
|
|
577
|
+
|
|
578
|
+
```ruby
|
|
579
|
+
SessionState.new(driver, name: 'admin').reuse(valid: -> { dashboard.tap(&:visit).admin? }) do
|
|
580
|
+
# the real login for the admin
|
|
581
|
+
end
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
With Watir, pass `browser.driver`, the Selenium driver underneath the browser.
|
|
585
|
+
|
|
586
|
+
- `login_state: max_age:` in `config/config.yml` is how many seconds a saved login stays valid (default 3600).
|
|
587
|
+
- `RAIDER_LOGIN_STATE=off` always does the real login and saves nothing; `RAIDER_LOGIN_STATE=refresh` ignores the saved
|
|
588
|
+
file, logs in and saves again.
|
|
589
|
+
- The state lives in `.raider/auth/` and is git-ignored. It holds live session tokens, so it must not be committed,
|
|
590
|
+
cached or uploaded as a CI artifact. Nothing in it is ever printed.
|
|
591
|
+
|
|
592
|
+
<%- end -%>
|
|
85
593
|
## Reporting
|
|
86
594
|
|
|
87
595
|
<%- if rspec? -%>
|
|
@@ -193,30 +701,6 @@ would exit 2 on every run.
|
|
|
193
701
|
Parallel test jobs that upload to one build must each set `RAIDER_SHARD_TOTAL` (the number of jobs) and
|
|
194
702
|
`RAIDER_SHARD_INDEX` (1 to that number), so the gate waits for all of them instead of judging the first one to finish.
|
|
195
703
|
|
|
196
|
-
<%- end -%>
|
|
197
|
-
<%- if web? -%>
|
|
198
|
-
## Reusing the login
|
|
199
|
-
|
|
200
|
-
Tag an example `:logged_in` (RSpec) or a scenario `@logged_in` (Cucumber) and the suite logs in once, saves the
|
|
201
|
-
session, and restores it for the next tagged example or run instead of filling in the login form again. The generated
|
|
202
|
-
account example shows it. If the saved session no longer works, the real login runs again and the state is saved anew.
|
|
203
|
-
|
|
204
|
-
For another role, call it yourself with its own name:
|
|
205
|
-
|
|
206
|
-
```ruby
|
|
207
|
-
SessionState.new(driver, name: 'admin').reuse(valid: -> { dashboard.tap(&:visit).admin? }) do
|
|
208
|
-
# the real login for the admin
|
|
209
|
-
end
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
With Watir, pass `browser.driver`, the Selenium driver underneath the browser.
|
|
213
|
-
|
|
214
|
-
- `login_state: max_age:` in `config/config.yml` is how many seconds a saved login stays valid (default 3600).
|
|
215
|
-
- `RAIDER_LOGIN_STATE=off` always does the real login and saves nothing; `RAIDER_LOGIN_STATE=refresh` ignores the saved
|
|
216
|
-
file, logs in and saves again.
|
|
217
|
-
- The state lives in `.raider/auth/` and is git-ignored. It holds live session tokens, so it must not be committed,
|
|
218
|
-
cached or uploaded as a CI artifact. Nothing in it is ever printed.
|
|
219
|
-
|
|
220
704
|
<%- end -%>
|
|
221
705
|
<%- if azure? && mobile? -%>
|
|
222
706
|
## CI
|
|
@@ -4,9 +4,7 @@
|
|
|
4
4
|
arguments = { options: create_webdriver_options(*opts) }
|
|
5
5
|
listener = VideoHelper.listener
|
|
6
6
|
arguments[:listener] = listener if listener
|
|
7
|
-
|
|
8
|
-
driver.manage.timeouts.implicit_wait = @config.fetch('timeout', 10)
|
|
9
|
-
driver
|
|
7
|
+
Selenium::WebDriver.for(browser.to_sym, **arguments)
|
|
10
8
|
end
|
|
11
9
|
|
|
12
10
|
def browser_arguments(*opts)
|