ruby_raider 3.11.0 → 3.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/reek.yml +5 -1
  3. data/.github/workflows/release.yml +2 -1
  4. data/.github/workflows/system_tests.yml +1 -1
  5. data/.reek.yml +13 -0
  6. data/README.md +43 -4
  7. data/lib/commands/utility_commands.rb +1 -1
  8. data/lib/generators/automation/automation_generator.rb +15 -10
  9. data/lib/generators/automation/templates/appium_caps.tt +2 -4
  10. data/lib/generators/automation/templates/home.tt +1 -2
  11. data/lib/generators/automation/templates/login.tt +6 -6
  12. data/lib/generators/automation/templates/page.tt +10 -3
  13. data/lib/generators/automation/templates/partials/home_page_selector.tt +4 -6
  14. data/lib/generators/automation/templates/partials/pdp_page_selector.tt +4 -6
  15. data/lib/generators/automation/templates/pdp.tt +3 -2
  16. data/lib/generators/cucumber/cucumber_generator.rb +15 -2
  17. data/lib/generators/cucumber/templates/partials/driver_world.tt +9 -0
  18. data/lib/generators/cucumber/templates/partials/mobile_steps.tt +1 -1
  19. data/lib/generators/cucumber/templates/partials/selenium_env.tt +4 -0
  20. data/lib/generators/cucumber/templates/partials/web_steps.tt +4 -0
  21. data/lib/generators/generator.rb +15 -4
  22. data/lib/generators/helper_generator.rb +8 -0
  23. data/lib/generators/invoke_generators.rb +23 -0
  24. data/lib/generators/menu_generator.rb +26 -14
  25. data/lib/generators/rspec/rspec_generator.rb +10 -3
  26. data/lib/generators/rspec/templates/spec.tt +15 -1
  27. data/lib/generators/template_renderer/template_error.rb +8 -8
  28. data/lib/generators/templates/common/partials/mobile_config.tt +1 -0
  29. data/lib/generators/templates/common/partials/web_config.tt +1 -1
  30. data/lib/generators/templates/common/read_me.tt +508 -24
  31. data/lib/generators/templates/helpers/partials/selenium_driver.tt +1 -3
  32. data/lib/generators/templates/helpers/raider_element.tt +159 -0
  33. data/lib/generators/templates/helpers/session_state.tt +45 -5
  34. data/lib/generators/templates/helpers/spec_helper.tt +9 -1
  35. data/lib/generators/templates/helpers/wait_helper.tt +470 -0
  36. data/lib/ruby_raider/ffmpeg.rb +7 -7
  37. data/lib/ruby_raider.rb +2 -0
  38. data/lib/scaffolding/project_detector.rb +6 -0
  39. data/lib/scaffolding/scaffolding.rb +4 -0
  40. data/lib/scaffolding/templates/component.tt +4 -0
  41. data/lib/scaffolding/templates/page_object.tt +8 -0
  42. data/lib/version +1 -1
  43. data/sig/generators/generator.rbs +2 -0
  44. data/sig/scaffolding/project_detector.rbs +1 -0
  45. data/sig/scaffolding/scaffolding.rbs +1 -0
  46. data/spec/commands/raider_commands_spec.rb +24 -0
  47. data/spec/fixtures/watir_master/cucumber_watir/Readme.md.snapshot +175 -0
  48. data/spec/fixtures/watir_master/cucumber_watir/features/step_definitions/login_steps.rb.snapshot +24 -0
  49. data/spec/fixtures/watir_master/cucumber_watir/page_objects/abstract/page.rb.snapshot +39 -0
  50. data/spec/fixtures/watir_master/cucumber_watir/page_objects/pages/login.rb.snapshot +33 -0
  51. data/spec/fixtures/watir_master/rspec_watir/Readme.md.snapshot +174 -0
  52. data/spec/fixtures/watir_master/rspec_watir/page_objects/abstract/page.rb.snapshot +39 -0
  53. data/spec/fixtures/watir_master/rspec_watir/page_objects/pages/login.rb.snapshot +33 -0
  54. data/spec/fixtures/watir_master/rspec_watir/spec/login_page_spec.rb.snapshot +49 -0
  55. data/spec/integration/commands/scaffolding_commands_spec.rb +49 -0
  56. data/spec/integration/commands/utility_commands_spec.rb +8 -0
  57. data/spec/integration/content/config_content_spec.rb +44 -0
  58. data/spec/integration/content/helper_content_spec.rb +79 -0
  59. data/spec/integration/content/page_content_spec.rb +75 -4
  60. data/spec/integration/content/readme_content_spec.rb +126 -5
  61. data/spec/integration/content/syntax_validation_spec.rb +23 -0
  62. data/spec/integration/content/test_content_spec.rb +57 -0
  63. data/spec/integration/end_to_end_spec.rb +50 -28
  64. data/spec/integration/generators/automation_generator_spec.rb +50 -0
  65. data/spec/integration/generators/common_generator_spec.rb +20 -0
  66. data/spec/integration/generators/config_features_spec.rb +2 -2
  67. data/spec/integration/generators/cucumber_generator_spec.rb +36 -0
  68. data/spec/integration/generators/helpers_generator_spec.rb +38 -0
  69. data/spec/integration/generators/invoke_generators_spec.rb +70 -0
  70. data/spec/integration/generators/rspec_generator_spec.rb +20 -0
  71. data/spec/integration/helpers/session_state_spec.rb +46 -0
  72. data/spec/integration/helpers/wait_helper_mobile_spec.rb +281 -0
  73. data/spec/integration/helpers/wait_helper_spec.rb +496 -0
  74. data/spec/integration/helpers/wait_helper_support.rb +183 -0
  75. data/spec/integration/settings_helper.rb +4 -1
  76. data/spec/integration/spec_helper.rb +2 -2
  77. data/spec/menus/menu_generator_spec.rb +64 -2
  78. data/spec/scaffolding/scaffold_project_detector_spec.rb +12 -0
  79. metadata +15 -2
@@ -61,4 +61,40 @@ describe CucumberGenerator do
61
61
  include_examples 'creates cucumber files', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::WATIR}", 'login'
62
62
  include_examples 'creates factories for web projects', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::WATIR}", 'login'
63
63
  end
64
+
65
+ describe 'without example tests' do
66
+ %w[selenium ios cross_platform].each do |automation|
67
+ context "with cucumber and #{automation}" do
68
+ let(:name) { ensure_project(framework: 'cucumber', automation:, examples: false) }
69
+
70
+ it 'creates no feature files' do
71
+ expect(Dir.glob("#{name}/features/**/*.feature")).to be_empty
72
+ end
73
+
74
+ it 'keeps an empty step_definitions folder' do
75
+ expect(Dir.children("#{name}/features/step_definitions")).to eq(['.keep'])
76
+ end
77
+
78
+ it 'creates the support files' do
79
+ expect(Dir.children("#{name}/features/support")).to contain_exactly('env.rb', 'world.rb')
80
+ end
81
+
82
+ it 'creates the cucumber.yml file' do
83
+ expect(File).to exist("#{name}/cucumber.yml")
84
+ end
85
+
86
+ it 'creates no user factory' do
87
+ expect(File).not_to exist("#{name}/models/user_factory.rb")
88
+ end
89
+ end
90
+ end
91
+
92
+ it 'creates no accessibility example' do
93
+ InvokeGenerators.generate_framework(framework: 'cucumber', automation: 'selenium', name: 'skeleton_axe_cucumber',
94
+ examples: false, accessibility: true)
95
+ expect(Dir.glob('skeleton_axe_cucumber/features/**/accessibility*')).to be_empty
96
+ ensure
97
+ FileUtils.rm_rf('skeleton_axe_cucumber')
98
+ end
99
+ end
64
100
  end
@@ -32,6 +32,18 @@ describe HelpersGenerator do
32
32
  it 'creates a browser helper file' do
33
33
  expect(File).to exist("#{name}/helpers/browser_helper.rb")
34
34
  end
35
+
36
+ it 'does not create the wait helper or the element wrapper' do
37
+ expect(File).not_to exist("#{name}/helpers/wait_helper.rb")
38
+ expect(File).not_to exist("#{name}/helpers/raider_element.rb")
39
+ end
40
+ end
41
+
42
+ shared_examples 'creates the wait helper' do |name|
43
+ it 'creates the wait helper and the element wrapper' do
44
+ expect(File).to exist("#{name}/helpers/wait_helper.rb")
45
+ expect(File).to exist("#{name}/helpers/raider_element.rb")
46
+ end
35
47
  end
36
48
 
37
49
  shared_examples 'creates rspec helpers' do |name|
@@ -56,6 +68,7 @@ describe HelpersGenerator do
56
68
  include_examples 'creates common helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::SELENIUM}"
57
69
  include_examples 'creates a session state helper', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::SELENIUM}"
58
70
  include_examples 'creates selenium helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::SELENIUM}"
71
+ include_examples 'creates the wait helper', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::SELENIUM}"
59
72
  include_examples 'creates rspec helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::SELENIUM}"
60
73
  end
61
74
 
@@ -70,6 +83,7 @@ describe HelpersGenerator do
70
83
  include_examples 'creates common helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::SELENIUM}"
71
84
  include_examples 'creates a session state helper', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::SELENIUM}"
72
85
  include_examples 'creates selenium helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::SELENIUM}"
86
+ include_examples 'creates the wait helper', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::SELENIUM}"
73
87
  include_examples 'creates cucumber helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::SELENIUM}"
74
88
  end
75
89
 
@@ -84,6 +98,7 @@ describe HelpersGenerator do
84
98
  include_examples 'creates common helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::ANDROID}"
85
99
  include_examples 'skips the session state helper', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::ANDROID}"
86
100
  include_examples 'creates selenium helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::ANDROID}"
101
+ include_examples 'creates the wait helper', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::ANDROID}"
87
102
  include_examples 'creates rspec helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::ANDROID}"
88
103
  end
89
104
 
@@ -91,6 +106,7 @@ describe HelpersGenerator do
91
106
  include_examples 'creates common helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::IOS}"
92
107
  include_examples 'skips the session state helper', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::IOS}"
93
108
  include_examples 'creates selenium helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::IOS}"
109
+ include_examples 'creates the wait helper', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::IOS}"
94
110
  include_examples 'creates rspec helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::IOS}"
95
111
  end
96
112
 
@@ -98,6 +114,7 @@ describe HelpersGenerator do
98
114
  include_examples 'creates common helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::ANDROID}"
99
115
  include_examples 'skips the session state helper', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::ANDROID}"
100
116
  include_examples 'creates selenium helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::ANDROID}"
117
+ include_examples 'creates the wait helper', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::ANDROID}"
101
118
  include_examples 'creates cucumber helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::ANDROID}"
102
119
  end
103
120
 
@@ -105,6 +122,7 @@ describe HelpersGenerator do
105
122
  include_examples 'creates common helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::IOS}"
106
123
  include_examples 'skips the session state helper', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::IOS}"
107
124
  include_examples 'creates selenium helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::IOS}"
125
+ include_examples 'creates the wait helper', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::IOS}"
108
126
  include_examples 'creates cucumber helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::IOS}"
109
127
  end
110
128
 
@@ -112,6 +130,7 @@ describe HelpersGenerator do
112
130
  include_examples 'creates common helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::CROSS_PLATFORM}"
113
131
  include_examples 'skips the session state helper', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::CROSS_PLATFORM}"
114
132
  include_examples 'creates selenium helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::CROSS_PLATFORM}"
133
+ include_examples 'creates the wait helper', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::CROSS_PLATFORM}"
115
134
  include_examples 'creates rspec helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::CROSS_PLATFORM}"
116
135
  include_examples 'creates cross platform helpers', "#{FrameworkIndex::RSPEC}_#{AutomationIndex::CROSS_PLATFORM}"
117
136
  end
@@ -120,7 +139,26 @@ describe HelpersGenerator do
120
139
  include_examples 'creates common helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::CROSS_PLATFORM}"
121
140
  include_examples 'skips the session state helper', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::CROSS_PLATFORM}"
122
141
  include_examples 'creates selenium helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::CROSS_PLATFORM}"
142
+ include_examples 'creates the wait helper', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::CROSS_PLATFORM}"
123
143
  include_examples 'creates cucumber helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::CROSS_PLATFORM}"
124
144
  include_examples 'creates cross platform helpers', "#{FrameworkIndex::CUCUMBER}_#{AutomationIndex::CROSS_PLATFORM}"
125
145
  end
146
+
147
+ describe 'without example tests' do
148
+ %w[rspec cucumber].product(%w[selenium ios cross_platform]).each do |framework, automation|
149
+ context "with #{framework} and #{automation}" do
150
+ let(:name) { ensure_project(framework:, automation:, examples: false) }
151
+
152
+ it 'creates the wait helper and RaiderElement' do
153
+ expect(File).to exist("#{name}/helpers/wait_helper.rb")
154
+ expect(File).to exist("#{name}/helpers/raider_element.rb")
155
+ end
156
+ end
157
+ end
158
+
159
+ it 'keeps the session state for web' do
160
+ name = ensure_project(framework: 'rspec', automation: 'selenium', examples: false)
161
+ expect(File).to exist("#{name}/helpers/session_state.rb")
162
+ end
163
+ end
126
164
  end
@@ -49,4 +49,74 @@ describe InvokeGenerators do
49
49
  expect(File).not_to exist('case_bad_ci')
50
50
  end
51
51
  end
52
+
53
+ describe '.collect_flags' do
54
+ it 'adds no_examples when examples is false' do
55
+ expect(described_class.collect_flags(automation: 'selenium', examples: false)).to include('no_examples')
56
+ end
57
+
58
+ it 'keeps the examples by default' do
59
+ expect(described_class.collect_flags(automation: 'selenium')).not_to include('no_examples')
60
+ end
61
+
62
+ it 'keeps the examples for watir, whose templates are frozen' do
63
+ expect(described_class.collect_flags(automation: 'watir', examples: false)).not_to include('no_examples')
64
+ end
65
+ end
66
+
67
+ context 'with the Watir deprecation notice' do
68
+ let(:notice) do
69
+ 'Watir is deprecated and will be removed in the next major version of ruby_raider; new projects should use Selenium.'
70
+ end
71
+
72
+ it 'prints it for watir' do
73
+ expect { described_class.generate_framework(framework: 'rspec', automation: 'watir', name: 'case_dep') }
74
+ .to output(/#{Regexp.escape(notice)}/).to_stdout
75
+ end
76
+
77
+ it 'does not print it for selenium' do
78
+ expect { described_class.generate_framework(framework: 'rspec', automation: 'selenium', name: 'case_nodep') }
79
+ .not_to output(/Watir is deprecated/).to_stdout
80
+ end
81
+
82
+ it 'still prints it for raider new with automation:watir' do
83
+ root = File.expand_path('../../..', __dir__)
84
+ out, status = Open3.capture2e('ruby', "#{root}/bin/raider", 'new', 'case_cli_watir', '-p',
85
+ 'framework:rspec', 'automation:watir')
86
+ expect(status.success?).to be(true)
87
+ expect(out).to include(notice)
88
+ expect(File).to exist('case_cli_watir/.rspec')
89
+ end
90
+ end
91
+
92
+ context 'with examples false for watir' do
93
+ it 'says the examples are kept' do
94
+ expect { described_class.generate_framework(framework: 'rspec', automation: 'watir', name: 'case_watir', examples: false) }
95
+ .to output(/Watir projects always include the example tests/).to_stdout
96
+ end
97
+ end
98
+ end
99
+
100
+ describe 'Projects named like a generator option' do
101
+ after { FileUtils.rm_rf(%w[no_examples watir selenium]) }
102
+
103
+ it 'keeps the example tests in a project named no_examples' do
104
+ InvokeGenerators.generate_framework(automation: 'selenium', framework: 'rspec', name: 'no_examples')
105
+ expect(File).to exist('no_examples/spec/login_page_spec.rb')
106
+ expect(File).to exist('no_examples/page_objects/pages/login.rb')
107
+ end
108
+
109
+ it 'gives a Selenium project named watir the wait helper it wires in' do
110
+ InvokeGenerators.generate_framework(automation: 'selenium', framework: 'cucumber', name: 'watir')
111
+ expect(File).to exist('watir/helpers/wait_helper.rb')
112
+ expect(File.read('watir/features/support/env.rb')).not_to include('Watir')
113
+ expect(File.read('watir/page_objects/abstract/page.rb')).to include('include WaitHelper')
114
+ end
115
+
116
+ it 'keeps a Watir project named selenium on Watir' do
117
+ InvokeGenerators.generate_framework(automation: 'watir', framework: 'rspec', name: 'selenium')
118
+ expect(File).not_to exist('selenium/helpers/wait_helper.rb')
119
+ expect(File.read('selenium/page_objects/abstract/page.rb')).to include('browser.element')
120
+ expect(File.read('selenium/page_objects/abstract/page.rb')).not_to include('find_element')
121
+ end
52
122
  end
@@ -46,4 +46,24 @@ describe RspecGenerator do
46
46
  include_examples 'creates rspec files examples',
47
47
  "#{FrameworkIndex::RSPEC}_#{AutomationIndex::CROSS_PLATFORM}", 'pdp'
48
48
  end
49
+
50
+ describe 'without example tests' do
51
+ %w[selenium ios cross_platform].each do |automation|
52
+ context "with rspec and #{automation}" do
53
+ let(:name) { ensure_project(framework: 'rspec', automation:, examples: false) }
54
+
55
+ it 'creates an empty spec folder' do
56
+ expect(Dir.children("#{name}/spec")).to eq(['.keep'])
57
+ end
58
+ end
59
+ end
60
+
61
+ it 'creates no accessibility example' do
62
+ InvokeGenerators.generate_framework(framework: 'rspec', automation: 'selenium', name: 'skeleton_axe_rspec',
63
+ examples: false, accessibility: true)
64
+ expect(Dir.children('skeleton_axe_rspec/spec')).to eq(['.keep'])
65
+ ensure
66
+ FileUtils.rm_rf('skeleton_axe_rspec')
67
+ end
68
+ end
49
69
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'erb'
4
4
  require 'json'
5
+ require 'timeout'
5
6
  require 'selenium-webdriver'
6
7
  require 'time'
7
8
  require 'tmpdir'
@@ -409,6 +410,51 @@ describe 'SessionState' do
409
410
  expect(Time.iso8601(saved['saved_at'])).to be > before
410
411
  end
411
412
 
413
+ it 'lets concurrent callers restore a fresh login at the same time' do
414
+ write_state('cookies' => [saved_cookie('session')])
415
+ arrived = Queue.new
416
+ checked = Queue.new
417
+ threads = Array.new(2) do
418
+ Thread.new do
419
+ valid = lambda do
420
+ arrived << 1
421
+ Timeout.timeout(2) { sleep 0.01 until arrived.size == 2 }
422
+ checked << 1
423
+ end
424
+ SessionState.new(SessionStateSpec::FakeDriver.new(cookies:)).reuse(valid:) { raise 'logged in' }
425
+ end
426
+ end
427
+ threads.each(&:join)
428
+ expect(checked.size).to eq(2)
429
+ end
430
+
431
+ it 'restores a login another worker saved meanwhile, without a fallback warning' do
432
+ write_state('cookies' => [saved_cookie('session')])
433
+ checks = 0
434
+ valid = lambda do
435
+ checks += 1
436
+ write_state('cookies' => [saved_cookie('session')], 'saved_at' => Time.now.utc.iso8601) if checks == 1
437
+ checks > 1
438
+ end
439
+ logins = 0
440
+ expect { state.reuse(valid:) { logins += 1 } }.not_to output.to_stderr
441
+ expect([logins, checks]).to eq([0, 2])
442
+ end
443
+
444
+ it 'warns once when the second restore after a peer save is also invalid' do
445
+ write_state('cookies' => [saved_cookie('session')])
446
+ checks = 0
447
+ valid = lambda do
448
+ checks += 1
449
+ write_state('cookies' => [saved_cookie('session')], 'saved_at' => Time.now.utc.iso8601) if checks == 1
450
+ false
451
+ end
452
+ logins = 0
453
+ expect { state.reuse(valid:) { logins += 1 } }
454
+ .to output(/\A\[login_state\] login restored from \S+ is no longer valid, logging in again\n\z/).to_stderr
455
+ expect(logins).to eq(1)
456
+ end
457
+
412
458
  it 'serializes concurrent callers on the lock' do
413
459
  count = 0
414
460
  drivers = Array.new(2) { SessionStateSpec::FakeDriver.new(cookies:) }
@@ -0,0 +1,281 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'wait_helper_support'
4
+
5
+ # The mobile render loads into App, so its WaitHelper and RaiderElement never meet the web
6
+ # render's top-level ones when both spec files run in one process.
7
+ module WaitHelperMobileSpec
8
+ module App; end
9
+ WaitHelperSpec.load_templates(mobile: true, into: App)
10
+
11
+ class Page
12
+ include App::WaitHelper
13
+
14
+ attr_reader :driver
15
+
16
+ def initialize(driver)
17
+ @driver = driver
18
+ end
19
+ end
20
+
21
+ RSpec.describe 'WaitHelper on mobile' do
22
+ let(:driver) { WaitHelperSpec::FakeDriver.new(device: :ios) }
23
+ let(:android) { WaitHelperSpec::FakeDriver.new(device: :android) }
24
+ let(:page) { Page.new(driver) }
25
+ let(:per_os) { { ios: { accessibility_id: 'ios-card' }, android: { accessibility_id: 'android-card' } } }
26
+
27
+ def element(**state) = WaitHelperSpec::FakeElement.new(**state)
28
+ def clock = Process.clock_gettime(Process::CLOCK_MONOTONIC)
29
+ def screen_area = { left: 0, top: 20, width: 390, height: 800 }
30
+
31
+ describe '#tap and #click' do
32
+ it 'click once the element is visible, enabled and stable' do
33
+ %i[tap click].each do |name|
34
+ target = element(rects: [[0, 0, 10, 10], [5, 5, 10, 10], [5, 5, 10, 10]])
35
+ driver.script({ accessibility_id: name.to_s }, nil, element(displayed: false), element(enabled: false), target)
36
+ expect(page.public_send(name, { accessibility_id: name.to_s }, timeout: 2)).to eq(target)
37
+ expect(target.log).to eq([[:click]])
38
+ end
39
+ end
40
+ end
41
+
42
+ describe 'tap with a block' do
43
+ it 'is Object#tap on a RaiderElement: yields and returns the wrapper without clicking' do
44
+ card = element
45
+ driver.script({ accessibility_id: 'card' }, card)
46
+ wrapper = page.find_element(accessibility_id: 'card')
47
+ seen = nil
48
+ expect(wrapper.tap { |yielded| seen = yielded }).to be(wrapper)
49
+ expect([seen, card.log]).to eq([wrapper, []])
50
+ end
51
+
52
+ it 'is Object#tap on the mixin: yields self without looking anything up' do
53
+ seen = nil
54
+ expect(page.tap { |yielded| seen = yielded }).to be(page)
55
+ expect(page.tap(&:driver)).to be(page)
56
+ expect([seen, driver.finds, driver.scripts]).to eq([page, [], []])
57
+ end
58
+
59
+ it 'still clicks without a block' do
60
+ card = element
61
+ driver.script({ id: 'x' }, card)
62
+ page.tap({ id: 'x' }, timeout: 1)
63
+ page.tap(id: 'x')
64
+ page.find_element(id: 'x').tap
65
+ expect(card.log).to eq([[:click]] * 3)
66
+ end
67
+ end
68
+
69
+ describe '#swipe' do
70
+ it 'uses mobile: swipe on iOS' do
71
+ card = element
72
+ driver.script({ accessibility_id: 'card' }, card)
73
+ expect(page.swipe('left', { accessibility_id: 'card' }, timeout: 1)).to eq(card)
74
+ expect(driver.scripts).to eq([['mobile: swipe', { direction: 'left', element: card.id }]])
75
+ end
76
+
77
+ it 'uses mobile: swipeGesture on Android' do
78
+ card = element
79
+ android.script({ accessibility_id: 'card' }, card)
80
+ Page.new(android).swipe('left', { accessibility_id: 'card' }, timeout: 1)
81
+ expect(android.scripts)
82
+ .to eq([['mobile: swipeGesture', { elementId: card.id, direction: 'left', percent: 0.75 }]])
83
+ end
84
+
85
+ it 'waits for the element to be visible first' do
86
+ card = element
87
+ driver.script({ accessibility_id: 'card' }, nil, element(displayed: false), card)
88
+ page.swipe('up', { accessibility_id: 'card' }, timeout: 1)
89
+ expect([driver.finds.size, driver.scripts.size]).to eq([3, 1])
90
+ end
91
+
92
+ it 'swipes the whole screen without a locator' do
93
+ expect(page.swipe('up')).to be_nil
94
+ Page.new(android).swipe('up')
95
+ expect(driver.scripts).to eq([['mobile: swipe', { direction: 'up' }]])
96
+ expect(android.scripts).to eq([['mobile: swipeGesture', { **screen_area, direction: 'up', percent: 0.75 }]])
97
+ end
98
+
99
+ it 'rejects a direction other than up, down, left or right' do
100
+ expect { page.swipe('sideways') }.to raise_error(ArgumentError, /up, down, left or right/)
101
+ expect(driver.scripts).to be_empty
102
+ end
103
+ end
104
+
105
+ describe '#scroll' do
106
+ it 'scrolls the screen without a locator' do
107
+ page.scroll('down')
108
+ Page.new(android).scroll('down')
109
+ expect(driver.scripts).to eq([['mobile: scroll', { direction: 'down' }]])
110
+ expect(android.scripts).to eq([['mobile: scrollGesture', { **screen_area, direction: 'down', percent: 0.75 }]])
111
+ end
112
+
113
+ it 'scrolls a container' do
114
+ list = element
115
+ android.script({ id: 'list' }, list)
116
+ Page.new(android).scroll('up', { id: 'list' }, timeout: 1)
117
+ expect(android.scripts).to eq([['mobile: scrollGesture', { elementId: list.id, direction: 'up', percent: 0.75 }]])
118
+ end
119
+ end
120
+
121
+ describe 'an unknown device' do
122
+ it 'raises NotImplementedError naming it, before waiting' do
123
+ windows = WaitHelperSpec::FakeDriver.new(device: :windows)
124
+ started = clock
125
+ expect { Page.new(windows).swipe('left', { id: 'never' }, timeout: 2) }
126
+ .to raise_error(NotImplementedError, /windows/)
127
+ expect { Page.new(windows).scroll('down') }.to raise_error(NotImplementedError, /windows/)
128
+ expect(clock - started).to be < 0.5
129
+ end
130
+ end
131
+
132
+ describe '#scroll_to' do
133
+ it 'returns a RaiderElement once the element turns visible after 2 scrolls' do
134
+ footer = element
135
+ driver.script({ accessibility_id: 'footer' }, nil, element(displayed: false), footer)
136
+ found = page.scroll_to({ accessibility_id: 'footer' }, timeout: 2)
137
+ expect(found).to be_a(App::RaiderElement).and eq(footer)
138
+ expect(driver.scripts).to eq([['mobile: scroll', { direction: 'down' }]] * 2)
139
+ end
140
+
141
+ it 'scrolls in the given direction' do
142
+ driver.script({ accessibility_id: 'header' }, nil, element)
143
+ page.scroll_to({ accessibility_id: 'header' }, direction: 'up', timeout: 2)
144
+ expect(driver.scripts).to eq([['mobile: scroll', { direction: 'up' }]])
145
+ end
146
+
147
+ it 'stops after max_scrolls scrolls and says so' do
148
+ started = clock
149
+ expect { page.scroll_to({ accessibility_id: 'footer' }, max_scrolls: 3, timeout: 5) }
150
+ .to raise_error(App::WaitHelper::TimeoutError) { |error|
151
+ expect(error.message).to include('to be visible').and include('after 3 scrolls')
152
+ }
153
+ expect(driver.scripts.size).to eq(3)
154
+ expect(driver.finds.size).to eq(4)
155
+ expect(clock - started).to be < 2
156
+ end
157
+
158
+ it 'also stops at the timeout, naming the scrolls so far' do
159
+ expect { page.scroll_to({ accessibility_id: 'footer' }, timeout: 0) }
160
+ .to raise_error(App::WaitHelper::TimeoutError, /after 0 scrolls/)
161
+ expect(driver.scripts).to be_empty
162
+ end
163
+ end
164
+
165
+ describe 'per-OS locators' do
166
+ it 'resolve on tap and swipe' do
167
+ card = element
168
+ android.script({ accessibility_id: 'android-card' }, card)
169
+ mobile = Page.new(android)
170
+ mobile.tap(per_os, timeout: 1)
171
+ mobile.swipe('right', per_os, timeout: 1)
172
+ expect(android.finds.uniq).to eq([{ accessibility_id: 'android-card' }])
173
+ expect([card.log, android.scripts.size]).to eq([[[:click]], 1])
174
+ end
175
+
176
+ it 'resolve on a nested find_element' do
177
+ row = element
178
+ list = element.script({ accessibility_id: 'ios-row' }, row)
179
+ driver.script({ accessibility_id: 'ios-card' }, list)
180
+ found = page.find_element(per_os).find_element({ ios: { accessibility_id: 'ios-row' }, android: { id: 'row' } })
181
+ expect(found).to eq(row)
182
+ end
183
+ end
184
+
185
+ describe 'RaiderElement' do
186
+ it 'has the mobile set without the locator' do
187
+ card = element
188
+ driver.script({ accessibility_id: 'card' }, card)
189
+ wrapper = page.find_element(accessibility_id: 'card')
190
+ expect(wrapper.tap.swipe('left').scroll('down').scroll_to).to be(wrapper)
191
+ expect(card.log).to eq([[:click]])
192
+ expect(driver.scripts).to eq([['mobile: swipe', { direction: 'left', element: card.id }],
193
+ ['mobile: scroll', { direction: 'down', element: card.id }]])
194
+ end
195
+
196
+ it 'adds exactly the planned public methods' do
197
+ expect(App::RaiderElement.public_instance_methods(false).sort)
198
+ .to eq(%i[== attribute clear click displayed? enabled? eql? find_element find_elements hash native scroll
199
+ scroll_to selected? send_keys swipe tap text])
200
+ end
201
+ end
202
+
203
+ describe 'the choke point' do
204
+ let(:actions) { [] }
205
+
206
+ before do
207
+ driver.script({ id: 'x' }, element.script({ css: 'y' }, element).script_many({ css: 'y' }, [element]))
208
+ allow_any_instance_of(App::WaitHelper::Waiter).to receive(:perform_action).and_wrap_original do |original, *args, **options, &block|
209
+ actions << args.first
210
+ original.call(*args, **options, &block)
211
+ end
212
+ end
213
+
214
+ def calls(locator)
215
+ { find_element: [locator], find_elements: [locator], click: [locator], tap: [locator], clear: [locator],
216
+ text: [locator], displayed?: [locator], enabled?: [locator], selected?: [locator],
217
+ send_keys: [locator, 'a'], attribute: [locator, 'name'], swipe: ['left', locator],
218
+ scroll: ['down', locator], scroll_to: [locator] }
219
+ end
220
+
221
+ it 'is called once per mixin method, with the method name' do
222
+ calls({ id: 'x' }).each do |name, args|
223
+ actions.clear
224
+ page.public_send(name, *args, timeout: 1)
225
+ expect(actions).to eq([name])
226
+ end
227
+ end
228
+
229
+ it 'is called once for a whole-screen gesture' do
230
+ %i[swipe scroll].each do |name|
231
+ actions.clear
232
+ page.public_send(name, 'up')
233
+ expect(actions).to eq([name])
234
+ end
235
+ end
236
+
237
+ it 'is called once per wrapper method, with the method name' do
238
+ wrapper = page.find_element(id: 'x')
239
+ calls({ css: 'y' }).each do |name, args|
240
+ args = args.reject { |arg| arg.is_a?(Hash) } unless %i[find_element find_elements].include?(name)
241
+ actions.clear
242
+ wrapper.public_send(name, *args, timeout: 1)
243
+ expect(actions).to eq([name])
244
+ end
245
+ end
246
+ end
247
+
248
+ describe 'footprint' do
249
+ it 'adds exactly the mobile API, with no private methods and nothing named wait' do
250
+ expect(App::WaitHelper.public_instance_methods(false).sort)
251
+ .to eq(%i[attribute clear click displayed? enabled? eventually find_element find_elements scroll scroll_to
252
+ selected? send_keys swipe tap text])
253
+ expect(App::WaitHelper.private_instance_methods(false)).to be_empty
254
+ expect(page.respond_to?(:wait, true)).to be(false)
255
+ end
256
+ end
257
+
258
+ describe '#eventually' do
259
+ it 'retries the block as on web' do
260
+ tries = 0
261
+ expect(page.eventually(timeout: 1) { expect(tries += 1).to eq(2) }).to be(true)
262
+ end
263
+
264
+ it 'retries a nested timeout: until its own deadline, as on web' do
265
+ started = clock
266
+ expect { page.eventually(timeout: 0.6) { page.text({ accessibility_id: 'never' }, timeout: 0.2) } }
267
+ .to raise_error(App::WaitHelper::TimeoutError, /\A\(still failing after 0.6s\)/)
268
+ expect(clock - started).to be_between(0.5, 1.0)
269
+ end
270
+
271
+ it 'raises at once for a stale find_elements member used as a parent' do
272
+ row = element.script({ accessibility_id: 'cell' }, element)
273
+ driver.script_many({ accessibility_id: 'row' }, [row])
274
+ member = page.find_elements(accessibility_id: 'row').first
275
+ row.go_stale
276
+ expect { member.find_element(accessibility_id: 'cell', timeout: 2) }
277
+ .to raise_error(Selenium::WebDriver::Error::StaleElementReferenceError, /find_elements again/)
278
+ end
279
+ end
280
+ end
281
+ end