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.
- checksums.yaml +4 -4
- data/.github/workflows/reek.yml +5 -1
- data/.github/workflows/release.yml +2 -1
- data/.github/workflows/system_tests.yml +1 -1
- data/.reek.yml +13 -0
- data/README.md +43 -4
- data/lib/commands/utility_commands.rb +1 -1
- data/lib/generators/automation/automation_generator.rb +15 -10
- data/lib/generators/automation/templates/appium_caps.tt +2 -4
- data/lib/generators/automation/templates/home.tt +1 -2
- data/lib/generators/automation/templates/login.tt +6 -6
- data/lib/generators/automation/templates/page.tt +10 -3
- data/lib/generators/automation/templates/partials/home_page_selector.tt +4 -6
- data/lib/generators/automation/templates/partials/pdp_page_selector.tt +4 -6
- data/lib/generators/automation/templates/pdp.tt +3 -2
- data/lib/generators/cucumber/cucumber_generator.rb +15 -2
- data/lib/generators/cucumber/templates/partials/driver_world.tt +9 -0
- data/lib/generators/cucumber/templates/partials/mobile_steps.tt +1 -1
- data/lib/generators/cucumber/templates/partials/selenium_env.tt +4 -0
- data/lib/generators/cucumber/templates/partials/web_steps.tt +4 -0
- data/lib/generators/generator.rb +15 -4
- data/lib/generators/helper_generator.rb +8 -0
- data/lib/generators/invoke_generators.rb +23 -0
- data/lib/generators/menu_generator.rb +26 -14
- data/lib/generators/rspec/rspec_generator.rb +10 -3
- data/lib/generators/rspec/templates/spec.tt +15 -1
- data/lib/generators/template_renderer/template_error.rb +8 -8
- data/lib/generators/templates/common/partials/mobile_config.tt +1 -0
- data/lib/generators/templates/common/partials/web_config.tt +1 -1
- data/lib/generators/templates/common/read_me.tt +508 -24
- data/lib/generators/templates/helpers/partials/selenium_driver.tt +1 -3
- data/lib/generators/templates/helpers/raider_element.tt +159 -0
- data/lib/generators/templates/helpers/session_state.tt +45 -5
- data/lib/generators/templates/helpers/spec_helper.tt +9 -1
- data/lib/generators/templates/helpers/wait_helper.tt +470 -0
- data/lib/ruby_raider/ffmpeg.rb +7 -7
- data/lib/ruby_raider.rb +2 -0
- data/lib/scaffolding/project_detector.rb +6 -0
- data/lib/scaffolding/scaffolding.rb +4 -0
- data/lib/scaffolding/templates/component.tt +4 -0
- data/lib/scaffolding/templates/page_object.tt +8 -0
- data/lib/version +1 -1
- data/sig/generators/generator.rbs +2 -0
- data/sig/scaffolding/project_detector.rbs +1 -0
- data/sig/scaffolding/scaffolding.rbs +1 -0
- data/spec/commands/raider_commands_spec.rb +24 -0
- data/spec/fixtures/watir_master/cucumber_watir/Readme.md.snapshot +175 -0
- data/spec/fixtures/watir_master/cucumber_watir/features/step_definitions/login_steps.rb.snapshot +24 -0
- data/spec/fixtures/watir_master/cucumber_watir/page_objects/abstract/page.rb.snapshot +39 -0
- data/spec/fixtures/watir_master/cucumber_watir/page_objects/pages/login.rb.snapshot +33 -0
- data/spec/fixtures/watir_master/rspec_watir/Readme.md.snapshot +174 -0
- data/spec/fixtures/watir_master/rspec_watir/page_objects/abstract/page.rb.snapshot +39 -0
- data/spec/fixtures/watir_master/rspec_watir/page_objects/pages/login.rb.snapshot +33 -0
- data/spec/fixtures/watir_master/rspec_watir/spec/login_page_spec.rb.snapshot +49 -0
- data/spec/integration/commands/scaffolding_commands_spec.rb +49 -0
- data/spec/integration/commands/utility_commands_spec.rb +8 -0
- data/spec/integration/content/config_content_spec.rb +44 -0
- data/spec/integration/content/helper_content_spec.rb +79 -0
- data/spec/integration/content/page_content_spec.rb +75 -4
- data/spec/integration/content/readme_content_spec.rb +126 -5
- data/spec/integration/content/syntax_validation_spec.rb +23 -0
- data/spec/integration/content/test_content_spec.rb +57 -0
- data/spec/integration/end_to_end_spec.rb +50 -28
- data/spec/integration/generators/automation_generator_spec.rb +50 -0
- data/spec/integration/generators/common_generator_spec.rb +20 -0
- data/spec/integration/generators/config_features_spec.rb +2 -2
- data/spec/integration/generators/cucumber_generator_spec.rb +36 -0
- data/spec/integration/generators/helpers_generator_spec.rb +38 -0
- data/spec/integration/generators/invoke_generators_spec.rb +70 -0
- data/spec/integration/generators/rspec_generator_spec.rb +20 -0
- data/spec/integration/helpers/session_state_spec.rb +46 -0
- data/spec/integration/helpers/wait_helper_mobile_spec.rb +281 -0
- data/spec/integration/helpers/wait_helper_spec.rb +496 -0
- data/spec/integration/helpers/wait_helper_support.rb +183 -0
- data/spec/integration/settings_helper.rb +4 -1
- data/spec/integration/spec_helper.rb +2 -2
- data/spec/menus/menu_generator_spec.rb +64 -2
- data/spec/scaffolding/scaffold_project_detector_spec.rb +12 -0
- metadata +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
|