async-webdriver 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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 -69
  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'