judit-pickle 0.4.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 (75) hide show
  1. data/.gitignore +6 -0
  2. data/Gemfile +20 -0
  3. data/Gemfile.lock +98 -0
  4. data/History.txt +409 -0
  5. data/License.txt +20 -0
  6. data/README.rdoc +367 -0
  7. data/Rakefile +20 -0
  8. data/Rakefile.d/cucumber.rake +24 -0
  9. data/Rakefile.d/jeweler.rake +23 -0
  10. data/Rakefile.d/rcov.rake +18 -0
  11. data/Rakefile.d/rspec.rake +7 -0
  12. data/Rakefile.d/yard.rake +5 -0
  13. data/Todo.txt +3 -0
  14. data/VERSION +1 -0
  15. data/features/app/app.rb +122 -0
  16. data/features/app/blueprints.rb +11 -0
  17. data/features/app/factories.rb +23 -0
  18. data/features/app/views/notifier/email.erb +1 -0
  19. data/features/app/views/notifier/user_email.erb +6 -0
  20. data/features/email/email.feature +64 -0
  21. data/features/generator/generators.feature +59 -0
  22. data/features/path/models_page.feature +44 -0
  23. data/features/path/named_route_page.feature +10 -0
  24. data/features/pickle/create_from_active_record.feature +76 -0
  25. data/features/pickle/create_from_factory_girl.feature +63 -0
  26. data/features/pickle/create_from_machinist.feature +39 -0
  27. data/features/step_definitions/email_steps.rb +63 -0
  28. data/features/step_definitions/extra_email_steps.rb +7 -0
  29. data/features/step_definitions/fork_steps.rb +4 -0
  30. data/features/step_definitions/generator_steps.rb +46 -0
  31. data/features/step_definitions/path_steps.rb +14 -0
  32. data/features/step_definitions/pickle_steps.rb +100 -0
  33. data/features/step_definitions/raise_error_steps.rb +3 -0
  34. data/features/support/email.rb +21 -0
  35. data/features/support/env.rb +52 -0
  36. data/features/support/paths.rb +47 -0
  37. data/features/support/pickle.rb +26 -0
  38. data/features/support/pickle_app.rb +4 -0
  39. data/init.rb +0 -0
  40. data/lib/generators/pickle_generator.rb +69 -0
  41. data/lib/pickle/adapter.rb +137 -0
  42. data/lib/pickle/adapters/active_record.rb +57 -0
  43. data/lib/pickle/adapters/data_mapper.rb +42 -0
  44. data/lib/pickle/adapters/mongoid.rb +44 -0
  45. data/lib/pickle/config.rb +49 -0
  46. data/lib/pickle/email/parser.rb +18 -0
  47. data/lib/pickle/email/world.rb +13 -0
  48. data/lib/pickle/email.rb +77 -0
  49. data/lib/pickle/parser/matchers.rb +87 -0
  50. data/lib/pickle/parser.rb +65 -0
  51. data/lib/pickle/path/world.rb +5 -0
  52. data/lib/pickle/path.rb +45 -0
  53. data/lib/pickle/session/parser.rb +34 -0
  54. data/lib/pickle/session.rb +205 -0
  55. data/lib/pickle/version.rb +9 -0
  56. data/lib/pickle/world.rb +14 -0
  57. data/lib/pickle.rb +26 -0
  58. data/pickle.gemspec +134 -0
  59. data/rails_generators/pickle/pickle_generator.rb +33 -0
  60. data/rails_generators/pickle/templates/email.rb +21 -0
  61. data/rails_generators/pickle/templates/email_steps.rb +63 -0
  62. data/rails_generators/pickle/templates/paths.rb +47 -0
  63. data/rails_generators/pickle/templates/pickle.rb +28 -0
  64. data/rails_generators/pickle/templates/pickle_steps.rb +100 -0
  65. data/spec/pickle/adapter_spec.rb +186 -0
  66. data/spec/pickle/config_spec.rb +109 -0
  67. data/spec/pickle/email/parser_spec.rb +51 -0
  68. data/spec/pickle/email_spec.rb +166 -0
  69. data/spec/pickle/parser/matchers_spec.rb +70 -0
  70. data/spec/pickle/parser_spec.rb +161 -0
  71. data/spec/pickle/path_spec.rb +101 -0
  72. data/spec/pickle/session_spec.rb +434 -0
  73. data/spec/pickle_spec.rb +24 -0
  74. data/spec/spec_helper.rb +8 -0
  75. metadata +199 -0
@@ -0,0 +1,52 @@
1
+ # IMPORTANT: This file is generated by cucumber-rails - edit at your own peril.
2
+ # It is recommended to regenerate this file in the future when you upgrade to a
3
+ # newer version of cucumber-rails. Consider adding your own code to a new file
4
+ # instead of editing this one. Cucumber will automatically load all features/**/*.rb
5
+ # files.
6
+
7
+ ENV["RAILS_ENV"] ||= "cucumber"
8
+ require File.expand_path(File.dirname(__FILE__) + '/../../cucumber_test_app/config/environment')
9
+
10
+ require 'cucumber/formatter/unicode' # Remove this line if you don't want Cucumber Unicode support
11
+ require 'cucumber/rails/world'
12
+ require 'cucumber/rails/active_record'
13
+ require 'cucumber/web/tableish'
14
+
15
+ require 'webrat'
16
+ require 'webrat/core/matchers'
17
+
18
+ Webrat.configure do |config|
19
+ config.mode = :rails
20
+ config.open_error_files = false # Set to true if you want error pages to pop up in the browser
21
+ end
22
+
23
+
24
+ # If you set this to false, any error raised from within your app will bubble
25
+ # up to your step definition and out to cucumber unless you catch it somewhere
26
+ # on the way. You can make Rails rescue errors and render error pages on a
27
+ # per-scenario basis by tagging a scenario or feature with the @allow-rescue tag.
28
+ #
29
+ # If you set this to true, Rails will rescue all errors and render error
30
+ # pages, more or less in the same way your application would behave in the
31
+ # default production environment. It's not recommended to do this for all
32
+ # of your scenarios, as this makes it hard to discover errors in your application.
33
+ ActionController::Base.allow_rescue = false
34
+
35
+ # If you set this to true, each scenario will run in a database transaction.
36
+ # You can still turn off transactions on a per-scenario basis, simply tagging
37
+ # a feature or scenario with the @no-txn tag. If you are using Capybara,
38
+ # tagging with @culerity or @javascript will also turn transactions off.
39
+ #
40
+ # If you set this to false, transactions will be off for all scenarios,
41
+ # regardless of whether you use @no-txn or not.
42
+ #
43
+ # Beware that turning transactions off will leave data in your database
44
+ # after each scenario, which can lead to hard-to-debug failures in
45
+ # subsequent scenarios. If you do this, we recommend you create a Before
46
+ # block that will explicitly put your database in a known state.
47
+ Cucumber::Rails::World.use_transactional_fixtures = true
48
+
49
+ # How to clean your database when transactions are turned off. See
50
+ # http://github.com/bmabey/database_cleaner for more info.
51
+ require 'database_cleaner'
52
+ DatabaseCleaner.strategy = :truncation
@@ -0,0 +1,47 @@
1
+ module NavigationHelpers
2
+ # Maps a name to a path. Used by the
3
+ #
4
+ # When /^I go to (.+)$/ do |page_name|
5
+ #
6
+ # step definition in web_steps.rb
7
+ #
8
+ def path_to(page_name)
9
+ case page_name
10
+
11
+ when /the home\s?page/
12
+ '/'
13
+
14
+ # the following are examples using path_to_pickle
15
+
16
+ when /^#{capture_model}(?:'s)? page$/ # eg. the forum's page
17
+ path_to_pickle $1
18
+
19
+ when /^#{capture_model}(?:'s)? #{capture_model}(?:'s)? page$/ # eg. the forum's post's page
20
+ path_to_pickle $1, $2
21
+
22
+ when /^#{capture_model}(?:'s)? #{capture_model}'s (.+?) page$/ # eg. the forum's post's comments page
23
+ path_to_pickle $1, $2, :extra => $3 # or the forum's post's edit page
24
+
25
+ when /^#{capture_model}(?:'s)? (.+?) page$/ # eg. the forum's posts page
26
+ path_to_pickle $1, :extra => $2 # or the forum's edit page
27
+
28
+ # Add more mappings here.
29
+ # Here is an example that pulls values out of the Regexp:
30
+ #
31
+ # when /^(.*)'s profile page$/i
32
+ # user_profile_path(User.find_by_login($1))
33
+
34
+ else
35
+ begin
36
+ page_name =~ /the (.*) page/
37
+ path_components = $1.split(/\s+/)
38
+ self.send(path_components.push('path').join('_').to_sym)
39
+ rescue Object => e
40
+ raise "Can't find mapping from \"#{page_name}\" to a path.\n" +
41
+ "Now, go and add a mapping in #{__FILE__}"
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ World(NavigationHelpers)
@@ -0,0 +1,26 @@
1
+ # this file generated by script/generate pickle [paths] [email]
2
+ #
3
+ # Make sure that you are loading your factory of choice in your cucumber environment
4
+ #
5
+ # For machinist add: features/support/machinist.rb
6
+ #
7
+ # require 'machinist/active_record' # or your chosen adaptor
8
+ # require File.dirname(__FILE__) + '/../../spec/blueprints' # or wherever your blueprints are
9
+ # Before { Sham.reset } # to reset Sham's seed between scenarios so each run has same random sequences
10
+ #
11
+ # For FactoryGirl add: features/support/factory_girl.rb
12
+ #
13
+ # require 'factory_girl'
14
+ # require File.dirname(__FILE__) + '/../../spec/factories' # or wherever your factories are
15
+ #
16
+ # You may also need to add gem dependencies on your factory of choice in <tt>config/environments/cucumber.rb</tt>
17
+
18
+ require 'pickle/world'
19
+ # Example of configuring pickle:
20
+ #
21
+ # Pickle.configure do |config|
22
+ # config.adapters = [:machinist]
23
+ # config.map 'I', 'myself', 'me', 'my', :to => 'user: "me"'
24
+ # end
25
+ require 'pickle/path/world'
26
+ require 'pickle/email/world'
@@ -0,0 +1,4 @@
1
+ Pickle.configure do |c|
2
+ c.map 'I', :to => 'user: "me"'
3
+ c.map 'killah fork', :to => 'fancy fork: "of cornwood"'
4
+ end
data/init.rb ADDED
File without changes
@@ -0,0 +1,69 @@
1
+ require 'rails/generators'
2
+
3
+ class PickleGenerator < Rails::Generators::Base
4
+ desc "Generates Pickle step files."
5
+
6
+ # Use the same templates as Rails 2 generator
7
+ source_root File.expand_path("../../../rails_generators/pickle/templates", __FILE__)
8
+
9
+ class_option :paths, :desc => "Generate features/support/paths.rb file.", :type => :boolean
10
+ class_option :email, :desc => "Generate features/step_definitions/email_steps.rb file", :type => :boolean
11
+
12
+ def initialize(args = [], options = {}, config = {})
13
+ super
14
+
15
+ if self.options.paths? && !File.exists?("features/support/paths.rb")
16
+ say "features/support/paths.rb not found, is your cucumber up to date?", :red
17
+ exit
18
+ end
19
+ end
20
+
21
+ def create_directories
22
+ empty_directory "features/step_definitions"
23
+ empty_directory "features/support"
24
+ end
25
+
26
+ def copy_pickle_steps_file
27
+ template "pickle_steps.rb", "features/step_definitions/pickle_steps.rb"
28
+ template "pickle.rb", "features/support/pickle.rb"
29
+ end
30
+
31
+ def copy_paths_file
32
+ return unless options.paths?
33
+
34
+ current_paths = File.read("features/support/paths.rb")
35
+ unless current_paths.include?('#{capture_model}')
36
+ if current_paths =~ /^(.*)(\n\s+else\n\s+raise "Can't find.*".*$)/m
37
+ @current_paths_header = $1
38
+ @current_paths_footer = $2
39
+ end
40
+ template "paths.rb", "features/support/paths.rb"
41
+ end
42
+ end
43
+
44
+ def copy_email_steps_file
45
+ return unless options.email?
46
+ template "email_steps.rb", "features/step_definitions/email_steps.rb"
47
+ template "email.rb", "features/support/email.rb"
48
+ end
49
+
50
+
51
+ private
52
+
53
+ # Compatibility methods for Rails 2 templates
54
+ def pickle_path
55
+ options.paths?
56
+ end
57
+
58
+ def pickle_email
59
+ options.email?
60
+ end
61
+
62
+ def current_paths_header
63
+ @current_paths_header
64
+ end
65
+
66
+ def current_paths_footer
67
+ @current_paths_footer
68
+ end
69
+ end
@@ -0,0 +1,137 @@
1
+ require 'active_support/core_ext'
2
+
3
+ module Pickle
4
+ # Abstract Factory adapter class, if you have a factory type setup, you
5
+ # can easily create an adaptor to make it work with Pickle.
6
+ #
7
+ # The factory adaptor must have a #factories class method that returns
8
+ # its instances, and each instance must respond to:
9
+ #
10
+ # #name : identifies the factory by name (default is attr_reader)
11
+ # #klass : returns the associated model class for this factory (default is attr_reader)
12
+ # #create(attrs = {}) : returns a newly created object
13
+ class Adapter
14
+ attr_reader :name, :klass
15
+
16
+ def create(attrs = {})
17
+ raise NotImplementedError, "create and return an object with the given attributes"
18
+ end
19
+
20
+ if respond_to?(:class_attribute)
21
+ class_attribute :model_classes
22
+ else
23
+ cattr_writer :model_classes
24
+ end
25
+
26
+ self.model_classes = nil
27
+
28
+ # Include this module into your ORM adapter
29
+ # this will register the adapter with pickle and it will be picked up for you
30
+ # To create an adapter you should create an inner constant "PickleAdapter"
31
+ #
32
+ # e.g. ActiveRecord::Base::PickleAdapter
33
+ #
34
+ # @see pickle/adapters/active_record
35
+ # @see pickle/adapters/datamapper
36
+ # @see pickle/adapters/mongoid
37
+ module Base
38
+ def self.included(base)
39
+ adapters << base
40
+ end
41
+
42
+ # A collection of registered adapters
43
+ def self.adapters
44
+ @@adapters ||= []
45
+ end
46
+ end
47
+
48
+ class << self
49
+ def factories
50
+ raise NotImplementedError, "return an array of factory adapter objects"
51
+ end
52
+
53
+ def model_classes
54
+ @@model_classes ||= self::Base.adapters.map{ |a| a.model_classes }.flatten
55
+ end
56
+
57
+ # Returns the column names for the given ORM model class.
58
+ def column_names(klass)
59
+ klass.const_get(:PickleAdapter).column_names(klass)
60
+ end
61
+
62
+ def get_model(klass, id)
63
+ klass.const_get(:PickleAdapter).get_model(klass, id)
64
+ end
65
+
66
+ def find_first_model(klass, conditions)
67
+ klass.const_get(:PickleAdapter).find_first_model(klass, conditions)
68
+ end
69
+
70
+ def find_all_models(klass, conditions)
71
+ klass.const_get(:PickleAdapter).find_all_models(klass, conditions)
72
+ end
73
+
74
+ def create_model(klass, attributes)
75
+ klass.const_get(:PickleAdapter).create_model(klass, attributes)
76
+ end
77
+ end
78
+
79
+ # machinist adapter
80
+ class Machinist < Adapter
81
+ def self.factories
82
+ factories = []
83
+ model_classes.each do |klass|
84
+ if blueprints = klass.instance_variable_get('@blueprints')
85
+ blueprints.keys.each {|blueprint| factories << new(klass, blueprint)}
86
+ end
87
+ end
88
+ factories
89
+ end
90
+
91
+ def initialize(klass, blueprint)
92
+ @klass, @blueprint = klass, blueprint
93
+ @name = @klass.name.underscore.gsub('/','_')
94
+ @name = "#{@blueprint}_#{@name}" unless @blueprint == :master
95
+ end
96
+
97
+ def create(attrs = {})
98
+ if @klass.respond_to?('make!')
99
+ @klass.send(:make!, @blueprint, attrs)
100
+ else
101
+ @klass.send(:make, @blueprint, attrs)
102
+ end
103
+ end
104
+ end
105
+
106
+ # factory-girl adapter
107
+ class FactoryGirl < Adapter
108
+ def self.factories
109
+ (::Factory.factories.values rescue []).map {|factory| new(factory)}
110
+ end
111
+
112
+ def initialize(factory)
113
+ @klass, @name = factory.build_class, factory.factory_name.to_s
114
+ end
115
+
116
+ def create(attrs = {})
117
+ Factory(@name, attrs)
118
+ end
119
+ end
120
+
121
+ # ORM adapter. If you have no factory adapter, you can use this adapter to
122
+ # use your orm as 'factory' - ie create objects
123
+ class Orm < Adapter
124
+ def self.factories
125
+ model_classes.map{|k| new(k)}
126
+ end
127
+
128
+ def initialize(klass)
129
+ @klass, @name = klass, klass.name.underscore.gsub('/','_')
130
+ end
131
+
132
+ def create(attrs = {})
133
+ Pickle::Adapter.create_model(@klass, attrs)
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,57 @@
1
+ begin
2
+ require 'activerecord'
3
+ rescue LoadError
4
+ require 'active_record'
5
+ end
6
+
7
+ class ActiveRecord::Base
8
+ module PickleAdapter
9
+ include Pickle::Adapter::Base
10
+
11
+ # Do not consider these to be part of the class list
12
+ def self.except_classes
13
+ @@except_classes ||= [
14
+ "CGI::Session::ActiveRecordStore::Session",
15
+ "ActiveRecord::SessionStore::Session"
16
+ ]
17
+ end
18
+
19
+ # Gets a list of the available models for this adapter
20
+ def self.model_classes
21
+ begin
22
+ klasses = ::ActiveRecord::Base.__send__(:descendants) # Rails 3
23
+ rescue
24
+ klasses = ::ActiveRecord::Base.__send__(:subclasses) # Rails 2
25
+ end
26
+
27
+ klasses.select do |klass|
28
+ !klass.abstract_class? && klass.table_exists? && !except_classes.include?(klass.name)
29
+ end
30
+ end
31
+
32
+ # get a list of column names for a given class
33
+ def self.column_names(klass)
34
+ klass.column_names
35
+ end
36
+
37
+ # Get an instance by id of the model
38
+ def self.get_model(klass, id)
39
+ klass.find(id)
40
+ end
41
+
42
+ # Find the first instance matching conditions
43
+ def self.find_first_model(klass, conditions)
44
+ klass.find(:first, :conditions => conditions)
45
+ end
46
+
47
+ # Find all models matching conditions
48
+ def self.find_all_models(klass, conditions)
49
+ klass.find(:all, :conditions => conditions)
50
+ end
51
+
52
+ # Create a model using attributes
53
+ def self.create_model(klass, attributes)
54
+ klass.create!(attributes)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,42 @@
1
+ require 'dm-core'
2
+
3
+ module DataMapper::Resource
4
+ module PickleAdapter
5
+ include Pickle::Adapter::Base
6
+
7
+ # Do not consider these to be part of the class list
8
+ def self.except_classes
9
+ @@except_classes ||= []
10
+ end
11
+
12
+ # Gets a list of the available models for this adapter
13
+ def self.model_classes
14
+ ::DataMapper::Model.descendants.to_a.select{|k| !except_classes.include?(k.name)}
15
+ end
16
+
17
+ # get a list of column names for a given class
18
+ def self.column_names(klass)
19
+ klass.properties.map(&:name)
20
+ end
21
+
22
+ # Get an instance by id of the model
23
+ def self.get_model(klass, id)
24
+ klass.get(id)
25
+ end
26
+
27
+ # Find the first instance matching conditions
28
+ def self.find_first_model(klass, conditions)
29
+ klass.first(conditions)
30
+ end
31
+
32
+ # Find all models matching conditions
33
+ def self.find_all_models(klass, conditions)
34
+ klass.all(conditions)
35
+ end
36
+
37
+ # Create a model using attributes
38
+ def self.create_model(klass, attributes)
39
+ klass.create(attributes)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,44 @@
1
+ require 'mongoid'
2
+
3
+ module Mongoid
4
+ module Document
5
+ module PickleAdapter
6
+ include Pickle::Adapter::Base
7
+
8
+ # Do not consider these to be part of the class list
9
+ def self.except_classes
10
+ @@except_classes ||= []
11
+ end
12
+
13
+ # Gets a list of the available models for this adapter
14
+ def self.model_classes
15
+ ObjectSpace.each_object(Class).to_a.select {|klass| klass.ancestors.include? Mongoid::Document}
16
+ end
17
+
18
+ # get a list of column names for a given class
19
+ def self.column_names(klass)
20
+ klass.fields.keys
21
+ end
22
+
23
+ # Get an instance by id of the model
24
+ def self.get_model(klass, id)
25
+ klass.find(id)
26
+ end
27
+
28
+ # Find the first instance matching conditions
29
+ def self.find_first_model(klass, conditions)
30
+ klass.first(:conditions => conditions)
31
+ end
32
+
33
+ # Find all models matching conditions
34
+ def self.find_all_models(klass, conditions)
35
+ klass.all(:conditions => conditions)
36
+ end
37
+
38
+ # Create a model with given attributes
39
+ def self.create_model(klass, attributes)
40
+ klass.create!(attributes)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,49 @@
1
+ module Pickle
2
+ class Config
3
+ attr_writer :adapters, :factories, :mappings, :predicates
4
+
5
+ def initialize(&block)
6
+ configure(&block) if block_given?
7
+ end
8
+
9
+ def configure(&block)
10
+ yield(self)
11
+ end
12
+
13
+ def adapters
14
+ @adapters ||= [:machinist, :factory_girl, :orm]
15
+ end
16
+
17
+ def adapter_classes
18
+ adapters.map {|a| a.is_a?(Class) ? a : "pickle/adapter/#{a}".classify.constantize}
19
+ end
20
+
21
+ def factories
22
+ @factories ||= adapter_classes.reverse.inject({}) do |factories, adapter|
23
+ factories.merge(adapter.factories.inject({}){|h, f| h.merge(f.name => f)})
24
+ end
25
+ end
26
+
27
+ def predicates
28
+ @predicates ||= Pickle::Adapter.model_classes.map do |k|
29
+ k.public_instance_methods.select {|m| m =~ /\?$/} + Pickle::Adapter.column_names(k)
30
+ end.flatten.uniq
31
+ end
32
+
33
+ class Mapping < Struct.new(:search, :replacement)
34
+ end
35
+
36
+ def mappings
37
+ @mappings ||= []
38
+ end
39
+
40
+ # Usage: map 'me', 'myself', 'I', :to => 'user: "me"'
41
+ def map(*args)
42
+ options = args.extract_options!
43
+ raise ArgumentError, "Usage: map 'search' [, 'search2', ...] :to => 'replace'" unless args.any? && options[:to].is_a?(String)
44
+ args.each do |search|
45
+ self.mappings << Mapping.new(search, options[:to])
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,18 @@
1
+ module Pickle
2
+ module Email
3
+ # add ability to parse emails
4
+ module Parser
5
+ def match_email
6
+ "(?:#{match_prefix}?(?:#{match_index} )?email)"
7
+ end
8
+
9
+ def capture_email
10
+ "(#{match_email})"
11
+ end
12
+
13
+ def capture_index_in_email
14
+ "(?:#{match_prefix}?(?:#{capture_index} )?email)"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,13 @@
1
+ require 'pickle'
2
+ require 'pickle/email'
3
+ require 'pickle/email/parser'
4
+
5
+ # add email parser expressions
6
+ Pickle::Parser.send :include, Pickle::Email::Parser
7
+
8
+ World(Pickle::Email)
9
+
10
+ # shortcuts for use in step regexps
11
+ class << self
12
+ delegate :capture_email, :to => 'Pickle.parser'
13
+ end
@@ -0,0 +1,77 @@
1
+ module Pickle
2
+ module Email
3
+ # return the deliveries array, optionally selected by the passed fields
4
+ def emails(fields = nil)
5
+ @emails = ActionMailer::Base.deliveries.select {|m| email_has_fields?(m, fields)}
6
+ end
7
+
8
+ def email(ref, fields = nil)
9
+ (match = ref.match(/^#{capture_index_in_email}$/)) or raise ArgumentError, "argument should match #{match_email}"
10
+ @emails or raise RuntimeError, "Call #emails before calling #email"
11
+ index = parse_index(match[1])
12
+ email_has_fields?(@emails[index], fields) ? @emails[index] : nil
13
+ end
14
+
15
+ def email_has_fields?(email, fields)
16
+ parse_fields(fields).each do |key, val|
17
+ return false unless (Array(email.send(key)) & Array(val)).any?
18
+ end
19
+ true
20
+ end
21
+
22
+ def visit_in_email(email, link_text)
23
+ visit(parse_email_for_link(email, link_text))
24
+ end
25
+
26
+ def click_first_link_in_email(email)
27
+ link = links_in_email(email).first
28
+ visit link
29
+ end
30
+
31
+ protected
32
+ def open_in_browser(path) # :nodoc
33
+ require "launchy"
34
+ Launchy::Browser.run(path)
35
+ rescue LoadError
36
+ warn "Sorry, you need to install launchy to open emails: `gem install launchy`"
37
+ end
38
+
39
+ # Saves the emails out to RAILS_ROOT/tmp/ and opens it in the default
40
+ # web browser if on OS X. (depends on webrat)
41
+ def save_and_open_emails
42
+ emails_to_open = @emails || emails
43
+ filename = "pickle-email-#{Time.now.to_i}.html"
44
+ File.open(filename, "w") do |f|
45
+ emails_to_open.each_with_index do |e, i|
46
+ f.write "<h1>Email #{i+1}</h1><pre>#{e}</pre><hr />"
47
+ end
48
+ end
49
+ open_in_browser(filename)
50
+ end
51
+
52
+ def parse_email_for_link(email, text_or_regex)
53
+ url = parse_email_for_explicit_link(email, text_or_regex)
54
+ url ||= parse_email_for_anchor_text_link(email, text_or_regex)
55
+ raise "No link found matching #{text_or_regex.inspect} in #{email}" unless url
56
+ url
57
+ end
58
+
59
+ # e.g. confirm in http://confirm
60
+ def parse_email_for_explicit_link(email, regex)
61
+ regex = /#{Regexp.escape(regex)}/ unless regex.is_a?(Regexp)
62
+ links_in_email(email).detect { |link| link =~ regex }
63
+ end
64
+
65
+ # e.g. Click here in <a href="http://confirm">Click here</a>
66
+ def parse_email_for_anchor_text_link(email, link_text)
67
+ if match_data = email.encoded.match(%r{<a[^>]*href=['"]?([^'"]*)['"]?[^>]*?>[^<]*?#{link_text}[^<]*?</a>})
68
+ match_data[1]
69
+ end
70
+ end
71
+
72
+ def links_in_email(email, protos=['http', 'https'])
73
+ URI.extract(email.encoded.to_s, protos)
74
+ end
75
+
76
+ end
77
+ end