testa_appium_driver 0.1.11 → 0.1.14

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/.gitattributes +23 -0
  3. data/.gitignore +16 -16
  4. data/.rspec +3 -3
  5. data/.rubocop.yml +13 -13
  6. data/CHANGELOG.md +5 -5
  7. data/CODE_OF_CONDUCT.md +102 -102
  8. data/Gemfile +12 -12
  9. data/Gemfile.lock +74 -0
  10. data/LICENSE.txt +21 -21
  11. data/README.md +402 -378
  12. data/Rakefile +12 -12
  13. data/{testa_appium_driver.iml → appium-driver.iml} +71 -78
  14. data/bin/console +17 -17
  15. data/bin/setup +8 -8
  16. data/lib/testa_appium_driver/android/class_selectors.rb +437 -437
  17. data/lib/testa_appium_driver/android/driver.rb +70 -69
  18. data/lib/testa_appium_driver/android/locator/attributes.rb +117 -113
  19. data/lib/testa_appium_driver/android/locator.rb +141 -141
  20. data/lib/testa_appium_driver/android/scroll_actions/uiautomator_scroll_actions.rb +61 -61
  21. data/lib/testa_appium_driver/android/selenium_element.rb +11 -7
  22. data/lib/testa_appium_driver/common/bounds.rb +149 -149
  23. data/lib/testa_appium_driver/common/constants.rb +37 -36
  24. data/lib/testa_appium_driver/common/exceptions/strategy_mix_exception.rb +11 -11
  25. data/lib/testa_appium_driver/common/helpers.rb +270 -270
  26. data/lib/testa_appium_driver/common/locator/scroll_actions.rb +397 -397
  27. data/lib/testa_appium_driver/common/locator.rb +627 -610
  28. data/lib/testa_appium_driver/common/scroll_actions/json_wire_scroll_actions.rb +3 -3
  29. data/lib/testa_appium_driver/common/scroll_actions/w3c_scroll_actions.rb +304 -237
  30. data/lib/testa_appium_driver/common/scroll_actions.rb +253 -246
  31. data/lib/testa_appium_driver/common/selenium_element.rb +19 -19
  32. data/lib/testa_appium_driver/driver.rb +328 -312
  33. data/lib/testa_appium_driver/ios/driver.rb +48 -48
  34. data/lib/testa_appium_driver/ios/locator/attributes.rb +84 -80
  35. data/lib/testa_appium_driver/ios/locator.rb +71 -70
  36. data/lib/testa_appium_driver/ios/selenium_element.rb +6 -6
  37. data/lib/testa_appium_driver/ios/type_selectors.rb +187 -187
  38. data/lib/testa_appium_driver/version.rb +5 -5
  39. data/lib/testa_appium_driver.rb +6 -6
  40. data/testa_appium_driver.gemspec +41 -41
  41. metadata +9 -16
  42. data/.idea/deployment.xml +0 -22
  43. data/.idea/inspectionProfiles/Project_Default.xml +0 -9
  44. data/.idea/misc.xml +0 -6
  45. data/.idea/modules.xml +0 -8
  46. data/.idea/runConfigurations/Android_Test.xml +0 -42
  47. data/.idea/runConfigurations.xml +0 -10
  48. data/.idea/sshConfigs.xml +0 -13
  49. data/.idea/vcs.xml +0 -6
  50. data/.idea/webServers.xml +0 -21
@@ -1,247 +1,254 @@
1
- require_relative 'scroll_actions/json_wire_scroll_actions'
2
- require_relative 'scroll_actions/w3c_scroll_actions'
3
-
4
- module TestaAppiumDriver
5
-
6
- # Class for handling scroll actions
7
- class ScrollActions
8
- # @param [TestaAppiumDriver::Locator, nil] scrollable container that will be used to determine the bounds for scrolling
9
- # @param [Hash] params
10
- # acceptable params
11
- # - locator - element that should be found with scrolling actions
12
- # - deadzone - [Hash] that stores top, bottom, left and right deadzone values. If deadzone[:top] is 200 then 200px from top of the scrollable container will not be used for scrolling
13
- # - max_scrolls - [Integer] maximum number of scrolls before exception is thrown
14
- # - default_scroll_strategy - defines which scroll strategy will be used if a scroll action is valid for multiple strategies
15
- def initialize(scrollable, params = {})
16
- @scrollable = scrollable
17
- @locator = params[:locator]
18
- @deadzone = params[:deadzone]
19
- @max_scrolls = params[:max_scrolls]
20
- @default_scroll_strategy = params[:default_scroll_strategy]
21
- @driver = @locator.driver
22
-
23
- if @scrollable.nil?
24
- # if we dont have a scrollable element or if we do have it, but it is not compatible with uiautomator
25
- # then find first scrollable in document
26
- @scrollable = @driver.scrollable
27
- end
28
-
29
- @strategy = nil
30
- if @scrollable.strategy == FIND_STRATEGY_XPATH || # uiautomator cannot resolve scrollable from a xpath locator
31
- !@deadzone.nil? ||
32
- !@scrollable.from_element.instance_of?(TestaAppiumDriver::Driver) # uiautomator cannot resolve nested scrollable
33
- @strategy = SCROLL_STRATEGY_W3C
34
- end
35
-
36
- @bounds = @scrollable.bounds
37
- end
38
-
39
- def align(with, scroll_to_find)
40
- w3c_align(with, scroll_to_find)
41
- @locator
42
- end
43
-
44
- # @return [Array]
45
- def each(&block)
46
- w3c_each(nil, &block)
47
- end
48
-
49
- def each_down(&block)
50
- w3c_each(:down, &block)
51
- end
52
-
53
- def each_up(&block)
54
- w3c_each(:up, &block)
55
- end
56
-
57
- def each_right(&block)
58
- w3c_each(:right, &block)
59
- end
60
-
61
- def each_left(&block)
62
- w3c_each(:left, &block)
63
- end
64
-
65
- def resolve_strategy
66
- if @strategy.nil?
67
- @default_scroll_strategy
68
- else
69
- @strategy
70
- end
71
- end
72
-
73
- def scroll_to
74
- if @locator.strategy != FIND_STRATEGY_XPATH && resolve_strategy == SCROLL_STRATEGY_UIAUTOMATOR
75
- uiautomator_scroll_to
76
- elsif resolve_strategy == SCROLL_STRATEGY_W3C
77
- w3c_scroll_to(nil)
78
- end
79
- end
80
-
81
- def scroll_down_to
82
- w3c_scroll_to(:down)
83
- end
84
-
85
- def scroll_up_to
86
- w3c_scroll_to(:up)
87
- end
88
-
89
- def scroll_right_to
90
- w3c_scroll_to(:right)
91
- end
92
-
93
- def scroll_left_to
94
- w3c_scroll_to(:left)
95
- end
96
-
97
- def page_next
98
- if @scrollable.scroll_orientation == :vertical
99
- page_down
100
- else
101
- page_left
102
- end
103
- end
104
-
105
- def page_back
106
- if @scrollable.scroll_orientation == :vertical
107
- page_up
108
- else
109
- page_right
110
- end
111
- end
112
-
113
- def page_down
114
- if resolve_strategy == SCROLL_STRATEGY_UIAUTOMATOR
115
- uiautomator_page_or_fling(SCROLL_ACTION_TYPE_SCROLL, :down)
116
- elsif resolve_strategy == SCROLL_STRATEGY_W3C
117
- w3c_page_or_fling(SCROLL_ACTION_TYPE_SCROLL, :down)
118
- end
119
- end
120
-
121
- def page_right
122
- if resolve_strategy == SCROLL_STRATEGY_UIAUTOMATOR
123
- uiautomator_page_or_fling(SCROLL_ACTION_TYPE_SCROLL, :right)
124
- elsif resolve_strategy == SCROLL_STRATEGY_W3C
125
- w3c_page_or_fling(SCROLL_ACTION_TYPE_SCROLL, :right)
126
- end
127
- end
128
-
129
- def page_up
130
- if resolve_strategy == SCROLL_STRATEGY_UIAUTOMATOR
131
- uiautomator_page_or_fling(SCROLL_ACTION_TYPE_SCROLL, :up)
132
- elsif resolve_strategy == SCROLL_STRATEGY_W3C
133
- w3c_page_or_fling(SCROLL_ACTION_TYPE_SCROLL, :up)
134
- end
135
- end
136
-
137
- def page_left
138
- if resolve_strategy == SCROLL_STRATEGY_UIAUTOMATOR
139
- uiautomator_page_or_fling(SCROLL_ACTION_TYPE_SCROLL, :left)
140
- elsif resolve_strategy == SCROLL_STRATEGY_W3C
141
- w3c_page_or_fling(SCROLL_ACTION_TYPE_SCROLL, :left)
142
- end
143
- end
144
-
145
- def scroll_to_start
146
- if resolve_strategy == SCROLL_STRATEGY_UIAUTOMATOR
147
- uiautomator_scroll_to_start_or_end(:start)
148
-
149
- elsif resolve_strategy == SCROLL_STRATEGY_W3C
150
- w3c_scroll_to_start_or_end(:start)
151
- end
152
- end
153
-
154
- def scroll_to_end
155
- if resolve_strategy == SCROLL_STRATEGY_UIAUTOMATOR
156
- uiautomator_scroll_to_start_or_end(:end)
157
- elsif resolve_strategy == SCROLL_STRATEGY_W3C
158
- w3c_scroll_to_start_or_end(:end)
159
- end
160
- end
161
-
162
- def fling_down
163
- if resolve_strategy == SCROLL_STRATEGY_UIAUTOMATOR
164
- uiautomator_page_or_fling(SCROLL_ACTION_TYPE_FLING, :down)
165
- elsif resolve_strategy == SCROLL_STRATEGY_W3C
166
- w3c_page_or_fling(SCROLL_ACTION_TYPE_FLING, :down)
167
- end
168
- end
169
-
170
- def fling_right
171
- if resolve_strategy == SCROLL_STRATEGY_UIAUTOMATOR
172
- uiautomator_page_or_fling(SCROLL_ACTION_TYPE_FLING, :right)
173
- elsif resolve_strategy == SCROLL_STRATEGY_W3C
174
- w3c_page_or_fling(SCROLL_ACTION_TYPE_FLING, :right)
175
- end
176
- end
177
-
178
- def fling_up
179
- if resolve_strategy == SCROLL_STRATEGY_UIAUTOMATOR
180
- uiautomator_page_or_fling(SCROLL_ACTION_TYPE_FLING, :up)
181
- elsif resolve_strategy == SCROLL_STRATEGY_W3C
182
- w3c_page_or_fling(SCROLL_ACTION_TYPE_FLING, :up)
183
- end
184
- end
185
-
186
- def fling_left
187
- if resolve_strategy == SCROLL_STRATEGY_UIAUTOMATOR
188
- uiautomator_page_or_fling(SCROLL_ACTION_TYPE_FLING, :left)
189
- elsif resolve_strategy == SCROLL_STRATEGY_W3C
190
- w3c_page_or_fling(SCROLL_ACTION_TYPE_FLING, :left)
191
- end
192
- end
193
-
194
- def drag_to(x0, y0, x1, y1)
195
- w3c_drag_to(x0, y0, x1, y1)
196
- end
197
-
198
- private
199
-
200
- def is_end_of_scroll?
201
- old_elements = @previous_elements
202
- @previous_elements = @scrollable.first_and_last_leaf
203
- old_elements == @previous_elements
204
- end
205
-
206
- def default_deadzone!
207
- @deadzone = {} if @deadzone.nil?
208
- if @deadzone[:top].nil?
209
- @deadzone[:top] = 1
210
- else
211
- @deadzone[:top] = @deadzone[:top].to_f
212
- end
213
- if @deadzone[:bottom].nil?
214
- @deadzone[:bottom] = 1
215
- else
216
- @deadzone[:bottom] = @deadzone[:bottom].to_f
217
- end
218
- if @deadzone[:right].nil?
219
- @deadzone[:right] = 1
220
- else
221
- @deadzone[:right] = @deadzone[:right].to_f
222
- end
223
- if @deadzone[:left].nil?
224
- @deadzone[:left] = 1
225
- else
226
- @deadzone[:left] = @deadzone[:left].to_f
227
- end
228
- end
229
-
230
- def is_aligned?(with, element)
231
- align_bounds = @locator.bounds(force_cache_element: element)
232
- case with
233
- when :top
234
- @align_offset = align_bounds.top_left.y - @bounds.top_left.y + @deadzone[:top]
235
- when :bottom
236
- @align_offset = @bounds.bottom_right.y - @deadzone[:bottom] - align_bounds.bottom_right.y
237
- when :right
238
- @align_offset = @bounds.bottom_right.x - @deadzone[:right] - align_bounds.bottom_right.x
239
- when :left
240
- @align_offset = align_bounds.top_left.x - @bounds.top_left.x + @deadzone[:left]
241
- else
242
- raise "Unsupported align with option: #{with}"
243
- end
244
- @align_offset < SCROLL_ALIGNMENT_THRESHOLD
245
- end
246
- end
1
+ require_relative 'scroll_actions/json_wire_scroll_actions'
2
+ require_relative 'scroll_actions/w3c_scroll_actions'
3
+
4
+ module TestaAppiumDriver
5
+
6
+ # Class for handling scroll actions
7
+ class ScrollActions
8
+ include W3cScrollActions
9
+ include JsonWireScrollActions
10
+
11
+ attr_accessor :locator
12
+ # @param [TestaAppiumDriver::Locator, nil] scrollable container that will be used to determine the bounds for scrolling
13
+ # @param [Hash] params
14
+ # acceptable params
15
+ # - locator - element that should be found with scrolling actions
16
+ # - deadzone - [Hash] that stores top, bottom, left and right deadzone values. If deadzone[:top] is 200 then 200px from top of the scrollable container will not be used for scrolling
17
+ # - max_scrolls - [Integer] maximum number of scrolls before exception is thrown
18
+ # - default_scroll_strategy - defines which scroll strategy will be used if a scroll action is valid for multiple strategies
19
+ def initialize(scrollable, params = {})
20
+ @scrollable = scrollable
21
+ @locator = params[:locator]
22
+ # TODO: raise error if locator is for multiple, but not for scroll each, chekc other cases aswell
23
+ @deadzone = params[:deadzone]
24
+ @max_scrolls = params[:max_scrolls]
25
+ @default_scroll_strategy = params[:default_scroll_strategy]
26
+ @driver = @locator.driver
27
+
28
+ if @scrollable.nil?
29
+ # if we dont have a scrollable element or if we do have it, but it is not compatible with uiautomator
30
+ # then find first scrollable in document
31
+ @scrollable = @driver.scrollable
32
+ end
33
+
34
+ @strategy = nil
35
+ if @scrollable.strategy == FIND_STRATEGY_XPATH || # uiautomator cannot resolve scrollable from a xpath locator
36
+ !@deadzone.nil? ||
37
+ !@scrollable.from_element.instance_of?(TestaAppiumDriver::Driver) # uiautomator cannot resolve nested scrollable
38
+ @strategy = SCROLL_STRATEGY_W3C
39
+ end
40
+
41
+ @bounds = @scrollable.bounds
42
+ end
43
+
44
+ def align(with, scroll_to_find, max_attempts)
45
+ w3c_align(with, scroll_to_find, max_attempts)
46
+ @locator
47
+ end
48
+
49
+ # @return [Array]
50
+ def scroll_each(&block)
51
+ w3c_scroll_each(nil, &block)
52
+ end
53
+
54
+ def scroll_each_down(&block)
55
+ w3c_scroll_each(:down, &block)
56
+ end
57
+
58
+ def scroll_each_up(&block)
59
+ w3c_scroll_each(:up, &block)
60
+ end
61
+
62
+ def scroll_each_right(&block)
63
+ w3c_scroll_each(:right, &block)
64
+ end
65
+
66
+ def scroll_each_left(&block)
67
+ w3c_scroll_each(:left, &block)
68
+ end
69
+
70
+ def resolve_strategy
71
+ if @strategy.nil?
72
+ @default_scroll_strategy
73
+ else
74
+ @strategy
75
+ end
76
+ end
77
+
78
+ def scroll_to
79
+ if @locator.strategy != FIND_STRATEGY_XPATH && resolve_strategy == SCROLL_STRATEGY_UIAUTOMATOR
80
+ uiautomator_scroll_to
81
+ elsif resolve_strategy == SCROLL_STRATEGY_W3C
82
+ w3c_scroll_to(nil)
83
+ end
84
+ end
85
+
86
+ def scroll_down_to
87
+ w3c_scroll_to(:down)
88
+ end
89
+
90
+ def scroll_up_to
91
+ w3c_scroll_to(:up)
92
+ end
93
+
94
+ def scroll_right_to
95
+ w3c_scroll_to(:right)
96
+ end
97
+
98
+ def scroll_left_to
99
+ w3c_scroll_to(:left)
100
+ end
101
+
102
+ def page_next
103
+ if @scrollable.scroll_orientation == :vertical
104
+ page_down
105
+ else
106
+ page_left
107
+ end
108
+ end
109
+
110
+ def page_back
111
+ if @scrollable.scroll_orientation == :vertical
112
+ page_up
113
+ else
114
+ page_right
115
+ end
116
+ end
117
+
118
+ def page_down
119
+ if resolve_strategy == SCROLL_STRATEGY_UIAUTOMATOR
120
+ uiautomator_page_or_fling(SCROLL_ACTION_TYPE_SCROLL, :down)
121
+ elsif resolve_strategy == SCROLL_STRATEGY_W3C
122
+ w3c_page_or_fling(SCROLL_ACTION_TYPE_SCROLL, :down)
123
+ end
124
+ end
125
+
126
+ def page_right
127
+ if resolve_strategy == SCROLL_STRATEGY_UIAUTOMATOR
128
+ uiautomator_page_or_fling(SCROLL_ACTION_TYPE_SCROLL, :right)
129
+ elsif resolve_strategy == SCROLL_STRATEGY_W3C
130
+ w3c_page_or_fling(SCROLL_ACTION_TYPE_SCROLL, :right)
131
+ end
132
+ end
133
+
134
+ def page_up
135
+ if resolve_strategy == SCROLL_STRATEGY_UIAUTOMATOR
136
+ uiautomator_page_or_fling(SCROLL_ACTION_TYPE_SCROLL, :up)
137
+ elsif resolve_strategy == SCROLL_STRATEGY_W3C
138
+ w3c_page_or_fling(SCROLL_ACTION_TYPE_SCROLL, :up)
139
+ end
140
+ end
141
+
142
+ def page_left
143
+ if resolve_strategy == SCROLL_STRATEGY_UIAUTOMATOR
144
+ uiautomator_page_or_fling(SCROLL_ACTION_TYPE_SCROLL, :left)
145
+ elsif resolve_strategy == SCROLL_STRATEGY_W3C
146
+ w3c_page_or_fling(SCROLL_ACTION_TYPE_SCROLL, :left)
147
+ end
148
+ end
149
+
150
+ def scroll_to_start
151
+ if resolve_strategy == SCROLL_STRATEGY_UIAUTOMATOR
152
+ uiautomator_scroll_to_start_or_end(:start)
153
+
154
+ elsif resolve_strategy == SCROLL_STRATEGY_W3C
155
+ w3c_scroll_to_start_or_end(:start)
156
+ end
157
+ end
158
+
159
+ def scroll_to_end
160
+ if resolve_strategy == SCROLL_STRATEGY_UIAUTOMATOR
161
+ uiautomator_scroll_to_start_or_end(:end)
162
+ elsif resolve_strategy == SCROLL_STRATEGY_W3C
163
+ w3c_scroll_to_start_or_end(:end)
164
+ end
165
+ end
166
+
167
+ def fling_down
168
+ if resolve_strategy == SCROLL_STRATEGY_UIAUTOMATOR
169
+ uiautomator_page_or_fling(SCROLL_ACTION_TYPE_FLING, :down)
170
+ elsif resolve_strategy == SCROLL_STRATEGY_W3C
171
+ w3c_page_or_fling(SCROLL_ACTION_TYPE_FLING, :down)
172
+ end
173
+ end
174
+
175
+ def fling_right
176
+ if resolve_strategy == SCROLL_STRATEGY_UIAUTOMATOR
177
+ uiautomator_page_or_fling(SCROLL_ACTION_TYPE_FLING, :right)
178
+ elsif resolve_strategy == SCROLL_STRATEGY_W3C
179
+ w3c_page_or_fling(SCROLL_ACTION_TYPE_FLING, :right)
180
+ end
181
+ end
182
+
183
+ def fling_up
184
+ if resolve_strategy == SCROLL_STRATEGY_UIAUTOMATOR
185
+ uiautomator_page_or_fling(SCROLL_ACTION_TYPE_FLING, :up)
186
+ elsif resolve_strategy == SCROLL_STRATEGY_W3C
187
+ w3c_page_or_fling(SCROLL_ACTION_TYPE_FLING, :up)
188
+ end
189
+ end
190
+
191
+ def fling_left
192
+ if resolve_strategy == SCROLL_STRATEGY_UIAUTOMATOR
193
+ uiautomator_page_or_fling(SCROLL_ACTION_TYPE_FLING, :left)
194
+ elsif resolve_strategy == SCROLL_STRATEGY_W3C
195
+ w3c_page_or_fling(SCROLL_ACTION_TYPE_FLING, :left)
196
+ end
197
+ end
198
+
199
+ def drag_to(x0, y0, x1, y1)
200
+ w3c_drag_to(x0, y0, x1, y1)
201
+ end
202
+
203
+
204
+ def is_aligned?(with, element)
205
+ align_bounds = @locator.bounds(force_cache_element: element)
206
+ case with
207
+ when :top
208
+ @align_offset = align_bounds.top_left.y - @bounds.top_left.y - @deadzone[:top]
209
+ when :bottom
210
+ @align_offset = @bounds.bottom_right.y - @deadzone[:bottom] - align_bounds.bottom_right.y
211
+ when :right
212
+ @align_offset = @bounds.bottom_right.x - @deadzone[:right] - align_bounds.bottom_right.x
213
+ when :left
214
+ @align_offset = align_bounds.top_left.x - @bounds.top_left.x - @deadzone[:left]
215
+ else
216
+ raise "Unsupported align with option: #{with}"
217
+ end
218
+ @align_offset < SCROLL_ALIGNMENT_THRESHOLD
219
+ end
220
+
221
+
222
+
223
+ def is_end_of_scroll?
224
+ old_elements = @previous_elements
225
+ @previous_elements = @scrollable.first_and_last_leaf
226
+ old_elements == @previous_elements
227
+ end
228
+
229
+ def default_deadzone!
230
+ @deadzone = {} if @deadzone.nil?
231
+ if @deadzone[:top].nil?
232
+ @deadzone[:top] = 1
233
+ else
234
+ @deadzone[:top] = @deadzone[:top].to_f
235
+ end
236
+ if @deadzone[:bottom].nil?
237
+ @deadzone[:bottom] = 1
238
+ else
239
+ @deadzone[:bottom] = @deadzone[:bottom].to_f
240
+ end
241
+ if @deadzone[:right].nil?
242
+ @deadzone[:right] = 1
243
+ else
244
+ @deadzone[:right] = @deadzone[:right].to_f
245
+ end
246
+ if @deadzone[:left].nil?
247
+ @deadzone[:left] = 1
248
+ else
249
+ @deadzone[:left] = @deadzone[:left].to_f
250
+ end
251
+ end
252
+
253
+ end
247
254
  end
@@ -1,19 +1,19 @@
1
- module Selenium
2
- module WebDriver
3
- class Element
4
- # sets the testa appium driver instance for the current phone
5
- def self.set_driver(driver, udid)
6
- udid = "unknown" if udid.nil?
7
- @@drivers ||= {}
8
- @@drivers[udid] = driver
9
- end
10
-
11
- # @return [TestaAppiumDriver::Driver] testa appium driver instance for the current phone
12
- def get_driver
13
- udid = @bridge.capabilities.instance_variable_get(:@capabilities)["udid"]
14
- udid = "unknown" if udid.nil?
15
- @@drivers[udid]
16
- end
17
- end
18
- end
19
- end
1
+ module ::Appium
2
+ module Core
3
+ class Element
4
+ # sets the testa appium driver instance for the current phone
5
+ def self.set_driver(driver, udid)
6
+ udid = "unknown" if udid.nil?
7
+ @@drivers ||= {}
8
+ @@drivers[udid] = driver
9
+ end
10
+
11
+ # @return [TestaAppiumDriver::Driver] testa appium driver instance for the current phone
12
+ def get_driver
13
+ udid = @bridge.capabilities.instance_variable_get(:@capabilities)["udid"]
14
+ udid = "unknown" if udid.nil?
15
+ @@drivers[udid]
16
+ end
17
+ end
18
+ end
19
+ end