capybara 0.3.9 → 0.4.0.rc

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. data/History.txt +43 -1
  2. data/README.rdoc +168 -98
  3. data/lib/capybara.rb +77 -15
  4. data/lib/capybara/driver/base.rb +21 -16
  5. data/lib/capybara/driver/celerity_driver.rb +39 -41
  6. data/lib/capybara/driver/culerity_driver.rb +2 -1
  7. data/lib/capybara/driver/node.rb +66 -0
  8. data/lib/capybara/driver/rack_test_driver.rb +66 -67
  9. data/lib/capybara/driver/selenium_driver.rb +43 -47
  10. data/lib/capybara/dsl.rb +44 -6
  11. data/lib/capybara/node.rb +185 -24
  12. data/lib/capybara/node/actions.rb +170 -0
  13. data/lib/capybara/node/finders.rb +150 -0
  14. data/lib/capybara/node/matchers.rb +360 -0
  15. data/lib/capybara/rails.rb +1 -0
  16. data/lib/capybara/selector.rb +52 -0
  17. data/lib/capybara/server.rb +68 -87
  18. data/lib/capybara/session.rb +221 -207
  19. data/lib/capybara/spec/driver.rb +45 -35
  20. data/lib/capybara/spec/public/test.js +1 -1
  21. data/lib/capybara/spec/session.rb +28 -53
  22. data/lib/capybara/spec/session/all_spec.rb +7 -3
  23. data/lib/capybara/spec/session/check_spec.rb +50 -52
  24. data/lib/capybara/spec/session/click_button_spec.rb +9 -0
  25. data/lib/capybara/spec/session/click_link_or_button_spec.rb +37 -0
  26. data/lib/capybara/spec/session/current_url_spec.rb +7 -0
  27. data/lib/capybara/spec/session/find_button_spec.rb +4 -2
  28. data/lib/capybara/spec/session/find_by_id_spec.rb +4 -2
  29. data/lib/capybara/spec/session/find_field_spec.rb +7 -3
  30. data/lib/capybara/spec/session/find_link_spec.rb +5 -3
  31. data/lib/capybara/spec/session/find_spec.rb +71 -6
  32. data/lib/capybara/spec/session/has_field_spec.rb +1 -1
  33. data/lib/capybara/spec/session/has_selector_spec.rb +129 -0
  34. data/lib/capybara/spec/session/has_xpath_spec.rb +4 -4
  35. data/lib/capybara/spec/session/javascript.rb +25 -5
  36. data/lib/capybara/spec/session/select_spec.rb +16 -2
  37. data/lib/capybara/spec/session/unselect_spec.rb +8 -1
  38. data/lib/capybara/spec/session/within_spec.rb +5 -5
  39. data/lib/capybara/spec/views/form.erb +65 -1
  40. data/lib/capybara/spec/views/popup_one.erb +8 -0
  41. data/lib/capybara/spec/views/popup_two.erb +8 -0
  42. data/lib/capybara/spec/views/with_html.erb +5 -0
  43. data/lib/capybara/spec/views/within_popups.erb +25 -0
  44. data/lib/capybara/{save_and_open_page.rb → util/save_and_open_page.rb} +3 -3
  45. data/lib/capybara/util/timeout.rb +27 -0
  46. data/lib/capybara/version.rb +1 -1
  47. data/spec/capybara_spec.rb +18 -8
  48. data/spec/driver/celerity_driver_spec.rb +10 -14
  49. data/spec/driver/culerity_driver_spec.rb +4 -3
  50. data/spec/driver/rack_test_driver_spec.rb +39 -2
  51. data/spec/driver/remote_culerity_driver_spec.rb +5 -7
  52. data/spec/driver/remote_selenium_driver_spec.rb +7 -10
  53. data/spec/driver/selenium_driver_spec.rb +3 -2
  54. data/spec/dsl_spec.rb +5 -14
  55. data/spec/save_and_open_page_spec.rb +19 -19
  56. data/spec/server_spec.rb +22 -10
  57. data/spec/session/celerity_session_spec.rb +17 -21
  58. data/spec/session/culerity_session_spec.rb +3 -3
  59. data/spec/session/rack_test_session_spec.rb +2 -2
  60. data/spec/session/selenium_session_spec.rb +2 -2
  61. data/spec/spec_helper.rb +27 -6
  62. data/spec/{wait_until_spec.rb → timeout_spec.rb} +14 -14
  63. metadata +88 -46
  64. data/lib/capybara/searchable.rb +0 -54
  65. data/lib/capybara/spec/session/click_spec.rb +0 -24
  66. data/lib/capybara/spec/session/locate_spec.rb +0 -65
  67. data/lib/capybara/wait_until.rb +0 -28
  68. data/lib/capybara/xpath.rb +0 -179
  69. data/spec/searchable_spec.rb +0 -66
  70. data/spec/xpath_spec.rb +0 -180
@@ -5,4 +5,11 @@ shared_examples_for "current_url" do
5
5
  @session.current_url.should =~ %r(http://[^/]+/form)
6
6
  end
7
7
  end
8
+
9
+ describe '#current_path' do
10
+ it 'should show the correct location' do
11
+ @session.visit('/foo')
12
+ @session.current_path.should == '/foo'
13
+ end
14
+ end
8
15
  end
@@ -9,8 +9,10 @@ shared_examples_for "find_button" do
9
9
  @session.find_button('crap321').value.should == "crappy"
10
10
  end
11
11
 
12
- it "should return nil if the field doesn't exist" do
13
- @session.find_button('Does not exist').should be_nil
12
+ it "should raise error if the field doesn't exist" do
13
+ running do
14
+ @session.find_button('Does not exist')
15
+ end.should raise_error(Capybara::ElementNotFound)
14
16
  end
15
17
  end
16
18
  end
@@ -9,8 +9,10 @@ shared_examples_for "find_by_id" do
9
9
  @session.find_by_id('hidden_via_ancestor').tag_name.should == 'div'
10
10
  end
11
11
 
12
- it "should return nil if no element with id is found" do
13
- @session.find_by_id('nothing_with_this_id').should be_nil
12
+ it "should raise error if no element with id is found" do
13
+ running do
14
+ @session.find_by_id('nothing_with_this_id')
15
+ end.should raise_error(Capybara::ElementNotFound)
14
16
  end
15
17
  end
16
18
  end
@@ -10,13 +10,17 @@ shared_examples_for "find_field" do
10
10
  @session.find_field('Region')[:name].should == 'form[region]'
11
11
  end
12
12
 
13
- it "should be nil if the field doesn't exist" do
14
- @session.find_field('Does not exist').should be_nil
13
+ it "should raise error if the field doesn't exist" do
14
+ running do
15
+ @session.find_field('Does not exist')
16
+ end.should raise_error(Capybara::ElementNotFound)
15
17
  end
16
18
 
17
19
  it "should be aliased as 'field_labeled' for webrat compatibility" do
18
20
  @session.field_labeled('Dog').value.should == 'dog'
19
- @session.field_labeled('Does not exist').should be_nil
21
+ running do
22
+ @session.field_labeled('Does not exist')
23
+ end.should raise_error(Capybara::ElementNotFound)
20
24
  end
21
25
  end
22
26
  end
@@ -7,11 +7,13 @@ shared_examples_for "find_link" do
7
7
 
8
8
  it "should find any field" do
9
9
  @session.find_link('foo').text.should == "ullamco"
10
- @session.find_link('labore')[:href].should == "/with_simple_html"
10
+ @session.find_link('labore')[:href].should =~ %r(/with_simple_html$)
11
11
  end
12
12
 
13
- it "should return nil if the field doesn't exist" do
14
- @session.find_link('Does not exist').should be_nil
13
+ it "should raise error if the field doesn't exist" do
14
+ running do
15
+ @session.find_link('Does not exist')
16
+ end.should raise_error(Capybara::ElementNotFound)
15
17
  end
16
18
  end
17
19
  end
@@ -1,14 +1,45 @@
1
- shared_examples_for "find" do
1
+ shared_examples_for "find" do
2
2
  describe '#find' do
3
3
  before do
4
4
  @session.visit('/with_html')
5
5
  end
6
6
 
7
+ after do
8
+ Capybara::Selector.remove(:monkey)
9
+ end
10
+
7
11
  it "should find the first element using the given locator" do
8
12
  @session.find('//h1').text.should == 'This is a test'
9
13
  @session.find("//input[@id='test_field']")[:value].should == 'monkey'
10
14
  end
11
15
 
16
+ it "should be aliased as locate for backward compatibility" do
17
+ Capybara.should_receive(:deprecate).with("locate", "find").twice
18
+ @session.locate('//h1').text.should == 'This is a test'
19
+ @session.locate("//input[@id='test_field']")[:value].should == 'monkey'
20
+ end
21
+
22
+ it "should find the first element using the given locator and options" do
23
+ @session.find('//a', :text => 'Redirect')[:id].should == 'red'
24
+ @session.find(:css, 'a', :text => 'A link')[:title].should == 'twas a fine link'
25
+ end
26
+
27
+ describe 'the returned node' do
28
+ it "should act like a session object" do
29
+ @session.visit('/form')
30
+ @form = @session.find(:css, '#get-form')
31
+ @form.should have_field('Middle Name')
32
+ @form.should have_no_field('Languages')
33
+ @form.fill_in('Middle Name', :with => 'Monkey')
34
+ @form.click_button('med')
35
+ extract_results(@session)['middle_name'].should == 'Monkey'
36
+ end
37
+
38
+ it "should scope CSS selectors" do
39
+ @session.find(:css, '#second').should have_no_css('h1')
40
+ end
41
+ end
42
+
12
43
  context "with css selectors" do
13
44
  it "should find the first element using the given locator" do
14
45
  @session.find(:css, 'h1').text.should == 'This is a test'
@@ -16,6 +47,14 @@ shared_examples_for "find" do
16
47
  end
17
48
  end
18
49
 
50
+ context "with id selectors" do
51
+ it "should find the first element using the given locator" do
52
+ @session.find(:id, 'john_monkey').text.should == 'Monkey John'
53
+ @session.find(:id, 'red').text.should == 'Redirect'
54
+ @session.find(:red).text.should == 'Redirect'
55
+ end
56
+ end
57
+
19
58
  context "with xpath selectors" do
20
59
  it "should find the first element using the given locator" do
21
60
  @session.find(:xpath, '//h1').text.should == 'This is a test'
@@ -23,6 +62,24 @@ shared_examples_for "find" do
23
62
  end
24
63
  end
25
64
 
65
+ context "with custom selector" do
66
+ it "should use the custom selector" do
67
+ Capybara::Selector.add(:monkey) { |name| ".//*[@id='#{name}_monkey']" }
68
+ @session.find(:monkey, 'john').text.should == 'Monkey John'
69
+ @session.find(:monkey, 'paul').text.should == 'Monkey Paul'
70
+ end
71
+ end
72
+
73
+ context "with custom selector with :for option" do
74
+ it "should use the selector when it matches the :for option" do
75
+ Capybara::Selector.add(:monkey, :for => Fixnum) { |num| ".//*[contains(@id, 'monkey')][#{num}]" }
76
+ @session.find(:monkey, '2').text.should == 'Monkey Paul'
77
+ @session.find(1).text.should == 'Monkey John'
78
+ @session.find(2).text.should == 'Monkey Paul'
79
+ @session.find('//h1').text.should == 'This is a test'
80
+ end
81
+ end
82
+
26
83
  context "with css as default selector" do
27
84
  before { Capybara.default_selector = :css }
28
85
  it "should find the first element using the given locator" do
@@ -32,13 +89,21 @@ shared_examples_for "find" do
32
89
  after { Capybara.default_selector = :xpath }
33
90
  end
34
91
 
35
- it "should return nil when nothing was found" do
36
- @session.find('//div[@id="nosuchthing"]').should be_nil
92
+ it "should raise ElementNotFound with specified fail message if nothing was found" do
93
+ running do
94
+ @session.find(:xpath, '//div[@id="nosuchthing"]', :message => 'arghh').should be_nil
95
+ end.should raise_error(Capybara::ElementNotFound, "arghh")
96
+ end
97
+
98
+ it "should raise ElementNotFound with a useful default message if nothing was found" do
99
+ running do
100
+ @session.find(:xpath, '//div[@id="nosuchthing"]').should be_nil
101
+ end.should raise_error(Capybara::ElementNotFound, "Unable to find '//div[@id=\"nosuchthing\"]'")
37
102
  end
38
103
 
39
104
  it "should accept an XPath instance and respect the order of paths" do
40
105
  @session.visit('/form')
41
- @xpath = Capybara::XPath.text_field('Name')
106
+ @xpath = XPath::HTML.fillable_field('Name')
42
107
  @session.find(@xpath).value.should == 'John Smith'
43
108
  end
44
109
 
@@ -49,8 +114,8 @@ shared_examples_for "find" do
49
114
 
50
115
  it "should find the first element using the given locator" do
51
116
  @session.within(:xpath, "//div[@id='for_bar']") do
52
- @session.find('//li').text.should =~ /With Simple HTML/
53
- end
117
+ @session.find('.//li').text.should =~ /With Simple HTML/
118
+ end
54
119
  end
55
120
  end
56
121
  end
@@ -42,7 +42,7 @@ shared_examples_for "has_field" do
42
42
  end
43
43
 
44
44
  context 'with value' do
45
- it "should be flase if a field with the given value is on the page" do
45
+ it "should be false if a field with the given value is on the page" do
46
46
  @session.should_not have_no_field('First Name', :with => 'John')
47
47
  @session.should_not have_no_field('Phone', :with => '+1 555 7021')
48
48
  @session.should_not have_no_field('Street', :with => 'Sesame street 66')
@@ -0,0 +1,129 @@
1
+ shared_examples_for "has_selector" do
2
+ describe '#has_selector?' do
3
+ before do
4
+ @session.visit('/with_html')
5
+ end
6
+
7
+ it "should be true if the given selector is on the page" do
8
+ @session.should have_selector(:xpath, "//p")
9
+ @session.should have_selector(:css, "p a#foo")
10
+ @session.should have_selector(:foo)
11
+ @session.should have_selector("//p[contains(.,'est')]")
12
+ end
13
+
14
+ it "should be false if the given selector is not on the page" do
15
+ @session.should_not have_selector(:xpath, "//abbr")
16
+ @session.should_not have_selector(:css, "p a#doesnotexist")
17
+ @session.should_not have_selector(:doesnotexist)
18
+ @session.should_not have_selector("//p[contains(.,'thisstringisnotonpage')]")
19
+ end
20
+
21
+ it "should use default selector" do
22
+ Capybara.default_selector = :css
23
+ @session.should_not have_selector("p a#doesnotexist")
24
+ @session.should have_selector("p a#foo")
25
+ end
26
+
27
+ it "should respect scopes" do
28
+ @session.within "//p[@id='first']" do
29
+ @session.should have_selector(".//a[@id='foo']")
30
+ @session.should_not have_selector(".//a[@id='red']")
31
+ end
32
+ end
33
+
34
+ context "with count" do
35
+ it "should be true if the content is on the page the given number of times" do
36
+ @session.should have_selector("//p", :count => 3)
37
+ @session.should have_selector("//p//a[@id='foo']", :count => 1)
38
+ @session.should have_selector("//p[contains(.,'est')]", :count => 1)
39
+ end
40
+
41
+ it "should be false if the content is on the page the given number of times" do
42
+ @session.should_not have_selector("//p", :count => 6)
43
+ @session.should_not have_selector("//p//a[@id='foo']", :count => 2)
44
+ @session.should_not have_selector("//p[contains(.,'est')]", :count => 5)
45
+ end
46
+
47
+ it "should be false if the content isn't on the page at all" do
48
+ @session.should_not have_selector("//abbr", :count => 2)
49
+ @session.should_not have_selector("//p//a[@id='doesnotexist']", :count => 1)
50
+ end
51
+ end
52
+
53
+ context "with text" do
54
+ it "should discard all matches where the given string is not contained" do
55
+ @session.should have_selector("//p//a", :text => "Redirect", :count => 1)
56
+ @session.should_not have_selector("//p", :text => "Doesnotexist")
57
+ end
58
+
59
+ it "should discard all matches where the given regexp is not matched" do
60
+ @session.should have_selector("//p//a", :text => /re[dab]i/i, :count => 1)
61
+ @session.should_not have_selector("//p//a", :text => /Red$/)
62
+ end
63
+ end
64
+ end
65
+
66
+ describe '#has_no_selector?' do
67
+ before do
68
+ @session.visit('/with_html')
69
+ end
70
+
71
+ it "should be false if the given selector is on the page" do
72
+ @session.should_not have_no_selector(:xpath, "//p")
73
+ @session.should_not have_no_selector(:css, "p a#foo")
74
+ @session.should_not have_no_selector(:foo)
75
+ @session.should_not have_no_selector("//p[contains(.,'est')]")
76
+ end
77
+
78
+ it "should be true if the given selector is not on the page" do
79
+ @session.should have_no_selector(:xpath, "//abbr")
80
+ @session.should have_no_selector(:css, "p a#doesnotexist")
81
+ @session.should have_no_selector(:doesnotexist)
82
+ @session.should have_no_selector("//p[contains(.,'thisstringisnotonpage')]")
83
+ end
84
+
85
+ it "should use default selector" do
86
+ Capybara.default_selector = :css
87
+ @session.should have_no_selector("p a#doesnotexist")
88
+ @session.should_not have_no_selector("p a#foo")
89
+ end
90
+
91
+ it "should respect scopes" do
92
+ @session.within "//p[@id='first']" do
93
+ @session.should_not have_no_selector(".//a[@id='foo']")
94
+ @session.should have_no_selector(".//a[@id='red']")
95
+ end
96
+ end
97
+
98
+ context "with count" do
99
+ it "should be false if the content is on the page the given number of times" do
100
+ @session.should_not have_no_selector("//p", :count => 3)
101
+ @session.should_not have_no_selector("//p//a[@id='foo']", :count => 1)
102
+ @session.should_not have_no_selector("//p[contains(.,'est')]", :count => 1)
103
+ end
104
+
105
+ it "should be true if the content is on the page the wrong number of times" do
106
+ @session.should have_no_selector("//p", :count => 6)
107
+ @session.should have_no_selector("//p//a[@id='foo']", :count => 2)
108
+ @session.should have_no_selector("//p[contains(.,'est')]", :count => 5)
109
+ end
110
+
111
+ it "should be true if the content isn't on the page at all" do
112
+ @session.should have_no_selector("//abbr", :count => 2)
113
+ @session.should have_no_selector("//p//a[@id='doesnotexist']", :count => 1)
114
+ end
115
+ end
116
+
117
+ context "with text" do
118
+ it "should discard all matches where the given string is contained" do
119
+ @session.should_not have_no_selector("//p//a", :text => "Redirect", :count => 1)
120
+ @session.should have_no_selector("//p", :text => "Doesnotexist")
121
+ end
122
+
123
+ it "should discard all matches where the given regexp is matched" do
124
+ @session.should_not have_no_selector("//p//a", :text => /re[dab]i/i, :count => 1)
125
+ @session.should have_no_selector("//p//a", :text => /Red$/)
126
+ end
127
+ end
128
+ end
129
+ end
@@ -23,8 +23,8 @@ shared_examples_for "has_xpath" do
23
23
 
24
24
  it "should respect scopes" do
25
25
  @session.within "//p[@id='first']" do
26
- @session.should have_xpath("//a[@id='foo']")
27
- @session.should_not have_xpath("//a[@id='red']")
26
+ @session.should have_xpath(".//a[@id='foo']")
27
+ @session.should_not have_xpath(".//a[@id='red']")
28
28
  end
29
29
  end
30
30
 
@@ -84,8 +84,8 @@ shared_examples_for "has_xpath" do
84
84
 
85
85
  it "should respect scopes" do
86
86
  @session.within "//p[@id='first']" do
87
- @session.should_not have_no_xpath("//a[@id='foo']")
88
- @session.should have_no_xpath("//a[@id='red']")
87
+ @session.should_not have_no_xpath(".//a[@id='foo']")
88
+ @session.should have_no_xpath(".//a[@id='red']")
89
89
  end
90
90
  end
91
91
 
@@ -7,6 +7,26 @@ shared_examples_for "session with javascript support" do
7
7
  after do
8
8
  Capybara.default_wait_time = 0
9
9
  end
10
+
11
+ describe '#drag' do
12
+ it "should drag and drop an object" do
13
+ pending "drag/drop is currently broken under celerity/culerity" if @session.driver.is_a?(Capybara::Driver::Celerity)
14
+ @session.visit('/with_js')
15
+ @session.drag('//div[@id="drag"]', '//div[@id="drop"]')
16
+ @session.find('//div[contains(., "Dropped!")]').should_not be_nil
17
+ end
18
+ end
19
+
20
+ describe 'Node#drag_to' do
21
+ it "should drag and drop an object" do
22
+ pending "drag/drop is currently broken under celerity/culerity" if @session.driver.is_a?(Capybara::Driver::Celerity)
23
+ @session.visit('/with_js')
24
+ element = @session.find('//div[@id="drag"]')
25
+ target = @session.find('//div[@id="drop"]')
26
+ element.drag_to(target)
27
+ @session.find('//div[contains(., "Dropped!")]').should_not be_nil
28
+ end
29
+ end
10
30
 
11
31
  describe '#find' do
12
32
  it "should allow triggering of custom JS events" do
@@ -49,11 +69,11 @@ shared_examples_for "session with javascript support" do
49
69
  end
50
70
  end
51
71
 
52
- describe '#locate' do
72
+ describe '#find' do
53
73
  it "should wait for asynchronous load" do
54
74
  @session.visit('/with_js')
55
75
  @session.click_link('Click me')
56
- @session.locate("//a[contains(.,'Has been clicked')]")[:href].should == '#'
76
+ @session.find(:css, "a#has-been-clicked").text.should include('Has been clicked')
57
77
  end
58
78
  end
59
79
 
@@ -80,7 +100,7 @@ shared_examples_for "session with javascript support" do
80
100
  @session.visit('/with_html')
81
101
  Proc.new do
82
102
  @session.wait_until(0.1) do
83
- @session.find('//div[@id="nosuchthing"]')
103
+ @session.all('//div[@id="nosuchthing"]').first
84
104
  end
85
105
  end.should raise_error(::Capybara::TimeoutError)
86
106
  end
@@ -109,11 +129,11 @@ shared_examples_for "session with javascript support" do
109
129
  end
110
130
  end
111
131
 
112
- describe '#click' do
132
+ describe '#click_link_or_button' do
113
133
  it "should wait for asynchronous load" do
114
134
  @session.visit('/with_js')
115
135
  @session.click_link('Click me')
116
- @session.click('Has been clicked')
136
+ @session.click_link_or_button('Has been clicked')
117
137
  end
118
138
  end
119
139
 
@@ -3,7 +3,7 @@ shared_examples_for "select" do
3
3
  before do
4
4
  @session.visit('/form')
5
5
  end
6
-
6
+
7
7
  it "should return value of the first option" do
8
8
  @session.find_field('Title').value.should == 'Mrs'
9
9
  end
@@ -13,6 +13,10 @@ shared_examples_for "select" do
13
13
  @session.find_field('Title').value.should == 'Miss'
14
14
  end
15
15
 
16
+ it "should return the value attribute rather than content if present" do
17
+ @session.find_field('Locale').value.should == 'en'
18
+ end
19
+
16
20
  it "should select an option from a select box by id" do
17
21
  @session.select("Finish", :from => 'form_locale')
18
22
  @session.click_button('awesome')
@@ -25,6 +29,12 @@ shared_examples_for "select" do
25
29
  extract_results(@session)['locale'].should == 'fi'
26
30
  end
27
31
 
32
+ it "should select an option without giving a select box" do
33
+ @session.select("Mr")
34
+ @session.click_button('awesome')
35
+ extract_results(@session)['title'].should == 'Mr'
36
+ end
37
+
28
38
  it "should favour exact matches to option labels" do
29
39
  @session.select("Mr", :from => 'Title')
30
40
  @session.click_button('awesome')
@@ -59,7 +69,7 @@ shared_examples_for "select" do
59
69
 
60
70
  context "with an option that doesn't exist" do
61
71
  it "should raise an error" do
62
- running { @session.select('Does not Exist', :from => 'form_locale') }.should raise_error(Capybara::OptionNotFound)
72
+ running { @session.select('Does not Exist', :from => 'form_locale') }.should raise_error(Capybara::ElementNotFound)
63
73
  end
64
74
  end
65
75
 
@@ -86,6 +96,10 @@ shared_examples_for "select" do
86
96
  @session.click_button('awesome')
87
97
  extract_results(@session)['languages'].should include('Ruby', 'Javascript')
88
98
  end
99
+
100
+ it "should return value attribute rather than content if present" do
101
+ @session.find_field('Underwear').value.should include('thermal')
102
+ end
89
103
  end
90
104
  end
91
105
  end