snapshot_inspector 0.1.1
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +227 -0
- data/Rakefile +8 -0
- data/app/assets/config/snapshot_inspector/manifest.js +2 -0
- data/app/assets/javascripts/snapshot_inspector/application.js +1 -0
- data/app/assets/stylesheets/snapshot_inspector/application.css +33 -0
- data/app/assets/stylesheets/snapshot_inspector/snapshots/mail.css +48 -0
- data/app/assets/stylesheets/snapshot_inspector/snapshots/not_found.css +15 -0
- data/app/assets/stylesheets/snapshot_inspector/snapshots/response.css +9 -0
- data/app/assets/stylesheets/snapshot_inspector/snapshots.css +73 -0
- data/app/controllers/snapshot_inspector/application_controller.rb +14 -0
- data/app/controllers/snapshot_inspector/snapshots/mail_controller.rb +57 -0
- data/app/controllers/snapshot_inspector/snapshots/response_controller.rb +15 -0
- data/app/controllers/snapshot_inspector/snapshots_controller.rb +7 -0
- data/app/helpers/snapshot_inspector/application_helper.rb +15 -0
- data/app/helpers/snapshot_inspector/snapshots_helper.rb +37 -0
- data/app/mailers/snapshot_inspector/application_mailer.rb +6 -0
- data/app/models/snapshot_inspector/snapshot/context.rb +49 -0
- data/app/models/snapshot_inspector/snapshot/mail_type.rb +35 -0
- data/app/models/snapshot_inspector/snapshot/response_type.rb +19 -0
- data/app/models/snapshot_inspector/snapshot/rspec_context.rb +52 -0
- data/app/models/snapshot_inspector/snapshot/test_unit_context.rb +44 -0
- data/app/models/snapshot_inspector/snapshot/type.rb +52 -0
- data/app/models/snapshot_inspector/snapshot.rb +86 -0
- data/app/views/layouts/snapshot_inspector/application.html.erb +18 -0
- data/app/views/snapshot_inspector/snapshots/index.html.erb +29 -0
- data/app/views/snapshot_inspector/snapshots/mail/show.html.erb +107 -0
- data/app/views/snapshot_inspector/snapshots/not_found.html.erb +8 -0
- data/app/views/snapshot_inspector/snapshots/response/raw.html.erb +1 -0
- data/app/views/snapshot_inspector/snapshots/response/show.html.erb +1 -0
- data/config/importmap.rb +12 -0
- data/config/routes.rb +9 -0
- data/lib/minitest/snapshot_inspector_plugin.rb +28 -0
- data/lib/snapshot_inspector/engine.rb +67 -0
- data/lib/snapshot_inspector/storage.rb +60 -0
- data/lib/snapshot_inspector/test/action_mailer_headers.rb +18 -0
- data/lib/snapshot_inspector/test/rspec_helpers.rb +45 -0
- data/lib/snapshot_inspector/test/test_unit_helpers.rb +48 -0
- data/lib/snapshot_inspector/version.rb +3 -0
- data/lib/snapshot_inspector.rb +42 -0
- data/lib/tasks/tmp.rake +10 -0
- metadata +159 -0
| @@ -0,0 +1,35 @@ | |
| 1 | 
            +
            require "mail"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module SnapshotInspector
         | 
| 4 | 
            +
              class Snapshot
         | 
| 5 | 
            +
                class MailType < Type
         | 
| 6 | 
            +
                  snapshotee ActionMailer::MessageDelivery
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  # @private
         | 
| 9 | 
            +
                  def extract(snapshotee)
         | 
| 10 | 
            +
                    @message = snapshotee.to_s
         | 
| 11 | 
            +
                    @bcc = snapshotee.bcc
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  # @private
         | 
| 15 | 
            +
                  def from_hash(hash)
         | 
| 16 | 
            +
                    @message = hash[:message]
         | 
| 17 | 
            +
                    @bcc = hash[:bcc]
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  def message
         | 
| 21 | 
            +
                    message = Mail::Message.new(@message)
         | 
| 22 | 
            +
                    message.bcc = @bcc
         | 
| 23 | 
            +
                    message
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  def mailer_name
         | 
| 27 | 
            +
                    message.header["X-SnapshotInspector-Mailer-Name"].value
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  def action_name
         | 
| 31 | 
            +
                    message.header["X-SnapshotInspector-Action-Name"].value
         | 
| 32 | 
            +
                  end
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
              end
         | 
| 35 | 
            +
            end
         | 
| @@ -0,0 +1,19 @@ | |
| 1 | 
            +
            module SnapshotInspector
         | 
| 2 | 
            +
              class Snapshot
         | 
| 3 | 
            +
                class ResponseType < Type
         | 
| 4 | 
            +
                  snapshotee ActionDispatch::TestResponse
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  attr_reader :body
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  # @private
         | 
| 9 | 
            +
                  def extract(snapshotee)
         | 
| 10 | 
            +
                    @body = snapshotee.parsed_body
         | 
| 11 | 
            +
                  end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  # @private
         | 
| 14 | 
            +
                  def from_hash(hash)
         | 
| 15 | 
            +
                    @body = hash[:body]
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
            end
         | 
| @@ -0,0 +1,52 @@ | |
| 1 | 
            +
            module SnapshotInspector
         | 
| 2 | 
            +
              class Snapshot
         | 
| 3 | 
            +
                class RspecContext < Context
         | 
| 4 | 
            +
                  test_framework :rspec
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  attr_reader :test_framework, :example, :take_snapshot_index
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  # @private
         | 
| 9 | 
            +
                  def extract(context)
         | 
| 10 | 
            +
                    @test_framework = context[:test_framework]
         | 
| 11 | 
            +
                    @example = context[:example]
         | 
| 12 | 
            +
                    @take_snapshot_index = context[:take_snapshot_index]
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  # @private
         | 
| 16 | 
            +
                  def from_hash(hash)
         | 
| 17 | 
            +
                    @test_framework = hash[:test_framework].to_sym
         | 
| 18 | 
            +
                    @example = hash[:example]
         | 
| 19 | 
            +
                    @take_snapshot_index = hash[:take_snapshot_index]
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  def to_slug
         | 
| 23 | 
            +
                    spec_path_without_extension = @example[:file_path].delete_suffix(File.extname(@example[:file_path])).delete_prefix("./")
         | 
| 24 | 
            +
                    [spec_path_without_extension, @example[:line_number], @take_snapshot_index].join("_")
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  def name
         | 
| 28 | 
            +
                    @example[:full_description].gsub(test_group, "").strip
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  def test_group
         | 
| 32 | 
            +
                    root_example_group_description(@example)
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  def order_index
         | 
| 36 | 
            +
                    [@example[:file_path], @example[:line_number], @take_snapshot_index]
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  private
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  def root_example_group_description(hash)
         | 
| 42 | 
            +
                    if hash[:example_group].present?
         | 
| 43 | 
            +
                      root_example_group_description(hash[:example_group])
         | 
| 44 | 
            +
                    elsif hash[:parent_example_group].present?
         | 
| 45 | 
            +
                      root_example_group_description(hash[:parent_example_group])
         | 
| 46 | 
            +
                    else
         | 
| 47 | 
            +
                      hash[:description]
         | 
| 48 | 
            +
                    end
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
              end
         | 
| 52 | 
            +
            end
         | 
| @@ -0,0 +1,44 @@ | |
| 1 | 
            +
            module SnapshotInspector
         | 
| 2 | 
            +
              class Snapshot
         | 
| 3 | 
            +
                class TestUnitContext < Context
         | 
| 4 | 
            +
                  test_framework :test_unit
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  attr_reader :test_framework, :test_case_name, :method_name, :source_location, :take_snapshot_index
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  # @private
         | 
| 9 | 
            +
                  def extract(context)
         | 
| 10 | 
            +
                    @test_framework = context[:test_framework]
         | 
| 11 | 
            +
                    @test_case_name = context[:test_case_name]
         | 
| 12 | 
            +
                    @method_name = context[:method_name]
         | 
| 13 | 
            +
                    @source_location = context[:source_location]
         | 
| 14 | 
            +
                    @take_snapshot_index = context[:take_snapshot_index]
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  # @private
         | 
| 18 | 
            +
                  def from_hash(hash)
         | 
| 19 | 
            +
                    @test_framework = hash[:test_framework].to_sym
         | 
| 20 | 
            +
                    @test_case_name = hash[:test_case_name]
         | 
| 21 | 
            +
                    @method_name = hash[:method_name]
         | 
| 22 | 
            +
                    @source_location = hash[:source_location]
         | 
| 23 | 
            +
                    @take_snapshot_index = hash[:take_snapshot_index]
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  def to_slug
         | 
| 27 | 
            +
                    spec_path_without_extension = source_location[0].delete_suffix(File.extname(source_location[0])).delete_prefix(Rails.root.to_s + "/")
         | 
| 28 | 
            +
                    [spec_path_without_extension, source_location[1], take_snapshot_index].join("_")
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  def name
         | 
| 32 | 
            +
                    method_name.gsub(/^test_/, "").humanize(capitalize: false)
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  def test_group
         | 
| 36 | 
            +
                    test_case_name
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  def order_index
         | 
| 40 | 
            +
                    source_location.dup << take_snapshot_index
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
              end
         | 
| 44 | 
            +
            end
         | 
| @@ -0,0 +1,52 @@ | |
| 1 | 
            +
            module SnapshotInspector
         | 
| 2 | 
            +
              class Snapshot
         | 
| 3 | 
            +
                class Type
         | 
| 4 | 
            +
                  class UnknownSnapshotee < StandardError; end
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  class_attribute :registry, default: {}, instance_writer: false, instance_predicate: false
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  def self.snapshotee(class_name)
         | 
| 9 | 
            +
                    registry[class_name] = self
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def self.extract(snapshotee)
         | 
| 13 | 
            +
                    record = type_class(snapshotee.class).new
         | 
| 14 | 
            +
                    record.extract(snapshotee)
         | 
| 15 | 
            +
                    record
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  def self.from_hash(hash)
         | 
| 19 | 
            +
                    record = type_class(hash[:snapshotee_class].constantize).new
         | 
| 20 | 
            +
                    record.from_hash(hash)
         | 
| 21 | 
            +
                    record
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  private_class_method def self.type_class(snapshotee_class)
         | 
| 25 | 
            +
                    registry[snapshotee_class] || raise(UnknownSnapshotee.new(unknown_snapshotee_class_message(snapshotee_class)))
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  private_class_method def self.unknown_snapshotee_class_message(snapshotee_class)
         | 
| 29 | 
            +
                    list_of_known_classes = registry.keys.map(&:to_s).sort.map { |class_name| "`#{class_name}`" }.join(" or ")
         | 
| 30 | 
            +
                    "#take_snapshot only accepts an argument of kind #{list_of_known_classes}. You provided `#{snapshotee_class}`."
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  # @private
         | 
| 34 | 
            +
                  def extract(_snapshotee)
         | 
| 35 | 
            +
                    raise "Implement in a child class."
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  # @private
         | 
| 39 | 
            +
                  def from_hash(_hash)
         | 
| 40 | 
            +
                    raise "Implement in a child class."
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  def type
         | 
| 44 | 
            +
                    self.class.to_s.underscore.split("/").last.gsub("_type", "")
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                  def as_json(data = {})
         | 
| 48 | 
            +
                    {snapshotee_class: registry.key(self.class)}.merge(super)
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
              end
         | 
| 52 | 
            +
            end
         | 
| @@ -0,0 +1,86 @@ | |
| 1 | 
            +
            require "snapshot_inspector/snapshot/response_type"
         | 
| 2 | 
            +
            require "snapshot_inspector/snapshot/mail_type"
         | 
| 3 | 
            +
            require "snapshot_inspector/snapshot/test_unit_context"
         | 
| 4 | 
            +
            require "snapshot_inspector/snapshot/rspec_context"
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module SnapshotInspector
         | 
| 7 | 
            +
              class Snapshot
         | 
| 8 | 
            +
                class NotFound < StandardError; end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                attr_reader :context, :slug, :created_at
         | 
| 11 | 
            +
                delegate_missing_to :@type_data
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def self.persist(snapshotee:, context:)
         | 
| 14 | 
            +
                  new.extract(snapshotee: snapshotee, context: context).persist
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def self.find(slug)
         | 
| 18 | 
            +
                  hash = JSON.parse(Storage.read(slug), symbolize_names: true)
         | 
| 19 | 
            +
                  new.from_hash(hash)
         | 
| 20 | 
            +
                rescue Errno::ENOENT
         | 
| 21 | 
            +
                  raise NotFound.new("Snapshot with a slug `#{slug}` can't be found.")
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                def self.grouped_by_test_case
         | 
| 25 | 
            +
                  all.group_by do |snapshot|
         | 
| 26 | 
            +
                    snapshot.context.test_group
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                private_class_method def self.all
         | 
| 31 | 
            +
                  snapshots = Storage.list.map { |slug| find(slug) }
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  order_by_line_number(snapshots)
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                private_class_method def self.order_by_line_number(snapshots)
         | 
| 37 | 
            +
                  snapshots.sort_by do |snapshot|
         | 
| 38 | 
            +
                    snapshot.context.order_index
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                # @private
         | 
| 43 | 
            +
                def extract(snapshotee:, context:)
         | 
| 44 | 
            +
                  extract_type_specific_data(snapshotee)
         | 
| 45 | 
            +
                  extract_context(context)
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                  @slug = @context.to_slug
         | 
| 48 | 
            +
                  @created_at = Time.current
         | 
| 49 | 
            +
                  self
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                # @private
         | 
| 53 | 
            +
                def persist
         | 
| 54 | 
            +
                  Storage.write(slug, JSON.pretty_generate(as_json))
         | 
| 55 | 
            +
                  self
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                # @private
         | 
| 59 | 
            +
                def from_hash(hash)
         | 
| 60 | 
            +
                  from_hash_type_specific_data(hash)
         | 
| 61 | 
            +
                  from_hash_context(hash)
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                  @slug = hash[:slug]
         | 
| 64 | 
            +
                  @created_at = Time.zone.parse(hash[:created_at])
         | 
| 65 | 
            +
                  self
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                private
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                def extract_type_specific_data(snapshotee)
         | 
| 71 | 
            +
                  @type_data = Type.extract(snapshotee)
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                def extract_context(context)
         | 
| 75 | 
            +
                  @context = Context.extract(context)
         | 
| 76 | 
            +
                end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                def from_hash_type_specific_data(hash)
         | 
| 79 | 
            +
                  @type_data = Type.from_hash(hash[:type_data])
         | 
| 80 | 
            +
                end
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                def from_hash_context(hash)
         | 
| 83 | 
            +
                  @context = Context.from_hash(hash[:context])
         | 
| 84 | 
            +
                end
         | 
| 85 | 
            +
              end
         | 
| 86 | 
            +
            end
         | 
| @@ -0,0 +1,18 @@ | |
| 1 | 
            +
            <!DOCTYPE html>
         | 
| 2 | 
            +
            <html>
         | 
| 3 | 
            +
            <head>
         | 
| 4 | 
            +
              <title>Snapshot Inspector</title>
         | 
| 5 | 
            +
              <%= csrf_meta_tags %>
         | 
| 6 | 
            +
              <%= csp_meta_tag %>
         | 
| 7 | 
            +
             | 
| 8 | 
            +
              <%= stylesheet_link_tag "snapshot_inspector/application", media: "all" %>
         | 
| 9 | 
            +
              <%= snapshot_inspector_importmap_tags %>
         | 
| 10 | 
            +
              <%= javascript_import_module_tag "snapshot_inspector/application" %>
         | 
| 11 | 
            +
              <% if defined?(Hotwire::Livereload::DISABLE_FILE) && !File.exist?(Rails.root.join(Hotwire::Livereload::DISABLE_FILE)) %>
         | 
| 12 | 
            +
                <%= hotwire_livereload_tags %>
         | 
| 13 | 
            +
              <% end %>
         | 
| 14 | 
            +
            </head>
         | 
| 15 | 
            +
            <body id="<%= "#{controller_name}_#{action_name}" %>">
         | 
| 16 | 
            +
              <%= yield %>
         | 
| 17 | 
            +
            </body>
         | 
| 18 | 
            +
            </html>
         | 
| @@ -0,0 +1,29 @@ | |
| 1 | 
            +
            <main>
         | 
| 2 | 
            +
              <h1>Snapshots</h1>
         | 
| 3 | 
            +
             | 
| 4 | 
            +
              <% if @grouped_by_test_class.blank? %>
         | 
| 5 | 
            +
                <p>Place `take_screenshot response` in your integration tests after a `response` object is populated and run the tests. Snapshots will appear below.</p>
         | 
| 6 | 
            +
              <% end %>
         | 
| 7 | 
            +
             | 
| 8 | 
            +
              <%= form_tag "", method: :get, class: "enable_javascript" do %>
         | 
| 9 | 
            +
                <%= label_tag :enable_javascript do %>
         | 
| 10 | 
            +
                  <%= check_box_tag :enable_javascript, "true", params[:enable_javascript] == "true", onclick: "submit()" %>
         | 
| 11 | 
            +
                  <span>Open snapshots with JavaScript enabled (by default all JavaScript tags are removed)</span>
         | 
| 12 | 
            +
                <% end %>
         | 
| 13 | 
            +
              <% end %>
         | 
| 14 | 
            +
             | 
| 15 | 
            +
              <% @grouped_by_test_class.each do |test_group, snapshots| %>
         | 
| 16 | 
            +
                <h2><%= test_group %></h2>
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                <ul>
         | 
| 19 | 
            +
                  <% snapshots.each do |snapshot| %>
         | 
| 20 | 
            +
                    <li>
         | 
| 21 | 
            +
                      <%= link_to SnapshotInspector::SnapshotsHelper.snapshot_path(snapshot, enable_javascript: params[:enable_javascript]) do %>
         | 
| 22 | 
            +
                        <%= snapshot.context.name %>
         | 
| 23 | 
            +
                        <%= if snapshot.context.take_snapshot_index > 0 then "(#{(snapshot.context.take_snapshot_index + 1).ordinalize} in the same test)" end %>
         | 
| 24 | 
            +
                      <% end %>
         | 
| 25 | 
            +
                    </li>
         | 
| 26 | 
            +
                  <% end %>
         | 
| 27 | 
            +
                </ul>
         | 
| 28 | 
            +
              <% end %>
         | 
| 29 | 
            +
            </main>
         | 
| @@ -0,0 +1,107 @@ | |
| 1 | 
            +
            <header>
         | 
| 2 | 
            +
              <dl>
         | 
| 3 | 
            +
                <% if @email.respond_to?(:smtp_envelope_from) && Array(@email.from) != Array(@email.smtp_envelope_from) %>
         | 
| 4 | 
            +
                  <dt>SMTP-From:</dt>
         | 
| 5 | 
            +
                  <dd id="smtp_from"><%= @email.smtp_envelope_from %></dd>
         | 
| 6 | 
            +
                <% end %>
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                <% if @email.respond_to?(:smtp_envelope_to) && @email.to != @email.smtp_envelope_to %>
         | 
| 9 | 
            +
                  <dt>SMTP-To:</dt>
         | 
| 10 | 
            +
                  <dd id="smtp_to"><%= @email.smtp_envelope_to %></dd>
         | 
| 11 | 
            +
                <% end %>
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                <dt>From:</dt>
         | 
| 14 | 
            +
                <dd id="from"><%= @email.header['from'] %></dd>
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                <% if @email.reply_to %>
         | 
| 17 | 
            +
                  <dt>Reply-To:</dt>
         | 
| 18 | 
            +
                  <dd id="reply_to"><%= @email.header['reply-to'] %></dd>
         | 
| 19 | 
            +
                <% end %>
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                <dt>To:</dt>
         | 
| 22 | 
            +
                <dd id="to"><%= @email.header['to'] %></dd>
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                <% if @email.cc %>
         | 
| 25 | 
            +
                  <dt>CC:</dt>
         | 
| 26 | 
            +
                  <dd id="cc"><%= @email.header['cc'] %></dd>
         | 
| 27 | 
            +
                <% end %>
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                <% if @email.bcc %>
         | 
| 30 | 
            +
                  <dt>BCC:</dt>
         | 
| 31 | 
            +
                  <dd id="bcc"><%= @email.header['bcc'] %></dd>
         | 
| 32 | 
            +
                <% end %>
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                <dt>Date:</dt>
         | 
| 35 | 
            +
                <dd id="date"><%= Time.current.rfc2822 %></dd>
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                <dt>Subject:</dt>
         | 
| 38 | 
            +
                <dd><strong id="subject"><%= @email.subject %></strong></dd>
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                <% unless @email.attachments.nil? || @email.attachments.empty? %>
         | 
| 41 | 
            +
                  <dt>Attachments:</dt>
         | 
| 42 | 
            +
                  <dd>
         | 
| 43 | 
            +
                    <% @email.attachments.each do |a| %>
         | 
| 44 | 
            +
                      <% filename = a.respond_to?(:original_filename) ? a.original_filename : a.filename %>
         | 
| 45 | 
            +
                      <%= link_to filename, "data:application/octet-stream;charset=utf-8;base64,#{Base64.encode64(a.body.to_s)}", download: filename %>
         | 
| 46 | 
            +
                    <% end %>
         | 
| 47 | 
            +
                  </dd>
         | 
| 48 | 
            +
                <% end %>
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                <dt>Format:</dt>
         | 
| 51 | 
            +
                <% if @email.html_part && @email.text_part %>
         | 
| 52 | 
            +
                  <dd>
         | 
| 53 | 
            +
                    <select id="part" onchange="refreshBody();">
         | 
| 54 | 
            +
                      <option <%= request.format == Mime[:html] ? 'selected' : '' %> value="<%= part_query('text/html') %>">View as HTML email</option>
         | 
| 55 | 
            +
                      <option <%= request.format == Mime[:text] ? 'selected' : '' %> value="<%= part_query('text/plain') %>">View as plain-text email</option>
         | 
| 56 | 
            +
                    </select>
         | 
| 57 | 
            +
                  </dd>
         | 
| 58 | 
            +
                <% elsif @part %>
         | 
| 59 | 
            +
                  <dd id="mime_type" data-mime-type="<%= part_query(@part.mime_type) %>"><%= @part.mime_type == 'text/html' ? 'HTML email' : 'plain-text email' %></dd>
         | 
| 60 | 
            +
                <% else %>
         | 
| 61 | 
            +
                  <dd id="mime_type" data-mime-type=""></dd>
         | 
| 62 | 
            +
                <% end %>
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                <% unless @email.header_fields.blank? %>
         | 
| 65 | 
            +
                  <dt>Headers:</dt>
         | 
| 66 | 
            +
                  <dd>
         | 
| 67 | 
            +
                    <details>
         | 
| 68 | 
            +
                      <summary>Show all headers</summary>
         | 
| 69 | 
            +
                      <table>
         | 
| 70 | 
            +
                        <% @email.header_fields.each do |field| %>
         | 
| 71 | 
            +
                          <tr>
         | 
| 72 | 
            +
                            <td align="right" style="color: #7f7f7f"><%= field.name %>:</td>
         | 
| 73 | 
            +
                            <td><%= field.value %></td>
         | 
| 74 | 
            +
                          </tr>
         | 
| 75 | 
            +
                        <% end %>
         | 
| 76 | 
            +
                      </table>
         | 
| 77 | 
            +
                    </details>
         | 
| 78 | 
            +
                  </dd>
         | 
| 79 | 
            +
                <% end %>
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                <dt>EML File:</dt>
         | 
| 82 | 
            +
                <dd><%= link_to "Download", format: :eml %></dd>
         | 
| 83 | 
            +
              </dl>
         | 
| 84 | 
            +
            </header>
         | 
| 85 | 
            +
             | 
| 86 | 
            +
            <% if @part && @part.mime_type %>
         | 
| 87 | 
            +
              <iframe name="messageBody" src="<%= raw_mail_snapshot_path(slug: @snapshot.slug, part: @part.mime_type) %>"></iframe>
         | 
| 88 | 
            +
            <% else %>
         | 
| 89 | 
            +
              <p>
         | 
| 90 | 
            +
                You are trying to preview an email that does not have any content.
         | 
| 91 | 
            +
                This is probably because the <em>mail</em> method has not been called in <em><%= @preview.preview_name %>#<%= @email_action %></em>.
         | 
| 92 | 
            +
              </p>
         | 
| 93 | 
            +
            <% end %>
         | 
| 94 | 
            +
             | 
| 95 | 
            +
            <script>
         | 
| 96 | 
            +
              function refreshBody() {
         | 
| 97 | 
            +
                const part_select = document.querySelector('select#part');
         | 
| 98 | 
            +
                const part_param = part_select ?
         | 
| 99 | 
            +
                  part_select.options[part_select.selectedIndex].value :
         | 
| 100 | 
            +
                  document.querySelector('#mime_type').dataset.mimeType;
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                const url = location.pathname.replace(/\.(txt|html)$/, '');
         | 
| 103 | 
            +
                const format = /html/.test(part_param) ? '.html' : '.txt';
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                location.href = url + format;
         | 
| 106 | 
            +
              }
         | 
| 107 | 
            +
            </script>
         | 
| @@ -0,0 +1 @@ | |
| 1 | 
            +
            <%= prepare_for_render(@snapshot.body, enable_javascript: params[:enable_javascript]) %>
         | 
| @@ -0,0 +1 @@ | |
| 1 | 
            +
            <iframe id="body" src="<%= raw_response_snapshot_path(@snapshot.slug, enable_javascript: params[:enable_javascript]) %>"></iframe>
         | 
    
        data/config/importmap.rb
    ADDED
    
    | @@ -0,0 +1,12 @@ | |
| 1 | 
            +
            require "snapshot_inspector"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            SnapshotInspector.configuration.importmap.draw do
         | 
| 4 | 
            +
              pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
         | 
| 5 | 
            +
              pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
         | 
| 6 | 
            +
              pin "stimulus-use", to: "https://ga.jspm.io/npm:stimulus-use@0.51.3/dist/index.js"
         | 
| 7 | 
            +
              pin "hotkeys-js", to: "https://ga.jspm.io/npm:hotkeys-js@3.10.1/dist/hotkeys.esm.js"
         | 
| 8 | 
            +
             | 
| 9 | 
            +
              pin "application", to: "snapshot_inspector/application.js", preload: true
         | 
| 10 | 
            +
             | 
| 11 | 
            +
              pin_all_from SnapshotInspector::Engine.root.join("app/assets/javascripts/snapshot_inspector/controllers"), under: "controllers", to: "snapshot_inspector/controllers"
         | 
| 12 | 
            +
            end
         | 
    
        data/config/routes.rb
    ADDED
    
    | @@ -0,0 +1,9 @@ | |
| 1 | 
            +
            SnapshotInspector::Engine.routes.draw do
         | 
| 2 | 
            +
              root to: "snapshots#index"
         | 
| 3 | 
            +
             | 
| 4 | 
            +
              get "mail/raw/*slug", to: "snapshots/mail#raw", as: :raw_mail_snapshot
         | 
| 5 | 
            +
              get "mail/*slug", to: "snapshots/mail#show", as: :mail_snapshot
         | 
| 6 | 
            +
             | 
| 7 | 
            +
              get "response/raw/*slug", to: "snapshots/response#raw", as: :raw_response_snapshot
         | 
| 8 | 
            +
              get "response/*slug", to: "snapshots/response#show", as: :response_snapshot
         | 
| 9 | 
            +
            end
         | 
| @@ -0,0 +1,28 @@ | |
| 1 | 
            +
            require "snapshot_inspector"
         | 
| 2 | 
            +
            require "snapshot_inspector/storage"
         | 
| 3 | 
            +
            require "minitest"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Minitest
         | 
| 6 | 
            +
              class SnapshotInspectorReporter < Reporter
         | 
| 7 | 
            +
                def report
         | 
| 8 | 
            +
                  SnapshotInspector::Storage.move_files_from_processing_directory_to_snapshots_directory if SnapshotInspector::Storage.processing_directory.exist?
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  io.print "\n\nInspect snapshots on #{SnapshotInspector.configuration.host + SnapshotInspector.configuration.route_path}"
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
              end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
              class << self
         | 
| 15 | 
            +
                def plugin_snapshot_inspector_options(opts, _options)
         | 
| 16 | 
            +
                  opts.on "--take-snapshots", "Take snapshots of responses for inspecting at #{SnapshotInspector.configuration.host + SnapshotInspector.configuration.route_path}" do
         | 
| 17 | 
            +
                    SnapshotInspector.configuration.snapshot_taking_enabled = true
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def plugin_snapshot_inspector_init(_options)
         | 
| 22 | 
            +
                  return unless SnapshotInspector.configuration.snapshot_taking_enabled
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  reporter << SnapshotInspectorReporter.new
         | 
| 25 | 
            +
                  SnapshotInspector::Storage.clear(:processing)
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
              end
         | 
| 28 | 
            +
            end
         | 
| @@ -0,0 +1,67 @@ | |
| 1 | 
            +
            require "importmap-rails"
         | 
| 2 | 
            +
            require "snapshot_inspector/test/action_mailer_headers"
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module SnapshotInspector
         | 
| 5 | 
            +
              class Engine < ::Rails::Engine
         | 
| 6 | 
            +
                isolate_namespace SnapshotInspector
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                config.before_configuration do |_app|
         | 
| 9 | 
            +
                  SnapshotInspector.configuration.storage_directory = Rails.root.join(SnapshotInspector::STORAGE_DIRECTORY)
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                unless Rails.env.test?
         | 
| 13 | 
            +
                  rake_tasks do
         | 
| 14 | 
            +
                    load "tasks/tmp.rake"
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                initializer "snapshot_inspector.importmap", before: "importmap" do |app|
         | 
| 19 | 
            +
                  app.config.importmap.paths << root.join("config/importmap.rb")
         | 
| 20 | 
            +
                  app.config.importmap.cache_sweepers << root.join("app/assets/javascripts")
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                initializer "snapshot_inspector.assets.precompile" do |app|
         | 
| 24 | 
            +
                  app.config.assets.precompile += %w[snapshot_inspector/manifest]
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                initializer "snapshot_inspector.include_test_helpers" do |_app|
         | 
| 28 | 
            +
                  if defined?(RSpec)
         | 
| 29 | 
            +
                    RSpec.configure do |config|
         | 
| 30 | 
            +
                      config.include SnapshotInspector::Test::RSpecHelpers
         | 
| 31 | 
            +
                      config.after :suite do
         | 
| 32 | 
            +
                        SnapshotInspector::Storage.move_files_from_processing_directory_to_snapshots_directory if SnapshotInspector::Storage.processing_directory.exist?
         | 
| 33 | 
            +
                      end
         | 
| 34 | 
            +
                    end
         | 
| 35 | 
            +
                  else
         | 
| 36 | 
            +
                    ActiveSupport.on_load(:active_support_test_case) do
         | 
| 37 | 
            +
                      include SnapshotInspector::Test::TestUnitHelpers
         | 
| 38 | 
            +
                    end
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  ActiveSupport.on_load(:action_mailer) do
         | 
| 42 | 
            +
                    include SnapshotInspector::Test::ActionMailerHeaders
         | 
| 43 | 
            +
                  end
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                initializer "snapshot_inspector.configure_default_url_options" do |_app|
         | 
| 47 | 
            +
                  url_options =
         | 
| 48 | 
            +
                    if Rails.application.routes.default_url_options.present?
         | 
| 49 | 
            +
                      Rails.application.routes.default_url_options
         | 
| 50 | 
            +
                    elsif Rails.application.config.action_controller.default_url_options.present?
         | 
| 51 | 
            +
                      Rails.application.config.action_controller.default_url_options
         | 
| 52 | 
            +
                    end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  SnapshotInspector.configuration.host = url_options ? [url_options[:host], url_options[:port]].join(":") : SnapshotInspector.configuration.host
         | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                initializer "snapshot_inspector.register_eml_mime_type" do |_app|
         | 
| 58 | 
            +
                  Mime::Type.register "application/octet-stream", :eml
         | 
| 59 | 
            +
                end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                config.after_initialize do |app|
         | 
| 62 | 
            +
                  app.routes.prepend do
         | 
| 63 | 
            +
                    mount SnapshotInspector::Engine, at: SnapshotInspector.configuration.route_path
         | 
| 64 | 
            +
                  end
         | 
| 65 | 
            +
                end
         | 
| 66 | 
            +
              end
         | 
| 67 | 
            +
            end
         | 
| @@ -0,0 +1,60 @@ | |
| 1 | 
            +
            module SnapshotInspector
         | 
| 2 | 
            +
              class Storage
         | 
| 3 | 
            +
                class << self
         | 
| 4 | 
            +
                  def snapshots_directory
         | 
| 5 | 
            +
                    SnapshotInspector.configuration.storage_directory.join("snapshots")
         | 
| 6 | 
            +
                  end
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  def processing_directory
         | 
| 9 | 
            +
                    SnapshotInspector.configuration.storage_directory.join("processing")
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def write(key, value)
         | 
| 13 | 
            +
                    file_path = to_file_path_for_writing(key)
         | 
| 14 | 
            +
                    file_path.dirname.mkpath
         | 
| 15 | 
            +
                    file_path.write(value)
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  def read(key)
         | 
| 19 | 
            +
                    File.read(to_file_path_for_reading(key))
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  def list
         | 
| 23 | 
            +
                    Dir
         | 
| 24 | 
            +
                      .glob("#{snapshots_directory}/**/*.{json}")
         | 
| 25 | 
            +
                      .map { |file_path| to_key(file_path) }
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  def clear(directory = nil)
         | 
| 29 | 
            +
                    case directory
         | 
| 30 | 
            +
                    when :snapshots
         | 
| 31 | 
            +
                      snapshots_directory.rmtree
         | 
| 32 | 
            +
                    when :processing
         | 
| 33 | 
            +
                      processing_directory.rmtree
         | 
| 34 | 
            +
                    else
         | 
| 35 | 
            +
                      snapshots_directory.rmtree
         | 
| 36 | 
            +
                      processing_directory.rmtree
         | 
| 37 | 
            +
                    end
         | 
| 38 | 
            +
                  end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  def move_files_from_processing_directory_to_snapshots_directory
         | 
| 41 | 
            +
                    clear(:snapshots)
         | 
| 42 | 
            +
                    processing_directory.rename(snapshots_directory)
         | 
| 43 | 
            +
                  end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                  private
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                  def to_key(file_path)
         | 
| 48 | 
            +
                    file_path.gsub(snapshots_directory.to_s + "/", "").gsub(".json", "")
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                  def to_file_path_for_reading(key)
         | 
| 52 | 
            +
                    snapshots_directory.join("#{key}.json")
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  def to_file_path_for_writing(key)
         | 
| 56 | 
            +
                    processing_directory.join("#{key}.json")
         | 
| 57 | 
            +
                  end
         | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
              end
         | 
| 60 | 
            +
            end
         | 
| @@ -0,0 +1,18 @@ | |
| 1 | 
            +
            module SnapshotInspector
         | 
| 2 | 
            +
              module Test
         | 
| 3 | 
            +
                module ActionMailerHeaders
         | 
| 4 | 
            +
                  extend ActiveSupport::Concern
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  included do
         | 
| 7 | 
            +
                    before_action :snapshot_inspector_headers, if: -> { Rails.env.test? }
         | 
| 8 | 
            +
                  end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  private
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def snapshot_inspector_headers
         | 
| 13 | 
            +
                    headers["X-SnapshotInspector-Mailer-Name"] = mailer_name
         | 
| 14 | 
            +
                    headers["X-SnapshotInspector-Action-Name"] = action_name
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
              end
         | 
| 18 | 
            +
            end
         |