ruby_raider 3.11.0 → 3.12.1
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/.ruby-version +1 -1
- 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 +18 -6
- data/lib/generators/automation/templates/partials/home_page_selector.tt +4 -6
- data/lib/generators/automation/templates/partials/initialize_selector.tt +3 -3
- data/lib/generators/automation/templates/partials/pdp_page_selector.tt +4 -6
- data/lib/generators/automation/templates/partials/url_methods.tt +3 -2
- data/lib/generators/automation/templates/partials/visit_method.tt +5 -5
- data/lib/generators/automation/templates/pdp.tt +3 -2
- data/lib/generators/cucumber/cucumber_generator.rb +16 -3
- data/lib/generators/cucumber/templates/env.tt +3 -3
- 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/cucumber/templates/steps.tt +2 -2
- data/lib/generators/cucumber/templates/user_factory.tt +2 -0
- data/lib/generators/cucumber/templates/world.tt +5 -3
- data/lib/generators/generator.rb +29 -14
- 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/accessibility_spec.tt +10 -10
- data/lib/generators/rspec/templates/spec.tt +17 -2
- data/lib/generators/template_renderer/template_error.rb +8 -8
- data/lib/generators/templates/common/gemfile.tt +9 -9
- 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/appium_helper.tt +17 -15
- data/lib/generators/templates/helpers/browser_helper.tt +1 -2
- data/lib/generators/templates/helpers/partials/appium_driver.tt +1 -1
- 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 +46 -6
- data/lib/generators/templates/helpers/spec_helper.tt +10 -2
- data/lib/generators/templates/helpers/wait_helper.tt +470 -0
- data/lib/ruby_raider/ffmpeg.rb +7 -7
- data/lib/ruby_raider.rb +9 -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 +4 -0
- data/sig/ruby_raider.rbs +1 -0
- data/sig/scaffolding/project_detector.rbs +1 -0
- data/sig/scaffolding/scaffolding.rbs +1 -0
- data/spec/commands/raider_commands_spec.rb +48 -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 +37 -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 +37 -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/content_helper.rb +1 -5
- 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/rubocop_content_spec.rb +87 -0
- data/spec/integration/content/syntax_validation_spec.rb +33 -30
- data/spec/integration/content/test_content_spec.rb +60 -0
- data/spec/integration/end_to_end_spec.rb +73 -28
- data/spec/integration/frozen_string_literal_matcher.rb +13 -0
- 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 +130 -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/scaffolding_e2e_spec.rb +1 -5
- 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 +17 -2
|
@@ -16,6 +16,54 @@ RSpec.describe RubyRaider::Raider do # rubocop:disable RSpec/FilePath
|
|
|
16
16
|
hash_including(framework: 'rspec', automation: 'selenium', name: 'my_project')
|
|
17
17
|
)
|
|
18
18
|
end
|
|
19
|
+
|
|
20
|
+
it 'passes examples:false through as false' do
|
|
21
|
+
allow(InvokeGenerators).to receive(:generate_framework)
|
|
22
|
+
|
|
23
|
+
described_class.new.invoke(:new, nil, %w[my_project -p framework:rspec automation:selenium examples:false])
|
|
24
|
+
|
|
25
|
+
expect(InvokeGenerators).to have_received(:generate_framework).with(hash_including(examples: false))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'keeps the examples by default' do
|
|
29
|
+
allow(InvokeGenerators).to receive(:generate_framework)
|
|
30
|
+
|
|
31
|
+
described_class.new.invoke(:new, nil, %w[my_project -p framework:rspec automation:selenium])
|
|
32
|
+
|
|
33
|
+
expect(InvokeGenerators).to have_received(:generate_framework).with(hash_excluding(:examples))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'accepts examples:FALSE in any case as false' do
|
|
37
|
+
allow(InvokeGenerators).to receive(:generate_framework)
|
|
38
|
+
|
|
39
|
+
described_class.new.invoke(:new, nil, %w[my_project -p framework:rspec automation:selenium examples:FALSE])
|
|
40
|
+
|
|
41
|
+
expect(InvokeGenerators).to have_received(:generate_framework).with(hash_including(examples: false))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'accepts examples:True in any case as true' do
|
|
45
|
+
allow(InvokeGenerators).to receive(:generate_framework)
|
|
46
|
+
|
|
47
|
+
described_class.new.invoke(:new, nil, %w[my_project -p framework:rspec automation:selenium examples:True])
|
|
48
|
+
|
|
49
|
+
expect(InvokeGenerators).to have_received(:generate_framework).with(hash_including(examples: true))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'rejects any other examples value before generating' do
|
|
53
|
+
allow(InvokeGenerators).to receive(:generate_framework)
|
|
54
|
+
|
|
55
|
+
expect { described_class.new.invoke(:new, nil, %w[my_project -p framework:rspec automation:selenium examples:no]) }
|
|
56
|
+
.to raise_error(Thor::Error, 'Invalid examples "no". Allowed values: true, false')
|
|
57
|
+
expect(InvokeGenerators).not_to have_received(:generate_framework)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'keeps the examples for true' do
|
|
61
|
+
allow(InvokeGenerators).to receive(:generate_framework)
|
|
62
|
+
|
|
63
|
+
described_class.new.invoke(:new, nil, %w[my_project -p framework:rspec automation:selenium examples:true])
|
|
64
|
+
|
|
65
|
+
expect(InvokeGenerators).to have_received(:generate_framework).with(hash_including(examples: true))
|
|
66
|
+
end
|
|
19
67
|
end
|
|
20
68
|
|
|
21
69
|
context 'without parameters' do
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Ruby Raider
|
|
2
|
+
|
|
3
|
+
<!-- PROJECT LOGO -->
|
|
4
|
+
<br />
|
|
5
|
+
<div align="center">
|
|
6
|
+
<a href="https://github.com/RubyRaider/ruby_raider">
|
|
7
|
+
<img src="https://www.ruby-raider.com/assets/icon-DYY74ofR.png" alt="Logo" style="width:200px;">
|
|
8
|
+
</a>
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://github.com/RubyRaider/ruby_raider#getting-started"><strong>Explore the docs</strong></a>
|
|
11
|
+
<br />
|
|
12
|
+
<br />
|
|
13
|
+
<a href="https://rubygems.org/gems/ruby_raider">Rubygems</a>
|
|
14
|
+
·
|
|
15
|
+
<a href="https://github.com/RubyRaider/ruby_raider/issues">Report Bug</a>
|
|
16
|
+
·
|
|
17
|
+
<a href="https://github.com/RubyRaider/ruby_raider/issues">Request Feature</a>
|
|
18
|
+
</p>
|
|
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
|
+
</div>
|
|
21
|
+
|
|
22
|
+
> **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).
|
|
23
|
+
|
|
24
|
+
## What is Ruby Raider?
|
|
25
|
+
|
|
26
|
+
Ruby Raider is a generator and scaffolding gem to make UI test automation easier.
|
|
27
|
+
|
|
28
|
+
### Supported Frameworks
|
|
29
|
+
|
|
30
|
+
| Web Testing Framework | Mobile Testing Framework |
|
|
31
|
+
|-----------------------|------------------------------------|
|
|
32
|
+
| Cucumber and Selenium | Cucumber and Appium for iOS |
|
|
33
|
+
| RSpec and Selenium | RSpec and Appium for iOS |
|
|
34
|
+
| Cucumber and Watir | Cucumber and Appium for Android |
|
|
35
|
+
| RSpec and Watir | RSpec and Appium for Android |
|
|
36
|
+
| | Cucumber and Appium Cross-platform |
|
|
37
|
+
| | RSpec and Appium Cross-platform |
|
|
38
|
+
|
|
39
|
+
**Optional add-on for web projects:**
|
|
40
|
+
- **Accessibility testing (axe)**: `--accessibility` flag
|
|
41
|
+
|
|
42
|
+
***The web tests run against the [Raider Test Store](https://raider-test-site.onrender.com/).***
|
|
43
|
+
|
|
44
|
+
***To run Appium tests, download the example [app](https://github.com/RaiderHQ/raider_test_app) and start the server:***
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
raider u start_appium
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
This works on all platforms (Mac OS, Linux and Windows).
|
|
51
|
+
|
|
52
|
+
## Getting Started
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
gem install ruby_raider
|
|
56
|
+
raider new [name_of_project]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Or with parameters:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
raider new [name_of_project] -p framework:[framework] automation:[automation_type]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Commands
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
raider generate # Scaffolding commands
|
|
69
|
+
raider utility # Utility commands
|
|
70
|
+
raider new [NAME] # Create new project
|
|
71
|
+
raider version # Show version
|
|
72
|
+
raider help [COMMAND] # Describe commands
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Shortcuts: `g` (generate), `n` (new), `u` (utility), `v` (version)
|
|
76
|
+
|
|
77
|
+
### Scaffolding
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
raider g page [NAME] # Create page object
|
|
81
|
+
raider g spec [NAME] # Create RSpec test
|
|
82
|
+
raider g feature [NAME] # Create Cucumber feature
|
|
83
|
+
raider g steps [NAME] # Create step definitions
|
|
84
|
+
raider g scaffold [NAME] # Create page + test + steps
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Reusing the login
|
|
88
|
+
|
|
89
|
+
Tag an example `:logged_in` (RSpec) or a scenario `@logged_in` (Cucumber) and the suite logs in once, saves the
|
|
90
|
+
session, and restores it for the next tagged example or run instead of filling in the login form again. The generated
|
|
91
|
+
account example shows it. If the saved session no longer works, the real login runs again and the state is saved anew.
|
|
92
|
+
|
|
93
|
+
For another role, call it yourself with its own name:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
SessionState.new(driver, name: 'admin').reuse(valid: -> { dashboard.tap(&:visit).admin? }) do
|
|
97
|
+
# the real login for the admin
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
With Watir, pass `browser.driver`, the Selenium driver underneath the browser.
|
|
102
|
+
|
|
103
|
+
- `login_state: max_age:` in `config/config.yml` is how many seconds a saved login stays valid (default 3600).
|
|
104
|
+
- `RAIDER_LOGIN_STATE=off` always does the real login and saves nothing; `RAIDER_LOGIN_STATE=refresh` ignores the saved
|
|
105
|
+
file, logs in and saves again.
|
|
106
|
+
- The state lives in `.raider/auth/` and is git-ignored. It holds live session tokens, so it must not be committed,
|
|
107
|
+
cached or uploaded as a CI artifact. Nothing in it is ever printed.
|
|
108
|
+
|
|
109
|
+
## Reporting
|
|
110
|
+
|
|
111
|
+
Every run writes machine-readable results to `raider-results/` (including `run.json`) and a browsable report,
|
|
112
|
+
`raider-results/index.html`. The reporter runs in plugin mode:
|
|
113
|
+
`features/support/env.rb` requires `raider_reporter/cucumber`, so no extra Cucumber formatter flag is needed.
|
|
114
|
+
|
|
115
|
+
To upload runs to Raider, set these environment variables before running the suite.
|
|
116
|
+
`RAIDER_TOKEN` and `RAIDER_ENDPOINT` come from your Raider Hideout server (its setup page, or `bin/rails hideout:setup` /
|
|
117
|
+
`hideout:token`):
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
export RAIDER_TOKEN=your-token
|
|
121
|
+
export RAIDER_ENDPOINT=https://hideout.example.com
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Without `RAIDER_TOKEN`, nothing is uploaded and the suite output is unchanged. Failure screenshots are attached to the
|
|
125
|
+
report and to Raider. See the [raider_reporter README](https://github.com/RaiderHQ/raider_reporter) for details.
|
|
126
|
+
|
|
127
|
+
### Video replay
|
|
128
|
+
|
|
129
|
+
Every test is recorded, and the video is kept only for failing tests. It is attached
|
|
130
|
+
to the report as `video` and saved under `raider-results/attachments/`, next to the failure screenshot.
|
|
131
|
+
The video is a `video.webm` built from page screenshots with ffmpeg.
|
|
132
|
+
|
|
133
|
+
- Video is on by default: `video:` in `config/config.yml` (`enabled: true`, `fps: 5`, `max_width: 1280`).
|
|
134
|
+
- Turn it off for one run with `VIDEO=off bundle exec cucumber`, or for the project with
|
|
135
|
+
`raider u video off` (`raider u video on` turns it back on).
|
|
136
|
+
- ffmpeg is downloaded on first use, checked against a pinned SHA-256 and cached in `~/.cache/raider`
|
|
137
|
+
(`%LOCALAPPDATA%\raider` on Windows). `raider u video install` downloads it ahead of time (CI does this before the
|
|
138
|
+
tests and caches the folder), and `raider u video status` prints the binary in use.
|
|
139
|
+
- `RAIDER_FFMPEG_PATH` uses your own ffmpeg (it needs libvpx) instead of the download, and `RAIDER_CACHE_DIR` moves
|
|
140
|
+
the cache.
|
|
141
|
+
- The RaiderHQ `ffmpeg-7.1` release that the download comes from is not published yet. Until it is, set
|
|
142
|
+
`RAIDER_FFMPEG_PATH` to get videos; without it the suite runs as usual and prints one `[video]` line.
|
|
143
|
+
- If ffmpeg is missing or recording fails, the suite still runs and its exit code is unchanged; you get one `[video]`
|
|
144
|
+
line on stderr and no video.
|
|
145
|
+
- Known limits: a frame is taken after each page load, click and text change, plus one at the end of a failing test,
|
|
146
|
+
so changes the page makes on its own between actions show up in the next frame. There is one recorded driver per
|
|
147
|
+
process.
|
|
148
|
+
|
|
149
|
+
### Quarantine
|
|
150
|
+
|
|
151
|
+
CI runs the suite through `bundle exec raider-reporter exec -- …` with `RAIDER_QUARANTINE` set. Tests quarantined in
|
|
152
|
+
Raider still run and are still reported (as `quarantined`), and a build whose only failures are quarantined stays green.
|
|
153
|
+
Local runs keep quarantine off. To turn it off in CI, remove `RAIDER_QUARANTINE` from the pipeline.
|
|
154
|
+
|
|
155
|
+
### Quality gate
|
|
156
|
+
|
|
157
|
+
The `raider-gate` CI job runs `bundle exec raider-reporter gate` after the tests and
|
|
158
|
+
fails the pipeline when the Raider quality gate fails. It is off until you turn it on:
|
|
159
|
+
|
|
160
|
+
- set the CI variable `RAIDER_GATE=1` (a repository variable under Settings → Secrets and variables → Actions → Variables);
|
|
161
|
+
- turn on rules under the project's Gate page in Raider: a minimum pass rate, no new failures against the default
|
|
162
|
+
branch, no new flaky tests;
|
|
163
|
+
- optionally require the `raider/gate` check in branch protection.
|
|
164
|
+
|
|
165
|
+
`raider-reporter gate` exits 1 when the gate fails, naming the failed rules, and 2 when it gets no verdict (timeout,
|
|
166
|
+
Raider unreachable, token rejected, build not found). `RAIDER_GATE_TIMEOUT` sets how long it waits, in seconds
|
|
167
|
+
(default 600). The job stays off by default because a project without `RAIDER_TOKEN`, or a pull request from a fork,
|
|
168
|
+
would exit 2 on every run.
|
|
169
|
+
|
|
170
|
+
Parallel test jobs that upload to one build must each set `RAIDER_SHARD_TOTAL` (the number of jobs) and
|
|
171
|
+
`RAIDER_SHARD_INDEX` (1 to that number), so the gate waits for all of them instead of judging the first one to finish.
|
|
172
|
+
|
|
173
|
+
### Sponsors
|
|
174
|
+
|
|
175
|
+
This project is tested with BrowserStack.
|
data/spec/fixtures/watir_master/cucumber_watir/features/step_definitions/login_steps.rb.snapshot
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../page_objects/pages/account'
|
|
4
|
+
require_relative '../../page_objects/pages/login'
|
|
5
|
+
|
|
6
|
+
Given("I'm a {user} on the login page") do |user|
|
|
7
|
+
@user = user
|
|
8
|
+
@login_page = Login.new(browser)
|
|
9
|
+
@login_page.visit
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
Given("I'm a {user}") do |user|
|
|
13
|
+
@user = user
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
When('I login with my credentials') do
|
|
17
|
+
@login_page.login(@user['username'], @user['password'])
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
Then("I'm logged in") do
|
|
21
|
+
account_page = Account.new(browser)
|
|
22
|
+
account_page.visit
|
|
23
|
+
expect(account_page.header.customer_name).to eq "Welcome back #{@user['name']}"
|
|
24
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../components/header'
|
|
4
|
+
|
|
5
|
+
class Page
|
|
6
|
+
attr_reader :browser
|
|
7
|
+
|
|
8
|
+
def initialize(browser)
|
|
9
|
+
@browser = browser
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def visit(*page)
|
|
13
|
+
@browser.goto full_url(page.first)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def full_url(*page)
|
|
17
|
+
"#{base_url}#{url(*page)}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def base_url
|
|
21
|
+
YAML.load_file('config/config.yml')['url']
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def url(_page)
|
|
25
|
+
raise 'Url must be defined on child pages'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def to_s
|
|
29
|
+
self.class.to_s.sub('Page', ' Page')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Components
|
|
33
|
+
|
|
34
|
+
def header
|
|
35
|
+
Header.new(browser.element(css: '.welcome-msg'))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../abstract/page'
|
|
4
|
+
|
|
5
|
+
class Login < Page
|
|
6
|
+
def url(_page)
|
|
7
|
+
'index.php?rt=account/login'
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Actions
|
|
11
|
+
|
|
12
|
+
def login(username, password)
|
|
13
|
+
username_field.set username
|
|
14
|
+
password_field.set password
|
|
15
|
+
login_button.click
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
# Elements
|
|
21
|
+
|
|
22
|
+
def username_field
|
|
23
|
+
browser.text_field(id: 'loginFrm_loginname')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def password_field
|
|
27
|
+
browser.text_field(id: 'loginFrm_password')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def login_button
|
|
31
|
+
browser.button(xpath: "//button[@title='Login']")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Ruby Raider
|
|
2
|
+
|
|
3
|
+
<!-- PROJECT LOGO -->
|
|
4
|
+
<br />
|
|
5
|
+
<div align="center">
|
|
6
|
+
<a href="https://github.com/RubyRaider/ruby_raider">
|
|
7
|
+
<img src="https://www.ruby-raider.com/assets/icon-DYY74ofR.png" alt="Logo" style="width:200px;">
|
|
8
|
+
</a>
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://github.com/RubyRaider/ruby_raider#getting-started"><strong>Explore the docs</strong></a>
|
|
11
|
+
<br />
|
|
12
|
+
<br />
|
|
13
|
+
<a href="https://rubygems.org/gems/ruby_raider">Rubygems</a>
|
|
14
|
+
·
|
|
15
|
+
<a href="https://github.com/RubyRaider/ruby_raider/issues">Report Bug</a>
|
|
16
|
+
·
|
|
17
|
+
<a href="https://github.com/RubyRaider/ruby_raider/issues">Request Feature</a>
|
|
18
|
+
</p>
|
|
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
|
+
</div>
|
|
21
|
+
|
|
22
|
+
> **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).
|
|
23
|
+
|
|
24
|
+
## What is Ruby Raider?
|
|
25
|
+
|
|
26
|
+
Ruby Raider is a generator and scaffolding gem to make UI test automation easier.
|
|
27
|
+
|
|
28
|
+
### Supported Frameworks
|
|
29
|
+
|
|
30
|
+
| Web Testing Framework | Mobile Testing Framework |
|
|
31
|
+
|-----------------------|------------------------------------|
|
|
32
|
+
| Cucumber and Selenium | Cucumber and Appium for iOS |
|
|
33
|
+
| RSpec and Selenium | RSpec and Appium for iOS |
|
|
34
|
+
| Cucumber and Watir | Cucumber and Appium for Android |
|
|
35
|
+
| RSpec and Watir | RSpec and Appium for Android |
|
|
36
|
+
| | Cucumber and Appium Cross-platform |
|
|
37
|
+
| | RSpec and Appium Cross-platform |
|
|
38
|
+
|
|
39
|
+
**Optional add-on for web projects:**
|
|
40
|
+
- **Accessibility testing (axe)**: `--accessibility` flag
|
|
41
|
+
|
|
42
|
+
***The web tests run against the [Raider Test Store](https://raider-test-site.onrender.com/).***
|
|
43
|
+
|
|
44
|
+
***To run Appium tests, download the example [app](https://github.com/RaiderHQ/raider_test_app) and start the server:***
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
raider u start_appium
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
This works on all platforms (Mac OS, Linux and Windows).
|
|
51
|
+
|
|
52
|
+
## Getting Started
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
gem install ruby_raider
|
|
56
|
+
raider new [name_of_project]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Or with parameters:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
raider new [name_of_project] -p framework:[framework] automation:[automation_type]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Commands
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
raider generate # Scaffolding commands
|
|
69
|
+
raider utility # Utility commands
|
|
70
|
+
raider new [NAME] # Create new project
|
|
71
|
+
raider version # Show version
|
|
72
|
+
raider help [COMMAND] # Describe commands
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Shortcuts: `g` (generate), `n` (new), `u` (utility), `v` (version)
|
|
76
|
+
|
|
77
|
+
### Scaffolding
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
raider g page [NAME] # Create page object
|
|
81
|
+
raider g spec [NAME] # Create RSpec test
|
|
82
|
+
raider g feature [NAME] # Create Cucumber feature
|
|
83
|
+
raider g steps [NAME] # Create step definitions
|
|
84
|
+
raider g scaffold [NAME] # Create page + test + steps
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Reusing the login
|
|
88
|
+
|
|
89
|
+
Tag an example `:logged_in` (RSpec) or a scenario `@logged_in` (Cucumber) and the suite logs in once, saves the
|
|
90
|
+
session, and restores it for the next tagged example or run instead of filling in the login form again. The generated
|
|
91
|
+
account example shows it. If the saved session no longer works, the real login runs again and the state is saved anew.
|
|
92
|
+
|
|
93
|
+
For another role, call it yourself with its own name:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
SessionState.new(driver, name: 'admin').reuse(valid: -> { dashboard.tap(&:visit).admin? }) do
|
|
97
|
+
# the real login for the admin
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
With Watir, pass `browser.driver`, the Selenium driver underneath the browser.
|
|
102
|
+
|
|
103
|
+
- `login_state: max_age:` in `config/config.yml` is how many seconds a saved login stays valid (default 3600).
|
|
104
|
+
- `RAIDER_LOGIN_STATE=off` always does the real login and saves nothing; `RAIDER_LOGIN_STATE=refresh` ignores the saved
|
|
105
|
+
file, logs in and saves again.
|
|
106
|
+
- The state lives in `.raider/auth/` and is git-ignored. It holds live session tokens, so it must not be committed,
|
|
107
|
+
cached or uploaded as a CI artifact. Nothing in it is ever printed.
|
|
108
|
+
|
|
109
|
+
## Reporting
|
|
110
|
+
|
|
111
|
+
Every run writes machine-readable results to `raider-results/` (including `run.json`) and a browsable report,
|
|
112
|
+
`raider-results/index.html`. Open it in a browser after a run; `bundle exec raider-reporter report` rebuilds it.
|
|
113
|
+
|
|
114
|
+
To upload runs to Raider, set these environment variables before running the suite.
|
|
115
|
+
`RAIDER_TOKEN` and `RAIDER_ENDPOINT` come from your Raider Hideout server (its setup page, or `bin/rails hideout:setup` /
|
|
116
|
+
`hideout:token`):
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
export RAIDER_TOKEN=your-token
|
|
120
|
+
export RAIDER_ENDPOINT=https://hideout.example.com
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Without `RAIDER_TOKEN`, nothing is uploaded and the suite output is unchanged. Failure screenshots are attached to the
|
|
124
|
+
report and to Raider. See the [raider_reporter README](https://github.com/RaiderHQ/raider_reporter) for details.
|
|
125
|
+
|
|
126
|
+
### Video replay
|
|
127
|
+
|
|
128
|
+
Every test is recorded, and the video is kept only for failing tests (on the last retry attempt). It is attached
|
|
129
|
+
to the report as `video` and saved under `raider-results/attachments/`, next to the failure screenshot.
|
|
130
|
+
The video is a `video.webm` built from page screenshots with ffmpeg.
|
|
131
|
+
|
|
132
|
+
- Video is on by default: `video:` in `config/config.yml` (`enabled: true`, `fps: 5`, `max_width: 1280`).
|
|
133
|
+
- Turn it off for one run with `VIDEO=off bundle exec rspec`, or for the project with
|
|
134
|
+
`raider u video off` (`raider u video on` turns it back on).
|
|
135
|
+
- ffmpeg is downloaded on first use, checked against a pinned SHA-256 and cached in `~/.cache/raider`
|
|
136
|
+
(`%LOCALAPPDATA%\raider` on Windows). `raider u video install` downloads it ahead of time (CI does this before the
|
|
137
|
+
tests and caches the folder), and `raider u video status` prints the binary in use.
|
|
138
|
+
- `RAIDER_FFMPEG_PATH` uses your own ffmpeg (it needs libvpx) instead of the download, and `RAIDER_CACHE_DIR` moves
|
|
139
|
+
the cache.
|
|
140
|
+
- The RaiderHQ `ffmpeg-7.1` release that the download comes from is not published yet. Until it is, set
|
|
141
|
+
`RAIDER_FFMPEG_PATH` to get videos; without it the suite runs as usual and prints one `[video]` line.
|
|
142
|
+
- If ffmpeg is missing or recording fails, the suite still runs and its exit code is unchanged; you get one `[video]`
|
|
143
|
+
line on stderr and no video.
|
|
144
|
+
- Known limits: a frame is taken after each page load, click and text change, plus one at the end of a failing test,
|
|
145
|
+
so changes the page makes on its own between actions show up in the next frame. There is one recorded driver per
|
|
146
|
+
process.
|
|
147
|
+
|
|
148
|
+
### Quarantine
|
|
149
|
+
|
|
150
|
+
CI runs the suite through `bundle exec raider-reporter exec -- …` with `RAIDER_QUARANTINE` set. Tests quarantined in
|
|
151
|
+
Raider still run and are still reported (as `quarantined`), and a build whose only failures are quarantined stays green.
|
|
152
|
+
Local runs keep quarantine off. To turn it off in CI, remove `RAIDER_QUARANTINE` from the pipeline.
|
|
153
|
+
|
|
154
|
+
### Quality gate
|
|
155
|
+
|
|
156
|
+
The `raider-gate` CI job runs `bundle exec raider-reporter gate` after the tests and
|
|
157
|
+
fails the pipeline when the Raider quality gate fails. It is off until you turn it on:
|
|
158
|
+
|
|
159
|
+
- set the CI variable `RAIDER_GATE=1` (a repository variable under Settings → Secrets and variables → Actions → Variables);
|
|
160
|
+
- turn on rules under the project's Gate page in Raider: a minimum pass rate, no new failures against the default
|
|
161
|
+
branch, no new flaky tests;
|
|
162
|
+
- optionally require the `raider/gate` check in branch protection.
|
|
163
|
+
|
|
164
|
+
`raider-reporter gate` exits 1 when the gate fails, naming the failed rules, and 2 when it gets no verdict (timeout,
|
|
165
|
+
Raider unreachable, token rejected, build not found). `RAIDER_GATE_TIMEOUT` sets how long it waits, in seconds
|
|
166
|
+
(default 600). The job stays off by default because a project without `RAIDER_TOKEN`, or a pull request from a fork,
|
|
167
|
+
would exit 2 on every run.
|
|
168
|
+
|
|
169
|
+
Parallel test jobs that upload to one build must each set `RAIDER_SHARD_TOTAL` (the number of jobs) and
|
|
170
|
+
`RAIDER_SHARD_INDEX` (1 to that number), so the gate waits for all of them instead of judging the first one to finish.
|
|
171
|
+
|
|
172
|
+
### Sponsors
|
|
173
|
+
|
|
174
|
+
This project is tested with BrowserStack.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../components/header'
|
|
4
|
+
|
|
5
|
+
class Page
|
|
6
|
+
attr_reader :browser
|
|
7
|
+
|
|
8
|
+
def initialize(browser)
|
|
9
|
+
@browser = browser
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def visit(*page)
|
|
13
|
+
@browser.goto full_url(page.first)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def full_url(*page)
|
|
17
|
+
"#{base_url}#{url(*page)}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def base_url
|
|
21
|
+
YAML.load_file('config/config.yml')['url']
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def url(_page)
|
|
25
|
+
raise 'Url must be defined on child pages'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def to_s
|
|
29
|
+
self.class.to_s.sub('Page', ' Page')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Components
|
|
33
|
+
|
|
34
|
+
def header
|
|
35
|
+
Header.new(browser.element(css: '.welcome-msg'))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../abstract/page'
|
|
4
|
+
|
|
5
|
+
class Login < Page
|
|
6
|
+
def url(_page)
|
|
7
|
+
'index.php?rt=account/login'
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Actions
|
|
11
|
+
|
|
12
|
+
def login(username, password)
|
|
13
|
+
username_field.set username
|
|
14
|
+
password_field.set password
|
|
15
|
+
login_button.click
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
# Elements
|
|
21
|
+
|
|
22
|
+
def username_field
|
|
23
|
+
browser.text_field(id: 'loginFrm_loginname')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def password_field
|
|
27
|
+
browser.text_field(id: 'loginFrm_password')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def login_button
|
|
31
|
+
browser.button(xpath: "//button[@title='Login']")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../helpers/spec_helper'
|
|
4
|
+
require_relative '../models/model_factory'
|
|
5
|
+
require_relative '../page_objects/pages/account'
|
|
6
|
+
require_relative '../page_objects/pages/login'
|
|
7
|
+
|
|
8
|
+
# rubocop:disable RSpec/MultipleDescribes
|
|
9
|
+
describe 'Login' do
|
|
10
|
+
subject(:header) { account_page.header.customer_name }
|
|
11
|
+
|
|
12
|
+
let(:user) { ModelFactory.for('users')['registered user'] }
|
|
13
|
+
let(:login_page) { Login.new(browser) }
|
|
14
|
+
let(:account_page) { Account.new(browser) }
|
|
15
|
+
|
|
16
|
+
before do
|
|
17
|
+
login_page.visit
|
|
18
|
+
login_page.login(user['username'], password)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
context 'with right credentials' do
|
|
22
|
+
let(:password) { user['password'] }
|
|
23
|
+
|
|
24
|
+
it 'can successfully log in' do
|
|
25
|
+
account_page.visit
|
|
26
|
+
expect(header).to eq "Welcome back #{user['name']}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
context 'with wrong credentials' do
|
|
31
|
+
let(:password) { 'wrongPassword' }
|
|
32
|
+
|
|
33
|
+
it 'cannot log in' do
|
|
34
|
+
login_page.visit
|
|
35
|
+
expect(header).to eq 'Login or register'
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe 'Account', :logged_in do
|
|
41
|
+
let(:user) { ModelFactory.for('users')['registered user'] }
|
|
42
|
+
let(:account_page) { Account.new(browser) }
|
|
43
|
+
|
|
44
|
+
it 'opens the account page with the saved login' do
|
|
45
|
+
account_page.visit
|
|
46
|
+
expect(account_page.header.customer_name).to eq "Welcome back #{user['name']}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
# rubocop:enable RSpec/MultipleDescribes
|
|
@@ -94,4 +94,53 @@ describe ScaffoldingCommands do
|
|
|
94
94
|
expect(Pathname.new("#{new_path}/#{name}_helper.rb")).to be_file
|
|
95
95
|
end
|
|
96
96
|
end
|
|
97
|
+
|
|
98
|
+
describe 'waiting examples' do
|
|
99
|
+
let(:tmp_dir) { 'tmp_scaffold_wait_helper_test' }
|
|
100
|
+
let(:selenium_example) { "driver.find_element(id: 'element_id')" }
|
|
101
|
+
let(:waiting_example) { "click({ id: 'element_id' })" }
|
|
102
|
+
|
|
103
|
+
def scaffold_in_project(gem_name, kind, wait_helper:)
|
|
104
|
+
FileUtils.mkdir_p("#{tmp_dir}/helpers")
|
|
105
|
+
File.write("#{tmp_dir}/Gemfile", "gem 'rspec'\ngem '#{gem_name}'\n")
|
|
106
|
+
File.write("#{tmp_dir}/helpers/wait_helper.rb", '') if wait_helper
|
|
107
|
+
Dir.chdir(tmp_dir) { scaffold.new.invoke(kind, nil, %w[foo]) }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
after do
|
|
111
|
+
Dir.chdir orig_dir
|
|
112
|
+
FileUtils.rm_rf(tmp_dir)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'shows the waiting example in a page when the project has the wait helper' do
|
|
116
|
+
scaffold_in_project('selenium-webdriver', :page, wait_helper: true)
|
|
117
|
+
content = File.read("#{tmp_dir}/page_objects/pages/foo.rb")
|
|
118
|
+
expect(content).to include(waiting_example)
|
|
119
|
+
expect(content).not_to include(selenium_example)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it 'keeps the driver example in a page without the wait helper' do
|
|
123
|
+
scaffold_in_project('selenium-webdriver', :page, wait_helper: false)
|
|
124
|
+
content = File.read("#{tmp_dir}/page_objects/pages/foo.rb")
|
|
125
|
+
expect(content).to include(selenium_example)
|
|
126
|
+
expect(content).not_to include(waiting_example)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it 'shows the waiting example in a component when the project has the wait helper' do
|
|
130
|
+
scaffold_in_project('selenium-webdriver', :component, wait_helper: true)
|
|
131
|
+
expect(File.read("#{tmp_dir}/page_objects/components/foo.rb")).to include("component.find_element(css: '.selector').click")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it 'keeps the driver example in a component without the wait helper' do
|
|
135
|
+
scaffold_in_project('selenium-webdriver', :component, wait_helper: false)
|
|
136
|
+
expect(File.read("#{tmp_dir}/page_objects/components/foo.rb")).to include("driver.find_element(css: '.selector')")
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it 'leaves Watir output unchanged even with a wait helper present' do
|
|
140
|
+
scaffold_in_project('watir', :page, wait_helper: true)
|
|
141
|
+
content = File.read("#{tmp_dir}/page_objects/pages/foo.rb")
|
|
142
|
+
expect(content).to include("browser.text_field(id: 'element_id')")
|
|
143
|
+
expect(content).not_to include(waiting_example)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
97
146
|
end
|