howitzer 2.1.1 → 2.2.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.
- checksums.yaml +5 -5
- data/.rubocop.yml +27 -18
- data/.travis.yml +4 -3
- data/CHANGELOG.md +15 -1
- data/README.md +6 -3
- data/Rakefile +1 -1
- data/features/cli_new.feature +12 -8
- data/features/cli_update.feature +14 -9
- data/features/step_definitions/common_steps.rb +3 -3
- data/generators/base_generator.rb +1 -1
- data/generators/config/config_generator.rb +2 -1
- data/generators/config/templates/boot.rb +1 -1
- data/generators/config/templates/capybara.rb +2 -1
- data/generators/config/templates/default.yml +22 -4
- data/generators/config/templates/drivers/appium.rb +25 -0
- data/generators/config/templates/drivers/headless_firefox.rb +23 -0
- data/generators/cucumber/templates/cuke_sniffer.rake +2 -2
- data/generators/cucumber/templates/env.rb +8 -0
- data/generators/prerequisites/templates/factory_bot.rb +1 -0
- data/generators/root/root_generator.rb +1 -1
- data/generators/root/templates/{.rubocop.yml → .rubocop.yml.erb} +34 -13
- data/generators/root/templates/Gemfile.erb +4 -0
- data/generators/rspec/templates/spec_helper.rb +1 -0
- data/generators/turnip/templates/spec_helper.rb +1 -0
- data/howitzer.gemspec +3 -3
- data/lib/howitzer.rb +40 -0
- data/lib/howitzer/cache.rb +19 -18
- data/lib/howitzer/capybara_helpers.rb +13 -5
- data/lib/howitzer/email.rb +1 -0
- data/lib/howitzer/mail_adapters/gmail.rb +3 -0
- data/lib/howitzer/mail_adapters/mailgun.rb +2 -0
- data/lib/howitzer/mail_adapters/mailtrap.rb +3 -0
- data/lib/howitzer/mailgun_api/connector.rb +1 -0
- data/lib/howitzer/meta.rb +11 -0
- data/lib/howitzer/meta/actions.rb +38 -0
- data/lib/howitzer/meta/element.rb +38 -0
- data/lib/howitzer/meta/entry.rb +62 -0
- data/lib/howitzer/meta/iframe.rb +41 -0
- data/lib/howitzer/meta/section.rb +30 -0
- data/lib/howitzer/version.rb +1 -1
- data/lib/howitzer/web/capybara_context_holder.rb +1 -0
- data/lib/howitzer/web/capybara_methods_proxy.rb +4 -1
- data/lib/howitzer/web/element_dsl.rb +1 -0
- data/lib/howitzer/web/iframe_dsl.rb +2 -0
- data/lib/howitzer/web/page.rb +10 -0
- data/lib/howitzer/web/page_dsl.rb +3 -0
- data/lib/howitzer/web/page_validator.rb +2 -0
- data/lib/howitzer/web/section.rb +8 -0
- data/lib/howitzer/web/section_dsl.rb +1 -0
- data/spec/support/shared_examples/capybara_context_holder.rb +1 -1
- data/spec/support/shared_examples/meta_highlight_xpath.rb +41 -0
- data/spec/unit/generators/config_generator_spec.rb +4 -2
- data/spec/unit/generators/root_generator_spec.rb +32 -21
- data/spec/unit/generators/templates/cucumber_spec.rb +97 -0
- data/spec/unit/generators/templates/rspec_spec.rb +88 -0
- data/spec/unit/generators/templates/turnip_spec.rb +98 -0
- data/spec/unit/lib/capybara_helpers_spec.rb +37 -4
- data/spec/unit/lib/howitzer_spec.rb +23 -0
- data/spec/unit/lib/meta/element_spec.rb +59 -0
- data/spec/unit/lib/meta/entry_spec.rb +77 -0
- data/spec/unit/lib/meta/iframe_spec.rb +66 -0
- data/spec/unit/lib/meta/section_spec.rb +43 -0
- data/spec/unit/lib/utils/string_extensions_spec.rb +1 -1
- data/spec/unit/lib/web/element_dsl_spec.rb +10 -1
- data/spec/unit/lib/web/page_spec.rb +7 -0
- data/spec/unit/lib/web/section_spec.rb +7 -0
- metadata +31 -15
- data/generators/config/templates/drivers/phantomjs.rb +0 -19
@@ -0,0 +1,25 @@
|
|
1
|
+
CapybaraHelpers.load_driver_gem!(:appium, 'appium_capybara', 'appium_capybara')
|
2
|
+
|
3
|
+
Capybara.register_driver(:appium) do |app|
|
4
|
+
caps = {}
|
5
|
+
caps['deviceName'] = Howitzer.appium_device_name
|
6
|
+
caps['deviceOrientation'] = Howitzer.appium_device_orientation
|
7
|
+
caps['platformVersion'] = Howitzer.appium_platform_version
|
8
|
+
caps['platformName'] = Howitzer.appium_platform_name
|
9
|
+
caps['browserName'] = Howitzer.appium_browser_name
|
10
|
+
|
11
|
+
url = Howitzer.appium_url
|
12
|
+
|
13
|
+
appium_lib_options = {
|
14
|
+
server_url: url
|
15
|
+
}
|
16
|
+
all_options = {
|
17
|
+
appium_lib: appium_lib_options,
|
18
|
+
caps: caps
|
19
|
+
}
|
20
|
+
Appium::Capybara::Driver.new app, all_options
|
21
|
+
end
|
22
|
+
|
23
|
+
Capybara::Screenshot.class_eval do
|
24
|
+
register_driver :appium, ®istered_drivers[:selenium]
|
25
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# :headless_firefox driver
|
2
|
+
|
3
|
+
Capybara.register_driver :headless_firefox do |app|
|
4
|
+
startup_flags = ['--headless']
|
5
|
+
startup_flags.concat(Howitzer.headless_firefox_flags.split(/\s*,\s*/)) if Howitzer.headless_firefox_flags
|
6
|
+
ff_profile = Selenium::WebDriver::Firefox::Profile.new.tap do |profile|
|
7
|
+
profile['network.http.phishy-userpass-length'] = 255
|
8
|
+
profile['browser.safebrowsing.malware.enabled'] = false
|
9
|
+
profile['network.automatic-ntlm-auth.allow-non-fqdn'] = true
|
10
|
+
profile['network.ntlm.send-lm-response'] = true
|
11
|
+
profile['network.automatic-ntlm-auth.trusted-uris'] = Howitzer.app_host
|
12
|
+
profile['general.useragent.override'] = Howitzer.user_agent if Howitzer.user_agent.present?
|
13
|
+
end
|
14
|
+
options = Selenium::WebDriver::Firefox::Options.new(args: startup_flags, profile: ff_profile)
|
15
|
+
params = { browser: :firefox, options: options }
|
16
|
+
Capybara::Selenium::Driver.new app, params
|
17
|
+
end
|
18
|
+
|
19
|
+
Capybara.javascript_driver = :headless_firefox
|
20
|
+
|
21
|
+
Capybara::Screenshot.class_eval do
|
22
|
+
register_driver :headless_firefox, ®istered_drivers[:selenium]
|
23
|
+
end
|
@@ -1,11 +1,11 @@
|
|
1
1
|
require 'cuke_sniffer'
|
2
2
|
|
3
3
|
def path_to_features
|
4
|
-
@
|
4
|
+
@path_to_features ||= File.expand_path(File.join(__dir__, '..', 'features'))
|
5
5
|
end
|
6
6
|
|
7
7
|
def cuke_sniffer
|
8
|
-
@
|
8
|
+
@cuke_sniffer ||= CukeSniffer::CLI.new(
|
9
9
|
features_location: path_to_features,
|
10
10
|
step_definitions_location: File.join(path_to_features, 'step_definitions'),
|
11
11
|
hooks_location: File.join(path_to_features, 'support'),
|
@@ -10,6 +10,14 @@ RSpec.configure do |config|
|
|
10
10
|
config.disable_monkey_patching!
|
11
11
|
end
|
12
12
|
|
13
|
+
AfterConfiguration do |config|
|
14
|
+
if Howitzer.test_order.present?
|
15
|
+
order, seed = Howitzer.test_order.split(':')
|
16
|
+
config.instance_variable_get(:@options)[:order] = order
|
17
|
+
config.instance_variable_get(:@options)[:seed] = seed
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
13
21
|
FileUtils.mkdir_p(Howitzer.log_dir)
|
14
22
|
|
15
23
|
Howitzer::Log.settings_as_formatted_text
|
@@ -7,11 +7,11 @@ module Howitzer
|
|
7
7
|
{ files:
|
8
8
|
[
|
9
9
|
{ source: '.gitignore', destination: '.gitignore' },
|
10
|
-
{ source: '.rubocop.yml', destination: '.rubocop.yml' },
|
11
10
|
{ source: 'Rakefile', destination: 'Rakefile' }
|
12
11
|
],
|
13
12
|
templates:
|
14
13
|
[
|
14
|
+
{ source: '.rubocop.yml.erb', destination: '.rubocop.yml' },
|
15
15
|
{ source: 'Gemfile.erb', destination: 'Gemfile' }
|
16
16
|
] }
|
17
17
|
end
|
@@ -2,34 +2,55 @@
|
|
2
2
|
# To see all cops used see here: https://github.com/bbatsov/rubocop/blob/master/config/enabled.yml
|
3
3
|
|
4
4
|
AllCops:
|
5
|
-
|
6
|
-
|
7
|
-
- Rakefile
|
8
|
-
- Gemfile
|
9
|
-
|
10
|
-
LineLength:
|
11
|
-
Max: 120
|
5
|
+
DisplayCopNames: true
|
6
|
+
TargetRubyVersion: 2.4
|
12
7
|
|
13
8
|
Layout/CaseIndentation:
|
14
9
|
Enabled: false
|
10
|
+
<% if cucumber -%>
|
15
11
|
|
16
|
-
|
12
|
+
Lint/AmbiguousBlockAssociation:
|
17
13
|
Enabled: false
|
14
|
+
<% end -%>
|
18
15
|
|
19
16
|
Lint/AmbiguousRegexpLiteral:
|
20
17
|
Enabled: false
|
21
18
|
|
22
|
-
|
19
|
+
Metrics/BlockLength:
|
23
20
|
Enabled: false
|
24
21
|
|
22
|
+
Metrics/LineLength:
|
23
|
+
Max: 120
|
24
|
+
|
25
|
+
Metrics/ModuleLength:
|
26
|
+
Max: 150
|
27
|
+
|
25
28
|
Style/CaseEquality:
|
26
29
|
Enabled: false
|
27
30
|
|
28
|
-
Documentation:
|
31
|
+
Style/Documentation:
|
29
32
|
Enabled: false
|
30
33
|
|
31
|
-
Style/
|
34
|
+
Style/EmptyElse:
|
32
35
|
Enabled: false
|
33
36
|
|
34
|
-
|
35
|
-
|
37
|
+
Style/FrozenStringLiteralComment:
|
38
|
+
Enabled: false
|
39
|
+
<% if turnip -%>
|
40
|
+
|
41
|
+
Style/MixinGrouping:
|
42
|
+
EnforcedStyle: separated
|
43
|
+
Exclude:
|
44
|
+
- '**/*_steps.rb'
|
45
|
+
<% end -%>
|
46
|
+
<% if cucumber || turnip -%>
|
47
|
+
|
48
|
+
Style/SymbolProc:
|
49
|
+
Exclude:
|
50
|
+
<% if cucumber -%>
|
51
|
+
- 'features/step_definitions/**/*.rb'
|
52
|
+
<%- end -%>
|
53
|
+
<% if turnip -%>
|
54
|
+
- 'spec/steps/**/*.rb'
|
55
|
+
<%- end -%>
|
56
|
+
<% end -%>
|
@@ -14,6 +14,10 @@ gem 'rubocop'
|
|
14
14
|
<%= "gem 'turnip'\n" if turnip -%>
|
15
15
|
<%= "gem 'syntax'\n" if cucumber -%>
|
16
16
|
|
17
|
+
# Uncomment it if you are going to use 'appium' driver. Appium and Android SDK should be installed.
|
18
|
+
# See https://appium.io/docs/en/about-appium/getting-started/
|
19
|
+
# gem 'appium_capybara'
|
20
|
+
|
17
21
|
# Uncomment it if you are going to use 'webkit' driver. QT library should be installed.
|
18
22
|
# See https://github.com/thoughtbot/capybara-webkit/wiki/Installing-Qt-and-compiling-capybara-webkit
|
19
23
|
#
|
data/howitzer.gemspec
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require File.expand_path('
|
1
|
+
require File.expand_path('lib/howitzer/version', __dir__)
|
2
2
|
|
3
3
|
Gem::Specification.new do |gem|
|
4
4
|
gem.author = 'Roman Parashchenko'
|
@@ -16,10 +16,10 @@ Gem::Specification.new do |gem|
|
|
16
16
|
gem.name = 'howitzer'
|
17
17
|
gem.require_paths = ['lib']
|
18
18
|
gem.version = Howitzer::VERSION
|
19
|
-
gem.required_ruby_version = '>= 2.
|
19
|
+
gem.required_ruby_version = '>= 2.3'
|
20
20
|
|
21
21
|
gem.add_runtime_dependency 'activesupport', '~>5.0'
|
22
|
-
gem.add_runtime_dependency 'capybara',
|
22
|
+
gem.add_runtime_dependency 'capybara', '< 4.0'
|
23
23
|
gem.add_runtime_dependency 'colorize'
|
24
24
|
gem.add_runtime_dependency 'gli'
|
25
25
|
gem.add_runtime_dependency 'gmail'
|
data/lib/howitzer.rb
CHANGED
@@ -29,6 +29,45 @@ module Howitzer
|
|
29
29
|
::SexySettings::Base.instance.all['mailgun_idle_timeout']
|
30
30
|
end
|
31
31
|
|
32
|
+
# @return active session name
|
33
|
+
|
34
|
+
def session_name
|
35
|
+
@session_name ||= 'default'
|
36
|
+
end
|
37
|
+
|
38
|
+
# Sets new session name
|
39
|
+
#
|
40
|
+
# @param name [String] string identifier for the session
|
41
|
+
#
|
42
|
+
# @example Executing code in another browser
|
43
|
+
# Howitzer.session_name = 'browser2'
|
44
|
+
# LoginPage.on do
|
45
|
+
# expect(title).to eq('Login Page')
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# # Switching back to main browser
|
49
|
+
# Howitzer.session_name = 'default'
|
50
|
+
|
51
|
+
def session_name=(name)
|
52
|
+
@session_name = name
|
53
|
+
Capybara.session_name = @session_name
|
54
|
+
end
|
55
|
+
|
56
|
+
# Yield a block using a specific session name
|
57
|
+
#
|
58
|
+
# @param name [String] string identifier for the session
|
59
|
+
#
|
60
|
+
# @example Opening page in another browser
|
61
|
+
# Howitzer.using_session('browser2') do
|
62
|
+
# LoginPage.on do
|
63
|
+
# expect(title).to eq('Login Page')
|
64
|
+
# end
|
65
|
+
# end
|
66
|
+
|
67
|
+
def using_session(name)
|
68
|
+
Capybara.using_session(name) { yield }
|
69
|
+
end
|
70
|
+
|
32
71
|
attr_accessor :current_rake_task
|
33
72
|
end
|
34
73
|
|
@@ -60,6 +99,7 @@ module Howitzer
|
|
60
99
|
|
61
100
|
def self.sexy_setting!(name)
|
62
101
|
return Howitzer.public_send(name) if Howitzer.respond_to?(name)
|
102
|
+
|
63
103
|
raise UndefinedSexySettingError,
|
64
104
|
"Undefined '#{name}' setting. Please add the setting to config/default.yml:\n #{name}: some_value\n"
|
65
105
|
end
|
data/lib/howitzer/cache.rb
CHANGED
@@ -12,35 +12,35 @@ module Howitzer
|
|
12
12
|
|
13
13
|
# Saves data into memory. Marking by a namespace and a key
|
14
14
|
#
|
15
|
-
# @param
|
15
|
+
# @param namespace [String] a namespace
|
16
16
|
# @param key [String] a key that should be uniq within the namespace
|
17
17
|
# @param value [Object] everything you want to store in Memory
|
18
18
|
# @raise [NoDataError] if the namespace missing
|
19
19
|
|
20
|
-
def store(
|
21
|
-
check_ns(
|
22
|
-
@data[
|
20
|
+
def store(namespace, key, value)
|
21
|
+
check_ns(namespace)
|
22
|
+
@data[namespace][key] = value
|
23
23
|
end
|
24
24
|
|
25
25
|
# Gets data from memory. Can get all namespace or single data value in namespace using key
|
26
26
|
#
|
27
|
-
# @param
|
27
|
+
# @param namespace [String] a namespace
|
28
28
|
# @param key [String] key that isn't necessary required
|
29
29
|
# @return [Object, Hash] all data from the namespace if the key is ommited, otherwise returs
|
30
30
|
# all data for the namespace
|
31
31
|
# @raise [NoDataError] if the namespace missing
|
32
32
|
|
33
|
-
def extract(
|
34
|
-
check_ns(
|
35
|
-
key ? @data[
|
33
|
+
def extract(namespace, key = nil)
|
34
|
+
check_ns(namespace)
|
35
|
+
key ? @data[namespace][key] : @data[namespace]
|
36
36
|
end
|
37
37
|
|
38
38
|
# Deletes all data from a namespace
|
39
39
|
#
|
40
|
-
# @param
|
40
|
+
# @param namespace [String] a namespace
|
41
41
|
|
42
|
-
def clear_ns(
|
43
|
-
init_ns(
|
42
|
+
def clear_ns(namespace)
|
43
|
+
init_ns(namespace)
|
44
44
|
end
|
45
45
|
|
46
46
|
# Deletes all namespaces with data
|
@@ -53,17 +53,18 @@ module Howitzer
|
|
53
53
|
|
54
54
|
private
|
55
55
|
|
56
|
-
def check_ns(
|
57
|
-
raise Howitzer::NoDataError, 'Data storage namespace can not be empty' unless
|
58
|
-
|
56
|
+
def check_ns(namespace)
|
57
|
+
raise Howitzer::NoDataError, 'Data storage namespace can not be empty' unless namespace
|
58
|
+
|
59
|
+
init_ns(namespace) if ns_absent?(namespace)
|
59
60
|
end
|
60
61
|
|
61
|
-
def ns_absent?(
|
62
|
-
!@data.key?(
|
62
|
+
def ns_absent?(namespace)
|
63
|
+
!@data.key?(namespace)
|
63
64
|
end
|
64
65
|
|
65
|
-
def init_ns(
|
66
|
-
@data[
|
66
|
+
def init_ns(namespace)
|
67
|
+
@data[namespace] = {}
|
67
68
|
end
|
68
69
|
end
|
69
70
|
end
|
@@ -14,7 +14,7 @@ module Howitzer
|
|
14
14
|
].freeze,
|
15
15
|
LOCAL_BROWSERS = [
|
16
16
|
HEADLESS_CHROME = :headless_chrome,
|
17
|
-
|
17
|
+
HEADLESS_FIREFOX = :headless_firefox,
|
18
18
|
POLTERGEIST = :poltergeist,
|
19
19
|
SELENIUM = :selenium,
|
20
20
|
SELENIUM_GRID = :selenium_grid,
|
@@ -51,7 +51,7 @@ module Howitzer
|
|
51
51
|
# @raise [SelBrowserNotSpecifiedError] if selenium driver and missing browser name
|
52
52
|
|
53
53
|
def chrome_browser?
|
54
|
-
browser?
|
54
|
+
browser? :chrome
|
55
55
|
end
|
56
56
|
|
57
57
|
# @return [Boolean] whether or not current browser is Safari.
|
@@ -155,13 +155,13 @@ module Howitzer
|
|
155
155
|
unless Howitzer.cloud_browser_name.nil?
|
156
156
|
return browser_aliases.include?(Howitzer.cloud_browser_name.to_s.downcase.to_sym)
|
157
157
|
end
|
158
|
+
|
158
159
|
raise Howitzer::CloudBrowserNotSpecifiedError, CHECK_YOUR_SETTINGS_MSG
|
159
160
|
end
|
160
161
|
|
161
162
|
def selenium_browser?(*browser_aliases)
|
162
|
-
unless Howitzer.selenium_browser.nil?
|
163
|
-
|
164
|
-
end
|
163
|
+
return browser_aliases.include?(Howitzer.selenium_browser.to_s.to_sym) unless Howitzer.selenium_browser.nil?
|
164
|
+
|
165
165
|
raise Howitzer::SelBrowserNotSpecifiedError, CHECK_YOUR_SETTINGS_MSG
|
166
166
|
end
|
167
167
|
|
@@ -169,6 +169,14 @@ module Howitzer
|
|
169
169
|
Howitzer.driver.to_sym == SELENIUM
|
170
170
|
end
|
171
171
|
|
172
|
+
def headless_chrome_driver?
|
173
|
+
Howitzer.driver.to_sym == HEADLESS_CHROME
|
174
|
+
end
|
175
|
+
|
176
|
+
def headless_firefox_driver?
|
177
|
+
Howitzer.driver.to_sym == HEADLESS_FIREFOX
|
178
|
+
end
|
179
|
+
|
172
180
|
def selenium_grid_driver?
|
173
181
|
Howitzer.driver.to_sym == SELENIUM_GRID
|
174
182
|
end
|
data/lib/howitzer/email.rb
CHANGED
@@ -16,6 +16,7 @@ module Howitzer
|
|
16
16
|
message = {}
|
17
17
|
retryable(find_retry_params(wait)) { message = get_message(recipient, subject) }
|
18
18
|
return new(message) if message.present?
|
19
|
+
|
19
20
|
raise Howitzer::EmailNotFoundError,
|
20
21
|
"Message with subject '#{subject}' for recipient '#{recipient}' was not found."
|
21
22
|
end
|
@@ -68,12 +69,14 @@ module Howitzer
|
|
68
69
|
def mime_part!
|
69
70
|
files = mime_part
|
70
71
|
return files if files.present?
|
72
|
+
|
71
73
|
raise Howitzer::NoAttachmentsError, 'No attachments were found.'
|
72
74
|
end
|
73
75
|
|
74
76
|
def self.get_message(recipient, subject)
|
75
77
|
message = Howitzer::GmailApi::Client.new.find_message(recipient, subject)
|
76
78
|
raise Howitzer::EmailNotFoundError if message.blank?
|
79
|
+
|
77
80
|
message
|
78
81
|
end
|
79
82
|
private_class_method :get_message
|
@@ -17,6 +17,7 @@ module Howitzer
|
|
17
17
|
message = {}
|
18
18
|
retryable(find_retry_params(wait)) { message = retrieve_message(recipient, subject) }
|
19
19
|
return new(message) if message.present?
|
20
|
+
|
20
21
|
raise Howitzer::EmailNotFoundError,
|
21
22
|
"Message with subject '#{subject}' for recipient '#{recipient}' was not found."
|
22
23
|
end
|
@@ -75,6 +76,7 @@ module Howitzer
|
|
75
76
|
def mime_part!
|
76
77
|
files = mime_part
|
77
78
|
return files if files.present?
|
79
|
+
|
78
80
|
raise Howitzer::NoAttachmentsError, 'No attachments were found.'
|
79
81
|
end
|
80
82
|
|
@@ -16,6 +16,7 @@ module Howitzer
|
|
16
16
|
message = {}
|
17
17
|
retryable(find_retry_params(wait)) { message = retrieve_message(recipient, subject) }
|
18
18
|
return new(message) if message.present?
|
19
|
+
|
19
20
|
raise Howitzer::EmailNotFoundError,
|
20
21
|
"Message with subject '#{subject}' for recipient '#{recipient}' was not found."
|
21
22
|
end
|
@@ -74,6 +75,7 @@ module Howitzer
|
|
74
75
|
def mime_part!
|
75
76
|
files = mime_part
|
76
77
|
return files if files.present?
|
78
|
+
|
77
79
|
raise Howitzer::NoAttachmentsError, 'No attachments were found.'
|
78
80
|
end
|
79
81
|
|
@@ -91,6 +93,7 @@ module Howitzer
|
|
91
93
|
def self.retrieve_message(recipient, subject)
|
92
94
|
message = Howitzer::MailtrapApi::Client.new.find_message(recipient, subject)
|
93
95
|
raise Howitzer::EmailNotFoundError, 'Message not received yet, retry...' unless message
|
96
|
+
|
94
97
|
message
|
95
98
|
end
|
96
99
|
private_class_method :retrieve_message
|