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
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec'
4
+ require_relative '../../../lib/ruby_raider/runtime'
5
+ require_relative '../support/browser'
6
+
7
+ RSpec.describe 'Raider finders in a real browser', :browser do
8
+ include Raider::Helpers
9
+
10
+ let(:driver) { Raider::Spec::Browser.driver }
11
+
12
+ before(:all) { Raider::Spec::Browser.open('locators.html') } # rubocop:disable RSpec/BeforeAfterAll
13
+
14
+ after(:all) { Raider::Spec::Browser.quit } # rubocop:disable RSpec/BeforeAfterAll
15
+
16
+ def id_of(element)
17
+ element.attribute('id')
18
+ end
19
+
20
+ describe 'label:' do
21
+ it 'finds a field through label for' do
22
+ expect(id_of(driver.find_element(label: 'Password'))).to eq('password')
23
+ end
24
+
25
+ it 'finds a field inside a wrapping label' do
26
+ expect(id_of(driver.find_element(label: 'Username'))).to eq('username')
27
+ end
28
+
29
+ it 'finds a field by aria-label' do
30
+ expect(id_of(driver.find_element(label: 'Search site'))).to eq('search')
31
+ end
32
+
33
+ it 'finds a field by aria-labelledby' do
34
+ expect(id_of(driver.find_element(label: 'Phone number'))).to eq('phone')
35
+ end
36
+
37
+ it 'matches a substring and skips the longer label under exact: true' do
38
+ expect(id_of(driver.find_element(label: 'Email address'))).to eq('email-address')
39
+ expect(id_of(driver.find_element(label: ['Email address', { exact: true }]))).to eq('email-address')
40
+ end
41
+
42
+ it 'skips the longer label when the exact label is scoped' do
43
+ scope = driver.find_element(id: 'scope')
44
+ expect(id_of(scope.find_element(label: ['Email', { exact: true }]))).to eq('scoped-email')
45
+ end
46
+
47
+ it 'treats a substring as ambiguous and exact as unique' do
48
+ expect { driver.find_element(label: 'Email') }.to raise_error(Raider::AmbiguousMatch)
49
+ expect(driver.find_elements(label: 'Email').size).to eq(3)
50
+ expect(driver.find_elements(label: ['Email', { exact: true }]).size).to eq(2)
51
+ end
52
+
53
+ it 'scopes to the section' do
54
+ scope = driver.find_element(id: 'scope')
55
+ expect(id_of(scope.find_element(label: 'Email'))).to eq('scoped-email')
56
+ end
57
+
58
+ it 'raises NoSuchElementError when nothing matches' do
59
+ expect { driver.find_element(label: 'Nope') }.to raise_error(Selenium::WebDriver::Error::NoSuchElementError)
60
+ end
61
+ end
62
+
63
+ describe 'placeholder:' do
64
+ it 'matches a substring' do
65
+ expect(driver.find_elements(placeholder: 'Enter').size).to eq(2)
66
+ end
67
+
68
+ it 'matches exactly' do
69
+ expect(id_of(driver.find_element(placeholder: ['Enter code', { exact: true }]))).to eq('code')
70
+ end
71
+ end
72
+
73
+ it 'finds an image by alt' do
74
+ expect(id_of(driver.find_element(alt: 'Company logo'))).to eq('logo')
75
+ end
76
+
77
+ it 'finds a link by title' do
78
+ expect(id_of(driver.find_element(title: 'Read the docs'))).to eq('docs-link')
79
+ end
80
+
81
+ describe 'text:' do
82
+ it 'returns the innermost element, not its ancestor' do
83
+ expect(id_of(driver.find_element(text: 'Sign in'))).to eq('sign-in')
84
+ end
85
+
86
+ it 'ignores the title and script elements that contain the text' do
87
+ expect(driver.find_elements(text: 'Sign in').size).to eq(1)
88
+ end
89
+
90
+ it 'raises AmbiguousMatch naming three buttons' do
91
+ expect { driver.find_element(text: 'Save') }.to raise_error(Raider::AmbiguousMatch) { |error|
92
+ expect(error.message).to include('matched 3 elements', '(+0 more)')
93
+ expect(error.message).to include('<button id="save-a" class="primary">Save</button>',
94
+ '<button id="save-b" class="secondary">Save</button>')
95
+ }
96
+ end
97
+
98
+ it 'find_elements is never strict' do
99
+ expect(driver.find_elements(text: 'Save').size).to eq(3)
100
+ end
101
+ end
102
+
103
+ describe 'test_id:' do
104
+ it 'matches the data-testid value exactly' do
105
+ expect(driver.find_elements(test_id: 'item').size).to eq(0)
106
+ expect(id_of(driver.find_element(test_id: 'item-1'))).to eq('item-one')
107
+ expect(id_of(driver.find_element(test_id: 'item-2'))).to eq('item-two')
108
+ end
109
+ end
110
+
111
+ describe 'by_role' do
112
+ before(:all) { Raider::Spec::Browser.open('roles.html') } # rubocop:disable RSpec/BeforeAfterAll
113
+
114
+ after(:all) { Raider::Spec::Browser.open('locators.html') } # rubocop:disable RSpec/BeforeAfterAll
115
+
116
+ it 'scopes to a shadow root and leaves the document search blind to it' do
117
+ host = driver.find_element(id: 'shadow-host')
118
+ expect(host.shadow_root.find_element(by_role('button', name: 'Shadow inner')).text).to eq('Shadow inner')
119
+ expect(driver.find_elements(by_role('button', name: 'Shadow inner'))).to be_empty # documented engine gap
120
+ end
121
+
122
+ it 'filters headings by level' do
123
+ expect(driver.find_element(by_role('heading', level: 1)).text).to eq('Roles')
124
+ end
125
+
126
+ it 'finds a role inside the scope element' do
127
+ scope = driver.find_element(id: 'scope')
128
+ button = scope.find_element(by_role('button', name: 'Save'))
129
+ expect(driver.execute_script('return arguments[0].closest("#scope") !== null', button)).to be(true)
130
+ end
131
+
132
+ it 'is strict at the page level when two buttons match' do
133
+ expect { driver.find_element(by_role('button', name: 'Save')) }.to raise_error(Raider::AmbiguousMatch)
134
+ end
135
+ end
136
+
137
+ describe 'with a BiDi session' do
138
+ let(:bidi_driver) { Raider::Spec::Browser.bidi_driver }
139
+
140
+ before(:all) { Raider::Spec::Browser.bidi_driver.navigate.to(Raider::Spec::Browser.fixture_url('locators.html')) } # rubocop:disable RSpec/BeforeAfterAll
141
+
142
+ after(:all) { Raider::Spec::Browser.quit_bidi } # rubocop:disable RSpec/BeforeAfterAll
143
+
144
+ it 'finds a field by label through the BiDi bridge' do
145
+ expect(id_of(bidi_driver.find_element(label: 'Password'))).to eq('password')
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec'
4
+ require_relative '../../../lib/ruby_raider/runtime'
5
+ require_relative '../../../lib/ruby_raider/runtime/rspec'
6
+ require_relative '../support/browser'
7
+
8
+ RSpec.describe 'Raider matchers in a real browser', :browser do
9
+ include Raider::Matchers
10
+
11
+ let(:driver) { Raider::Spec::Browser.driver }
12
+ let(:heading) { driver.find_element(id: 'heading') }
13
+
14
+ before do
15
+ Raider.timeout = 2
16
+ Raider::Spec::Browser.open('matchers.html')
17
+ end
18
+
19
+ after do
20
+ Raider.reset
21
+ end
22
+
23
+ after(:all) { Raider::Spec::Browser.quit } # rubocop:disable RSpec/BeforeAfterAll
24
+
25
+ it 'waits for the heading text to change' do
26
+ expect(heading).to have_text('Ready')
27
+ end
28
+
29
+ it 'waits for a button to hide after a click' do
30
+ button = driver.find_element(id: 'hide-me')
31
+ button.click
32
+ expect(button).to be_hidden
33
+ end
34
+
35
+ it 'reads an input value and its attributes' do
36
+ field = driver.find_element(id: 'name')
37
+ expect(field).to have_value('Ann')
38
+ expect(field).to have_attribute('type', 'text')
39
+ expect(field).to be_enabled
40
+ end
41
+
42
+ it 'waits for the list to grow to three items' do
43
+ expect { driver.find_elements(css: 'li') }.to have_count(3)
44
+ end
45
+
46
+ it 'waits for the title and url after a click' do
47
+ driver.find_element(id: 'finish').click
48
+ expect(driver).to have_title('Done')
49
+ expect(driver).to have_url(/#done$/)
50
+ end
51
+
52
+ it 'passes a negated matcher once the text has moved on' do
53
+ expect(heading).not_to have_text('Loading')
54
+ end
55
+
56
+ it 'works in a plain class that mixes in both matcher modules' do
57
+ world = Class.new do
58
+ include RSpec::Matchers
59
+ include Raider::Matchers
60
+
61
+ def check(element)
62
+ expect(element).to have_text('Ready', wait: 2)
63
+ end
64
+ end
65
+ expect(world.new.check(heading)).to be(true)
66
+ end
67
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec'
4
+ require_relative '../../../lib/ruby_raider/runtime'
5
+ require_relative '../../../lib/ruby_raider/runtime/rspec'
6
+ require_relative '../support/browser'
7
+
8
+ RSpec.describe 'Raider block expectations in a real browser', :browser do
9
+ include Raider::Matchers
10
+
11
+ let(:driver) { Raider::Spec::Browser.driver }
12
+
13
+ before do
14
+ Raider.reset
15
+ Raider.timeout = 3
16
+ Raider::Spec::Browser.open('actionability.html')
17
+ end
18
+
19
+ after { Raider.reset }
20
+
21
+ after(:all) { Raider::Spec::Browser.quit } # rubocop:disable RSpec/BeforeAfterAll
22
+
23
+ it 'waits for text set after 300 ms' do
24
+ driver.execute_script(<<~JS)
25
+ document.body.insertAdjacentHTML('beforeend', '<p id="status">pending</p>');
26
+ setTimeout(() => { document.getElementById('status').textContent = 'done'; }, 300);
27
+ JS
28
+ expect { driver.find_element(id: 'status').text }.to eq('done')
29
+ end
30
+
31
+ it 'finds an element inserted after 300 ms through have_element' do
32
+ driver.execute_script(<<~JS)
33
+ setTimeout(() => document.body.insertAdjacentHTML('beforeend', '<p id="late-p">x</p>'), 300);
34
+ JS
35
+ expect(driver).to have_element(id: 'late-p')
36
+ end
37
+ end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec'
4
+ require_relative '../../../lib/ruby_raider/runtime'
5
+ require_relative '../support/browser'
6
+
7
+ RSpec.describe 'The in-page role engine', :browser do
8
+ let(:driver) { Raider::Spec::Browser.driver }
9
+
10
+ before(:all) { Raider::Spec::Browser.open('roles.html') } # rubocop:disable RSpec/BeforeAfterAll
11
+
12
+ after(:all) { Raider::Spec::Browser.quit } # rubocop:disable RSpec/BeforeAfterAll
13
+
14
+ def engine(body, *args)
15
+ driver.execute_script("#{Raider::RoleFinder::ENGINE}\n#{body}", *args)
16
+ end
17
+
18
+ def element(id)
19
+ driver.find_element(id: id)
20
+ end
21
+
22
+ def ids(elements)
23
+ elements.map { |e| e.attribute('id') }
24
+ end
25
+
26
+ def query(scope, role, options = {})
27
+ engine('return window.__raider.role(arguments[0] || document, arguments[1], arguments[2])',
28
+ scope, role, { 'name' => nil, 'exact' => false, 'includeHidden' => false, 'level' => nil }.merge(options))
29
+ end
30
+
31
+ role_cases = {
32
+ 'r-h1' => 'heading', 'r-h2' => 'heading', 'r-h3' => 'heading', 'r-h4' => 'heading', 'r-h5' => 'heading',
33
+ 'r-h6' => 'heading', 'r-link' => 'link', 'r-anchor' => 'generic', 'r-area' => 'link',
34
+ 'r-article' => 'article', 'r-header-in-article' => 'generic', 'r-footer-in-article' => 'generic',
35
+ 'r-aside' => 'complementary', 'r-button' => 'button', 'r-datalist' => 'listbox', 'r-details' => 'group',
36
+ 'r-summary' => nil, 'r-dialog' => 'dialog', 'r-fieldset' => 'group', 'r-figure' => 'figure',
37
+ 'r-header' => 'banner', 'r-footer' => 'contentinfo', 'r-form' => 'form', 'r-hr' => 'separator',
38
+ 'r-img' => 'img', 'r-img-empty-alt' => 'presentation', 'r-img-presentation' => 'presentation',
39
+ 'r-in-button' => 'button', 'r-in-image' => 'button', 'r-in-reset' => 'button', 'r-in-submit' => 'button',
40
+ 'r-in-checkbox' => 'checkbox', 'r-in-radio' => 'radio', 'r-in-range' => 'slider',
41
+ 'r-in-number' => 'spinbutton', 'r-in-search' => 'searchbox', 'r-in-search-list' => 'combobox',
42
+ 'r-in-email' => 'textbox', 'r-in-tel' => 'textbox', 'r-in-text' => 'textbox', 'r-in-url' => 'textbox',
43
+ 'r-in-password' => 'textbox', 'r-in-default' => 'textbox', 'r-in-text-list' => 'combobox',
44
+ 'r-in-hidden' => nil, 'r-ul' => 'list', 'r-ol' => 'list', 'r-menu' => 'list', 'r-li' => 'listitem',
45
+ 'r-li-orphan' => 'generic', 'r-main' => 'main', 'r-math' => 'math', 'r-meter' => 'meter',
46
+ 'r-nav' => 'navigation', 'r-select' => 'combobox', 'r-option' => 'option', 'r-select-multiple' => 'listbox',
47
+ 'r-select-size' => 'listbox', 'r-output' => 'status', 'r-progress' => 'progressbar',
48
+ 'r-section' => 'generic', 'r-section-label' => 'region', 'r-section-labelledby' => 'region',
49
+ 'r-table' => 'table', 'r-thead' => 'rowgroup', 'r-tbody' => 'rowgroup', 'r-tfoot' => 'rowgroup',
50
+ 'r-tr' => 'row', 'r-th-col' => 'columnheader', 'r-th-colscope' => 'columnheader',
51
+ 'r-th-first' => 'rowheader', 'r-th-only-1' => 'columnheader', 'r-th-only-2' => 'columnheader',
52
+ 'cy-self' => 'region', 'cy-a' => 'region', 'cy-b' => 'region', 'cy-button' => 'button', 'r-th-second' => 'columnheader', 'r-th-scoperow' => 'rowheader',
53
+ 'r-td' => 'cell', 'r-td-grid' => 'gridcell', 'r-td-grid-ancestor' => 'gridcell', 'r-textarea' => 'textbox',
54
+ 'r-div' => 'generic', 'r-span' => 'generic', 'r-div-button' => 'button', 'r-role-tokens' => 'button',
55
+ 'r-bad-role' => 'button', 'r-explicit-over-implicit' => 'button',
56
+ 'r-p' => 'paragraph', 'r-blockquote' => 'blockquote', 'r-code' => 'code', 'r-em' => 'emphasis',
57
+ 'r-strong' => 'strong', 'r-del' => 'deletion', 'r-ins' => 'insertion', 'r-sub' => 'subscript',
58
+ 'r-sup' => 'superscript', 'r-time' => 'time', 'r-dfn' => 'term', 'r-dt' => 'term', 'r-dd' => 'definition',
59
+ 'r-caption' => 'caption', 'r-html' => 'document', 'r-search' => 'search', 'r-address' => 'group',
60
+ 'r-optgroup' => 'group'
61
+ }.freeze
62
+
63
+ name_cases = {
64
+ 'n-labelledby' => 'First Second label', 'n-lb-hidden' => 'Hidden label', 'n-lb-nested' => 'Nest text',
65
+ 'n-lb-missing' => 'Falls back', 'n-arialabel' => 'Close dialog', 'n-labelfor' => 'Email address',
66
+ 'n-wrapped' => 'Wrapped label', 'n-img' => 'Company logo', 'n-input-image' => 'Go', 'n-area' => 'Area name',
67
+ 'n-submit' => 'Send', 'n-input-button' => 'Plain value', 'n-reset' => 'Clear',
68
+ # Known gap: a valueless submit has a browser-supplied default label ("Submit") the engine does not know.
69
+ 'n-submit-empty' => '',
70
+ 'n-table' => 'Sales', 'n-fieldset' => 'Contact', 'n-figure' => 'Chart caption', 'n-title' => 'Tooltip only',
71
+ 'n-title-fallback' => 'Text wins', 'n-embedded' => 'Search for cats', 'n-slider' => 'Rate 7 stars',
72
+ 'n-select' => 'Pick Two', 'n-content' => 'Go icon now', 'n-blocks' => 'One Two', 'n-inline' => 'Abcd',
73
+ 'n-heading' => 'Spaced title', 'n-hidden-child' => 'Visible', 'n-div-button' => 'Div button',
74
+ 'n-generic' => '', 'n-li' => '', 'n-unreferenced-hidden' => '', 'n-unlabelled' => '', 'cy-self' => '', 'cy-a' => 'B', 'cy-b' => 'A', 'cy-button' => 'Self button',
75
+ 'n-placeholder' => 'Email address', 'n-placeholder-title' => 'Title wins', 'n-placeholder-area' => 'Say more'
76
+ }.freeze
77
+
78
+ hidden_cases = {
79
+ 'h-visible' => false, 'h-aria-hidden' => true, 'h-anc-aria' => true, 'h-anc-none' => true,
80
+ 'h-display-none' => true, 'h-attr' => true, 'h-visibility' => true, 'h-vis-parent-child' => false,
81
+ 'h-vis-inherit' => true, 'r-in-hidden' => true, 'o-option' => false, 'dc-button' => false
82
+ }.freeze
83
+
84
+ describe 'getRole' do
85
+ role_cases.each do |id, role|
86
+ it "maps ##{id} to #{role.inspect}" do
87
+ expect(engine('return window.__raider.getRole(arguments[0])', element(id))).to eq(role)
88
+ end
89
+ end
90
+ end
91
+
92
+ describe 'accessibleName' do
93
+ name_cases.each do |id, name|
94
+ it "names ##{id} #{name.inspect}" do
95
+ expect(engine('return window.__raider.accessibleName(arguments[0])', element(id))).to eq(name)
96
+ end
97
+ end
98
+ end
99
+
100
+ describe 'isHidden' do
101
+ hidden_cases.each do |id, hidden|
102
+ it "reports ##{id} hidden=#{hidden}" do
103
+ expect(engine('return window.__raider.isHidden(arguments[0])', element(id))).to eq(hidden)
104
+ end
105
+ end
106
+ end
107
+
108
+ describe 'role' do
109
+ it 'matches the name case-insensitively as a substring' do
110
+ expect(ids(query(element('saves'), 'button', 'name' => 'save'))).to eq(%w[save-1 save-2 save-3])
111
+ end
112
+
113
+ it 'matches a name exactly and case-sensitively' do
114
+ expect(ids(query(element('saves'), 'button', 'name' => 'Save', 'exact' => true))).to eq(['save-1'])
115
+ end
116
+
117
+ it 'adds hidden elements under includeHidden' do
118
+ found = ids(query(element('saves'), 'button', 'name' => 'save', 'includeHidden' => true))
119
+ expect(found).to eq(%w[save-1 save-2 save-3 save-hidden])
120
+ end
121
+
122
+ it 'searches the whole document in document order' do
123
+ expect(ids(query(nil, 'button', 'name' => 'Save', 'exact' => true))).to eq(%w[save-1 scope-save])
124
+ end
125
+
126
+ it 'limits the search to the scope and leaves the outside button out' do
127
+ expect(ids(query(element('scope'), 'button'))).to eq(['scope-save'])
128
+ end
129
+
130
+ it 'leaves out buttons under hidden ancestors and keeps the visible child of a hidden parent' do
131
+ found = ids(query(nil, 'button'))
132
+ expect(found).to include('h-visible', 'h-vis-parent-child')
133
+ expect(found).not_to include('h-anc-aria', 'h-anc-none', 'h-vis-inherit', 'h-visibility')
134
+ end
135
+
136
+ it 'finds an option inside a closed select and a display: contents button' do
137
+ expect(ids(query(nil, 'option', 'name' => 'Pick me'))).to eq(['o-option'])
138
+ expect(ids(query(nil, 'button', 'name' => 'Contents button'))).to eq(['dc-button'])
139
+ end
140
+
141
+ it 'filters headings by level from the tag or aria-level' do
142
+ expect(ids(query(nil, 'heading', 'level' => 2))).to eq(%w[r-h2 lv-aria n-heading])
143
+ expect(ids(query(nil, 'heading', 'level' => 1))).to eq(['r-h1'])
144
+ end
145
+
146
+ it 'searches a shadow root scope and resolves labelledby inside it' do
147
+ host = element('shadow-host')
148
+ names = driver.execute_script('return arguments[0].shadowRoot.querySelectorAll("button").length', host)
149
+ expect(names).to eq(2)
150
+ found = engine('return window.__raider.role(arguments[0].shadowRoot, "button", { name: "Labelled inner" })', host)
151
+ expect(found.size).to eq(1)
152
+ end
153
+
154
+ it 'never returns the scope element itself' do
155
+ expect(ids(query(element('scope-button'), 'button'))).to eq(['scope-inner'])
156
+ end
157
+
158
+ it 'finds by an explicit role on a div' do
159
+ expect(ids(query(element('scope-button'), 'button', 'name' => 'inner'))).to eq(['scope-inner'])
160
+ end
161
+
162
+ it 'still answers every role query with labelledby cycles on the page' do
163
+ expect(ids(query(element('cy-button').find_element(xpath: '..'), 'button', 'name' => 'Self button')))
164
+ .to include('cy-button')
165
+ expect(query(nil, 'button')).not_to be_empty
166
+ end
167
+
168
+ it 'writes nothing to the page but window.__raider' do
169
+ added = driver.execute_script(
170
+ "delete window.__raider; var before = Object.keys(window);\n#{Raider::RoleFinder::ENGINE}\n" \
171
+ 'return Object.keys(window).filter(function (k) { return before.indexOf(k) < 0 })'
172
+ )
173
+ expect(added).to eq(['__raider'])
174
+ expect(engine('return Object.keys(window.__raider).sort()')).to eq(%w[accessibleName getRole isHidden role])
175
+ end
176
+
177
+ it 'merges into an existing window.__raider instead of replacing it' do
178
+ result = driver.execute_script(
179
+ "window.__raider = { keep: 1 }; #{Raider::RoleFinder::ENGINE}\nreturn [window.__raider.keep, typeof window.__raider.role]"
180
+ )
181
+ expect(result).to eq([1, 'function'])
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec'
4
+ require_relative '../../../lib/ruby_raider/runtime'
5
+ require_relative '../support/browser'
6
+
7
+ RSpec.describe 'Raider find waits in a real browser', :browser do
8
+ let(:driver) { Raider::Spec::Browser.driver }
9
+
10
+ before do
11
+ Raider.reset
12
+ Raider.timeout = 3
13
+ Raider::Spec::Browser.open('actionability.html')
14
+ end
15
+
16
+ after { Raider.reset }
17
+
18
+ after(:all) { Raider::Spec::Browser.quit } # rubocop:disable RSpec/BeforeAfterAll
19
+
20
+ def insert_after(delay, html)
21
+ driver.execute_script(<<~JS, delay, html)
22
+ const [delay, html] = arguments;
23
+ setTimeout(() => document.body.insertAdjacentHTML('beforeend', html), delay);
24
+ JS
25
+ end
26
+
27
+ it 'finds and clicks a button inserted after 300 ms' do
28
+ driver.execute_script('window.clicked = false')
29
+ insert_after(300, '<button id="late" onclick="window.clicked = true">Late</button>')
30
+ driver.find_element(id: 'late').click
31
+ expect(driver.execute_script('return window.clicked')).to be(true)
32
+ end
33
+
34
+ it 'returns three rows inserted after 300 ms from find_elements' do
35
+ insert_after(300, '<p class="late-row">1</p><p class="late-row">2</p><p class="late-row">3</p>')
36
+ expect(driver.find_elements(css: '.late-row').size).to eq(3)
37
+ end
38
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec'
4
+ require_relative '../../lib/ruby_raider/runtime'
5
+
6
+ RSpec.describe 'Raider.by_role' do
7
+ it 'returns just the role for a bare role' do
8
+ expect(Raider.by_role('button')).to eq(role: ['button'])
9
+ end
10
+
11
+ it 'adds the name option' do
12
+ expect(Raider.by_role('button', name: 'Save')).to eq(role: ['button', { name: 'Save' }])
13
+ end
14
+
15
+ it 'adds exact and include_hidden only when set' do
16
+ expect(Raider.by_role('link', name: 'Home', exact: true, include_hidden: true))
17
+ .to eq(role: ['link', { name: 'Home', exact: true, include_hidden: true }])
18
+ expect(Raider.by_role('link', include_hidden: true)).to eq(role: ['link', { include_hidden: true }])
19
+ end
20
+
21
+ it 'carries a heading level' do
22
+ expect(Raider.by_role('heading', name: 'Intro', level: 2)).to eq(role: ['heading', { name: 'Intro', level: 2 }])
23
+ end
24
+
25
+ it 'rejects a level on another role or outside 1 to 6' do
26
+ expect { Raider.by_role('button', level: 1) }.to raise_error(ArgumentError, /heading/)
27
+ expect { Raider.by_role('heading', level: 0) }.to raise_error(ArgumentError, /1 to 6/)
28
+ expect { Raider.by_role('heading', level: '2') }.to raise_error(ArgumentError, /1 to 6/)
29
+ end
30
+
31
+ it 'stringifies a symbol role' do
32
+ expect(Raider.by_role(:heading)).to eq(role: ['heading'])
33
+ end
34
+
35
+ it 'rejects an unknown role' do
36
+ expect { Raider.by_role('foo') }
37
+ .to raise_error(ArgumentError, "unknown role 'foo'; roles are the ARIA role names such as button, link, textbox")
38
+ end
39
+
40
+ it 'rejects a non-String name' do
41
+ expect { Raider.by_role('button', name: /Save/) }.to raise_error(ArgumentError)
42
+ end
43
+
44
+ it 'has a frozen ROLES list' do
45
+ expect(Raider::ROLES).to be_frozen.and include('button', 'generic')
46
+ end
47
+
48
+ it 'is available on Raider::Helpers' do
49
+ helper = Object.new.extend(Raider::Helpers)
50
+ expect(helper.by_role('tab', name: 'One')).to eq(role: ['tab', { name: 'One' }])
51
+ end
52
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec'
4
+ require 'tmpdir'
5
+ require 'fileutils'
6
+ require_relative '../../lib/ruby_raider/runtime/config'
7
+
8
+ RSpec.describe 'Raider configuration' do
9
+ let(:env_keys) { %w[RAIDER_STRICT RAIDER_ACTIONABILITY RAIDER_TIMEOUT RAIDER_TEST_ID_ATTRIBUTE] }
10
+
11
+ around do |example|
12
+ saved = env_keys.to_h { |key| [key, ENV.fetch(key, nil)] }
13
+ env_keys.each { |key| ENV.delete(key) }
14
+ Raider.reset
15
+ Dir.mktmpdir do |dir|
16
+ Dir.chdir(dir) { example.run }
17
+ end
18
+ ensure
19
+ saved.each { |key, value| value ? ENV[key] = value : ENV.delete(key) }
20
+ Raider.reset
21
+ end
22
+
23
+ def write_config(content)
24
+ FileUtils.mkdir_p('config')
25
+ File.write(Raider::CONFIG_FILE, content)
26
+ Raider.reset
27
+ end
28
+
29
+ it 'uses defaults without a file or env' do
30
+ expect([Raider.strict, Raider.actionability, Raider.timeout, Raider.test_id_attribute])
31
+ .to eq([true, true, 10, 'data-testid'])
32
+ end
33
+
34
+ it 'prefers file values over defaults' do
35
+ write_config("strict: false\nactionability: 'off'\ntimeout: 3.5\ntest_id_attribute: data-qa\n")
36
+ expect([Raider.strict, Raider.actionability, Raider.timeout, Raider.test_id_attribute])
37
+ .to eq([false, false, 3.5, 'data-qa'])
38
+ end
39
+
40
+ it 'prefers env over the file' do
41
+ write_config("strict: true\n")
42
+ ENV['RAIDER_STRICT'] = 'off'
43
+ expect(Raider.strict).to be(false)
44
+ end
45
+
46
+ it 'reads env timeout and test id attribute' do
47
+ ENV['RAIDER_TIMEOUT'] = '2.5'
48
+ ENV['RAIDER_TEST_ID_ATTRIBUTE'] = 'data-cy'
49
+ expect([Raider.timeout, Raider.test_id_attribute]).to eq([2.5, 'data-cy'])
50
+ end
51
+
52
+ it 'prefers the setter over env' do
53
+ ENV['RAIDER_STRICT'] = 'off'
54
+ Raider.strict = true
55
+ expect(Raider.strict).to be(true)
56
+ end
57
+
58
+ it 'ignores a non-Hash YAML file' do
59
+ write_config("- one\n- two\n")
60
+ expect(Raider.timeout).to eq(10)
61
+ end
62
+
63
+ it 'reads a file that holds a symbol value' do
64
+ write_config("platform: :ios\ntimeout: 7\n")
65
+ expect(Raider.timeout).to eq(7)
66
+ end
67
+
68
+ it 'raises Raider::Error naming the file for malformed YAML' do
69
+ write_config("timeout: [7\n")
70
+ expect { Raider.timeout }.to raise_error(Raider::Error, %r{config/config\.yml})
71
+ end
72
+
73
+ it 're-reads the file after reset' do
74
+ write_config("timeout: 4\n")
75
+ expect(Raider.timeout).to eq(4)
76
+ write_config("timeout: 6\n")
77
+ expect(Raider.timeout).to eq(6)
78
+ end
79
+ end