webaccount 0.3.0 → 0.4.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/CHANGELOG +93 -0
- data/Gemfile +2 -0
- data/README.md +181 -0
- data/Rakefile +9 -0
- data/lib/Thoran/Selenium/WebDriver/Driver/Attempt/attempt.rb +29 -19
- data/lib/Thoran/Selenium/WebDriver/Driver/ElementPresentQ/element_presentQ.rb +37 -0
- data/lib/Thoran/Selenium.rb +9 -6
- data/lib/WebAccount/VERSION.rb +4 -3
- data/lib/web_account.rb +1 -1
- data/lib/{WebAccount.rb → webaccount.rb} +68 -38
- data/test/Thoran/Selenium/WebDriver/Driver/Attempt_test.rb +169 -0
- data/webaccount.gemspec +47 -0
- metadata +56 -17
- data/lib/Thoran/Array/AllButFirst/all_but_first.rb +0 -28
- data/lib/Thoran/Array/FirstX/firstX.rb +0 -21
- data/lib/Thoran/Selenium/WebDriver/Remote/W3C/Bridge/ConvertLocators/convert_locators.rb +0 -51
- data/lib/Thoran/Selenium/WebDriver/SearchContext/ElementPresentQ/element_presentQ.rb +0 -39
- data/lib/Thoran/Selenium/WebDriver/SearchContext/ElementsPresentQ/elements_presentQ.rb +0 -39
- data/lib/Thoran/Selenium/WebDriver/Setup/setup.rb +0 -66
- data/lib/Thoran/String/ToConst/to_const.rb +0 -47
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 219aecddbc5a0e05b1463300ade4350d9afd789de19f1a43fe34e492d5ad9296
|
|
4
|
+
data.tar.gz: 6cbc7db55da4afc34b164f3a8a163da4e4e716b313e09b266d2453c98af0c115
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fdb4db1ec12daf5e307f0df3212f51f1eb4da9b1ca505a68a1008e626598d30214f7d235e330b9f856d215efb816c276eb4525a4578937eb7329dedf064d0581
|
|
7
|
+
data.tar.gz: b4515525fce7cb22a90b15f7a05ddc3da14cb870c02d4e95d31959a2ff3c3753d74ad96adfb0bd96deb64937f466c20a0ec7ae3395b9d4d43eb59b72b4881dff
|
data/CHANGELOG
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# CHANGELOG
|
|
2
|
+
|
|
3
|
+
## 20260717
|
|
4
|
+
|
|
5
|
+
0.4.0: Selenium 4 compatibility, achieved mostly by deletion, since Selenium 4 has absorbed most of what these extensions were written for.
|
|
6
|
+
|
|
7
|
+
Breaking:
|
|
8
|
+
a. - WebAccount#user_agent_alias, #user_agent_alias=: Replaced by the browser: argument to new(). A sub-class which set or over-rode user_agent_alias must now pass browser: or over-ride #browser().
|
|
9
|
+
b. ~ webaccount.gemspec: Require selenium-webdriver >= 4.0 and Ruby >= 3.3. This release depends on Selenium 4 behaviour and will not work on Selenium 3.
|
|
10
|
+
c. ~ Thoran::Selenium::WebDriver::Driver::ElementPresentQ#element_present?(): No longer waits. The implicit wait removed in 16 below gave it up to 5s for an element to appear; it is now an instantaneous check, so a sub-class which relied on it as a wait will now silently get false rather than true. Wrap it in a Selenium::WebDriver::Wait, as login() does.
|
|
11
|
+
d. /lib/WebAccount.rb/lib/webaccount.rb/: require 'webaccount', which is what the README has always documented and what the gem name implies, raised a LoadError on a case-sensitive file system. It now works, and it is require 'WebAccount' which raises instead. require 'web_account' is unaffected.
|
|
12
|
+
|
|
13
|
+
1. - Thoran::Selenium::WebDriver::Remote::W3C::Bridge::ConvertLocators: The namespace no longer exists in Selenium 4, which raised a NameError on require, and locator conversion is now native.
|
|
14
|
+
2. - Thoran::Selenium::WebDriver::SearchContext::ElementsPresentQ: find_elements_by() returns an empty array rather than raising, so elements_present?() was unconditionally true.
|
|
15
|
+
3. - Thoran::Selenium::WebDriver::Setup: Firefox::Binary is gone from Selenium 4 and there was never a Chrome or Safari equivalent, so only the browser name branch remained, which is Selenium::WebDriver.for().
|
|
16
|
+
4. - Thoran::String::ToConst: Only ever used by Setup, and Object.const_get() has handled nested constants since Ruby 2.0.
|
|
17
|
+
5. - Thoran::Array::AllButFirst: Only ever used by ToConst.
|
|
18
|
+
6. - Thoran::Array::FirstX: Only ever used by AllButFirst.
|
|
19
|
+
7. ~ Thoran::Selenium: - the three deleted requires.
|
|
20
|
+
8. ~ Thoran::Selenium::WebDriver::Driver::Attempt#attempt(): Yield self, so the documented block argument is the driver rather than nil; use retry rather than loop/break; re-raise rather than calling quit on @driver, which was never assigned, and exiting the process.
|
|
21
|
+
9. ~ Thoran::Selenium::WebDriver::Driver::Attempt#attempt(): - Timeout::Error, which nothing in Selenium 4 raises, so the rescue was dead. Selenium's own timeouts, and those of Selenium::WebDriver::Wait, are Selenium::WebDriver::Error::TimeoutError. Timeout::Error also only resolved at all because selenium-webdriver happens to require 'timeout' for Firefox.
|
|
22
|
+
10. + Thoran::Selenium::WebDriver::Driver::Attempt::TRANSIENT_ERRORS: The errors a flaky page actually raises: ElementClickInterceptedError, ElementNotInteractableError, NoSuchElementError, StaleElementReferenceError, TimeoutError, UnexpectedAlertOpenError, and UnknownError. Only the last two were caught before. InvalidSelectorError and NoSuchDriverError are deliberately excluded, being programming and environment errors respectively, so retrying only delays the report.
|
|
23
|
+
11. ~ Thoran::Selenium::WebDriver::Driver::ElementPresentQ#element_present?(): Use the public find_elements() rather than the private bridge.
|
|
24
|
+
12. /Thoran::Selenium::WebDriver::SearchContext::ElementPresentQ/Thoran::Selenium::WebDriver::Driver::ElementPresentQ/: It patches Driver, as Driver::Attempt does, so it now sits alongside it. Selenium::WebDriver::SearchContext is the find_element()/find_elements() contract which Driver, Element, and ShadowRoot implement, rather than a home for conveniences built on top of it, and Selenium itself has no presence predicate to follow.
|
|
25
|
+
13. + WebAccount#default_browser(), #default_driver_options(): Return :chrome and nil, for over-riding in a sub-class.
|
|
26
|
+
14. + WebAccount#browser(), #driver_options(): Fall back to the defaults. browser() also normalises a String or a capitalised Symbol, since Selenium::WebDriver.for() matches on a lowercase Symbol.
|
|
27
|
+
15. ~ WebAccount#driver(): Construct the driver directly, with browser() and driver_options(), rather than via the deleted Selenium::WebDriver.setup().
|
|
28
|
+
16. ~ WebAccount#driver(): - the implicit wait, since mixing implicit and explicit waits made driver_wait() poll every 5s rather than every 0.2s. See c above.
|
|
29
|
+
17. + WebAccount#find(): Extracted from the four finder methods.
|
|
30
|
+
18. + WebAccount#find_wait(), #default_find_timeout(): A Wait of its own for find(), at 5s. Sharing driver_wait() would have given a missing element 180s to not appear, where the implicit wait removed in 16 raised at 5s. Selenium::WebDriver::Wait ignores NoSuchElementError by default, so find() polls rather than raising, and 5s restores the previous contract.
|
|
31
|
+
19. ~ WebAccount#driver_wait(): /180/30/, via the new #default_wait_timeout(). 180s was tolerable only because the implicit wait removed in 16 made each poll cost 5s, so it bought 36 polls; at the 0.2s interval it now buys 900 polls of the same page. 30s covers a submit, the authentication behind it, and the redirect, and is 90s worst case through attempt()'s three tries rather than 9 minutes.
|
|
32
|
+
20. + WebAccount#username_field_selector(), #password_field_selector(), #login_page_submit_button_selector(), #logout_button_selector(): Fall back to the defaults in one place.
|
|
33
|
+
21. ~ WebAccount#username_field(), #password_field(), #login_page_submit_button(), #logout_button(): Use find() and the selector methods.
|
|
34
|
+
22. ~ WebAccount#logout_button(): Fix @logout_page_submit_button_selector, which was never assigned, so a supplied logout button selector was always ignored in favour of the default.
|
|
35
|
+
23. ~ WebAccount#login(): Use logout_button_selector(), which no longer raises when no selector was supplied.
|
|
36
|
+
24. ~ WebAccount#login(): - attempts = 0, which was unused.
|
|
37
|
+
25. /WebAccount#default_username_field/WebAccount#default_username_field_selector/
|
|
38
|
+
26. /WebAccount#default_password_field/WebAccount#default_password_field_selector/
|
|
39
|
+
27. ~ lib/web_account.rb: /require_relative './WebAccount'/require_relative './webaccount'/, per d above.
|
|
40
|
+
28. ~ WebAccount::VERSION: /0.3.1/0.4.0/
|
|
41
|
+
29. + lib/webaccount.rb: require_relative './WebAccount/VERSION', so that WebAccount::VERSION is defined by requiring the library. It was previously loaded only by the gemspec.
|
|
42
|
+
30. ~ lib/webaccount.rb: /require 'Thoran/Selenium'/require_relative './Thoran/Selenium'/. Thoran/Selenium ships in this gem, so requiring it by $LOAD_PATH meant it only loaded when lib was on the path, and left it to path order in a namespace which several other gems here also ship. require './lib/webaccount' now works.
|
|
43
|
+
31. ~ lib/webaccount.rb: /# WebAccount.rb/# webaccount.rb/ in the header, per d above.
|
|
44
|
+
32. ~ webaccount.gemspec: /spec.license = 'Ruby'/spec.license = 'MIT'/, per the more recent gems.
|
|
45
|
+
33. + test/Thoran/Selenium/WebDriver/Driver/Attempt_test.rb: Cover attempt() against a dummy including the module: the value and receiver yielded, which errors are retried and which are not, the attempt count, and the warning. The transient and non-transient errors are listed in the test rather than read from TRANSIENT_ERRORS, so that the test states the intent rather than agreeing with the implementation. Nothing else in WebAccount is reachable to test: browser(), driver_options(), and the four selector methods are all private, and login(), find(), and logout() need a browser and a settled timeout.
|
|
46
|
+
34. + Rakefile: rake test, per the other gems.
|
|
47
|
+
35. ~ .gitignore (*.lock): Running bundle writes a Gemfile.lock, which is not committed for a gem.
|
|
48
|
+
36. ~ webaccount.gemspec: + minitest, minitest-spec-context, and rake as development dependencies, and + Rakefile and test/ to spec.files.
|
|
49
|
+
37. ~ webaccount.gemspec: Match the layout of the more recent gemspecs: + the dependencies=/development_dependencies= sugar; + spec.require_paths, which was missing; list spec.files explicitly, so that CHANGELOG, Gemfile, and README.md are packaged rather than lib alone; and - spec.date, which Rubygems sets itself and which was stale in every release so far.
|
|
50
|
+
38. ~ README.md: + Choosing a browser, Driver options, and Timeouts; note that logged_out_xpath is required by logout() and shutdown(); prefer find() to driver.find_element() in the sub-classing example, since that example was relying on the implicit wait removed in 16; and fix a stray quotation mark, a missing comma, and a fork URL pointing at month.rb.
|
|
51
|
+
|
|
52
|
+
Known: These are all one question, which is what login(), logout(), and logged_out?() should do
|
|
53
|
+
when the page does not do what was expected: raise, or report. They want one answer between
|
|
54
|
+
them rather than three, they are not compatibility problems, and 0.5's fixtures and
|
|
55
|
+
integration tests are what would pin them, so they are left alone here.
|
|
56
|
+
|
|
57
|
+
i. WebAccount#logged_out?() returns truthy or raises: Wait#until does not return false, so
|
|
58
|
+
the `if logged_out?` in logout() is dead code and logout() always either raises or logs out.
|
|
59
|
+
ii. WebAccount#login() raises Selenium::WebDriver::Error::TimeoutError rather than returning
|
|
60
|
+
false when the logout button never appears, so a wrong password is an exception rather than
|
|
61
|
+
a return value. As in 0.3.1.
|
|
62
|
+
iii. WebAccount#logged_out?() with no logged_out_xpath raises
|
|
63
|
+
Selenium::WebDriver::Error::InvalidSelectorError, from find_elements(:xpath, nil) reaching
|
|
64
|
+
the bridge, which sends nil.to_s. shutdown() calls logout(), so this is reachable from the
|
|
65
|
+
README's implicit-defaults usage.
|
|
66
|
+
iv. Thoran::Selenium::WebDriver::Driver::ElementPresentQ#element_present?() rests on
|
|
67
|
+
find_elements() returning an empty array rather than raising, which is what the deleted
|
|
68
|
+
ElementsPresentQ in 2 above got wrong. Only a driver can confirm it, so it is untested until
|
|
69
|
+
0.5.
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
## 20240407
|
|
73
|
+
|
|
74
|
+
0.3.1: ./lib/README.md --> ./README.md
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
## 20240407
|
|
78
|
+
|
|
79
|
+
0.3.0: Can now use all of Selenium's page selectors (:css, :id, :name:, :xpath) instead of just :id and/or overriding methods in the sub-class.
|
|
80
|
+
1. + WebAccount#default_username_field(): For use in username_field if username_field_selector is nil.
|
|
81
|
+
2. ~ WebAccount#username_field(): Will optionally use the default_username_field if nothing is supplied.
|
|
82
|
+
3. + WebAccount#default_password_field(): For use in password_field if username_field_selector is nil.
|
|
83
|
+
4. ~ WebAccount#password_field(): Will optionally use the default_username_field if nothing is supplied.
|
|
84
|
+
5. + WebAccount#default_login_page_submit_button_selector(): For use in login_page_submit_button if login_page_submit_button is nil.
|
|
85
|
+
6. ~ WebAccount#login_page_submit_button(): Will optionally use the default_login_page_submit_button_selector if nothing is supplied.
|
|
86
|
+
7. + WebAccount#default_logout_button_selector(): For use in logout_button if logout_button is nil.
|
|
87
|
+
8. + WebAccount#logout_button(): Consistent with other elements having a method and will also optionally use the default_logout_button_selector if nothing is supplied.
|
|
88
|
+
9. ~ WebAccount#login(): Use new generic selector for detecting logged-in state.
|
|
89
|
+
10. ~ WebAccount#logout(): Use new logout_button() method instead of in-lining Selenium.
|
|
90
|
+
11. ~ WebAccount::VERSION: /0.2.5/0.3.0/
|
|
91
|
+
12. ~ webaccount.gemspec: Change the date, fix the summary, and the description.
|
|
92
|
+
13. + CHANGELOG.txt
|
|
93
|
+
14. + README.md
|
data/Gemfile
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# WebAccount
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Easily log into web accounts by supplying just a username and password, the URL of the login page, and a few selectors for page elements to a sub-class of WebAccount.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add this line to your application's Gemfile:
|
|
10
|
+
```ruby
|
|
11
|
+
gem 'webaccount'
|
|
12
|
+
```
|
|
13
|
+
And then execute:
|
|
14
|
+
```shell
|
|
15
|
+
$ bundle
|
|
16
|
+
```
|
|
17
|
+
Or install it yourself as:
|
|
18
|
+
```shell
|
|
19
|
+
$ gem install webaccount
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
### Instantiation
|
|
25
|
+
|
|
26
|
+
#### 1. With implicit defaults
|
|
27
|
+
```ruby
|
|
28
|
+
require 'webaccount'
|
|
29
|
+
|
|
30
|
+
class MyAccount < WebAccount; end
|
|
31
|
+
|
|
32
|
+
MyAccount.new(username: username, password: password)
|
|
33
|
+
# => #<MyAccount @username=username, @password=password, @login_page_url=nil,
|
|
34
|
+
# @login_page_password_field_selector=nil, @login_page_username_field_selector=nil,
|
|
35
|
+
# @login_page_submit_button_selector=nil,
|
|
36
|
+
# @logout_button_selector=nil, @logged_out_xpath=nil>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
#### 2. With explicit defaults, then in the comment the ultimate defaults if those values remain nil
|
|
40
|
+
```ruby
|
|
41
|
+
require 'webaccount'
|
|
42
|
+
|
|
43
|
+
class MyAccount < WebAccount
|
|
44
|
+
PAGE_SELECTORS = {
|
|
45
|
+
login_page_url: nil, # There are no defaults, so something like 'https://mysite.com/login' is required.
|
|
46
|
+
login_page_username_field_selector: nil, # Will default to: {name: 'username'}
|
|
47
|
+
login_page_password_field_selector: nil, # Will default to: {name: 'password'}
|
|
48
|
+
login_page_submit_button_selector: nil, # Will default to: {xpath: '//input[@type="submit" and @value="Login"]'}
|
|
49
|
+
logout_button_selector: nil, # Will default to: {xpath: '//input[@type="submit" and @value="Logout"]'}
|
|
50
|
+
logged_out_xpath: nil, # No default value for this, and logout and shutdown require it.
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
MyAccount.new(
|
|
55
|
+
**{username: username, password: password}\
|
|
56
|
+
.merge(MyAccount::PAGE_SELECTORS)
|
|
57
|
+
)
|
|
58
|
+
# => #<MyAccount @username=username, @password=password...>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
#### 3. With supplied values
|
|
62
|
+
```ruby
|
|
63
|
+
require 'webaccount'
|
|
64
|
+
|
|
65
|
+
class MyAccount < WebAccount
|
|
66
|
+
PAGE_SELECTORS = {
|
|
67
|
+
login_page_url: 'https://mysite.com/login',
|
|
68
|
+
login_page_username_field_selector: {id: 'username'},
|
|
69
|
+
login_page_password_field_selector: {id: 'password'},
|
|
70
|
+
login_page_submit_button_selector: {xpath: '//button[@type="submit" and @value="Login"]'},
|
|
71
|
+
logout_button_selector: {css: '#button-logout'},
|
|
72
|
+
logged_out_xpath: '//div[@class="logged-out"]',
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
MyAccount.new(
|
|
77
|
+
**{username: username, password: password}
|
|
78
|
+
.merge(MyAccount::PAGE_SELECTORS)
|
|
79
|
+
)
|
|
80
|
+
# => #<MyAccount @username=username, @password=password...>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Choosing a browser
|
|
84
|
+
|
|
85
|
+
`browser` is passed to `Selenium::WebDriver.for`, so it accepts whatever Selenium does: `:chrome`, `:firefox`, `:safari`, `:edge`, `:ie`. It defaults to `:chrome`, and a String or a capitalised Symbol is fine.
|
|
86
|
+
```ruby
|
|
87
|
+
MyAccount.new(username: username, password: password, browser: :firefox)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Driver options
|
|
91
|
+
|
|
92
|
+
`driver_options` is passed to `Selenium::WebDriver.for` as `options:`. It defaults to nil, which is what Selenium itself defaults to, so leaving it alone gets you a stock browser.
|
|
93
|
+
```ruby
|
|
94
|
+
options = Selenium::WebDriver::Chrome::Options.new
|
|
95
|
+
options.add_argument('--headless=new')
|
|
96
|
+
|
|
97
|
+
MyAccount.new(username: username, password: password, driver_options: options)
|
|
98
|
+
```
|
|
99
|
+
Or in a sub-class, if every instance should be configured the same way:
|
|
100
|
+
```ruby
|
|
101
|
+
class MyAccount < WebAccount
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def default_driver_options
|
|
105
|
+
Selenium::WebDriver::Chrome::Options.new.tap do |options|
|
|
106
|
+
options.add_argument('--headless=new')
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Timeouts
|
|
113
|
+
|
|
114
|
+
There are two, both overridable in a sub-class:
|
|
115
|
+
|
|
116
|
+
| Method | Default | Waits for |
|
|
117
|
+
| --- | --- | --- |
|
|
118
|
+
| `default_find_timeout` | 5s | An element on a page which has already loaded. |
|
|
119
|
+
| `default_wait_timeout` | 30s | Navigation: a submit, the authentication behind it, and the redirect. |
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
class SlowAccount < WebAccount
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def default_wait_timeout
|
|
126
|
+
60
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Logging in, out, and shutting down
|
|
132
|
+
```ruby
|
|
133
|
+
require 'webaccount'
|
|
134
|
+
|
|
135
|
+
class MyAccount < WebAccount; end
|
|
136
|
+
|
|
137
|
+
my_account = MyAccount.new(username: username, password: password)
|
|
138
|
+
my_account.login
|
|
139
|
+
my_account.logout
|
|
140
|
+
my_account.shutdown # If necessary it logs out and then kills the selenium session.
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Overriding the login method to customise logins
|
|
144
|
+
```ruby
|
|
145
|
+
require 'webaccount'
|
|
146
|
+
|
|
147
|
+
class MyAccount < WebAccount
|
|
148
|
+
def login
|
|
149
|
+
# Put custom login code here.
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Create new methods for selection and navigation
|
|
155
|
+
|
|
156
|
+
Prefer `find` over `driver.find_element`: it waits up to `default_find_timeout` for the element, whereas `driver.find_element` raises the moment the element isn't there.
|
|
157
|
+
```ruby
|
|
158
|
+
require 'webaccount'
|
|
159
|
+
|
|
160
|
+
class MyAccount < WebAccount
|
|
161
|
+
def list_page_link
|
|
162
|
+
find(id: 'list')
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def list_page
|
|
166
|
+
login unless logged_in?
|
|
167
|
+
list_page_link.click
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
my_account = MyAccount.new(username: username, password: password)
|
|
172
|
+
my_account.list_page
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Contributing
|
|
176
|
+
|
|
177
|
+
1. Fork it: https://github.com/thoran/WebAccount/fork
|
|
178
|
+
2. Create your feature branch: `git checkout -b my-new-feature`
|
|
179
|
+
3. Commit your changes: `git commit -am 'Add some feature'`
|
|
180
|
+
4. Push to the branch: `git push origin my-new-feature`
|
|
181
|
+
5. Create a new pull request
|
data/Rakefile
ADDED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# Thoran/Selenium/WebDriver/Attempt/attempt.rb
|
|
2
|
-
# Thoran::Selenium::WebDriver::Attempt
|
|
1
|
+
# Thoran/Selenium/WebDriver/Driver/Attempt/attempt.rb
|
|
2
|
+
# Thoran::Selenium::WebDriver::Driver::Attempt#attempt
|
|
3
3
|
|
|
4
|
-
#
|
|
5
|
-
# 0.0
|
|
4
|
+
# 20260717
|
|
5
|
+
# 0.1.0
|
|
6
6
|
|
|
7
7
|
# Examples:
|
|
8
8
|
# 1. driver.attempt do |driver|
|
|
@@ -13,31 +13,41 @@
|
|
|
13
13
|
# end
|
|
14
14
|
|
|
15
15
|
# Changes:
|
|
16
|
-
# 1.
|
|
16
|
+
# 1. Yield self, so that the documented block argument is the driver rather than nil.
|
|
17
|
+
# 2. Use retry rather than loop/break.
|
|
18
|
+
# 3. Re-raise rather than calling quit on @driver, which was never assigned, and exiting the process.
|
|
19
|
+
# 4. - Timeout::Error: Nothing in Selenium 4 raises it. Its own timeouts, and those of
|
|
20
|
+
# Selenium::WebDriver::Wait, are Selenium::WebDriver::Error::TimeoutError.
|
|
21
|
+
# 5. + The errors a flaky page actually raises, none of which were caught before.
|
|
22
|
+
# Deliberately excluded: Error::InvalidSelectorError and Error::NoSuchDriverError, which
|
|
23
|
+
# are programming and environment errors respectively, so retrying only delays the report.
|
|
17
24
|
|
|
18
25
|
module Thoran
|
|
19
26
|
module Selenium
|
|
20
27
|
module WebDriver
|
|
21
28
|
module Driver
|
|
22
29
|
module Attempt
|
|
30
|
+
TRANSIENT_ERRORS = [
|
|
31
|
+
::Selenium::WebDriver::Error::ElementClickInterceptedError,
|
|
32
|
+
::Selenium::WebDriver::Error::ElementNotInteractableError,
|
|
33
|
+
::Selenium::WebDriver::Error::NoSuchElementError,
|
|
34
|
+
::Selenium::WebDriver::Error::StaleElementReferenceError,
|
|
35
|
+
::Selenium::WebDriver::Error::TimeoutError,
|
|
36
|
+
::Selenium::WebDriver::Error::UnexpectedAlertOpenError,
|
|
37
|
+
::Selenium::WebDriver::Error::UnknownError,
|
|
38
|
+
].freeze
|
|
23
39
|
|
|
24
|
-
def attempt(max_attempts = 3
|
|
40
|
+
def attempt(max_attempts = 3)
|
|
25
41
|
attempts = 0
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
@driver.quit
|
|
34
|
-
puts "Giving up after #{max_attempts} attempts to #{__method__} because #{e}."
|
|
35
|
-
exit
|
|
36
|
-
end
|
|
37
|
-
end
|
|
42
|
+
begin
|
|
43
|
+
yield self
|
|
44
|
+
rescue *TRANSIENT_ERRORS => e
|
|
45
|
+
attempts += 1
|
|
46
|
+
warn "Attempt #{attempts} of #{max_attempts} failed: #{e.class}"
|
|
47
|
+
retry if attempts < max_attempts
|
|
48
|
+
raise
|
|
38
49
|
end
|
|
39
50
|
end
|
|
40
|
-
|
|
41
51
|
end
|
|
42
52
|
end
|
|
43
53
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Thoran/Selenium/WebDriver/Driver/ElementPresentQ/element_presentQ.rb
|
|
2
|
+
# Thoran::Selenium::WebDriver::Driver::ElementPresentQ#element_present?
|
|
3
|
+
|
|
4
|
+
# 20260717
|
|
5
|
+
# 0.1.0
|
|
6
|
+
|
|
7
|
+
# Examples:
|
|
8
|
+
# 1. driver.element_present?(:xpath, '//a[@id="submit"]')
|
|
9
|
+
# 2. driver.element_present?(:css, '#button-logout')
|
|
10
|
+
|
|
11
|
+
# Changes:
|
|
12
|
+
# 1. Use the public find_elements() rather than the private bridge.
|
|
13
|
+
# 2. ^Driver, alongside Driver::Attempt, since this patches Driver too. Selenium's
|
|
14
|
+
# SearchContext is the find_element()/find_elements() contract, which Driver, Element,
|
|
15
|
+
# and ShadowRoot implement, rather than a home for conveniences built on top of it.
|
|
16
|
+
|
|
17
|
+
module Thoran
|
|
18
|
+
module Selenium
|
|
19
|
+
module WebDriver
|
|
20
|
+
module Driver
|
|
21
|
+
module ElementPresentQ
|
|
22
|
+
def element_present?(selector_type, selector)
|
|
23
|
+
!find_elements(selector_type, selector).empty?
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
module Selenium
|
|
32
|
+
module WebDriver
|
|
33
|
+
class Driver
|
|
34
|
+
include Thoran::Selenium::WebDriver::Driver::ElementPresentQ
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/Thoran/Selenium.rb
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# Thoran/Selenium.rb
|
|
2
2
|
# Thoran::Selenium
|
|
3
3
|
|
|
4
|
-
#
|
|
5
|
-
# 0.
|
|
4
|
+
# 20260717
|
|
5
|
+
# 0.1.0
|
|
6
|
+
|
|
7
|
+
# Changes:
|
|
8
|
+
# 1. - Selenium::WebDriver::Remote::W3C::Bridge::ConvertLocators
|
|
9
|
+
# 2. - Selenium::WebDriver::SearchContext::ElementsPresentQ
|
|
10
|
+
# 3. - Selenium::WebDriver::Setup
|
|
11
|
+
# 4. /Selenium::WebDriver::SearchContext::ElementPresentQ/Selenium::WebDriver::Driver::ElementPresentQ/
|
|
6
12
|
|
|
7
13
|
require_relative './Selenium/WebDriver/Driver/Attempt/attempt'
|
|
8
|
-
require_relative './Selenium/WebDriver/
|
|
9
|
-
require_relative './Selenium/WebDriver/SearchContext/ElementPresentQ/element_presentQ'
|
|
10
|
-
require_relative './Selenium/WebDriver/SearchContext/ElementsPresentQ/elements_presentQ'
|
|
11
|
-
require_relative './Selenium/WebDriver/Setup/setup'
|
|
14
|
+
require_relative './Selenium/WebDriver/Driver/ElementPresentQ/element_presentQ'
|
data/lib/WebAccount/VERSION.rb
CHANGED
data/lib/web_account.rb
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
#
|
|
1
|
+
# webaccount.rb
|
|
2
2
|
# WebAccount
|
|
3
3
|
|
|
4
4
|
require 'selenium-webdriver'
|
|
5
|
-
|
|
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?(
|
|
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
|
|
@@ -70,20 +70,50 @@ class WebAccount
|
|
|
70
70
|
|
|
71
71
|
private
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
# driver construction
|
|
74
|
+
|
|
75
|
+
def default_browser
|
|
76
|
+
:chrome
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def browser
|
|
80
|
+
(@browser || default_browser).to_sym.downcase
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def default_driver_options
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def driver_options
|
|
88
|
+
@driver_options || default_driver_options
|
|
75
89
|
end
|
|
76
90
|
|
|
77
91
|
def driver
|
|
78
|
-
@driver ||= (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
92
|
+
@driver ||= Selenium::WebDriver.for(browser, options: 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:
|
|
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
|
|
102
|
-
{name:
|
|
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
|
-
|
|
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
|
|
119
|
-
{name:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/webaccount.gemspec
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
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 = 'http://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
|
+
'Gemfile',
|
|
31
|
+
Dir['lib/**/*.rb'],
|
|
32
|
+
'Rakefile',
|
|
33
|
+
'README.md',
|
|
34
|
+
Dir['test/**/*.rb'],
|
|
35
|
+
'webaccount.gemspec'
|
|
36
|
+
].flatten
|
|
37
|
+
|
|
38
|
+
spec.dependencies = [
|
|
39
|
+
['selenium-webdriver', '>= 4.0']
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
spec.development_dependencies = %w{
|
|
43
|
+
minitest
|
|
44
|
+
minitest-spec-context
|
|
45
|
+
rake
|
|
46
|
+
}
|
|
47
|
+
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.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- thoran
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
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,22 @@ executables: []
|
|
|
31
72
|
extensions: []
|
|
32
73
|
extra_rdoc_files: []
|
|
33
74
|
files:
|
|
34
|
-
-
|
|
35
|
-
-
|
|
75
|
+
- CHANGELOG
|
|
76
|
+
- Gemfile
|
|
77
|
+
- README.md
|
|
78
|
+
- Rakefile
|
|
36
79
|
- lib/Thoran/Selenium.rb
|
|
37
80
|
- lib/Thoran/Selenium/WebDriver/Driver/Attempt/attempt.rb
|
|
38
|
-
- lib/Thoran/Selenium/WebDriver/
|
|
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
|
|
81
|
+
- lib/Thoran/Selenium/WebDriver/Driver/ElementPresentQ/element_presentQ.rb
|
|
44
82
|
- lib/WebAccount/VERSION.rb
|
|
45
83
|
- lib/web_account.rb
|
|
84
|
+
- lib/webaccount.rb
|
|
85
|
+
- test/Thoran/Selenium/WebDriver/Driver/Attempt_test.rb
|
|
86
|
+
- webaccount.gemspec
|
|
46
87
|
homepage: http://github.com/thoran/WebAccount
|
|
47
88
|
licenses:
|
|
48
|
-
-
|
|
89
|
+
- MIT
|
|
49
90
|
metadata: {}
|
|
50
|
-
post_install_message:
|
|
51
91
|
rdoc_options: []
|
|
52
92
|
require_paths:
|
|
53
93
|
- lib
|
|
@@ -55,15 +95,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
55
95
|
requirements:
|
|
56
96
|
- - ">="
|
|
57
97
|
- !ruby/object:Gem::Version
|
|
58
|
-
version: '
|
|
98
|
+
version: '3.3'
|
|
59
99
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
60
100
|
requirements:
|
|
61
101
|
- - ">="
|
|
62
102
|
- !ruby/object:Gem::Version
|
|
63
103
|
version: '0'
|
|
64
104
|
requirements: []
|
|
65
|
-
rubygems_version:
|
|
66
|
-
signing_key:
|
|
105
|
+
rubygems_version: 4.0.16
|
|
67
106
|
specification_version: 4
|
|
68
107
|
summary: An abstract super-class for navigating a web-based user account via Selenium
|
|
69
108
|
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
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# Thoran/Selenium/WebDriver/SearchContext/ElementPresentQ/element_presentQ.rb
|
|
2
|
-
# Thoran::Selenium::WebDriver::SearchContext::ElementPresentQ.element_present?
|
|
3
|
-
|
|
4
|
-
# 20200418
|
|
5
|
-
# 0.0.0
|
|
6
|
-
|
|
7
|
-
# Examples:
|
|
8
|
-
# 1. driver.element_present?(:xpath, '//a[@id="submit"]')
|
|
9
|
-
# 2. driver.element_present?(:id, @logout_button_id)
|
|
10
|
-
|
|
11
|
-
# Changes:
|
|
12
|
-
# 1. + Thoran namespace.
|
|
13
|
-
|
|
14
|
-
module Thoran
|
|
15
|
-
module Selenium
|
|
16
|
-
module WebDriver
|
|
17
|
-
module SearchContext
|
|
18
|
-
module ElementPresentQ
|
|
19
|
-
|
|
20
|
-
def element_present?(selector_type, selector)
|
|
21
|
-
bridge.find_element_by(selector_type, selector)
|
|
22
|
-
true
|
|
23
|
-
rescue ::Selenium::WebDriver::Error::NoSuchElementError
|
|
24
|
-
false
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
module Selenium
|
|
34
|
-
module WebDriver
|
|
35
|
-
class Driver
|
|
36
|
-
include Thoran::Selenium::WebDriver::SearchContext::ElementPresentQ
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# Thoran/Selenium/WebDriver/SearchContext/ElementsPresentQ/elements_presentQ.rb
|
|
2
|
-
# Thoran::Selenium::WebDriver::SearchContext::ElementsPresentQ.elements_present?
|
|
3
|
-
|
|
4
|
-
# 20200418
|
|
5
|
-
# 0.0.0
|
|
6
|
-
|
|
7
|
-
# Examples:
|
|
8
|
-
# 1. driver.elements_present?(:xpath, '//li[@class="item"]')
|
|
9
|
-
# 2. driver.elements_present?(:css, 'li.item')
|
|
10
|
-
|
|
11
|
-
# Changes:
|
|
12
|
-
# 1. + Thoran namespace.
|
|
13
|
-
|
|
14
|
-
module Thoran
|
|
15
|
-
module Selenium
|
|
16
|
-
module WebDriver
|
|
17
|
-
module SearchContext
|
|
18
|
-
module ElementsPresentQ
|
|
19
|
-
|
|
20
|
-
def elements_present?(selector_type, selector)
|
|
21
|
-
bridge.find_elements_by(selector_type, selector)
|
|
22
|
-
true
|
|
23
|
-
rescue ::Selenium::WebDriver::Error::NoSuchElementError
|
|
24
|
-
false
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
module Selenium
|
|
34
|
-
module WebDriver
|
|
35
|
-
class Driver
|
|
36
|
-
include Thoran::Selenium::WebDriver::SearchContext::ElementsPresentQ
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
# Thoran/Selenium/WebDriver/Setup/setup.rb
|
|
2
|
-
# Thoran::Selenium::WebDriver.setup
|
|
3
|
-
|
|
4
|
-
# 20200419
|
|
5
|
-
# 0.0.1
|
|
6
|
-
|
|
7
|
-
# Usage:
|
|
8
|
-
# 1. Binary is in the path:
|
|
9
|
-
# Selenium::WebDriver.setup('chrome') # String - lowercase
|
|
10
|
-
# Selenium::WebDriver.setup('Chrome') # String - capitalised
|
|
11
|
-
# Selenium::WebDriver.setup(:chrome) # Symbol - lowercase
|
|
12
|
-
# Selenium::WebDriver.setup(:Chrome) # Symbol - capitalised
|
|
13
|
-
# 2. Binary is not in the path:
|
|
14
|
-
# Selenium::WebDriver.setup('~/Applications/Firefox.app/Contents/MacOS/firefox-bin')
|
|
15
|
-
|
|
16
|
-
# Changes:
|
|
17
|
-
# 1. + Thoran namespace.
|
|
18
|
-
|
|
19
|
-
require 'selenium-webdriver'
|
|
20
|
-
require 'Thoran/String/ToConst/to_const'
|
|
21
|
-
|
|
22
|
-
module Thoran
|
|
23
|
-
module Selenium
|
|
24
|
-
module WebDriver
|
|
25
|
-
module Setup
|
|
26
|
-
|
|
27
|
-
def setup(path_or_browser)
|
|
28
|
-
if File.exist?(File.expand_path(path_or_browser.to_s))
|
|
29
|
-
path = path_or_browser
|
|
30
|
-
at(path)
|
|
31
|
-
::Selenium::WebDriver.for(selenium_browser(path))
|
|
32
|
-
else
|
|
33
|
-
browser = path_or_browser.to_sym.downcase
|
|
34
|
-
::Selenium::WebDriver.for(browser)
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
private
|
|
39
|
-
|
|
40
|
-
def at(path)
|
|
41
|
-
selenium_binary_constant(path).path = File.expand_path(path)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def selenium_browser(path)
|
|
45
|
-
case path
|
|
46
|
-
when /chrome/i; :chrome
|
|
47
|
-
when /firefox/i; :firefox
|
|
48
|
-
when /safari/i; :safari
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# This method seem to be necessary only to Firefox, since there are no equivalent constants for Safari or Chrome.
|
|
53
|
-
def selenium_binary_constant(path)
|
|
54
|
-
"Selenium::WebDriver::#{selenium_browser(path).to_s.capitalize}::Binary".to_const
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
module Selenium
|
|
63
|
-
module WebDriver
|
|
64
|
-
extend Thoran::Selenium::WebDriver::Setup
|
|
65
|
-
end
|
|
66
|
-
end
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# Thoran/String/ToConst/to_const.rb
|
|
2
|
-
# Thoran::String::ToConst#to_const
|
|
3
|
-
|
|
4
|
-
# 20141223
|
|
5
|
-
# 0.2.0
|
|
6
|
-
|
|
7
|
-
# Description: This takes a string and returns a constant, with unlimited namespacing.
|
|
8
|
-
|
|
9
|
-
# History: Derived from Object#to_const 0.3.0, and superceding Object#to_const.
|
|
10
|
-
|
|
11
|
-
# Changes:
|
|
12
|
-
# 1. + Thoran namespace.
|
|
13
|
-
|
|
14
|
-
# Todo:
|
|
15
|
-
# 1. This only works for two levels of constants. Three and you're stuffed. So, this needs to be recursive...
|
|
16
|
-
# Done iteratively as of 0.1.0.
|
|
17
|
-
# 2. Make this work for symbols. However, this will only work if there's no namespacing. ie. :A is OK, but :A::B is not.
|
|
18
|
-
|
|
19
|
-
# Discussion:
|
|
20
|
-
# 1. Should this go separately into classes for which ::const_get will work and be removed from Object? Done as of 0.1.0.
|
|
21
|
-
|
|
22
|
-
require 'Thoran/Array/AllButFirst/all_but_first'
|
|
23
|
-
|
|
24
|
-
module Thoran
|
|
25
|
-
module String
|
|
26
|
-
module ToConst
|
|
27
|
-
|
|
28
|
-
def to_const
|
|
29
|
-
if self =~ /::/
|
|
30
|
-
constants = self.split('::')
|
|
31
|
-
constant = Object.const_get(constants.first)
|
|
32
|
-
constants = constants.all_but_first
|
|
33
|
-
until constants.empty? do
|
|
34
|
-
constant = constant.const_get(constants.shift)
|
|
35
|
-
end
|
|
36
|
-
else
|
|
37
|
-
constant = Object.const_get(self)
|
|
38
|
-
end
|
|
39
|
-
constant
|
|
40
|
-
end
|
|
41
|
-
alias_method :to_constant, :to_const
|
|
42
|
-
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
String.send(:include, Thoran::String::ToConst)
|