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 +4 -4
- data/README.md +71 -11
- data/email_spectacular.gemspec +3 -3
- data/lib/email_spectacular.rb +23 -0
- data/lib/email_spectacular/adaptors/action_mailer_adaptor.rb +23 -0
- data/lib/email_spectacular/adaptors/capybara_adaptor.rb +42 -0
- data/lib/email_spectacular/{dsl.rb → concerns/dsl.rb} +3 -2
- data/lib/email_spectacular/{failure_descriptions.rb → concerns/failure_descriptions.rb} +41 -13
- data/lib/email_spectacular/concerns/matchers.rb +78 -0
- data/lib/email_spectacular/email_filter.rb +3 -3
- data/lib/email_spectacular/extensions/action_mailer_extension.rb +15 -0
- data/lib/email_spectacular/rspec.rb +21 -19
- data/lib/email_spectacular/{expectation.rb → rspec_matcher.rb} +10 -15
- data/lib/email_spectacular/version.rb +1 -1
- data/spec/email_expectation_spec.rb +42 -7
- data/spec/support/email_mock.rb +4 -0
- metadata +15 -13
- data/lib/email_spectacular/matchers.rb +0 -66
- data/lib/email_spectacular/parser.rb +0 -26
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9271c7cddf1f63951c54a4685af8f6f8069ee9a8
|
4
|
+
data.tar.gz: aa500292e90e1d059b5434348fa6405cabafc02a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 23a2af089ff52cce59e7c0922b933e34b1e055a0e7b87f2e14d0f06aebbc63e9cc76e8bb5a6c01ff5e1d306b71860f0a720c22e3e5e286db606703c7ae950070
|
7
|
+
data.tar.gz: 2d5f76c70907cf4d980c55e4712034e9c1d8e4368c3d3b730fc00c783ac54159669cbb9e51c10bfe70f3eeb1327383b01d30e326eea7289c50b541c6f06f0f26
|
data/README.md
CHANGED
@@ -1,4 +1,7 @@
|
|
1
|
-
|
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
|
[]()
|
4
7
|
[](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:
|
data/email_spectacular.gemspec
CHANGED
@@ -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'
|
24
|
+
spec.add_dependency 'actionmailer'
|
25
25
|
spec.add_dependency 'capybara', '~> 2.5', '>= 2.5.0'
|
26
26
|
|
27
|
-
spec.add_development_dependency 'bundler', '~>
|
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', '
|
30
|
+
spec.add_development_dependency 'rake', '>= 12.3.3'
|
31
31
|
spec.add_development_dependency 'rspec', '>= 3.5.0'
|
32
32
|
end
|
data/lib/email_spectacular.rb
CHANGED
@@ -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::
|
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/
|
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
|
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(
|
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
|
-
|
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
|
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
|
-
|
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)}
|
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 <<
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
11
|
-
|
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
|
-
|
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
|
34
|
-
# expect(email).to
|
35
|
-
def
|
36
|
-
EmailSpectacular
|
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/
|
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
|
17
|
-
|
18
|
-
|
19
|
-
|
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)
|
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.
|
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
391
|
-
|
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
|
-
|
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
|
|
data/spec/support/email_mock.rb
CHANGED
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.
|
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:
|
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: '
|
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: '
|
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:
|
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:
|
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/
|
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/
|
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
|