webaccount 0.3.1 → 0.5.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.
@@ -1,17 +1,17 @@
1
- # WebAccount.rb
1
+ # webaccount.rb
2
2
  # WebAccount
3
3
 
4
4
  require 'selenium-webdriver'
5
- require 'Thoran/Selenium'
5
+ require_relative './Thoran/Selenium'
6
+ require_relative './WebAccount/VERSION'
6
7
 
7
8
  class WebAccount
8
-
9
9
  attr_accessor :username
10
10
  attr_accessor :password
11
- attr_writer :user_agent_alias
12
11
 
13
12
  def initialize(
14
13
  username: nil, password: nil,
14
+ browser: nil, driver_options: nil,
15
15
  login_page_url: nil,
16
16
  login_page_username_field_selector: nil, login_page_password_field_selector: nil,
17
17
  login_page_submit_button_selector: nil,
@@ -19,6 +19,7 @@ class WebAccount
19
19
  logged_out_xpath: nil
20
20
  )
21
21
  @username, @password = username, password
22
+ @browser, @driver_options = browser, driver_options
22
23
  @login_page_url = login_page_url
23
24
  @login_page_username_field_selector, @login_page_password_field_selector = login_page_username_field_selector, login_page_password_field_selector
24
25
  @login_page_submit_button_selector = login_page_submit_button_selector
@@ -30,7 +31,6 @@ class WebAccount
30
31
  @logged_in = false
31
32
  username ||= self.username
32
33
  password ||= self.password
33
- attempts = 0
34
34
  driver.attempt do
35
35
  get_login_page
36
36
  enter_username(username)
@@ -38,7 +38,7 @@ class WebAccount
38
38
  login_page_submit_button.click
39
39
  end
40
40
  @logged_in = driver_wait.until do
41
- driver.element_present?(@logout_button_selector.keys.first, @logout_button_selector.values.first)
41
+ driver.element_present?(logout_button_selector.keys.first, logout_button_selector.values.first)
42
42
  end
43
43
  end
44
44
  alias_method :logon, :login
@@ -56,6 +56,10 @@ class WebAccount
56
56
  driver.quit
57
57
  end
58
58
 
59
+ def driver
60
+ @driver ||= Selenium::WebDriver.for(browser, options: driver_options)
61
+ end
62
+
59
63
  # predicate methods
60
64
 
61
65
  def logged_in?
@@ -70,20 +74,46 @@ class WebAccount
70
74
 
71
75
  private
72
76
 
73
- def user_agent_alias
74
- @user_agent_alias ||= @user_agent_alias || :chrome
77
+ # driver construction
78
+
79
+ def default_browser
80
+ :chrome
75
81
  end
76
82
 
77
- def driver
78
- @driver ||= (
79
- driver = Selenium::WebDriver.setup(user_agent_alias)
80
- driver.manage.timeouts.implicit_wait = 5
81
- driver
82
- )
83
+ def browser
84
+ (@browser || default_browser).to_sym.downcase
85
+ end
86
+
87
+ def default_driver_options
88
+ nil
89
+ end
90
+
91
+ def driver_options
92
+ @driver_options || default_driver_options
93
+ end
94
+
95
+ # waiting
96
+
97
+ # For navigation: a submit, the authentication behind it, and the redirect.
98
+ def default_wait_timeout
99
+ 30
83
100
  end
84
101
 
85
102
  def driver_wait
86
- @driver_wait ||= Selenium::WebDriver::Wait.new(timeout: 180)
103
+ @driver_wait ||= Selenium::WebDriver::Wait.new(timeout: default_wait_timeout)
104
+ end
105
+
106
+ # For lookups: the element should already be on a page which has loaded.
107
+ def default_find_timeout
108
+ 5
109
+ end
110
+
111
+ def find_wait
112
+ @find_wait ||= Selenium::WebDriver::Wait.new(timeout: default_find_timeout)
113
+ end
114
+
115
+ def find(selector)
116
+ find_wait.until{driver.find_element(selector.keys.first, selector.values.first)}
87
117
  end
88
118
 
89
119
  # logging in
@@ -98,16 +128,16 @@ class WebAccount
98
128
  username_field.send_keys(username)
99
129
  end
100
130
 
101
- def default_username_field
102
- {name: "username"}
131
+ def default_username_field_selector
132
+ {name: 'username'}
133
+ end
134
+
135
+ def username_field_selector
136
+ @login_page_username_field_selector || default_username_field_selector
103
137
  end
104
138
 
105
139
  def username_field
106
- if @login_page_username_field_selector
107
- driver.find_element(@login_page_username_field_selector.keys.first, @login_page_username_field_selector.values.first)
108
- else
109
- driver.find_element(default_username_field.keys.first, default_username_field.values.first)
110
- end
140
+ find(username_field_selector)
111
141
  end
112
142
 
113
143
  def enter_password(password = nil)
@@ -115,28 +145,28 @@ class WebAccount
115
145
  password_field.send_keys(password)
116
146
  end
117
147
 
118
- def default_password_field
119
- {name: "password"}
148
+ def default_password_field_selector
149
+ {name: 'password'}
150
+ end
151
+
152
+ def password_field_selector
153
+ @login_page_password_field_selector || default_password_field_selector
120
154
  end
121
155
 
122
156
  def password_field
123
- if @login_page_password_field_selector
124
- driver.find_element(@login_page_password_field_selector.keys.first, @login_page_password_field_selector.values.first)
125
- else
126
- driver.find_element(default_password_field.keys.first, default_password_field.values.first)
127
- end
157
+ find(password_field_selector)
128
158
  end
129
159
 
130
160
  def default_login_page_submit_button_selector
131
161
  {xpath: '//input[@type="submit" and @value="Login"]'}
132
162
  end
133
163
 
164
+ def login_page_submit_button_selector
165
+ @login_page_submit_button_selector || default_login_page_submit_button_selector
166
+ end
167
+
134
168
  def login_page_submit_button
135
- if @login_page_submit_button_selector
136
- driver.find_element(@login_page_submit_button_selector.keys.first, @login_page_submit_button_selector.values.first)
137
- else
138
- driver.find_element(default_login_page_submit_button_selector.keys.first, default_login_page_submit_button_selector.values.first)
139
- end
169
+ find(login_page_submit_button_selector)
140
170
  end
141
171
 
142
172
  # logging out
@@ -145,11 +175,11 @@ class WebAccount
145
175
  {xpath: '//input[@type="submit" and @value="Logout"]'}
146
176
  end
147
177
 
178
+ def logout_button_selector
179
+ @logout_button_selector || default_logout_button_selector
180
+ end
181
+
148
182
  def logout_button
149
- if @logout_page_submit_button_selector
150
- driver.find_element(@logout_button_selector.keys.first, @logout_button_selector.values.first)
151
- else
152
- driver.find_element(default_logout_button_selector.keys.first, default_logout_button_selector.values.first)
153
- end
183
+ find(logout_button_selector)
154
184
  end
155
185
  end
@@ -0,0 +1,169 @@
1
+ # test/Thoran/Selenium/WebDriver/Driver/Attempt_test.rb
2
+
3
+ # Notes:
4
+ # 1. selenium-webdriver is required first because attempt.rb resolves the Selenium error
5
+ # classes at load time, in TRANSIENT_ERRORS, but does not require them itself.
6
+ # 2. The transient and non-transient errors are listed here rather than read from
7
+ # TRANSIENT_ERRORS, so that these tests state which errors are meant to be retried rather
8
+ # than agreeing with whatever the implementation happens to say.
9
+ # 3. No driver, no browser: the errors are raised directly at a dummy including the module.
10
+
11
+ require 'selenium-webdriver'
12
+ require_relative '../../../../../lib/Thoran/Selenium/WebDriver/Driver/Attempt/attempt'
13
+
14
+ require 'minitest/autorun'
15
+ require 'minitest-spec-context'
16
+
17
+ class AttemptDummy
18
+ include Thoran::Selenium::WebDriver::Driver::Attempt
19
+ end
20
+
21
+ describe Thoran::Selenium::WebDriver::Driver::Attempt do
22
+ TRANSIENT = [
23
+ Selenium::WebDriver::Error::ElementClickInterceptedError,
24
+ Selenium::WebDriver::Error::ElementNotInteractableError,
25
+ Selenium::WebDriver::Error::NoSuchElementError,
26
+ Selenium::WebDriver::Error::StaleElementReferenceError,
27
+ Selenium::WebDriver::Error::TimeoutError,
28
+ Selenium::WebDriver::Error::UnexpectedAlertOpenError,
29
+ Selenium::WebDriver::Error::UnknownError,
30
+ ]
31
+
32
+ NON_TRANSIENT = [
33
+ Selenium::WebDriver::Error::InvalidSelectorError,
34
+ Selenium::WebDriver::Error::NoSuchDriverError,
35
+ ArgumentError,
36
+ ]
37
+
38
+ before do
39
+ @dummy = AttemptDummy.new
40
+ end
41
+
42
+ context "when the block succeeds" do
43
+ it "returns the value of the block" do
44
+ expect(@dummy.attempt{42}).must_equal(42)
45
+ end
46
+
47
+ it "yields itself, so that the documented block argument is the receiver" do
48
+ yielded = nil
49
+ @dummy.attempt{|driver| yielded = driver}
50
+ expect(yielded).must_be_same_as(@dummy)
51
+ end
52
+
53
+ it "calls the block once" do
54
+ calls = 0
55
+ @dummy.attempt{calls += 1}
56
+ expect(calls).must_equal(1)
57
+ end
58
+
59
+ it "says nothing" do
60
+ _out, err = capture_io{@dummy.attempt{42}}
61
+ expect(err).must_be_empty
62
+ end
63
+ end
64
+
65
+ context "when the block raises a transient error" do
66
+ TRANSIENT.each do |error_class|
67
+ it "retries #{error_class}" do
68
+ calls = 0
69
+ capture_io do
70
+ @dummy.attempt(3) do
71
+ calls += 1
72
+ raise error_class if calls < 3
73
+ end
74
+ end
75
+ expect(calls).must_equal(3)
76
+ end
77
+ end
78
+
79
+ it "returns the value of the attempt which succeeds" do
80
+ calls = 0
81
+ value = nil
82
+ capture_io do
83
+ value = @dummy.attempt(3) do
84
+ calls += 1
85
+ raise Selenium::WebDriver::Error::StaleElementReferenceError if calls < 2
86
+ :recovered
87
+ end
88
+ end
89
+ expect(value).must_equal(:recovered)
90
+ end
91
+
92
+ it "re-raises once max_attempts is reached" do
93
+ capture_io do
94
+ expect do
95
+ @dummy.attempt(3){raise Selenium::WebDriver::Error::UnknownError}
96
+ end.must_raise(Selenium::WebDriver::Error::UnknownError)
97
+ end
98
+ end
99
+
100
+ it "makes exactly max_attempts attempts before re-raising" do
101
+ calls = 0
102
+ capture_io do
103
+ begin
104
+ @dummy.attempt(3){calls += 1; raise Selenium::WebDriver::Error::UnknownError}
105
+ rescue Selenium::WebDriver::Error::UnknownError
106
+ end
107
+ end
108
+ expect(calls).must_equal(3)
109
+ end
110
+
111
+ it "honours a supplied max_attempts" do
112
+ calls = 0
113
+ capture_io do
114
+ begin
115
+ @dummy.attempt(5){calls += 1; raise Selenium::WebDriver::Error::UnknownError}
116
+ rescue Selenium::WebDriver::Error::UnknownError
117
+ end
118
+ end
119
+ expect(calls).must_equal(5)
120
+ end
121
+
122
+ it "defaults to three attempts" do
123
+ calls = 0
124
+ capture_io do
125
+ begin
126
+ @dummy.attempt{calls += 1; raise Selenium::WebDriver::Error::UnknownError}
127
+ rescue Selenium::WebDriver::Error::UnknownError
128
+ end
129
+ end
130
+ expect(calls).must_equal(3)
131
+ end
132
+
133
+ it "warns once per failed attempt, naming the error" do
134
+ _out, err = capture_io do
135
+ begin
136
+ @dummy.attempt(2){raise Selenium::WebDriver::Error::StaleElementReferenceError}
137
+ rescue Selenium::WebDriver::Error::StaleElementReferenceError
138
+ end
139
+ end
140
+ expect(err.lines.size).must_equal(2)
141
+ expect(err).must_match(/Attempt 1 of 2 failed: Selenium::WebDriver::Error::StaleElementReferenceError/)
142
+ expect(err).must_match(/Attempt 2 of 2 failed: Selenium::WebDriver::Error::StaleElementReferenceError/)
143
+ end
144
+ end
145
+
146
+ context "when the block raises a non-transient error" do
147
+ NON_TRANSIENT.each do |error_class|
148
+ it "does not retry #{error_class}" do
149
+ calls = 0
150
+ capture_io do
151
+ expect do
152
+ @dummy.attempt(3){calls += 1; raise error_class}
153
+ end.must_raise(error_class)
154
+ end
155
+ expect(calls).must_equal(1)
156
+ end
157
+ end
158
+
159
+ it "says nothing, since it did not retry" do
160
+ _out, err = capture_io do
161
+ begin
162
+ @dummy.attempt(3){raise Selenium::WebDriver::Error::InvalidSelectorError}
163
+ rescue Selenium::WebDriver::Error::InvalidSelectorError
164
+ end
165
+ end
166
+ expect(err).must_be_empty
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,45 @@
1
+ # test/webaccount_test.rb
2
+
3
+ # Notes:
4
+ # 1. No browser: Selenium::WebDriver.for is swapped for a stand-in for the length of
5
+ # each example and put back afterwards, so what is asserted is what WebAccount asks
6
+ # of Selenium and what it hands back.
7
+
8
+ require_relative '../lib/webaccount'
9
+
10
+ require 'minitest/autorun'
11
+ require 'minitest-spec-context'
12
+
13
+ describe WebAccount do
14
+ def with_driver_factory(&block)
15
+ calls = []
16
+ stand_in = Object.new
17
+ original = Selenium::WebDriver.method(:for)
18
+ Selenium::WebDriver.define_singleton_method(:for){|browser, options: nil| calls << [browser, options]; stand_in}
19
+ block.call(stand_in, calls)
20
+ ensure
21
+ Selenium::WebDriver.define_singleton_method(:for, original)
22
+ end
23
+
24
+ describe "#driver" do
25
+ it "is public, so that the logged-in browser can be driven from outside" do
26
+ _(WebAccount.public_method_defined?(:driver)).must_equal(true)
27
+ end
28
+
29
+ it "is the browser Selenium made, made once" do
30
+ with_driver_factory do |stand_in, calls|
31
+ account = WebAccount.new
32
+ _(account.driver).must_be_same_as(stand_in)
33
+ _(account.driver).must_be_same_as(stand_in)
34
+ _(calls.length).must_equal(1)
35
+ end
36
+ end
37
+
38
+ it "is made with the browser and driver options given" do
39
+ with_driver_factory do |stand_in, calls|
40
+ WebAccount.new(browser: 'Firefox', driver_options: :options).driver
41
+ _(calls).must_equal([[:firefox, :options]])
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,48 @@
1
+ require_relative './lib/WebAccount/VERSION'
2
+
3
+ class Gem::Specification
4
+ def dependencies=(gems)
5
+ gems.each{|gem| add_dependency(*gem)}
6
+ end
7
+
8
+ def development_dependencies=(gems)
9
+ gems.each{|gem| add_development_dependency(*gem)}
10
+ end
11
+ end
12
+
13
+ Gem::Specification.new do |spec|
14
+ spec.name = 'webaccount'
15
+ spec.version = WebAccount::VERSION
16
+
17
+ spec.summary = "An abstract super-class for navigating a web-based user account via Selenium WebDriver."
18
+ spec.description = "Easily log into web accounts by supplying just a username and password, the URL of the login page, and a few page ids to a sub-class of WebAccount."
19
+
20
+ spec.author = 'thoran'
21
+ spec.email = 'code@thoran.com'
22
+ spec.homepage = 'https://github.com/thoran/webaccount'
23
+ spec.license = 'MIT'
24
+
25
+ spec.require_paths = ['lib']
26
+ spec.required_ruby_version = '>= 3.3'
27
+
28
+ spec.files = [
29
+ 'CHANGELOG',
30
+ 'LICENSE',
31
+ 'Gemfile',
32
+ Dir['lib/**/*.rb'],
33
+ 'Rakefile',
34
+ 'README.md',
35
+ Dir['test/**/*.rb'],
36
+ 'webaccount.gemspec'
37
+ ].flatten
38
+
39
+ spec.dependencies = [
40
+ ['selenium-webdriver', '>= 4.0']
41
+ ]
42
+
43
+ spec.development_dependencies = %w{
44
+ minitest
45
+ minitest-spec-context
46
+ rake
47
+ }
48
+ end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: webaccount
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-04-07 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: selenium-webdriver
@@ -16,9 +15,51 @@ dependencies:
16
15
  requirements:
17
16
  - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: '0'
18
+ version: '4.0'
20
19
  type: :runtime
21
20
  prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '4.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: minitest
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: minitest-spec-context
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rake
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :development
62
+ prerelease: false
22
63
  version_requirements: !ruby/object:Gem::Requirement
23
64
  requirements:
24
65
  - - ">="
@@ -31,23 +72,24 @@ executables: []
31
72
  extensions: []
32
73
  extra_rdoc_files: []
33
74
  files:
34
- - lib/Thoran/Array/AllButFirst/all_but_first.rb
35
- - lib/Thoran/Array/FirstX/firstX.rb
75
+ - CHANGELOG
76
+ - Gemfile
77
+ - LICENSE
78
+ - README.md
79
+ - Rakefile
36
80
  - lib/Thoran/Selenium.rb
37
81
  - lib/Thoran/Selenium/WebDriver/Driver/Attempt/attempt.rb
38
- - lib/Thoran/Selenium/WebDriver/Remote/W3C/Bridge/ConvertLocators/convert_locators.rb
39
- - lib/Thoran/Selenium/WebDriver/SearchContext/ElementPresentQ/element_presentQ.rb
40
- - lib/Thoran/Selenium/WebDriver/SearchContext/ElementsPresentQ/elements_presentQ.rb
41
- - lib/Thoran/Selenium/WebDriver/Setup/setup.rb
42
- - lib/Thoran/String/ToConst/to_const.rb
43
- - lib/WebAccount.rb
44
- - lib/WebAccount/VERSION.rb
82
+ - lib/Thoran/Selenium/WebDriver/Driver/ElementPresentQ/element_presentQ.rb
45
83
  - lib/web_account.rb
46
- homepage: http://github.com/thoran/WebAccount
84
+ - lib/webaccount.rb
85
+ - lib/webaccount/VERSION.rb
86
+ - test/Thoran/Selenium/WebDriver/Driver/Attempt_test.rb
87
+ - test/webaccount_test.rb
88
+ - webaccount.gemspec
89
+ homepage: https://github.com/thoran/webaccount
47
90
  licenses:
48
- - Ruby
91
+ - MIT
49
92
  metadata: {}
50
- post_install_message:
51
93
  rdoc_options: []
52
94
  require_paths:
53
95
  - lib
@@ -55,15 +97,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
55
97
  requirements:
56
98
  - - ">="
57
99
  - !ruby/object:Gem::Version
58
- version: '2.5'
100
+ version: '3.3'
59
101
  required_rubygems_version: !ruby/object:Gem::Requirement
60
102
  requirements:
61
103
  - - ">="
62
104
  - !ruby/object:Gem::Version
63
105
  version: '0'
64
106
  requirements: []
65
- rubygems_version: 3.5.7
66
- signing_key:
107
+ rubygems_version: 4.0.22
67
108
  specification_version: 4
68
109
  summary: An abstract super-class for navigating a web-based user account via Selenium
69
110
  WebDriver.
@@ -1,28 +0,0 @@
1
- # Thoran/Array/AllButFirst/all_but_first.rb
2
- # Thoran::Array::AllButFirst#all_but_first
3
-
4
- # 20141223
5
- # 0.1.0
6
-
7
- # Description: This returns a copy of the receiving array with the first element removed.
8
-
9
- # Changes:
10
- # 1. + Thoran namespace.
11
-
12
- require 'Thoran/Array/FirstX/firstX'
13
-
14
- module Thoran
15
- module Array
16
- module AllButFirst
17
-
18
- def all_but_first
19
- d = self.dup
20
- d.first!
21
- d
22
- end
23
-
24
- end
25
- end
26
- end
27
-
28
- Array.send(:include, Thoran::Array::AllButFirst)
@@ -1,21 +0,0 @@
1
- # Thoran/Array/FirstX/firstX.rb
2
- # Thoran::Array::FirstX#first!
3
-
4
- # 20141223
5
- # 0.1.0
6
-
7
- # Description: Sometimes it makes more sense to treat arrays this way.
8
-
9
- module Thoran
10
- module Array
11
- module FirstX
12
-
13
- def first!
14
- shift
15
- end
16
-
17
- end
18
- end
19
- end
20
-
21
- Array.send(:include, Thoran::Array::FirstX)
@@ -1,51 +0,0 @@
1
- # Thoran/Selenium/WebDriver/Remote/W3C/BridgeMonkeyPatch/ConvertLocators.convert_locators.rb
2
- # Thoran::Selenium::WebDriver::Remote::W3C::BridgeMonkeyPatch::ConvertLocators.convert_locators
3
-
4
- # 20200418
5
- # 0.0.0
6
-
7
- # Changes:
8
- # 1. + Thoran namespace.
9
-
10
- module Thoran
11
- module Selenium
12
- module WebDriver
13
- module Remote
14
- module W3C
15
- module Bridge
16
- module ConvertLocators
17
-
18
- def convert_locators(how, what)
19
- case how.to_s
20
- when 'class name'
21
- how = 'css selector'
22
- what = ".#{escape_css(what)}"
23
- when 'id'
24
- how = 'css selector'
25
- what = "##{escape_css(what)}"
26
- when 'name'
27
- how = 'css selector'
28
- what = "*[name='#{escape_css(what)}']"
29
- when 'tag name'
30
- how = 'css selector'
31
- end
32
- [how, what]
33
- end
34
-
35
- end
36
- end
37
- end
38
- end
39
- end
40
- end
41
- end
42
-
43
- module Selenium
44
- module WebDriver
45
- module Remote
46
- module W3C
47
- Bridge.prepend(Thoran::Selenium::WebDriver::Remote::W3C::Bridge::ConvertLocators)
48
- end
49
- end
50
- end
51
- end