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,315 @@
1
+ (function () {
2
+ 'use strict';
3
+ if (window.__raider && window.__raider.role) { return; }
4
+
5
+ // Keep in step with Raider::ROLES in lib/ruby_raider/runtime/by_role.rb.
6
+ var ROLES = ('alert alertdialog application article banner blockquote button caption cell checkbox code ' +
7
+ 'columnheader combobox complementary contentinfo definition deletion dialog directory document emphasis feed ' +
8
+ 'figure form generic grid gridcell group heading img insertion link list listbox listitem log main marquee math ' +
9
+ 'menu menubar menuitem menuitemcheckbox menuitemradio meter navigation none note option paragraph presentation ' +
10
+ 'progressbar radio radiogroup region row rowgroup rowheader scrollbar search searchbox separator slider ' +
11
+ 'spinbutton status strong subscript superscript switch tab table tablist tabpanel term textbox time timer ' +
12
+ 'toolbar tooltip tree treegrid treeitem').split(' ');
13
+
14
+ var NAME_FROM_CONTENT = ('button link heading cell option tab menuitem treeitem row gridcell columnheader ' +
15
+ 'rowheader switch checkbox radio tooltip menuitemcheckbox menuitemradio').split(' ');
16
+
17
+ // Implicit roles by tag; tags that need a decision are handled in implicitRole.
18
+ var TAG_ROLES = {
19
+ article: 'article', aside: 'complementary', button: 'button', datalist: 'listbox', details: 'group',
20
+ dialog: 'dialog', fieldset: 'group', figure: 'figure', form: 'form', h1: 'heading', h2: 'heading',
21
+ h3: 'heading', h4: 'heading', h5: 'heading', h6: 'heading', hr: 'separator', main: 'main', math: 'math',
22
+ menu: 'list', ol: 'list', ul: 'list', meter: 'meter', nav: 'navigation', option: 'option', output: 'status',
23
+ progress: 'progressbar', table: 'table', tbody: 'rowgroup', thead: 'rowgroup', tfoot: 'rowgroup',
24
+ td: 'cell', textarea: 'textbox', tr: 'row', div: 'generic', span: 'generic', p: 'paragraph',
25
+ blockquote: 'blockquote', code: 'code', em: 'emphasis', strong: 'strong', del: 'deletion', ins: 'insertion',
26
+ sub: 'subscript', sup: 'superscript', time: 'time', dfn: 'term', dt: 'term', dd: 'definition',
27
+ caption: 'caption', html: 'document', search: 'search', address: 'group', optgroup: 'group'
28
+ };
29
+
30
+ var PLACEHOLDER_TYPES = ['text', 'search', 'url', 'tel', 'email', 'password', 'number'];
31
+
32
+ var INPUT_ROLES = {
33
+ button: 'button', image: 'button', reset: 'button', submit: 'button', checkbox: 'checkbox', radio: 'radio',
34
+ range: 'slider', number: 'spinbutton', search: 'searchbox', email: 'textbox', tel: 'textbox',
35
+ text: 'textbox', url: 'textbox', password: 'textbox'
36
+ };
37
+
38
+ var LIST_INPUTS = ['search', 'email', 'tel', 'text', 'url'];
39
+
40
+ // A label or aria-labelledby id resolves in the element's own tree, so shadow roots work.
41
+ function byId(el, id) {
42
+ var root = el.getRootNode();
43
+ return root && root.getElementById ? root.getElementById(id) : el.ownerDocument.getElementById(id);
44
+ }
45
+
46
+ function has(object, key) { return Object.prototype.hasOwnProperty.call(object, key); }
47
+
48
+ function normalize(text) { return String(text).replace(/\s+/g, ' ').trim(); }
49
+
50
+ function explicitRole(el) {
51
+ var tokens = (el.getAttribute('role') || '').toLowerCase().split(/\s+/);
52
+ for (var i = 0; i < tokens.length; i++) {
53
+ if (ROLES.indexOf(tokens[i]) >= 0) { return tokens[i]; }
54
+ }
55
+ return null;
56
+ }
57
+
58
+ function insideGrid(el) {
59
+ for (var n = el.parentElement; n; n = n.parentElement) {
60
+ var role = explicitRole(n);
61
+ if (role === 'grid' || role === 'treegrid') { return true; }
62
+ }
63
+ return false;
64
+ }
65
+
66
+ function inputRole(el) {
67
+ var type = el.type;
68
+ if (type === 'hidden') { return null; }
69
+ if (el.hasAttribute('list') && LIST_INPUTS.indexOf(type) >= 0) { return 'combobox'; }
70
+ return has(INPUT_ROLES, type) ? INPUT_ROLES[type] : null;
71
+ }
72
+
73
+ function headerRole(el, landmark) {
74
+ var parent = el.parentElement;
75
+ return parent && parent.closest('article, aside, main, nav, section') ? 'generic' : landmark;
76
+ }
77
+
78
+ function hasDataCell(row) {
79
+ for (var i = 0; i < row.cells.length; i++) {
80
+ if (row.cells[i].localName === 'td') { return true; }
81
+ }
82
+ return false;
83
+ }
84
+
85
+ function thRole(el) {
86
+ var scope = (el.getAttribute('scope') || '').toLowerCase();
87
+ if (scope === 'row') { return 'rowheader'; }
88
+ if (scope === 'col') { return 'columnheader'; }
89
+ var row = el.parentElement;
90
+ var first = row && row.localName === 'tr' && el.cellIndex === 0;
91
+ return first && hasDataCell(row) && !el.closest('thead') ? 'rowheader' : 'columnheader';
92
+ }
93
+
94
+ function implicitRole(el) {
95
+ var tag = el.localName;
96
+ switch (tag) {
97
+ case 'a': return el.hasAttribute('href') ? 'link' : 'generic';
98
+ case 'area': return el.hasAttribute('href') ? 'link' : 'generic';
99
+ case 'header': return headerRole(el, 'banner');
100
+ case 'footer': return headerRole(el, 'contentinfo');
101
+ case 'img': return el.getAttribute('alt') === '' ? 'presentation' : 'img';
102
+ case 'input': return inputRole(el);
103
+ case 'li': return el.parentElement && /^(ul|ol|menu)$/.test(el.parentElement.localName) ? 'listitem' : 'generic';
104
+ case 'section': return isLabelled(el) ? 'region' : 'generic';
105
+ case 'select': return el.multiple || el.size > 1 ? 'listbox' : 'combobox';
106
+ case 'td': return insideGrid(el) ? 'gridcell' : 'cell';
107
+ case 'th': return thRole(el);
108
+ case 'summary': return null;
109
+ default: return has(TAG_ROLES, tag) ? TAG_ROLES[tag] : null;
110
+ }
111
+ }
112
+
113
+ // Cheap and non-recursive: never computes a name, so getRole cannot re-enter nameOf.
114
+ function isLabelled(el) {
115
+ var label = el.getAttribute('aria-label');
116
+ if (label && normalize(label)) { return true; }
117
+ var ids = (el.getAttribute('aria-labelledby') || '').split(/\s+/);
118
+ for (var i = 0; i < ids.length; i++) {
119
+ var ref = ids[i] ? byId(el, ids[i]) : null;
120
+ if (ref && (normalize(ref.textContent) || normalize(ref.getAttribute('aria-label') || ''))) { return true; }
121
+ }
122
+ return false;
123
+ }
124
+
125
+ function getRole(el) { return explicitRole(el) || implicitRole(el); }
126
+
127
+ function hiddenSelf(el) {
128
+ if (el.getAttribute('aria-hidden') === 'true') { return true; }
129
+ if (el.localName === 'input' && el.type === 'hidden') { return true; }
130
+ var style = window.getComputedStyle(el);
131
+ return (style.display === 'none' && el.localName !== 'area') || style.visibility === 'hidden' || style.visibility === 'collapse';
132
+ }
133
+
134
+ // The element whose layout box decides visibility: options of a closed select and display: contents
135
+ // elements have no box of their own.
136
+ function boxOwner(el) {
137
+ var n = el.localName === 'option' ? el.closest('select') || el : el;
138
+ while (n.parentElement && window.getComputedStyle(n).display === 'contents') { n = n.parentElement; }
139
+ return n;
140
+ }
141
+
142
+ function isHidden(el) {
143
+ if (el.closest('[aria-hidden="true"]')) { return true; }
144
+ var owner = boxOwner(el);
145
+ if (el.localName !== 'area' && owner.checkVisibility && !owner.checkVisibility()) { return true; }
146
+ for (var n = el; n && n.nodeType === 1; n = n.parentElement) {
147
+ if (n.localName !== 'area' && window.getComputedStyle(n).display === 'none') { return true; }
148
+ }
149
+ return hiddenSelf(el);
150
+ }
151
+
152
+ function textOfIds(el, attribute) {
153
+ var ids = (el.getAttribute(attribute) || '').split(/\s+/);
154
+ var parts = [];
155
+ for (var i = 0; i < ids.length; i++) {
156
+ var ref = ids[i] ? byId(el, ids[i]) : null;
157
+ if (ref) { parts.push(nameOf(ref, { inRef: true, content: true, ref: true })); }
158
+ }
159
+ return parts.join(' ');
160
+ }
161
+
162
+ function ariaName(el) {
163
+ if (el.hasAttribute('aria-labelledby')) {
164
+ var referenced = textOfIds(el, 'aria-labelledby');
165
+ if (normalize(referenced)) { return referenced; }
166
+ }
167
+ var label = el.getAttribute('aria-label');
168
+ return label && normalize(label) ? label : '';
169
+ }
170
+
171
+ function selectedText(select) {
172
+ var out = [];
173
+ for (var i = 0; i < select.selectedOptions.length; i++) { out.push(select.selectedOptions[i].textContent); }
174
+ return out.join(' ');
175
+ }
176
+
177
+ function embeddedValue(el, role) {
178
+ if (role === 'textbox') { return 'value' in el ? el.value : el.textContent; }
179
+ if (role === 'combobox' || role === 'listbox') {
180
+ if (el.localName === 'select') { return selectedText(el); }
181
+ return 'value' in el ? el.value : '';
182
+ }
183
+ if (role === 'slider' || role === 'spinbutton') {
184
+ return el.getAttribute('aria-valuetext') || el.getAttribute('aria-valuenow') || el.value || '';
185
+ }
186
+ return '';
187
+ }
188
+
189
+ function inlineDisplay(el) {
190
+ var display = window.getComputedStyle(el).display;
191
+ return display === 'contents' || display.indexOf('inline') === 0;
192
+ }
193
+
194
+ function childrenText(el, o) {
195
+ var out = '';
196
+ for (var n = el.firstChild; n; n = n.nextSibling) {
197
+ if (n.nodeType === 3) {
198
+ out += n.data;
199
+ } else if (n.nodeType === 1) {
200
+ var text = nameOf(n, { inRef: o.inRef, content: true, skip: o.skip, ref: false });
201
+ out += inlineDisplay(n) ? text : ' ' + text + ' ';
202
+ }
203
+ }
204
+ return out;
205
+ }
206
+
207
+ function labelsText(el, o) {
208
+ var out = [];
209
+ var labels = el.labels || [];
210
+ for (var i = 0; i < labels.length; i++) {
211
+ out.push(childrenText(labels[i], { inRef: o.inRef, skip: el }));
212
+ }
213
+ return out.join(' ');
214
+ }
215
+
216
+ function childElement(el, tag) {
217
+ for (var n = el.firstElementChild; n; n = n.nextElementSibling) {
218
+ if (n.localName === tag) { return n; }
219
+ }
220
+ return null;
221
+ }
222
+
223
+ function nativeName(el, o) {
224
+ var tag = el.localName;
225
+ var type = tag === 'input' ? el.type : '';
226
+ var text = '';
227
+ if (type === 'button' || type === 'submit' || type === 'reset') { return el.value || ''; }
228
+ if (tag === 'img' || tag === 'area' || type === 'image') {
229
+ text = el.getAttribute('alt') || '';
230
+ } else if (tag === 'table') {
231
+ text = el.caption ? childrenText(el.caption, { inRef: o.inRef }) : '';
232
+ } else if (tag === 'fieldset' || tag === 'figure') {
233
+ var holder = childElement(el, tag === 'fieldset' ? 'legend' : 'figcaption');
234
+ text = holder ? childrenText(holder, { inRef: o.inRef }) : '';
235
+ } else if (el.labels) {
236
+ text = labelsText(el, o);
237
+ }
238
+ return text;
239
+ }
240
+
241
+ // o.top: the element asked about; o.ref: named by aria-labelledby (hidden state ignored);
242
+ // o.inRef: inside a labelledby walk (no second labelledby); o.content: walking a name-from-content subtree.
243
+ var computing = [];
244
+
245
+ function nameOf(el, o) {
246
+ if (el === o.skip || computing.indexOf(el) >= 0) { return ''; }
247
+ computing.push(el);
248
+ try {
249
+ return computeName(el, o);
250
+ } finally {
251
+ computing.pop();
252
+ }
253
+ }
254
+
255
+ function computeName(el, o) {
256
+ if (!o.ref && (o.top ? isHidden(el) : hiddenSelf(el))) { return ''; }
257
+ var role = getRole(el);
258
+ var text = o.inRef ? '' : ariaName(el);
259
+ if (!o.inRef && normalize(text)) { return text; }
260
+ var label = o.inRef ? el.getAttribute('aria-label') : '';
261
+ if (label && normalize(label)) { return label; }
262
+ if (!o.top && normalize(embeddedValue(el, role))) { return embeddedValue(el, role); }
263
+ text = nativeName(el, o);
264
+ if (normalize(text)) { return text; }
265
+ if (o.content || o.top && NAME_FROM_CONTENT.indexOf(role) >= 0) {
266
+ text = childrenText(el, o);
267
+ if (normalize(text)) { return text; }
268
+ }
269
+ var title = el.getAttribute('title');
270
+ return title && normalize(title) ? title : placeholderOf(el);
271
+ }
272
+
273
+ function placeholderOf(el) {
274
+ var typed = el.localName === 'input' && PLACEHOLDER_TYPES.indexOf(el.type) >= 0;
275
+ return typed || el.localName === 'textarea' ? el.getAttribute('placeholder') || '' : '';
276
+ }
277
+
278
+ function nameWithin(el, includeHidden) {
279
+ return normalize(nameOf(el, { top: true, ref: includeHidden }));
280
+ }
281
+
282
+ function accessibleName(el) { return nameWithin(el, false); }
283
+
284
+ function headingLevel(el) {
285
+ var explicit = parseInt(el.getAttribute('aria-level'), 10);
286
+ if (explicit >= 1) { return explicit; }
287
+ var tagged = /^h([1-6])$/.exec(el.localName);
288
+ return tagged ? Number(tagged[1]) : 2;
289
+ }
290
+
291
+ function role(scope, wanted, options) {
292
+ var root = scope || document;
293
+ var opts = options || {};
294
+ var named = opts.name !== null && opts.name !== undefined;
295
+ var needle = named ? normalize(opts.name) : '';
296
+ var all = root.querySelectorAll('*');
297
+ var found = [];
298
+ for (var i = 0; i < all.length; i++) {
299
+ var el = all[i];
300
+ if (getRole(el) !== wanted) { continue; }
301
+ if (opts.level !== null && opts.level !== undefined && headingLevel(el) !== opts.level) { continue; }
302
+ if (!opts.includeHidden && isHidden(el)) { continue; }
303
+ if (named) {
304
+ var name = nameWithin(el, opts.includeHidden === true);
305
+ var ok = opts.exact ? name === needle : name.toLowerCase().indexOf(needle.toLowerCase()) >= 0;
306
+ if (!ok) { continue; }
307
+ }
308
+ found.push(el);
309
+ }
310
+ return found;
311
+ }
312
+
313
+ window.__raider = window.__raider || {};
314
+ Object.assign(window.__raider, { role: role, getRole: getRole, accessibleName: accessibleName, isHidden: isHidden });
315
+ })();
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'selenium-webdriver'
4
+ require_relative 'errors'
5
+ require_relative 'selectors'
6
+
7
+ module Raider
8
+ # Wraps a bridge's locator converter: translates the Raider wire names into
9
+ # XPath or CSS through Raider::Selectors and hands every other locator,
10
+ # Appium's included, to the converter it wraps untouched.
11
+ class LocatorConverter
12
+ XPATH = {
13
+ 'raider label' => :label,
14
+ 'raider text' => :text
15
+ }.freeze
16
+ ATTRIBUTES = {
17
+ 'raider placeholder' => 'placeholder',
18
+ 'raider alt' => 'alt',
19
+ 'raider title' => 'title'
20
+ }.freeze
21
+ TEST_ID = 'raider test id'
22
+ ROLE = 'raider role'
23
+
24
+ attr_reader :inner
25
+
26
+ def initialize(inner)
27
+ @inner = inner
28
+ end
29
+
30
+ def convert(how, what)
31
+ key = how.to_s
32
+ return inner.convert(how, what) unless owns?(key)
33
+ raise NotSupported, 'internal error: the role finder did not intercept this role locator' if key == ROLE
34
+
35
+ value, exact = split(key, what)
36
+ raise ArgumentError, "#{label_for(key)}: needs a non-empty value" if value.to_s.strip.empty?
37
+
38
+ translate(key, value, exact)
39
+ end
40
+
41
+ private
42
+
43
+ def owns?(key)
44
+ XPATH.key?(key) || ATTRIBUTES.key?(key) || [TEST_ID, ROLE].include?(key)
45
+ end
46
+
47
+ def translate(key, value, exact)
48
+ return ['xpath', Selectors.public_send(XPATH.fetch(key), value, exact:)] if XPATH.key?(key)
49
+ return ['css selector', Selectors.attribute_css(ATTRIBUTES.fetch(key), value, exact:)] if ATTRIBUTES.key?(key)
50
+
51
+ ['css selector', Selectors.test_id(value)]
52
+ end
53
+
54
+ def split(key, what)
55
+ return [what, false] unless what.is_a?(Array)
56
+
57
+ value, options = what
58
+ raise ArgumentError, "#{label_for(key)}: expected a value or [value, { exact: }]" unless valid_pair?(what)
59
+
60
+ [value, options&.fetch(:exact, false) ? true : false]
61
+ end
62
+
63
+ def valid_pair?(pair)
64
+ value, options = pair
65
+ (value.is_a?(String) || value.is_a?(Symbol)) && (options.nil? || options.is_a?(Hash)) && pair.size <= 2
66
+ end
67
+
68
+ def label_for(key)
69
+ key.delete_prefix('raider ').tr(' ', '_')
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/expectations'
4
+ require 'selenium-webdriver'
5
+ require_relative '../config'
6
+ require_relative '../errors'
7
+ require_relative '../wait'
8
+
9
+ module Raider
10
+ module Matchers
11
+ # Shared polling for the web-first matchers. Subclasses implement `read(subject)`,
12
+ # `pass?(value)` and `verb`, and may override `absent_passes?`.
13
+ class Base
14
+ include RSpec::Matchers::Composable
15
+
16
+ # `and` / `or` that build the sequential compounds, so every link of a chain
17
+ # stays sequential.
18
+ module Chaining
19
+ def and(other)
20
+ Base::And.new(self, other)
21
+ end
22
+ alias & and
23
+
24
+ def or(other)
25
+ Base::Or.new(self, other)
26
+ end
27
+ alias | or
28
+ end
29
+
30
+ include Chaining
31
+
32
+ # Compound evaluation that hands each matcher the actual as is, so a block
33
+ # actual is resolved by each matcher itself rather than nested.
34
+ module Sequential
35
+ include Chaining
36
+
37
+ private
38
+
39
+ def evaluate(actual)
40
+ @evaluator = RSpec::Matchers::BuiltIn::Compound::SequentialEvaluator.new(actual, matcher_1, matcher_2)
41
+ end
42
+ end
43
+
44
+ # `a.and(b)` evaluated sequentially.
45
+ class And < RSpec::Matchers::BuiltIn::Compound::And
46
+ include Sequential
47
+
48
+ private
49
+
50
+ def match(_expected, actual)
51
+ evaluate(actual)
52
+ matcher_1_matches? && matcher_2_matches?
53
+ end
54
+ end
55
+
56
+ # `a.or(b)` evaluated sequentially.
57
+ class Or < RSpec::Matchers::BuiltIn::Compound::Or
58
+ include Sequential
59
+
60
+ private
61
+
62
+ def match(_expected, actual)
63
+ evaluate(actual)
64
+ matcher_1_matches? || matcher_2_matches?
65
+ end
66
+ end
67
+
68
+ NOT_FOUND = Object.new.freeze
69
+ STALE_HINT = 'the element is stale; use the block form (expect { page.element }) so it is found again'
70
+
71
+ # Raised inside the poll to end it at once: a raw element went stale.
72
+ class StaleActual < StandardError; end
73
+
74
+ # What the last poll saw, for the failure message.
75
+ Poll = Struct.new(:actual, :subject, :value, :waited, :elapsed, :stale)
76
+
77
+ attr_reader :expected
78
+
79
+ def initialize(expected, wait: nil, **options)
80
+ @expected = expected
81
+ @wait = wait
82
+ @options = options
83
+ @poll = Poll.new(nil, nil, NOT_FOUND, nil, 0, false)
84
+ end
85
+
86
+ def matches?(actual)
87
+ poll(actual, negated: false)
88
+ end
89
+
90
+ def does_not_match?(actual)
91
+ poll(actual, negated: true)
92
+ end
93
+
94
+ def failure_message
95
+ message('to')
96
+ end
97
+
98
+ def failure_message_when_negated
99
+ message('not to')
100
+ end
101
+
102
+ def supports_block_expectations?
103
+ true
104
+ end
105
+
106
+ def description
107
+ "#{verb} #{expected.inspect}"
108
+ end
109
+
110
+ def absent_passes?
111
+ false
112
+ end
113
+
114
+ def resolve(actual)
115
+ actual.is_a?(Proc) ? actual.call : actual
116
+ end
117
+
118
+ def read_with_zero_wait(subject, method, *args)
119
+ return subject.public_send(method, *args) unless accepts_timeout?(subject, method)
120
+
121
+ subject.public_send(method, *args, timeout: 0)
122
+ end
123
+
124
+ private
125
+
126
+ def option(key)
127
+ @options[key]
128
+ end
129
+
130
+ def polled_actual
131
+ @poll.actual
132
+ end
133
+
134
+ def wait_for(_actual)
135
+ @wait || Raider.timeout
136
+ end
137
+
138
+ def poll(actual, negated:)
139
+ @poll = Poll.new(actual, nil, NOT_FOUND, wait_for(actual), 0, false)
140
+ started = clock
141
+ begin
142
+ Raider::Wait.until(timeout: @poll.waited) { settled?(negated) }
143
+ true
144
+ rescue Raider::TimeoutError, StaleActual
145
+ false
146
+ ensure
147
+ @poll.elapsed = clock - started
148
+ end
149
+ end
150
+
151
+ def clock
152
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
153
+ end
154
+
155
+ def settled?(negated)
156
+ Raider::Wait.immediately do
157
+ @poll.subject = resolve(@poll.actual)
158
+ @poll.value = read(@poll.subject)
159
+ end
160
+ pass?(@poll.value) != negated || throw(:not_ready)
161
+ rescue *retried_errors => e
162
+ @poll.value = NOT_FOUND
163
+ gone(e, negated)
164
+ end
165
+
166
+ # The read found nothing: pass where absence counts, fail at once for a stale raw
167
+ # element, and otherwise try again.
168
+ def gone(error, negated)
169
+ return true if negated ? negated_absent_passes? : absent_passes?
170
+
171
+ raise_stale if error.is_a?(Selenium::WebDriver::Error::StaleElementReferenceError) && raw_element?
172
+ throw :not_ready
173
+ end
174
+
175
+ def negated_absent_passes?
176
+ false
177
+ end
178
+
179
+ def raw_element?
180
+ @poll.actual.is_a?(Selenium::WebDriver::Element)
181
+ end
182
+
183
+ def raise_stale
184
+ @poll.stale = true
185
+ raise StaleActual
186
+ end
187
+
188
+ def retried_errors
189
+ [Selenium::WebDriver::Error::NoSuchElementError,
190
+ Selenium::WebDriver::Error::StaleElementReferenceError,
191
+ Selenium::WebDriver::Error::TimeoutError, Raider::AmbiguousMatch]
192
+ end
193
+
194
+ def accepts_timeout?(subject, method)
195
+ subject.method(method).parameters.any? { |_kind, name| name == :timeout }
196
+ end
197
+
198
+ def message(polarity)
199
+ head = "expected #{target} #{polarity} #{description}"
200
+ return "#{head}, but #{STALE_HINT}" if @poll.stale
201
+
202
+ "#{head}, #{last_seen} after #{Raider::Describe.seconds(@poll.elapsed)}s"
203
+ end
204
+
205
+ def target
206
+ return 'the block' if @poll.actual.is_a?(Proc)
207
+
208
+ shown = @poll.subject || @poll.actual
209
+ shown.respond_to?(:tag_name) ? Raider::Describe.element(shown) : shown.inspect
210
+ end
211
+
212
+ def last_seen
213
+ value = @poll.value
214
+ value.equal?(NOT_FOUND) ? 'last seen: not found' : "last seen #{show(value)}"
215
+ end
216
+
217
+ def show(value)
218
+ value.inspect
219
+ end
220
+ end
221
+ end
222
+ end