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,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec'
4
+ require_relative '../../lib/ruby_raider/runtime/context'
5
+
6
+ RSpec.describe 'Raider::Context' do
7
+ let(:appium_bridge_class) do
8
+ stub_const('Appium::Core::Base::Bridge', Class.new(Selenium::WebDriver::Remote::Bridge))
9
+ end
10
+
11
+ def appium_bridge(context)
12
+ bridge = appium_bridge_class.allocate
13
+ bridge.define_singleton_method(:current_context) { context }
14
+ bridge
15
+ end
16
+
17
+ def plain_bridge
18
+ Selenium::WebDriver::Remote::Bridge.allocate
19
+ end
20
+
21
+ def holder(klass, bridge)
22
+ klass.allocate.tap { |object| object.instance_variable_set(:@bridge, bridge) }
23
+ end
24
+
25
+ describe '.web?' do
26
+ it 'is false on an Appium NATIVE_APP context' do
27
+ expect(Raider::Context.web?(appium_bridge('NATIVE_APP'))).to be(false)
28
+ end
29
+
30
+ it 'is false on NATIVE_APP when a listener wraps the Appium bridge' do
31
+ wrapped = Selenium::WebDriver::Support::EventFiringBridge.new(appium_bridge('NATIVE_APP'), [])
32
+ expect(Raider::Context.web?(wrapped)).to be(false)
33
+ end
34
+
35
+ it 'is true on an Appium webview context' do
36
+ expect(Raider::Context.web?(appium_bridge('WEBVIEW_1'))).to be(true)
37
+ end
38
+
39
+ it 'is true on an Appium CHROMIUM context' do
40
+ expect(Raider::Context.web?(appium_bridge('CHROMIUM'))).to be(true)
41
+ end
42
+
43
+ it 'is true for a non-Appium bridge without asking current_context' do
44
+ bridge = plain_bridge
45
+ expect(bridge).not_to respond_to(:current_context)
46
+ expect(Raider::Context.web?(bridge)).to be(true)
47
+ end
48
+
49
+ it 'is true for a non-Appium bridge when Appium is not loaded' do
50
+ expect(Raider::Context.web?(Object.new)).to be(true)
51
+ end
52
+ end
53
+
54
+ describe '.web_for?' do
55
+ it 'reads the bridge of a driver' do
56
+ driver = Selenium::WebDriver::Driver
57
+ expect(Raider::Context.web_for?(holder(driver, appium_bridge('NATIVE_APP')))).to be(false)
58
+ expect(Raider::Context.web_for?(holder(driver, appium_bridge('WEBVIEW_1')))).to be(true)
59
+ expect(Raider::Context.web_for?(holder(driver, plain_bridge))).to be(true)
60
+ end
61
+
62
+ it 'reads the bridge of an element' do
63
+ element = Selenium::WebDriver::Element
64
+ expect(Raider::Context.web_for?(holder(element, appium_bridge('NATIVE_APP')))).to be(false)
65
+ expect(Raider::Context.web_for?(holder(element, appium_bridge('CHROMIUM')))).to be(true)
66
+ expect(Raider::Context.web_for?(holder(element, plain_bridge))).to be(true)
67
+ end
68
+
69
+ it 'is true when the object has no bridge' do
70
+ expect(Raider::Context.web_for?(Object.new)).to be(true)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec'
4
+ require_relative '../../lib/ruby_raider/runtime/errors'
5
+
6
+ RSpec.describe 'Raider runtime errors' do
7
+ after { Raider.reset }
8
+
9
+ def fake(tag: 'button', id: nil, klass: nil, text: '')
10
+ instance_double(Selenium::WebDriver::Element, tag_name: tag, text: text).tap do |element|
11
+ allow(element).to receive(:attribute).with('id').and_return(id)
12
+ allow(element).to receive(:attribute).with('class').and_return(klass)
13
+ end
14
+ end
15
+
16
+ describe 'ancestry' do
17
+ it 'subclasses the Selenium errors' do
18
+ expect(Raider::Error.ancestors).to include(StandardError)
19
+ expect(Raider::AmbiguousMatch.ancestors).to include(Selenium::WebDriver::Error::WebDriverError)
20
+ expect(Raider::NotActionable.ancestors).to include(Selenium::WebDriver::Error::ElementNotInteractableError)
21
+ expect(Raider::NotSupported.ancestors).to include(Selenium::WebDriver::Error::UnsupportedOperationError)
22
+ expect(Raider::TimeoutError.ancestors).to include(Selenium::WebDriver::Error::TimeoutError)
23
+ end
24
+ end
25
+
26
+ describe Raider::AmbiguousMatch do
27
+ it 'describes the first three elements and counts the rest' do
28
+ elements = [fake(id: 'save', text: 'Save'), fake(klass: 'toolbar'), fake, fake]
29
+ error = described_class.new("by_role('button', name: 'Save')", elements)
30
+ expect(error.message).to eq(
31
+ "`by_role('button', name: 'Save')` matched 4 elements: `<button id=\"save\">Save</button>`, " \
32
+ '`<button class="toolbar">`, `<button>` (+1 more); use find_elements, or narrow the locator'
33
+ )
34
+ expect(error.elements).to eq(elements)
35
+ end
36
+
37
+ it 'shows +0 more with three or fewer' do
38
+ error = described_class.new('x', [fake, fake])
39
+ expect(error.message).to eq('`x` matched 2 elements: `<button>`, `<button>` (+0 more); ' \
40
+ 'use find_elements, or narrow the locator')
41
+ end
42
+ end
43
+
44
+ describe Raider::NotActionable do
45
+ it 'reports the check and the covering element' do
46
+ covering = { element: '<div class="overlay">', x: 412, y: 300 }
47
+ error = described_class.new('<button id="save">', 'receive events', covering: covering)
48
+ expect(error.message).to eq('waited 10s for `<button id="save">` to receive events; ' \
49
+ '`<div class="overlay">` covers it at (412, 300)')
50
+ expect(error.check).to eq('receive events')
51
+ expect(error.covering).to eq(covering)
52
+ end
53
+
54
+ it 'omits the covering clause' do
55
+ error = described_class.new('<input id="q">', 'be visible')
56
+ expect(error.message).to eq('waited 10s for `<input id="q">` to be visible')
57
+ end
58
+
59
+ it 'reports the waited seconds when given' do
60
+ expect(described_class.new('<a>', 'be visible', waited: 0.25).message).to start_with('waited 0.25s for')
61
+ end
62
+
63
+ it 'formats fractional timeouts' do
64
+ Raider.timeout = 2.5
65
+ expect(described_class.new('<a>', 'be visible').message).to start_with('waited 2.5s for')
66
+ end
67
+ end
68
+
69
+ describe Raider::Describe do
70
+ it 'formats seconds' do
71
+ expect([10, 2.5, 10.0, 2.504].map { |value| described_class.seconds(value) }).to eq([10, 2.5, 10, 2.5])
72
+ end
73
+
74
+ it 'limits classes to two and truncates text to 30 characters' do
75
+ element = fake(klass: 'a b c', text: " #{'x' * 40} ")
76
+ expect(described_class.element(element)).to eq("<button class=\"a b\">#{'x' * 30}</button>")
77
+ end
78
+
79
+ it 'skips empty id and class' do
80
+ expect(described_class.element(fake(tag: 'div', id: '', klass: ' '))).to eq('<div>')
81
+ end
82
+
83
+ it 'returns <element> on a WebDriver error' do
84
+ element = instance_double(Selenium::WebDriver::Element)
85
+ allow(element).to receive(:tag_name).and_raise(Selenium::WebDriver::Error::StaleElementReferenceError)
86
+ expect(described_class.element(element)).to eq('<element>')
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec'
4
+ require_relative '../../lib/ruby_raider/runtime'
5
+ require_relative 'support/fake_bridge'
6
+
7
+ RSpec.describe 'Raider finders' do
8
+ let(:bridge) { Raider::Spec::FakeBridge.new }
9
+ let(:driver) { Raider::Spec.driver(bridge) }
10
+ let(:search_context) { Selenium::WebDriver::SearchContext }
11
+
12
+ before { bridge.enqueue(['a']) }
13
+
14
+ it 'registers every Raider key with Selenium' do
15
+ expect(search_context.finders).to include(Raider::FINDERS)
16
+ end
17
+
18
+ it 'converts label to xpath' do
19
+ driver.find_element(label: 'Email')
20
+ expect(bridge.calls.last).to eq([:find_elements_by, 'xpath', Raider::Selectors.label('Email', exact: false), [:driver, nil]])
21
+ end
22
+
23
+ it 'converts text to xpath' do
24
+ driver.find_element(text: ['Go', { exact: true }])
25
+ expect(bridge.calls.last[1..2]).to eq(['xpath', Raider::Selectors.text('Go', exact: true)])
26
+ end
27
+
28
+ it 'converts placeholder with exact to css' do
29
+ driver.find_element(placeholder: ['Name', { exact: true }])
30
+ expect(bridge.calls.last[1..2]).to eq(['css selector', '[placeholder="Name"]'])
31
+ end
32
+
33
+ it 'converts alt and title to css' do
34
+ driver.find_element(alt: 'Logo')
35
+ Raider::Wait.immediately { driver.find_elements(title: 'Help') }
36
+ expect(bridge.calls.map { |call| call[2] }).to eq(['[alt*="Logo"]', '[title*="Help"]'])
37
+ end
38
+
39
+ it 'converts test_id to an exact css match' do
40
+ driver.find_element(test_id: 'x')
41
+ expect(bridge.calls.last[1..2]).to eq(['css selector', Raider::Selectors.test_id('x')])
42
+ end
43
+
44
+ it 'leaves other keys to the inner converter' do
45
+ driver.find_element(id: 'x')
46
+ expect(bridge.calls.last[1..2]).to eq(['css selector', '#x'])
47
+ end
48
+
49
+ it 'passes the element ref for an element scope' do
50
+ element = Selenium::WebDriver::Element.new(bridge, 'root')
51
+ element.find_element(label: 'Email')
52
+ expect(bridge.calls.last.last).to eq([:element, 'root'])
53
+ expect(bridge.calls.last[2].split(' | ')).to all(start_with('.//'))
54
+ end
55
+
56
+ it 'guards the converter role branch with an internal error' do
57
+ expect { Raider::LocatorConverter.new(nil).convert(Raider::LocatorConverter::ROLE, 'button') }
58
+ .to raise_error(Raider::NotSupported, /did not intercept/)
59
+ end
60
+
61
+ it 'raises ArgumentError naming the key for a malformed value' do
62
+ expect { driver.find_element(label: [1, {}]) }.to raise_error(ArgumentError, /label/)
63
+ expect { driver.find_element(label: %w[a b]) }.to raise_error(ArgumentError, /label/)
64
+ end
65
+
66
+ it 'rejects nil and blank values' do
67
+ [{ label: nil }, { text: '' }, { placeholder: [' ', {}] }].each do |locator|
68
+ expect { driver.find_element(locator) }.to raise_error(ArgumentError, /#{locator.keys.first}: needs a non-empty value/)
69
+ end
70
+ end
71
+
72
+ describe '.install_converter' do
73
+ it 'is idempotent' do
74
+ bridge_class = Selenium::WebDriver::Remote::Bridge
75
+ before_converter = bridge_class.locator_converter
76
+ Raider::Finders.install_converter(bridge_class)
77
+ expect(bridge_class.locator_converter).to equal(before_converter)
78
+ end
79
+
80
+ it 'wraps a plain converter exactly once' do
81
+ klass = Class.new { class << self; attr_accessor :locator_converter; end }
82
+ klass.locator_converter = Selenium::WebDriver::Remote::Bridge::LocatorConverter.new
83
+ 2.times { Raider::Finders.install_converter(klass) }
84
+ expect(klass.locator_converter.inner).not_to be_a(Raider::LocatorConverter)
85
+ end
86
+ end
87
+
88
+ describe 'with Appium finders assigned after Raider' do
89
+ around do |example|
90
+ previous = search_context.extra_finders
91
+ example.run
92
+ ensure
93
+ search_context.extra_finders = previous
94
+ end
95
+
96
+ before do
97
+ stub_const('Appium::Core::Base::SearchContext::APPIUM_EXTRA_FINDERS', { accessibility_id: 'accessibility id' })
98
+ search_context.extra_finders = Appium::Core::Base::SearchContext::APPIUM_EXTRA_FINDERS
99
+ end
100
+
101
+ it 'keeps both tables after register' do
102
+ Raider::Finders.register
103
+ expect(search_context.finders).to include(:accessibility_id, :label)
104
+ end
105
+
106
+ it 'restores the keys through ensure_registered' do
107
+ expect(search_context.finders).not_to include(:label)
108
+ Raider::Finders.ensure_registered
109
+ expect(search_context.finders).to include(:accessibility_id, :label)
110
+ end
111
+ end
112
+
113
+ describe '.ensure_registered with a late Appium bridge' do
114
+ it 'wraps the converter Appium assigned' do
115
+ appium = stub_const('Appium::Core::Base::Bridge', Class.new(Selenium::WebDriver::Remote::Bridge))
116
+ inner = Selenium::WebDriver::Remote::Bridge::LocatorConverter.new
117
+ appium.locator_converter = inner
118
+ Raider::Finders.ensure_registered
119
+ expect(appium.locator_converter).to be_a(Raider::LocatorConverter)
120
+ expect(appium.locator_converter.inner).to equal(inner)
121
+ end
122
+ end
123
+
124
+ describe 'BiDi bridges' do
125
+ it 'gives the Selenium BiDi bridge class the Raider converter' do
126
+ expect(Selenium::WebDriver::Remote::BiDiBridge.locator_converter).to be_a(Raider::LocatorConverter)
127
+ end
128
+
129
+ it 'gives a BiDi bridge instance the Raider converter' do
130
+ bidi = Raider::Spec.real_bridge(Selenium::WebDriver::Remote::BiDiBridge)
131
+ expect(bidi.instance_variable_get(:@locator_converter)).to be_a(Raider::LocatorConverter)
132
+ end
133
+
134
+ it 'wraps a late Appium BiDi bridge in ensure_registered' do
135
+ appium = stub_const('Appium::Core::Base::BiDiBridge', Class.new(Selenium::WebDriver::Remote::BiDiBridge))
136
+ inner = Selenium::WebDriver::Remote::Bridge::LocatorConverter.new
137
+ appium.locator_converter = inner
138
+ Raider::Finders.ensure_registered
139
+ expect(appium.locator_converter).to be_a(Raider::LocatorConverter)
140
+ expect(appium.locator_converter.inner).to equal(inner)
141
+ end
142
+ end
143
+
144
+ describe '.guard' do
145
+ it 'raises when a bridge already exists' do
146
+ existing = Selenium::WebDriver::Remote::Bridge.allocate
147
+ expect { Raider::Finders.guard }.to raise_error(Raider::Error, /before creating the driver/)
148
+ expect(existing).to be_a(Selenium::WebDriver::Remote::Bridge)
149
+ end
150
+ end
151
+
152
+ describe '.check_api' do
153
+ it 'raises when the bridge cannot take a converter' do
154
+ allow(Selenium::WebDriver::Remote::Bridge).to receive(:respond_to?).and_call_original
155
+ allow(Selenium::WebDriver::Remote::Bridge).to receive(:respond_to?).with(:locator_converter=).and_return(false)
156
+ expect { Raider::Finders.check_api }.to raise_error(Raider::Error, /locator_converter=/)
157
+ end
158
+
159
+ it 'raises when SearchContext cannot take extra finders' do
160
+ allow(search_context).to receive(:respond_to?).and_call_original
161
+ allow(search_context).to receive(:respond_to?).with(:extra_finders=).and_return(false)
162
+ expect { Raider::Finders.check_api }.to raise_error(Raider::Error, /extra_finders=/)
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,70 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Actionability fixture</title>
6
+ <style>
7
+ html { overflow-x: hidden; }
8
+ body { margin: 0; padding: 80px 20px 0; font-family: sans-serif; }
9
+ #overlay { position: fixed; top: 0; left: 0; width: 220px; height: 60px; background: #c00; opacity: 0.9; z-index: 10; }
10
+ #covered { position: absolute; top: 10px; left: 10px; width: 120px; height: 30px; }
11
+ #hidden-display { display: none; }
12
+ #hidden-visibility { visibility: hidden; }
13
+ #moving { position: relative; animation: slide 60s linear forwards; }
14
+ #bouncer { position: absolute; top: 1200px; left: 600px; animation: fall 60s linear forwards; }
15
+ @keyframes fall { from { transform: translateY(0); } to { transform: translateY(2000px); } }
16
+ #hidden-box { display: none; }
17
+ @keyframes slide { from { transform: translateX(0); } to { transform: translateX(3000px); } }
18
+ #far { position: absolute; top: 3000px; left: 400px; }
19
+ #sticky-zone { position: absolute; top: 4000px; left: 0; width: 100%; height: 2500px; }
20
+ #top { position: sticky; top: 0; height: 70vh; background: #036; opacity: 0.9; z-index: 5; }
21
+ #under-header { position: absolute; top: 1500px; left: 400px; }
22
+ #scroller { width: 300px; height: 120px; overflow: auto; border: 1px solid #999; }
23
+ #scroller .pad { height: 300px; }
24
+ #zero { position: absolute; top: 0; left: 0; width: 0; height: 0; padding: 0; border: 0; overflow: hidden; }
25
+ #file { position: absolute; width: 0; height: 0; opacity: 0; }
26
+ </style>
27
+ </head>
28
+ <body>
29
+ <div id="overlay"></div>
30
+ <button id="covered">Covered</button>
31
+
32
+ <p><button id="plain">Plain</button></p>
33
+ <p><button id="hidden-display">Gone</button> <button id="hidden-visibility">Invisible</button></p>
34
+ <p><button id="disabled" disabled>Disabled</button></p>
35
+ <fieldset id="locked" disabled>
36
+ <legend><button id="legend-button">Legend</button></legend>
37
+ <button id="in-fieldset">In fieldset</button>
38
+ </fieldset>
39
+ <p>
40
+ <input id="readonly" type="text" readonly value="read only">
41
+ <input id="writable" type="text" value="writable">
42
+ <textarea id="area"></textarea>
43
+ <div id="editable" contenteditable="true">Edit me</div>
44
+ </p>
45
+ <div id="inert-region" inert><button id="in-inert">Inert</button></div>
46
+ <dialog id="dlg"><button id="in-dialog">Inside</button><div id="shadow-host"></div></dialog>
47
+ <div id="scroller"><div class="pad"></div><button id="deep" onclick="this.setAttribute('data-clicked', '1')">Deep</button></div>
48
+ <button id="zero">Zero</button>
49
+ <p id="many"><button id="m0">0</button><button id="m1">1</button><button id="m2">2</button><button id="m3">3</button><button id="m4">4</button><button id="m5">5</button><button id="m6">6</button><button id="m7">7</button><button id="m8">8</button><button id="m9">9</button></p>
50
+ <p><button id="outside-dialog">Outside</button></p>
51
+ <p><button id="moving">Moving</button></p>
52
+ <p>
53
+ <select id="choices"><option id="opt-a" value="a">A</option><option id="opt-b" value="b">B</option><option id="opt-blue" value="blue">Blue</option></select>
54
+ <label>File <input id="file" type="file"></label>
55
+ </p>
56
+
57
+ <button id="bouncer">Bouncing</button>
58
+ <div id="hidden-box"><input id="file-hidden" type="file"></div>
59
+ <button id="far" onclick="this.setAttribute('data-clicked', '1')">Far away</button>
60
+ <div id="sticky-zone">
61
+ <header id="top">Sticky header</header>
62
+ <button id="under-header">Under the header</button>
63
+ </div>
64
+ <details id="collapsed"><summary>More</summary><button id="in-details">Hidden in details</button></details>
65
+ <div style="height: 7000px"></div>
66
+ <script>
67
+ document.getElementById('shadow-host').attachShadow({ mode: 'open' }).innerHTML = '<button id="shadow-button">Shadow</button>';
68
+ </script>
69
+ </body>
70
+ </html>
@@ -0,0 +1,46 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head><meta charset="utf-8"><title>Sign in</title></head>
4
+ <body>
5
+ <script type="application/json">{"action": "Sign in"}</script>
6
+ <form>
7
+ <label for="email">Email</label>
8
+ <input id="email" type="text">
9
+
10
+ <label for="email-address">Email address</label>
11
+ <input id="email-address" type="text">
12
+
13
+ <label for="password">Password</label>
14
+ <input id="password" type="password">
15
+
16
+ <label>Username <input type="hidden" name="remember" value="0"><input id="username" type="text"></label>
17
+
18
+ <input id="search" type="text" aria-label="Search site">
19
+
20
+ <span id="phone-label">Phone number</span>
21
+ <input id="phone" type="text" aria-labelledby="phone-label">
22
+
23
+ <input id="code" type="text" placeholder="Enter code">
24
+ <input id="name-field" type="text" placeholder="Enter your full name">
25
+ </form>
26
+
27
+ <section id="scope">
28
+ <label for="scoped-email">Email</label>
29
+ <input id="scoped-email" type="text">
30
+ </section>
31
+
32
+ <img id="logo" src="data:image/gif;base64,R0lGODlhAQABAAAAACw=" alt="Company logo">
33
+ <a id="docs-link" href="#docs" title="Read the docs">Docs</a>
34
+
35
+ <div id="nested"><p id="sign-in">Sign in</p></div>
36
+
37
+ <ul>
38
+ <li id="item-one" data-testid="item-1">One</li>
39
+ <li id="item-two" data-testid="item-2">Two</li>
40
+ </ul>
41
+
42
+ <button id="save-a" class="primary">Save</button>
43
+ <button id="save-b" class="secondary">Save</button>
44
+ <button id="save-c" class="tertiary">Save</button>
45
+ </body>
46
+ </html>
@@ -0,0 +1,33 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Matchers</title>
6
+ </head>
7
+ <body>
8
+ <h1 id="heading">Loading</h1>
9
+ <input id="name" type="text" value="Ann">
10
+ <button id="hide-me" type="button">Hide me</button>
11
+ <ul id="items"><li>One</li></ul>
12
+ <a id="finish" href="#">Finish</a>
13
+ <script>
14
+ setTimeout(function () { document.getElementById('heading').textContent = 'Ready'; }, 500);
15
+ document.getElementById('hide-me').addEventListener('click', function (event) {
16
+ setTimeout(function () { event.target.style.display = 'none'; }, 300);
17
+ });
18
+ setTimeout(function () {
19
+ var list = document.getElementById('items');
20
+ ['Two', 'Three'].forEach(function (text) {
21
+ var item = document.createElement('li');
22
+ item.textContent = text;
23
+ list.appendChild(item);
24
+ });
25
+ }, 400);
26
+ document.getElementById('finish').addEventListener('click', function (event) {
27
+ event.preventDefault();
28
+ document.title = 'Done';
29
+ location.hash = '#done';
30
+ });
31
+ </script>
32
+ </body>
33
+ </html>
@@ -0,0 +1,162 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" id="r-html">
3
+ <head><meta charset="utf-8"><title>Roles fixture</title></head>
4
+ <body>
5
+ <h1 id="r-h1">Roles</h1><h2 id="r-h2">Two</h2><h3 id="r-h3">Three</h3><h4 id="r-h4">Four</h4><h5 id="r-h5">Five</h5><h6 id="r-h6">Six</h6>
6
+
7
+ <!-- implicit roles -->
8
+ <a id="r-link" href="#x">Link</a>
9
+ <a id="r-anchor" name="x">Anchor</a>
10
+ <map name="m"><area id="r-area" href="#x" alt="Area alt" shape="rect" coords="0,0,1,1"></map>
11
+ <article id="r-article">Article <header id="r-header-in-article">Inner header</header><footer id="r-footer-in-article">Inner footer</footer></article>
12
+ <aside id="r-aside">Aside</aside>
13
+ <button id="r-button">Plain button</button>
14
+ <datalist id="r-datalist"><option value="a"></option></datalist>
15
+ <details id="r-details"><summary id="r-summary">Summary</summary>Body</details>
16
+ <dialog id="r-dialog">Dialog</dialog>
17
+ <fieldset id="r-fieldset"><legend>Group</legend></fieldset>
18
+ <figure id="r-figure">Figure</figure>
19
+ <header id="r-header">Top header</header>
20
+ <footer id="r-footer">Bottom footer</footer>
21
+ <form id="r-form"></form>
22
+ <hr id="r-hr">
23
+ <img id="r-img" src="data:," alt="Photo">
24
+ <img id="r-img-empty-alt" src="data:," alt="">
25
+ <img id="r-img-presentation" src="data:," alt="Decor" role="presentation">
26
+ <input id="r-in-button" type="button" value="B">
27
+ <input id="r-in-image" type="image" alt="I" src="data:,">
28
+ <input id="r-in-reset" type="reset">
29
+ <input id="r-in-submit" type="submit" value="S">
30
+ <input id="r-in-checkbox" type="checkbox">
31
+ <input id="r-in-radio" type="radio">
32
+ <input id="r-in-range" type="range">
33
+ <input id="r-in-number" type="number">
34
+ <input id="r-in-search" type="search">
35
+ <input id="r-in-search-list" type="search" list="r-datalist">
36
+ <input id="r-in-email" type="email">
37
+ <input id="r-in-tel" type="tel">
38
+ <input id="r-in-text" type="text">
39
+ <input id="r-in-url" type="url">
40
+ <input id="r-in-password" type="password">
41
+ <input id="r-in-default">
42
+ <input id="r-in-text-list" type="text" list="r-datalist">
43
+ <input id="r-in-hidden" type="hidden">
44
+ <ul id="r-ul"><li id="r-li">Item</li></ul>
45
+ <ol id="r-ol"><li>One</li></ol>
46
+ <menu id="r-menu"><li>M</li></menu>
47
+ <li id="r-li-orphan">Orphan</li>
48
+ <main id="r-main">Main</main>
49
+ <math id="r-math"><mi>x</mi></math>
50
+ <meter id="r-meter" value="0.5"></meter>
51
+ <nav id="r-nav">Nav</nav>
52
+ <select id="r-select"><option id="r-option" value="1">One</option></select>
53
+ <select id="r-select-multiple" multiple><option>One</option></select>
54
+ <select id="r-select-size" size="3"><option>One</option></select>
55
+ <output id="r-output">0</output>
56
+ <progress id="r-progress" value="1" max="2"></progress>
57
+ <section id="r-section">Unnamed section</section>
58
+ <section id="r-section-label" aria-label="Named section">Named</section>
59
+ <span id="r-lb-target">Heading words</span>
60
+ <section id="r-section-labelledby" aria-labelledby="r-lb-target">Named by id</section>
61
+ <table id="r-table">
62
+ <caption id="r-caption">Cap</caption>
63
+ <thead id="r-thead"><tr id="r-tr"><th id="r-th-col">Name</th><th id="r-th-colscope" scope="col">Age</th></tr></thead>
64
+ <tbody id="r-tbody">
65
+ <tr><th id="r-th-first">Row one</th><td id="r-td">Cell</td></tr>
66
+ <tr><td>x</td><th id="r-th-second">Late th</th><th id="r-th-scoperow" scope="row">Scoped row</th></tr>
67
+ </tbody>
68
+ <tfoot id="r-tfoot"><tr><td>f</td></tr></tfoot>
69
+ </table>
70
+ <table><tbody><tr><th id="r-th-only-1">Name</th><th id="r-th-only-2">Age</th></tr></tbody></table>
71
+ <table role="grid"><tbody><tr><td id="r-td-grid">Grid cell</td></tr></tbody></table>
72
+ <div role="grid"><div role="row"><table><tbody><tr><td id="r-td-grid-ancestor">Deep</td></tr></tbody></table></div></div>
73
+ <textarea id="r-textarea"></textarea>
74
+ <div id="r-div">Div</div>
75
+ <span id="r-span">Span</span>
76
+ <div id="r-div-button" role="button">Div button</div>
77
+ <div id="r-role-tokens" role="bogus button">Tokens</div>
78
+ <button id="r-bad-role" role="bogus">Bad role</button>
79
+ <a id="r-explicit-over-implicit" href="#x" role="button">Link as button</a>
80
+
81
+ <p id="r-p">Para</p><blockquote id="r-blockquote">Quote</blockquote><code id="r-code">c</code><em id="r-em">e</em>
82
+ <strong id="r-strong">s</strong><del id="r-del">d</del><ins id="r-ins">i</ins><sub id="r-sub">b</sub><sup id="r-sup">p</sup>
83
+ <time id="r-time">now</time><dfn id="r-dfn">term</dfn><dl><dt id="r-dt">T</dt><dd id="r-dd">D</dd></dl>
84
+ <search id="r-search">Search</search><address id="r-address">Addr</address>
85
+ <select id="o-select"><optgroup id="r-optgroup" label="G"><option id="o-option">Pick me</option></optgroup><option id="o-other">Other</option></select>
86
+ <div style="display:contents"><button id="dc-button" style="display:contents">Contents button</button></div>
87
+ <div id="lv-aria" role="heading" aria-level="2">Aria two</div>
88
+ <div id="shadow-host"></div>
89
+ <script>
90
+ document.getElementById('shadow-host').attachShadow({ mode: 'open' }).innerHTML =
91
+ '<span id="sl">Labelled inner</span><button>Shadow inner</button><button aria-labelledby="sl">x</button>';
92
+ </script>
93
+
94
+ <!-- accessible names -->
95
+ <input id="n-placeholder" placeholder="Email address">
96
+ <input id="n-placeholder-title" title="Title wins" placeholder="Ignored">
97
+ <textarea id="n-placeholder-area" placeholder="Say more"></textarea>
98
+ <span id="n-ll1">First</span>
99
+ <span id="n-ll2" aria-label="Second label">ignored text</span>
100
+ <button id="n-labelledby" aria-labelledby="n-ll1 n-ll2">own text</button>
101
+ <span id="n-hid" style="display:none">Hidden <b>label</b></span>
102
+ <button id="n-lb-hidden" aria-labelledby="n-hid">own</button>
103
+ <span id="n-nest" aria-labelledby="n-ll1">Nest text</span>
104
+ <button id="n-lb-nested" aria-labelledby="n-nest">own</button>
105
+ <button id="n-lb-missing" aria-labelledby="nope-nothing">Falls back</button>
106
+ <button id="n-arialabel" aria-label="Close dialog">X</button>
107
+ <label for="n-labelfor">Email
108
+ address</label><input id="n-labelfor" type="text">
109
+ <label>Wrapped label <input id="n-wrapped" type="text"></label>
110
+ <img id="n-img" src="data:," alt="Company logo">
111
+ <input id="n-input-image" type="image" src="data:," alt="Go">
112
+ <map name="m2"><area id="n-area" href="#x" alt="Area name" shape="rect" coords="0,0,1,1"></map>
113
+ <input id="n-submit" type="submit" value="Send">
114
+ <input id="n-input-button" type="button" value="Plain value">
115
+ <input id="n-reset" type="reset" value="Clear">
116
+ <input id="n-submit-empty" type="submit">
117
+ <table id="n-table"><caption>Sales</caption><tbody><tr><td>1</td></tr></tbody></table>
118
+ <fieldset id="n-fieldset"><legend>Contact</legend><input type="text"></fieldset>
119
+ <figure id="n-figure"><img src="data:," alt=""><figcaption>Chart caption</figcaption></figure>
120
+ <button id="n-title" title="Tooltip only"></button>
121
+ <a id="n-title-fallback" href="#x" title="ignored title">Text wins</a>
122
+ <a id="n-embedded" href="#x">Search for <input type="text" value="cats"></a>
123
+ <a id="n-slider" href="#x">Rate <input type="range" min="0" max="10" value="7"> stars</a>
124
+ <a id="n-select" href="#x">Pick <select><option>One</option><option selected>Two</option></select></a>
125
+ <button id="n-content">Go <img src="data:," alt="icon"> now</button>
126
+ <a id="n-blocks" href="#x"><div>One</div><div>Two</div></a>
127
+ <a id="n-inline" href="#x"><span>Ab</span><span>cd</span></a>
128
+ <h2 id="n-heading"> Spaced
129
+ title </h2>
130
+ <button id="n-hidden-child">Visible<span style="display:none"> secret</span><span aria-hidden="true"> aria</span></button>
131
+ <div id="n-div-button" role="button">Div button</div>
132
+ <div id="n-generic">Generic text</div>
133
+ <ul><li id="n-li">Item text</li></ul>
134
+ <button id="n-unreferenced-hidden" style="display:none">Nope</button>
135
+ <input id="n-unlabelled" type="text">
136
+
137
+ <section id="cy-self" aria-labelledby="cy-self">Self</section>
138
+ <section id="cy-a" aria-labelledby="cy-b">A</section>
139
+ <section id="cy-b" aria-labelledby="cy-a">B</section>
140
+ <button id="cy-button" aria-labelledby="cy-button">Self button</button>
141
+
142
+ <!-- hidden cases -->
143
+ <button id="h-visible">Shown</button>
144
+ <button id="h-aria-hidden" aria-hidden="true">Aria hidden</button>
145
+ <div aria-hidden="true"><button id="h-anc-aria">Under aria-hidden</button></div>
146
+ <div style="display:none"><button id="h-anc-none">Under display none</button></div>
147
+ <button id="h-display-none" style="display:none">Display none</button>
148
+ <button id="h-attr" hidden>Hidden attribute</button>
149
+ <button id="h-visibility" style="visibility:hidden">Visibility hidden</button>
150
+ <div style="visibility:hidden"><button id="h-vis-parent-child" style="visibility:visible">Visible child</button><button id="h-vis-inherit">Inherits hidden</button></div>
151
+
152
+ <!-- queries -->
153
+ <div id="saves">
154
+ <button id="save-1">Save</button>
155
+ <button id="save-2">Save as</button>
156
+ <button id="save-3">save</button>
157
+ <button id="save-hidden" style="display:none">Save later</button>
158
+ </div>
159
+ <div id="scope"><button id="scope-save">Save</button></div>
160
+ <div id="scope-button" role="button"><button id="scope-inner">Inner</button></div>
161
+ </body>
162
+ </html>