macros4cuke 0.0.02 → 0.1.00
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.
- data/features/demo01.feature +4 -6
- data/features/demo02.feature +4 -6
- data/features/demo03.feature +5 -8
- data/features/demo04.feature +4 -6
- data/features/support/env.rb +0 -1
- data/lib/macro_steps.rb +24 -49
- data/lib/macros4cuke/constants.rb +1 -1
- data/lib/macros4cuke/exceptions.rb +46 -0
- data/lib/macros4cuke/macro-collection.rb +81 -0
- data/lib/macros4cuke/macro-step-support.rb +29 -27
- data/lib/macros4cuke/macro-step.rb +12 -9
- metadata +6 -4
data/features/demo01.feature
CHANGED
@@ -4,14 +4,12 @@ Feature: Show the use of a basic macro
|
|
4
4
|
As a Cuke user
|
5
5
|
So that I enjoy writing scenario.
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
# the macros will be available to every scenario in the feature file.
|
10
|
-
Background:
|
7
|
+
|
8
|
+
Scenario: Definition of a simple macro-step
|
11
9
|
# The next step creates a macro(-step)
|
12
|
-
# The syntax of the new macro-step is specified between
|
10
|
+
# The syntax of the new macro-step is specified between double quotes.
|
13
11
|
# The steps to execute when the macro is used/invoked are listed in the multiline triple quotes arguments.
|
14
|
-
Given I define the step
|
12
|
+
Given I define the step "When I [log in]" to mean:
|
15
13
|
"""
|
16
14
|
Given I landed in the homepage
|
17
15
|
When I click "Sign in"
|
data/features/demo02.feature
CHANGED
@@ -4,15 +4,13 @@ Feature: Show the use of a basic macro with one argument
|
|
4
4
|
As a Cuke user
|
5
5
|
So that I enjoy writing scenario.
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
# the macros will be available to every scenario in the feature file.
|
10
|
-
Background:
|
7
|
+
|
8
|
+
Scenario: Creating a basic scenario with one argument
|
11
9
|
# The next step creates a macro(-step)
|
12
|
-
# The syntax of the new macro-step is specified between the
|
10
|
+
# The syntax of the new macro-step is specified between the double quotes.
|
13
11
|
# The steps to execute when the macro is used/invoked are listed in the multiline triple quotes arguments.
|
14
12
|
# The macro argument is put between double(triple) curly braces {{...}} as required by the Mustache template library.
|
15
|
-
Given I define the step
|
13
|
+
Given I define the step "When I [log in as {{userid}}]" to mean:
|
16
14
|
"""
|
17
15
|
Given I landed in the homepage
|
18
16
|
When I click "Sign in"
|
data/features/demo03.feature
CHANGED
@@ -4,15 +4,12 @@ Feature: Show the use of a basic macro with multiple arguments
|
|
4
4
|
As a Cuke user
|
5
5
|
So that I enjoy writing scenario.
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
#
|
10
|
-
Background:
|
11
|
-
# The next step creates a macro(-step)
|
12
|
-
# The syntax of the new macro-step is specified between the < ... > delimiters.
|
7
|
+
|
8
|
+
Scenario: defining basic macro with multiple arguments
|
9
|
+
# The next step creates a macro(-step)double quotes.
|
13
10
|
# The steps to execute when the macro is used/invoked are listed in the multiline triple quotes arguments.
|
14
11
|
# The macro argument is put between double(triple) curly braces {{...}} as required by the Mustache template library.
|
15
|
-
Given I define the step
|
12
|
+
Given I define the step "When I [enter my userid {{userid}} and password {{password}}]" to mean:
|
16
13
|
"""
|
17
14
|
Given I landed in the homepage
|
18
15
|
When I click "Sign in"
|
@@ -36,7 +33,7 @@ Invoked step: ... I click "Submit"
|
|
36
33
|
"""
|
37
34
|
|
38
35
|
Scenario: A macro invoking another macro (YES, it's possible!)
|
39
|
-
Given I define the step
|
36
|
+
Given I define the step "When I [enter my credentials]" to mean:
|
40
37
|
"""
|
41
38
|
{{! Notice that the next step is invoking the first macro above}}
|
42
39
|
When I [enter my userid "guest" and password "unguessable"]
|
data/features/demo04.feature
CHANGED
@@ -4,15 +4,13 @@ Feature: Show the use of a macro with multiple arguments in a table
|
|
4
4
|
As a Cuke user
|
5
5
|
So that I enjoy writing scenario.
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
# the macros will be available to every scenario in the feature file.
|
10
|
-
Background:
|
7
|
+
|
8
|
+
Scenario: Defining a macro to be used with multiple arguments in a table
|
11
9
|
# The next step creates a macro(-step)
|
12
|
-
# The syntax of the new macro-step is specified between
|
10
|
+
# The syntax of the new macro-step is specified between double quotes.
|
13
11
|
# The steps to execute when the macro is used/invoked are listed in the multiline triple quotes arguments.
|
14
12
|
# The macro argument is put between double(triple) curly braces {{...}} as required by the Mustache template library.
|
15
|
-
Given I define the step
|
13
|
+
Given I define the step "When I [enter my credentials as]:" to mean:
|
16
14
|
"""
|
17
15
|
Given I landed in the homepage
|
18
16
|
When I click "Sign in"
|
data/features/support/env.rb
CHANGED
data/lib/macro_steps.rb
CHANGED
@@ -3,69 +3,44 @@
|
|
3
3
|
|
4
4
|
|
5
5
|
|
6
|
-
|
7
|
-
Example:
|
8
|
-
Given I define the step
|
9
|
-
"""
|
10
|
-
|
11
|
-
When I
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
"""
|
16
|
-
|
17
|
-
Given(/^I define the step
|
6
|
+
# This step is used to define a macro-step
|
7
|
+
# Example:
|
8
|
+
# Given I define the step "When I [log in as {{userid}}]" to mean:
|
9
|
+
# """
|
10
|
+
# Given I landed in the homepage
|
11
|
+
# When I click "Sign in"
|
12
|
+
# And I fill in "Username" with "{{userid}}"
|
13
|
+
# And I fill in "Password" with "unguessable"
|
14
|
+
# And I click "Submit"
|
15
|
+
# """
|
16
|
+
|
17
|
+
Given(/^I define the step "When I \[([^\]]+\]:?)" to mean:$/) do |macro_phrase, template|
|
18
18
|
add_macro(macro_phrase, template)
|
19
19
|
end
|
20
20
|
|
21
|
-
|
21
|
+
# This step is used to invoke a simple macro-step
|
22
22
|
# Example:
|
23
|
-
|
24
|
-
|
25
|
-
"""
|
26
|
-
When I display the text "Departure: {{origin}}."
|
27
|
-
When I display the text "Stop at: {{waypoint}}."
|
28
|
-
When I display the text "Destination {{destination}}."
|
29
|
-
"""
|
30
|
-
=end
|
23
|
+
# When I [log in as "guest"]
|
24
|
+
#
|
31
25
|
When(/^I \[([^\]]+\])$/) do |macro_phrase|
|
32
|
-
|
33
|
-
raise StandardError, "Undefined macro step for '[#{macro_phrase}'." if macro.nil?
|
34
|
-
|
35
|
-
# Retrieve macro argument names and their associated value from the table
|
36
|
-
params = macro.validate_params(macro_phrase, nil)
|
37
|
-
|
38
|
-
# Render the steps
|
39
|
-
rendered_steps = macro.expand(params)
|
40
|
-
|
41
|
-
# Execute the steps
|
42
|
-
steps(rendered_steps)
|
26
|
+
invoke_macro(macro_phrase) # This will call the macro with the given phrase
|
43
27
|
end
|
44
28
|
|
45
29
|
|
46
30
|
# This step is used to invoke a macro-step with a table argument.
|
47
31
|
# Example:
|
48
|
-
# When I [
|
49
|
-
# |
|
50
|
-
# |
|
51
|
-
# |street| Main street|
|
52
|
-
# |street3| Small street|
|
32
|
+
# When I [enter my credentials as]:
|
33
|
+
# |userid |guest |
|
34
|
+
# |password|unguessable|
|
53
35
|
When(/^I \[([^\]]+\]:)$/) do |macro_phrase, table_argument|
|
54
|
-
|
55
|
-
raise StandardError, "Undefined macro step for '#{macro_phrase}'." if macro.nil?
|
56
|
-
|
36
|
+
# Ensure that the second argument is of the correct type
|
57
37
|
unless table_argument.kind_of?(Cucumber::Ast::Table)
|
58
|
-
raise StandardError, "This step must have a table as an argument."
|
38
|
+
raise StandardError, "This step must have a data table as an argument."
|
59
39
|
end
|
60
40
|
|
61
|
-
#
|
62
|
-
|
63
|
-
|
64
|
-
# Render the steps
|
65
|
-
rendered_steps = macro.expand(params)
|
66
|
-
|
67
|
-
# Execute the steps
|
68
|
-
steps(rendered_steps)
|
41
|
+
# This will call the macro with the given phrase.
|
42
|
+
# The second argument consists of a hash with pairs of the kind: argument name => actual value
|
43
|
+
invoke_macro(macro_phrase, table_argument.rows_hash())
|
69
44
|
end
|
70
45
|
|
71
46
|
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# encoding: utf-8 -- You should see a paragraph character: §
|
2
|
+
# File: exceptions.rb
|
3
|
+
|
4
|
+
module Macros4Cuke # Module used as a namespace
|
5
|
+
|
6
|
+
# Base class for any exception explicitly raised in the Macros4Cuke methods.
|
7
|
+
class Macros4CukeError < StandardError
|
8
|
+
end # class
|
9
|
+
|
10
|
+
# Raised when one attempts to define a new macro
|
11
|
+
# that has the same phrase as an existing macro.
|
12
|
+
class DuplicateMacroError < Macros4CukeError
|
13
|
+
def initialize(aPhrase)
|
14
|
+
super("A macro-step with phrase '[#{aPhrase}' already exist.")
|
15
|
+
end
|
16
|
+
end # class
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
# Raised when one invokes a macro-step with an unknown phrase.
|
21
|
+
class UnknownMacroError < Macros4CukeError
|
22
|
+
def initialize(aPhrase)
|
23
|
+
super("Unknown macro-step with phrase: '[#{aPhrase}'.")
|
24
|
+
end
|
25
|
+
end # class
|
26
|
+
|
27
|
+
|
28
|
+
|
29
|
+
# Raised when one invokes a macro-step with an argument
|
30
|
+
# that has an unknown name.
|
31
|
+
class UnknownArgumentError < Macros4CukeError
|
32
|
+
def initialize(argName)
|
33
|
+
super("Unknown macro argument #{argName}.")
|
34
|
+
end
|
35
|
+
end # class
|
36
|
+
|
37
|
+
|
38
|
+
# Raised when Macros4Cuke encountered an issue
|
39
|
+
# that it can't handle properly.
|
40
|
+
class InternalError < Macros4CukeError
|
41
|
+
end # class
|
42
|
+
|
43
|
+
|
44
|
+
end # module
|
45
|
+
|
46
|
+
# End of file
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# File: macro-collection.rb
|
2
|
+
# Purpose: Implementation of the MacroCollection class.
|
3
|
+
|
4
|
+
require 'singleton' # We'll use the Singleton design pattern for this class.
|
5
|
+
require_relative "macro-step"
|
6
|
+
|
7
|
+
module Macros4Cuke # Module used as a namespace
|
8
|
+
|
9
|
+
# Represents a container of macros.
|
10
|
+
# It gather all the macros encountered by Cucumber while "executing" the feature files.
|
11
|
+
class MacroCollection
|
12
|
+
include Singleton # Use the Singleton design pattern.
|
13
|
+
|
14
|
+
# A Hash with pairs of the form: phrase => MacroStep object
|
15
|
+
attr_reader(:macro_steps)
|
16
|
+
|
17
|
+
# Init the pool if it was not done yet.
|
18
|
+
def init()
|
19
|
+
@macro_steps = {} if @macro_steps.nil?
|
20
|
+
end
|
21
|
+
|
22
|
+
public
|
23
|
+
# Return true iff the host has a macro with the given key.
|
24
|
+
def has_macro?(aPhrase, mode)
|
25
|
+
key = MacroStep::macro_key(aPhrase, mode)
|
26
|
+
return @macro_steps.include? key
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
# Add a new macro.
|
31
|
+
# Pre-condition: there is no existing macro with the same key.
|
32
|
+
# [aPhrase] The text that is enclosed between the square brackets.
|
33
|
+
# [aTemplate] A text that consists of a sequence of Cucumber steps.
|
34
|
+
# These steps
|
35
|
+
def add_macro(aPhrase, aTemplate)
|
36
|
+
new_macro = MacroStep.new(aPhrase, aTemplate)
|
37
|
+
|
38
|
+
# Prevent collision of macros (macros with same phrase).
|
39
|
+
# This can occur if a macro was defined in a background section.
|
40
|
+
# An exception is raised if the phrase syntax of both macros are the
|
41
|
+
if find_macro(aPhrase)
|
42
|
+
pp find_macro(aPhrase)
|
43
|
+
raise DuplicateMacroError.new(aPhrase)
|
44
|
+
end
|
45
|
+
|
46
|
+
@macro_steps[new_macro.name] = new_macro
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
# Render the steps associated to the macro with given phrase
|
51
|
+
# and (optionally) given a table of values.
|
52
|
+
# Return the rendered steps as a text.
|
53
|
+
# [aPhrase] an instance of the macro phrase.
|
54
|
+
# [rawData] An Array of couples.
|
55
|
+
# Each couple is of the form: argument name, a value.
|
56
|
+
# Multiple rows with same argument name are acceptable.
|
57
|
+
def render_steps(aPhrase, rawData = nil)
|
58
|
+
|
59
|
+
macro = find_macro(aPhrase)
|
60
|
+
raise UnknownMacroError.new(aPhrase) if macro.nil?
|
61
|
+
|
62
|
+
# Retrieve macro argument names and their associated value from the table
|
63
|
+
params = macro.validate_params(aPhrase, rawData)
|
64
|
+
|
65
|
+
# Render the steps
|
66
|
+
rendered_steps = macro.expand(params)
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
# Retrieve the macro, given a phrase.
|
72
|
+
def find_macro(aMacroPhrase)
|
73
|
+
return @macro_steps[MacroStep::macro_key(aMacroPhrase, :invokation)]
|
74
|
+
end
|
75
|
+
|
76
|
+
end # class
|
77
|
+
|
78
|
+
end # module
|
79
|
+
|
80
|
+
|
81
|
+
# End of file
|
@@ -1,46 +1,48 @@
|
|
1
1
|
# File: macro-step-support.rb
|
2
2
|
|
3
|
-
require_relative "
|
3
|
+
require_relative "exceptions"
|
4
|
+
require_relative "macro-collection"
|
4
5
|
|
5
6
|
module Macros4Cuke # Module used as a namespace
|
6
7
|
|
7
|
-
# Mix-in module that should be extending World objects
|
8
|
-
# Synopsis (in env.rb):
|
9
|
-
|
8
|
+
# Mix-in module that should be extending World objects in Cucumber.
|
9
|
+
# Synopsis (in env.rb):
|
10
|
+
# require 'macros4cuke'
|
11
|
+
# ...
|
12
|
+
# World(Macros4Cuke::MacroStepSupport) # Extend the world object with this module.
|
13
|
+
#
|
14
|
+
module MacroStepSupport
|
10
15
|
|
11
16
|
# Callback invoked when a World object is extend(ed) with this module.
|
12
17
|
def self.extended(world)
|
13
|
-
# Add & initialize an instance variable for macro support.
|
14
|
-
|
18
|
+
# Add & initialize an instance variable for macro support.
|
19
|
+
MacroCollection::instance.init()
|
15
20
|
end
|
16
21
|
|
17
|
-
|
22
|
+
|
18
23
|
public
|
19
|
-
|
20
|
-
def clear_macro_steps()
|
21
|
-
@macro_steps = {}
|
22
|
-
end
|
23
|
-
|
24
|
-
# Return true iff the host has a macro with the given key.
|
25
|
-
def has_macro?(aMacroPhrase, mode)
|
26
|
-
key = MacroStep::macro_key(aMacroPhrase, mode)
|
27
|
-
return @macro_steps.include? key
|
28
|
-
end
|
29
|
-
|
24
|
+
|
30
25
|
# Add a new macro.
|
31
26
|
# Pre-condition: there is no existing macro with the same key.
|
27
|
+
# [aPhrase] The text that is enclosed between the square brackets.
|
28
|
+
# [aTemplate] A text that consists of a sequence of Cucumber steps.
|
29
|
+
# These steps
|
32
30
|
def add_macro(aPhrase, aTemplate)
|
33
|
-
|
34
|
-
raise StandardError, "Macro step for '[#{aPhrase}' already exist."
|
35
|
-
else
|
36
|
-
new_macro = MacroStep.new(aPhrase, aTemplate)
|
37
|
-
@macro_steps[new_macro.name] = new_macro
|
38
|
-
end
|
31
|
+
MacroCollection::instance.add_macro(aPhrase, aTemplate)
|
39
32
|
end
|
33
|
+
|
40
34
|
|
41
|
-
#
|
42
|
-
|
43
|
-
|
35
|
+
# Invoke a macro with given phrase and (optionally) a table of values
|
36
|
+
# [aPhrase] an instance of the macro phrase.
|
37
|
+
# [rawData] An Array of couples.
|
38
|
+
# Each couple is of the form: argument name, a value.
|
39
|
+
# Multiple rows with same argument name are acceptable.
|
40
|
+
def invoke_macro(aPhrase, rawData = nil)
|
41
|
+
# Generate a text rendition of the step to be executed.
|
42
|
+
rendered_steps = MacroCollection::instance.render_steps(aPhrase, rawData)
|
43
|
+
|
44
|
+
# Execute the steps
|
45
|
+
steps(rendered_steps)
|
44
46
|
end
|
45
47
|
|
46
48
|
end # module
|
@@ -5,7 +5,8 @@ require 'mustache' # Load the Mustache template library
|
|
5
5
|
|
6
6
|
module Macros4Cuke # Module used as a namespace
|
7
7
|
|
8
|
-
# In essence, a macro step object represents a Cucumber step that
|
8
|
+
# In essence, a macro step object represents a Cucumber step that is itself
|
9
|
+
# an aggregation of lower-level Cucumber steps.
|
9
10
|
class MacroStep
|
10
11
|
# A Mustache instance that expands the steps upon request.
|
11
12
|
attr_reader(:renderer)
|
@@ -18,6 +19,7 @@ class MacroStep
|
|
18
19
|
|
19
20
|
# The list of macro argument names (as appearing in the Mustache template and in the macro phrase).
|
20
21
|
attr_reader(:args)
|
22
|
+
|
21
23
|
|
22
24
|
# Constructor.
|
23
25
|
# [aMacroPhrase]
|
@@ -106,7 +108,7 @@ class MacroStep
|
|
106
108
|
|
107
109
|
unless rawData.nil?
|
108
110
|
rawData.each do |(key, value)|
|
109
|
-
raise
|
111
|
+
raise UnknownArgumentError.new(key) unless @args.include? key
|
110
112
|
if macro_parameters.include? key
|
111
113
|
if macro_parameters[key].kind_of?(Array)
|
112
114
|
macro_parameters[key] << value
|
@@ -122,6 +124,7 @@ class MacroStep
|
|
122
124
|
return macro_parameters
|
123
125
|
end
|
124
126
|
|
127
|
+
|
125
128
|
|
126
129
|
private
|
127
130
|
# Retrieve from the macro phrase, all the text between "mustaches" or double quotes.
|
@@ -140,7 +143,7 @@ private
|
|
140
143
|
when :invokation
|
141
144
|
/"([^"]*)"/
|
142
145
|
else
|
143
|
-
raise
|
146
|
+
raise InternalError, "Internal error: Unknown mode argument #{mode}"
|
144
147
|
end
|
145
148
|
raw_result = aMacroPhrase.scan(pattern)
|
146
149
|
return raw_result.flatten.compact
|
@@ -152,7 +155,7 @@ private
|
|
152
155
|
def add_tags_multi(tokens)
|
153
156
|
first_token = tokens.shift
|
154
157
|
unless first_token == :multi
|
155
|
-
raise
|
158
|
+
raise InternalError, "Expecting a :multi token instead of a #{first_token}"
|
156
159
|
end
|
157
160
|
|
158
161
|
tokens.each do |an_opcode|
|
@@ -166,7 +169,7 @@ private
|
|
166
169
|
when String
|
167
170
|
#Do nothing...
|
168
171
|
else
|
169
|
-
raise
|
172
|
+
raise InternalError, "Unknown Mustache token type #{an_opcode.first}"
|
170
173
|
end
|
171
174
|
end
|
172
175
|
end
|
@@ -178,8 +181,8 @@ private
|
|
178
181
|
case mustache_opcode[0]
|
179
182
|
when :etag
|
180
183
|
triplet = mustache_opcode[1]
|
181
|
-
raise
|
182
|
-
raise
|
184
|
+
raise InternalError, "expected 'mustache' token instead of '#{triplet[0]}'" unless triplet[0] == :mustache
|
185
|
+
raise InternalError, "expected 'fetch' token instead of '#{triplet[1]}'" unless triplet[1] == :fetch
|
183
186
|
@args << triplet.last
|
184
187
|
|
185
188
|
when :fetch
|
@@ -189,7 +192,7 @@ private
|
|
189
192
|
add_tags_section(mustache_opcode)
|
190
193
|
|
191
194
|
else
|
192
|
-
raise
|
195
|
+
raise InternalError, "Unknown Mustache token type #{mustache_opcode.first}"
|
193
196
|
end
|
194
197
|
end
|
195
198
|
|
@@ -208,7 +211,7 @@ private
|
|
208
211
|
when String
|
209
212
|
return
|
210
213
|
else
|
211
|
-
raise
|
214
|
+
raise InternalError, "Unknown Mustache token type #{op.first}"
|
212
215
|
end
|
213
216
|
end
|
214
217
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: macros4cuke
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.00
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-04-
|
12
|
+
date: 2013-04-22 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rake
|
@@ -60,8 +60,8 @@ dependencies:
|
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: '0'
|
62
62
|
description: ! "\tMacros4Cuke is a lightweight library that adds a macro facility
|
63
|
-
your Cucumber scenarios.\n In short, you
|
64
|
-
|
63
|
+
your Cucumber scenarios.\n In short, you can create new steps that replace a sequence
|
64
|
+
of lower-level steps. \n"
|
65
65
|
email: famished.tiger@yahoo.com
|
66
66
|
executables: []
|
67
67
|
extensions: []
|
@@ -74,6 +74,8 @@ files:
|
|
74
74
|
- lib/macros4cuke.rb
|
75
75
|
- lib/macro_steps.rb
|
76
76
|
- lib/macros4cuke/constants.rb
|
77
|
+
- lib/macros4cuke/exceptions.rb
|
78
|
+
- lib/macros4cuke/macro-collection.rb
|
77
79
|
- lib/macros4cuke/macro-step-support.rb
|
78
80
|
- lib/macros4cuke/macro-step.rb
|
79
81
|
- features/demo01.feature
|