Wouldn’t it be nice if you could write acceptance specs for your email like this?
feature 'Subscribe to email list' do
scenario 'Successful subscription' do
visit '/'
fill_in 'Email', with: 'don@example.com'
click_button 'Subscribe'
page.should have_content 'Check your email'
"don@example.com".should receive_email(:subject => 'Welcome')
click_link_in_email "Confirm my subscription", :subject => 'Welcome'
page.should have_content 'Subscription confirmed'
end
end
Yes, yes it would. Here is how you do it. Drop this in your spec support folder:
module AcceptanceEmailHelpers
# Finds the latest email to match the given options, locates an HTML link
# matching the given text, and visits it. Options are not validated - they
# are passed straight through to the found mail object.
def click_link_in_email(text, options)
mail = find_email_matching(options)
unless mail
fail "Could not locate mail matching: #{options.inspect}"
end
element = Nokogiri::HTML(mail.body.to_s).css('a').detect {|x| x.text == text }
unless element
fail "Could not find link with text '#{text}' in:\n\n#{mail.body.to_s}"
end
visit strip_hostname(element.attribute('href').value)
end
def strip_hostname(url)
uri = URI.parse(url)
if uri.host.blank?
raise "Host was not set in email url => #{url}"
end
[uri.path, uri.query].compact.join('?')
end
def find_email_matching(options)
options[:from] = extract_plain_email(options[:from]) if options[:from]
options[:to] = extract_plain_email(options[:to]) if options[:to]
ActionMailer::Base.deliveries.reverse.detect do |mail|
options.all? do |method, value|
[*mail.send(method)].any? {|x| x.include?(value) }
end
end
end
# Emails can look like "bill@example.com" or "Bill <bill>", hence
# grab only the plain email address bit
def extract_plain_email(email)
email[/([\w\.%\+\-]+)@([\w\-]+\.)+([\w]{2,})/i]
end
# Called on an Author, Editor, User, or Institution. Will regexp match all the
# given fields against any email delivered to that user. Supported fields:
# :subject
# :body
RSpec::Matchers.define :receive_email do |options = {}|
match do |recipient|
find_email_matching(options_with_recipient(options, recipient))
end
failure_message_for_should do |recipient|
<<-EOS
Expected to receive an email matching #{options_with_recipient(options, recipient).inspect}"}
Received:
#{ActionMailer::Base.deliveries.map {|x| "#{x.to.join(', ')}: #{x.subject}" }.join("\n ")}
EOS
end
failure_message_for_should_not do |recipient|
<<-EOS
Expected not to receive an email matching #{options_with_recipient(options, recipient).inspect}"}
Received:
#{ActionMailer::Base.deliveries.map {|x| "#{x.to.join(', ')}: #{x.subject}" }.join("\n ")}
EOS
end
def options_with_recipient(options, recipient)
options.merge(to: email_for_recipient(recipient))
end
def email_for_recipient(recipient)
if recipient.is_a?(String)
recipient
else
recipient.email
end
end
end
end
RSpec.configuration.include EmailHelpers
There’s a bit of complexity in there but it’s all helpful. It’s a veritable treasure chest.