async-webdriver 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/lib/async/webdriver/bridge/chrome.rb +81 -0
  4. data/lib/async/webdriver/bridge/firefox.rb +80 -0
  5. data/lib/async/webdriver/bridge/generic.rb +91 -0
  6. data/lib/async/webdriver/bridge/pool.rb +99 -0
  7. data/lib/async/webdriver/bridge/process_group.rb +77 -0
  8. data/lib/async/webdriver/bridge.rb +30 -0
  9. data/lib/async/webdriver/client.rb +71 -26
  10. data/lib/async/webdriver/element.rb +270 -17
  11. data/lib/async/webdriver/error.rb +214 -0
  12. data/lib/async/webdriver/locator.rb +127 -0
  13. data/lib/async/webdriver/request_helper.rb +120 -0
  14. data/lib/async/webdriver/scope/alerts.rb +40 -0
  15. data/lib/async/webdriver/scope/cookies.rb +43 -0
  16. data/lib/async/webdriver/scope/document.rb +41 -0
  17. data/lib/async/webdriver/scope/elements.rb +111 -0
  18. data/lib/async/webdriver/scope/fields.rb +66 -0
  19. data/lib/async/webdriver/scope/frames.rb +33 -0
  20. data/lib/async/webdriver/scope/navigation.rb +50 -0
  21. data/lib/async/webdriver/scope/printing.rb +22 -0
  22. data/lib/async/webdriver/scope/screen_capture.rb +23 -0
  23. data/lib/async/webdriver/scope/timeouts.rb +63 -0
  24. data/lib/async/webdriver/scope.rb +15 -0
  25. data/lib/async/webdriver/session.rb +107 -65
  26. data/lib/async/webdriver/version.rb +8 -3
  27. data/lib/async/webdriver/xpath.rb +29 -0
  28. data/lib/async/webdriver.rb +7 -12
  29. data/{LICENSE.txt → license.md} +6 -6
  30. data/readme.md +37 -0
  31. data.tar.gz.sig +0 -0
  32. metadata +71 -149
  33. metadata.gz.sig +0 -0
  34. data/.gitignore +0 -11
  35. data/.rspec +0 -3
  36. data/.ruby-gemset +0 -1
  37. data/.ruby-version +0 -1
  38. data/.travis.yml +0 -7
  39. data/Gemfile +0 -6
  40. data/Gemfile.lock +0 -103
  41. data/Guardfile +0 -45
  42. data/README.md +0 -3
  43. data/Rakefile +0 -6
  44. data/async-webdriver.gemspec +0 -37
  45. data/bin/console +0 -12
  46. data/bin/setup +0 -8
  47. data/examples/multiple_sessions.rb +0 -29
  48. data/lib/async/webdriver/connection.rb +0 -78
  49. data/lib/async/webdriver/connection_path.rb +0 -25
  50. data/lib/async/webdriver/execute.rb +0 -29
  51. data/lib/async/webdriver/session_creator.rb +0 -22
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2023, by Samuel Williams.
5
+
6
+ require_relative 'version'
7
+ require_relative 'error'
8
+
9
+ module Async
10
+ module WebDriver
11
+ # Wraps the HTTP client to provide a consistent interface.
12
+ module RequestHelper
13
+ # The web element identifier is the string constant "element-6066-11e4-a52e-4f735466cecf".
14
+ ELEMENT_KEY = "element-6066-11e4-a52e-4f735466cecf"
15
+
16
+ # The content type for requests and responses.
17
+ CONTENT_TYPE = "application/json"
18
+
19
+ # Headers to send with GET requests.
20
+ GET_HEADERS = [
21
+ ["user-agent", "Async::WebDriver/#{VERSION}"],
22
+ ["accept", CONTENT_TYPE],
23
+ ].freeze
24
+
25
+ # Headers to send with POST requests.
26
+ POST_HEADERS = GET_HEADERS + [
27
+ ["content-type", "#{CONTENT_TYPE}; charset=UTF-8"],
28
+ ].freeze
29
+
30
+ # The path used for making requests to the web driver bridge.
31
+ # @parameter path [String | Nil] The path to append to the request path.
32
+ # @returns [String] The path used for making requests to the web driver bridge.
33
+ def request_path(path = nil)
34
+ if path
35
+ "/#{path}"
36
+ else
37
+ "/"
38
+ end
39
+ end
40
+
41
+ # Unwrap JSON objects into their corresponding Ruby objects.
42
+ #
43
+ # If the value is a Hash and represents an element, then it will be unwrapped into an {ruby Element}.
44
+ #
45
+ # @parameter value [Hash | Array | Object] The value to unwrap.
46
+ # @returns [Object] The unwrapped value.
47
+ def unwrap_object(value)
48
+ if value.is_a?(Hash) and value.key?(ELEMENT_KEY)
49
+ Element.new(self.session, value[ELEMENT_KEY])
50
+ else
51
+ value
52
+ end
53
+ end
54
+
55
+ # Used by `JSON.load` to unwrap objects.
56
+ def unwrap_objects(value)
57
+ case value
58
+ when Hash
59
+ value.transform_values!(&method(:unwrap_object))
60
+ when Array
61
+ value.map!(&method(:unwrap_object))
62
+ end
63
+ end
64
+
65
+ # Extract the value from the reply.
66
+ #
67
+ # If the value is a Hash and represents an error, then it will be raised as an appropriate subclass of {ruby Error}.
68
+ #
69
+ # @parameter reply [Hash] The reply from the server.
70
+ # @returns [Object] The value of the reply.
71
+ def extract_value(reply)
72
+ value = reply["value"]
73
+
74
+ if value.is_a?(Hash) and error = value["error"]
75
+ raise ERROR_CODES.fetch(error, Error), value["message"]
76
+ end
77
+
78
+ if block_given?
79
+ return yield(reply)
80
+ else
81
+ return value
82
+ end
83
+ end
84
+
85
+ # Make a GET request to the bridge and extract the value.
86
+ # @parameter path [String | Nil] The path to append to the request path.
87
+ # @returns [Object | Nil] The value of the reply.
88
+ def get(path)
89
+ Console.debug(self, "GET #{request_path(path)}")
90
+ response = @delegate.get(request_path(path), GET_HEADERS)
91
+ reply = JSON.load(response.read, self.method(:unwrap_objects))
92
+
93
+ return extract_value(reply)
94
+ end
95
+
96
+ # Make a POST request to the bridge and extract the value.
97
+ # @parameter path [String | Nil] The path to append to the request path.
98
+ # @parameter arguments [Hash | Nil] The arguments to send with the request.
99
+ # @returns [Object | Nil] The value of the reply.
100
+ def post(path, arguments = {}, &block)
101
+ Console.debug(self, "POST #{request_path(path)}", arguments: arguments)
102
+ response = @delegate.post(request_path(path), POST_HEADERS, arguments ? JSON.dump(arguments) : nil)
103
+ reply = JSON.load(response.read, self.method(:unwrap_objects))
104
+
105
+ return extract_value(reply, &block)
106
+ end
107
+
108
+ # Make a DELETE request to the bridge and extract the value.
109
+ # @parameter path [String | Nil] The path to append to the request path.
110
+ # @returns [Object | Nil] The value of the reply, if any.
111
+ def delete(path = nil)
112
+ Console.debug(self, "DELETE #{request_path(path)}")
113
+ response = @delegate.delete(request_path(path), POST_HEADERS)
114
+ reply = JSON.load(response.read, self.method(:unwrap_objects))
115
+
116
+ return extract_value(reply)
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2023, by Samuel Williams.
5
+
6
+ module Async
7
+ module WebDriver
8
+ module Scope
9
+ # Helpers for working with alerts.
10
+ #
11
+ # ``` ruby
12
+ # session.dismiss_alert
13
+ # session.accept_alert
14
+ # session.alert_text
15
+ # session.set_alert_text("Hello, World!")
16
+ # ```
17
+ module Alerts
18
+ # Dismiss the current alert.
19
+ def dismiss_alert
20
+ session.post("alert/dismiss")
21
+ end
22
+
23
+ # Accept the current alert.
24
+ def accept_alert
25
+ session.post("alert/accept")
26
+ end
27
+
28
+ # Get the text of the current alert.
29
+ def alert_text
30
+ session.get("alert/text")
31
+ end
32
+
33
+ # Set the text input of the current alert.
34
+ def set_alert_text(text)
35
+ session.post("alert/text", {text: text})
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2023, by Samuel Williams.
5
+
6
+ module Async
7
+ module WebDriver
8
+ module Scope
9
+ # Helpers for working with cookies.
10
+ module Cookies
11
+ # Get all cookies.
12
+ def cookies
13
+ session.get("cookie")
14
+ end
15
+
16
+ # Get a cookie by name.
17
+ # @parameter name [String] The name of the cookie.
18
+ def cookie(name)
19
+ session.get("cookie/#{name}")
20
+ end
21
+
22
+ # Add a cookie.
23
+ # @parameter name [String] The name of the cookie.
24
+ # @parameter value [String] The value of the cookie.
25
+ # @parameter options [Hash] Additional options.
26
+ def add_cookie(name, value, **options)
27
+ session.post("cookie", {name: name, value: value}.merge(options))
28
+ end
29
+
30
+ # Delete a cookie by name.
31
+ # @parameter name [String] The name of the cookie.
32
+ def delete_cookie(name)
33
+ session.delete("cookie/#{name}")
34
+ end
35
+
36
+ # Delete all cookies.
37
+ def delete_all_cookies
38
+ session.delete("cookie")
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2023, by Samuel Williams.
5
+
6
+ module Async
7
+ module WebDriver
8
+ module Scope
9
+ # Helpers for working with the document.
10
+ module Document
11
+ # Get the current document title.
12
+ # @returns [String] The document title.
13
+ def title
14
+ get("title")
15
+ end
16
+
17
+ # Get the current document source.
18
+ # @returns [String] The document source.
19
+ def source
20
+ get("source")
21
+ end
22
+
23
+ # Execute a script in the current document.
24
+ # @parameter script [String] The script to execute.
25
+ # @parameter arguments [Array] The arguments to pass to the script.
26
+ # @returns [Object] The result of the script.
27
+ def execute(script, *arguments)
28
+ post("execute/sync", {script: script, args: arguments})
29
+ end
30
+
31
+ # Execute a script in the current document asynchronously.
32
+ # @parameter script [String] The script to execute.
33
+ # @parameter arguments [Array] The arguments to pass to the script.
34
+ # @returns [Object] The result of the script.
35
+ def execute_async(script, *arguments)
36
+ post("execute/async", {script: script, args: arguments})
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2023, by Samuel Williams.
5
+
6
+ require 'base64'
7
+
8
+ module Async
9
+ module WebDriver
10
+ module Scope
11
+ # Helpers for finding elements.
12
+ module Elements
13
+ # Find an element using the given locator. If no element is found, an exception is raised.
14
+ # @parameter locator [Locator] The locator to use.
15
+ # @returns [Element] The element.
16
+ # @raises [NoSuchElementError] If the element does not exist.
17
+ def find_element(locator)
18
+ current_scope.post("element", locator)
19
+ end
20
+
21
+ # Find an element using the given CSS selector.
22
+ # @parameter css [String] The CSS selector to use.
23
+ # @returns [Element] The element.
24
+ # @raises [NoSuchElementError] If the element does not exist.
25
+ def find_element_by_css(css)
26
+ find_element({using: "css selector", value: css})
27
+ end
28
+
29
+ # Find an element using the given link text.
30
+ # @parameter text [String] The link text to use.
31
+ # @returns [Element] The element.
32
+ # @raises [NoSuchElementError] If the element does not exist.
33
+ def find_element_by_link_text(text)
34
+ find_element({using: "link text", value: text})
35
+ end
36
+
37
+ # Find an element using the given partial link text.
38
+ # @parameter text [String] The partial link text to use.
39
+ # @returns [Element] The element.
40
+ # @raises [NoSuchElementError] If the element does not exist.
41
+ def find_element_by_partial_link_text(text)
42
+ find_element({using: "partial link text", value: text})
43
+ end
44
+
45
+ # Find an element using the given tag name.
46
+ # @parameter name [String] The tag name to use.
47
+ # @returns [Element] The element.
48
+ # @raises [NoSuchElementError] If the element does not exist.
49
+ def find_element_by_tag_name(name)
50
+ find_element({using: "tag name", value: name})
51
+ end
52
+
53
+ # Find an element using the given XPath expression.
54
+ # @parameter xpath [String] The XPath expression to use.
55
+ # @returns [Element] The element.
56
+ # @raises [NoSuchElementError] If the element does not exist.
57
+ def find_element_by_xpath(xpath)
58
+ find_element({using: "xpath", value: xpath})
59
+ end
60
+
61
+ # Find all elements using the given locator. If no elements are found, an empty array is returned.
62
+ # @parameter locator [Locator] The locator to use.
63
+ # @returns [Array(Element)] The elements.
64
+ def find_elements(locator)
65
+ current_scope.post("elements", locator)
66
+ end
67
+
68
+ # Find all elements using the given CSS selector.
69
+ # @parameter css [String] The CSS selector to use.
70
+ # @returns [Array(Element)] The elements.
71
+ def find_elements_by_css(css)
72
+ find_elements({using: "css selector", value: css})
73
+ end
74
+
75
+ # Find all elements using the given link text.
76
+ # @parameter text [String] The link text to use.
77
+ # @returns [Array(Element)] The elements.
78
+ def find_elements_by_link_text(text)
79
+ find_elements({using: "link text", value: text})
80
+ end
81
+
82
+ # Find all elements using the given partial link text.
83
+ # @parameter text [String] The partial link text to use.
84
+ # @returns [Array(Element)] The elements.
85
+ def find_elements_by_partial_link_text(text)
86
+ find_elements({using: "partial link text", value: text})
87
+ end
88
+
89
+ # Find all elements using the given tag name.
90
+ # @parameter name [String] The tag name to use.
91
+ # @returns [Array(Element)] The elements.
92
+ def find_elements_by_tag_name(name)
93
+ find_elements({using: "tag name", value: name})
94
+ end
95
+
96
+ # Find all elements using the given XPath expression.
97
+ # @parameter xpath [String] The XPath expression to use.
98
+ # @returns [Array(Element)] The elements.
99
+ def find_elements_by_xpath(xpath)
100
+ find_elements({using: "xpath", value: xpath})
101
+ end
102
+
103
+ # Find all children of the current element.
104
+ # @returns [Array(Element)] The children of the current element.
105
+ def children
106
+ find_elements_by_xpath("./child::*")
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2023, by Samuel Williams.
5
+
6
+ require 'base64'
7
+
8
+ require_relative '../xpath'
9
+
10
+ module Async
11
+ module WebDriver
12
+ module Scope
13
+ # Helpers for working with forms and form fields.
14
+ module Fields
15
+ # Find a field with the given name.
16
+ # @parameter name [String] The name of the field.
17
+ # @returns [Element] The field.
18
+ # @raises [NoSuchElementError] If the field does not exist.
19
+ def find_field(name)
20
+ current_scope.find_element_by_xpath("//*[@name=#{XPath::escape(name)}]")
21
+ end
22
+
23
+ # Fill in a field with the given name.
24
+ #
25
+ # Clears the field before filling it in.
26
+ #
27
+ # @parameter name [String] The name of the field.
28
+ # @parameter value [String] The value to fill in.
29
+ # @raises [NoSuchElementError] If the field does not exist.
30
+ def fill_in(name, value)
31
+ element = find_field(name)
32
+
33
+ if element.tag_name == "input" || element.tag_name == "textarea"
34
+ element.clear
35
+ end
36
+
37
+ element.send_keys(value)
38
+ end
39
+
40
+ # Click a button with the given label.
41
+ # @parameter label [String] The label of the button.
42
+ # @raises [NoSuchElementError] If the button does not exist.
43
+ def click_button(label)
44
+ element = current_scope.find_element_by_xpath("//button[text()=#{XPath::escape(label)}] | //input[@type='submit' and @value=#{XPath::escape(label)}] | //input[@type='button' and @value=#{XPath::escape(label)}]")
45
+
46
+ element.click
47
+ end
48
+
49
+ # Check a checkbox with the given name.
50
+ #
51
+ # Does not modify the checkbox if it is already in the desired state.
52
+ #
53
+ # @parameter field_name [String] The name of the checkbox.
54
+ # @parameter value [Boolean] The value to set the checkbox to.
55
+ # @raises [NoSuchElementError] If the checkbox does not exist.
56
+ def check(field_name, value = true)
57
+ element = current_scope.find_element(xpath: "//input[@type='checkbox' and @name='#{field_name}']")
58
+
59
+ if element.checked? != value
60
+ element.click
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2023, by Samuel Williams.
5
+
6
+ module Async
7
+ module WebDriver
8
+ module Scope
9
+ # Helpers for working with frames.
10
+ #
11
+ # ``` ruby
12
+ # session.switch_to_frame(frame)
13
+ # session.switch_to_parent_frame
14
+ # ```
15
+ module Frames
16
+ # Switch to the given frame.
17
+ #
18
+ # @parameter frame [Element] The frame to switch to.
19
+ # @raises [NoSuchFrameError] If the frame does not exist.
20
+ def switch_to_frame(frame)
21
+ session.post("frame", {id: frame})
22
+ end
23
+
24
+ # Switch back to the parent frame.
25
+ #
26
+ # You should use this method to switch back to the parent frame after switching to a child frame.
27
+ def switch_to_parent_frame
28
+ session.post("frame/parent")
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2023, by Samuel Williams.
5
+
6
+ require 'uri'
7
+
8
+ module Async
9
+ module WebDriver
10
+ module Scope
11
+ # Helpers for navigating the browser.
12
+ module Navigation
13
+ # Navigate to the given URL.
14
+ # @parameter url [String] The URL to navigate to.
15
+ def navigate_to(url)
16
+ session.post("url", {url: url})
17
+ end
18
+
19
+ alias visit navigate_to
20
+
21
+ # Get the current URL.
22
+ # @returns [String] The current URL.
23
+ def current_url
24
+ session.get("url")
25
+ end
26
+
27
+ # Get the path component of the current URL.
28
+ # @returns [String] The current path.
29
+ def current_path
30
+ URI.parse(current_url).path
31
+ end
32
+
33
+ # Navigate back in the browser history.
34
+ def navigate_back
35
+ session.post("back")
36
+ end
37
+
38
+ # Navigate forward in the browser history.
39
+ def navigate_forward
40
+ session.post("forward")
41
+ end
42
+
43
+ # Refresh the current page.
44
+ def refresh
45
+ session.post("refresh")
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2023, by Samuel Williams.
5
+
6
+ require 'base64'
7
+
8
+ module Async
9
+ module WebDriver
10
+ module Scope
11
+ # Helpers for working with printing.
12
+ module Printing
13
+ # Print the current page and return the result as a Base64 encoded string containing a PDF representation of the paginated document.
14
+ def print(page_ranges: nil, total_pages: nil)
15
+ reply = session.post("print", {pageRanges: page_ranges, totalPages: total_pages}.compact)
16
+
17
+ return Base64.decode64(reply["value"])
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2023, by Samuel Williams.
5
+
6
+ require 'base64'
7
+
8
+ module Async
9
+ module WebDriver
10
+ module Scope
11
+ # Helpers for working with screen capture.
12
+ module ScreenCapture
13
+ # Take a screenshot of the current page or element.
14
+ # @returns [String] The screenshot as a Base64 encoded string.
15
+ def screenshot
16
+ reply = current_scope.post("screenshot")
17
+
18
+ return Base64.decode64(reply)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2023, by Samuel Williams.
5
+
6
+ module Async
7
+ module WebDriver
8
+ module Scope
9
+ # Helpers for working with timeouts.
10
+ #
11
+ # If your tests are failing because the page is not loading fast enough, you can increase the page load timeout:
12
+ #
13
+ # ``` ruby
14
+ # session.script_timeout = 1000 # 1 second
15
+ # session.implicit_wait_timeout = 10_000 # 10 seconds
16
+ # session.page_load_timeout = 60_000 # 60 seconds
17
+ # ```
18
+ module Timeouts
19
+ # Get the current timeouts.
20
+ # @returns [Hash] The timeouts.
21
+ def timeouts
22
+ session.get("timeouts")
23
+ end
24
+
25
+ # The script timeout is the amount of time the driver should wait when executing JavaScript asynchronously.
26
+ # @returns [Integer] The timeout in milliseconds.
27
+ def script_timeout
28
+ timeouts["script"]
29
+ end
30
+
31
+ # Set the script timeout.
32
+ # @parameter value [Integer] The timeout in milliseconds.
33
+ def script_timeout=(value)
34
+ session.post("timeouts", {script: value})
35
+ end
36
+
37
+ # The implicit wait timeout is the amount of time the driver should wait when searching for elements.
38
+ # @returns [Integer] The timeout in milliseconds.
39
+ def implicit_wait_timeout
40
+ timeouts["implicit"]
41
+ end
42
+
43
+ # Set the implicit wait timeout.
44
+ # @parameter value [Integer] The timeout in milliseconds.
45
+ def implicit_wait_timeout=(value)
46
+ session.post("timeouts", {implicit: value})
47
+ end
48
+
49
+ # The page load timeout is the amount of time the driver should wait when loading a page.
50
+ # @returns [Integer] The timeout in milliseconds.
51
+ def page_load_timeout
52
+ timeouts["pageLoad"]
53
+ end
54
+
55
+ # Set the page load timeout.
56
+ # @parameter value [Integer] The timeout in milliseconds.
57
+ def page_load_timeout=(value)
58
+ session.post("timeouts", {pageLoad: value})
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2023, by Samuel Williams.
5
+
6
+ require_relative 'scope/alerts'
7
+ require_relative 'scope/cookies'
8
+ require_relative 'scope/document'
9
+ require_relative 'scope/elements'
10
+ require_relative 'scope/fields'
11
+ require_relative 'scope/frames'
12
+ require_relative 'scope/navigation'
13
+ require_relative 'scope/printing'
14
+ require_relative 'scope/screen_capture'
15
+ require_relative 'scope/timeouts'