capybara 1.1.4 → 2.0.0.beta2

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 (84) hide show
  1. data/History.txt +100 -0
  2. data/License.txt +22 -0
  3. data/README.md +829 -0
  4. data/lib/capybara.rb +124 -6
  5. data/lib/capybara/cucumber.rb +2 -5
  6. data/lib/capybara/driver/base.rb +5 -5
  7. data/lib/capybara/driver/node.rb +2 -2
  8. data/lib/capybara/dsl.rb +3 -121
  9. data/lib/capybara/node/actions.rb +12 -28
  10. data/lib/capybara/node/base.rb +5 -13
  11. data/lib/capybara/node/element.rb +21 -21
  12. data/lib/capybara/node/finders.rb +27 -89
  13. data/lib/capybara/node/matchers.rb +107 -69
  14. data/lib/capybara/node/simple.rb +11 -13
  15. data/lib/capybara/query.rb +78 -0
  16. data/lib/capybara/rack_test/browser.rb +16 -27
  17. data/lib/capybara/rack_test/driver.rb +11 -1
  18. data/lib/capybara/rack_test/node.rb +17 -1
  19. data/lib/capybara/result.rb +84 -0
  20. data/lib/capybara/rspec/matchers.rb +28 -63
  21. data/lib/capybara/selector.rb +97 -33
  22. data/lib/capybara/selenium/driver.rb +14 -61
  23. data/lib/capybara/selenium/node.rb +6 -15
  24. data/lib/capybara/server.rb +32 -27
  25. data/lib/capybara/session.rb +54 -30
  26. data/lib/capybara/spec/public/jquery-ui.js +791 -0
  27. data/lib/capybara/spec/public/jquery.js +9046 -0
  28. data/lib/capybara/spec/public/test.js +4 -1
  29. data/lib/capybara/spec/session.rb +56 -27
  30. data/lib/capybara/spec/session/all_spec.rb +8 -4
  31. data/lib/capybara/spec/session/attach_file_spec.rb +12 -9
  32. data/lib/capybara/spec/session/check_spec.rb +6 -3
  33. data/lib/capybara/spec/session/choose_spec.rb +4 -1
  34. data/lib/capybara/spec/session/click_button_spec.rb +5 -14
  35. data/lib/capybara/spec/session/click_link_or_button_spec.rb +2 -1
  36. data/lib/capybara/spec/session/click_link_spec.rb +3 -17
  37. data/lib/capybara/spec/session/current_url_spec.rb +77 -9
  38. data/lib/capybara/spec/session/fill_in_spec.rb +8 -18
  39. data/lib/capybara/spec/session/find_spec.rb +19 -46
  40. data/lib/capybara/spec/session/first_spec.rb +2 -34
  41. data/lib/capybara/spec/session/has_css_spec.rb +1 -1
  42. data/lib/capybara/spec/session/has_field_spec.rb +28 -0
  43. data/lib/capybara/spec/session/has_select_spec.rb +84 -31
  44. data/lib/capybara/spec/session/has_table_spec.rb +7 -69
  45. data/lib/capybara/spec/session/has_text_spec.rb +168 -0
  46. data/lib/capybara/spec/session/javascript.rb +65 -81
  47. data/lib/capybara/spec/session/node_spec.rb +115 -0
  48. data/lib/capybara/spec/session/screenshot.rb +29 -0
  49. data/lib/capybara/spec/session/select_spec.rb +12 -12
  50. data/lib/capybara/spec/session/text_spec.rb +9 -4
  51. data/lib/capybara/spec/session/unselect_spec.rb +12 -6
  52. data/lib/capybara/spec/session/visit_spec.rb +76 -0
  53. data/lib/capybara/spec/session/within_frame_spec.rb +33 -0
  54. data/lib/capybara/spec/session/within_spec.rb +47 -58
  55. data/lib/capybara/spec/session/within_window_spec.rb +40 -0
  56. data/lib/capybara/spec/test_app.rb +27 -3
  57. data/lib/capybara/spec/views/form.erb +11 -10
  58. data/lib/capybara/spec/views/host_links.erb +2 -2
  59. data/lib/capybara/spec/views/tables.erb +6 -66
  60. data/lib/capybara/spec/views/with_html.erb +3 -3
  61. data/lib/capybara/spec/views/with_js.erb +11 -8
  62. data/lib/capybara/util/save_and_open_page.rb +4 -3
  63. data/lib/capybara/version.rb +1 -1
  64. data/spec/basic_node_spec.rb +15 -3
  65. data/spec/dsl_spec.rb +12 -10
  66. data/spec/rack_test_spec.rb +152 -0
  67. data/spec/rspec/features_spec.rb +0 -2
  68. data/spec/rspec/matchers_spec.rb +164 -89
  69. data/spec/rspec_spec.rb +0 -2
  70. data/spec/selenium_spec.rb +67 -0
  71. data/spec/server_spec.rb +35 -23
  72. data/spec/spec_helper.rb +18 -2
  73. metadata +30 -30
  74. data/README.rdoc +0 -722
  75. data/lib/capybara/spec/driver.rb +0 -301
  76. data/lib/capybara/spec/session/current_host_spec.rb +0 -68
  77. data/lib/capybara/spec/session/has_content_spec.rb +0 -106
  78. data/lib/capybara/util/timeout.rb +0 -27
  79. data/spec/driver/rack_test_driver_spec.rb +0 -89
  80. data/spec/driver/selenium_driver_spec.rb +0 -37
  81. data/spec/session/rack_test_session_spec.rb +0 -55
  82. data/spec/session/selenium_session_spec.rb +0 -26
  83. data/spec/string_spec.rb +0 -77
  84. data/spec/timeout_spec.rb +0 -28
@@ -0,0 +1,168 @@
1
+ shared_examples_for 'has_text' do
2
+ describe '#has_text?' do
3
+ it "should be true if the given text is on the page at least once" do
4
+ @session.visit('/with_html')
5
+ @session.should have_text('est')
6
+ @session.should have_text('Lorem')
7
+ @session.should have_text('Redirect')
8
+ end
9
+
10
+ it "should be true if scoped to an element which has the text" do
11
+ @session.visit('/with_html')
12
+ @session.within("//a[@title='awesome title']") do
13
+ @session.should have_text('labore')
14
+ end
15
+ end
16
+
17
+ it "should be false if scoped to an element which does not have the text" do
18
+ @session.visit('/with_html')
19
+ @session.within("//a[@title='awesome title']") do
20
+ @session.should_not have_text('monkey')
21
+ end
22
+ end
23
+
24
+ it "should ignore tags" do
25
+ @session.visit('/with_html')
26
+ @session.should_not have_text('exercitation <a href="/foo" id="foo">ullamco</a> laboris')
27
+ @session.should have_text('exercitation ullamco laboris')
28
+ end
29
+
30
+ it "should ignore extra whitespace and newlines" do
31
+ @session.visit('/with_html')
32
+ @session.should have_text('text with whitespace')
33
+ end
34
+
35
+ it "should be false if the given text is not on the page" do
36
+ @session.visit('/with_html')
37
+ @session.should_not have_text('xxxxyzzz')
38
+ @session.should_not have_text('monkey')
39
+ end
40
+
41
+ it 'should handle single quotes in the text' do
42
+ @session.visit('/with-quotes')
43
+ @session.should have_text("can't")
44
+ end
45
+
46
+ it 'should handle double quotes in the text' do
47
+ @session.visit('/with-quotes')
48
+ @session.should have_text(%q{"No," he said})
49
+ end
50
+
51
+ it 'should handle mixed single and double quotes in the text' do
52
+ @session.visit('/with-quotes')
53
+ @session.should have_text(%q{"you can't do that."})
54
+ end
55
+
56
+ it 'should be false if text is in the title tag in the head' do
57
+ @session.visit('/with_js')
58
+ @session.should_not have_text('with_js')
59
+ end
60
+
61
+ it 'should be false if text is inside a script tag in the body' do
62
+ @session.visit('/with_js')
63
+ @session.should_not have_text('a javascript comment')
64
+ @session.should_not have_text('aVar')
65
+ end
66
+
67
+ it "should be false if the given text is on the page but not visible" do
68
+ @session.visit('/with_html')
69
+ @session.should_not have_text('Inside element with hidden ancestor')
70
+ end
71
+
72
+ it "should be true if the text in the page matches given regexp" do
73
+ @session.visit('/with_html')
74
+ @session.should have_text(/Lorem/)
75
+ end
76
+
77
+ it "should be false if the text in the page doesn't match given regexp" do
78
+ @session.visit('/with_html')
79
+ @session.should_not have_text(/xxxxyzzz/)
80
+ end
81
+
82
+ it "should escape any characters that would have special meaning in a regexp" do
83
+ @session.visit('/with_html')
84
+ @session.should_not have_text('.orem')
85
+ end
86
+ end
87
+
88
+ describe '#has_no_text?' do
89
+ it "should be false if the given text is on the page at least once" do
90
+ @session.visit('/with_html')
91
+ @session.should_not have_no_text('est')
92
+ @session.should_not have_no_text('Lorem')
93
+ @session.should_not have_no_text('Redirect')
94
+ end
95
+
96
+ it "should be false if scoped to an element which has the text" do
97
+ @session.visit('/with_html')
98
+ @session.within("//a[@title='awesome title']") do
99
+ @session.should_not have_no_text('labore')
100
+ end
101
+ end
102
+
103
+ it "should be true if scoped to an element which does not have the text" do
104
+ @session.visit('/with_html')
105
+ @session.within("//a[@title='awesome title']") do
106
+ @session.should have_no_text('monkey')
107
+ end
108
+ end
109
+
110
+ it "should ignore tags" do
111
+ @session.visit('/with_html')
112
+ @session.should have_no_text('exercitation <a href="/foo" id="foo">ullamco</a> laboris')
113
+ @session.should_not have_no_text('exercitation ullamco laboris')
114
+ end
115
+
116
+ it "should be true if the given text is not on the page" do
117
+ @session.visit('/with_html')
118
+ @session.should have_no_text('xxxxyzzz')
119
+ @session.should have_no_text('monkey')
120
+ end
121
+
122
+ it 'should handle single quotes in the text' do
123
+ @session.visit('/with-quotes')
124
+ @session.should_not have_no_text("can't")
125
+ end
126
+
127
+ it 'should handle double quotes in the text' do
128
+ @session.visit('/with-quotes')
129
+ @session.should_not have_no_text(%q{"No," he said})
130
+ end
131
+
132
+ it 'should handle mixed single and double quotes in the text' do
133
+ @session.visit('/with-quotes')
134
+ @session.should_not have_no_text(%q{"you can't do that."})
135
+ end
136
+
137
+ it 'should be true if text is in the title tag in the head' do
138
+ @session.visit('/with_js')
139
+ @session.should have_no_text('with_js')
140
+ end
141
+
142
+ it 'should be true if text is inside a script tag in the body' do
143
+ @session.visit('/with_js')
144
+ @session.should have_no_text('a javascript comment')
145
+ @session.should have_no_text('aVar')
146
+ end
147
+
148
+ it "should be true if the given text is on the page but not visible" do
149
+ @session.visit('/with_html')
150
+ @session.should have_no_text('Inside element with hidden ancestor')
151
+ end
152
+
153
+ it "should be true if the text in the page doesn't match given regexp" do
154
+ @session.visit('/with_html')
155
+ @session.should have_no_text(/xxxxyzzz/)
156
+ end
157
+
158
+ it "should be false if the text in the page matches given regexp" do
159
+ @session.visit('/with_html')
160
+ @session.should_not have_no_text(/Lorem/)
161
+ end
162
+
163
+ it "should escape any characters that would have special meaning in a regexp" do
164
+ @session.visit('/with_html')
165
+ @session.should have_no_text('.orem')
166
+ end
167
+ end
168
+ end
@@ -26,8 +26,8 @@ shared_examples_for "session with javascript support" do
26
26
  node = @session.find(:css, '#reload-me')
27
27
  @session.click_link('Reload!')
28
28
  sleep(0.3)
29
- node.reload.text.should == 'has been reloaded'
30
- node.text.should == 'has been reloaded'
29
+ node.reload.text.should == 'RELOADED'
30
+ node.text.should == 'RELOADED'
31
31
  end
32
32
 
33
33
  it "should reload a parent node" do
@@ -35,8 +35,8 @@ shared_examples_for "session with javascript support" do
35
35
  node = @session.find(:css, '#reload-me').find(:css, 'em')
36
36
  @session.click_link('Reload!')
37
37
  sleep(0.3)
38
- node.reload.text.should == 'has been reloaded'
39
- node.text.should == 'has been reloaded'
38
+ node.reload.text.should == 'RELOADED'
39
+ node.text.should == 'RELOADED'
40
40
  end
41
41
 
42
42
  it "should not automatically reload" do
@@ -44,7 +44,7 @@ shared_examples_for "session with javascript support" do
44
44
  node = @session.find(:css, '#reload-me')
45
45
  @session.click_link('Reload!')
46
46
  sleep(0.3)
47
- running { node.text.should == 'has been reloaded' }.should raise_error
47
+ running { node.text.should == 'RELOADED' }.should raise_error
48
48
  end
49
49
  after { Capybara.automatic_reload = true }
50
50
  end
@@ -55,7 +55,7 @@ shared_examples_for "session with javascript support" do
55
55
  node = @session.find(:css, '#reload-me')
56
56
  @session.click_link('Reload!')
57
57
  sleep(0.3)
58
- node.text.should == 'has been reloaded'
58
+ node.text.should == 'RELOADED'
59
59
  end
60
60
 
61
61
  it "should reload a parent node automatically" do
@@ -63,7 +63,7 @@ shared_examples_for "session with javascript support" do
63
63
  node = @session.find(:css, '#reload-me').find(:css, 'em')
64
64
  @session.click_link('Reload!')
65
65
  sleep(0.3)
66
- node.text.should == 'has been reloaded'
66
+ node.text.should == 'RELOADED'
67
67
  end
68
68
 
69
69
  it "should reload a node automatically when using find" do
@@ -71,7 +71,7 @@ shared_examples_for "session with javascript support" do
71
71
  node = @session.find(:css, '#reload-me')
72
72
  @session.click_link('Reload!')
73
73
  sleep(0.3)
74
- node.find(:css, 'a').text.should == 'has been reloaded'
74
+ node.find(:css, 'a').text.should == 'RELOADED'
75
75
  end
76
76
 
77
77
  it "should not reload nodes which haven't been found" do
@@ -82,40 +82,40 @@ shared_examples_for "session with javascript support" do
82
82
  running { node.text.should == 'Foo' }.should raise_error
83
83
  running { node.text.should == 'Bar' }.should raise_error
84
84
  end
85
-
86
- it "should reload nodes with options" do
87
- @session.visit('/with_js')
88
- node = @session.find(:css, 'em', :text => "reloaded")
89
- @session.click_link('Reload!')
90
- sleep(1)
91
- node.text.should == 'has been reloaded'
92
- end
93
85
  end
94
86
  end
95
87
 
96
88
  describe '#find' do
97
89
  it "should allow triggering of custom JS events" do
98
- pending "cannot figure out how to do this with selenium" if @session.mode == :selenium
99
- @session.visit('/with_js')
100
- @session.find(:css, '#with_focus_event').trigger(:focus)
101
- @session.should have_css('#focus_event_triggered')
90
+ # Not supported by Selenium without resorting to JavaScript execution
91
+ # http://code.google.com/p/selenium/wiki/FrequentlyAskedQuestions#Q:_How_can_I_trigger_arbitrary_events_on_the_page?
92
+ unless @session.mode == :selenium
93
+ @session.visit('/with_js')
94
+ @session.find(:css, '#with_focus_event').trigger(:focus)
95
+ @session.should have_css('#focus_event_triggered')
96
+ end
102
97
  end
103
98
  end
104
99
 
105
- describe '#body' do
100
+ describe '#html' do
106
101
  it "should return the current state of the page" do
107
102
  @session.visit('/with_js')
108
- @session.body.should include('I changed it')
109
- @session.body.should_not include('This is text')
103
+ @session.html.should include('I changed it')
104
+ @session.html.should_not include('This is text')
110
105
  end
111
106
  end
112
107
 
113
- describe '#source' do
114
- it "should return the original, unmodified source of the page" do
115
- pending "cannot figure out how to do this with selenium" if @session.mode == :selenium
116
- @session.visit('/with_js')
117
- @session.source.should include('This is text')
118
- @session.source.should_not include('I changed it')
108
+ [:body, :source].each do |method|
109
+ describe "##{method}" do
110
+ it "should return the original, unmodified source of the page" do
111
+ # Not supported by Selenium. See for example
112
+ # http://stackoverflow.com/questions/6050805
113
+ unless @session.mode == :selenium
114
+ @session.visit('/with_js')
115
+ @session.send(method).should include('This is text')
116
+ @session.send(method).should_not include('I changed it')
117
+ end
118
+ end
119
119
  end
120
120
  end
121
121
 
@@ -151,58 +151,6 @@ shared_examples_for "session with javascript support" do
151
151
  end
152
152
  end
153
153
 
154
- describe '#wait_until' do
155
- before do
156
- @default_timeout = Capybara.default_wait_time
157
- end
158
-
159
- after do
160
- Capybara.default_wait_time = @default_wait_time
161
- end
162
-
163
- it "should wait for block to return true" do
164
- @session.visit('/with_js')
165
- @session.select('My Waiting Option', :from => 'waiter')
166
- @session.evaluate_script('activeRequests == 1').should be_true
167
- @session.wait_until do
168
- @session.evaluate_script('activeRequests == 0')
169
- end
170
- @session.evaluate_script('activeRequests == 0').should be_true
171
- end
172
-
173
- it "should raise Capybara::TimeoutError if block doesn't return true within timeout" do
174
- @session.visit('/with_html')
175
- Proc.new do
176
- @session.wait_until(0.1) do
177
- @session.all('//div[@id="nosuchthing"]').first
178
- end
179
- end.should raise_error(::Capybara::TimeoutError)
180
- end
181
-
182
- it "should accept custom timeout in seconds" do
183
- start = Time.now
184
- Capybara.default_wait_time = 5
185
- begin
186
- @session.wait_until(0.1) { false }
187
- rescue Capybara::TimeoutError; end
188
- (Time.now - start).should be_within(0.1).of(0.1)
189
- end
190
-
191
- it "should default to Capybara.default_wait_time before timeout" do
192
- @session.driver # init the driver to exclude init timing from test
193
- start = Time.now
194
- Capybara.default_wait_time = 0.2
195
- begin
196
- @session.wait_until { false }
197
- rescue Capybara::TimeoutError; end
198
- if @session.driver.has_shortcircuit_timeout?
199
- (Time.now - start).should be_within(0.1).of(0)
200
- else
201
- (Time.now - start).should be_within(0.1).of(0.2)
202
- end
203
- end
204
- end
205
-
206
154
  describe '#click_link_or_button' do
207
155
  it "should wait for asynchronous load" do
208
156
  @session.visit('/with_js')
@@ -233,6 +181,13 @@ shared_examples_for "session with javascript support" do
233
181
  @session.click_link('Click me')
234
182
  @session.fill_in('new_field', :with => 'Testing...')
235
183
  end
184
+
185
+ context 'on a pre-populated textfield with a reformatting onchange' do
186
+ it 'should only trigger onchange once' do
187
+ @session.fill_in('with_change_event', :with => 'some value')
188
+ @session.find(:css, '#with_change_event').value.should == 'some value'
189
+ end
190
+ end
236
191
  end
237
192
 
238
193
  describe '#check' do
@@ -291,6 +246,35 @@ shared_examples_for "session with javascript support" do
291
246
  end
292
247
  end
293
248
 
249
+ describe '#has_text?' do
250
+ it "should wait for text to appear" do
251
+ @session.visit('/with_js')
252
+ @session.click_link('Click me')
253
+ @session.should have_text("Has been clicked")
254
+ end
255
+ end
256
+
257
+ describe '#has_no_text?' do
258
+ it "should wait for text to disappear" do
259
+ @session.visit('/with_js')
260
+ @session.click_link('Click me')
261
+ @session.should have_no_text("I changed it")
262
+ end
263
+ end
264
+
265
+ describe "#current_path" do
266
+ it "is affected by pushState" do
267
+ @session.visit("/with_js")
268
+ @session.execute_script("window.history.pushState({}, '', '/pushed')")
269
+ @session.current_path.should == "/pushed"
270
+ end
271
+
272
+ it "is affected by replaceState" do
273
+ @session.visit("/with_js")
274
+ @session.execute_script("window.history.replaceState({}, '', '/replaced')")
275
+ @session.current_path.should == "/replaced"
276
+ end
277
+ end
294
278
  end
295
279
  end
296
280
 
@@ -0,0 +1,115 @@
1
+ shared_examples_for "node" do
2
+ describe "node" do
3
+ before do
4
+ @session.visit('/with_html')
5
+ end
6
+
7
+ it "should act like a session object" do
8
+ @session.visit('/form')
9
+ @form = @session.find(:css, '#get-form')
10
+ @form.should have_field('Middle Name')
11
+ @form.should have_no_field('Languages')
12
+ @form.fill_in('Middle Name', :with => 'Monkey')
13
+ @form.click_button('med')
14
+ extract_results(@session)['middle_name'].should == 'Monkey'
15
+ end
16
+
17
+ it "should scope CSS selectors" do
18
+ @session.find(:css, '#second').should have_no_css('h1')
19
+ end
20
+
21
+ describe "#parent" do
22
+ it "should have a reference to its parent if there is one" do
23
+ @node = @session.find(:css, '#first')
24
+ @node.parent.should == @node.session.document
25
+ @node.find(:css, '#foo').parent.should == @node
26
+ end
27
+ end
28
+
29
+ describe "#text" do
30
+ it "should extract node texts" do
31
+ @session.all('//a')[0].text.should == 'labore'
32
+ @session.all('//a')[1].text.should == 'ullamco'
33
+ end
34
+
35
+ it "should return document text on /html selector" do
36
+ @session.visit('/with_simple_html')
37
+ @session.all('/html')[0].text.should == 'Bar'
38
+ end
39
+ end
40
+
41
+ describe "#[]" do
42
+ it "should extract node attributes" do
43
+ @session.all('//a')[0][:class].should == 'simple'
44
+ @session.all('//a')[1][:id].should == 'foo'
45
+ @session.all('//input')[0][:type].should == 'text'
46
+ end
47
+
48
+ it "should extract boolean node attributes" do
49
+ @session.find('//input[@id="checked_field"]')[:checked].should be_true
50
+ end
51
+ end
52
+
53
+ describe "#value" do
54
+ it "should allow retrieval of the value" do
55
+ @session.find('//textarea[@id="normal"]').value.should == 'banana'
56
+ end
57
+
58
+ it "should not swallow extra newlines in textarea" do
59
+ @session.find('//textarea[@id="additional_newline"]').value.should == "\nbanana"
60
+ end
61
+ end
62
+
63
+ describe "#set" do
64
+ it "should allow assignment of field value" do
65
+ @session.first('//input').value.should == 'monkey'
66
+ @session.first('//input').set('gorilla')
67
+ @session.first('//input').value.should == 'gorilla'
68
+ end
69
+ end
70
+
71
+ describe "#tag_name" do
72
+ it "should extract node tag name" do
73
+ @session.all('//a')[0].tag_name.should == 'a'
74
+ @session.all('//a')[1].tag_name.should == 'a'
75
+ @session.all('//p')[1].tag_name.should == 'p'
76
+ end
77
+ end
78
+
79
+ describe "#visible?" do
80
+ it "should extract node visibility" do
81
+ @session.first('//a').should be_visible
82
+
83
+ @session.find('//div[@id="hidden"]').should_not be_visible
84
+ @session.find('//div[@id="hidden_via_ancestor"]').should_not be_visible
85
+ end
86
+ end
87
+
88
+ describe "#checked?" do
89
+ it "should extract node checked state" do
90
+ @session.visit('/form')
91
+ @session.find('//input[@id="gender_female"]').should be_checked
92
+ @session.find('//input[@id="gender_male"]').should_not be_checked
93
+ @session.first('//h1').should_not be_checked
94
+ end
95
+ end
96
+
97
+ describe "#selected?" do
98
+ it "should extract node selected state" do
99
+ @session.visit('/form')
100
+ @session.find('//option[@value="en"]').should be_selected
101
+ @session.find('//option[@value="sv"]').should_not be_selected
102
+ @session.first('//h1').should_not be_selected
103
+ end
104
+ end
105
+
106
+ describe "#==" do
107
+ it "preserve object identity" do
108
+ (@session.find('//h1') == @session.find('//h1')).should be_true
109
+ (@session.find('//h1') === @session.find('//h1')).should be_true
110
+ (@session.find('//h1').eql? @session.find('//h1')).should be_false
111
+ end
112
+ end
113
+
114
+ end
115
+ end