test_assistant 0.1.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9311d43bef33250abf4b13c6f10cfa03df96f8ed
4
- data.tar.gz: 1fb3475a4bed626f4a5744724c340ac8702888b4
3
+ metadata.gz: 8304109fa9e5a8dcd0e7664b4034699179311eb5
4
+ data.tar.gz: 847f3f376f40a1a4665e4a5866131488a2f04278
5
5
  SHA512:
6
- metadata.gz: '00185e2575b08c763500c145db50601143a73da0fa669c40eb70f7cc4e9156905c9b49ab13b3d4c6db8c7fe27fb82b60d9bd1d7a87df21c9472034e83c893609'
7
- data.tar.gz: e76955c99b919c852f83de86ebb6ee173ac41acc8dbff90b8100d5784f0847fc35f09c4f03a0b3d61ee672a479aa51ba2f1f37f5cf52d8ee05e4a3a723289eb9
6
+ metadata.gz: a1293ac55e9244756384126f45f2c8b54fca5cf6f9f55f9a001e8838661aa3fe224f479a5dee95fb838379950e43d28c32d3a121049eb5d8187babdd92d028d4
7
+ data.tar.gz: c3732fbe8aae17fa613a69d80328fe06063373f9917c02d83610c6cb90dbeaecea1df64ef390f28d31c40c1971ee014b49921659ebc4d174cbbb12cac5c84a94
data/README.md CHANGED
@@ -1,12 +1,10 @@
1
1
  # TestAssistant
2
2
 
3
- RSpec toolbox for writing and diagnosing Ruby on Rails tests, faster - especially emails and JSON APIs.
3
+ RSpec toolbox for writing and diagnosing Ruby on Rails tests, faster.
4
4
 
5
5
  ## Features
6
6
 
7
7
  * Light-weight, scoped, lazily executed and composable tool-box, so you only include the features you want to use, when you want to use them with no unnecessary overhead
8
- * JSON assertion that gives noise-free reports on complex nested structures, so you can find out exactly what has changed with your JSON API without having to manually diff large objects
9
- * Expressive email assertions that let you succinctly describe when emails should and should not sent
10
8
  * Automatic reporting of the context around failing tests, so you don't have to re-run them with additional logging or a debugger
11
9
 
12
10
  ## Installation
@@ -35,155 +33,8 @@ RSpec.configure do |config|
35
33
  end
36
34
  ```
37
35
 
38
- ## JSON expectations
39
-
40
- Test Assistant lets you include helpers in your controller and request specs to get succinct declarative methods for defining the expected results for JSON responses.
41
-
42
- ### Setup
43
-
44
- ```ruby
45
- TestAssistant.configure(config) do |ta_config|
46
- ta_config.include_json_helpers type: :request
47
- end
48
- ```
49
-
50
- ### Asserting JSON responses
51
-
52
- Among the helpers provided are `json_response`, which automatically parses the last response object as json, and a custom assertion `eql_json` that reports failures in a format that is much clearer than anything provided by RSpec.
53
-
54
- The full `expected` and `actual` values are still reported, but below is a separate report that only includes the paths to the failed nested values and their differences, removing the need to manually compare the two complete objects to find the difference.
55
-
56
- ```ruby
57
- RSpec.describe 'making some valid request', type: :request do
58
- context 'some important context' do
59
- it 'should return some complicated JSON' do
60
-
61
- perform_request
62
-
63
- expect(json_response).to eql_json([
64
- {
65
- "a" => [
66
- 1, 2, 3
67
- ],
68
- "c" => { "d" => "d'"}
69
- },
70
- {
71
- "b" => [
72
- 1, 2, 3
73
- ],
74
- "c" => { "d" => "d'"}
75
- }
76
- ])
77
- end
78
- end
79
- end
80
- ```
81
-
82
- ## Email expectations
83
-
84
- Test Assistant provides a declarative API for describing when emails should be sent and their characteristics.
85
-
86
- ### Setup
87
-
88
- ```ruby
89
- TestAssistant.configure(config) do |ta_config|
90
- ta_config.include_email_helpers type: :request
91
- end
92
- ```
93
-
94
- ### Clearing emails
95
-
96
- Emails can be cleared at any point by calling `clear_emails` in your tests. This is helpful when you are testing a user workflow that may trigger multiple emails.
97
-
98
- Emails are automatically cleared between each request spec.
99
-
100
- ### Email receiver address
101
-
102
- It's possible to assert an email was sent to one or more or more addresses using the following format:
103
-
104
- ```ruby
105
- expect(email).to have_been_sent.to('user@email.com')
106
- ```
107
-
108
- ### Email sender address
109
-
110
- Similarly, you can assert an email was sent from an address:
111
-
112
- ```ruby
113
- expect(email).to have_been_sent.from('user@email.com')
114
- ```
115
-
116
- ### Email subject
117
-
118
- You can assert an email's subject:
119
-
120
- ```ruby
121
- expect(email).to have_been_sent.with_subject('Welcome!')
122
- ```
123
-
124
-
125
- ### Email body
126
-
127
- You can assert the body of an email by text:
128
-
129
- ```ruby
130
- expect(email).to have_been_sent.with_text('Welcome, user@email.com')
131
- ```
132
-
133
- Or using a selector on the email's HTML:
134
-
135
- ```ruby
136
- expect(email).to have_been_sent.with_selector('#password')
137
- ```
138
-
139
- Or look for links:
140
-
141
- ```ruby
142
- expect(email).to have_been_sent.with_link('www.site.com/onboarding/1')
143
- ```
144
-
145
- Or images:
146
-
147
- ```ruby
148
- expect(email).to have_been_sent.with_image('www.site.com/assets/images/welcome.png')
149
- ```
150
-
151
- ### Chaining assertions
152
-
153
- You can chain any combination of the above that you want for ultra specific assertions:
154
-
155
-
156
- ```ruby
157
- expect(email).to have_been_sent
158
- .to('user@email.com')
159
- .from('admin@site.com')
160
- .with_subject('Welcome!')
161
- .with_text('Welcome, user@email.com')
162
- .with_selector('#password').and('#username')
163
- .with_link('www.site.com/onboarding/1')
164
- .with_image('www.site.com/assets/images/welcome.png')
165
-
166
- ```
167
-
168
- You can also chain multiple assertions of the the same type with the `and` method:
169
-
170
- ```ruby
171
- expect(email).to have_been_sent
172
- .with_text('Welcome, user@email.com').and('Thanks for signing up')
173
- ```
174
-
175
- ### Asserting emails are NOT sent
176
-
177
- The `have_sent_email` assertion works with the negative case as well:
178
-
179
- ```ruby
180
- expect(email).to_not have_been_sent.with_text('Secret token')
181
- ```
182
-
183
36
  ## Failure Reporting
184
37
 
185
-
186
-
187
38
  ### Rendering a response context when a test fails
188
39
 
189
40
  Test Assistant can automatically render the server response in your browser when a test fails and you have applied a nominated tag.
@@ -215,8 +66,6 @@ The `debug_failed_responses` accepts a the following options:
215
66
 
216
67
  ```ruby
217
68
  TestAssistant.configure(config) do |ta_config|
218
- ta_config.include_json_helpers type: :request
219
-
220
69
  ta_config.debug_failed_responses tag: :debugger
221
70
  end
222
71
  ```
@@ -231,14 +80,6 @@ RSpec.describe 'making some valid request', type: :request do
231
80
  end
232
81
  ```
233
82
 
234
- ## Test suite
235
-
236
- TestAssistant comes with close to complete test coverage. You can run the test suite as follows:
237
-
238
- ```bash
239
- rspec
240
- ```
241
-
242
83
  ## Contributing
243
84
 
244
85
  1. Fork it ( https://github.com/greena13/test_assistant/fork )
@@ -246,7 +87,3 @@ rspec
246
87
  3. Commit your changes (`git commit -am 'Add some feature'`)
247
88
  4. Push to the branch (`git push origin my-new-feature`)
248
89
  5. Create a new Pull Request
249
-
250
- ## Inspirations
251
-
252
- * [CapybaraEmail](https://github.com/DockYard/capybara-email)
@@ -4,8 +4,6 @@ require 'test_assistant/configuration'
4
4
  # Utility module for working with RSpec test suites for Rails or similar applications
5
5
  #
6
6
  # Contains:
7
- # - Expressive syntax for asserting testing emails
8
- # - Sophisticated JSON matchers and diff failure reports
9
7
  # - Rendering and debugging tools for viewing test failures
10
8
  #
11
9
  # @see https://github.com/greena13/test_assistant Test Assistant Github page
@@ -1,6 +1,4 @@
1
1
  module TestAssistant
2
- autoload :Json, 'test_assistant/json/helpers'
3
- autoload :Email, 'test_assistant/email/helpers'
4
2
  autoload :FailureReporter, 'test_assistant/failure_reporter'
5
3
 
6
4
  # Class that provides configuration methods to control what parts of Test Assistant
@@ -19,49 +17,6 @@ module TestAssistant
19
17
  @rspec_config = rspec_config
20
18
  end
21
19
 
22
- # Configures RSpec to include the JSON helpers provided by Test Assistant in the
23
- # the test suite's scope
24
- #
25
- # @see TestAssistant::Json::Helpers
26
- # @see RSpec::Core::Configuration#include
27
- #
28
- # @param [Hash] options RSpec::Core::Configuration#include options
29
- # @return void
30
- #
31
- # @example Include JSON helpers in your RSpec test suite
32
- # RSpec.configure do |config|
33
- # TestAssistant.configure(config) do |ta_config|
34
- # ta_config.include_json_helpers
35
- # end
36
- # end
37
- def include_json_helpers(options = {})
38
- @rspec_config.include Json::Helpers, options
39
- end
40
-
41
- # Configures RSpec to include the email helpers provided by Test Assistant in the
42
- # the test suite's scope and to clear the list of emails sent after each test
43
- #
44
- # @see TestAssistant::Email::Helpers
45
- # @see RSpec::Core::Configuration#include
46
- #
47
- # @param [Hash] options RSpec::Core::Configuration#include options
48
- # @return void
49
- #
50
- # @example Include Email helpers in your RSpec test suite
51
- # RSpec.configure do |config|
52
- # TestAssistant.configure(config) do |ta_config|
53
- # ta_config.include_email_helpers
54
- # end
55
- # end
56
- def include_email_helpers(options = {})
57
- @rspec_config.include Email::Helpers, options
58
-
59
- @rspec_config.after :each, type: :request do
60
- # clear emails after every request spec
61
- ActionMailer::Base.deliveries = []
62
- end
63
- end
64
-
65
20
  # Configures under what circumstances a failing test should open a failure report
66
21
  # detailing the last HTTP request and response in a browser
67
22
  #
@@ -105,12 +60,17 @@ module TestAssistant
105
60
  no_type_filter = !type_filter
106
61
 
107
62
  @rspec_config.after(:each) do |example|
108
- if example.exception
109
- if (example.metadata[tag_filter] || no_tag_filter) && (example.metadata[:type] == type_filter || no_type_filter)
110
- reporter = FailureReporter.report(request, response)
111
- reporter.write
112
- reporter.open
113
- end
63
+ next unless example.exception
64
+
65
+ metadata = example.metadata
66
+ next unless (metadata[tag_filter] || no_tag_filter) && (metadata[:type] == type_filter || no_type_filter)
67
+
68
+ if metadata[:type] == :feature
69
+ save_and_open_page
70
+ else
71
+ reporter = FailureReporter.report(request, response)
72
+ reporter.write
73
+ reporter.open
114
74
  end
115
75
  end
116
76
  end
@@ -153,16 +113,18 @@ module TestAssistant
153
113
  no_type_filter = !type_filter
154
114
 
155
115
  @rspec_config.after(:each) do |example|
156
- if example.exception
157
- if (example.metadata[tag_filter]) && (example.metadata[:type] == type_filter || no_type_filter)
158
- if defined? binding
159
- binding.pry
160
- elsif defined? byebug
161
- byebug
162
- else
163
- debugger
164
- end
165
- end
116
+ next unless example.exception
117
+
118
+ metadata = example.metadata
119
+ next unless (metadata.key?(tag_filter)) && (metadata[:type] == type_filter || no_type_filter)
120
+
121
+ # noinspection RubyResolve
122
+ if defined? binding
123
+ binding.pry
124
+ elsif defined? byebug
125
+ byebug
126
+ else
127
+ debugger
166
128
  end
167
129
  end
168
130
  end
@@ -1,3 +1,3 @@
1
1
  module TestAssistant
2
- VERSION = "0.1.2"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -18,9 +18,6 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_dependency 'capybara', '~> 2.5', '>= 2.5.0'
22
- spec.add_dependency 'hashdiff', '~> 0'
23
-
24
21
  spec.add_development_dependency "bundler", "~> 1.6"
25
22
  spec.add_development_dependency "rake", "~> 0"
26
23
  spec.add_development_dependency "guard", "~> 2.1"
metadata CHANGED
@@ -1,49 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: test_assistant
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 1.0.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: 2018-05-01 00:00:00.000000000 Z
11
+ date: 2019-04-13 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: capybara
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '2.5'
20
- - - ">="
21
- - !ruby/object:Gem::Version
22
- version: 2.5.0
23
- type: :runtime
24
- prerelease: false
25
- version_requirements: !ruby/object:Gem::Requirement
26
- requirements:
27
- - - "~>"
28
- - !ruby/object:Gem::Version
29
- version: '2.5'
30
- - - ">="
31
- - !ruby/object:Gem::Version
32
- version: 2.5.0
33
- - !ruby/object:Gem::Dependency
34
- name: hashdiff
35
- requirement: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - "~>"
38
- - !ruby/object:Gem::Version
39
- version: '0'
40
- type: :runtime
41
- prerelease: false
42
- version_requirements: !ruby/object:Gem::Requirement
43
- requirements:
44
- - - "~>"
45
- - !ruby/object:Gem::Version
46
- version: '0'
47
13
  - !ruby/object:Gem::Dependency
48
14
  name: bundler
49
15
  requirement: !ruby/object:Gem::Requirement
@@ -132,16 +98,9 @@ files:
132
98
  - Rakefile
133
99
  - lib/test_assistant.rb
134
100
  - lib/test_assistant/configuration.rb
135
- - lib/test_assistant/email/expectation.rb
136
- - lib/test_assistant/email/helpers.rb
137
101
  - lib/test_assistant/failure_reporter.rb
138
- - lib/test_assistant/json/expectation.rb
139
- - lib/test_assistant/json/helpers.rb
140
102
  - lib/test_assistant/version.rb
141
- - spec/email_expectation_spec.rb
142
- - spec/eql_json_spec.rb
143
103
  - spec/spec_helper.rb
144
- - spec/support/email_mock.rb
145
104
  - test_assistant.gemspec
146
105
  homepage: https://github.com/greena13/test_assistant
147
106
  licenses:
@@ -168,7 +127,4 @@ signing_key:
168
127
  specification_version: 4
169
128
  summary: A toolbox for increased testing efficiency with RSpec
170
129
  test_files:
171
- - spec/email_expectation_spec.rb
172
- - spec/eql_json_spec.rb
173
130
  - spec/spec_helper.rb
174
- - spec/support/email_mock.rb
@@ -1,456 +0,0 @@
1
- require 'capybara/rspec'
2
-
3
- module TestAssistant::Email
4
- # Backing class for have_been_sent declarative syntax for specifying email
5
- # expectations. Provides ability to assert emails have been sent in a given test
6
- # that match particular attribute values, such as sender, receiver or contents.
7
- #
8
- # Expected to be used as part of a RSpec test suite and with
9
- # TestAssistant::Email::Helpers#email.
10
- #
11
- # Has two major components:
12
- # - A Builder or chainable methods syntax for constructing arbitrarily specific
13
- # expectations
14
- # - An implementation of the same interface as RSpec custom matcher classes to
15
- # allow evaluating those expectations those expectations
16
- #
17
- # @see TestAssistant::Email::Helpers#email
18
- # @see TestAssistant::Email::Helpers#have_been_sent
19
- class Expectation
20
- MATCHERS = {
21
- to: :to,
22
- from: :from,
23
- with_subject: :subject,
24
- with_text: {
25
- match: ->(_, email, value){ value.all?{|text| email.has_content?(text) }},
26
- actual: ->(_, email){ email.text}
27
- },
28
- matching_selector: {
29
- match: ->(_, email, value){ value.all?{|selector| email.has_selector?(selector) }},
30
- actual: ->(_, email){ email.native },
31
- actual_name: :with_body
32
- },
33
- with_link: {
34
- match: ->(_, email, value){ value.all?{|url| email.has_selector?("a[href='#{url}']") }},
35
- actual: ->(_, email){ email.native },
36
- actual_name: :with_body
37
- },
38
- with_image: {
39
- match: ->(_, email, value){ value.all?{|url| email.has_selector?("img[src='#{url}']") }},
40
- actual: ->(_, email){ email.native },
41
- actual_name: :with_body
42
- }
43
- }
44
-
45
- # Creates a new TestAssistant::Email::Expectation object
46
- #
47
- # @return [TestAssistant::Email::Expectation] new expectation object
48
- def initialize
49
- @expectations = {}
50
- @failure_message = 'Expected email to be sent'
51
- @and_scope = nil
52
- end
53
-
54
- #
55
- # Expectation creation methods
56
- #
57
-
58
- # Allows chaining two assertions on the same email attribute together without having
59
- # to repeat the same method. Intended as syntactical sugar only and is functionally
60
- # equivalent to repeating the method.
61
- #
62
- # @example Asserting an email was sent to two email addresses
63
- # expect(email).to have_been_sent.to('user1@email.com').and('user2@email.com')
64
- #
65
- # @param [Array] arguments parameters to pass to whatever assertion is being
66
- # extended.
67
- # @return [TestAssistant::Email::Expectation] reference to self, to allow for
68
- # further method chaining
69
- def and(*arguments)
70
- if @and_scope
71
- self.send(@and_scope, *arguments)
72
- else
73
- ArugmentError.new("Cannot use and without a proceeding assertion.")
74
- end
75
- end
76
-
77
- # For constructing an assertion that at least one email was sent to a particular
78
- # email address
79
- #
80
- # @example Asserting an email was sent to user@email.com
81
- # expect(email).to have_been_sent.to('user@email.com')
82
- #
83
- # @param [String, Array<String>] email_address address email is expected to be
84
- # sent to. If an array of email addresses, the email is expected to have been
85
- # sent to all of them.
86
- # @return [TestAssistant::Email::Expectation] reference to self, to allow for
87
- # further method chaining
88
- def to(email_address)
89
- @expectations[:to] ||= []
90
-
91
- if email_address.kind_of?(Array)
92
- @expectations[:to] = @expectations[:to].concat(email_address)
93
- else
94
- @expectations[:to] ||= []
95
- @expectations[:to] << email_address
96
- end
97
-
98
- @and_scope = :to
99
-
100
- self
101
- end
102
-
103
- # For constructing an assertion that at least one email was sent from a particular
104
- # email address
105
- #
106
- # @example Asserting an email was sent from admin@site.com
107
- # expect(email).to have_been_sent.from('admin@site.com')
108
- #
109
- # @param [String] email_address address email is expected to be sent from.
110
- # @raise ArgumentError when from is called more than once on the same expectation,
111
- # as an email can only ben sent from a single sender.
112
- # @return [TestAssistant::Email::Expectation] reference to self, to allow for
113
- # further method chaining
114
- def from(email_address)
115
- if @expectations[:from]
116
- raise ArgumentError('An email can only have one from address, but you tried to assert the presence of 2 or more values')
117
- else
118
- @expectations[:from] = email_address
119
- end
120
-
121
- @and_scope = :from
122
-
123
- self
124
- end
125
-
126
- # For constructing an assertion that at least one email was sent with a particular
127
- # subject line
128
- #
129
- # @example Asserting an email was sent with subject line 'Hello'
130
- # expect(email).to have_been_sent.with_subject('Hello')
131
- #
132
- # @param [String] subject Subject line an email is expected to have been sent with
133
- # @raise ArgumentError when with_subject is called more than once on the same
134
- # expectation, as an email can only have one subject line.
135
- # @return [TestAssistant::Email::Expectation] reference to self, to allow for
136
- # further method chaining
137
- def with_subject(subject)
138
- if @expectations[:with_subject]
139
- raise ArgumentError('An email can only have one subject, but you tried to assert the presence of 2 or more values')
140
- else
141
- @expectations[:with_subject] = subject
142
- end
143
-
144
- @and_scope = :with_subject
145
-
146
- self
147
- end
148
-
149
- # For constructing an assertion that at least one email was sent with a particular
150
- # string in the body of the email
151
- #
152
- # @example Asserting an email was sent with the text 'User 1'
153
- # expect(email).to have_been_sent.with_text('User 1')
154
- #
155
- # @param [String] text Text an email is expected to have been sent with in the body
156
- # @return [TestAssistant::Email::Expectation] reference to self, to allow for
157
- # further method chaining
158
- def with_text(text)
159
- @expectations[:with_text] ||= []
160
- @expectations[:with_text].push(text)
161
-
162
- @and_scope = :with_text
163
- self
164
- end
165
-
166
- # For constructing an assertion that at least one email was sent with a body that
167
- # matches a particular CSS selector
168
- #
169
- # @example Asserting an email was sent with a body matching selector '#imporant-div'
170
- # expect(email).to have_been_sent.matching_selector('#imporant-div')
171
- #
172
- # @param [String] selector CSS selector that should match at least one sent
173
- # email's body
174
- # @return [TestAssistant::Email::Expectation] reference to self, to allow for
175
- # further method chaining
176
- def matching_selector(selector)
177
- @expectations[:matching_selector] ||= []
178
- @expectations[:matching_selector].push(selector)
179
-
180
- @and_scope = :matching_selector
181
- self
182
- end
183
-
184
- # For constructing an assertion that at least one email was sent with a link to
185
- # a particular url in the body
186
- #
187
- # @example Asserting an email was sent with a link to http://www.example.com
188
- # expect(email).to have_been_sent.with_link('http://www.example.com')
189
- #
190
- # @param [String] href URL that should appear in at least one sent email's body
191
- # @return [TestAssistant::Email::Expectation] reference to self, to allow for
192
- # further method chaining
193
- def with_link(href)
194
- @expectations[:with_link] ||= []
195
- @expectations[:with_link].push(href)
196
-
197
- @and_scope = :with_link
198
- self
199
- end
200
-
201
- # For constructing an assertion that at least one email was sent with an image
202
- # hosted at a particular URL
203
- #
204
- # @example Asserting an email was sent with the image http://www.example.com/image.png
205
- # expect(email).to have_been_sent.with_link('http://www.example.com/image.png')
206
- #
207
- # @param [String] src URL of the image that should appear in at least one sent
208
- # email's body
209
- # @return [TestAssistant::Email::Expectation] reference to self, to allow for
210
- # further method chaining
211
- def with_image(src)
212
- @expectations[:with_image] ||= []
213
- @expectations[:with_image].push(src)
214
-
215
- @and_scope = :with_image
216
- self
217
- end
218
-
219
- #
220
- # RSpec Matcher methods
221
- #
222
-
223
- # Declares that RSpec should not attempt to diff the actual and expected values
224
- # to put in the failure message. This class takes care of diffing and presenting
225
- # the differences, itself.
226
- # @return [false] always returns false
227
- def diffable?
228
- false
229
- end
230
-
231
- # Whether at least one email was sent during the current test that matches the
232
- # constructed expectation
233
- # @return [Boolean] whether a matching email was sent
234
- def matches?(emails)
235
- @emails = emails
236
-
237
- matching_emails = @emails
238
-
239
- if @expectations.any?
240
- @expectations.each do |attribute, expected|
241
- @failed_attribute = attribute
242
- @failed_expected = expected
243
-
244
- matching_emails =
245
- matching_emails.select do |email|
246
- email_matches?(email, MATCHERS[attribute], expected)
247
- end
248
-
249
- if matching_emails.empty?
250
- return false
251
- end
252
- end
253
-
254
- true
255
- else
256
- @emails.any?
257
- end
258
- end
259
-
260
- # Message to display to StdOut by RSpec if the equality check fails. Includes a
261
- # complete a human-readable summary of the differences between what emails were
262
- # expected to be sent, and what were actually sent (if any). Only used when the
263
- # positive assertion is used, i.e. expect(email).to have_been_sent. For the
264
- # failure message used for negative assertions, i.e.
265
- # expect(email).to_not have_been_sent, see #failure_message_when_negated
266
- #
267
- # @see #failure_message_when_negated
268
- #
269
- # @return [String] message full failure message with explanation of the differences
270
- # between what emails were expected and what was actually sent
271
- def failure_message
272
- field_descs = attribute_descriptions
273
- value_descs = value_descriptions
274
-
275
- base_clause = expectation_description(
276
- 'Expected an email to be sent',
277
- field_descs,
278
- value_descs
279
- )
280
-
281
- if @emails.length == 0
282
- "#{base_clause} However, no emails were sent."
283
- else
284
- email_values = sent_email_values
285
-
286
- if email_values.any?
287
- base_clause + " However, #{email_pluralisation(@emails)} sent #{result_description(field_descs, [to_sentence(email_values)])}."
288
- else
289
- base_clause
290
- end
291
- end
292
- end
293
-
294
- # Failure message to display for negative RSpec assertions, i.e.
295
- # expect(email).to_not have_been_sent. For the failure message displayed for positive
296
- # assertions, see #failure_message.
297
- #
298
- # @see #failure_message
299
- #
300
- # @return [String] message full failure message with explanation of the differences
301
- # between what emails were expected and what was actually sent
302
- def failure_message_when_negated
303
- field_descs = attribute_descriptions(negated: true)
304
- value_descs = value_descriptions(negated: true)
305
-
306
- expectation_description(
307
- 'Expected no emails to be sent',
308
- field_descs,
309
- value_descs
310
- )
311
- end
312
-
313
- private
314
-
315
- def result_description(field_descriptions, values)
316
- to_sentence(
317
- field_descriptions.map.with_index do |field_description, index|
318
- value = values[index]
319
-
320
- if [ 'matching selector', 'with link', 'with image' ].include?(field_description)
321
- "with body #{value}"
322
- else
323
- "#{field_description} #{value}"
324
- end
325
- end
326
- )
327
- end
328
-
329
- def sent_email_values
330
- @emails.inject([]) do |memo, email|
331
-
332
- if [ :matching_selector, :with_link, :with_image ].include?(@failed_attribute)
333
- memo << email_body(email)
334
- else
335
- matcher = MATCHERS[@failed_attribute]
336
-
337
- value =
338
- case matcher
339
- when String, Symbol
340
- email.send(matcher)
341
- when Hash
342
- field_description = matcher[:actual_name] if matcher[:actual_name]
343
- matcher[:actual].(email, parsed_emails(email))
344
- end
345
-
346
- value = value.kind_of?(String) ? "'#{value}'" : value.map{|element| "'#{element}'"}
347
- memo << value
348
- end
349
-
350
- memo
351
- end
352
- end
353
-
354
- def expectation_description(base_clause, field_descriptions, value_descriptions)
355
- description = base_clause
356
-
357
- additional_clauses = []
358
-
359
- field_descriptions.each.with_index do |field_description, index|
360
- clause = ''
361
- clause += " #{field_description}" if field_description.length > 0
362
-
363
- if (value_description = value_descriptions[index])
364
- clause += " #{value_description}"
365
- end
366
-
367
- additional_clauses.push(clause) if clause.length > 0
368
- end
369
-
370
- description + additional_clauses.join('') + '.'
371
- end
372
-
373
- def attribute_descriptions(negated: false)
374
- attributes_to_describe =
375
- if negated
376
- @expectations.keys
377
- else
378
- [ @failed_attribute ]
379
- end
380
-
381
- attributes_to_describe.map do |attribute|
382
- attribute.to_s.gsub('_', ' ')
383
- end
384
- end
385
-
386
- def value_descriptions(negated: false)
387
- values_to_describe =
388
- if negated
389
- @expectations.values
390
- else
391
- [ @failed_expected ]
392
- end
393
-
394
- values_to_describe.map do |value|
395
- case value
396
- when String
397
- "'#{value}'"
398
- when Array
399
- to_sentence(value.map{|val| "'#{val}'"})
400
- else
401
- value
402
- end
403
- end
404
-
405
- end
406
-
407
- def email_pluralisation(emails)
408
- emails.length > 2 ? "#{emails.length} were": "1 was"
409
- end
410
-
411
- def to_sentence(items)
412
- case items.length
413
- when 0, 1
414
- items.join('')
415
- when 2
416
- items.join(' and ')
417
- else
418
- items[0..(items.length-3)].join(', ') + items[(items.length-3)..items.length-1].join(' and ')
419
- end
420
- end
421
-
422
- def parsed_emails(email)
423
- @parsed_emails ||= {}
424
- @parsed_emails[email] ||= parser(email)
425
- @parsed_emails[email]
426
- end
427
-
428
- def parser(email)
429
- Capybara::Node::Simple.new(email_body(email))
430
- end
431
-
432
- def email_body(email)
433
- if email.parts.first
434
- email.parts.first.body.decoded
435
- else
436
- email.body.encoded
437
- end
438
- end
439
-
440
- def email_matches?(email, assertion, expected)
441
-
442
- case assertion
443
- when :to
444
- (expected & email.send(assertion)).length > 0
445
- when String, Symbol
446
- email.send(assertion).include?(expected)
447
- when Hash
448
- assertion[:match].(email, parsed_emails(email), expected)
449
- else
450
- raise RuntimeError.new(
451
- "Unsupported assertion mapping '#{assertion_match}' of type #{assertion_match.class.name}"
452
- )
453
- end
454
- end
455
- end
456
- end