email_spectacular 1.0.0 → 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0f37914ebc9f8c1fba4ef39afba24d808dce2557
4
- data.tar.gz: 59dd333158d579bd98ba60c68eb795e9a88d380f
3
+ metadata.gz: 9271c7cddf1f63951c54a4685af8f6f8069ee9a8
4
+ data.tar.gz: aa500292e90e1d059b5434348fa6405cabafc02a
5
5
  SHA512:
6
- metadata.gz: 17185632819ec97f045453a508ad419d4ea9bbb8f669415d070ccb249a118fcdaa7656ef7eea29d26062f5895396fba2cc91e65a8a3af396b2721f90cb96cd25
7
- data.tar.gz: 1168413e77f916258af5a793dda207dfa0d98c4cda0e055c7e95e944393807faf175618eca0511e0d4083ec459b9ec54a110e886355479d62beaedd7c5f06b97
6
+ metadata.gz: 23a2af089ff52cce59e7c0922b933e34b1e055a0e7b87f2e14d0f06aebbc63e9cc76e8bb5a6c01ff5e1d306b71860f0a720c22e3e5e286db606703c7ae950070
7
+ data.tar.gz: 2d5f76c70907cf4d980c55e4712034e9c1d8e4368c3d3b730fc00c783ac54159669cbb9e51c10bfe70f3eeb1327383b01d30e326eea7289c50b541c6f06f0f26
data/README.md CHANGED
@@ -1,4 +1,7 @@
1
- # EmailSpectacular
1
+ <p align="center">
2
+ <img src="https://svgshare.com/i/CSb.svg" width="200px"><br/>
3
+ <h2 align="center">EmailSpectacular</h2>
4
+ </p>
2
5
 
3
6
  [![Gem](https://img.shields.io/gem/dt/email_spectacular.svg)]()
4
7
  [![Build Status](https://travis-ci.org/greena13/email_spectacular.svg)](https://travis-ci.org/greena13/email_spectacular)
@@ -6,6 +9,16 @@
6
9
 
7
10
  High-level email spec helpers for acceptance, feature and request tests.
8
11
 
12
+ ## Basic Usage
13
+
14
+ ```ruby
15
+ it 'does many things, including sending an email' do
16
+ # ...
17
+
18
+ expect(email).to have_been_sent.to('user@email.com')
19
+ end
20
+ ```
21
+
9
22
  ## What EmailSpectacular is
10
23
 
11
24
  Expressive email assertions that let you succinctly describe when emails should and should not be sent.
@@ -27,22 +40,19 @@ end
27
40
  Add `email_spectacular` to your `spec/rails_helper.rb`
28
41
 
29
42
  ```ruby
30
-
31
- require 'email_spectacular'
32
-
33
- # ...
43
+ require 'email_spectacular/rspec'
34
44
 
35
45
  RSpec.configure do |config|
36
46
  # ...
37
47
 
38
48
  email_spectacular_spec_types = %i[acceptance feature request]
39
-
40
- config.after(:each, type: email_spectacular_spec_types) do
41
- # Clear emails between specs
42
- clear_emails
43
- end
44
-
49
+
45
50
  email_spectacular_spec_types.each do |spec_type|
51
+ config.after(:each, type: spec_type) do
52
+ # Clear emails between specs
53
+ clear_emails
54
+ end
55
+
46
56
  # Include email spectacular syntax in rspec tests
47
57
  config.include EmailSpectacular::RSpec, type: spec_type
48
58
  end
@@ -53,6 +63,46 @@ And then execute:
53
63
 
54
64
  ```bash
55
65
  bundle install
66
+ ```
67
+
68
+ ### Configuration
69
+
70
+ Email Spectacular is configured using the `configure` method. It's suggested you place this in your `spec/rails_helper.rb` file, after you require `email_specatular`:
71
+
72
+
73
+ ```ruby
74
+ require 'email_spectacular/rspec'
75
+
76
+ EmailSpectacular.configure do |config|
77
+ # Configuration here
78
+ end
79
+ ```
80
+
81
+ #### Setting the name of the email helper
82
+
83
+ By default, Email Spectacular makes a `email` helper available for your expectation syntax (all examples below assume this default helper), however if this conflicts with anything in your test suite or is not preferred, you can specify a different helper name:
84
+
85
+ ```ruby
86
+ EmailSpectacular.configure do |config|
87
+ config.helper_name = :an_email # Default is 'email'
88
+ end
89
+ ```
90
+
91
+ #### Working with enqueued emails
92
+
93
+ If your emails are not sent immediately in your application - using `deliver_later` - you must mock this method in test mode so they appear to have sent to Email Spectacular, which is enabled using the `mock_sending_enqueued_emails` option:
94
+
95
+ ```ruby
96
+ EmailSpectacular.configure do |config|
97
+ # Mocks the enqueueing of emails so they appear in the list of sent email
98
+ config.mock_sending_enqueued_emails = true
99
+ end
100
+ ```
101
+
102
+ This then enables the assertion `have_been_enqueued`, which has the same arguments and behaviour as `have_been_sent`, but will verify the email has been enqueued rather than sent immediately:
103
+
104
+ ```ruby
105
+ expect(email).to have_been_enqueued.to('user@email.com')
56
106
  ```
57
107
 
58
108
  ## Usage
@@ -145,6 +195,16 @@ Emails can be cleared at any point by calling `clear_emails` in your tests. This
145
195
 
146
196
  If you followed in installation steps above, emails will automatically be cleared between each spec.
147
197
 
198
+ ## Gotchas and Troubleshooting
199
+
200
+ EmailSpectacular expects your application to configure `ActionMailer` to store emails in the `ActionMailer::Base.deliveries` array.
201
+
202
+ In a Rails app, this is done (automatically, by default) in your environment file: `config/environment/test.rb`
203
+
204
+ ```ruby
205
+ config.action_mailer.delivery_method = :test
206
+ ```
207
+
148
208
  ## Test suite
149
209
 
150
210
  `email_spectacular` comes with close-to-complete test coverage. You can run the test suite as follows:
@@ -21,12 +21,12 @@ Gem::Specification.new do |spec|
21
21
  spec.test_files = spec.files.grep(%r{^(spec)/})
22
22
  spec.require_paths = ['lib']
23
23
 
24
- spec.add_dependency 'actionmailer', '>= 0'
24
+ spec.add_dependency 'actionmailer'
25
25
  spec.add_dependency 'capybara', '~> 2.5', '>= 2.5.0'
26
26
 
27
- spec.add_development_dependency 'bundler', '~> 1.6'
27
+ spec.add_development_dependency 'bundler', '~> 2'
28
28
  spec.add_development_dependency 'guard', '~> 2.1'
29
29
  spec.add_development_dependency 'guard-rspec', '~> 4.7'
30
- spec.add_development_dependency 'rake', '~> 0'
30
+ spec.add_development_dependency 'rake', '>= 12.3.3'
31
31
  spec.add_development_dependency 'rspec', '>= 3.5.0'
32
32
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'email_spectacular/version'
4
+ require 'email_spectacular/adaptors/action_mailer_adaptor'
5
+ require 'email_spectacular/extensions/action_mailer_extension'
4
6
 
5
7
  # High-level email spec helpers for acceptance, feature and request tests.
6
8
  #
@@ -8,4 +10,25 @@ require 'email_spectacular/version'
8
10
  #
9
11
  # @see https://github.com/greena13/email_spectacular EmailSpectacular Github page
10
12
  module EmailSpectacular
13
+ class << self
14
+ def helper_name=(method_name)
15
+ EmailSpectacular::ActionMailerAdaptor.alias_method method_name, :email
16
+ EmailSpectacular::ActionMailerAdaptor.remove_method :email
17
+ end
18
+
19
+ def mock_sending_enqueued_emails=(enabled)
20
+ return unless enabled
21
+
22
+ @_mocking_sending_enqueued_emails = true
23
+ ActionMailer::MessageDelivery.include(EmailSpectacular::ActionMailerExtension)
24
+ end
25
+
26
+ attr_reader :_mocking_sending_enqueued_emails
27
+
28
+ def configure
29
+ if block_given?
30
+ yield(EmailSpectacular)
31
+ end
32
+ end
33
+ end
11
34
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EmailSpectacular
4
+ module ActionMailerAdaptor
5
+ # Syntactic sugar for referencing the list of emails sent since the start of the
6
+ # test
7
+ #
8
+ # @example Asserting email has been sent
9
+ # expect(email).to have_been_sent.to('test@email.com')
10
+ #
11
+ # @return [Array<Mail::Message>] List of sent emails
12
+ def email
13
+ ActionMailer::Base.deliveries
14
+ end
15
+
16
+ # Clears the list of sent emails.
17
+ #
18
+ # @return void
19
+ def clear_emails
20
+ ActionMailer::Base.deliveries = []
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara'
4
+
5
+ module EmailSpectacular
6
+ # Module for parsing email bodies
7
+ #
8
+ # @author Aleck Greenham
9
+ module CapybaraAdaptor
10
+ def parsed_email_parts(email)
11
+ email_parts_as_hash(email) do |email_part|
12
+ parse(email_part)
13
+ end
14
+ end
15
+
16
+ def raw_email_parts(email)
17
+ email_parts_as_hash(email)
18
+ end
19
+
20
+ private
21
+
22
+ def email_parts_as_hash(email)
23
+ if email.parts.any?
24
+ email.parts.each_with_object({}) do |email_part, memo|
25
+ decoded = email_part.body.decoded
26
+ memo[content_type_key(email_part)] = block_given? ? yield(decoded) : decoded
27
+ end
28
+ else
29
+ encoded = email.body.encoded
30
+ { content_type_key(email) => block_given? ? yield(encoded) : encoded }
31
+ end
32
+ end
33
+
34
+ def parse(target)
35
+ Capybara::Node::Simple.new(target)
36
+ end
37
+
38
+ def content_type_key(target)
39
+ target.content_type.split(';').first
40
+ end
41
+ end
42
+ end
@@ -7,9 +7,10 @@ module EmailSpectacular
7
7
  module DSL
8
8
  def self.included(base) # rubocop:disable Metrics/MethodLength
9
9
  base.class_eval do
10
- def initialize
10
+ def initialize(options = {})
11
11
  @scopes = {}
12
12
  @and_scope = nil
13
+ @enqueued = options[:enqueued]
13
14
  end
14
15
 
15
16
  # Allows chaining two assertions on the same email attribute together without
@@ -130,7 +131,7 @@ module EmailSpectacular
130
131
  #
131
132
  # @param [String] selector CSS selector that should match at least one sent
132
133
  # email's body
133
- # @return [EmailSpectacular::Expectation] reference to self, to allow for
134
+ # @return [EmailSpectacular::RSpecMatcher] reference to self, to allow for
134
135
  # further method chaining
135
136
  def matching_selector(selector)
136
137
  @scopes[:matching_selector] ||= []
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'email_spectacular/parser'
4
- require 'email_spectacular/matchers'
3
+ require 'email_spectacular/adaptors/capybara_adaptor'
4
+ require 'email_spectacular/concerns/matchers'
5
5
 
6
6
  module EmailSpectacular
7
7
  # Module containing the helper methods to describe the difference between the expected
@@ -9,7 +9,7 @@ module EmailSpectacular
9
9
  #
10
10
  # @author Aleck Greenham
11
11
  module FailureDescriptions # rubocop:disable Metrics/ModuleLength
12
- include Parser
12
+ include CapybaraAdaptor
13
13
 
14
14
  def self.included(base) # rubocop:disable Metrics/MethodLength
15
15
  base.class_eval do
@@ -19,7 +19,9 @@ module EmailSpectacular
19
19
  scopes.each do |attribute, expected|
20
20
  matching_emails =
21
21
  emails.select do |email|
22
- email_matches?(email, EmailSpectacular::Matchers::MATCHERS[attribute], expected)
22
+ email_matches?(email, EmailSpectacular::Matchers::MATCHERS[attribute], expected) &&
23
+ (!EmailSpectacular._mocking_sending_enqueued_emails ||
24
+ email.instance_variable_get(:@enqueued) == @enqueued)
23
25
  end
24
26
 
25
27
  return [attribute, expected] if matching_emails.empty?
@@ -28,23 +30,30 @@ module EmailSpectacular
28
30
  [nil, nil]
29
31
  end
30
32
 
31
- def describe_failed_assertion(emails, attribute_name, attribute_value)
33
+ def describe_failed_assertion(attribute_name, attribute_value)
34
+ action = mail_action_description
35
+
32
36
  field_descriptions = attribute_descriptions([attribute_name])
33
37
  value_descriptions = value_descriptions([attribute_value])
34
38
 
35
39
  base_clause = expectation_description(
36
- 'Expected an email to be sent',
40
+ "Expected an email to be #{action}",
37
41
  field_descriptions,
38
42
  value_descriptions
39
43
  )
40
44
 
41
- if emails.empty?
42
- "#{base_clause} However, no emails were sent."
45
+ if @emails.empty?
46
+ "#{base_clause} However, no emails were #{action}."
47
+ elsif @matching_emails[:sent].any? || @matching_emails[:enqueued].any?
48
+ opposite_action = @enqueued ? 'sent' : 'enqueued'
49
+ "#{base_clause} However, it was #{opposite_action} instead."
43
50
  else
44
- email_values = sent_email_values(emails, attribute_name)
51
+ field_descriptions = attribute_descriptions([attribute_name])
52
+
53
+ email_values = sent_email_values(@emails, attribute_name)
45
54
 
46
55
  if email_values.any?
47
- base_clause + " However, #{email_pluralisation(emails)} sent " \
56
+ base_clause + " However, #{email_pluralisation(@emails)} #{action} " \
48
57
  "#{result_description(field_descriptions, [to_sentence(email_values)])}."
49
58
  else
50
59
  base_clause
@@ -92,6 +101,14 @@ module EmailSpectacular
92
101
 
93
102
  private
94
103
 
104
+ def mail_action_description
105
+ if EmailSpectacular._mocking_sending_enqueued_emails
106
+ @enqueued ? 'enqueued' : 'sent'
107
+ else
108
+ 'sent'
109
+ end
110
+ end
111
+
95
112
  def result_description(field_descriptions, values)
96
113
  to_sentence(
97
114
  field_descriptions.map.with_index do |field_description, index|
@@ -109,7 +126,11 @@ module EmailSpectacular
109
126
  def sent_email_values(emails, attribute)
110
127
  emails.each_with_object([]) do |email, memo|
111
128
  if %i[matching_selector with_link with_image].include?(attribute)
112
- memo << email_body(email)
129
+ memo << raw_email_parts(email).inject([]) do |description, (content_type, raw_email_part)|
130
+ description.push(
131
+ "\n\n(Content Type #{content_type}):\n\n#{raw_email_part}"
132
+ )
133
+ end.join('')
113
134
  else
114
135
  matcher = EmailSpectacular::Matchers::MATCHERS[attribute]
115
136
 
@@ -118,12 +139,19 @@ module EmailSpectacular
118
139
  when String, Symbol
119
140
  email.send(matcher)
120
141
  when Hash
121
- matcher[:actual].call(email, parsed_emails(email))
142
+ parsed_email_parts(email).inject([]) do |description, (content_type, parsed_email_part)|
143
+ description.push(
144
+ "\n\n(Content Type #{content_type}):\n\n#{matcher[:actual].call(email, parsed_email_part)}"
145
+ )
146
+ end.join('')
122
147
  else
123
148
  raise ArgumentError, "Failure related to an unknown or unsupported email attribute #{attribute}"
124
149
  end
125
150
 
126
- value = value.is_a?(String) ? "'#{value}'" : value.map { |element| "'#{element}'" }
151
+ unless attribute == :with_text
152
+ value = value.is_a?(String) ? "'#{value}'" : value.map { |element| "'#{element}'" }
153
+ end
154
+
127
155
  memo << value
128
156
  end
129
157
  end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'email_spectacular/adaptors/capybara_adaptor'
4
+
5
+ module EmailSpectacular
6
+ # Module containing helper methods for matching expectations against emails
7
+ #
8
+ # @author Aleck Greenham
9
+ module Matchers
10
+ include CapybaraAdaptor
11
+
12
+ MATCHERS = {
13
+ to: :to,
14
+ from: :from,
15
+ with_subject: :subject,
16
+ with_text: {
17
+ match: lambda { |_, email_parts, value|
18
+ value.all? { |text| email_parts.values.any? { |email_part| email_part.has_text?(text) } }
19
+ },
20
+ actual: ->(_, parsed_email_part) { parsed_email_part.text }
21
+ },
22
+ matching_selector: {
23
+ match: lambda { |_, email_parts, value|
24
+ email_parts.values.any? { |email_part| value.all? { |selector| email_part.has_selector?(selector) } }
25
+ },
26
+ actual: ->(_, parsed_email_part) { parsed_email_part.native },
27
+ actual_name: :with_body
28
+ },
29
+ with_link: {
30
+ match: lambda { |_, email_parts, value|
31
+ email_parts.values.any? { |email_part| value.all? { |url| email_part.has_selector?("a[href='#{url}']") } }
32
+ },
33
+ actual: ->(_, parsed_email_part) { parsed_email_part.native },
34
+ actual_name: :with_body
35
+ },
36
+ with_image: {
37
+ match: lambda { |_, email_parts, value|
38
+ email_parts.values.any? { |email_part| value.all? { |url| email_part.has_selector?("img[src='#{url}']") } }
39
+ },
40
+ actual: ->(_, parsed_email_part) { parsed_email_part.native },
41
+ actual_name: :with_body
42
+ }
43
+ }.freeze
44
+
45
+ def self.included(base) # rubocop:disable Metrics/MethodLength
46
+ base.class_eval do
47
+ def matching_emails(emails, scopes)
48
+ emails.each_with_object(sent: [], enqueued: []) do |email, memo|
49
+ matches_scopes = scopes.all? do |attribute, expected|
50
+ email_matches?(email, MATCHERS[attribute], expected)
51
+ end
52
+
53
+ if matches_scopes
54
+ if email.instance_variable_get(:@enqueued)
55
+ memo[:enqueued] << email
56
+ else
57
+ memo[:sent] << email
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ def email_matches?(email, assertion, expected)
64
+ case assertion
65
+ when :to
66
+ !(expected & email.send(assertion)).empty?
67
+ when String, Symbol
68
+ email.send(assertion).include?(expected)
69
+ when Hash
70
+ assertion[:match].call(email, parsed_email_parts(email), expected)
71
+ else
72
+ raise "Unsupported assertion mapping '#{assertion}' of type #{assertion.class.name}"
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'email_spectacular/dsl'
4
- require 'email_spectacular/failure_descriptions'
5
- require 'email_spectacular/matchers'
3
+ require 'email_spectacular/concerns/dsl'
4
+ require 'email_spectacular/concerns/failure_descriptions'
5
+ require 'email_spectacular/concerns/matchers'
6
6
 
7
7
  module EmailSpectacular
8
8
  class EmailFilter
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EmailSpectacular
4
+ # Extensions to ActionMailer::MessageDelivery to mock the enqueuing of emails
5
+ module ActionMailerExtension
6
+ def self.included(base)
7
+ base.class_eval do
8
+ def deliver_later(options = {})
9
+ message.instance_variable_set(:@enqueued, true)
10
+ deliver_now
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,39 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'email_spectacular/expectation'
3
+ require 'email_spectacular.rb'
4
+ require 'email_spectacular/rspec_matcher'
5
+ require 'email_spectacular/adaptors/action_mailer_adaptor'
4
6
 
5
7
  module EmailSpectacular
6
8
  # Module containing email helper methods that can be mixed into the RSpec test scope
7
9
  #
8
10
  # @author Aleck Greenham
9
11
  module RSpec
10
- # Syntactic sugar for referencing the list of emails sent since the start of the
11
- # test
12
+ include ActionMailerAdaptor
13
+
14
+ # Creates a new email expectation that allows asserting emails should have specific
15
+ # attributes, applied only to send emails.
16
+ #
17
+ # @see EmailSpectacular::Expectation
12
18
  #
13
19
  # @example Asserting email has been sent
14
20
  # expect(email).to have_been_sent.to('test@email.com')
15
- #
16
- # @return [Array<Mail::Message>] List of sent emails
17
- def email
18
- ActionMailer::Base.deliveries
19
- end
20
-
21
- # Clears the list of sent emails.
22
- #
23
- # @return void
24
- def clear_emails
25
- ActionMailer::Base.deliveries = []
21
+ def have_been_sent # rubocop:disable Naming/PredicateName
22
+ EmailSpectacular::RSpecMatcher.new(enqueued: false)
26
23
  end
27
24
 
28
25
  # Creates a new email expectation that allows asserting emails should have specific
29
- # attributes.
26
+ # attributes, applied only to emails that have been enqueued to be sent.
30
27
  #
31
28
  # @see EmailSpectacular::Expectation
32
29
  #
33
- # @example Asserting email has been sent
34
- # expect(email).to have_been_sent.to('test@email.com')
35
- def have_been_sent # rubocop:disable Naming/PredicateName
36
- EmailSpectacular::Expectation.new
30
+ # @example Asserting email has been enqueued
31
+ # expect(email).to have_been_enqueued.to('test@email.com')
32
+ def have_been_enqueued # rubocop:disable Naming/PredicateName
33
+ unless EmailSpectacular._mocking_sending_enqueued_emails
34
+ raise 'EmailSpectacular: Cannot use the have_been_enqueued assertion without setting the ' \
35
+ 'mock_sending_enqueued_emails configuration option.'
36
+ end
37
+
38
+ EmailSpectacular::RSpecMatcher.new(enqueued: true)
37
39
  end
38
40
  end
39
41
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'email_spectacular/email_filter'
3
+ require 'email_spectacular/concerns/dsl'
4
+ require 'email_spectacular/concerns/failure_descriptions'
5
+ require 'email_spectacular/concerns/matchers'
4
6
 
5
7
  module EmailSpectacular
6
8
  # Backing class for {#have_been_sent} declarative syntax for specifying email
@@ -13,14 +15,10 @@ module EmailSpectacular
13
15
  #
14
16
  # @see EmailSpectacular::RSpec#email
15
17
  # @see EmailSpectacular::RSpec#have_been_sent
16
- class Expectation < EmailFilter
17
- # Creates a new EmailSpectacular::Expectation object
18
- #
19
- # @return [EmailSpectacular::Expectation] new expectation object
20
- def initialize
21
- @failure_message = 'Expected email to be sent'
22
- super
23
- end
18
+ class RSpecMatcher
19
+ include DSL
20
+ include Matchers
21
+ include FailureDescriptions
24
22
 
25
23
  # Declares that RSpec should not attempt to diff the actual and expected values
26
24
  # to put in the failure message. This class takes care of diffing and presenting
@@ -37,7 +35,8 @@ module EmailSpectacular
37
35
  # @return [Boolean] True when a matching email was sent
38
36
  def matches?(emails)
39
37
  @emails = emails
40
- matching_emails(emails, @scopes).any?
38
+ @matching_emails = matching_emails(emails, @scopes)
39
+ (@enqueued ? @matching_emails[:enqueued] : @matching_emails[:sent]).any?
41
40
  end
42
41
 
43
42
  # Message to display to StdOut by RSpec if the equality check fails. Includes a
@@ -58,11 +57,7 @@ module EmailSpectacular
58
57
  attribute, expected_value =
59
58
  attribute_and_expected_value(@scopes, @emails)
60
59
 
61
- describe_failed_assertion(
62
- @emails,
63
- attribute,
64
- expected_value
65
- )
60
+ describe_failed_assertion(attribute, expected_value)
66
61
  end
67
62
 
68
63
  # Failure message to display for negative RSpec assertions, i.e.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EmailSpectacular
4
- VERSION = '1.0.0'
4
+ VERSION = '1.1.0'
5
5
  end
@@ -249,7 +249,13 @@ RSpec.describe 'have_sent_email' do
249
249
  expect do
250
250
  expect(subject).to have_been_sent.with_text('Other text')
251
251
  end.to raise_error.with_message(
252
- "Expected an email to be sent with text 'Other text'. However, 1 was sent with text '#{subject[0].text}'."
252
+ <<~MSG.chomp
253
+ Expected an email to be sent with text 'Other text'. However, 1 was sent with text
254
+
255
+ (Content Type text/html):
256
+
257
+ #{subject[0].text}.
258
+ MSG
253
259
  )
254
260
  end
255
261
 
@@ -263,7 +269,13 @@ RSpec.describe 'have_sent_email' do
263
269
  expect do
264
270
  expect(subject).to have_been_sent.matching_selector('.other')
265
271
  end.to raise_error.with_message(
266
- "Expected an email to be sent matching selector '.other'. However, 1 was sent with body #{subject[0].body}."
272
+ <<~MSG.chomp
273
+ Expected an email to be sent matching selector '.other'. However, 1 was sent with body
274
+
275
+ (Content Type text/html):
276
+
277
+ #{subject[0].body}.
278
+ MSG
267
279
  )
268
280
  end
269
281
 
@@ -277,7 +289,13 @@ RSpec.describe 'have_sent_email' do
277
289
  expect do
278
290
  expect(subject).to have_been_sent.with_link('www.other.com')
279
291
  end.to raise_error.with_message(
280
- "Expected an email to be sent with link 'www.other.com'. However, 1 was sent with body #{subject[0].body}."
292
+ <<~MSG.chomp
293
+ Expected an email to be sent with link 'www.other.com'. However, 1 was sent with body
294
+
295
+ (Content Type text/html):
296
+
297
+ #{subject[0].body}.
298
+ MSG
281
299
  )
282
300
  end
283
301
 
@@ -291,7 +309,13 @@ RSpec.describe 'have_sent_email' do
291
309
  expect do
292
310
  expect(subject).to have_been_sent.with_image('www.other.com')
293
311
  end.to raise_error.with_message(
294
- "Expected an email to be sent with image 'www.other.com'. However, 1 was sent with body #{subject[0].body}."
312
+ <<~MSG.chomp
313
+ Expected an email to be sent with image 'www.other.com'. However, 1 was sent with body
314
+
315
+ (Content Type text/html):
316
+
317
+ #{subject[0].body}.
318
+ MSG
295
319
  )
296
320
  end
297
321
 
@@ -387,8 +411,13 @@ RSpec.describe 'have_sent_email' do
387
411
  expect do
388
412
  expect(subject).to have_been_sent.with_text('Other').and('Email')
389
413
  end.to raise_error.with_message(
390
- "Expected an email to be sent with text 'Other' and 'Email'. However, 1 was " \
391
- "sent with text '#{subject[0].text}'."
414
+ <<~MSG.chomp
415
+ Expected an email to be sent with text 'Other' and 'Email'. However, 1 was sent with text
416
+
417
+ (Content Type text/html):
418
+
419
+ #{subject[0].text}.
420
+ MSG
392
421
  )
393
422
  end
394
423
 
@@ -396,7 +425,13 @@ RSpec.describe 'have_sent_email' do
396
425
  expect do
397
426
  expect(subject).to have_been_sent.with_text('Test').and('Other')
398
427
  end.to raise_error.with_message(
399
- "Expected an email to be sent with text 'Test' and 'Other'. However, 1 was sent with text '#{subject[0].text}'."
428
+ <<~MSG.chomp
429
+ Expected an email to be sent with text 'Test' and 'Other'. However, 1 was sent with text
430
+
431
+ (Content Type text/html):
432
+
433
+ #{subject[0].text}.
434
+ MSG
400
435
  )
401
436
  end
402
437
 
@@ -42,4 +42,8 @@ class EmailBodyMock
42
42
  def decoded
43
43
  @text
44
44
  end
45
+
46
+ def content_type
47
+ 'text/html; charset=UTF-8'
48
+ end
45
49
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: email_spectacular
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aleck Greenham
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-04-11 00:00:00.000000000 Z
11
+ date: 2020-04-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionmailer
@@ -50,14 +50,14 @@ dependencies:
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: '1.6'
53
+ version: '2'
54
54
  type: :development
55
55
  prerelease: false
56
56
  version_requirements: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: '1.6'
60
+ version: '2'
61
61
  - !ruby/object:Gem::Dependency
62
62
  name: guard
63
63
  requirement: !ruby/object:Gem::Requirement
@@ -90,16 +90,16 @@ dependencies:
90
90
  name: rake
91
91
  requirement: !ruby/object:Gem::Requirement
92
92
  requirements:
93
- - - "~>"
93
+ - - ">="
94
94
  - !ruby/object:Gem::Version
95
- version: '0'
95
+ version: 12.3.3
96
96
  type: :development
97
97
  prerelease: false
98
98
  version_requirements: !ruby/object:Gem::Requirement
99
99
  requirements:
100
- - - "~>"
100
+ - - ">="
101
101
  - !ruby/object:Gem::Version
102
- version: '0'
102
+ version: 12.3.3
103
103
  - !ruby/object:Gem::Dependency
104
104
  name: rspec
105
105
  requirement: !ruby/object:Gem::Requirement
@@ -134,13 +134,15 @@ files:
134
134
  - Rakefile
135
135
  - email_spectacular.gemspec
136
136
  - lib/email_spectacular.rb
137
- - lib/email_spectacular/dsl.rb
137
+ - lib/email_spectacular/adaptors/action_mailer_adaptor.rb
138
+ - lib/email_spectacular/adaptors/capybara_adaptor.rb
139
+ - lib/email_spectacular/concerns/dsl.rb
140
+ - lib/email_spectacular/concerns/failure_descriptions.rb
141
+ - lib/email_spectacular/concerns/matchers.rb
138
142
  - lib/email_spectacular/email_filter.rb
139
- - lib/email_spectacular/expectation.rb
140
- - lib/email_spectacular/failure_descriptions.rb
141
- - lib/email_spectacular/matchers.rb
142
- - lib/email_spectacular/parser.rb
143
+ - lib/email_spectacular/extensions/action_mailer_extension.rb
143
144
  - lib/email_spectacular/rspec.rb
145
+ - lib/email_spectacular/rspec_matcher.rb
144
146
  - lib/email_spectacular/version.rb
145
147
  - spec/email_expectation_spec.rb
146
148
  - spec/spec_helper.rb
@@ -1,66 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'email_spectacular/parser'
4
-
5
- module EmailSpectacular
6
- # Module containing helper methods for matching expectations against emails
7
- #
8
- # @author Aleck Greenham
9
- module Matchers
10
- include Parser
11
-
12
- MATCHERS = {
13
- to: :to,
14
- from: :from,
15
- with_subject: :subject,
16
- with_text: {
17
- match: ->(_, email, value) { value.all? { |text| email.has_text?(text) } },
18
- actual: ->(_, email) { email.text }
19
- },
20
- matching_selector: {
21
- match: ->(_, email, value) { value.all? { |selector| email.has_selector?(selector) } },
22
- actual: ->(_, email) { email.native },
23
- actual_name: :with_body
24
- },
25
- with_link: {
26
- match: ->(_, email, value) { value.all? { |url| email.has_selector?("a[href='#{url}']") } },
27
- actual: ->(_, email) { email.native },
28
- actual_name: :with_body
29
- },
30
- with_image: {
31
- match: ->(_, email, value) { value.all? { |url| email.has_selector?("img[src='#{url}']") } },
32
- actual: ->(_, email) { email.native },
33
- actual_name: :with_body
34
- }
35
- }.freeze
36
-
37
- def self.included(base) # rubocop:disable Metrics/MethodLength
38
- base.class_eval do
39
- def matching_emails(emails, scopes)
40
- if scopes.any?
41
- emails.select do |email|
42
- scopes.all? do |attribute, expected|
43
- email_matches?(email, MATCHERS[attribute], expected)
44
- end
45
- end
46
- else
47
- emails
48
- end
49
- end
50
-
51
- def email_matches?(email, assertion, expected)
52
- case assertion
53
- when :to
54
- !(expected & email.send(assertion)).empty?
55
- when String, Symbol
56
- email.send(assertion).include?(expected)
57
- when Hash
58
- assertion[:match].call(email, parsed_emails(email), expected)
59
- else
60
- raise "Unsupported assertion mapping '#{assertion}' of type #{assertion.class.name}"
61
- end
62
- end
63
- end
64
- end
65
- end
66
- end
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'capybara'
4
-
5
- module EmailSpectacular
6
- # Module for parsing email bodies
7
- #
8
- # @author Aleck Greenham
9
- module Parser
10
- def parsed_emails(email)
11
- parser(email)
12
- end
13
-
14
- def parser(email)
15
- Capybara::Node::Simple.new(email_body(email))
16
- end
17
-
18
- def email_body(email)
19
- if email.parts.first
20
- email.parts.first.body.decoded
21
- else
22
- email.body.encoded
23
- end
24
- end
25
- end
26
- end