async-webdriver 0.1.2 → 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 -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
@@ -1,20 +1,273 @@
1
- require "selenium-webdriver"
2
- module Async
3
- module Webdriver
4
- class Element
5
- def initialize(json:, connection:)
6
- @id = json.fetch "ELEMENT"
7
- @element_connection = ConnectionPath.new "element/#{@id}", connection: connection
8
- end
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2023, by Samuel Williams.
9
5
 
10
- def tag_name
11
- @element_connection.call method: "get", path: "name"
12
- value
13
- end
6
+ require_relative 'request_helper'
14
7
 
15
- def text
16
- @element_connection.call method: "get", path: "text"
17
- end
18
- end
19
- end
8
+ require_relative 'scope'
9
+
10
+ module Async
11
+ module WebDriver
12
+ # An element represents a DOM element. This class is used to interact with the DOM.
13
+ #
14
+ # ``` ruby
15
+ # element = session.find_element(:css, "main#content")
16
+ # element.click
17
+ # ```
18
+ class Element
19
+ # Attributes associated with an element.
20
+ class Attributes
21
+ include Enumerable
22
+
23
+ def initialize(element)
24
+ @element = element
25
+ @keys = nil
26
+ end
27
+
28
+ # Get the value of an attribute.
29
+ # @parameter name [String] The name of the attribute.
30
+ # @returns [Object] The value of the attribute with the given name.
31
+ def [](name)
32
+ @element.attribute(name)
33
+ end
34
+
35
+ # Set the value of an attribute.
36
+ # @parameter name [String] The name of the attribute.
37
+ # @parameter value [Object] The value of the attribute.
38
+ def []=(name, value)
39
+ @element.set_attribute(name, value)
40
+ end
41
+
42
+ # Get the names of all attributes.
43
+ def keys
44
+ @element.execute("return this.getAttributeNames()")
45
+ end
46
+
47
+ # Check if an attribute exists.
48
+ # @parameter name [String] The name of the attribute.
49
+ # @returns [Boolean] True if the attribute exists.
50
+ def key?(name)
51
+ @element.execute("return this.hasAttribute(...arguments)", name)
52
+ end
53
+
54
+ # Iterate over all attributes.
55
+ # @yields {|name, value| ...} The name and value of each attribute.
56
+ # @parameter name [String] The name of the attribute.
57
+ # @parameter value [Object] The value of the attribute.
58
+ def each(&block)
59
+ return to_enum unless block_given?
60
+
61
+ keys.each do |key|
62
+ yield key, self[key]
63
+ end
64
+ end
65
+ end
66
+
67
+ # Initialize the element.
68
+ # @parameter session [Session] The session the element belongs to.
69
+ # @parameter id [String] The element identifier.
70
+ def initialize(session, id)
71
+ @session = session
72
+ @delegate = session.delegate
73
+ @id = id
74
+
75
+ @attributes = nil
76
+ @properties = nil
77
+ end
78
+
79
+ # @returns [Hash] The JSON representation of the element.
80
+ def as_json
81
+ {ELEMENT_KEY => @id}
82
+ end
83
+
84
+ # @returns [String] The JSON representation of the element.
85
+ def to_json(...)
86
+ as_json.to_json(...)
87
+ end
88
+
89
+ # @attribute [Session] The session the element belongs to.
90
+ attr :session
91
+
92
+ # @attribute [Protocol::HTTP::Middleware] The underlying HTTP client (or wrapper).
93
+ attr :delegate
94
+
95
+ # @attribute [String] The element identifier.
96
+ attr :id
97
+
98
+ # The path used for making requests to the web driver bridge.
99
+ # @parameter path [String | Nil] The path to append to the request path.
100
+ # @returns [String] The path used for making requests to the web driver bridge.
101
+ def request_path(path = nil)
102
+ if path
103
+ "/session/#{@session.id}/element/#{@id}/#{path}"
104
+ else
105
+ "/session/#{@session}/element/#{@id}"
106
+ end
107
+ end
108
+
109
+ include RequestHelper
110
+
111
+ # The current scope to use for making subsequent requests.
112
+ # @returns [Element] The element.
113
+ def current_scope
114
+ self
115
+ end
116
+
117
+ include Scope::Alerts
118
+ include Scope::Cookies
119
+ include Scope::Elements
120
+ include Scope::Fields
121
+ include Scope::Printing
122
+ include Scope::ScreenCapture
123
+
124
+ # Execute a script in the context of the element. `this` will be the element.
125
+ # @parameter script [String] The script to execute.
126
+ # @parameter arguments [Array] The arguments to pass to the script.
127
+ def execute(script, *arguments)
128
+ @session.execute("return (function(){#{script}}).call(...arguments)", self, *arguments)
129
+ end
130
+
131
+ # Execute a script in the context of the element. `this` will be the element.
132
+ # @parameter script [String] The script to execute.
133
+ # @parameter arguments [Array] The arguments to pass to the script.
134
+ def execute_async(script, *arguments)
135
+ @session.execute_async("return (function(){#{script}}).call(...arguments)", self, *arguments)
136
+ end
137
+
138
+ # Get the value of an attribute.
139
+ #
140
+ # Given an attribute name, e.g. `href`, this method will return the value of the attribute, as if you had executed the following JavaScript:
141
+ #
142
+ # ```js
143
+ # element.getAttribute("href")
144
+ # ```
145
+ #
146
+ # @parameter name [String] The name of the attribute.
147
+ # @returns [Object] The value of the attribute.
148
+ def attribute(name)
149
+ get("attribute/#{name}")
150
+ end
151
+
152
+ # Set the value of an attribute.
153
+ # @parameter name [String] The name of the attribute.
154
+ # @parameter value [Object] The value of the attribute.
155
+ def set_attribute(name, value)
156
+ execute("this.setAttribute(...arguments)", name, value)
157
+ end
158
+
159
+ # Get attributes associated with the element.
160
+ # @returns [Attributes] The attributes associated with the element.
161
+ def attributes
162
+ @attributes ||= Attributes.new(self)
163
+ end
164
+
165
+ # Get the value of a property.
166
+ #
167
+ # Given a property name, e.g. `offsetWidth`, this method will return the value of the property, as if you had executed the following JavaScript:
168
+ #
169
+ # ```js
170
+ # element.offsetWidth
171
+ # ```
172
+ #
173
+ # @parameter name [String] The name of the property.
174
+ # @returns [Object] The value of the property.
175
+ def property(name)
176
+ get("property/#{name}")
177
+ end
178
+
179
+ # Get the value of a CSS property.
180
+ #
181
+ # Given a CSS property name, e.g. `width`, this method will return the value of the property, as if you had executed the following JavaScript:
182
+ #
183
+ # ```js
184
+ # window.getComputedStyle(element).width
185
+ # ```
186
+ #
187
+ # @parameter name [String] The name of the CSS property.
188
+ # @returns [String] The value of the CSS property.
189
+ def css(name)
190
+ get("css/#{name}")
191
+ end
192
+
193
+ # Get the text content of the element.
194
+ #
195
+ # This method will return the text content of the element, as if you had executed the following JavaScript:
196
+ #
197
+ # ```js
198
+ # element.textContent
199
+ # ```
200
+ #
201
+ # @returns [String] The text content of the element.
202
+ def text
203
+ get("text")
204
+ end
205
+
206
+ # Get the element's tag name.
207
+ #
208
+ # This method will return the tag name of the element, as if you had executed the following JavaScript:
209
+ #
210
+ # ```js
211
+ # element.tagName
212
+ # ```
213
+ #
214
+ # @returns [String] The tag name of the element.
215
+ def tag_name
216
+ get("name")
217
+ end
218
+
219
+ # A struct representing the size of an element.
220
+ Rectangle = Struct.new(:x, :y, :width, :height)
221
+
222
+ # Get the element's bounding rectangle.
223
+ # @returns [Rectangle] The element's bounding rectangle.
224
+ def rectangle
225
+ get("rect").tap do |reply|
226
+ Rectangle.new(reply["x"], reply["y"], reply["width"], reply["height"])
227
+ end
228
+ end
229
+
230
+ # Whether the element is selected OR checked.
231
+ # @returns [Boolean] True if the element is selected OR checked.
232
+ def selected?
233
+ get("selected")
234
+ end
235
+
236
+ alias checked? selected?
237
+
238
+ # Whether the element is enabled.
239
+ # @returns [Boolean] True if the element is enabled.
240
+ def enabled?
241
+ get("enabled")
242
+ end
243
+
244
+ # Whether the element is displayed.
245
+ # @returns [Boolean] True if the element is displayed.
246
+ def displayed?
247
+ get("displayed")
248
+ end
249
+
250
+ # Click the element.
251
+ def click
252
+ post("click")
253
+ end
254
+
255
+ # Clear the element.
256
+ def clear
257
+ post("clear")
258
+ end
259
+
260
+ # Send keys to the element. Simulates a user typing keys while the element is focused.
261
+ def send_keys(text)
262
+ post("value", {text: text})
263
+ end
264
+
265
+ FRAME_TAGS = ["frame", "iframe"].freeze
266
+
267
+ # Whether the element is a frame.
268
+ def frame?
269
+ FRAME_TAGS.include?(self.tag_name)
270
+ end
271
+ end
272
+ end
20
273
  end
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2023, by Samuel Williams.
5
+
6
+ require_relative 'version'
7
+
8
+ module Async
9
+ module WebDriver
10
+ # Error Code HTTP Status JSON Error Code Description
11
+ # element click intercepted 400 element click intercepted The Element Click command could not be completed because the element receiving the events is obscuring the element that was requested clicked.
12
+ # element not interactable 400 element not interactable A command could not be completed because the element is not pointer- or keyboard interactable.
13
+ # insecure certificate 400 insecure certificate Navigation caused the user agent to hit a certificate warning, which is usually the result of an expired or invalid TLS certificate.
14
+ # invalid argument 400 invalid argument The arguments passed to a command are either invalid or malformed.
15
+ # invalid cookie domain 400 invalid cookie domain An illegal attempt was made to set a cookie under a different domain than the current page.
16
+ # invalid element state 400 invalid element state A command could not be completed because the element is in an invalid state, e.g. attempting to clear an element that isn’t both editable and resettable.
17
+ # invalid selector 400 invalid selector Argument was an invalid selector.
18
+ # invalid session id 404 invalid session id Occurs if the given session id is not in the list of active sessions, meaning the session either does not exist or that it’s not active.
19
+ # javascript error 500 javascript error An error occurred while executing JavaScript supplied by the user.
20
+ # move target out of bounds 500 move target out of bounds The target for mouse interaction is not in the browser’s viewport and cannot be brought into that viewport.
21
+ # no such alert 404 no such alert An attempt was made to operate on a modal dialog when one was not open.
22
+ # no such cookie 404 no such cookie No cookie matching the given path name was found amongst the associated cookies of the current browsing context’s active document.
23
+ # no such element 404 no such element An element could not be located on the page using the given search parameters.
24
+ # no such frame 404 no such frame A command to switch to a frame could not be satisfied because the frame could not be found.
25
+ # no such window 404 no such window A command to switch to a window could not be satisfied because the window could not be found.
26
+ # no such shadow root 404 no such shadow root The element does not have a shadow root.
27
+ # script timeout error 500 script timeout A script did not complete before its timeout expired.
28
+ # session not created 500 session not created A new session could not be created.
29
+ # stale element reference 404 stale element reference A command failed because the referenced element is no longer attached to the DOM.
30
+ # detached shadow root 404 detached shadow root A command failed because the referenced shadow root is no longer attached to the DOM.
31
+ # timeout 500 timeout An operation did not complete before its timeout expired.
32
+ # unable to set cookie 500 unable to set cookie A command to set a cookie’s value could not be satisfied.
33
+ # unable to capture screen 500 unable to capture screen A screen capture was made impossible.
34
+ # unexpected alert open 500 unexpected alert open A modal dialog was open, blocking this operation.
35
+ # unknown command 404 unknown command A command could not be executed because the remote end is not aware of it.
36
+ # unknown error 500 unknown error An unknown error occurred in the remote end while processing the command.
37
+ # unknown method 405 unknown method The requested command matched a known URL but did not match any method for that URL.
38
+ # unsupported operation 500 unsupported operation Indicates that a command that should have executed properly cannot be supported for some reason.
39
+
40
+ class Error < StandardError
41
+ end
42
+
43
+ # The Element Click command could not be completed because the element receiving the events is obscuring the element that was requested clicked.
44
+ class ElementClickInterceptedError < Error
45
+ CODE = "element click intercepted"
46
+ end
47
+
48
+ # A command could not be completed because the element is not pointer- or keyboard interactable.
49
+ class ElementNotInteractableError < Error
50
+ CODE = "element not interactable"
51
+ end
52
+
53
+ # Navigation caused the user agent to hit a certificate warning, which is usually the result of an expired or invalid TLS certificate.
54
+ class InsecureCertificateError < Error
55
+ CODE = "insecure certificate"
56
+ end
57
+
58
+ # The arguments passed to a command are either invalid or malformed.
59
+ class InvalidArgumentError < Error
60
+ CODE = "invalid argument"
61
+ end
62
+
63
+ # An illegal attempt was made to set a cookie under a different domain than the current page.
64
+ class InvalidCookieDomainError < Error
65
+ CODE = "invalid cookie domain"
66
+ end
67
+
68
+ # A command could not be completed because the element is in an invalid state, e.g. attempting to clear an element that isn’t both editable and resettable.
69
+ class InvalidElementStateError < Error
70
+ CODE = "invalid element state"
71
+ end
72
+
73
+ # Argument was an invalid selector.
74
+ class InvalidSelectorError < Error
75
+ CODE = "invalid selector"
76
+ end
77
+
78
+ # Occurs if the given session id is not in the list of active sessions, meaning the session either does not exist or that it’s not active.
79
+ class InvalidSessionIdError < Error
80
+ CODE = "invalid session id"
81
+ end
82
+
83
+ # An error occurred while executing JavaScript supplied by the user.
84
+ class JavaScriptError < Error
85
+ CODE = "javascript error"
86
+ end
87
+
88
+ # The target for mouse interaction is not in the browser’s viewport and cannot be brought into that viewport.
89
+ class MoveTargetOutOfBoundsError < Error
90
+ CODE = "move target out of bounds"
91
+ end
92
+
93
+ # An attempt was made to operate on a modal dialog when one was not open.
94
+ class NoSuchAlertError < Error
95
+ CODE = "no such alert"
96
+ end
97
+
98
+ # No cookie matching the given path name was found amongst the associated cookies of the current browsing context’s active document.
99
+ class NoSuchCookieError < Error
100
+ CODE = "no such cookie"
101
+ end
102
+
103
+ # An element could not be located on the page using the given search parameters.
104
+ class NoSuchElementError < Error
105
+ CODE = "no such element"
106
+ end
107
+
108
+ # A command to switch to a frame could not be satisfied because the frame could not be found.
109
+ class NoSuchFrameError < Error
110
+ CODE = "no such frame"
111
+ end
112
+
113
+ # A command to switch to a window could not be satisfied because the window could not be found.
114
+ class NoSuchWindowError < Error
115
+ CODE = "no such window"
116
+ end
117
+
118
+ # The element does not have a shadow root.
119
+ class NoSuchShadowRootError < Error
120
+ CODE = "no such shadow root"
121
+ end
122
+
123
+ # A script did not complete before its timeout expired.
124
+ class ScriptTimeoutError < Error
125
+ CODE = "script timeout error"
126
+ end
127
+
128
+ # A new session could not be created.
129
+ class SessionNotCreatedError < Error
130
+ CODE = "session not created"
131
+ end
132
+
133
+ # A command failed because the referenced element is no longer attached to the DOM.
134
+ class StaleElementReferenceError < Error
135
+ CODE = "stale element reference"
136
+ end
137
+
138
+ # A command failed because the referenced shadow root is no longer attached to the DOM.
139
+ class DetachedShadowRootError < Error
140
+ CODE = "detached shadow root"
141
+ end
142
+
143
+ # An operation did not complete before its timeout expired.
144
+ class TimeoutError < Error
145
+ CODE = "timeout"
146
+ end
147
+
148
+ # A command to set a cookie’s value could not be satisfied.
149
+ class UnableToSetCookieError < Error
150
+ CODE = "unable to set cookie"
151
+ end
152
+
153
+ # A screen capture was made impossible.
154
+ class UnableToCaptureScreenError < Error
155
+ CODE = "unable to capture screen"
156
+ end
157
+
158
+ # A modal dialog was open, blocking this operation.
159
+ class UnexpectedAlertOpenError < Error
160
+ CODE = "unexpected alert open"
161
+ end
162
+
163
+ # A command could not be executed because the remote end is not aware of it.
164
+ class UnknownCommandError < Error
165
+ CODE = "unknown command"
166
+ end
167
+
168
+ # An unknown error occurred in the remote end while processing the command.
169
+ class UnknownError < Error
170
+ CODE = "unknown error"
171
+ end
172
+
173
+ # The requested command matched a known URL but did not match any method for that URL.
174
+ class UnknownMethodError < Error
175
+ CODE = "unknown method"
176
+ end
177
+
178
+ # Indicates that a command that should have executed properly cannot be supported for some reason.
179
+ class UnsupportedOperationError < Error
180
+ CODE = "unsupported operation"
181
+ end
182
+
183
+ ERROR_CODES = {
184
+ ElementClickInterceptedError::CODE => ElementClickInterceptedError,
185
+ ElementNotInteractableError::CODE => ElementNotInteractableError,
186
+ InsecureCertificateError::CODE => InsecureCertificateError,
187
+ InvalidArgumentError::CODE => InvalidArgumentError,
188
+ InvalidCookieDomainError::CODE => InvalidCookieDomainError,
189
+ InvalidElementStateError::CODE => InvalidElementStateError,
190
+ InvalidSelectorError::CODE => InvalidSelectorError,
191
+ InvalidSessionIdError::CODE => InvalidSessionIdError,
192
+ JavaScriptError::CODE => JavaScriptError,
193
+ MoveTargetOutOfBoundsError::CODE => MoveTargetOutOfBoundsError,
194
+ NoSuchAlertError::CODE => NoSuchAlertError,
195
+ NoSuchCookieError::CODE => NoSuchCookieError,
196
+ NoSuchElementError::CODE => NoSuchElementError,
197
+ NoSuchFrameError::CODE => NoSuchFrameError,
198
+ NoSuchWindowError::CODE => NoSuchWindowError,
199
+ NoSuchShadowRootError::CODE => NoSuchShadowRootError,
200
+ ScriptTimeoutError::CODE => ScriptTimeoutError,
201
+ SessionNotCreatedError::CODE => SessionNotCreatedError,
202
+ StaleElementReferenceError::CODE => StaleElementReferenceError,
203
+ DetachedShadowRootError::CODE => DetachedShadowRootError,
204
+ TimeoutError::CODE => TimeoutError,
205
+ UnableToSetCookieError::CODE => UnableToSetCookieError,
206
+ UnableToCaptureScreenError::CODE => UnableToCaptureScreenError,
207
+ UnexpectedAlertOpenError::CODE => UnexpectedAlertOpenError,
208
+ UnknownCommandError::CODE => UnknownCommandError,
209
+ UnknownError::CODE => UnknownError,
210
+ UnknownMethodError::CODE => UnknownMethodError,
211
+ UnsupportedOperationError::CODE => UnsupportedOperationError,
212
+ }
213
+ end
214
+ end
@@ -0,0 +1,127 @@
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
+ # A locator is used to find elements in the DOM.
11
+ #
12
+ # You can use the following convenience methods to create locators:
13
+ #
14
+ # ``` ruby
15
+ # Locator.css("main#content")
16
+ # Locator.xpath("//main[@id='content']")
17
+ # Locator.link_text("Home")
18
+ # Locator.partial_link_text("Ho")
19
+ # Locator.tag_name("main")
20
+ # ```
21
+ #
22
+ # You can also use the `Locator.wrap` method to create locators from a hash:
23
+ #
24
+ # ``` ruby
25
+ # Locator.wrap(css: "main#content")
26
+ # Locator.wrap(xpath: "//main[@id='content']")
27
+ # Locator.wrap(link_text: "Home")
28
+ # Locator.wrap(partial_link_text: "Ho")
29
+ # Locator.wrap(tag_name: "main")
30
+ # ```
31
+ #
32
+ # For more information, see: <https://w3c.github.io/webdriver/#locator-strategies>.
33
+ class Locator
34
+ # A convenience wrapper for specifying locators.
35
+ #
36
+ # You may provide either:
37
+ # 1. A locator instance, or
38
+ # 2. A single option `css:`, `xpath:`, `link_text:`, `partial_link_text:` or `tag_name:`, or
39
+ # 3. A `using:` and `value:` option which will be used directly.
40
+ #
41
+ # @parameter locator [Locator] A locator to use directly.
42
+ # @option css [String] A CSS selector.
43
+ # @option xpath [String] An XPath expression.
44
+ # @option link_text [String] The exact text of a link.
45
+ # @option partial_link_text [String] A partial match for the text of a link.
46
+ # @option tag_name [String] The name of a tag.
47
+ # @option using [String] The locator strategy to use.
48
+ # @option value [String] The value to use with the locator strategy.
49
+ def self.wrap(locator = nil, **options)
50
+ if locator.is_a?(Locator)
51
+ locator
52
+ elsif css = options[:css]
53
+ css(css)
54
+ elsif xpath = options[:xpath]
55
+ xpath(xpath)
56
+ elsif link_text = options[:link_text]
57
+ link_text(link_text)
58
+ elsif partial_link_text = options[:partial_link_text]
59
+ partial_link_text(partial_link_text)
60
+ elsif tag_name = options[:tag_name]
61
+ tag_name(tag_name)
62
+ elsif using = options[:using]
63
+ new(using, options[:value])
64
+ else
65
+ raise ArgumentError, "Unable to interpret #{locator.inspect} with #{options.inspect}!"
66
+ end
67
+ end
68
+
69
+ # A convenience wrapper for specifying CSS locators.
70
+ def self.css(css)
71
+ new("css selector", css)
72
+ end
73
+
74
+ # A convenience wrapper for specifying link text locators.
75
+ def self.link_text(text)
76
+ new("link text", text)
77
+ end
78
+
79
+ # A convenience wrapper for specifying partial link text locators.
80
+ def self.partial_link_text(text)
81
+ new("partial link text", text)
82
+ end
83
+
84
+ # A convenience wrapper for specifying tag name locators.
85
+ def self.tag_name(name)
86
+ new("tag name", name)
87
+ end
88
+
89
+ # A convenience wrapper for specifying XPath locators.
90
+ def self.xpath(xpath)
91
+ new("xpath", xpath)
92
+ end
93
+
94
+ # Initialize the locator.
95
+ #
96
+ # A locator strategy must usually be one of the following:
97
+ # - `css selector`: Used to find elements via CSS selectors.
98
+ # - `link text`: Used to find anchor elements by their link text.
99
+ # - `partial link text`: Used to find anchor elements by their partial link text.
100
+ # - `tag name`: Used to find elements by their tag name.
101
+ # - `xpath`: Used to find elements via XPath expressions.
102
+ #
103
+ # @parameter using [String] The locator strategy to use.
104
+ # @parameter value [String] The value to use with the locator strategy.
105
+ def initialize(using, value)
106
+ @using = using
107
+ @value = value
108
+ end
109
+
110
+ # @attribute [String] The locator strategy to use.
111
+ attr :using
112
+
113
+ # @attribute [String] The value to use with the locator strategy.
114
+ attr :value
115
+
116
+ # @returns [Hash] A JSON representation of the locator.
117
+ def as_json
118
+ {using: @using, value: @value}
119
+ end
120
+
121
+ # @returns [String] A JSON representation of the locator.
122
+ def to_json(...)
123
+ as_json.to_json(...)
124
+ end
125
+ end
126
+ end
127
+ end