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.
- checksums.yaml +4 -4
- data/CHANGELOG +115 -0
- data/Gemfile +2 -0
- data/LICENSE +21 -0
- data/README.md +198 -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/web_account.rb +1 -1
- data/lib/webaccount/VERSION.rb +6 -0
- data/lib/{WebAccount.rb → webaccount.rb} +69 -39
- data/test/Thoran/Selenium/WebDriver/Driver/Attempt_test.rb +169 -0
- data/test/webaccount_test.rb +45 -0
- data/webaccount.gemspec +48 -0
- metadata +60 -19
- 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
- data/lib/WebAccount/VERSION.rb +0 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 90b5b4da3fa0eee7ac4a4c6c85ce3b59a7640bc7aaff8065b7c8432920d8a255
|
|
4
|
+
data.tar.gz: 1a5efd14b2d09b0dd2d6eeaef4128272d7e34e18779e62cf968a95455858ee1a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dbf5f58ce0a99f2232796010962a20f39b45a55e8ba0c5c414b2c3c50ea3c8f54fe409ab4b36878d36a5cf849664b187c3119bc3a79e483c50c2e2196f3b55a8
|
|
7
|
+
data.tar.gz: 935406e37e640aafd799a60a422314282874d0ffc5369fe398dde3b0948987da01583fc649ffdb47ebf58c421c0881fcf2a00dbeabeb5595ad1e92cac890b37c
|
data/CHANGELOG
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# CHANGELOG
|
|
2
|
+
|
|
3
|
+
## 20261011
|
|
4
|
+
|
|
5
|
+
0.5.0: + WebAccount#driver, public, so that the logged-in browser can be driven from outside.
|
|
6
|
+
|
|
7
|
+
driver was private, so once login() had done its work the session it made could be used only by a sub-class. Something which wants the pages behind a login without being a WebAccount, a spider fetching and screenshotting them, had no way to reach the browser holding the session.
|
|
8
|
+
|
|
9
|
+
1. ~ WebAccount#driver(): public, after shutdown(); what it does is unchanged, the browser being made once, with browser() and driver_options(), upon first being asked for.
|
|
10
|
+
2. + test/webaccount_test.rb: driver public, made once, and made with the browser and driver options given, with Selenium::WebDriver.for swapped for a stand-in so that no browser is wanted. All three fail against 0.4.1.
|
|
11
|
+
3. ~ README.md: + Driving the browser once logged in.
|
|
12
|
+
4. ~ VERSION: /0.4.1/0.5.0/
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## 20260822
|
|
16
|
+
|
|
17
|
+
0.4.1: + LICENSE, which the gemspec has claimed without one being present.
|
|
18
|
+
|
|
19
|
+
1. + LICENSE: the MIT text, copyright 2020-2026 thoran. spec.license has named MIT with no licence text in the repository, so the gem shipped a licence nobody could read.
|
|
20
|
+
2. ~ webaccount.gemspec: + 'LICENSE' to spec.files, without which the file sits in the repository and still does not ship.
|
|
21
|
+
3. + README.md: a License section, saying MIT, last.
|
|
22
|
+
4. ~ VERSION: /0.4.0/0.4.1/
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
## 20260717
|
|
26
|
+
|
|
27
|
+
0.4.0: Selenium 4 compatibility, achieved mostly by deletion, since Selenium 4 has absorbed most of what these extensions were written for.
|
|
28
|
+
|
|
29
|
+
Breaking:
|
|
30
|
+
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().
|
|
31
|
+
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.
|
|
32
|
+
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.
|
|
33
|
+
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.
|
|
34
|
+
|
|
35
|
+
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.
|
|
36
|
+
2. - Thoran::Selenium::WebDriver::SearchContext::ElementsPresentQ: find_elements_by() returns an empty array rather than raising, so elements_present?() was unconditionally true.
|
|
37
|
+
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().
|
|
38
|
+
4. - Thoran::String::ToConst: Only ever used by Setup, and Object.const_get() has handled nested constants since Ruby 2.0.
|
|
39
|
+
5. - Thoran::Array::AllButFirst: Only ever used by ToConst.
|
|
40
|
+
6. - Thoran::Array::FirstX: Only ever used by AllButFirst.
|
|
41
|
+
7. ~ Thoran::Selenium: - the three deleted requires.
|
|
42
|
+
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.
|
|
43
|
+
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.
|
|
44
|
+
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.
|
|
45
|
+
11. ~ Thoran::Selenium::WebDriver::Driver::ElementPresentQ#element_present?(): Use the public find_elements() rather than the private bridge.
|
|
46
|
+
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.
|
|
47
|
+
13. + WebAccount#default_browser(), #default_driver_options(): Return :chrome and nil, for over-riding in a sub-class.
|
|
48
|
+
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.
|
|
49
|
+
15. ~ WebAccount#driver(): Construct the driver directly, with browser() and driver_options(), rather than via the deleted Selenium::WebDriver.setup().
|
|
50
|
+
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.
|
|
51
|
+
17. + WebAccount#find(): Extracted from the four finder methods.
|
|
52
|
+
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.
|
|
53
|
+
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.
|
|
54
|
+
20. + WebAccount#username_field_selector(), #password_field_selector(), #login_page_submit_button_selector(), #logout_button_selector(): Fall back to the defaults in one place.
|
|
55
|
+
21. ~ WebAccount#username_field(), #password_field(), #login_page_submit_button(), #logout_button(): Use find() and the selector methods.
|
|
56
|
+
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.
|
|
57
|
+
23. ~ WebAccount#login(): Use logout_button_selector(), which no longer raises when no selector was supplied.
|
|
58
|
+
24. ~ WebAccount#login(): - attempts = 0, which was unused.
|
|
59
|
+
25. /WebAccount#default_username_field/WebAccount#default_username_field_selector/
|
|
60
|
+
26. /WebAccount#default_password_field/WebAccount#default_password_field_selector/
|
|
61
|
+
27. ~ lib/web_account.rb: /require_relative './WebAccount'/require_relative './webaccount'/, per d above.
|
|
62
|
+
28. ~ WebAccount::VERSION: /0.3.1/0.4.0/
|
|
63
|
+
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.
|
|
64
|
+
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.
|
|
65
|
+
31. ~ lib/webaccount.rb: /# WebAccount.rb/# webaccount.rb/ in the header, per d above.
|
|
66
|
+
32. ~ webaccount.gemspec: /spec.license = 'Ruby'/spec.license = 'MIT'/, per the more recent gems.
|
|
67
|
+
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.
|
|
68
|
+
34. + Rakefile: rake test, per the other gems.
|
|
69
|
+
35. ~ .gitignore (*.lock): Running bundle writes a Gemfile.lock, which is not committed for a gem.
|
|
70
|
+
36. ~ webaccount.gemspec: + minitest, minitest-spec-context, and rake as development dependencies, and + Rakefile and test/ to spec.files.
|
|
71
|
+
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.
|
|
72
|
+
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.
|
|
73
|
+
|
|
74
|
+
Known: These are all one question, which is what login(), logout(), and logged_out?() should do
|
|
75
|
+
when the page does not do what was expected: raise, or report. They want one answer between
|
|
76
|
+
them rather than three, they are not compatibility problems, and 0.5's fixtures and
|
|
77
|
+
integration tests are what would pin them, so they are left alone here.
|
|
78
|
+
|
|
79
|
+
i. WebAccount#logged_out?() returns truthy or raises: Wait#until does not return false, so
|
|
80
|
+
the `if logged_out?` in logout() is dead code and logout() always either raises or logs out.
|
|
81
|
+
ii. WebAccount#login() raises Selenium::WebDriver::Error::TimeoutError rather than returning
|
|
82
|
+
false when the logout button never appears, so a wrong password is an exception rather than
|
|
83
|
+
a return value. As in 0.3.1.
|
|
84
|
+
iii. WebAccount#logged_out?() with no logged_out_xpath raises
|
|
85
|
+
Selenium::WebDriver::Error::InvalidSelectorError, from find_elements(:xpath, nil) reaching
|
|
86
|
+
the bridge, which sends nil.to_s. shutdown() calls logout(), so this is reachable from the
|
|
87
|
+
README's implicit-defaults usage.
|
|
88
|
+
iv. Thoran::Selenium::WebDriver::Driver::ElementPresentQ#element_present?() rests on
|
|
89
|
+
find_elements() returning an empty array rather than raising, which is what the deleted
|
|
90
|
+
ElementsPresentQ in 2 above got wrong. Only a driver can confirm it, so it is untested until
|
|
91
|
+
0.5.
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
## 20240407
|
|
95
|
+
|
|
96
|
+
0.3.1: ./lib/README.md --> ./README.md
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
## 20240407
|
|
100
|
+
|
|
101
|
+
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.
|
|
102
|
+
1. + WebAccount#default_username_field(): For use in username_field if username_field_selector is nil.
|
|
103
|
+
2. ~ WebAccount#username_field(): Will optionally use the default_username_field if nothing is supplied.
|
|
104
|
+
3. + WebAccount#default_password_field(): For use in password_field if username_field_selector is nil.
|
|
105
|
+
4. ~ WebAccount#password_field(): Will optionally use the default_username_field if nothing is supplied.
|
|
106
|
+
5. + WebAccount#default_login_page_submit_button_selector(): For use in login_page_submit_button if login_page_submit_button is nil.
|
|
107
|
+
6. ~ WebAccount#login_page_submit_button(): Will optionally use the default_login_page_submit_button_selector if nothing is supplied.
|
|
108
|
+
7. + WebAccount#default_logout_button_selector(): For use in logout_button if logout_button is nil.
|
|
109
|
+
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.
|
|
110
|
+
9. ~ WebAccount#login(): Use new generic selector for detecting logged-in state.
|
|
111
|
+
10. ~ WebAccount#logout(): Use new logout_button() method instead of in-lining Selenium.
|
|
112
|
+
11. ~ WebAccount::VERSION: /0.2.5/0.3.0/
|
|
113
|
+
12. ~ webaccount.gemspec: Change the date, fix the summary, and the description.
|
|
114
|
+
13. + CHANGELOG.txt
|
|
115
|
+
14. + README.md
|
data/Gemfile
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020-2026 thoran
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
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
|
+
### Driving the browser once logged in
|
|
144
|
+
|
|
145
|
+
`driver` is the logged-in browser, a `Selenium::WebDriver::Driver`, for whatever WebAccount doesn't do itself: fetching pages behind the login, reading them, or taking screenshots of them.
|
|
146
|
+
```ruby
|
|
147
|
+
my_account = MyAccount.new(username: username, password: password)
|
|
148
|
+
my_account.login
|
|
149
|
+
my_account.driver.get('https://mysite.com/members/page')
|
|
150
|
+
my_account.driver.page_source
|
|
151
|
+
my_account.driver.save_screenshot('page.png')
|
|
152
|
+
my_account.shutdown
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Overriding the login method to customise logins
|
|
156
|
+
```ruby
|
|
157
|
+
require 'webaccount'
|
|
158
|
+
|
|
159
|
+
class MyAccount < WebAccount
|
|
160
|
+
def login
|
|
161
|
+
# Put custom login code here.
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Create new methods for selection and navigation
|
|
167
|
+
|
|
168
|
+
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.
|
|
169
|
+
```ruby
|
|
170
|
+
require 'webaccount'
|
|
171
|
+
|
|
172
|
+
class MyAccount < WebAccount
|
|
173
|
+
def list_page_link
|
|
174
|
+
find(id: 'list')
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def list_page
|
|
178
|
+
login unless logged_in?
|
|
179
|
+
list_page_link.click
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
my_account = MyAccount.new(username: username, password: password)
|
|
184
|
+
my_account.list_page
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Contributing
|
|
188
|
+
|
|
189
|
+
1. Fork it: https://github.com/thoran/WebAccount/fork
|
|
190
|
+
2. Create your feature branch: `git checkout -b my-new-feature`
|
|
191
|
+
3. Commit your changes: `git commit -am 'Add some feature'`
|
|
192
|
+
4. Push to the branch: `git push origin my-new-feature`
|
|
193
|
+
5. Create a new pull request
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
MIT
|
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/web_account.rb
CHANGED