appium_lib_core 5.0.0.beta1 → 5.0.0.rc1

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -6
  3. data/README.md +2 -1
  4. data/Rakefile +4 -0
  5. data/appium_lib_core.gemspec +3 -3
  6. data/ci-jobs/functional_test.yml +1 -1
  7. data/docs/mobile_command.md +3 -2
  8. data/lib/appium_lib_core/android/device/auth_finger_print.rb +2 -1
  9. data/lib/appium_lib_core/common/base/bridge.rb +303 -7
  10. data/lib/appium_lib_core/common/base/device_ime.rb +49 -0
  11. data/lib/appium_lib_core/common/base/driver.rb +101 -105
  12. data/lib/appium_lib_core/common/base/driver_settings.rb +51 -0
  13. data/lib/appium_lib_core/common/base/has_location.rb +11 -4
  14. data/lib/appium_lib_core/common/base/has_network_connection.rb +1 -1
  15. data/lib/appium_lib_core/common/base/rotable.rb +1 -1
  16. data/lib/appium_lib_core/common/base/screenshot.rb +4 -3
  17. data/lib/appium_lib_core/common/base/search_context.rb +9 -4
  18. data/lib/appium_lib_core/common/base.rb +0 -2
  19. data/lib/appium_lib_core/common/command.rb +259 -3
  20. data/lib/appium_lib_core/common/device/keyevent.rb +4 -4
  21. data/lib/appium_lib_core/common/device/value.rb +4 -4
  22. data/lib/appium_lib_core/common/error.rb +4 -1
  23. data/lib/appium_lib_core/common/log.rb +4 -1
  24. data/lib/appium_lib_core/common/touch_action/touch_actions.rb +4 -1
  25. data/lib/appium_lib_core/device.rb +1 -1
  26. data/lib/appium_lib_core/driver.rb +3 -4
  27. data/lib/appium_lib_core/{patch.rb → element.rb} +9 -14
  28. data/lib/appium_lib_core/ios/uiautomation/patch.rb +1 -1
  29. data/lib/appium_lib_core/version.rb +2 -2
  30. data/lib/appium_lib_core.rb +2 -5
  31. data/release_notes.md +23 -0
  32. data/script/commands.rb +1 -12
  33. metadata +15 -17
  34. data/lib/appium_lib_core/common/base/bridge/w3c.rb +0 -348
  35. data/lib/appium_lib_core/common/base/command.rb +0 -145
  36. data/lib/appium_lib_core/common/command/common.rb +0 -110
  37. data/lib/appium_lib_core/common/command/w3c.rb +0 -56
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fd462f64262abe99f03dd5a421f2a2dd9c8064f1d44eff47bc1872a834793d27
4
- data.tar.gz: 1a1ca492c3551d8c992e9f2b740460e67239887400602351423062e49912b73f
3
+ metadata.gz: fc513ad9f9ba5689303763b09a884af18e1a9eb80e78a6e05cf838551905f732
4
+ data.tar.gz: 59241d4d8626cf39eeb63a51291f18deca6759bdba95a8e2bb942173e0b5df9e
5
5
  SHA512:
6
- metadata.gz: d9e4977dfb93ce734c59af5053a6331bfc2038bd3a03757a31fd7e48c3cdcc810204fb6f3027a1120afe91eda31ad61aa504fa25fdf2a2a21d36638c27982f8e
7
- data.tar.gz: 3e23c561984a3ad4bf4214aad79fc730f8673d0bc29c81d26ff5143c0dbeb38d6eacc9278ba0caa11e4dfc51f82267f79b97efe6937ad8315c837e64876df162
6
+ metadata.gz: 867c6a7bfbdf95243fae3a6292f0349bf9bda1fbb83b44cb7d2e12e8f5e0ecb2f41fb6103aeebdcaaded1024cbb16cd2d758d73bef9bf00697325d75b5c2d027
7
+ data.tar.gz: 345771f473fca33668d101f00be374ebb3b345b6795c5392b9955da73bb45588e5589b70ba8c983ce161e35c311b2f18adfd7bac3a8d816ba93863820b05b4f6
data/CHANGELOG.md CHANGED
@@ -5,27 +5,34 @@ Read `release_notes.md` for commit level details.
5
5
  ## [Unreleased]
6
6
 
7
7
  ### Enhancements
8
+ - Allow to override an existing method by `Appium::Core::Base::Driver#add_command` since Appium drivers/plugins allow to override them
8
9
 
9
10
  ### Bug fixes
10
11
 
11
12
  ### Deprecations
12
13
 
13
- ## [5.0.0.beta1]
14
+ ## [5.0.0.beta]
14
15
 
15
16
  - Update base selenium webdriver version to `4.0.0.beta2`
16
17
  - Support only W3C spec
17
18
  - Support Ruby 2.5+
19
+ - Raises `::Appium::Core::Error::ArgumentError` instead of `ArgumentError` for this library specific argument errors
18
20
 
19
- ## [4.5.0] - 2021-03-14
21
+ ## [4.7.0] - 2021-07-17
20
22
 
21
23
  ### Enhancements
22
- - Add `speed` argument for `Appium::Core::Base::Driver#set_location` since Appium 1.21.0
23
- - Add `multiple` and `match_neighbour_threshold` arguments for `Appium::Core::Base::Driver#find_image_occurrence`
24
+ - Add `satellites` option in `Appium::Core::Base::Driver#set_location`
24
25
 
25
- ### Bug fixes
26
+ ## [4.6.0] - 2021-06-03
26
27
 
27
- ### Deprecations
28
+ ### Enhancements
29
+ - Add `Appium::Core::Base::Driver#add_command` to allow you to add your own command
28
30
 
31
+ ## [4.5.0] - 2021-03-14
32
+
33
+ ### Enhancements
34
+ - Add `speed` argument for `Appium::Core::Base::Driver#set_location` since Appium 1.21.0
35
+ - Add `multiple` and `match_neighbour_threshold` arguments for `Appium::Core::Base::Driver#find_image_occurrence`
29
36
 
30
37
  ## [4.4.1(4.4.0)] - 2021-02-15(2021-02-13)
31
38
 
data/README.md CHANGED
@@ -11,6 +11,7 @@ This library wraps [selenium-webdriver](https://github.com/SeleniumHQ/selenium/w
11
11
  # Documentation
12
12
 
13
13
  - http://www.rubydoc.info/github/appium/ruby_lib_core
14
+ - You can find working API examples in test code, [test/functional](test/functional)
14
15
 
15
16
  # Related library
16
17
  - https://github.com/appium/ruby_lib
@@ -120,7 +121,7 @@ $ IGNORE_VERSION_SKIP=true CI=true bundle exec rake test:func:android
120
121
  $ ruby test.rb
121
122
  ```
122
123
 
123
- More examples are in [test/functional](test/functional)
124
+ More examples are in [test/functional](test/functional)
124
125
 
125
126
  ### Capabilities
126
127
 
data/Rakefile CHANGED
@@ -38,6 +38,7 @@ namespace :test do
38
38
  namespace :unit do
39
39
  desc('Run all iOS related unit tests in test directory')
40
40
  Rake::TestTask.new(:ios) do |t|
41
+ ENV['UNIT_TEST'] = '1'
41
42
  t.libs << 'test'
42
43
  t.libs << 'lib'
43
44
  t.test_files = FileList['test/unit/ios/**/*_test.rb']
@@ -45,6 +46,7 @@ namespace :test do
45
46
 
46
47
  desc('Run all Android related unit tests in test directory')
47
48
  Rake::TestTask.new(:android) do |t|
49
+ ENV['UNIT_TEST'] = '1'
48
50
  t.libs << 'test'
49
51
  t.libs << 'lib'
50
52
  t.test_files = FileList['test/unit/android/**/*_test.rb']
@@ -52,6 +54,7 @@ namespace :test do
52
54
 
53
55
  desc('Run all common related unit tests in test directory')
54
56
  Rake::TestTask.new(:common) do |t|
57
+ ENV['UNIT_TEST'] = '1'
55
58
  t.libs << 'test'
56
59
  t.libs << 'lib'
57
60
  t.test_files = FileList['test/unit/common/**/*_test.rb']
@@ -59,6 +62,7 @@ namespace :test do
59
62
 
60
63
  desc('Run all Windows related unit tests in test directory')
61
64
  Rake::TestTask.new(:windows) do |t|
65
+ ENV['UNIT_TEST'] = '1'
62
66
  t.libs << 'test'
63
67
  t.libs << 'lib'
64
68
  t.test_files = FileList['test/unit/windows/**/*_test.rb']
@@ -22,7 +22,7 @@ Gem::Specification.new do |spec|
22
22
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
23
  spec.require_paths = ['lib']
24
24
 
25
- spec.add_runtime_dependency 'selenium-webdriver', '4.0.0.beta2'
25
+ spec.add_runtime_dependency 'selenium-webdriver', '4.0.0.rc1'
26
26
  spec.add_runtime_dependency 'faye-websocket', '~> 0.11.0'
27
27
 
28
28
  spec.add_development_dependency 'bundler', '>= 1.14'
@@ -30,8 +30,8 @@ Gem::Specification.new do |spec|
30
30
  spec.add_development_dependency 'yard', '~> 0.9.11'
31
31
  spec.add_development_dependency 'minitest', '~> 5.0'
32
32
  spec.add_development_dependency 'minitest-reporters', '~> 1.1'
33
- spec.add_development_dependency 'webmock', '~> 3.12.1'
34
- spec.add_development_dependency 'rubocop', '1.11.0'
33
+ spec.add_development_dependency 'webmock', '~> 3.14.0'
34
+ spec.add_development_dependency 'rubocop', '1.12.1'
35
35
  spec.add_development_dependency 'appium_thor', '~> 1.0'
36
36
  spec.add_development_dependency 'pry'
37
37
  spec.add_development_dependency 'pry-byebug'
@@ -103,7 +103,7 @@ jobs:
103
103
  - template: ./functional/ios_setup.yml
104
104
  parameters:
105
105
  xcodeVersion: ${{ parameters.xcodeForIOS }}
106
- - script: brew install ffmpeg && brew tap wix/brew && brew install wix/brew/applesimutils
106
+ - script: brew install ffmpeg && brew tap wix/brew && brew install applesimutils
107
107
  displayName: Install ffmpeg and applesimutils
108
108
  - template: ./functional/run_appium.yml
109
109
  - script: bundle exec rake test:func:ios TESTS=test/functional/ios/ios/mjpeg_server_test.rb,test/functional/ios/ios/mobile_commands_test.rb
@@ -9,7 +9,7 @@ We can invoke them via `execute_script` command with `mobile:` arguments.
9
9
  - https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/android
10
10
  - iOS:
11
11
  - https://github.com/appium/appium/tree/master/docs/en/writing-running-appium/ios
12
-
12
+
13
13
 
14
14
  ```ruby
15
15
  # Android shell : https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/android/android-shell.md
@@ -28,7 +28,8 @@ To handle it, we would recommend you to handle the error based on the error mess
28
28
 
29
29
  ```ruby
30
30
  error = assert_raises ::Selenium::WebDriver::Error::UnknownError do
31
- @driver.execute_script 'mobile: scrollToPage', { element: el.ref, scrollToPage: -100 }
31
+ _, element_id = el.ref
32
+ @driver.execute_script 'mobile: scrollToPage', { element: element_id, scrollToPage: -100 }
32
33
  end
33
34
  assert error.message.include? 'be a non-negative integer'
34
35
  ```
@@ -21,7 +21,8 @@ module Appium
21
21
  ::Appium::Core::Device.add_endpoint_method(:finger_print) do
22
22
  def finger_print(finger_id)
23
23
  unless (1..10).cover? finger_id.to_i
24
- raise ArgumentError, "finger_id should be integer between 1 to 10. Not #{finger_id}"
24
+ raise ::Appium::Core::Error::ArgumentError,
25
+ "finger_id should be integer between 1 to 10. Not #{finger_id}"
25
26
  end
26
27
 
27
28
  execute(:finger_print, {}, { fingerprintId: finger_id.to_i })
@@ -16,11 +16,40 @@ module Appium
16
16
  module Core
17
17
  class Base
18
18
  class Bridge < ::Selenium::WebDriver::Remote::Bridge
19
- # TODO: Move W3C module to here
19
+ include Device::DeviceLock
20
+ include Device::Keyboard
21
+ include Device::ImeActions
22
+ include Device::Setting
23
+ include Device::Context
24
+ include Device::Value
25
+ include Device::FileManagement
26
+ include Device::KeyEvent
27
+ include Device::ImageComparison
28
+ include Device::AppManagement
29
+ include Device::AppState
30
+ include Device::ScreenRecord::Command
31
+ include Device::Device
32
+ include Device::TouchActions
33
+ include Device::ExecuteDriver
34
+ include Device::Orientation
20
35
 
21
36
  # Prefix for extra capability defined by W3C
22
37
  APPIUM_PREFIX = 'appium:'
23
38
 
39
+ # No 'browserName' means the session is native appium connection
40
+ APPIUM_NATIVE_BROWSER_NAME = 'appium'
41
+
42
+ attr_reader :available_commands
43
+
44
+ def browser
45
+ @browser ||= begin
46
+ name = @capabilities.browser_name
47
+ name ? name.tr(' ', '_').downcase.to_sym : 'unknown'
48
+ rescue KeyError
49
+ APPIUM_NATIVE_BROWSER_NAME
50
+ end
51
+ end
52
+
24
53
  # Override
25
54
  # Creates session handling both OSS and W3C dialects.
26
55
  #
@@ -45,16 +74,16 @@ module Appium
45
74
  # core = ::Appium::Core.for(caps)
46
75
  # driver = core.start_driver
47
76
  #
48
- def create_session(desired_capabilities)
49
- caps = add_appium_prefix(desired_capabilities)
77
+ def create_session(capabilities)
78
+ @available_commands = ::Appium::Core::Commands::COMMANDS.dup
79
+
80
+ caps = add_appium_prefix(capabilities)
50
81
  response = execute(:new_session, {}, { capabilities: { firstMatch: [caps] } })
51
82
 
52
83
  @session_id = response['sessionId']
53
- capabilities = response['capabilities']
54
-
55
84
  raise ::Selenium::WebDriver::Error::WebDriverError, 'no sessionId in returned payload' unless @session_id
56
85
 
57
- @capabilities = json_create(capabilities)
86
+ @capabilities = json_create(response['capabilities'])
58
87
  end
59
88
 
60
89
  # Append +appium:+ prefix for Appium following W3C spec
@@ -65,7 +94,7 @@ module Appium
65
94
  def add_appium_prefix(capabilities)
66
95
  w3c_capabilities = ::Selenium::WebDriver::Remote::Capabilities.new
67
96
 
68
- capabilities = capabilities.__send__(:capabilities) unless capabilities.is_a?(Hash)
97
+ capabilities = capabilities.send(:capabilities) unless capabilities.is_a?(Hash)
69
98
 
70
99
  capabilities.each do |name, value|
71
100
  next if value.nil?
@@ -99,6 +128,273 @@ module Appium
99
128
  def json_create(value)
100
129
  ::Selenium::WebDriver::Remote::Capabilities.json_create(value)
101
130
  end
131
+
132
+ public
133
+
134
+ # command for Appium 2.0.
135
+ def add_command(method:, url:, name:, &block)
136
+ ::Appium::Logger.info "Overriding the method '#{name}' for '#{url}'" if @available_commands.key? name
137
+
138
+ @available_commands[name] = [method, url]
139
+
140
+ ::Appium::Core::Device.add_endpoint_method name, &block
141
+ end
142
+
143
+ def commands(command)
144
+ @available_commands[command]
145
+ end
146
+
147
+ # Returns all available sessions on the Appium server instance
148
+ def sessions
149
+ execute :get_all_sessions
150
+ end
151
+
152
+ def status
153
+ execute :status
154
+ end
155
+
156
+ # Perform touch actions for W3C module.
157
+ # Generate +touch+ pointer action here and users can use this via +driver.action+
158
+ # - https://seleniumhq.github.io/selenium/docs/api/rb/Selenium/WebDriver/W3CActionBuilder.html
159
+ # - https://seleniumhq.github.io/selenium/docs/api/rb/Selenium/WebDriver/PointerActions.html
160
+ # - https://seleniumhq.github.io/selenium/docs/api/rb/Selenium/WebDriver/KeyActions.html
161
+ #
162
+ # 'mouse' action is by default in the Ruby client. Appium server force the +mouse+ action to +touch+ once in
163
+ # the server side. So we don't consider the case.
164
+ #
165
+ # @example
166
+ #
167
+ # element = @driver.find_element(:id, "some id")
168
+ # @driver.action.click(element).perform # The 'click' is a part of 'PointerActions'
169
+ #
170
+ def action(async = false)
171
+ # Used for default duration of each touch actions
172
+ # Override from 250 milliseconds to 50 milliseconds
173
+ action_builder = super
174
+ action_builder.default_move_duration = 0.05
175
+ action_builder
176
+ end
177
+
178
+ # Port from MJSONWP
179
+ def get_timeouts
180
+ execute :get_timeouts
181
+ end
182
+
183
+ # Port from MJSONWP
184
+ def session_capabilities
185
+ ::Selenium::WebDriver::Remote::Capabilities.json_create execute(:get_capabilities)
186
+ end
187
+
188
+ # Port from MJSONWP
189
+ def send_keys_to_active_element(key)
190
+ text = ::Selenium::WebDriver::Keys.encode(key).join('')
191
+ execute :send_keys_to_active_element, {}, { value: text.split(//) }
192
+ end
193
+
194
+ # For Appium
195
+ # override
196
+ def page_source
197
+ # For W3C
198
+ # execute_script('var source = document.documentElement.outerHTML;' \
199
+ # 'if (!source) { source = new XMLSerializer().serializeToString(document); }' \
200
+ # 'return source;')
201
+ execute :get_page_source
202
+ end
203
+
204
+ # For Appium
205
+ # override
206
+ def element_displayed?(element)
207
+ # For W3C
208
+ # https://github.com/SeleniumHQ/selenium/commit/b618499adcc3a9f667590652c5757c0caa703289
209
+ # execute_atom :isDisplayed, element
210
+ _, element_id = element.ref
211
+ execute :is_element_displayed, id: element_id
212
+ end
213
+
214
+ # For Appium
215
+ # override
216
+ def element_attribute(element, name)
217
+ # For W3C in Selenium Client
218
+ # execute_atom :getAttribute, element, name
219
+ _, element_id = element.ref
220
+ execute :get_element_attribute, id: element_id, name: name
221
+ end
222
+
223
+ # For Appium
224
+ # override
225
+ def active_element
226
+ ::Appium::Core::Element.new self, element_id_from(execute(:get_active_element))
227
+ end
228
+ alias switch_to_active_element active_element
229
+
230
+ # For Appium
231
+ # override
232
+ def find_element_by(how, what, parent_ref = [])
233
+ how, what = convert_locator(how, what)
234
+
235
+ return execute_atom(:findElements, Support::RelativeLocator.new(what).as_json).first if how == 'relative'
236
+
237
+ parent_type, parent_id = parent_ref
238
+ id = case parent_type
239
+ when :element
240
+ execute :find_child_element, { id: parent_id }, { using: how, value: what.to_s }
241
+ when :shadow_root
242
+ execute :find_shadow_child_element, { id: parent_id }, { using: how, value: what.to_s }
243
+ else
244
+ execute :find_element, {}, { using: how, value: what.to_s }
245
+ end
246
+
247
+ ::Appium::Core::Element.new self, element_id_from(id)
248
+ end
249
+
250
+ # For Appium
251
+ # override
252
+ def find_elements_by(how, what, parent_ref = [])
253
+ how, what = convert_locator(how, what)
254
+
255
+ return execute_atom :findElements, Support::RelativeLocator.new(what).as_json if how == 'relative'
256
+
257
+ parent_type, parent_id = parent_ref
258
+ ids = case parent_type
259
+ when :element
260
+ execute :find_child_elements, { id: parent_id }, { using: how, value: what.to_s }
261
+ when :shadow_root
262
+ execute :find_shadow_child_elements, { id: parent_id }, { using: how, value: what.to_s }
263
+ else
264
+ execute :find_elements, {}, { using: how, value: what.to_s }
265
+ end
266
+
267
+ ids.map { |id| ::Appium::Core::Element.new self, element_id_from(id) }
268
+ end
269
+
270
+ # For Appium
271
+ # @param [Hash] id The id which can get as a response from server
272
+ # @return [::Appium::Core::Element]
273
+ def convert_to_element(id)
274
+ ::Appium::Core::Element.new self, element_id_from(id)
275
+ end
276
+
277
+ # For Appium
278
+ # override
279
+ # called in 'extend DriverExtensions::HasNetworkConnection'
280
+ def network_connection
281
+ execute :get_network_connection
282
+ end
283
+
284
+ # For Appium
285
+ # override
286
+ # called in 'extend DriverExtensions::HasNetworkConnection'
287
+ def network_connection=(type)
288
+ execute :set_network_connection, {}, { parameters: { type: type } }
289
+ end
290
+
291
+ # For Appium
292
+ # No implementation for W3C webdriver module
293
+ # called in 'extend DriverExtensions::HasLocation'
294
+ def location
295
+ obj = execute(:get_location) || {}
296
+ ::Selenium::WebDriver::Location.new obj['latitude'], obj['longitude'], obj['altitude']
297
+ end
298
+
299
+ # For Appium
300
+ # No implementation for W3C webdriver module
301
+ def set_location(lat, lon, alt = 0.0, speed: nil, satellites: nil)
302
+ loc = { latitude: lat, longitude: lon, altitude: alt }
303
+ loc[:speed] = speed unless speed.nil?
304
+ loc[:satellites] = satellites unless satellites.nil?
305
+ execute :set_location, {}, { location: loc }
306
+ end
307
+
308
+ #
309
+ # logs
310
+ #
311
+ # For Appium
312
+ # No implementation for W3C webdriver module
313
+ def available_log_types
314
+ types = execute :get_available_log_types
315
+ Array(types).map(&:to_sym)
316
+ end
317
+
318
+ # For Appium
319
+ # No implementation for W3C webdriver module
320
+ def log(type)
321
+ data = execute :get_log, {}, { type: type.to_s }
322
+
323
+ Array(data).map do |l|
324
+ begin
325
+ ::Selenium::WebDriver::LogEntry.new l.fetch('level', 'UNKNOWN'), l.fetch('timestamp'), l.fetch('message')
326
+ rescue KeyError
327
+ next
328
+ end
329
+ end
330
+ end
331
+
332
+ # For Appium
333
+ def log_event(vendor, event)
334
+ execute :post_log_event, {}, { vendor: vendor, event: event }
335
+ end
336
+
337
+ # For Appium
338
+ def log_events(type = nil)
339
+ args = {}
340
+ args['type'] = type unless type.nil?
341
+
342
+ execute :get_log_events, {}, args
343
+ end
344
+
345
+ def viewport_screenshot
346
+ execute_script('mobile: viewportScreenshot')
347
+ end
348
+
349
+ def element_screenshot(element_id)
350
+ execute :take_element_screenshot, id: element_id
351
+ end
352
+
353
+ private
354
+
355
+ def unwrap_script_result(arg)
356
+ case arg
357
+ when Array
358
+ arg.map { |e| unwrap_script_result(e) }
359
+ when Hash
360
+ element_id = element_id_from(arg)
361
+ return ::Appium::Core::Element.new(self, element_id) if element_id
362
+
363
+ arg.each { |k, v| arg[k] = unwrap_script_result(v) }
364
+ else
365
+ arg
366
+ end
367
+ end
368
+
369
+ def element_id_from(id)
370
+ id['ELEMENT'] || id['element-6066-11e4-a52e-4f735466cecf']
371
+ end
372
+
373
+ # Don't convert locators for Appium in native context
374
+ def convert_locator(how, what)
375
+ # case how
376
+ # when 'class name'
377
+ # how = 'css selector'
378
+ # what = ".#{escape_css(what)}"
379
+ # when 'id'
380
+ # how = 'css selector'
381
+ # what = "##{escape_css(what)}"
382
+ # when 'name'
383
+ # how = 'css selector'
384
+ # what = "*[name='#{escape_css(what)}']"
385
+ # when 'tag name'
386
+ # how = 'css selector'
387
+ # end
388
+ #
389
+ # if what.is_a?(Hash)
390
+ # what = what.each_with_object({}) do |(h, w), hash|
391
+ # h, w = convert_locator(h.to_s, w)
392
+ # hash[h] = w
393
+ # end
394
+ # end
395
+
396
+ [how, what]
397
+ end
102
398
  end # class Bridge
103
399
  end # class Base
104
400
  end # module Core