effective_test_bot 0.0.10 → 0.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 +4 -4
- data/README.md +13 -30
- data/lib/effective_test_bot/engine.rb +10 -1
- data/lib/effective_test_bot/version.rb +2 -1
- data/lib/generators/effective_test_bot/install_generator.rb +1 -20
- data/lib/generators/templates/effective_test_bot.rb +3 -1
- data/lib/generators/templates/{minitest/test_helper.rb → test_helper.rb} +9 -5
- data/lib/tasks/effective_test_bot_tasks.rake +4 -22
- data/test/concerns/acts_as_test_botable.rb +57 -0
- data/test/support/effective_test_bot_assertions.rb +26 -0
- data/{app/helpers/effective_test_bot_helper.rb → test/support/effective_test_bot_form_helper.rb} +14 -45
- data/test/support/effective_test_bot_login_helper.rb +31 -0
- data/test/support/effective_test_bot_test_helper.rb +18 -0
- data/test/test_bot/integration/devise_test.rb +51 -0
- data/test/test_bot/integration/minitest_test.rb +49 -59
- data/test/test_bot/models/user_test.rb +1 -1
- data/test/test_botable/crud_test.rb +181 -0
- metadata +24 -15
- data/lib/generators/templates/rspec/rails_helper.rb +0 -62
- data/lib/generators/templates/rspec/spec_helper.rb +0 -90
- data/spec/features/crud/crud_spec.rb +0 -50
- data/spec/features/devise/sign_in_spec.rb +0 -43
- data/spec/features/devise/sign_up_spec.rb +0 -29
- data/spec/features/home_page_spec.rb +0 -8
- data/spec/models/user_spec.rb +0 -19
- data/test/fixtures/users.yml +0 -3
- data/test/test_bot/integration/devise/sign_in_test.rb +0 -37
- data/test/test_bot/integration/devise/sign_up_test.rb +0 -25
- data/test/test_bot/models/database_test.rb +0 -24
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6c091f3638ed0ef358cce23cb248daeb0f7040d3
|
4
|
+
data.tar.gz: d38922c2629ea9a12004fc150601331a47db6af7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3ae23c5221d6a46a3f2a369cd42a31d2e98d09c8e6b032286b14dd3920349a9503ca4360e8726eef3cb403541655512a16bcaa8f10a75e2547e90250fb7d0b7f
|
7
|
+
data.tar.gz: 5d690bb001ab760f86f1aa621a0c5e8876b1e53c16f248e221decb5493cd2e82fc919c71836bfb553fcc07d4ffe89b85367c15f261ea2e39217811e94421284a
|
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
A shared library of rails model & capybara-based feature tests that should pass in every Rails application.
|
4
4
|
|
5
|
-
Also provides a one-liner installation & configuration of
|
5
|
+
Also provides a one-liner installation & configuration of minitest and capybara test environment.
|
6
6
|
|
7
7
|
Rails 3.2.x and 4.x
|
8
8
|
|
@@ -27,33 +27,29 @@ Then run the generator:
|
|
27
27
|
rails generate effective_test_bot:install
|
28
28
|
```
|
29
29
|
|
30
|
-
The above command will first invoke the default `
|
30
|
+
The above command will first invoke the default `minitest` installation tasks, if they haven't already been run.
|
31
31
|
|
32
|
-
It will then copy the packaged `
|
32
|
+
It will then copy the packaged `test_helper.rb` that matches this gem author's opinionated testing environment.
|
33
33
|
|
34
|
-
Run the test suite with
|
34
|
+
Run the test suite with:
|
35
35
|
|
36
36
|
```ruby
|
37
|
-
bundle exec
|
38
|
-
```
|
39
|
-
|
40
|
-
or
|
41
|
-
|
42
|
-
```ruby
|
43
|
-
bundle exec rspec
|
37
|
+
bundle exec rake test:bot
|
44
38
|
```
|
45
39
|
|
46
40
|
You should now see multiple -- hopefully passing -- tests that you didn't write!
|
47
41
|
|
48
42
|
|
49
|
-
|
43
|
+
## Fixtures
|
50
44
|
|
51
|
-
|
45
|
+
TODO
|
52
46
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
47
|
+
users.yml:
|
48
|
+
|
49
|
+
```yaml
|
50
|
+
normal:
|
51
|
+
email: 'normal@agilestyle.com'
|
52
|
+
encrypted_password: <%= Devise::Encryptor.digest(User, 'password') %>
|
57
53
|
```
|
58
54
|
|
59
55
|
|
@@ -64,17 +60,6 @@ MIT License. Copyright [Code and Effect Inc.](http://www.codeandeffect.com/)
|
|
64
60
|
Code and Effect is the product arm of [AgileStyle](http://www.agilestyle.com/), an Edmonton-based shop that specializes in building custom web applications with Ruby on Rails.
|
65
61
|
|
66
62
|
|
67
|
-
## Testing
|
68
|
-
|
69
|
-
The test suite for this gem is unfortunately not yet complete.
|
70
|
-
|
71
|
-
Run tests by:
|
72
|
-
|
73
|
-
```ruby
|
74
|
-
rake spec
|
75
|
-
```
|
76
|
-
|
77
|
-
|
78
63
|
## Contributing
|
79
64
|
|
80
65
|
1. Fork it
|
@@ -83,5 +68,3 @@ rake spec
|
|
83
68
|
4. Push to the branch (`git push origin my-new-feature`)
|
84
69
|
5. Bonus points for test coverage
|
85
70
|
6. Create new Pull Request
|
86
|
-
|
87
|
-
#page.save_screenshot('sign-in-test-1.png')
|
@@ -2,6 +2,10 @@ module EffectiveTestBot
|
|
2
2
|
class Engine < ::Rails::Engine
|
3
3
|
engine_name 'effective_test_bot'
|
4
4
|
|
5
|
+
config.autoload_paths += Dir["#{config.root}/test/concerns/**/"]
|
6
|
+
config.autoload_paths += Dir["#{config.root}/test/support/**/"]
|
7
|
+
config.autoload_paths += Dir["#{config.root}/test/test_botable/**/"]
|
8
|
+
|
5
9
|
# Set up our default configuration options.
|
6
10
|
initializer "effective_test_bot.defaults", :before => :load_config_initializers do |app|
|
7
11
|
# Set up our defaults, as per our initializer template
|
@@ -10,7 +14,12 @@ module EffectiveTestBot
|
|
10
14
|
|
11
15
|
initializer 'effective_test_bot.test_suite' do |app|
|
12
16
|
Rails.application.config.to_prepare do
|
13
|
-
ActionDispatch::IntegrationTest.send(:include,
|
17
|
+
ActionDispatch::IntegrationTest.send(:include, ActsAsTestBotable)
|
18
|
+
|
19
|
+
ActionDispatch::IntegrationTest.send(:include, EffectiveTestBotAssertions)
|
20
|
+
ActionDispatch::IntegrationTest.send(:include, EffectiveTestBotFormHelper)
|
21
|
+
ActionDispatch::IntegrationTest.send(:include, EffectiveTestBotLoginHelper)
|
22
|
+
ActionDispatch::IntegrationTest.send(:include, EffectiveTestBotTestHelper)
|
14
23
|
end
|
15
24
|
end
|
16
25
|
|
@@ -13,19 +13,6 @@ module EffectiveTestBot
|
|
13
13
|
run 'bundle exec rails generate minitest:install'
|
14
14
|
end
|
15
15
|
|
16
|
-
# def install_rspec
|
17
|
-
# return if File.exists?('spec/spec_helper.rb')
|
18
|
-
# puts '[effective_test_bot] installing rspec'
|
19
|
-
# run 'bundle exec rails generate rspec:install'
|
20
|
-
# end
|
21
|
-
|
22
|
-
# def install_guard
|
23
|
-
# return if File.exists?('Guardfile')
|
24
|
-
# puts '[effective_test_bot] installing guard'
|
25
|
-
# run 'bundle exec guard init'
|
26
|
-
# puts ""
|
27
|
-
# end
|
28
|
-
|
29
16
|
def explain_overwrite
|
30
17
|
puts '[effective_test_bot] Successfully installed/detected: minitest'
|
31
18
|
puts ""
|
@@ -49,15 +36,9 @@ module EffectiveTestBot
|
|
49
36
|
end
|
50
37
|
|
51
38
|
def overwrite_minitest
|
52
|
-
template '
|
39
|
+
template 'test_helper.rb', 'test/test_helper.rb'
|
53
40
|
end
|
54
41
|
|
55
|
-
# def overwrite_rspec
|
56
|
-
# template 'rspec/.rspec', '.rspec'
|
57
|
-
# template 'rspec/rails_helper.rb', 'spec/rails_helper.rb'
|
58
|
-
# template 'rspec/spec_helper.rb', 'spec/spec_helper.rb'
|
59
|
-
# end
|
60
|
-
|
61
42
|
def thank_you
|
62
43
|
puts "Thanks for using EffectiveTestBot"
|
63
44
|
puts "Run tests by typing 'rake test:bot'"
|
@@ -11,6 +11,7 @@ require 'shoulda'
|
|
11
11
|
|
12
12
|
require 'capybara/webkit'
|
13
13
|
require 'capybara-screenshot/minitest'
|
14
|
+
require 'capybara/slow_finder_errors'
|
14
15
|
|
15
16
|
class ActiveSupport::TestCase
|
16
17
|
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
|
@@ -26,14 +27,16 @@ class ActionDispatch::IntegrationTest
|
|
26
27
|
include Capybara::Screenshot::MiniTestPlugin
|
27
28
|
include Warden::Test::Helpers if defined?(Devise)
|
28
29
|
|
29
|
-
#
|
30
|
-
# def setup
|
30
|
+
# def setup # Called before every test
|
31
31
|
# end
|
32
32
|
|
33
|
-
#
|
34
|
-
|
35
|
-
|
33
|
+
# def teardown # Called after every single test
|
34
|
+
# end
|
35
|
+
|
36
|
+
def after_teardown # I reset sessions here so capybara-screenshot can still make screenshots when tests fail
|
37
|
+
super() and Capybara.reset_sessions!
|
36
38
|
end
|
39
|
+
|
37
40
|
end
|
38
41
|
|
39
42
|
|
@@ -50,6 +53,7 @@ Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
|
|
50
53
|
|
51
54
|
# So the very first thing I do is set up a consistent database
|
52
55
|
Rake::Task['db:schema:load'].invoke
|
56
|
+
ActiveRecord::Migration.maintain_test_schema!
|
53
57
|
|
54
58
|
# or the following 3:
|
55
59
|
|
@@ -1,36 +1,18 @@
|
|
1
1
|
require 'rake/testtask'
|
2
2
|
require 'rails/test_unit/sub_test_task'
|
3
3
|
|
4
|
-
# namespace :test do
|
5
|
-
# task :bot => :environment do
|
6
|
-
# #Rails::TestTask.test_creator(Rake.application.top_level_tasks).invoke_rake_task
|
7
|
-
|
8
|
-
# #eval File.read("#{config.root}/lib/generators/templates/effective_test_bot.rb")
|
9
|
-
# Rails::TestTask.new('effective_test_bot' => 'test:prepare') do |t|
|
10
|
-
# t.libs << 'test'
|
11
|
-
# t.pattern = 'test/integration/**/*_test.rb'
|
12
|
-
# #t.test_files = FileList["../effective_test_bot/test/**/*_test.rb"].exclude('test/controllers/**/*_test.rb')
|
13
|
-
# end
|
14
|
-
# end
|
15
|
-
# end
|
16
|
-
|
17
4
|
namespace :test do
|
18
5
|
desc 'Runs Effective Test Bot'
|
19
6
|
task :bot do
|
20
7
|
Rake::Task["test:effective_test_bot"].invoke
|
21
8
|
end
|
22
9
|
|
23
|
-
#Rake::Task["db:seed"].invoke
|
24
|
-
# Or in rails 3 add to test/test_helper.rb
|
25
|
-
# Rails.application.load_seed
|
26
|
-
|
27
10
|
Rails::TestTask.new('effective_test_bot' => 'test:prepare') do |t|
|
28
11
|
t.libs << 'test'
|
29
|
-
t.test_files = FileList["#{File.dirname(__FILE__)}/../../test/**/*_test.rb"]
|
12
|
+
t.test_files = FileList["#{File.dirname(__FILE__)}/../../test/test_bot/**/*_test.rb"]
|
30
13
|
end
|
31
|
-
end
|
32
14
|
|
33
|
-
|
34
|
-
|
35
|
-
|
15
|
+
|
16
|
+
|
17
|
+
end
|
36
18
|
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module ActsAsTestBotable
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
module ClassMethods
|
5
|
+
CRUD_ACTIONS = [:index, :new, :create, :edit, :update, :show, :destroy]
|
6
|
+
|
7
|
+
def crud_test(obj, user, options = {})
|
8
|
+
# Check for expected usage
|
9
|
+
unless (obj.kind_of?(Class) || obj.kind_of?(ActiveRecord::Base)) && user.kind_of?(User) && options.kind_of?(Hash)
|
10
|
+
puts 'invalid parameters passed to crud_test(), expecting crud_test(Post || Post.new(), User.first, options_hash)' and return
|
11
|
+
end
|
12
|
+
|
13
|
+
# Make sure Obj.new() works
|
14
|
+
if obj.kind_of?(Class) && (obj.new() rescue false) == false
|
15
|
+
puts "effective_test_bot: failed to initialize object with #{obj}.new(), unable to proceed" and return
|
16
|
+
end
|
17
|
+
|
18
|
+
# Set up the crud_actions_to_test
|
19
|
+
crud_actions_to_test = if options[:only]
|
20
|
+
Array(options[:only]).flatten.compact.map(&:to_sym)
|
21
|
+
elsif options[:except]
|
22
|
+
(CRUD_ACTIONS - Array(options[:except]).flatten.compact.map(&:to_sym))
|
23
|
+
else
|
24
|
+
CRUD_ACTIONS
|
25
|
+
end
|
26
|
+
|
27
|
+
# Parse the resource and resourece class
|
28
|
+
resource = obj.kind_of?(Class) ? obj.new() : obj
|
29
|
+
resource_class = obj.kind_of?(Class) ? obj : obj.class
|
30
|
+
|
31
|
+
# If obj is an ActiveRecord object with attributes, Post.new(:title => 'My Title')
|
32
|
+
# then compute any explicit attributes, so forms will be filled with those values
|
33
|
+
resource_attributes = if obj.kind_of?(ActiveRecord::Base)
|
34
|
+
empty = resource_class.new()
|
35
|
+
{}.tap { |atts| resource.attributes.each { |k, v| atts[k] = v if empty.attributes[k] != v } }
|
36
|
+
end || {}
|
37
|
+
|
38
|
+
# Assign variables to be used in test/test_botable/crud_test.rb
|
39
|
+
let(:resource) { resource }
|
40
|
+
let(:resource_class) { resource_class }
|
41
|
+
let(:resource_name) { resource_class.name.underscore }
|
42
|
+
let(:resource_attributes) { resource_attributes }
|
43
|
+
let(:user) { user }
|
44
|
+
let(:controller_namespace) { options[:namespace] }
|
45
|
+
let(:crud_actions_to_test) { crud_actions_to_test }
|
46
|
+
|
47
|
+
include ::CrudTest
|
48
|
+
|
49
|
+
# This will run any CrudTest methods, in order, as it's defined in the file
|
50
|
+
# Then the rest of the methods in whatever order they occur originally (:random, :alpha, :sorted)
|
51
|
+
def self.runnable_methods
|
52
|
+
::CrudTest.public_instance_methods.map { |name| name.to_s if name.to_s.starts_with?('test_bot') }.compact + super.select { |name| !name.starts_with?('test_bot') }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module EffectiveTestBotAssertions
|
2
|
+
def assert_signed_in
|
3
|
+
visit new_user_session_path
|
4
|
+
assert_content I18n.t('devise.failure.already_authenticated')
|
5
|
+
assert page.has_no_selector?('form#new_user')
|
6
|
+
end
|
7
|
+
|
8
|
+
def assert_signed_out
|
9
|
+
visit new_user_session_path
|
10
|
+
refute_content I18n.t('devise.failure.already_authenticated')
|
11
|
+
assert page.has_selector?('form#new_user')
|
12
|
+
end
|
13
|
+
|
14
|
+
def assert_page_title(title = :any, message = 'page title is blank')
|
15
|
+
if title.present? && title != :any
|
16
|
+
assert_title(title) # Capybara TitleQuery, match this text
|
17
|
+
else
|
18
|
+
title = (page.find(:xpath, '//title', visible: false) rescue nil)
|
19
|
+
assert title.present?, message
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def assert_page_status(status = 200)
|
24
|
+
assert_equal status, page.status_code, "page failed to load with #{status} HTTP status code"
|
25
|
+
end
|
26
|
+
end
|
data/{app/helpers/effective_test_bot_helper.rb → test/support/effective_test_bot_form_helper.rb}
RENAMED
@@ -1,36 +1,6 @@
|
|
1
|
-
module
|
2
|
-
|
3
|
-
|
4
|
-
end
|
5
|
-
|
6
|
-
def sign_in(user) # Warden::Test::Helpers
|
7
|
-
user.kind_of?(String) == true ? login_as(User.find_by_email(user)) : login_as(user)
|
8
|
-
end
|
9
|
-
|
10
|
-
def sign_in_manually(email, password)
|
11
|
-
visit new_user_session_path
|
12
|
-
|
13
|
-
within('form#new_user') do
|
14
|
-
fill_form(:email => email, :password => password)
|
15
|
-
submit_form
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
def sign_up(email = Faker::Internet.email, password = Faker::Internet.password)
|
20
|
-
visit new_user_registration_path
|
21
|
-
|
22
|
-
within('form#new_user') do
|
23
|
-
fill_form(:email => email, :password => password, :password_confirmation => password)
|
24
|
-
submit_form
|
25
|
-
end
|
26
|
-
|
27
|
-
# These lines seem to need to be here, or minitest.rb 23 blows up.
|
28
|
-
# Some kind of threading issue is fucking with me.
|
29
|
-
assert_equal page.status_code, 200
|
30
|
-
assert_content I18n.t('devise.registrations.signed_up')
|
31
|
-
|
32
|
-
User.find_by_email(email)
|
33
|
-
end
|
1
|
+
module EffectiveTestBotFormHelper
|
2
|
+
DIGITS = ('1'..'9').to_a
|
3
|
+
LETTERS = ('A'..'Z').to_a
|
34
4
|
|
35
5
|
# fill_form(:email => 'somethign@soneone.com', :password => 'blahblah', 'user.last_name' => 'hlwerewr')
|
36
6
|
def fill_form(fills = {})
|
@@ -46,7 +16,7 @@ module EffectiveTestBotHelper
|
|
46
16
|
field.select(fill_value(field, fills), match: :first)
|
47
17
|
when 'input_file'
|
48
18
|
puts "Warning, input_file not yet supported"
|
49
|
-
when 'input_submit'
|
19
|
+
when 'input_submit', 'input_search'
|
50
20
|
# Do nothing
|
51
21
|
else
|
52
22
|
raise "unsupported field type #{[field.tag_name, field['type']].compact.join('_')}"
|
@@ -54,6 +24,10 @@ module EffectiveTestBotHelper
|
|
54
24
|
end
|
55
25
|
end
|
56
26
|
|
27
|
+
def clear_form
|
28
|
+
all('input,select,textarea').each { |field| field.set('') }
|
29
|
+
end
|
30
|
+
|
57
31
|
# Operates on just string keys
|
58
32
|
def fill_value(field, fills = nil)
|
59
33
|
attributes = field['name'].to_s.gsub(']', '').split('[') # user[something_attributes][last_name] => ['user', 'something_attributes', 'last_name']
|
@@ -67,12 +41,7 @@ module EffectiveTestBotHelper
|
|
67
41
|
|
68
42
|
if fills.key?(key)
|
69
43
|
fill_value = fills[key]
|
70
|
-
|
71
|
-
if field_name == 'select'
|
72
|
-
break
|
73
|
-
else
|
74
|
-
return fill_value
|
75
|
-
end
|
44
|
+
['select'].include?(field_name) ? break : (return fill_value)
|
76
45
|
end
|
77
46
|
end
|
78
47
|
end
|
@@ -85,8 +54,7 @@ module EffectiveTestBotHelper
|
|
85
54
|
when 'input_password'
|
86
55
|
Faker::Internet.password
|
87
56
|
when 'input_tel'
|
88
|
-
|
89
|
-
d = 10.times.map { digits.sample }
|
57
|
+
d = 10.times.map { DIGITS.sample }
|
90
58
|
d[0] + d[1] + d[2] + '-' + d[3] + d[4] + d[5] + '-' + d[6] + d[7] + d[8] + d[9]
|
91
59
|
when 'input_text'
|
92
60
|
classes = field['class'].to_s.split(' ')
|
@@ -101,6 +69,8 @@ module EffectiveTestBotHelper
|
|
101
69
|
Faker::Name.last_name
|
102
70
|
elsif attributes.last.to_s.include?('name')
|
103
71
|
Faker::Name.name
|
72
|
+
elsif attributes.last.to_s.include?('postal') # Make a Canadian Postal Code
|
73
|
+
LETTERS.sample + DIGITS.sample + LETTERS.sample + ' ' + DIGITS.sample + LETTERS.sample + DIGITS.sample
|
104
74
|
else
|
105
75
|
Faker::Lorem.word
|
106
76
|
end
|
@@ -117,7 +87,7 @@ module EffectiveTestBotHelper
|
|
117
87
|
when 'input_checkbox'
|
118
88
|
[true, false].sample
|
119
89
|
when 'input_radio'
|
120
|
-
|
90
|
+
[true, false].sample
|
121
91
|
else
|
122
92
|
raise "fill_value unsupported field type: #{field['type']}"
|
123
93
|
end
|
@@ -126,10 +96,9 @@ module EffectiveTestBotHelper
|
|
126
96
|
def submit_form(label = nil)
|
127
97
|
if label.present?
|
128
98
|
click_on(label)
|
129
|
-
#find_field(label).click
|
130
99
|
else
|
131
100
|
first(:css, "input[type='submit']").click
|
132
101
|
end
|
102
|
+
synchronize!
|
133
103
|
end
|
134
|
-
|
135
104
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# This is all assuming you're running Devise
|
2
|
+
|
3
|
+
module EffectiveTestBotLoginHelper
|
4
|
+
def as_user(user)
|
5
|
+
sign_in(user); yield; logout
|
6
|
+
end
|
7
|
+
|
8
|
+
def sign_in(user) # Warden::Test::Helpers
|
9
|
+
user.kind_of?(String) == true ? login_as(User.find_by_email!(user)) : login_as(user)
|
10
|
+
end
|
11
|
+
|
12
|
+
def sign_in_manually(email, password)
|
13
|
+
visit new_user_session_path
|
14
|
+
|
15
|
+
within('form#new_user') do
|
16
|
+
fill_form(:email => email, :password => password)
|
17
|
+
submit_form
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def sign_up(email = Faker::Internet.email, password = Faker::Internet.password)
|
22
|
+
visit new_user_registration_path
|
23
|
+
|
24
|
+
within('form#new_user') do
|
25
|
+
fill_form(:email => email, :password => password, :password_confirmation => password)
|
26
|
+
submit_form
|
27
|
+
end
|
28
|
+
|
29
|
+
User.find_by_email(email)
|
30
|
+
end
|
31
|
+
end
|