qat-web 6.0.0

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 (49) hide show
  1. checksums.yaml +7 -0
  2. data/lib/capybara/core_ext.rb +7 -0
  3. data/lib/capybara/core_ext/capybara_error.rb +14 -0
  4. data/lib/capybara/core_ext/node/element.rb +35 -0
  5. data/lib/capybara/core_ext/selenium/node.rb +75 -0
  6. data/lib/headless/core_ext/random_display.rb +15 -0
  7. data/lib/qat/cli/plugins/web.rb +7 -0
  8. data/lib/qat/web.rb +12 -0
  9. data/lib/qat/web/browser.rb +21 -0
  10. data/lib/qat/web/browser/autoload.rb +22 -0
  11. data/lib/qat/web/browser/factory.rb +39 -0
  12. data/lib/qat/web/browser/firefox/harexporttrigger-0.5.0-beta.10.xpi +0 -0
  13. data/lib/qat/web/browser/firefox/loader/helper.rb +49 -0
  14. data/lib/qat/web/browser/html_dump.rb +46 -0
  15. data/lib/qat/web/browser/loader.rb +75 -0
  16. data/lib/qat/web/browser/profile.rb +92 -0
  17. data/lib/qat/web/browser/screenshot.rb +46 -0
  18. data/lib/qat/web/configuration.rb +46 -0
  19. data/lib/qat/web/cucumber.rb +19 -0
  20. data/lib/qat/web/drivers.rb +3 -0
  21. data/lib/qat/web/drivers/chrome.rb +5 -0
  22. data/lib/qat/web/drivers/default.rb +25 -0
  23. data/lib/qat/web/drivers/firefox.rb +6 -0
  24. data/lib/qat/web/drivers/firefox/har_exporter.rb +16 -0
  25. data/lib/qat/web/elements.rb +78 -0
  26. data/lib/qat/web/elements/base.rb +50 -0
  27. data/lib/qat/web/elements/collection.rb +16 -0
  28. data/lib/qat/web/elements/config.rb +19 -0
  29. data/lib/qat/web/elements/element.rb +16 -0
  30. data/lib/qat/web/elements/selector.rb +108 -0
  31. data/lib/qat/web/elements/waiters.rb +103 -0
  32. data/lib/qat/web/error.rb +14 -0
  33. data/lib/qat/web/error/enrichment.rb +14 -0
  34. data/lib/qat/web/exceptions.rb +15 -0
  35. data/lib/qat/web/finders.rb +49 -0
  36. data/lib/qat/web/hooks/common.rb +17 -0
  37. data/lib/qat/web/hooks/har_exporter.rb +32 -0
  38. data/lib/qat/web/hooks/html_dump.rb +21 -0
  39. data/lib/qat/web/hooks/screenshot.rb +20 -0
  40. data/lib/qat/web/page.rb +158 -0
  41. data/lib/qat/web/page/validators.rb +37 -0
  42. data/lib/qat/web/page_manager.rb +71 -0
  43. data/lib/qat/web/screen.rb +2 -0
  44. data/lib/qat/web/screen/autoload.rb +22 -0
  45. data/lib/qat/web/screen/factory.rb +68 -0
  46. data/lib/qat/web/screen/loader.rb +93 -0
  47. data/lib/qat/web/screen/wrapper.rb +96 -0
  48. data/lib/qat/web/version.rb +9 -0
  49. metadata +278 -0
@@ -0,0 +1,108 @@
1
+ require 'qat/web/configuration'
2
+ require 'active_support/core_ext/hash/keys'
3
+
4
+ module QAT
5
+ module Web
6
+ module Elements
7
+ # Helper methods for handling selectors and selector transformations
8
+ module Selector
9
+ include QAT::Web::Configuration
10
+
11
+ # Invalid Configuration Errors
12
+ class InvalidConfigurationError < StandardError
13
+ # Creates a new InvalidConfigurationError
14
+ # @param config [Hash] element selector configuration
15
+ def initialize(config)
16
+ super "Invalid configuration found:\n#{config.ai}"
17
+ end
18
+ end
19
+
20
+ # Returns the selector from configuration
21
+ # @param config [Hash] element selector configuration
22
+ # @param args [Array] (Optional) Substitutions to the XPATH string base in $ syntax
23
+ # @return [Array] selector
24
+ #
25
+ # @example
26
+ # xpath with ".//div[@class='ng-pristine' and text()='$0']//span[contains(text(), '$1')]"
27
+ # args with: ["Some Text", "More Text" ]
28
+ # The final output will be ".//div[@class='ng-pristine' and text()='Some Text']//span[contains(text(), 'More Text')]"
29
+ #
30
+ def selector(config, *args)
31
+ extract_selector(config, *args)
32
+ end
33
+
34
+ private
35
+
36
+ # Extracts the ID or XPATH from the immediate node from the config
37
+ #
38
+ # @param config [Hash] element selector configuration
39
+ # @param args [Array] (Optional) Substitutions to the XPATH string base in $ syntax
40
+ # @return [Array] selector
41
+ #
42
+ # @example
43
+ # xpath with ".//div[@class='ng-pristine' and text()='$0']//span[contains(text(), '$1')]"
44
+ # args with: ["Some Text", "More Text" ]
45
+ # The final output will be ".//div[@class='ng-pristine' and text()='Some Text']//span[contains(text(), 'More Text')]"
46
+ #
47
+ def extract_selector(config, *args)
48
+ type = fetch_type(config)
49
+ value = parse_value(config[type], *args)
50
+ opts = options(config)
51
+ save_config(type, value, opts)
52
+ if opts.any?
53
+ return type.to_sym, value, opts
54
+ else
55
+ return type.to_sym, value
56
+ end
57
+ end
58
+
59
+ # Returns the correct selector type from configuration
60
+ # @param config [Hash] element selector configuration
61
+ # @return [Symbol]
62
+ def fetch_type(config)
63
+ types = Capybara::Selector.all.keys & config.symbolize_keys.keys
64
+
65
+ raise InvalidConfigurationError.new config unless types.size == 1
66
+
67
+ types.first
68
+ end
69
+
70
+ # Parse the selector value for the given configuration through given arguments
71
+ # A list of substitutions will be applied from those arguments
72
+ # @param value [String] selector value
73
+ # @param args [Array] list of substitutions to apply
74
+ # @see #extract_selector
75
+ def parse_value(value, *args)
76
+ if args.empty?
77
+ value
78
+ else
79
+ args.each_with_index do |arg, index|
80
+ value = value.gsub("$#{index}", arg)
81
+ end
82
+ value
83
+ end
84
+ end
85
+
86
+ # Return valid options for Capybara
87
+ # @param config [Hash] element selector configuration
88
+ # @return [Array]
89
+ def options(config)
90
+ config.symbolize_keys.select { |key, _| Capybara::Queries::SelectorQuery::VALID_KEYS.include?(key) }
91
+ end
92
+
93
+ # Saves the last configuration access
94
+ # @param type [Symbol] selector type
95
+ # @param value [String] selector value
96
+ # @param opts [Hash] selector options
97
+ # @return [Hash]
98
+ def save_config(type, value, opts)
99
+ config = { type => value }.merge(opts)
100
+ QAT::Web::Configuration.last_access = config
101
+ @last_access = config
102
+ end
103
+
104
+ extend self
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,103 @@
1
+ require_relative 'selector'
2
+ require_relative 'config'
3
+ require 'retriable'
4
+ require 'qat/logger'
5
+
6
+ module QAT
7
+ module Web
8
+ module Elements
9
+ # Helper methods for handling timeouts and wait periods
10
+ module Waiters
11
+ include QAT::Logger
12
+ include Selector
13
+ include Config
14
+
15
+ # Defines timeouts through a configuration file path
16
+ # @param path [String] path to timeouts configuration file
17
+ # @return [HashWithIndifferentAccess]
18
+ def timeouts_file(path)
19
+ raise(ArgumentError, "File '#{path}' does not exist!") unless File.exist?(path)
20
+ @timeouts_file = path
21
+ begin
22
+ config = YAML.load(File.read(@timeouts_file)) || {}
23
+ rescue Psych::SyntaxError
24
+ log.error "Failed to load file '#{@timeouts_file}'."
25
+ raise
26
+ end
27
+ timeouts_config(config)
28
+ end
29
+
30
+ # Defines timeouts through a configuration hash
31
+ # @param timeouts [Hash] timeouts configuration
32
+ # @return [HashWithIndifferentAccess]
33
+ def timeouts_config(timeouts)
34
+ valid_config?(timeouts, 'timeouts')
35
+ @timeouts = HashWithIndifferentAccess.new(timeouts)
36
+ end
37
+
38
+ # Returns the timeouts configuration
39
+ # @return [HashWithIndifferentAccess]
40
+ def timeouts
41
+ @timeouts
42
+ end
43
+
44
+ # Waits a maximum timeout time until an element is present on page
45
+ # @param selector [Hash] element selector
46
+ # @param timeout [Integer] maximum time to wait
47
+ def wait_until_present(selector, timeout)
48
+ type, value, options = build_selector(selector, { visible: false, wait: timeout })
49
+ has_selector?(type, value, options)
50
+ end
51
+
52
+ # Waits a maximum timeout time until an element is visible on page
53
+ # @param selector [Hash] element selector
54
+ # @param timeout [Integer] maximum time to wait
55
+ def wait_until_visible(selector, timeout)
56
+ type, value, options = build_selector(selector, { visible: true, wait: timeout })
57
+ has_selector?(type, value, options)
58
+ end
59
+
60
+ # Waits a maximum timeout time until an element is no longer present on page
61
+ # @param selector [Hash] element selector
62
+ # @param timeout [Integer] maximum time to wait
63
+ def wait_until_not_present(selector, timeout)
64
+ type, value, options = build_selector(selector, { wait: timeout })
65
+ has_no_selector?(type, value, options)
66
+ end
67
+
68
+ # Waits a maximum timeout time until an element is no longer visible on page
69
+ # @param selector [Hash] element selector
70
+ # @param timeout [Integer] maximum time to wait
71
+ def wait_until_not_visible(selector, timeout)
72
+ type, value, options = build_selector(selector, { visible: false, wait: timeout })
73
+ has_no_selector?(type, value, options)
74
+ end
75
+
76
+ # Generic wait method
77
+ # @param timeout [Integer] max time to wait
78
+ # @param klass [Class] (nil) an optional exception class to retry on error
79
+ # @yield
80
+ def wait_until(timeout, klass = nil, &block)
81
+ default_time = Capybara.default_max_wait_time
82
+ timeout ||= default_time
83
+ tries = timeout / default_time
84
+ exceptions = [Capybara::ElementNotFound, Capybara::ExpectationNotMet]
85
+ exceptions << klass if klass
86
+ Retriable.retriable(on: exceptions,
87
+ tries: tries.ceil,
88
+ base_interval: default_time) do
89
+ yield
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def build_selector(selector, options)
96
+ type, value, opts = *selector
97
+ options = opts.merge(options) if opts&.any?
98
+ [type, value, options]
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,14 @@
1
+ require_relative 'version'
2
+ require_relative 'error/enrichment'
3
+
4
+ module QAT::Web
5
+ #Basic Web error class
6
+ #@since 1.0.0
7
+ class Error < StandardError
8
+ include Enrichment
9
+
10
+ def initialize(msg=nil)
11
+ super(rich_msg(msg))
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ require_relative '../configuration'
2
+ # Module to handle exception enrichment.
3
+ # Should be include in the target exception class to be enriched in runtime.
4
+ module Enrichment
5
+ extend self
6
+
7
+ # Enrichs an exception message with the last configuration access in QAT::Web
8
+ #@param msg [String] exception message to be enriched
9
+ #@return [String]
10
+ def rich_msg(msg)
11
+ access = QAT::Web::Configuration.last_access
12
+ "#{msg}\nQAT::Web last access to configuration was '#{access}'"
13
+ end
14
+ end
@@ -0,0 +1,15 @@
1
+ require 'capybara'
2
+ require 'qat/web/error'
3
+
4
+ module QAT::Web
5
+ module Exceptions
6
+ GLOBAL = [
7
+ QAT::Web::Error,
8
+ Capybara::CapybaraError
9
+ ]
10
+ end
11
+ end
12
+
13
+ if defined?(Selenium)
14
+ QAT::Web::Exceptions::GLOBAL << Selenium::WebDriver::Error::WebDriverError
15
+ end
@@ -0,0 +1,49 @@
1
+ require_relative 'version'
2
+ require_relative 'configuration'
3
+ require 'capybara'
4
+
5
+ module QAT::Web
6
+ #Module with custom finders based on configuration
7
+ #since 1.0.0
8
+ module Finders
9
+ include Configuration
10
+ #Alias to Capybara::Node::Finders#find method with parameter loading from configuration.
11
+ #An +Hash+ should be provided to calculate the correct parameters to pass to the original +find+ method.
12
+ #@param config [Hash] Configuration to extract parameters from
13
+ #@option config [String] :type Selector type (Capybara.default_selector)
14
+ #@option config [String] :value Selector value
15
+ #@see Capybara::Node::Finders#find
16
+ #@see Capybara::Queries::SelectorQuery
17
+ #@return [Capybara::Session] Current session
18
+ #@since 1.0.0
19
+ def find_from_configuration config
20
+ type, value, opts = parse_configuration config
21
+ page.find type, value, opts
22
+ end
23
+
24
+ #Alias to Capybara::Node::Finders#within method with parameter loading from configuration.
25
+ #An +Hash+ should be provided to calculate the correct parameters to pass to the original +within+ method.
26
+ #@param config [Hash] Configuration to extract parameters from
27
+ #@option config [String] :type Selector type (Capybara.default_selector)
28
+ #@option config [String] :value Selector value
29
+ #@see Capybara::Node::Finders#find
30
+ #@see Capybara::Queries::SelectorQuery
31
+ #@return [Capybara::Session] Current session
32
+ #@since 1.0.0
33
+ def within_from_configuration config, &block
34
+ type, value, opts = parse_configuration config
35
+ page.within type.to_sym, value, opts, &block
36
+ end
37
+
38
+ #Alias to Capybara::DSL base method
39
+ #@return [Capybara::Session] Current session
40
+ #@since 1.0.0
41
+ def page
42
+ Capybara.current_session
43
+ end
44
+ end
45
+ end
46
+
47
+ Capybara::Node::Base.include QAT::Web::Finders
48
+ Capybara::Node::Simple.include QAT::Web::Finders
49
+
@@ -0,0 +1,17 @@
1
+ module QAT
2
+ module Web
3
+ module Hooks
4
+ module Common
5
+ def scenario_tag(scenario)
6
+ tag = QAT[:current_test_run_id] if QAT.respond_to?(:[])
7
+
8
+ tag ||= Time.now.strftime("%H%M%S%L")
9
+
10
+ "#{scenario.name.parameterize}_#{tag}"
11
+ end
12
+
13
+ module_function :scenario_tag
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,32 @@
1
+ After do |scenario|
2
+ if scenario.failed? and QAT::Web::Drivers::Firefox::HarExporter::EXCEPTIONS.include?(scenario.exception.class)
3
+
4
+ begin
5
+ log.info 'Saving HAR file'
6
+
7
+ Retriable.retriable(on: Selenium::WebDriver::Error::JavascriptError,
8
+ tries: 30,
9
+ base_interval: 5,
10
+ multiplier: 1,
11
+ rand_factor: 0,
12
+ on_retry: (proc do |exception, _scenario, _duration, _try|
13
+ log.warn 'Error saving HAR!'
14
+ log.debug exception
15
+ end)) do
16
+ result = Capybara.current_session.evaluate_script <<-JS
17
+ HAR.triggerExport({
18
+ token: '#{QAT::Web::Drivers::Firefox::HarExporter::TOKEN}', // Value of the token in your preferences
19
+ fileName: "http_requests_%d-%m-%yT%T", // T%T True if you want to also get HAR data as a string in the callback
20
+ }).then(result => {
21
+ // The local file is available now, result.data is null since options.getData wasn't set.
22
+ });
23
+ JS
24
+ end
25
+
26
+ log.info "Saved HAR file in configured folder."
27
+ rescue => e
28
+ log.error 'Could not save HAR file:'
29
+ log.error e
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,21 @@
1
+ require_relative 'common'
2
+ require 'qat/web/exceptions'
3
+
4
+ module QAT::Web
5
+ module Exceptions
6
+ HTML_DUMP = GLOBAL.dup
7
+ end
8
+ end
9
+
10
+ Before do |scenario|
11
+ QAT::Web::Browser::HTMLDump.html_dump_path = File.join('public', "#{QAT::Web::Hooks::Common.scenario_tag(scenario)}.html")
12
+ end
13
+
14
+ After do |scenario|
15
+ if scenario.failed?
16
+ if QAT::Web::Exceptions::HTML_DUMP.any? { |exception| scenario.exception.kind_of?(exception) }
17
+ # Embeds an existing HTML dump to Cucumber's HTML report
18
+ embed(QAT::Web::Browser::HTMLDump.take_html_dump, 'text/plain', 'HTML dump')
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,20 @@
1
+ require_relative 'common'
2
+ require 'qat/web/exceptions'
3
+
4
+ module QAT::Web
5
+ module Exceptions
6
+ SCREENSHOT = GLOBAL.dup
7
+ end
8
+ end
9
+
10
+ Before do |scenario|
11
+ QAT::Web::Browser::Screenshot.screenshot_path = File.join('public', "#{QAT::Web::Hooks::Common.scenario_tag(scenario)}.png")
12
+ end
13
+
14
+ After do |scenario|
15
+ if scenario.failed?
16
+ if QAT::Web::Exceptions::SCREENSHOT.any? { |exception| scenario.exception.kind_of?(exception) }
17
+ embed QAT::Web::Browser::Screenshot.take_screenshot, 'image/png', 'Screenshot'
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,158 @@
1
+ require_relative 'version'
2
+ require_relative 'elements'
3
+ require_relative 'page/validators'
4
+
5
+ module QAT::Web
6
+
7
+ #Class to represent a Web Page. Works based on a DSL to write user friendly and powerful transition models.
8
+ #Two class methods are provided by this class, in order to allow the mapping of getters for page values, and also map
9
+ #page transitions.
10
+ #The use of Capybara::DSL in the implementation is recommended.
11
+ # @example Sample Page Object implementation for http://localhost:8090/example
12
+ # class ExamplePage < QAT::Web::Page
13
+ # include Capybara::DSL
14
+ #
15
+ # def initialize
16
+ # visit 'http://localhost:8090/example'
17
+ # end
18
+ #
19
+ # get_value :title do
20
+ # find 'h1'
21
+ # end
22
+ #
23
+ # action :more_information, returns: ExampleInformationPage do
24
+ # click_link 'More information...'
25
+ # ExampleInformationPage.new
26
+ # end
27
+ # end
28
+ #
29
+ # current_page = ExamplePage.new
30
+ # current_page.title # 'Example Domain'
31
+ # current_page = current_page.more_information # ExampleInformationPage instance
32
+ #
33
+ #@see http://www.rubydoc.info/github/jnicklas/capybara/master/Capybara/DSL Capybara::DSL
34
+ #@abstract
35
+ #@since 1.0.0
36
+ class Page
37
+ include Elements
38
+ extend Elements
39
+ extend Elements::Waiters
40
+
41
+ class << self
42
+ include Validators
43
+
44
+ #@return [Array<Symbol>] List of value getter functions
45
+ def values
46
+ @values ||= []
47
+ if superclass.ancestors.include? QAT::Web::Page
48
+ @values + superclass.values
49
+ else
50
+ @values
51
+ end
52
+ end
53
+
54
+
55
+ #@return [Hash<Symbol, Array<QAT::Web::Page>>] List of page actions and respective transition return types
56
+ def actions
57
+ @actions ||= {}
58
+ if superclass.ancestors.include? QAT::Web::Page
59
+ superclass.actions.merge @actions
60
+ else
61
+ @actions
62
+ end
63
+ end
64
+
65
+ #Define a new method to get a value from a page. A new instance method will be defined with the given name
66
+ #parameter, and with the block as the method's code to be executed. The name of the method will be added to {QAT::Web::Page#values}
67
+ #@param name [String/Symbol] Name of the funcion to be defined
68
+ #@yield Block of code to define the getter method. The block will only be executed then the function defined by "name" is called
69
+ def get_value name, &block
70
+ name = name.to_sym
71
+ define_method name, &block
72
+ @values ||= []
73
+ @values << name
74
+ end
75
+
76
+ #Define a new method to represent a possible page transition to another page object controller. A new instance method
77
+ #will be defined with the given name parameter, and with the block as the method's code to be executed.
78
+ #Additionally, all possible return types must be delacred in the :returns key in the opts parameter.
79
+ #All return types must be implementarions of QAT::Web::Page.
80
+ #@param name [String/Symbol] Name of the funcion to be defined
81
+ #@param opts [Hash] Options hash
82
+ #@option opts [Class/Array<Class>] :returns Sublass or list of subclasses of QAT::Web::Page that can be returned by the method definition
83
+ #@yield Block of code to define the getter method. The block will only be executed then the function defined by "name" is called
84
+ #@yieldreturn [QAT::Web::Page] Instance of the destination page object
85
+ def action name, opts, &block
86
+
87
+ raise TypeError.new 'The opts parameter should be an Hash with a :returns key' unless opts.is_a? Hash
88
+
89
+ return_value = validate_return_value(opts[:returns])
90
+
91
+ name = name.to_sym
92
+ define_method name, &block
93
+ @actions ||= {}
94
+ @actions[name] = return_value
95
+ end
96
+
97
+ # Loads elements configuration from a configuration file given a file path
98
+ # @param path [String] path to configuration file
99
+ # @return [HashWithIndifferentAccess]
100
+ def elements_file(path)
101
+ raise(ArgumentError, "File '#{path}' does not exist!") unless File.exist?(path)
102
+ @elements_file = path
103
+ elements_config(load_elements_file(@elements_file))
104
+ end
105
+
106
+ # Defines elements through a configuration hash
107
+ # @param elements [Hash] elements configuration
108
+ # @return [HashWithIndifferentAccess]
109
+ def elements_config(elements)
110
+ valid_config?(elements, 'elements')
111
+ @elements = HashWithIndifferentAccess.new(elements)
112
+ end
113
+
114
+ # Returns the elements configuration
115
+ # @return [HashWithIndifferentAccess]
116
+ def elements
117
+ @elements
118
+ end
119
+
120
+ # Defines a web element accessor identified through the given name and configuration
121
+ # Auxiliary methods are also dynamically defined such as the element acessor, waiters and the configuration accessor
122
+ # @param args [Array] One or two arguments are expected. The first argument is the element *name*
123
+ # and the second an optional *configuration*.
124
+ # If none is given, it will be automatically loaded from the loaded configuration by the element name
125
+ # @see Capybara::Node::Finders#find
126
+ def web_element(*args)
127
+ element = QAT::Web::Elements::Element.new(elements, *args)
128
+ define_web_element_methods(element)
129
+ end
130
+
131
+ # Defines a list of web element accessors identified through the given names
132
+ # Configuration will be automatically loaded from the loaded configuration by element name
133
+ # Auxiliary methods are also dynamically defined such as the element acessor, waiters and the configuration accessor
134
+ # @param elements [Array|Symbol] list of names to load elements
135
+ # @see Capybara::Node::Finders#find
136
+ def web_elements(*elements)
137
+ elements.each { |element| web_element(element) }
138
+ end
139
+
140
+ # Defines a collection of web elements identified through a given name and configuration
141
+ # Auxiliary methods are also dynamically defined such as the element acessor, waiters and the configuration accessor
142
+ # @param args [Array] One or two arguments are expected. The first argument is the element *name*
143
+ # and the second an optional *configuration*.
144
+ # If none is given, it will be automatically loaded from the loaded configuration by the element name
145
+ # @see Capybara::Node::Finders#all
146
+ def web_collection(*args)
147
+ collection = QAT::Web::Elements::Collection.new(elements, *args)
148
+ define_web_element_methods(collection)
149
+ end
150
+ end
151
+
152
+ # Returns the elements configuration
153
+ # @return [HashWithIndifferentAccess]
154
+ def elements
155
+ self.class.elements
156
+ end
157
+ end
158
+ end