kelp 0.1.1 → 0.1.2

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 (56) hide show
  1. data/.gitignore +2 -0
  2. data/.yardopts +6 -0
  3. data/Gemfile +4 -9
  4. data/Gemfile.lock +8 -0
  5. data/History.md +22 -0
  6. data/README.md +154 -7
  7. data/kelp.gemspec +1 -1
  8. data/lib/kelp.rb +8 -1
  9. data/lib/kelp/attribute.rb +31 -0
  10. data/lib/kelp/checkbox.rb +31 -0
  11. data/lib/kelp/dropdown.rb +109 -0
  12. data/lib/kelp/field.rb +159 -0
  13. data/lib/kelp/helper.rb +14 -0
  14. data/lib/kelp/navigation.rb +63 -0
  15. data/lib/kelp/scoping.rb +45 -0
  16. data/lib/kelp/visibility.rb +176 -0
  17. data/lib/kelp/xpath.rb +14 -0
  18. data/spec/attribute_spec.rb +56 -0
  19. data/spec/checkbox_spec.rb +69 -0
  20. data/spec/dropdown_spec.rb +176 -0
  21. data/spec/field_spec.rb +290 -0
  22. data/spec/navigation_spec.rb +89 -0
  23. data/spec/scoping_spec.rb +0 -0
  24. data/spec/{capybara/spec_helper.rb → spec_helper.rb} +9 -5
  25. data/spec/test_app/views/form.erb +24 -0
  26. data/spec/visibility_spec.rb +315 -0
  27. data/spec/xpath_spec.rb +0 -0
  28. data/step_definitions/capybara_steps.rb +132 -0
  29. metadata +25 -32
  30. data/docs/Makefile +0 -130
  31. data/docs/_static/custom.css +0 -9
  32. data/docs/conf.py +0 -217
  33. data/docs/development.rst +0 -27
  34. data/docs/future.rst +0 -9
  35. data/docs/index.rst +0 -33
  36. data/docs/make.bat +0 -155
  37. data/docs/testing.rst +0 -15
  38. data/docs/usage.rst +0 -85
  39. data/lib/kelp/capybara.rb +0 -2
  40. data/lib/kelp/capybara/capybara_steps.rb +0 -225
  41. data/lib/kelp/capybara/form_helper.rb +0 -131
  42. data/lib/kelp/capybara/web_helper.rb +0 -148
  43. data/spec/capybara/click_link_in_row_spec.rb +0 -24
  44. data/spec/capybara/dropdown_spec.rb +0 -112
  45. data/spec/capybara/field_should_be_empty_spec.rb +0 -44
  46. data/spec/capybara/field_should_contain_spec.rb +0 -143
  47. data/spec/capybara/fill_in_fields_spec.rb +0 -67
  48. data/spec/capybara/follow_spec.rb +0 -35
  49. data/spec/capybara/page_should_have_spec.rb +0 -48
  50. data/spec/capybara/page_should_not_have_spec.rb +0 -53
  51. data/spec/capybara/press_spec.rb +0 -33
  52. data/spec/capybara/should_be_disabled_spec.rb +0 -28
  53. data/spec/capybara/should_be_enabled_spec.rb +0 -29
  54. data/spec/capybara/should_not_see_spec.rb +0 -97
  55. data/spec/capybara/should_see_in_same_row_spec.rb +0 -41
  56. data/spec/capybara/should_see_spec.rb +0 -80
data/.gitignore CHANGED
@@ -3,3 +3,5 @@
3
3
  coverage
4
4
  docs/_build
5
5
  .rvmrc
6
+ doc
7
+ .yardoc
data/.yardopts ADDED
@@ -0,0 +1,6 @@
1
+ --readme README.md
2
+ --markup markdown
3
+ lib/**/*.rb
4
+ -
5
+ History.md
6
+
data/Gemfile CHANGED
@@ -1,12 +1,7 @@
1
1
  source :rubygems
2
2
 
3
- group :capybara do
4
- gem 'capybara', '>= 0.4.0'
5
- end
3
+ gem 'bundler', '~> 1.0'
4
+
5
+ # Get all other dependencies from the gemspec
6
+ gemspec
6
7
 
7
- group :test do
8
- gem 'sinatra'
9
- gem 'rspec', '>= 2.2.0' # Rails 3
10
- gem 'rspec-rails'
11
- gem 'rcov'
12
- end
data/Gemfile.lock CHANGED
@@ -1,3 +1,9 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ kelp (0.1.2)
5
+ capybara (>= 0.4.0)
6
+
1
7
  GEM
2
8
  remote: http://rubygems.org/
3
9
  specs:
@@ -84,7 +90,9 @@ PLATFORMS
84
90
  ruby
85
91
 
86
92
  DEPENDENCIES
93
+ bundler (~> 1.0)
87
94
  capybara (>= 0.4.0)
95
+ kelp!
88
96
  rcov
89
97
  rspec (>= 2.2.0)
90
98
  rspec-rails
data/History.md ADDED
@@ -0,0 +1,22 @@
1
+ Kelp History
2
+ ============
3
+
4
+ 0.1.2
5
+ -----
6
+
7
+ - Improved namespacing for submodules, so you can be more specific about
8
+ requiring the helpers you need.
9
+ - Improved documentation and formatting, and converted all docs to YARD format.
10
+ - Added new methods fill_in_field, added scoping to field_should(_not)_contain
11
+ - Modified scope_within to use cucumber-rails' selector_for if it's defined
12
+
13
+
14
+ 0.1.1
15
+ -----
16
+
17
+ Initial public release. Includes basic helpers for checking element visibility
18
+ (should_see, should_not_see etc.) as well as navigation (press, follow,
19
+ click_link_in_row), form filling and verification (fill_in_fields,
20
+ fields_should_contain, dropdown_should_equal, etc.), with many of these
21
+ methods supporting a :within scope.
22
+
data/README.md CHANGED
@@ -1,16 +1,163 @@
1
1
  Kelp
2
2
  ====
3
3
 
4
- This project aims to package a collection of useful helper methods for
5
- Cucumber. Currently, the provided methods depend on Capybara, though support
6
- may eventually be added for Webrat, email-spec, or related tools.
4
+ This is the documentation for [Kelp](http://github.com/wapcaplet/kelp), a
5
+ collection of helpers that makes it easier to write step definitions for
6
+ [Cucumber](http://cukes.info). The Kelp gem is hosted on
7
+ [Rubygems](http://rubygems.org/gems/kelp), so you can install it with:
8
+
9
+ $ gem install kelp
10
+
11
+ The name "Kelp" is a contraction of "Cuke Helpers". It was chosen because it's
12
+ short, easy to remember, and is in keeping with the theme of greenish plants.
13
+ Kelp is licensed under the
14
+ [MIT License](http://www.opensource.org/licenses/mit-license.php).
15
+
16
+ Please use the [issue tracker](http://github.com/wapcaplet/kelp/issues)
17
+ to report any bugs or feature requests. Visit the `#kelp` channel on
18
+ `irc.freenode.net` to chat.
19
+
20
+
21
+ Usage
22
+ -----
23
+
24
+ To use Kelp's helpers in your Cucumber step definitions, simply `require` the
25
+ helper module you're interested in:
26
+
27
+ require 'kelp/visibility'
28
+
29
+ Then add the relevant modules to Cucumber's `World`:
30
+
31
+ World(Kelp::Visibility)
32
+
33
+ Many of the provided helpers are designed to make it easier to do things you
34
+ might otherwise be tempted to do with nested step definitions. For example, if
35
+ you need to verify the presence of several text strings on a webpage, you might
36
+ have a step definition like this:
37
+
38
+ Then /^I should see the login page$/ do
39
+ Then %{I should see "Welcome"}
40
+ And %{I should see "Thanks for visiting"}
41
+ And %{I should see "Login"}
42
+ end
43
+
44
+ Using the provided helper method `should_see`, you can do this instead:
45
+
46
+ Then /^I should see the login page$/ do
47
+ should_see "Welcome"
48
+ should_see "Thanks for visiting"
49
+ should_see "Login"
50
+ end
51
+
52
+ Or even this:
53
+
54
+ Then /^I should see the login page$/ do
55
+ should_see [
56
+ "Welcome",
57
+ "Thanks for visiting",
58
+ "Login"
59
+ ]
60
+ end
61
+
62
+ Many of the provided methods are similar to their counterparts in the
63
+ Cucumber-Rails generated step definitions. Following links, filling in fields,
64
+ and pressing buttons can all be easily done with Ruby code instead of nested
65
+ steps. Thus this:
66
+
67
+ When %{I follow "Login"}
68
+ And %{I fill in "Username" with "skroob"}
69
+ And %{I fill in "Password" with "12345"}
70
+ And %{I press "Log me in"}
71
+
72
+ translates to this:
73
+
74
+ follow "Login"
75
+ fill_in_fields \
76
+ "Username" => "skroob",
77
+ "Password" => "12345"
78
+ press "Log me in"
79
+
80
+ Several methods also accept keywords to define the scope of an action. For
81
+ instance, if you want to look within an element with id="greeting"`, do:
82
+
83
+ should_see "Welcome", :within => "#greeting"
84
+
85
+ At the moment, the `:within` keyword is the only accepted scope; the locator
86
+ you pass to this should be in whatever format your `Capybara.default_selector`
87
+ is set to. Other keywords like `:before` or `:after` may be supported in future
88
+ revisions.
89
+
90
+
91
+ Development
92
+ -----------
93
+
94
+ If you'd like to hack on Kelp, first fork the {http://github.com/wapcaplet/kelp
95
+ repository}, then clone your fork:
96
+
97
+ $ git clone git://github.com/your_username/kelp.git
98
+
99
+ Install [bundler](http://gembundler.com/):
100
+
101
+ $ gem install bundler
102
+
103
+ Then install Kelp's dependencies (specified in `Gemfile`):
104
+
105
+ $ cd /path/to/kelp
106
+ $ bundle install
107
+
108
+ It's a good idea to use [RVM](http://rvm.beginrescueend.com/)
109
+ with a new gemset to keep things tidy.
110
+
111
+ If you make changes that you'd like to share, push them into your Kelp fork,
112
+ then [submit a pull request](http://github.com/wapcaplet/kelp/pulls).
113
+
114
+
115
+ Testing
116
+ -------
117
+
118
+ Kelp comes with a `Rakefile`, so you can run the RSpec tests like so:
119
+
120
+ $ rake spec
121
+
122
+ You can also generate an [rcov](http://eigenclass.org/hiki.rb?rcov)
123
+ coverage report via:
124
+
125
+ $ rake rcov
126
+
127
+ This will write an HTML report to `coverage/index.html`.
128
+
129
+
130
+ Future plans
131
+ ------------
132
+
133
+ * Write Cucumber integration tests
134
+ * Support other stuff besides Capybara
135
+ * Turn the project into a proper Rails plugin, with generators
7
136
 
8
- [Read the docs](http://kelp.rtfd.org/) online or view the raw documentation in
9
- the `docs` folder.
10
137
 
11
138
  Copyright
12
139
  ---------
13
140
 
14
- Copyright (c) 2010 Eric Pierce, released under the MIT license.
15
- See MIT-LICENSE for details.
141
+ The MIT License
142
+
143
+ Copyright (c) 2010 Eric Pierce
144
+
145
+ Permission is hereby granted, free of charge, to any person obtaining
146
+ a copy of this software and associated documentation files (the
147
+ "Software"), to deal in the Software without restriction, including
148
+ without limitation the rights to use, copy, modify, merge, publish,
149
+ distribute, sublicense, and/or sell copies of the Software, and to
150
+ permit persons to whom the Software is furnished to do so, subject to
151
+ the following conditions:
152
+
153
+ The above copyright notice and this permission notice shall be
154
+ included in all copies or substantial portions of the Software.
155
+
156
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
157
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
158
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
159
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
160
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
161
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
162
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16
163
 
data/kelp.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = "kelp"
5
- s.version = "0.1.1"
5
+ s.version = "0.1.2"
6
6
  s.summary = "Cucumber helper methods"
7
7
  s.description = <<-EOS
8
8
  Kelp is a collection of helper methods for Cucumber to ease the process of
data/lib/kelp.rb CHANGED
@@ -1 +1,8 @@
1
- require 'kelp/capybara'
1
+ require 'kelp/attribute'
2
+ require 'kelp/checkbox'
3
+ require 'kelp/dropdown'
4
+ require 'kelp/field'
5
+ require 'kelp/navigation'
6
+ require 'kelp/scoping'
7
+ require 'kelp/visibility'
8
+
@@ -0,0 +1,31 @@
1
+ module Kelp
2
+ # This module defines helper methods for verifying attributes of HTML
3
+ # elements on a web page.
4
+ #
5
+ module Attribute
6
+ # Verify that the HTML element with the given ID exists, and is disabled (has
7
+ # the `disabled` attribute).
8
+ #
9
+ # @param [String] element_id
10
+ # HTML `id` attribute of the element that should be disabled
11
+ #
12
+ def should_be_disabled(element_id)
13
+ page.should have_xpath("//*[@id='#{element_id}']")
14
+ page.should have_xpath("//*[@id='#{element_id}' and @disabled]")
15
+ end
16
+
17
+
18
+ # Verify that the HTML element with the given ID exists, and is enabled (does
19
+ # not have the `disabled` attribute).
20
+ #
21
+ # @param [String] element_id
22
+ # HTML `id` attribute of the element that should be enabled
23
+ #
24
+ def should_be_enabled(element_id)
25
+ page.should have_xpath("//*[@id='#{element_id}']")
26
+ page.should have_no_xpath("//*[@id='#{element_id}' and @disabled]")
27
+ end
28
+
29
+ end
30
+
31
+ end
@@ -0,0 +1,31 @@
1
+ require 'kelp/helper'
2
+ require 'kelp/scoping'
3
+
4
+ module Kelp
5
+ module Checkbox
6
+ include Scoping
7
+ include Helper
8
+
9
+ def checkbox_should_be_checked(checkbox, scope={})
10
+ in_scope(scope) do
11
+ field_checked = find_field(checkbox)['checked']
12
+ if field_checked.respond_to? :should
13
+ field_checked.should be_true
14
+ else
15
+ assert field_checked
16
+ end
17
+ end
18
+ end
19
+
20
+ def checkbox_should_not_be_checked(checkbox, scope={})
21
+ in_scope(scope) do
22
+ field_checked = find_field(checkbox)['checked']
23
+ if field_checked.respond_to? :should
24
+ field_checked.should be_false
25
+ else
26
+ assert !field_checked
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,109 @@
1
+ require 'kelp/scoping'
2
+ require 'kelp/helper'
3
+
4
+ module Kelp
5
+ # This module defines methods for working with dropdown fields in a web form,
6
+ # including verifying their visible content as well as the `value` attribute
7
+ # of selected options.
8
+ #
9
+ module Dropdown
10
+ include Scoping
11
+ include Helper
12
+
13
+ # Verify that the selected option in a dropdown has the given
14
+ # value. Note that this is the *visible* content of the dropdown
15
+ # (the content of the <option> element), rather than the
16
+ # 'value' attribute of the option.
17
+ #
18
+ # @param [String] dropdown
19
+ # Capybara locator for the dropdown (the `select` element)
20
+ # @param [String] value
21
+ # Value you expect to see as the currently-selected option
22
+ #
23
+ def dropdown_should_equal(dropdown, value)
24
+ field = nice_find_field(dropdown)
25
+ # See if there's a 'selected' option
26
+ begin
27
+ selected = field.find(:xpath, ".//option[@selected='selected']")
28
+ # If not, find the option matching the first field value
29
+ rescue Capybara::ElementNotFound
30
+ selected = field.find(:xpath, ".//option[@value='#{field.value.first}']")
31
+ end
32
+ selected.text.should =~ /#{value}/
33
+ end
34
+
35
+
36
+ # Verify that a given dropdown includes all of the given strings.
37
+ # This looks for the visible values in the dropdown, *not* the 'value'
38
+ # attribute of each option.
39
+ #
40
+ # @example
41
+ # dropdown_should_include "Weekday", "Monday"
42
+ # dropdown_should_include "Size", ["Small", "Medium", "Large"]
43
+ #
44
+ # @param [String] dropdown
45
+ # Capybara locator for the dropdown (the `select` element)
46
+ # @param [Array] values
47
+ # Visible contents you expect to be able to select from the dropdown
48
+ # @param [Hash] scope
49
+ # Scoping keywords as understood by {#in_scope}
50
+ #
51
+ def dropdown_should_include(dropdown, values, scope={})
52
+ in_scope(scope) do
53
+ # If values is a String, convert it to an Array
54
+ values = [values] if values.class == String
55
+
56
+ field = nice_find_field(dropdown)
57
+ # Look for each value
58
+ values.each do |value|
59
+ page.should have_xpath(".//option[text()='#{value}']")
60
+ end
61
+ end
62
+ end
63
+
64
+
65
+ # Verify that a given dropdown does not include any of the given strings.
66
+ # This looks for the visible values in the dropdown, *not* the 'value'
67
+ # attribute of each option.
68
+ #
69
+ # @param [String] dropdown
70
+ # Capybara locator for the dropdown (the `select` element)
71
+ # @param [Array] values
72
+ # Visible contents you do not want to see in the dropdown
73
+ # @param [Hash] scope
74
+ # Scoping keywords as understood by {#in_scope}
75
+ #
76
+ # @since 0.1.2
77
+ #
78
+ def dropdown_should_not_include(dropdown, values, scope={})
79
+ in_scope(scope) do
80
+ # If values is a String, convert it to an Array
81
+ values = [values] if values.class == String
82
+
83
+ field = nice_find_field(dropdown)
84
+ # Look for each value
85
+ values.each do |value|
86
+ page.should have_no_xpath(".//option[text()='#{value}']")
87
+ end
88
+ end
89
+ end
90
+
91
+
92
+ # Verify that a dropdown currently has the option with the given `value`
93
+ # attribute selected. Note that this differs from {#dropdown_should_equal},
94
+ # in that it looks at the actual `value` attribute of the selected option,
95
+ # rather than its visible contents.
96
+ #
97
+ # @param [String] dropdown
98
+ # Capybara locator for the dropdown (the `select` element)
99
+ # @param [String] value
100
+ # Expected `value` attribute of the selected `option`
101
+ #
102
+ def dropdown_value_should_equal(dropdown, value)
103
+ # FIXME: When this returns False, does that fail the step?
104
+ field = find_field(dropdown)
105
+ field.value.should include(value)
106
+ end
107
+
108
+ end
109
+ end
data/lib/kelp/field.rb ADDED
@@ -0,0 +1,159 @@
1
+ require 'kelp/helper'
2
+ require 'kelp/scoping'
3
+
4
+ module Kelp
5
+ # This module defines helper methods for filling in and verifying the content
6
+ # of fields in a web form.
7
+ #
8
+ module Field
9
+ include Scoping
10
+ include Helper
11
+
12
+ # Fill in multiple fields according to values in a `Hash`.
13
+ #
14
+ # @example
15
+ # fill_in_fields "First name" => "Otto", "Last name" => "Scratchansniff"
16
+ # fill_in_fields "phone" => "303-224-7428", :within => "#home"
17
+ #
18
+ # @param [Hash] fields
19
+ # "field" => "value" for each field to fill in
20
+ # @param [Hash] scope
21
+ # Scoping keywords as understood by {#in_scope}
22
+ #
23
+ def fill_in_fields(fields, scope={})
24
+ in_scope(scope) do
25
+ fields.each do |name, value|
26
+ fill_in name, :with => value
27
+ end
28
+ end
29
+ end
30
+
31
+
32
+ # Fill in a single field within the scope of a given selector.
33
+ def fill_in_field(field, value, scope={})
34
+ fields = {field => value}
35
+ fill_in_fields fields, scope
36
+ end
37
+
38
+
39
+ # Fill in multiple fields within the scope of a given selector.
40
+ # Alias for:
41
+ #
42
+ # fill_in_fields fields, :within => selector
43
+ #
44
+ def fill_in_fields_within(selector, fields)
45
+ fill_in_fields fields, :within => selector
46
+ end
47
+
48
+
49
+ # Fill in a single fields within the scope of a given selector.
50
+ # Alias for:
51
+ #
52
+ # fill_in_field field, value, :within => selector
53
+ #
54
+ def fill_in_field_within(selector, field, value)
55
+ fill_in_field field, value, :within => selector
56
+ end
57
+
58
+
59
+ # Verify that the given field is empty or nil.
60
+ def field_should_be_empty(field)
61
+ _field = nice_find_field(field)
62
+ if _field.nil? || _field.value.nil?
63
+ return true
64
+ else
65
+ raise RuntimeError, "Expected field '#{field}' to be empty, but value is '#{_field.value}'"
66
+ end
67
+ end
68
+
69
+
70
+ # Verify that the given field contains the given value.
71
+ #
72
+ # @param [String] field
73
+ # Capybara locator for the field (name, id, or label text)
74
+ # @param [String] value
75
+ # Value you expect to see in the text field
76
+ #
77
+ def field_should_contain(field, value, scope={})
78
+ in_scope(scope) do
79
+ # TODO: Make this work equally well with any kind of field
80
+ # (text, single-select, multi-select)
81
+ field = find_field(field)
82
+ field_value = (field.tag_name == 'textarea') ? field.text : field.value
83
+ # If field value is an Array, take the first item
84
+ if field_value.class == Array
85
+ field_value = field_value.first
86
+ end
87
+ # Escape any problematic characters in the expected value
88
+ value = Regexp.escape(value)
89
+ # Match actual to expected
90
+ if field_value.respond_to? :should
91
+ field_value.should =~ /#{value}/
92
+ else
93
+ assert_match(/#{value}/, field_value)
94
+ end
95
+ end
96
+ end
97
+
98
+
99
+ def field_should_not_contain(field, value, scope={})
100
+ raise "Not implemented yet"
101
+ #with_scope(parent) do
102
+ #field = find_field(field)
103
+ #field_value = (field.tag_name == 'textarea') ? field.text : field.value
104
+ #if field_value.respond_to? :should_not
105
+ #field_value.should_not =~ /#{value}/
106
+ #else
107
+ #assert_no_match(/#{value}/, field_value)
108
+ #end
109
+ #end
110
+ end
111
+
112
+
113
+ # Verify the values of multiple fields given as a Hash.
114
+ #
115
+ # @param [Hash] field_values
116
+ # "field" => "value" for each field you want to verify
117
+ # @param [Hash] scope
118
+ # Scoping keywords as understood by {#in_scope}
119
+ #
120
+ def fields_should_contain(field_values, scope={})
121
+ in_scope(scope) do
122
+ field_values.each do |field, value|
123
+ _field = find_field(field)
124
+ # For nil/empty, check for nil field or nil value
125
+ if value.nil? or value.empty?
126
+ field_should_be_empty(field)
127
+ # If field is a dropdown
128
+ elsif _field.tag_name == 'select'
129
+ dropdown_should_equal(field, value)
130
+ # Otherwise treat as a text field
131
+ else
132
+ field_should_contain(field, value)
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+
139
+ # Verify a single field within the scope of a given selector.
140
+ # Alias for:
141
+ #
142
+ # field_should_contain field, value, :within => selector
143
+ #
144
+ def field_should_contain_within(selector, field, value)
145
+ field_should_contain field, value, :within => selector
146
+ end
147
+
148
+
149
+ # Verify fields within the scope of a given selector.
150
+ # Alias for:
151
+ #
152
+ # fields_should_contain field_values, :within => selector
153
+ #
154
+ def fields_should_contain_within(selector, field_values)
155
+ fields_should_contain field_values, :within => selector
156
+ end
157
+
158
+ end
159
+ end