briar 0.0.4

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 (57) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +29 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +36 -0
  5. data/README.md +25 -0
  6. data/Rakefile +12 -0
  7. data/briar.gemspec +26 -0
  8. data/dotirbc_briar_additions +31 -0
  9. data/features/step_definitions/alerts_and_sheets/action_sheet_steps.rb +8 -0
  10. data/features/step_definitions/alerts_and_sheets/alert_view_steps.rb +25 -0
  11. data/features/step_definitions/bars/navbar_steps.rb +95 -0
  12. data/features/step_definitions/bars/tabbar_steps.rb +32 -0
  13. data/features/step_definitions/bars/toolbar_steps.rb +17 -0
  14. data/features/step_definitions/briar_core_steps.rb +9 -0
  15. data/features/step_definitions/control/button_steps.rb +57 -0
  16. data/features/step_definitions/control/segmented_control_steps.rb +85 -0
  17. data/features/step_definitions/control/slider_steps.rb +9 -0
  18. data/features/step_definitions/email_steps.rb +59 -0
  19. data/features/step_definitions/image_view_steps.rb +9 -0
  20. data/features/step_definitions/keyboard_steps.rb +21 -0
  21. data/features/step_definitions/label_steps.rb +21 -0
  22. data/features/step_definitions/picker/date_picker_steps.rb +216 -0
  23. data/features/step_definitions/picker/picker_steps.rb +58 -0
  24. data/features/step_definitions/scroll_view_steps.rb +20 -0
  25. data/features/step_definitions/table_steps.rb +186 -0
  26. data/features/step_definitions/text_field_steps.rb +37 -0
  27. data/features/step_definitions/text_view_steps.rb +44 -0
  28. data/features/support/env.rb +0 -0
  29. data/features/support/hooks.rb +0 -0
  30. data/features/support/launch.rb +0 -0
  31. data/lib/briar.rb +72 -0
  32. data/lib/briar/alerts_and_sheets/alert_view.rb +16 -0
  33. data/lib/briar/bars/navbar.rb +104 -0
  34. data/lib/briar/bars/tabbar.rb +38 -0
  35. data/lib/briar/bars/toolbar.rb +36 -0
  36. data/lib/briar/briar_core.rb +56 -0
  37. data/lib/briar/briar_steps.rb +26 -0
  38. data/lib/briar/control/button.rb +47 -0
  39. data/lib/briar/control/segmented_control.rb +22 -0
  40. data/lib/briar/control/slider.rb +34 -0
  41. data/lib/briar/cucumber.rb +49 -0
  42. data/lib/briar/email.rb +58 -0
  43. data/lib/briar/gestalt.rb +80 -0
  44. data/lib/briar/image_view.rb +27 -0
  45. data/lib/briar/keyboard.rb +104 -0
  46. data/lib/briar/label.rb +34 -0
  47. data/lib/briar/picker/date_picker.rb +598 -0
  48. data/lib/briar/picker/picker.rb +32 -0
  49. data/lib/briar/picker/picker_shared.rb +34 -0
  50. data/lib/briar/scroll_view.rb +10 -0
  51. data/lib/briar/table.rb +237 -0
  52. data/lib/briar/text_field.rb +57 -0
  53. data/lib/briar/text_view.rb +21 -0
  54. data/lib/briar/version.rb +3 -0
  55. data/spec/briar/gestalt_spec.rb +62 -0
  56. data/spec/spec_helper.rb +17 -0
  57. metadata +164 -0
@@ -0,0 +1,20 @@
1
+ #include Briar::ScrollView
2
+ #include Briar::Core
3
+
4
+ Then /^I scroll (left|right|up|down) until I see "([^\"]*)" limit (\d+)$/ do |dir, name, limit|
5
+ unless exists?(name)
6
+ count = 0
7
+ begin
8
+ scroll("scrollView index:0", dir)
9
+ step_pause
10
+ count = count + 1
11
+ end while ((not exists?(name)) and count < limit.to_i)
12
+ end
13
+ unless exists?(name)
14
+ screenshot_and_raise "i scrolled '#{dir}' '#{limit}' times but did not see #{name}"
15
+ end
16
+ end
17
+
18
+ Then /^I scroll "([^"]*)" (left|right|up|down)"$/ do |name, dir|
19
+ swipe(dir, {:query => "view marked:'#{name}'"})
20
+ end
@@ -0,0 +1,186 @@
1
+ #include Briar::Table
2
+ #include Briar::Core
3
+
4
+ Then /^I should see (?:the|an?) "([^"]*)" row$/ do |name|
5
+ should_see_row name
6
+ end
7
+
8
+ Then /^I scroll (left|right|up|down) until I see the "([^\"]*)" row limit (\d+)$/ do |dir, row_name, limit|
9
+ scroll_until_i_see_row dir, row_name, limit
10
+ end
11
+
12
+
13
+ Then /^I touch (?:the) "([^"]*)" row and wait for (?:the) "([^"]*)" view to appear$/ do |row_id, view_id|
14
+ # problem
15
+ wait_for_animation
16
+ touch_row_and_wait_to_see row_id, view_id
17
+ end
18
+
19
+ Then /^I touch the "([^"]*)" row on the "([^"]*)" table and wait for the "([^"]*)" view to appear$/ do |row_id, table_id, view_id|
20
+ wait_for_animation
21
+ touch_row_and_wait_to_see row_id, view_id, table_id
22
+ end
23
+
24
+
25
+ Then /^I touch (?:the) "([^"]*)" row$/ do |row_name|
26
+ touch_row row_name
27
+ end
28
+
29
+
30
+ Then /^I touch the "([^"]*)" section header$/ do |header_name|
31
+ touch("tableView child view marked:'#{header_name}'")
32
+ wait_for_animation
33
+ end
34
+
35
+ Then /^I should see the "([^"]*)" table in edit mode$/ do |table_id|
36
+ unless query("tableView marked:'#{table_id}'", :isEditing).first.to_i == 1
37
+ screenshot_and_raise "expected to see table '#{table_id}' in edit mode"
38
+ end
39
+ end
40
+
41
+
42
+ Then /^the (first|second) row should be "([^"]*)"$/ do |idx, row_id|
43
+ (idx.eql? "first") ? index = 0 : index = 1
44
+ res = query("tableViewCell", :accessibilityIdentifier)[index]
45
+ unless res.eql? row_id
46
+ screenshot_and_raise "i expected the #{idx} row would be #{row_id}, but found #{res}"
47
+ end
48
+ end
49
+
50
+
51
+ Then /^I swipe (left|right) on the "([^"]*)" row$/ do |dir, row_name|
52
+ swipe_on_row dir, row_name
53
+ end
54
+
55
+
56
+ Then /^I should be able to swipe to delete the "([^"]*)" row$/ do |row_name|
57
+ swipe_on_row "left", row_name
58
+ should_see_delete_confirmation_in_row row_name
59
+ touch_delete_confirmation row_name
60
+ should_not_see_row row_name
61
+ end
62
+
63
+ Then /^I touch the delete button on the "([^"]*)" row$/ do |row_name|
64
+ should_see_edit_mode_delete_button row_name
65
+ touch("tableViewCell marked:'#{row_name}' child tableViewCellEditControl")
66
+ step_pause
67
+ should_see_delete_confirmation_in_row row_name
68
+ end
69
+
70
+ Then /^I use the edit mode delete button to delete the "([^"]*)" row$/ do |row_name|
71
+ macro %Q|I touch the delete button on the "#{row_name}" row|
72
+ touch_delete_confirmation row_name
73
+ should_not_see_row row_name
74
+ end
75
+
76
+ Then /^I should see "([^"]*)" in row (\d+)$/ do |cell_name, row|
77
+ # on ios 6 this is returning nil
78
+ #res = query("tableViewCell index:#{row}", :accessibilityIdentifier).first
79
+ access_ids = query("tableViewCell", :accessibilityIdentifier)
80
+ unless access_ids.index(cell_name) == row.to_i
81
+ screenshot_and_raise "expected to see '#{cell_name}' in row #{row} but found '#{access_ids[row.to_i]}'"
82
+ end
83
+ end
84
+
85
+ Then /^I should see the rows in this order "([^"]*)"$/ do |row_ids|
86
+ tokens = row_ids.split(",")
87
+ counter = 0
88
+ tokens.each do |token|
89
+ token.strip!
90
+ macro %Q|I should see "#{token}" in row #{counter}|
91
+ counter = counter + 1
92
+ end
93
+ end
94
+
95
+ Then /^I should see that the "([^"]*)" row has text "([^"]*)" in the "([^"]*)" label$/ do |row_id, text, label_id|
96
+ should_see_row_with_label_with_text row_id, label_id, text
97
+ end
98
+
99
+ Then /^I should see that the text I just entered is in the "([^"]*)" row "([^"]*)" label$/ do |row_id, label_id|
100
+ should_see_row_with_label_with_text row_id, label_id, @text_entered_by_keyboard
101
+ end
102
+
103
+ Then /^I move the "([^"]*)" row (up|down) (\d+) times? using the reorder edit control$/ do |row_id, dir, n|
104
+ should_see_row row_id
105
+ dir_str = (dir.eql?("up")) ? "drag_row_up" : "drag_row_down"
106
+ n.to_i.times do (
107
+ playback(dir_str,
108
+ {:query => "tableViewCell marked:'#{row_id}' descendant tableViewCellReorderControl"})
109
+ step_pause)
110
+ end
111
+ end
112
+
113
+ Then /^I should (see|not see) (?:the|an?) "([^"]*)" table$/ do |visibility, table_id|
114
+ if visibility.eql?("see")
115
+ should_see_table table_id
116
+ else
117
+ should_not_see_table table_id
118
+ end
119
+ end
120
+
121
+ Then /^I should see a "([^"]*)" button in the "([^"]*)" row$/ do |button_id, row_id|
122
+ should_see_row row_id
123
+ arr = query("tableViewCell marked:'#{row_id}' child tableViewCellContentView child button marked:'#{button_id}'", :accessibilityIdentifier)
124
+ (arr.length == 1)
125
+ end
126
+
127
+ Then /^I touch the "([^"]*)" button in the "([^"]*)" row$/ do |button_id, row_id|
128
+ should_see_row row_id
129
+ touch("tableViewCell marked:'#{row_id}' child tableViewCellContentView child button marked:'#{button_id}'")
130
+ end
131
+
132
+ Then /^I should see a switch for "([^"]*)" in the "([^"]*)" row that is in the "([^"]*)" position$/ do |switch_id, row_id, on_off|
133
+ should_see_row row_id
134
+ query_str = "tableViewCell marked:'#{row_id}' child tableViewCellContentView child switch marked:'#{switch_id}'"
135
+ res = query(query_str, :isOn).first
136
+ unless res
137
+ screenshot_and_raise "expected to find a switch marked '#{switch_id}' in row '#{row_id}'"
138
+ end
139
+ expected = (on_off.eql? "on") ? 1 : 0
140
+ unless res.to_i == expected
141
+ screenshot_and_raise "expected to find a switch marked '#{switch_id}' in row '#{row_id}' that is '#{on_off}' but found it was '#{res ? "on" : "off"}'"
142
+ end
143
+ end
144
+
145
+ Then /^I should see a detail disclosure chevron in the "([^"]*)" row$/ do |row_id|
146
+ should_see_row row_id
147
+ # gray disclosure chevron is accessory type 1
148
+ res = query("tableViewCell marked:'#{row_id}'", :accessoryType).first
149
+ unless res == 1
150
+ screenshot_and_raise "expected to see disclosure chevron in row '#{row_id}' but found '#{res}'"
151
+ end
152
+ end
153
+
154
+ Then /^I touch the "([^"]*)" switch in the "([^"]*)" row$/ do |switch_id, row_id|
155
+ should_see_row row_id
156
+ touch("tableViewCell marked:'#{row_id}' child tableViewCellContentView child switch marked:'#{switch_id}'")
157
+ end
158
+
159
+ Then /^I should see "([^"]*)" in the "([^"]*)" section (footer|header)$/ do |text, section_footer_id, head_or_foot|
160
+ res = query("tableView child view marked:'#{section_footer_id} section #{head_or_foot}' child label", :text).first
161
+ unless res.eql? text
162
+ screenshot_and_raise "expected to see '#{text}' in the '#{section_footer_id}' section #{head_or_foot} but found '#{res}'"
163
+ end
164
+ end
165
+
166
+ Then /^I should see a text field with text "([^"]*)" in the "([^"]*)" row$/ do |text, row_id|
167
+ should_see_row row_id
168
+ query_str = "tableViewCell marked:'#{row_id}' child tableViewCellContentView child textField"
169
+ res = query(query_str)
170
+ screenshot_and_raise "expected to see text field in '#{row_id}' row" if res.empty?
171
+ actual = query(query_str, :text).first
172
+ screenshot_and_raise "expected to find text field with '#{text}' in row '#{row_id}' but found '#{actual}'" if !text.eql? actual
173
+ end
174
+
175
+ When /^I touch the "([^"]*)" text field in the "([^"]*)" row the keyboard appears$/ do |text_field_id, row_id|
176
+ should_see_row row_id
177
+ query_str = "tableViewCell marked:'#{row_id}' child tableViewCellContentView child textField marked:'#{text_field_id}'"
178
+ touch(query_str)
179
+ wait_for_animation
180
+ should_see_keyboard
181
+ end
182
+
183
+
184
+ Then /^I should see that the "([^"]*)" row has image "([^"]*)"$/ do |row_id, image_id|
185
+ should_see_row_with_image row_id, image_id
186
+ end
@@ -0,0 +1,37 @@
1
+ #include Briar::TextField
2
+
3
+ Then /^I should see the "([^"]*)" text field$/ do |name|
4
+ should_see_text_field name
5
+ end
6
+
7
+ Then /^I should not see "([^"]*)" text field$/ do |name|
8
+ should_not_see_text_field name
9
+ end
10
+
11
+
12
+ Then /^I touch the clear button in the "([^"]*)" text field$/ do |name|
13
+ should_see_clear_button_in_text_field name
14
+ touch("textField marked:'#{name}' child button")
15
+ step_pause
16
+ end
17
+
18
+
19
+ Then /^I should see a clear button in the text field in the "([^"]*)" row$/ do |row_id|
20
+ query_str = "tableViewCell marked:'#{row_id}' child tableViewCellContentView child textField"
21
+ res = query(query_str)
22
+ screenshot_and_raise "expected to see text field in '#{row_id}' row" if res.empty?
23
+ end
24
+
25
+
26
+ Then /^I touch the clear button in the text field in the "([^"]*)" row$/ do |row_id|
27
+ query_str = "tableViewCell marked:'#{row_id}' child tableViewCellContentView child textField"
28
+ res = query(query_str)
29
+ screenshot_and_raise "expected to see text field in '#{row_id}' row" if res.empty?
30
+ touch("#{query_str} child button")
31
+ step_pause
32
+ end
33
+
34
+
35
+ Then /^I should see "([^"]*)" in the text field "([^"]*)"$/ do |text, text_field|
36
+ should_see_text_field_with_text text_field, text
37
+ end
@@ -0,0 +1,44 @@
1
+ #include Briar::TextView
2
+
3
+ Then /^I clear text view named "([^\"]*)"$/ do |name|
4
+ res = query("textView marked:'#{name}'")
5
+ if res
6
+ set_text("textView marked:'#{name}'", "")
7
+ end
8
+ end
9
+
10
+ Then /^I should not see "([^"]*)" text view$/ do |name|
11
+ should_not_see_text_view (name)
12
+ end
13
+
14
+ Then /^I should see the text I just entered in the "([^"]*)" text view$/ do |text_view_id|
15
+ should_see_text_view text_view_id
16
+ text = query("textView marked:'#{text_view_id}'", :text).first
17
+ unless @text_entered_by_keyboard.eql? text
18
+ screenshot_and_raise "i expected to see '#{@text_entered_by_keyboard}' in text view '#{text_view_id}' but found '#{text}'"
19
+ end
20
+ end
21
+
22
+ Then /^I am done text editing$/ do
23
+ touch_navbar_item "done text editing"
24
+ end
25
+
26
+ Then /^I should see text view "([^"]*)" with placeholder text "([^"]*)"$/ do |text_view, placeholder|
27
+ tv_exists = !query("textView marked:'#{text_view}'").empty?
28
+ unless tv_exists
29
+ screenshot_and_raise "could not find text view #{text_view}"
30
+ end
31
+ ph_arr = query("textView marked:'#{text_view}' child label", :text)
32
+ if ph_arr.empty?
33
+ screenshot_and_raise "could not find placeholder label in text view #{text_view}"
34
+ end
35
+ actual = ph_arr[0]
36
+ unless actual.eql? placeholder
37
+ screenshot_and_raise "could not find placeholder text '#{placeholder}'"
38
+ end
39
+ end
40
+
41
+ Then /^I touch text view "([^"]*)"$/ do |text_view|
42
+ touch("textView marked:'#{text_view}'")
43
+ sleep(STEP_PAUSE)
44
+ end
File without changes
File without changes
File without changes
data/lib/briar.rb ADDED
@@ -0,0 +1,72 @@
1
+ #$:.unshift File.dirname(__FILE__)
2
+
3
+ DEVICE_ENDPOINT = (ENV['DEVICE_ENDPOINT'] || 'http://localhost:37265')
4
+ AI = :accessibilityIdentifier
5
+
6
+ require 'briar/version'
7
+ require 'briar/gestalt'
8
+ require 'briar/briar_core'
9
+
10
+ require 'briar/alerts_and_sheets/alert_view'
11
+
12
+ require 'briar/bars/tabbar'
13
+ require 'briar/bars/navbar'
14
+ require 'briar/bars/toolbar'
15
+
16
+ require 'briar/control/button'
17
+ require 'briar/control/segmented_control'
18
+ require 'briar/control/slider'
19
+
20
+ require 'briar/picker/picker_shared'
21
+ require 'briar/picker/picker'
22
+ require 'briar/picker/date_picker'
23
+
24
+ require 'briar/email'
25
+ require 'briar/image_view'
26
+ require 'briar/keyboard'
27
+ require 'briar/label'
28
+ require 'briar/scroll_view'
29
+
30
+ require 'briar/table'
31
+ require 'briar/text_field'
32
+ require 'briar/text_view'
33
+
34
+ #require File.join(File.dirname(__FILE__), '..','features','step_definitions', 'briar_core_steps')
35
+ #
36
+ #require File.join(File.dirname(__FILE__), '..','features','step_definitions','alerts_and_sheets', 'action_sheet_steps')
37
+ #require File.join(File.dirname(__FILE__), '..','features','step_definitions','alerts_and_sheets', 'alert_view_steps')
38
+ #
39
+ #require File.join(File.dirname(__FILE__), '..','features','step_definitions','bars', 'tabbar_steps')
40
+ #require File.join(File.dirname(__FILE__), '..','features','step_definitions','bars', 'navbar_steps')
41
+ #require File.join(File.dirname(__FILE__), '..','features','step_definitions','bars', 'toolbar_steps')
42
+ #
43
+ #require File.join(File.dirname(__FILE__), '..','features','step_definitions','control', 'button_steps')
44
+ #require File.join(File.dirname(__FILE__), '..','features','step_definitions','control', 'segmented_control_steps')
45
+ #require File.join(File.dirname(__FILE__), '..','features','step_definitions','control', 'slider_steps')
46
+ #
47
+ #
48
+ #require File.join(File.dirname(__FILE__), '..','features','step_definitions','picker', 'picker_steps')
49
+ #require File.join(File.dirname(__FILE__), '..','features','step_definitions','picker', 'date_picker_steps')
50
+ #
51
+ #
52
+ #require File.join(File.dirname(__FILE__), '..','features','step_definitions','email_steps')
53
+ #require File.join(File.dirname(__FILE__), '..','features','step_definitions','image_view_steps')
54
+ #require File.join(File.dirname(__FILE__), '..','features','step_definitions','keyboard_steps')
55
+ #require File.join(File.dirname(__FILE__), '..','features','step_definitions','label_steps')
56
+ #require File.join(File.dirname(__FILE__), '..','features','step_definitions','scroll_view_steps')
57
+ #
58
+ #require File.join(File.dirname(__FILE__), '..','features','step_definitions','table_steps')
59
+ #require File.join(File.dirname(__FILE__), '..','features','step_definitions','text_field_steps')
60
+ #require File.join(File.dirname(__FILE__), '..','features','step_definitions','text_view_steps')
61
+
62
+ def gestalt ()
63
+ uri = URI("#{DEVICE_ENDPOINT}/version")
64
+ res = Net::HTTP.get(uri)
65
+ Briar::Gestalt.new(res)
66
+ end
67
+
68
+
69
+
70
+
71
+
72
+
@@ -0,0 +1,16 @@
1
+ require 'calabash-cucumber'
2
+
3
+ module Briar
4
+ module Alerts_and_Sheets
5
+ def alert_button_exists? (button_id)
6
+ query("alertView child button child label", :text).include?(button_id)
7
+ end
8
+
9
+ def should_see_alert_button (button_id)
10
+ unless alert_button_exists? button_id
11
+ screenshot_and_raise "could not find alert view with button '#{button_id}'"
12
+ end
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,104 @@
1
+ require 'calabash-cucumber'
2
+
3
+ module Briar
4
+ module Bars
5
+ def navbar_visible?
6
+ !query('navigationBar').empty?
7
+ end
8
+
9
+ def navbar_has_back_button?
10
+ !query("navigationItemButtonView").empty?
11
+ end
12
+
13
+ def should_see_navbar_back_button
14
+ unless navbar_has_back_button?
15
+ screenshot_and_raise "there is no navigation bar back button"
16
+ end
17
+ end
18
+
19
+ def should_not_see_navbar_back_button
20
+ if navbar_has_back_button?
21
+ screenshot_and_raise "i should not see navigation bar back button"
22
+ end
23
+ end
24
+
25
+
26
+ # will not work to detect left/right buttons
27
+ def index_of_navbar_button (name)
28
+ titles = query("navigationButton", :accessibilityLabel)
29
+ titles.index(name)
30
+ end
31
+
32
+ def should_see_navbar_button (name)
33
+ idx = index_of_navbar_button name
34
+ if idx.nil?
35
+ screenshot_and_raise "there should be a navbar button named '#{name}'"
36
+ end
37
+ end
38
+
39
+ def should_not_see_navbar_button (name)
40
+ idx = index_of_navbar_button name
41
+ unless idx.nil?
42
+ screenshot_and_raise "i should not see a navbar button named #{name}"
43
+ end
44
+ end
45
+
46
+ def date_is_in_navbar (date)
47
+ with_leading = date.strftime("%a %b %d")
48
+ without_leading = date.strftime("%a %b #{date.day}")
49
+ items = query("navigationItemView", :accessibilityLabel)
50
+ items.include?(with_leading) || items.include?(without_leading)
51
+ end
52
+
53
+
54
+ def go_back_after_waiting
55
+ wait_for_animation
56
+ touch("navigationItemButtonView first")
57
+ step_pause
58
+ end
59
+
60
+ def go_back_and_wait_for_view (view)
61
+ wait_for_animation
62
+ touch_transition("navigationItemButtonView first",
63
+ "view marked:'#{view}'",
64
+ {:timeout=>TOUCH_TRANSITION_TIMEOUT,
65
+ :retry_frequency=>TOUCH_TRANSITION_RETRY_FREQ})
66
+ end
67
+
68
+ def touch_navbar_item(name)
69
+ wait_for_animation
70
+ idx = index_of_navbar_button name
71
+ #puts "index of nav bar button: #{idx}"
72
+ if idx
73
+ touch("navigationButton index:#{idx}")
74
+ step_pause
75
+ else
76
+ screenshot_and_raise "could not find navbar item #{name}"
77
+ end
78
+ end
79
+
80
+
81
+ def touch_navbar_item_and_wait_for_view(item, view)
82
+ wait_for_animation
83
+ idx = index_of_navbar_button item
84
+ touch_transition("navigationButton index:#{idx}",
85
+ "view marked:'#{view}'",
86
+ {:timeout=>TOUCH_TRANSITION_TIMEOUT,
87
+ :retry_frequency=>TOUCH_TRANSITION_RETRY_FREQ})
88
+ end
89
+
90
+
91
+ def navbar_has_title (title)
92
+ wait_for_animation
93
+ query('navigationItemView', :accessibilityLabel).include?(title)
94
+ end
95
+
96
+ def navbar_should_have_title(title)
97
+ unless navbar_has_title title
98
+ screenshot_and_raise "after waiting, i did not see navbar with title #{title}"
99
+ end
100
+ end
101
+
102
+ end
103
+ end
104
+