rspec/rspec-rails

View on GitHub
lib/rspec/rails/view_rendering.rb

Summary

Maintainability
A
0 mins
Test Coverage
require 'action_view/testing/resolvers'

module RSpec
  module Rails
    # @api public
    # Helpers for optionally rendering views in controller specs.
    module ViewRendering
      extend ActiveSupport::Concern

      # @!attribute [r]
      # Returns the controller object instance under test.
      attr_reader :controller

      # @private
      attr_writer :controller
      private :controller=

      # DSL methods
      module ClassMethods
        # @see RSpec::Rails::ControllerExampleGroup
        def render_views(true_or_false = true)
          @render_views = true_or_false
        end

        # @api private
        def render_views?
          return @render_views if defined?(@render_views)

          if superclass.respond_to?(:render_views?)
            superclass.render_views?
          else
            RSpec.configuration.render_views?
          end
        end
      end

      # @api private
      def render_views?
        self.class.render_views? || !controller.class.respond_to?(:view_paths)
      end

      # @private
      class EmptyTemplateResolver
        def self.build(path)
          if path.is_a?(::ActionView::Resolver)
            ResolverDecorator.new(path)
          else
            FileSystemResolver.new(path)
          end
        end

        def self.nullify_template_rendering(templates)
          templates.map do |template|
            ::ActionView::Template.new(
              "",
              template.identifier,
              EmptyTemplateHandler,
              virtual_path: template.virtual_path,
              format: template_format(template),
              locals: []
            )
          end
        end

        if ::Rails::VERSION::STRING >= '6'
          def self.template_format(template)
            template.format
          end
        else
          def self.template_format(template)
            template.formats
          end
        end

        # Delegates all methods to the submitted resolver and for all methods
        # that return a collection of `ActionView::Template` instances, return
        # templates with modified source
        #
        # @private
        class ResolverDecorator
          def initialize(resolver)
            @resolver = resolver
          end

          def method_missing(name, *args, &block)
            result = @resolver.send(name, *args, &block)
            nullify_templates(result)
          end

        private

          def nullify_templates(collection)
            return collection unless collection.is_a?(Enumerable)
            return collection unless collection.all? { |element| element.is_a?(::ActionView::Template) }

            EmptyTemplateResolver.nullify_template_rendering(collection)
          end
        end

        # Delegates find_templates to the submitted path set and then returns
        # templates with modified source
        #
        # @private
        class FileSystemResolver < ::ActionView::FileSystemResolver
          private

          def find_templates(*args)
            templates = super
            EmptyTemplateResolver.nullify_template_rendering(templates)
          end
        end
      end

      # @private
      class EmptyTemplateHandler
        def self.call(_template, _source = nil)
          ::Rails.logger.info("  Template rendering was prevented by rspec-rails. Use `render_views` to verify rendered view contents if necessary.")

          %("")
        end
      end

      # Used to null out view rendering in controller specs.
      #
      # @private
      module EmptyTemplates
        def prepend_view_path(new_path)
          lookup_context.view_paths.unshift(*_path_decorator(*new_path))
        end

        def append_view_path(new_path)
          lookup_context.view_paths.push(*_path_decorator(*new_path))
        end

      private

        def _path_decorator(*paths)
          paths.map { |path| EmptyTemplateResolver.build(path) }
        end
      end

      # @private
      RESOLVER_CACHE = Hash.new do |hash, path|
        hash[path] = EmptyTemplateResolver.build(path)
      end

      included do
        before do
          unless render_views?
            @_original_path_set = controller.class.view_paths
            path_set = @_original_path_set.map { |resolver| RESOLVER_CACHE[resolver] }

            controller.class.view_paths = path_set
            controller.extend(EmptyTemplates)
          end
        end

        after do
          controller.class.view_paths = @_original_path_set unless render_views?
        end
      end
    end
  end
end