Testing rendering of a given layout with RSpec & Rails - ruby-on-rails

Is it possible to test the use of a given layout using RSpec with Rails, for example I'd like a matcher that does the following:
response.should use_layout('my_layout_name')
I found a use_layout matcher when Googling but it doesn't work as neither the response or controller seem to have a layout property that matcher was looking for.

David Chelimsky posted a good answer over on the Ruby Forum:
response.should render_template("layouts/some_layout")

This works for me with edge Rails and edge RSpec on Rails:
response.layout.should == 'layouts/application'
Shouldn't be hard to turn this into a matcher suitable for you.

There's already a perfectly functional matcher for this:
response.should render_template(:layout => 'fooo')
(Rspec 2.6.4)

I found an example of how to write a use_layout matcher that will do just that. Here's the code in case that link goes away:
# in spec_helper.rb
class UseLayout
def initialize(expected)
#expected = 'layouts/' + expected
end
def matches?(controller)
#actual = controller.layout
##actual.equal?(#expected)
#actual == #expected
end
def failure_message
return "use_layout expected #{#expected.inspect}, got #
{#actual.inspect}", #expected, #actual
end
def negeative_failure_message
return "use_layout expected #{#expected.inspect} not to equal #
{#actual.inspect}", #expected, #actual
end
end
def use_layout(expected)
UseLayout.new(expected)
end
# in controller spec
response.should use_layout("application")

I had to write the following to make this work:
response.should render_template("layouts/some_folder/some_layout", "template-name")

Here is an updated version of the matcher. I've updated it to conform to the latest version of RSpec. I've added the relevant read only attributes and remove old return format.
# in spec_helper.rb
class UseLayout
attr_reader :expected
attr_reader :actual
def initialize(expected)
#expected = 'layouts/' + expected
end
def matches?(controller)
if controller.is_a?(ActionController::Base)
#actual = 'layouts/' + controller.class.read_inheritable_attribute(:layout)
else
#actual = controller.layout
end
#actual ||= "layouts/application"
#actual == #expected
end
def description
"Determines if a controller uses a layout"
end
def failure_message
return "use_layout expected #{#expected.inspect}, got #{#actual.inspect}"
end
def negeative_failure_message
return "use_layout expected #{#expected.inspect} not to equal #{#actual.inspect}"
end
end
def use_layout(expected)
UseLayout.new(expected)
end
Additionally the matcher now also works with layouts specified at the controller class level and can be used as follows:
class PostsController < ApplicationController
layout "posts"
end
And in the controller spec you can simply use:
it { should use_layout("posts") }

Here's the solution I ended up going with. Its for rpsec 2 and rails 3.
I just added this file in the spec/support directory.
The link is: https://gist.github.com/971342
# spec/support/matchers/render_layout.rb
ActionView::Base.class_eval do
unless instance_methods.include?('_render_layout_with_tracking')
def _render_layout_with_tracking(layout, locals, &block)
controller.instance_variable_set(:#_rendered_layout, layout)
_render_layout_without_tracking(layout, locals, &block)
end
alias_method_chain :_render_layout, :tracking
end
end
# You can use this matcher anywhere that you have access to the controller instance,
# like in controller or integration specs.
#
# == Example Usage
#
# Expects no layout to be rendered:
# controller.should_not render_layout
# Expects any layout to be rendered:
# controller.should render_layout
# Expects app/views/layouts/application.html.erb to be rendered:
# controller.should render_layout('application')
# Expects app/views/layouts/application.html.erb not to be rendered:
# controller.should_not render_layout('application')
# Expects app/views/layouts/mobile/application.html.erb to be rendered:
# controller.should_not render_layout('mobile/application')
RSpec::Matchers.define :render_layout do |*args|
expected = args.first
match do |c|
actual = get_layout(c)
if expected.nil?
!actual.nil? # actual must be nil for the test to pass. Usage: should_not render_layout
elsif actual
actual == expected.to_s
else
false
end
end
failure_message_for_should do |c|
actual = get_layout(c)
if actual.nil? && expected.nil?
"expected a layout to be rendered but none was"
elsif actual.nil?
"expected layout #{expected.inspect} but no layout was rendered"
else
"expected layout #{expected.inspect} but #{actual.inspect} was rendered"
end
end
failure_message_for_should_not do |c|
actual = get_layout(c)
if expected.nil?
"expected no layout but #{actual.inspect} was rendered"
else
"expected #{expected.inspect} not to be rendered but it was"
end
end
def get_layout(controller)
if template = controller.instance_variable_get(:#_rendered_layout)
template.virtual_path.sub(/layouts\//, '')
end
end
end

response.should render_template("layouts/some_folder/some_layout")
response.should render_template("template-name")

controller.active_layout.name works for me.

Shoulda Matchers provides a matcher for this scenario. (Documentation)
This seems to work:
expect(response).to render_with_layout('my_layout')
it produces appropriate failure messages like:
Expected to render with the "calendar_layout" layout, but rendered with "application", "application"
Tested with rails 4.2, rspec 3.3 and shoulda-matchers 2.8.0
Edit: shoulda-matchers provides this method. Shoulda::Matchers::ActionController::RenderWithLayoutMatcher

Here's a version of dmcnally's code that allows no arguments to be passed, making "should use_layout" and "should_not use_layout" work (to assert that the controller is using any layout, or no layout, respectively - of which I would expect only the second to be useful as you should be more specific if it is using a layout):
class UseLayout
def initialize(expected = nil)
if expected.nil?
#expected = nil
else
#expected = 'layouts/' + expected
end
end
def matches?(controller)
#actual = controller.layout
##actual.equal?(#expected)
if #expected.nil?
#actual
else
#actual == #expected
end
end
def failure_message
if #expected.nil?
return 'use_layout expected a layout to be used, but none was', 'any', #actual
else
return "use_layout expected #{#expected.inspect}, got #{#actual.inspect}", #expected, #actual
end
end
def negative_failure_message
if #expected.nil?
return "use_layout expected no layout to be used, but #{#actual.inspect} found", 'any', #actual
else
return "use_layout expected #{#expected.inspect} not to equal #{#actual.inspect}", #expected, #actual
end
end
end
def use_layout(expected = nil)
UseLayout.new(expected)
end

Related

Target mailto: links with Rails::Html::TargetScrubber

I'm trying to scrub out mailto links while allowing others using Rails::HTML Scrubbers.
See: https://github.com/rails/rails-html-sanitizer
I have a Loofah fragment like this:
Loofah.fragment('tis but a scratch')
And my Scrubber is like so:
class TargetedHtmlScrubber < Rails::Html::TargetScrubber
def initialize
super
self.tags = %w[a]
end
def allowed_node?(node)
...
end
def scrub_attribute?(name)
...
end
end
But when I run scrub! in my specs, I can't see any of the methods allowed_node? or scrub_attribute being called as per the documentation.
My spec is like this. It scrubs everything..,.
require 'rails_helper'
RSpec.describe TargetedHtmlScrubber do
describe 'targeting html tags' do
it 'ignores <a>' do
fragment = html_fragment('Greg! the stop sign!!')
expect(
fragment.scrub!(subject).to_s
).to eq 'Greg! the stop sign!!'
end
it 'targets <a href=mailto:>' do
fragment = html_fragment('tis but a scratch')
expect(
fragment.scrub!(subject).to_s
).to eq 'tis but a scratch'
end
end
end
I am expecting to implement something in one of the two methods in my class.
Ok, I worked it out. To call those methods, you need to set the tags and attributes arrays:
def initialize
super
self.tags = %w[a]
self.attributes = %w[href]
end
It will then call allowed_node?(node) and scrub_attribute?(name). So in this case, I only need the tags.
To scrub out mailto: links:
class TargetedHtmlScrubber < Rails::Html::TargetScrubber
def initialize
super
self.tags = %w[a]
end
def allowed_node?(node)
href(node) !~ /^mailto:/
end
private
def href(node)
node.try(:attributes)['href'].try(:value)
end
end

Rspec test helper

I am having a hard time figuring out how to test this helper because current_group is not defined in my test case and I am not sure how I can stub it.
module Admin
module EmployeesHelper
def upload_access
policy(current_group).can_bulk_create_employees?
end
def dashboard_params
download_employee_url = upload_access ?
download_admin_group_employees_import_csv_index_path(current_group) : nil
upload_employee_url = upload_access ?
admin_group_employees_import_csv_index_path(current_group) : nil
make_hash(upload_employee_url, download_employee_url)
end
private
def make_hash(upload_url, download_url)
{
employees: #employees,
addEmployeeUrl: new_admin_group_employee_path(current_group),
terminated_employees: #terminated_employees,
new_employees: #new_employees,
test_employees: #test_employees,
group_id: current_group.id,
downloadEmployeeUrl: download_url,
uploadEmployeeUrl: upload_url
}
end
end
end
Here's what my test looks like, but it fails because current_group is not defined.
require 'rails_helper'
describe Admin::EmployeesHelper do
let!(:group) { create(:group) }
before do
# This stub doesn't work because helper doesn't implement current_group
allow(helper).to receive(:current_group).and_return(group)
end
it 'returns correct dashboard props' do
allow(helper).to receive(:upload_access).and_return(true)
props = helper.dashboard_params
expect(props).values_at(:downloadEmployeeUrl, :uploadEmployeeUrl).should_not include(nil)
end
end
If the problem is only with current_group method, you could include this helper in a dummy class. Something like this:
let(:klass) {
Class.new do
include Admin::EmployeesHelper
def current_group
nil
end
end
}
let(:instance) { klass.new }
before do
allow(instance).to receive(:current_group).and_return('whatever') # test-specific group
end
it 'returns correct dashboard props' do
allow(instance).to receive(:upload_access).and_return(true)
props = instance.dashboard_params
expect(props).values_at(:downloadEmployeeUrl, :uploadEmployeeUrl).should_not include(nil)
end
Although I foresee that you'll have to include something for the url helpers too. And set up those instance vars. All in all, it probably isn't worth the trouble.
I ended up refactoring the helper to have current_group as an argument instead.

How to test an unrouted controller action or exception_app in Rails

We have a custom exception app that has been raising (fail safe) exceptions (the application equivalent of having an exception in a rescue block).
I think I've fixed it, but am finding it hard to test. It's an unrouted controller, so I can't use controller tests (require routing).
i.e. I have Rails.configuration.exceptions_app = ExceptionController.action(:show), not Rails.configuration.exceptions_app = self.routes.
Basically what I think I need to do is
Generate a test request request = ActionDispatch::TestRequest.new
include Rack::Test or maybe mimic behavior in ActiveSupport::IntegrationTest
Set #app = ExceptionsController.action(:show)
Fake an exception request.env.merge! 'action_dispatch.exception' => ActionController::RoutingError.new(:foo)
Test response = #app.call(request.env)
Assert no exception is raised and correct response body and status
Problems:
The env needs
a warden / devise session with current_user request.env['warden'] = spy(Warden) and request.session = ActionDispatch::Integration::Session.new(#app)
to manipulate request formats so that I can check that a request without an accept defaults to json request.any?(:json)? constraints: { default: :json } ? `request.accept = "application/javascript"
work work with the respond_with responder
set action_dispatch.show_exceptions, consider all requests local, etc request.env["action_dispatch.show_detailed_exceptions"] = true
Also, I considered building a ActionDispatch::ShowException.new(app, ExceptionController.new) or a small rack app
But our gem has no tests and I haven't been able to apply anything that I've read in exception handling gems (most work at the rescue_action_in_public level or mix in to ShowException) or in the Rails source code
This is a Rails 4.2 app tested via Rspec and Capybara.
Thoughts, links, halp?
Example code and tests
RSpec.describe 'ExceptionController' do
class ExceptionController < ActionController::Base
use ActionDispatch::ShowExceptions, Rails.configuration.exceptions_app
use ActionDispatch::DebugExceptions
#Response
respond_to :html, :json
#Layout
layout :layout_status
#Dependencies
before_action :status, :app_name, :log_exception
def show
respond_with details, status: #status, location: nil
end
def show_detailed_exceptions?
request.local?
end
protected
####################
# Dependencies #
####################
#Info
def status
#exception = env['action_dispatch.exception']
#status = ActionDispatch::ExceptionWrapper.new(env, #exception).status_code
#response = ActionDispatch::ExceptionWrapper.rescue_responses[#exception.class.name]
end
#Format
def details
#details ||= {}.tap do |h|
I18n.with_options scope: [:exception, :show, #response], exception_name: #exception.class.name, exception_message: #exception.message do |i18n|
h[:name] = i18n.t "#{#exception.class.name.underscore}.title", default: i18n.t(:title, default: #exception.class.name)
h[:message] = i18n.t "#{#exception.class.name.underscore}.description", default: i18n.t(:description, default: #exception.message)
end
end
end
helper_method :details
####################
# Layout #
####################
private
def log_exception
if #status.to_s == '500'
request.env[:exception_details] = details
request.env[:exception_details][:location] = ActionDispatch::ExceptionWrapper.new(env, #exception).application_trace[0]
end
end
#Layout
def layout_status
#status.to_s != '404' ? 'error' : 'application'
end
#App
def app_name
#app_name = Rails.application.class.parent_name
end
end
include Rack::Test::Methods
include ActionDispatch::Integration::Runner
include ActionController::TemplateAssertions
include ActionDispatch::Routing::UrlFor
let(:exception) { ActionController::RoutingError.new(:foo) }
let(:request) { ActionDispatch::TestRequest.new }
def app
# Rails.application.config.exceptions_app
#app ||= ExceptionController.action(:show)
end
it 'logs unknown format errors' do
request.env['action_dispatch.show_exceptions'] = true
request.env['consider_all_requests_local'] = true
request.env['warden'] = spy(Warden)
request.session = ActionDispatch::Integration::Session.new(app)
exception = ActionController::RoutingError.new(:foo)
request.env.merge! 'action_dispatch.exception' => exception
post '/whatever'
expect(response.body).to eq("dunno?")
end
end
refs:
https://github.com/richpeck/exception_handler
https://github.com/plataformatec/responders/blob/8f03848a2f50d4685c15a31254a1f600af947bd7/test/action_controller/respond_with_test.rb#L265-L275
https://github.com/rails/rails/blob/1d43458c148f9532a81b92ee3a247da4f1c0b7ad/actionpack/test/dispatch/show_exceptions_test.rb#L92-L99
https://github.com/rails/rails/blob/3e36db4406beea32772b1db1e9a16cc1e8aea14c/railties/test/application/middleware/exceptions_test.rb#L86-L91
https://github.com/rails/rails/blob/34fa6658dd1b779b21e586f01ee64c6f59ca1537/actionpack/lib/action_dispatch/testing/integration.rb#L647-L674
https://github.com/rails/rails/blob/ef8d09d932e36b0614905ea5bc3fb6af318b6ce2/actionview/test/abstract_unit.rb#L146-L184
https://github.com/plataformatec/responders/blob/8f03848a2f50d4685c15a31254a1f600af947bd7/lib/action_controller/respond_with.rb#L196-L207
http://blog.plataformatec.com.br/2012/01/my-five-favorite-hidden-features-in-rails-3-2/
https://github.com/bugsnag/bugsnag-ruby/blob/master/lib/bugsnag/middleware/warden_user.rb
https://github.com/bugsnag/bugsnag-ruby/blob/master/lib/bugsnag/rails/action_controller_rescue.rb#L3-L33
https://github.com/bugsnag/bugsnag-ruby/blob/master/lib/bugsnag/rails/active_record_rescue.rb
https://github.com/rollbar/rollbar-gem/blob/master/spec/rollbar_spec.rb
http://andre.arko.net/2011/12/10/make-rails-3-stop-trying-to-serve-html/
https://github.com/rails/rails/blob/9503e65b9718e4f01860cf017c1cdcdccdfffde7/actionpack/lib/action_dispatch/middleware/show_exceptions.rb#L46-L49
Update 2015-08-27
It has been suggested that this question may be a duplicate of Testing error pages in Rails with Rspec + Capybara, however, that question addresses testing exception responses when the exceptions_app is set to routes.
As I wrote above, I'm using a Controller as the exceptions_app, so though I could use capybara to visit non-existing pages, I'd like to test Controller's action directly, rather than include the rest of the show exceptions stack. This is important because my problem is when the exceptions app is called with an unhandled content type, which I cannot easily test via capybara.
More generally, what I need to test is when the Exceptions app raises and exception, that I have fixed it.
I'm open to seeing some example code, though.

Testing mixin in ruby

I'm trying to write a rspec test for a mixin class. I have the following.
module one
module two
def method
method_details = super
if method_details.a && method_details.b
something
elsif method_details.b
another thing
else
last thing
end
end
end
end
Now I have mocked the "method" object that will be passed to the class.
But I'm struggling to access the super method.
I did,
let(:dummy_class) { Class.new { include one::two } }
How to pass the mocked method object to this dummy class?
How do I go about testing this? New to ruby, can someone show me a direction with this.
Thanks in advance.
UPDATE:
I tried,
let(:dummy_class) {
Class.new { |d|
include one::two
d.method = method_details
}
}
let (:method_details){
'different attributes'
}
still doesn't work. I get undefined local variable or method method_details for #<Class:0x007fc9a49cee18>
I personally test mixing with the class. Because the mixing (module) itself has no meaning unless its attached to a class/object.
Ex:
module SayName
def say_name
p 'say name'
end
end
class User
include SayName
end
So I believe you should test your module with attached to the relevant class / object.
How ever this is a different perspective on testing mixings
HTH
I think that in your specs, you'll need to explicitly provide a super class definition for when super is called in #method as "you can't mock super and you shouldn't".
I've attempted to spec out all three of your scenarios with the following minor changes:
Changed your example code slightly to become valid Ruby
Changed #method to #the_method so it doesn't conflict with Object#method
Used OpenStruct to represent the object that super returns, because all I know is that it's an object that has methods #a and #b. You can change that out as appropriate for your real specs
Copy and paste the class and specs below into a file and give them a try:
module One
module Two
def the_method
method_details = super
if method_details.a && method_details.b
'something'
elsif method_details.b
'another thing'
else
'last thing'
end
end
end
end
RSpec.describe One::Two do
require 'ostruct'
let(:one_twoable) { Class.new(super_class) { include One::Two }.new }
describe '#the_method' do
let(:the_method) { one_twoable.the_method }
context 'when method_details#a && method_details#b' do
let(:super_class) do
Class.new do
def the_method
OpenStruct.new(a: true, b: true)
end
end
end
it 'is "something"' do
expect(the_method).to eq('something')
end
end
context 'when just method#b' do
let(:super_class) do
Class.new do
def the_method
OpenStruct.new(a: false, b: true)
end
end
end
it 'is "another thing"' do
expect(the_method).to eq('another thing')
end
end
context 'when neither of the above' do
let(:super_class) do
Class.new do
def the_method
OpenStruct.new(a: false, b: false)
end
end
end
it 'is "last thing"' do
expect(the_method).to eq('last thing')
end
end
end
end

Instance variable in Rails helper not set

I try to set some options in a rails helper, but it seems it's got overridden every time.
module MetaTagHelper
def meta_options
#meta_options ||= {}
end
def add_meta_tag_options(opt)
meta_options.deep_merge(opt)
end
end
Here is the test
require 'rspec'
describe MetaTagHelper do
it 'options should be set' do
option = {region: "1"}
option2 = {country: "AT"}
helper.add_meta_tag_options(option)
helper.add_meta_tag_options(option2).should eql(option.merge option2)
end
end
expected: {:region=>"1", :country=>"AT"}
got: {:country=>"AT"}
How can I get the spec pass?
Try using deep_merge!:
def add_meta_tag_options(opt)
meta_options.deep_merge!(opt)
end

Resources