├── .rspec
├── spec
├── support
│ ├── aruba.rb
│ ├── test_app.rb
│ ├── html_reporter_context.rb
│ └── common_setup.rb
├── cucumber
│ ├── support
│ │ └── env.rb
│ ├── step_definitions
│ │ └── step_definitions.rb
│ └── cucumber_spec.rb
├── unit
│ ├── rspec_reporters
│ │ ├── html_embed_reporter_spec.rb
│ │ ├── html_link_reporter_spec.rb
│ │ ├── textmate_link_reporter_spec.rb
│ │ └── text_reporter_spec.rb
│ ├── base_reporter_spec.rb
│ ├── capybara_spec.rb
│ ├── capybara-screenshot_rspec_spec.rb
│ ├── pruner_spec.rb
│ ├── capybara-screenshot_spec.rb
│ ├── s3_saver_spec.rb
│ └── saver_spec.rb
├── spinach
│ ├── support
│ │ └── spinach_failure.rb
│ └── spinach_spec.rb
├── spec_helper.rb
├── feature
│ ├── minitest_spec.rb
│ └── testunit_spec.rb
└── rspec
│ └── rspec_spec.rb
├── lib
├── capybara-screenshot
│ ├── version.rb
│ ├── rspec
│ │ ├── json_reporter.rb
│ │ ├── textmate_link_reporter.rb
│ │ ├── base_reporter.rb
│ │ ├── html_embed_reporter.rb
│ │ ├── text_reporter.rb
│ │ └── html_link_reporter.rb
│ ├── helpers.rb
│ ├── capybara.rb
│ ├── spinach.rb
│ ├── minitest.rb
│ ├── cucumber.rb
│ ├── callbacks.rb
│ ├── testunit.rb
│ ├── pruner.rb
│ ├── s3_saver.rb
│ ├── rspec.rb
│ └── saver.rb
└── capybara-screenshot.rb
├── .gitignore
├── Gemfile
├── gemfiles
├── latest.gemfile
├── rspec.3.0.gemfile
├── spinach.0.8.gemfile
├── cucumber.1.3.gemfile
└── cucumber.2.4.gemfile
├── .travis.yml
├── Appraisals
├── Rakefile
├── LICENSE
├── capybara-screenshot.gemspec
├── CHANGELOG.md
└── README.md
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --format documentation
3 |
--------------------------------------------------------------------------------
/spec/support/aruba.rb:
--------------------------------------------------------------------------------
1 | require 'aruba/rspec'
2 | require 'aruba/config/jruby'
3 |
--------------------------------------------------------------------------------
/lib/capybara-screenshot/version.rb:
--------------------------------------------------------------------------------
1 | module Capybara
2 | module Screenshot
3 | VERSION = '1.0.17'
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | .bundle
3 | Gemfile.lock
4 | gemfiles/*.lock
5 | pkg/*
6 | .rvmrc
7 | bin
8 | tmp
9 | .ruby-version
10 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | # Specify your gem's dependencies in capybara-screenshot.gemspec
4 | gemspec
5 |
6 | gem 'rack', '~> 1.0'
7 | gem 'rake', '~> 10.0'
8 | gem 'appraisal', '~> 2.0'
9 | gem 'aruba', '~> 0.14.0'
10 |
--------------------------------------------------------------------------------
/gemfiles/latest.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "http://rubygems.org"
4 |
5 | gem "rack", "~> 1.0"
6 | gem "rake", "~> 10.0"
7 | gem "appraisal", "~> 2.0"
8 | gem "aruba", "~> 0.14.0"
9 |
10 | gemspec :path => "../"
11 |
--------------------------------------------------------------------------------
/gemfiles/rspec.3.0.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "http://rubygems.org"
4 |
5 | gem "rack", "~> 1.0"
6 | gem "rake", "~> 10.0"
7 | gem "appraisal", "~> 2.0"
8 | gem "aruba", "~> 0.14.0"
9 | gem "rspec", "3.0"
10 |
11 | gemspec :path => "../"
12 |
--------------------------------------------------------------------------------
/spec/support/test_app.rb:
--------------------------------------------------------------------------------
1 | require 'sinatra/base'
2 |
3 | Sinatra::Application.root = '.'
4 |
5 | class TestApp < Sinatra::Base
6 | get '/' do
7 | 'This is the root page'
8 | end
9 |
10 | get '/different_page' do
11 | 'This is a different page'
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/gemfiles/spinach.0.8.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "http://rubygems.org"
4 |
5 | gem "rack", "~> 1.0"
6 | gem "rake", "~> 10.0"
7 | gem "appraisal", "~> 2.0"
8 | gem "aruba", "~> 0.14.0"
9 | gem "spinach", "~>0.8.0"
10 |
11 | gemspec :path => "../"
12 |
--------------------------------------------------------------------------------
/gemfiles/cucumber.1.3.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "http://rubygems.org"
4 |
5 | gem "rack", "~> 1.0"
6 | gem "rake", "~> 10.0"
7 | gem "appraisal", "~> 2.0"
8 | gem "aruba", "~> 0.14.0"
9 | gem "cucumber", "~>1.3.0"
10 |
11 | gemspec :path => "../"
12 |
--------------------------------------------------------------------------------
/gemfiles/cucumber.2.4.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "http://rubygems.org"
4 |
5 | gem "rack", "~> 1.0"
6 | gem "rake", "~> 10.0"
7 | gem "appraisal", "~> 2.0"
8 | gem "aruba", "~> 0.14.0"
9 | gem "cucumber", "~>2.4.0"
10 |
11 | gemspec :path => "../"
12 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | script: "bundle exec rake travis:ci"
3 | rvm:
4 | - 2.0.0
5 | - 2.1.10
6 | - 2.2.6
7 | - 2.3.3
8 | - 2.4.0
9 | gemfile:
10 | - gemfiles/cucumber.1.3.gemfile
11 | - gemfiles/cucumber.2.4.gemfile
12 | - gemfiles/latest.gemfile
13 | - gemfiles/rspec.3.0.gemfile
14 | - gemfiles/spinach.0.8.gemfile
15 |
--------------------------------------------------------------------------------
/Appraisals:
--------------------------------------------------------------------------------
1 | appraise "rspec.3.0" do
2 | gem "rspec", "3.0"
3 | end
4 |
5 | appraise "cucumber.1.3" do
6 | gem "cucumber", "~>1.3.0"
7 | end
8 |
9 | appraise "cucumber.2.4" do
10 | gem "cucumber", "~>2.4.0"
11 | end
12 |
13 | appraise "spinach.0.8" do
14 | gem "spinach", "~>0.8.0"
15 | end
16 |
17 | appraise "latest" do
18 | # will get latest versions of all gems
19 | end
20 |
--------------------------------------------------------------------------------
/spec/cucumber/support/env.rb:
--------------------------------------------------------------------------------
1 | require 'capybara/cucumber'
2 | require 'capybara-screenshot'
3 | require 'capybara-screenshot/cucumber'
4 | require 'aruba/cucumber'
5 | require 'aruba/config/jruby'
6 |
7 | Capybara::Screenshot.register_filename_prefix_formatter(:cucumber) do |fault|
8 | 'my_screenshot'
9 | end
10 |
11 | Before do
12 | @aruba_timeout_seconds = 60
13 | end if RUBY_PLATFORM == 'java'
14 |
15 | After('@restore-capybara-default-session') do
16 | Capybara.session_name = nil
17 | end
18 |
--------------------------------------------------------------------------------
/spec/cucumber/step_definitions/step_definitions.rb:
--------------------------------------------------------------------------------
1 | When(/^I click on a missing link$/) do
2 | click_on "you'll never find me"
3 | end
4 |
5 | When(/^I click on a missing link on a different page in a different session$/) do
6 | using_session :different_session do
7 | visit '/different_page'
8 | click_on "you'll never find me"
9 | end
10 | end
11 |
12 | When(/^I visit "([^"]*)"$/) do |path|
13 | visit path
14 | end
15 |
16 | Then(/^I trigger an unhandled exception/) do
17 | raise "you can't handle me"
18 | end
19 |
--------------------------------------------------------------------------------
/lib/capybara-screenshot/rspec/json_reporter.rb:
--------------------------------------------------------------------------------
1 | require 'capybara-screenshot/rspec/base_reporter'
2 |
3 | module Capybara
4 | module Screenshot
5 | module RSpec
6 | module JsonReporter
7 | extend BaseReporter
8 |
9 | enhance_with_screenshot :format_example
10 |
11 | def format_example_with_screenshot(example)
12 | format_example_without_screenshot(example).merge({
13 | screenshot: example.metadata[:screenshot]
14 | })
15 | end
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/capybara-screenshot/rspec/textmate_link_reporter.rb:
--------------------------------------------------------------------------------
1 | require 'capybara-screenshot/rspec/base_reporter'
2 | require 'capybara-screenshot/rspec/html_link_reporter'
3 | require 'shellwords'
4 |
5 | module Capybara
6 | module Screenshot
7 | module RSpec
8 | module TextMateLinkReporter
9 | extend BaseReporter
10 | include HtmlLinkReporter
11 | enhance_with_screenshot :extra_failure_content
12 |
13 | def attributes_for_screenshot_link(url)
14 | super.merge("onclick" => "TextMate.system('open #{Shellwords.escape(url)}'); return false;")
15 | end
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/capybara-screenshot/helpers.rb:
--------------------------------------------------------------------------------
1 | class CapybaraScreenshot
2 | module Helpers
3 | extend self
4 |
5 | COLORS =
6 | {
7 | "black" => 0,
8 | "red" => 1,
9 | "green" => 2,
10 | "yellow" => 3,
11 | "blue" => 4,
12 | "purple" => 5,
13 | "magenta" => 5,
14 | "cyan" => 6,
15 | "white" => 7
16 | }
17 |
18 | COLORS.each_pair do |color, value|
19 | define_method color do |text|
20 | "\033[0;#{30+value}m#{text}\033[0m"
21 | end
22 |
23 | define_method "bright_#{color}" do |text|
24 | "\033[1;#{30+value}m#{text}\033[0m"
25 | end
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/capybara-screenshot/rspec/base_reporter.rb:
--------------------------------------------------------------------------------
1 | module Capybara
2 | module Screenshot
3 | module RSpec
4 | module BaseReporter
5 |
6 | # Automatically set up method aliases (very much like ActiveSupport's `alias_method_chain`)
7 | # when the module gets included.
8 | def enhance_with_screenshot(method)
9 | with_method, without_method = "#{method}_with_screenshot", "#{method}_without_screenshot"
10 | define_singleton_method :included do |mod|
11 | if mod.method_defined?(method) || mod.private_method_defined?(method)
12 | mod.send :alias_method, without_method, method
13 | mod.send :alias_method, method, with_method
14 | end
15 | end
16 | end
17 |
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/spec/unit/rspec_reporters/html_embed_reporter_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Capybara::Screenshot::RSpec::HtmlEmbedReporter do
4 | include_context 'html reporter'
5 |
6 | context 'when an image was saved' do
7 | before do
8 | set_example double("example", metadata: {screenshot: {image: "path/to/image"}})
9 | end
10 |
11 | it 'embeds the image base64 encoded into the content' do
12 | expect(File).to receive(:binread).with("path/to/image").and_return("image data")
13 | encoded_image_data = Base64.encode64('image data')
14 | content_without_styles = @reporter.extra_failure_content(nil).gsub(/ ?style='.*?' ?/, "")
15 | expect(content_without_styles).to eql("original content")
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/spec/unit/base_reporter_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Capybara::Screenshot::RSpec::BaseReporter do
4 | describe '#enhance_with_screenshot' do
5 | it 'makes the original method available under an alias and replaces it with the enhanced method' do
6 | reporter_module = Module.new do
7 | extend Capybara::Screenshot::RSpec::BaseReporter
8 | enhance_with_screenshot :foo
9 | def foo_with_screenshot
10 | [foo_without_screenshot, :enhanced]
11 | end
12 | end
13 |
14 | klass = Class.new do
15 | def foo
16 | :original
17 | end
18 | end
19 |
20 | expect(klass.new.foo).to eql(:original)
21 | klass.send :include, reporter_module
22 | expect(klass.new.foo).to eql([:original, :enhanced])
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'bundler/setup'
3 |
4 | Bundler::GemHelper.install_tasks
5 |
6 | require 'rspec/core/rake_task'
7 |
8 | rspec_rake_task = RSpec::Core::RakeTask.new(:spec)
9 |
10 | task default: [:spec]
11 |
12 | def target_gem
13 | gem_file = ENV['BUNDLE_GEMFILE'] || ''
14 | targets = %w(cucumber spinach rspec)
15 |
16 | target = gem_file.match(/(#{targets.join('|')})/)
17 | if target && targets.include?(target[1])
18 | target[1].to_sym
19 | else
20 | []
21 | end
22 | end
23 |
24 | namespace :travis do
25 | task :ci => target_gem do
26 | Rake::Task['spec'].invoke
27 | end
28 |
29 | task :cucumber do
30 | rspec_rake_task.pattern = 'spec/cucumber'
31 | end
32 |
33 | task :spinach do
34 | rspec_rake_task.pattern = 'spec/spinach'
35 | end
36 |
37 | task :rspec do
38 | rspec_rake_task.pattern = 'spec/rspec'
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/lib/capybara-screenshot/capybara.rb:
--------------------------------------------------------------------------------
1 | require "capybara-screenshot"
2 |
3 | module Capybara
4 | module DSL
5 | # Adds class methods to Capybara module and gets mixed into
6 | # the current scope during Cucumber and RSpec tests
7 |
8 | def screenshot_and_save_page
9 | Capybara::Screenshot.screenshot_and_save_page
10 | end
11 |
12 | def screenshot_and_open_image
13 | Capybara::Screenshot.screenshot_and_open_image
14 | end
15 |
16 | def using_session_with_screenshot(name,&blk)
17 | original_session_name = Capybara.session_name
18 | Capybara::Screenshot.final_session_name = name
19 | using_session_without_screenshot(name,&blk)
20 | Capybara::Screenshot.final_session_name = original_session_name
21 | end
22 |
23 | alias_method :using_session_without_screenshot, :using_session
24 | alias_method :using_session, :using_session_with_screenshot
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/spec/support/html_reporter_context.rb:
--------------------------------------------------------------------------------
1 | shared_context 'html reporter' do
2 | def set_example(example)
3 | @reporter.instance_variable_set :@failed_examples, [example]
4 | end
5 |
6 | before do
7 | # Mocking `RSpec::Core::Formatters::HtmlFormatter`, but only implementing the things that
8 | # are actually used in `HtmlLinkReporter#extra_failure_content_with_screenshot`.
9 | @reporter_class = Class.new do
10 | def extra_failure_content(exception)
11 | "original content"
12 | end
13 | end
14 |
15 | @reporter = @reporter_class.new
16 | @reporter.singleton_class.send :include, described_class
17 | end
18 |
19 | context 'when there is no screenshot' do
20 | before do
21 | set_example double("example", metadata: {})
22 | end
23 |
24 | it 'doesnt change the original content of the reporter' do
25 | expect(@reporter.extra_failure_content(nil)).to eql("original content")
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/capybara-screenshot/spinach.rb:
--------------------------------------------------------------------------------
1 | require "capybara-screenshot"
2 |
3 | Spinach.hooks.before_scenario do |scenario|
4 | Capybara::Screenshot.final_session_name = nil
5 | end
6 |
7 | module Capybara::Screenshot::Spinach
8 | def self.fail_with_screenshot(step_data, exception, location, step_definitions)
9 | if Capybara::Screenshot.autosave_on_failure
10 | Capybara.using_session(Capybara::Screenshot.final_session_name) do
11 | filename_prefix = Capybara::Screenshot.filename_prefix_for(:spinach, step_data)
12 | saver = Capybara::Screenshot.new_saver(Capybara, Capybara.page, true, filename_prefix)
13 | saver.save
14 | saver.output_screenshot_path
15 | end
16 | end
17 | end
18 | end
19 |
20 | Spinach.hooks.on_failed_step do |*args|
21 | Capybara::Screenshot::Spinach.fail_with_screenshot(*args)
22 | end
23 |
24 | Spinach.hooks.on_error_step do |*args|
25 | Capybara::Screenshot::Spinach.fail_with_screenshot(*args)
26 | end
27 |
--------------------------------------------------------------------------------
/lib/capybara-screenshot/rspec/html_embed_reporter.rb:
--------------------------------------------------------------------------------
1 | require 'capybara-screenshot/rspec/base_reporter'
2 | require 'base64'
3 |
4 | module Capybara
5 | module Screenshot
6 | module RSpec
7 | module HtmlEmbedReporter
8 | extend BaseReporter
9 | enhance_with_screenshot :extra_failure_content
10 |
11 | def extra_failure_content_with_screenshot(exception)
12 | result = extra_failure_content_without_screenshot(exception)
13 | example = @failed_examples.last
14 | # Ignores saved html file, only saved image will be embedded (if present)
15 | if (screenshot = example.metadata[:screenshot]) && screenshot[:image]
16 | image = File.binread(screenshot[:image])
17 | encoded_img = Base64.encode64(image)
18 | result += "
"
19 | end
20 | result
21 | end
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/capybara-screenshot/minitest.rb:
--------------------------------------------------------------------------------
1 | require "capybara-screenshot"
2 |
3 | module Capybara::Screenshot::MiniTestPlugin
4 | def before_setup
5 | super
6 | Capybara::Screenshot.final_session_name = nil
7 | end
8 |
9 | def before_teardown
10 | super
11 | if self.class.ancestors.map(&:to_s).include?('Capybara::DSL')
12 | if Capybara::Screenshot.autosave_on_failure && !passed? && !skipped?
13 | Capybara.using_session(Capybara::Screenshot.final_session_name) do
14 | filename_prefix = Capybara::Screenshot.filename_prefix_for(:minitest, self)
15 |
16 | saver = Capybara::Screenshot.new_saver(Capybara, Capybara.page, true, filename_prefix)
17 | saver.save
18 | saver.output_screenshot_path
19 | end
20 | end
21 | end
22 | end
23 | end
24 |
25 | begin
26 | Minitest.const_get('Test')
27 | class Minitest::Test
28 | include Capybara::Screenshot::MiniTestPlugin
29 | end
30 | rescue NameError => e
31 | class MiniTest::Unit::TestCase
32 | include Capybara::Screenshot::MiniTestPlugin
33 | end
34 | end
35 |
36 |
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2011 Matthew O'Riordan, inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/lib/capybara-screenshot/cucumber.rb:
--------------------------------------------------------------------------------
1 | require "capybara-screenshot"
2 |
3 | Before do |scenario|
4 | Capybara::Screenshot.final_session_name = nil
5 | end
6 |
7 | After do |scenario|
8 | if Capybara::Screenshot.autosave_on_failure && scenario.failed?
9 | Capybara.using_session(Capybara::Screenshot.final_session_name) do
10 | filename_prefix = Capybara::Screenshot.filename_prefix_for(:cucumber, scenario)
11 |
12 | saver = Capybara::Screenshot.new_saver(Capybara, Capybara.page, true, filename_prefix)
13 | saver.save
14 | saver.output_screenshot_path
15 |
16 | # Trying to embed the screenshot into our output."
17 | if File.exist?(saver.screenshot_path)
18 | require "base64"
19 | #encode the image into it's base64 representation
20 | image = open(saver.screenshot_path, 'rb') {|io|io.read}
21 | saver.display_image
22 | #this will embed the image in the HTML report, embed() is defined in cucumber
23 | encoded_img = Base64.encode64(image)
24 | embed(encoded_img, 'image/png;base64', "Screenshot of the error")
25 | end
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/capybara-screenshot/callbacks.rb:
--------------------------------------------------------------------------------
1 | module Capybara
2 | module Screenshot
3 | module Callbacks
4 | class CallbackSet < Array
5 | def call *args
6 | each do |callback|
7 | callback.call *args
8 | end
9 | end
10 | end
11 |
12 | module ClassMethods
13 | def callbacks
14 | @callbacks ||= {}
15 | end
16 |
17 | def define_callback name
18 | callbacks[name] ||= CallbackSet.new
19 |
20 | define_singleton_method name do |&block|
21 | callbacks[name] << block
22 | end
23 | end
24 |
25 | def run_callbacks name, *args
26 | if cb_set = callbacks[name]
27 | cb_set.call *args
28 | end
29 | end
30 | end
31 |
32 | module InstanceMethods
33 | def run_callbacks name, *args
34 | self.class.run_callbacks name, *args
35 | end
36 | end
37 |
38 | def self.included receiver
39 | receiver.extend ClassMethods
40 | receiver.send :include, InstanceMethods
41 | end
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/spec/unit/rspec_reporters/html_link_reporter_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Capybara::Screenshot::RSpec::HtmlLinkReporter do
4 | include_context 'html reporter'
5 |
6 | context 'when a html file was saved' do
7 | before do
8 | set_example double("example", metadata: {screenshot: {html: "path/to/a html file"}})
9 | end
10 |
11 | it 'appends a link to the html to the original content' do
12 | content_without_styles = @reporter.extra_failure_content(nil).gsub(/ ?style=".*?" ?/, "")
13 | expect(content_without_styles).to eql(%{original content
Saved files: HTML page
}) 14 | end 15 | end 16 | 17 | context 'when a html file and an image were saved' do 18 | before do 19 | set_example double("example", metadata: {screenshot: {html: "path/to/html", image: "path/to/an image"}}) 20 | end 21 | 22 | it 'appends links to both files to the original content' do 23 | content_without_styles = @reporter.extra_failure_content(nil).gsub(/ ?style=".*?" ?/, "") 24 | expect(content_without_styles).to eql(%{original contentSaved files: HTML pageScreenshot
}) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/capybara-screenshot/testunit.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit/testresult' 2 | 3 | module Capybara::Screenshot 4 | class << self 5 | attr_accessor :testunit_paths 6 | end 7 | 8 | self.testunit_paths = [%r{test/integration}] 9 | end 10 | 11 | Test::Unit::TestCase.class_eval do 12 | setup do 13 | Capybara::Screenshot.final_session_name = nil 14 | end 15 | end 16 | 17 | Test::Unit::TestResult.class_eval do 18 | private 19 | 20 | def notify_fault_with_screenshot(fault, *args) 21 | notify_fault_without_screenshot fault, *args 22 | is_integration_test = fault.location.any? do |location| 23 | Capybara::Screenshot.testunit_paths.any? { |path| location.match(path) } 24 | end 25 | if is_integration_test 26 | if Capybara::Screenshot.autosave_on_failure 27 | Capybara.using_session(Capybara::Screenshot.final_session_name) do 28 | filename_prefix = Capybara::Screenshot.filename_prefix_for(:testunit, fault) 29 | 30 | saver = Capybara::Screenshot.new_saver(Capybara, Capybara.page, true, filename_prefix) 31 | saver.save 32 | saver.output_screenshot_path 33 | end 34 | end 35 | end 36 | end 37 | alias notify_fault_without_screenshot notify_fault 38 | alias notify_fault notify_fault_with_screenshot 39 | end 40 | -------------------------------------------------------------------------------- /spec/spinach/support/spinach_failure.rb: -------------------------------------------------------------------------------- 1 | require 'capybara' 2 | require 'capybara/rspec' 3 | require 'capybara-screenshot' 4 | require 'capybara-screenshot/spinach' 5 | require 'spinach/capybara' 6 | require 'support/test_app' 7 | 8 | Spinach.config.failure_exceptions = [Capybara::ElementNotFound] 9 | 10 | class Spinach::Features::Failure < Spinach::FeatureSteps 11 | include ::Capybara::DSL 12 | 13 | before do 14 | ::Capybara::Screenshot.register_filename_prefix_formatter(:spinach) do |failed_step| 15 | raise 'expected failing step' if !@expected_failed_step.nil? && !failed_step.name.include?(@expected_failed_step) 16 | 'my_screenshot' 17 | end 18 | end 19 | 20 | step 'I visit "/"' do 21 | visit '/' 22 | end 23 | 24 | step 'I click on a missing link' do 25 | @expected_failed_step = 'I click on a missing link' 26 | click_on "you'll never find me" 27 | end 28 | 29 | step 'I trigger an unhandled exception' do 30 | @expected_failed_step = "I trigger an unhandled exception" 31 | raise "you can't handle me" 32 | end 33 | 34 | step 'I click on a missing link on a different page in a different session' do 35 | using_session :different_session do 36 | visit '/different_page' 37 | @expected_failed_step = 'I click on a missing link on a different page in a different session' 38 | click_on "you'll never find me" 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/capybara-screenshot/rspec/text_reporter.rb: -------------------------------------------------------------------------------- 1 | require 'capybara-screenshot/rspec/base_reporter' 2 | require 'capybara-screenshot/helpers' 3 | 4 | module Capybara 5 | module Screenshot 6 | module RSpec 7 | module TextReporter 8 | extend BaseReporter 9 | 10 | if ::RSpec::Core::Version::STRING.to_i <= 2 11 | enhance_with_screenshot :dump_failure_info 12 | else 13 | enhance_with_screenshot :example_failed 14 | end 15 | 16 | def dump_failure_info_with_screenshot(example) 17 | dump_failure_info_without_screenshot example 18 | output_screenshot_info(example) 19 | end 20 | 21 | def example_failed_with_screenshot(notification) 22 | example_failed_without_screenshot notification 23 | output_screenshot_info(notification.example) 24 | end 25 | 26 | private 27 | def output_screenshot_info(example) 28 | return unless (screenshot = example.metadata[:screenshot]) 29 | output.puts(long_padding + CapybaraScreenshot::Helpers.yellow("HTML screenshot: file://#{screenshot[:html]}")) if screenshot[:html] 30 | output.puts(long_padding + CapybaraScreenshot::Helpers.yellow("Image screenshot: file://#{screenshot[:image]}")) if screenshot[:image] 31 | end 32 | 33 | def long_padding 34 | " " 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/capybara-screenshot/rspec/html_link_reporter.rb: -------------------------------------------------------------------------------- 1 | require 'capybara-screenshot/rspec/base_reporter' 2 | require 'cgi' 3 | require 'uri' 4 | 5 | module Capybara 6 | module Screenshot 7 | module RSpec 8 | module HtmlLinkReporter 9 | extend BaseReporter 10 | enhance_with_screenshot :extra_failure_content 11 | 12 | def extra_failure_content_with_screenshot(exception) 13 | result = extra_failure_content_without_screenshot(exception) 14 | example = @failed_examples.last 15 | if (screenshot = example.metadata[:screenshot]) 16 | result << "Saved files: " 17 | result << link_to_screenshot("HTML page", screenshot[:html]) if screenshot[:html] 18 | result << link_to_screenshot("Screenshot", screenshot[:image]) if screenshot[:image] 19 | result << "
" 20 | end 21 | result 22 | end 23 | 24 | def link_to_screenshot(title, path) 25 | url = URI.escape("file://#{path}") 26 | title = CGI.escape_html(title) 27 | attributes = attributes_for_screenshot_link(url).map { |name, val| %{#{name}="#{CGI.escape_html(val)}"} }.join(" ") 28 | "#{title}" 29 | end 30 | 31 | def attributes_for_screenshot_link(url) 32 | {"href" => url, "style" => "margin-right: 10px; font-weight: bold"} 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rspec --init` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # Require this file using `require "spec_helper.rb"` to ensure that it is only 4 | # loaded once. 5 | # 6 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 7 | 8 | $: << '../lib' 9 | require 'rspec' 10 | require 'capybara-screenshot' 11 | require 'capybara-screenshot/rspec' 12 | require 'timecop' 13 | 14 | RSpec.configure do |config| 15 | if RSpec::Core::Version::STRING.to_i == 2 16 | config.treat_symbols_as_metadata_keys_with_true_values = true 17 | end 18 | config.run_all_when_everything_filtered = true 19 | config.filter_run :focus 20 | config.before do 21 | @aruba_timeout_seconds = 60 22 | end if RUBY_PLATFORM == 'java' 23 | end 24 | 25 | Capybara.app = lambda { |env| [200, {}, ["OK"]] } 26 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 27 | 28 | if RUBY_VERSION < '1.9.3' 29 | ::Dir.glob(::File.expand_path('../support/*.rb', __FILE__)).each { |f| require File.join(File.dirname(f), File.basename(f, '.rb')) } 30 | ::Dir.glob(::File.expand_path('../support/**/*.rb', __FILE__)).each { |f| require File.join(File.dirname(f), File.basename(f, '.rb')) } 31 | else 32 | ::Dir.glob(::File.expand_path('../support/*.rb', __FILE__)).each { |f| require_relative f } 33 | ::Dir.glob(::File.expand_path('../support/**/*.rb', __FILE__)).each { |f| require_relative f } 34 | end 35 | -------------------------------------------------------------------------------- /lib/capybara-screenshot/pruner.rb: -------------------------------------------------------------------------------- 1 | module Capybara 2 | module Screenshot 3 | class Pruner 4 | attr_reader :strategy 5 | 6 | def initialize(strategy) 7 | @strategy = strategy 8 | 9 | @strategy_proc = case strategy 10 | when :keep_all 11 | lambda { } 12 | when :keep_last_run 13 | lambda { prune_with_last_run_strategy } 14 | when Hash 15 | raise ArgumentError, ":keep key is required" unless strategy[:keep] 16 | raise ArgumentError, ":keep must be a Integer" unless strategy[:keep].kind_of?(Integer) 17 | raise ArgumentError, ":keep value must be number greater than zero" unless strategy[:keep].to_i > 0 18 | lambda { prune_with_numeric_strategy(strategy[:keep].to_i) } 19 | else 20 | fail "Invalid prune strategy #{strategy}. `:keep_all`or `{ keep: 10 }` are valid examples." 21 | end 22 | end 23 | 24 | def prune_old_screenshots 25 | strategy_proc.call 26 | end 27 | 28 | private 29 | attr_reader :strategy_proc 30 | 31 | def wildcard_path 32 | File.expand_path('*.{html,png}', Screenshot.capybara_root) 33 | end 34 | 35 | def prune_with_last_run_strategy 36 | FileUtils.rm_rf(Dir.glob(wildcard_path)) 37 | end 38 | 39 | def prune_with_numeric_strategy(count) 40 | files = Dir.glob(wildcard_path).sort_by do |file_name| 41 | File.mtime(File.expand_path(file_name, Screenshot.capybara_root)) 42 | end 43 | 44 | FileUtils.rm_rf(files[0...-count]) 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /capybara-screenshot.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "capybara-screenshot/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "capybara-screenshot" 7 | s.version = Capybara::Screenshot::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ["Matthew O'Riordan"] 10 | s.email = ["matthew.oriordan@gmail.com"] 11 | s.homepage = "http://github.com/mattheworiordan/capybara-screenshot" 12 | s.summary = %q{Automatically create snapshots when Cucumber steps fail with Capybara and Rails} 13 | s.description = %q{When a Cucumber step fails, it is useful to create a screenshot image and HTML file of the current page} 14 | s.license = 'MIT' 15 | 16 | s.rubyforge_project = "capybara-screenshot" 17 | 18 | if RUBY_VERSION < "1.9" 19 | s.add_dependency 'capybara', ['>= 1.0', '< 2'] 20 | else 21 | s.add_dependency 'capybara', ['>= 1.0', '< 3'] 22 | end 23 | s.add_dependency 'launchy' 24 | 25 | s.add_development_dependency 'rspec' 26 | s.add_development_dependency 'timecop' 27 | s.add_development_dependency 'cucumber' 28 | s.add_development_dependency 'aruba' 29 | s.add_development_dependency 'sinatra' 30 | s.add_development_dependency 'test-unit' 31 | s.add_development_dependency 'spinach' 32 | s.add_development_dependency 'minitest' 33 | s.add_development_dependency 'aws-sdk' 34 | 35 | s.files = `git ls-files`.split("\n") 36 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 37 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 38 | s.require_paths = ["lib"] 39 | end 40 | -------------------------------------------------------------------------------- /spec/unit/capybara_spec.rb: -------------------------------------------------------------------------------- 1 | require 'capybara-screenshot' 2 | require 'capybara/dsl' 3 | 4 | describe Capybara do 5 | it 'adds screen shot methods to the Capybara module' do 6 | expect(::Capybara).to respond_to(:screenshot_and_save_page) 7 | expect(::Capybara).to respond_to(:screenshot_and_open_image) 8 | end 9 | 10 | context 'request type example', :type => :request do 11 | it 'has access to screen shot instance methods' do 12 | expect(subject).to respond_to(:screenshot_and_save_page) 13 | expect(subject).to respond_to(:screenshot_and_open_image) 14 | end 15 | end 16 | 17 | describe 'using_session' do 18 | include Capybara::DSL 19 | 20 | it 'saves the name of the final session' do 21 | expect(Capybara::Screenshot).to receive(:final_session_name=).with(:different_session) 22 | expect { 23 | using_session :different_session do 24 | expect(0).to eq 1 25 | end 26 | }.to raise_exception ::RSpec::Expectations::ExpectationNotMetError 27 | end 28 | end 29 | end 30 | 31 | describe 'final_session_name' do 32 | subject { Capybara::Screenshot.clone } 33 | 34 | describe 'when the final session name has been set' do 35 | before do 36 | subject.final_session_name = 'my-failing-session' 37 | end 38 | 39 | it 'returns the name' do 40 | expect(subject.final_session_name).to eq 'my-failing-session' 41 | end 42 | end 43 | 44 | describe 'when the final session name has not been set' do 45 | it 'returns the current session name' do 46 | allow(Capybara).to receive(:session_name).and_return('my-current-session') 47 | expect(subject.final_session_name).to eq 'my-current-session' 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/unit/capybara-screenshot_rspec_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Capybara::Screenshot::RSpec do 4 | describe '.after_failed_example' do 5 | context 'for a failed example in a feature that can be snapshotted' do 6 | before do 7 | allow(Capybara.page).to receive(:current_url).and_return("http://test.local") 8 | allow(Capybara::Screenshot::Saver).to receive(:new).and_return(mock_saver) 9 | end 10 | let(:example_group) { Module.new.send(:include, Capybara::DSL) } 11 | let(:example) { double("example", exception: Exception.new, example_group: example_group, metadata: {}) } 12 | let(:mock_saver) do 13 | Capybara::Screenshot::Saver.new(Capybara, Capybara.page).tap do |saver| 14 | allow(saver).to receive(:save) 15 | end 16 | end 17 | 18 | it 'instantiates a saver and calls `save` on it' do 19 | expect(mock_saver).to receive(:save) 20 | described_class.after_failed_example(example) 21 | end 22 | 23 | it 'extends the metadata with an empty hash for screenshot metadata' do 24 | described_class.after_failed_example(example) 25 | expect(example.metadata).to have_key(:screenshot) 26 | expect(example.metadata[:screenshot]).to eql({}) 27 | end 28 | 29 | context 'when a html file gets saved' do 30 | before { allow(mock_saver).to receive(:html_saved?).and_return(true) } 31 | 32 | it 'adds the html file path to the screenshot metadata' do 33 | described_class.after_failed_example(example) 34 | expect(example.metadata[:screenshot][:html]).to match("./screenshot") 35 | end 36 | end 37 | 38 | context 'when an image gets saved' do 39 | before { allow(mock_saver).to receive(:screenshot_saved?).and_return(true) } 40 | 41 | it 'adds the image path to the screenshot metadata' do 42 | described_class.after_failed_example(example) 43 | expect(example.metadata[:screenshot][:image]).to match("./screenshot") 44 | end 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/unit/rspec_reporters/textmate_link_reporter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Capybara::Screenshot::RSpec::TextMateLinkReporter do 4 | include_context 'html reporter' 5 | 6 | context 'when a html file was saved' do 7 | before do 8 | set_example double("example", metadata: {screenshot: {html: "path/to/a html file"}}) 9 | end 10 | 11 | it 'appends a link to the html to the original content' do 12 | content_without_styles = @reporter.extra_failure_content(nil).gsub(/ ?style=".*?"/, "") 13 | # Single quotes are handled differently by CGI.escape_html in Ruby 1.9 / Ruby 2, so to be 14 | # compatible with both versions we can't hard code the final escaped string. 15 | expected_onclick_handler = CGI.escape_html("TextMate.system('open file://path/to/a\\%20html\\%20file'); return false;") 16 | expect(content_without_styles).to eql(%{original content} + 17 | %{Saved files: HTML page
} 18 | ) 19 | end 20 | end 21 | 22 | context 'when a html file and an image were saved' do 23 | before do 24 | set_example double("example", metadata: {screenshot: {html: "path/to/html", image: "path/to/an image"}}) 25 | end 26 | 27 | it 'appends links to both files to the original content' do 28 | content_without_styles = @reporter.extra_failure_content(nil).gsub(/ ?style=".*?"/, "") 29 | # Single quotes are handled differently by CGI.escape_html in Ruby 1.9 / Ruby 2, so to be 30 | # compatible with both versions we can't hard code the final escaped string. 31 | expected_onclick_handler_1 = CGI.escape_html("TextMate.system('open file://path/to/html'); return false;") 32 | expected_onclick_handler_2 = CGI.escape_html("TextMate.system('open file://path/to/an\\%20image'); return false;") 33 | expect(content_without_styles).to eql(%{original content} + 34 | %{Saved files: HTML page} + 35 | %{Screenshot
} 36 | ) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/support/common_setup.rb: -------------------------------------------------------------------------------- 1 | module CommonSetup 2 | def self.included(target) 3 | target.class_eval do 4 | include Aruba::Api 5 | end 6 | 7 | target.let(:gem_root) { File.expand_path('../..', File.dirname(__FILE__)) } 8 | 9 | target.let(:ensure_load_paths_valid) do 10 | <<-RUBY 11 | %w(lib spec).each do |include_folder| 12 | $LOAD_PATH.unshift(File.join('#{gem_root}', include_folder)) 13 | end 14 | RUBY 15 | end 16 | 17 | target.let(:screenshot_path) { 'tmp' } 18 | target.let(:screenshot_for_pruning_path) { "#{screenshot_path}/old_screenshot.html" } 19 | 20 | target.let(:setup_test_app) do 21 | <<-RUBY 22 | require 'support/test_app' 23 | Capybara::Screenshot.capybara_tmp_path = '#{screenshot_path}' 24 | Capybara.app = TestApp 25 | Capybara::Screenshot.append_timestamp = false 26 | #{@additional_setup_steps} 27 | RUBY 28 | end 29 | 30 | target.before do 31 | if ENV['BUNDLE_GEMFILE'] && ENV['BUNDLE_GEMFILE'].match(/^\.|^[^\/\.]/) 32 | ENV['BUNDLE_GEMFILE'] = File.join(gem_root, ENV['BUNDLE_GEMFILE']) 33 | end 34 | end 35 | 36 | target.after(:each) do |example| 37 | if example.exception 38 | puts "Output from failed Aruba test:" 39 | puts all_output.split(/\n/).map { |line| " #{line}"} 40 | puts "" 41 | end 42 | end 43 | 44 | def run_simple_with_retry(*args) 45 | run_simple(*args) 46 | rescue ChildProcess::TimeoutError => e 47 | puts "run_simple(#{args.join(', ')}) failed. Will retry once. `#{e.message}`" 48 | run_simple(*args) 49 | end 50 | 51 | def configure_prune_strategy(strategy) 52 | @additional_setup_steps = "Capybara::Screenshot.prune_strategy = :keep_last_run" 53 | end 54 | 55 | def create_screenshot_for_pruning 56 | write_file screenshot_for_pruning_path, '' 57 | end 58 | 59 | def assert_screenshot_pruned 60 | expect(screenshot_for_pruning_path).to_not be_an_existing_file 61 | end 62 | 63 | def assert_screenshot_not_pruned 64 | expect(screenshot_for_pruning_path).to be_an_existing_file 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/spinach/spinach_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "Using Capybara::Screenshot with Spinach" do 4 | include CommonSetup 5 | 6 | def run_failing_case(failure_message, code) 7 | write_file('steps/failure.rb', <<-RUBY) 8 | #{ensure_load_paths_valid} 9 | require 'spinach/support/spinach_failure.rb' 10 | #{setup_test_app} 11 | RUBY 12 | 13 | write_file('spinach.feature', code) 14 | cmd = 'bundle exec spinach -f .' 15 | run_simple_with_retry cmd, false 16 | expect(last_command_started.output).to match(failure_message) 17 | end 18 | 19 | it "saves a screenshot on failure" do 20 | run_failing_case(%q{Unable to find link or button "you'll never find me"}, <<-GHERKIN) 21 | Feature: Failure 22 | Scenario: Failure 23 | Given I visit "/" 24 | And I click on a missing link 25 | GHERKIN 26 | expect('tmp/my_screenshot.html').to have_file_content('This is the root page') 27 | end 28 | 29 | it "saves a screenshot on an error" do 30 | run_failing_case(%q{you can't handle me}, <<-GHERKIN) 31 | Feature: Failure 32 | Scenario: Failure 33 | Given I visit "/" 34 | And I trigger an unhandled exception 35 | GHERKIN 36 | expect('tmp/my_screenshot.html').to have_file_content('This is the root page') 37 | end 38 | 39 | it "saves a screenshot for the correct session for failures using_session" do 40 | run_failing_case(%q{Unable to find link or button "you'll never find me"}, <<-GHERKIN) 41 | Feature: Failure 42 | Scenario: Failure in different session 43 | Given I visit "/" 44 | And I click on a missing link on a different page in a different session 45 | GHERKIN 46 | expect('tmp/my_screenshot.html').to have_file_content('This is a different page') 47 | end 48 | 49 | it 'on failure it prunes previous screenshots when strategy is set' do 50 | create_screenshot_for_pruning 51 | configure_prune_strategy :last_run 52 | run_failing_case(%q{Unable to find link or button "you'll never find me"}, <<-GHERKIN) 53 | Feature: Failure 54 | Scenario: Failure 55 | Given I visit "/" 56 | And I click on a missing link 57 | GHERKIN 58 | assert_screenshot_pruned 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/capybara-screenshot/s3_saver.rb: -------------------------------------------------------------------------------- 1 | require 'aws-sdk' 2 | 3 | module Capybara 4 | module Screenshot 5 | class S3Saver 6 | DEFAULT_REGION = 'us-east-1' 7 | 8 | def initialize(saver, s3_client, bucket_name, object_configuration, options={}) 9 | @saver = saver 10 | @s3_client = s3_client 11 | @bucket_name = bucket_name 12 | @key_prefix = options[:key_prefix] 13 | @object_configuration = object_configuration 14 | end 15 | 16 | def self.new_with_configuration(saver, configuration, object_configuration) 17 | default_s3_client_credentials = { 18 | region: DEFAULT_REGION 19 | } 20 | 21 | s3_client_credentials = default_s3_client_credentials.merge( 22 | configuration.fetch(:s3_client_credentials) 23 | ) 24 | 25 | s3_client = Aws::S3::Client.new(s3_client_credentials) 26 | bucket_name = configuration.fetch(:bucket_name) 27 | 28 | new(saver, s3_client, bucket_name, object_configuration, configuration) 29 | rescue KeyError 30 | raise "Invalid S3 Configuration #{configuration}. Please refer to the documentation for the necessary configurations." 31 | end 32 | 33 | def save_and_upload_screenshot 34 | save_and do |local_file_path| 35 | File.open(local_file_path) do |file| 36 | object_payload = { 37 | bucket: bucket_name, 38 | key: "#{@key_prefix}#{File.basename(local_file_path)}", 39 | body: file 40 | } 41 | 42 | object_payload.merge!(object_configuration) unless object_configuration.empty? 43 | 44 | s3_client.put_object( 45 | object_payload 46 | ) 47 | end 48 | end 49 | end 50 | alias_method :save, :save_and_upload_screenshot 51 | 52 | def method_missing(method, *args) 53 | # Need to use @saver instead of S3Saver#saver attr_reader method because 54 | # using the method goes into infinite loop. Maybe attr_reader implements 55 | # its methods via method_missing? 56 | @saver.send(method, *args) 57 | end 58 | 59 | private 60 | attr_reader :saver, 61 | :s3_client, 62 | :bucket_name, 63 | :object_configuration 64 | :key_prefix 65 | 66 | def save_and 67 | saver.save 68 | 69 | yield(saver.html_path) if block_given? && saver.html_saved? 70 | yield(saver.screenshot_path) if block_given? && saver.screenshot_saved? 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/feature/minitest_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "Using Capybara::Screenshot with MiniTest" do 4 | include CommonSetup 5 | 6 | def run_failing_case(code) 7 | write_file('test_failure.rb', <<-RUBY) 8 | #{ensure_load_paths_valid} 9 | require 'minitest/autorun' 10 | require 'capybara' 11 | require 'capybara-screenshot' 12 | require 'capybara-screenshot/minitest' 13 | 14 | #{setup_test_app} 15 | Capybara::Screenshot.register_filename_prefix_formatter(:minitest) do |test_case| 16 | test_name = test_case.respond_to?(:name) ? test_case.name : test_case.__name__ 17 | raise "expected fault" unless test_name.include? 'test_failure' 18 | 'my_screenshot' 19 | end 20 | 21 | #{code} 22 | RUBY 23 | 24 | cmd = 'bundle exec ruby test_failure.rb' 25 | run_simple_with_retry cmd, false 26 | expect(last_command_started.output).to include %q{Unable to find link or button "you'll never find me"} 27 | end 28 | 29 | it 'saves a screenshot on failure' do 30 | run_failing_case <<-RUBY 31 | class TestFailure < MiniTest::Unit::TestCase 32 | include Capybara::DSL 33 | 34 | def test_failure 35 | visit '/' 36 | assert(page.body.include?('This is the root page')) 37 | click_on "you'll never find me" 38 | end 39 | end 40 | RUBY 41 | expect('tmp/my_screenshot.html').to have_file_content('This is the root page') 42 | end 43 | 44 | it 'saves a screenshot for the correct session for failures using_session' do 45 | run_failing_case <<-RUBY 46 | class TestFailure < Minitest::Unit::TestCase 47 | include Capybara::DSL 48 | 49 | def test_failure 50 | visit '/' 51 | assert(page.body.include?('This is the root page')) 52 | using_session :different_session do 53 | visit '/different_page' 54 | assert(page.body.include?('This is a different page')) 55 | click_on "you'll never find me" 56 | end 57 | end 58 | end 59 | RUBY 60 | expect('tmp/my_screenshot.html').to have_file_content('This is a different page') 61 | end 62 | 63 | it 'prunes screenshots on failure' do 64 | create_screenshot_for_pruning 65 | configure_prune_strategy :last_run 66 | run_failing_case <<-RUBY 67 | class TestFailure < Minitest::Unit::TestCase 68 | include Capybara::DSL 69 | 70 | def test_failure 71 | visit '/' 72 | assert(page.body.include?('This is the root page')) 73 | click_on "you'll never find me" 74 | end 75 | end 76 | RUBY 77 | assert_screenshot_pruned 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /spec/feature/testunit_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "Using Capybara::Screenshot with Test::Unit" do 4 | include CommonSetup 5 | 6 | def run_failing_case(code, integration_path = '.') 7 | write_file("#{integration_path}/test_failure.rb", <<-RUBY) 8 | #{ensure_load_paths_valid} 9 | require 'test/unit' 10 | require 'capybara' 11 | require 'capybara/rspec' 12 | require 'capybara-screenshot' 13 | require 'capybara-screenshot/testunit' 14 | 15 | #{setup_test_app} 16 | Capybara::Screenshot.register_filename_prefix_formatter(:testunit) do | fault | 17 | raise "expected fault" unless fault.exception.message.include? %q{Unable to find link or button "you'll never find me"} 18 | 'my_screenshot' 19 | end 20 | 21 | class TestFailure < Test::Unit::TestCase 22 | include Capybara::DSL 23 | 24 | def test_failure 25 | #{code} 26 | end 27 | end 28 | RUBY 29 | 30 | cmd = "bundle exec ruby #{integration_path}/test_failure.rb" 31 | run_simple_with_retry cmd, false 32 | expect(last_command_started.output).to include %q{Unable to find link or button "you'll never find me"} 33 | end 34 | 35 | it "saves a screenshot on failure for any test in path 'test/integration'" do 36 | run_failing_case <<-RUBY, 'test/integration' 37 | visit '/' 38 | assert(page.body.include?('This is the root page')) 39 | click_on "you'll never find me" 40 | RUBY 41 | expect('tmp/my_screenshot.html').to have_file_content('This is the root page') 42 | end 43 | 44 | it "does not generate a screenshot for tests that are not in 'test/integration'" do 45 | run_failing_case <<-RUBY, 'test/something-else' 46 | visit '/' 47 | assert(page.body.include?('This is the root page')) 48 | click_on "you'll never find me" 49 | RUBY 50 | 51 | expect('tmp/my_screenshot.html').to_not be_an_existing_file 52 | end 53 | 54 | it 'saves a screenshot for the correct session for failures using_session' do 55 | run_failing_case <<-RUBY, 'test/integration' 56 | visit '/' 57 | assert(page.body.include?('This is the root page')) 58 | using_session :different_session do 59 | visit '/different_page' 60 | assert(page.body.include?('This is a different page')) 61 | click_on "you'll never find me" 62 | end 63 | RUBY 64 | expect('tmp/my_screenshot.html').to have_file_content('This is a different page') 65 | end 66 | 67 | it 'prunes screenshots on failure' do 68 | create_screenshot_for_pruning 69 | configure_prune_strategy :last_run 70 | run_failing_case <<-RUBY, 'test/integration' 71 | visit '/' 72 | assert(page.body.include?('This is the root page')) 73 | click_on "you'll never find me" 74 | RUBY 75 | assert_screenshot_pruned 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /spec/cucumber/cucumber_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "Using Capybara::Screenshot with Cucumber" do 4 | include CommonSetup 5 | 6 | let(:cmd) { 'cucumber' } 7 | 8 | def run_failing_case(failure_message, code) 9 | run_case code 10 | expect(last_command_started.output).to match(failure_message) 11 | end 12 | 13 | def run_case(code, options = {}) 14 | write_file('features/support/env.rb', <<-RUBY) 15 | #{ensure_load_paths_valid} 16 | require 'cucumber/support/env.rb' 17 | #{setup_test_app} 18 | RUBY 19 | 20 | write_file('features/step_definitions/step_definitions.rb', <<-RUBY) 21 | %w(lib spec).each do |include_folder| 22 | $LOAD_PATH.unshift(File.join('#{gem_root}', include_folder)) 23 | end 24 | require 'cucumber/step_definitions/step_definitions.rb' 25 | RUBY 26 | 27 | write_file('features/cucumber.feature', code) 28 | 29 | run_simple_with_retry cmd, false 30 | 31 | expect(last_command_started.output).to_not match(/failed|failure/i) if options[:assert_all_passed] 32 | end 33 | 34 | it 'saves a screenshot on failure' do 35 | run_failing_case %q{Unable to find link or button "you'll never find me"}, <<-CUCUMBER 36 | Feature: Failure 37 | Scenario: Failure 38 | Given I visit "/" 39 | And I click on a missing link 40 | CUCUMBER 41 | expect('tmp/my_screenshot.html').to have_file_content('This is the root page') 42 | end 43 | 44 | it 'saves a screenshot on an error' do 45 | run_failing_case %q{you can't handle me}, <<-CUCUMBER 46 | Feature: Failure 47 | Scenario: Failure 48 | Given I visit "/" 49 | And I trigger an unhandled exception 50 | CUCUMBER 51 | expect('tmp/my_screenshot.html').to have_file_content('This is the root page') 52 | end 53 | 54 | it 'saves a screenshot for the correct session for failures using_session' do 55 | run_failing_case(%q{Unable to find link or button "you'll never find me"}, <<-CUCUMBER) 56 | Feature: Failure 57 | Scenario: Failure in different session 58 | Given I visit "/" 59 | And I click on a missing link on a different page in a different session 60 | CUCUMBER 61 | expect('tmp/my_screenshot.html').to have_file_content('This is a different page') 62 | end 63 | 64 | context 'pruning' do 65 | before do 66 | create_screenshot_for_pruning 67 | configure_prune_strategy :last_run 68 | end 69 | 70 | it 'on failure it prunes previous screenshots when strategy is set' do 71 | run_failing_case %q{Unable to find link or button "you'll never find me"}, <<-CUCUMBER 72 | Feature: Prune 73 | Scenario: Screenshots are pruned if strategy is set 74 | Given I visit "/" 75 | And I click on a missing link 76 | CUCUMBER 77 | assert_screenshot_pruned 78 | end 79 | 80 | it 'on success it never prunes' do 81 | run_case <<-CUCUMBER, assert_all_passed: true 82 | Feature: Prune 83 | Scenario: Screenshots are pruned if strategy is set 84 | Given I visit "/" 85 | CUCUMBER 86 | assert_screenshot_not_pruned 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /spec/unit/pruner_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Capybara::Screenshot::Pruner do 4 | describe '#initialize' do 5 | let(:pruner) { Capybara::Screenshot::Pruner.new(strategy) } 6 | 7 | context 'accepts generic strategies:' do 8 | [:keep_all, :keep_last_run].each do |strategy_sym| 9 | let(:strategy) { strategy_sym } 10 | 11 | it ":#{strategy_sym}" do 12 | expect(pruner.strategy).to eq(strategy) 13 | end 14 | end 15 | end 16 | 17 | context 'keep:int' do 18 | let(:strategy) { { keep: 50 } } 19 | 20 | it 'is a suitable strategy' do 21 | expect(pruner.strategy).to eq(strategy) 22 | end 23 | end 24 | 25 | context 'invalid strategy' do 26 | context 'symbol' do 27 | let(:strategy) { :invalid_strategy } 28 | 29 | it 'raises an error' do 30 | expect { pruner }.to raise_error(/Invalid prune strategy/) 31 | end 32 | end 33 | 34 | context 'keep:sym' do 35 | let(:strategy) { { keep: :symbol } } 36 | 37 | it 'raises an error' do 38 | expect { pruner }.to raise_error(/must be a Integer/) 39 | end 40 | end 41 | end 42 | end 43 | 44 | describe '#prune_old_screenshots' do 45 | let(:capybara_root) { Capybara::Screenshot.capybara_root } 46 | let(:remaining_files) { Dir.glob(File.expand_path('*', capybara_root)).sort } 47 | let(:files_created) { [] } 48 | let(:files_count) { 8 } 49 | let(:pruner) { Capybara::Screenshot::Pruner.new(strategy) } 50 | 51 | before do 52 | allow(Capybara::Screenshot).to receive(:capybara_root).and_return(Dir.mktmpdir.to_s) 53 | 54 | files_count.times do |i| 55 | files_created << FileUtils.touch("#{capybara_root}/#{i}.#{i % 2 == 0 ? 'png' : 'html'}").first.tap do |file_name| 56 | File.utime(Time.now, Time.now - files_count + i, file_name) 57 | end 58 | end 59 | 60 | pruner.prune_old_screenshots 61 | end 62 | 63 | after do 64 | FileUtils.rm_rf capybara_root 65 | end 66 | 67 | context 'with :keep_all strategy' do 68 | let(:strategy) { :keep_all } 69 | 70 | it 'should not remove screens' do 71 | expect(remaining_files).to eq(files_created) 72 | end 73 | end 74 | 75 | context 'with :keep_last_run strategy' do 76 | let(:strategy) { :keep_last_run } 77 | 78 | it 'should remove all screens' do 79 | expect(remaining_files).to be_empty 80 | end 81 | 82 | context 'when dir is missing' do 83 | before { FileUtils.rm_rf(Capybara::Screenshot.capybara_root) } 84 | 85 | it 'should not raise error' do 86 | expect { pruner.prune_old_screenshots }.to_not raise_error 87 | end 88 | end 89 | end 90 | 91 | context 'with :keep strategy' do 92 | let(:keep_count) { 3 } 93 | let(:strategy) { { keep: keep_count } } 94 | 95 | it 'should keep specified number of screens' do 96 | expect(remaining_files).to eq(files_created.last(keep_count)) 97 | end 98 | 99 | context 'when dir is missing' do 100 | before { FileUtils.rm_rf(Capybara::Screenshot.capybara_root) } 101 | 102 | it 'should not raise error when dir is missing' do 103 | expect { pruner.prune_old_screenshots }.to_not raise_error 104 | end 105 | end 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /spec/unit/rspec_reporters/text_reporter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'capybara-screenshot/helpers' 3 | 4 | describe Capybara::Screenshot::RSpec::TextReporter do 5 | before do 6 | # Mocking `RSpec::Core::Formatters::ProgressFormatter`, but only implementing the methods that 7 | # are actually used in `TextReporter#dump_failure_info_with_screenshot`. 8 | @reporter_class = Class.new do 9 | attr_reader :output 10 | 11 | def initialize 12 | @output = StringIO.new 13 | end 14 | 15 | protected 16 | 17 | def long_padding 18 | " " 19 | end 20 | 21 | def failure_color(str) 22 | "colorized(#{str})" 23 | end 24 | 25 | private 26 | 27 | def dump_failure_info(example) 28 | output.puts "original failure info" 29 | end 30 | alias_method :example_failed, :dump_failure_info 31 | end 32 | 33 | @reporter = @reporter_class.new 34 | @reporter.singleton_class.send :include, described_class 35 | end 36 | 37 | let(:example_failed_method) do 38 | if ::RSpec::Core::Version::STRING.to_i <= 2 39 | :dump_failure_info 40 | else 41 | :example_failed 42 | end 43 | end 44 | 45 | def example_failed_method_argument_double(metadata = {}) 46 | example_group = Module.new.send(:include, Capybara::DSL) 47 | example = double("example", metadata: metadata, example_group: example_group) 48 | if ::RSpec::Core::Version::STRING.to_i <= 2 49 | example 50 | else 51 | double("notification").tap do |notification| 52 | allow(notification).to receive(:example).and_return(example) 53 | end 54 | end 55 | end 56 | 57 | context 'when there is no screenshot' do 58 | let(:example) { example_failed_method_argument_double } 59 | 60 | it 'doesnt change the original output of the reporter' do 61 | @reporter.send(example_failed_method, example) 62 | expect(@reporter.output.string).to eql("original failure info\n") 63 | end 64 | end 65 | 66 | context 'when a html file was saved' do 67 | let(:example) { example_failed_method_argument_double(screenshot: { html: "path/to/html" }) } 68 | 69 | it 'appends the html file path to the original output' do 70 | @reporter.send(example_failed_method, example) 71 | expect(@reporter.output.string).to eql("original failure info\n #{CapybaraScreenshot::Helpers.yellow("HTML screenshot: file://path/to/html")}\n") 72 | end 73 | end 74 | 75 | context 'when a html file and an image were saved' do 76 | let(:example) { example_failed_method_argument_double(screenshot: { html: "path/to/html", image: "path/to/image" }) } 77 | 78 | it 'appends the image path to the original output' do 79 | @reporter.send(example_failed_method, example) 80 | expect(@reporter.output.string).to eql("original failure info\n #{CapybaraScreenshot::Helpers.yellow("HTML screenshot: file://path/to/html")}\n #{CapybaraScreenshot::Helpers.yellow("Image screenshot: file://path/to/image")}\n") 81 | end 82 | end 83 | 84 | 85 | it 'works with older RSpec formatters where `#red` is used instead of `#failure_color`' do 86 | old_reporter_class = Class.new(@reporter_class) do 87 | undef_method :failure_color 88 | def red(str) 89 | "red(#{str})" 90 | end 91 | end 92 | old_reporter = old_reporter_class.new 93 | old_reporter.singleton_class.send :include, described_class 94 | example = example_failed_method_argument_double(screenshot: { html: "path/to/html" }) 95 | old_reporter.send(example_failed_method, example) 96 | expect(old_reporter.output.string).to eql("original failure info\n #{CapybaraScreenshot::Helpers.yellow("HTML screenshot: file://path/to/html")}\n") 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /spec/unit/capybara-screenshot_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Capybara::Screenshot do 4 | describe '.register_driver' do 5 | before(:all) do 6 | @original_drivers = Capybara::Screenshot.registered_drivers.dup 7 | end 8 | 9 | after(:all) do 10 | Capybara::Screenshot.registered_drivers = @original_drivers 11 | end 12 | 13 | it 'stores driver with block' do 14 | block = lambda {} 15 | Capybara::Screenshot.register_driver :foo, &block 16 | 17 | expect(Capybara::Screenshot.registered_drivers[:foo]).to eql(block) 18 | end 19 | end 20 | 21 | describe '.register_filename_prefix_formatter' do 22 | before(:all) do 23 | @original_formatters = Capybara::Screenshot.filename_prefix_formatters.dup 24 | end 25 | 26 | after(:all) do 27 | Capybara::Screenshot.filename_prefix_formatters = @original_formatters 28 | end 29 | 30 | it 'stores test type with block' do 31 | block = lambda { |arg| } 32 | Capybara::Screenshot.register_filename_prefix_formatter :foo, &block 33 | 34 | expect(Capybara::Screenshot.filename_prefix_formatters[:foo]).to eql(block) 35 | end 36 | 37 | describe '.filename_prefix_for' do 38 | it 'returns "configured formatter" for specified formatter' do 39 | Capybara::Screenshot.register_filename_prefix_formatter(:foo) { |arg| 'custom_path' } 40 | expect(Capybara::Screenshot.filename_prefix_for(:foo, double('test'))).to eql('custom_path') 41 | end 42 | end 43 | end 44 | 45 | describe '.filename_prefix_for' do 46 | it 'returns "screenshot" for undefined formatter' do 47 | expect(Capybara::Screenshot.filename_prefix_for(:foo, double('test'))).to eql('screenshot') 48 | end 49 | end 50 | 51 | describe '.append_screenshot_path' do 52 | it 'prints a deprecation message and delegates to RSpec.add_link_to_screenshot_for_failed_examples' do 53 | begin 54 | original_stderr = $stderr 55 | $stderr = StringIO.new 56 | expect { 57 | Capybara::Screenshot.append_screenshot_path = false 58 | }.to change { 59 | Capybara::Screenshot::RSpec.add_link_to_screenshot_for_failed_examples 60 | }.from(true).to(false) 61 | expect($stderr.string).to include("append_screenshot_path is deprecated") 62 | ensure 63 | $stderr = original_stderr 64 | end 65 | end 66 | end 67 | 68 | describe '.new_saver' do 69 | it 'passes through to get a new Saver if the user has not configured s3' do 70 | saver_double = double('saver') 71 | args = double('args') 72 | expect(Capybara::Screenshot::Saver).to receive(:new).with(args).and_return(saver_double) 73 | 74 | expect(Capybara::Screenshot.new_saver(args)).to eq(saver_double) 75 | end 76 | 77 | it 'wraps the returned saver in an S3 saver if it has been configured' do 78 | require 'capybara-screenshot/s3_saver' 79 | 80 | saver_double = double('saver') 81 | args = double('args') 82 | s3_saver_double = double('s3_saver') 83 | s3_configuration = { hello: 'world' } 84 | s3_object_configuration = {} 85 | 86 | Capybara::Screenshot.s3_configuration = s3_configuration 87 | 88 | expect(Capybara::Screenshot::Saver).to receive(:new).with(args).and_return(saver_double) 89 | expect(Capybara::Screenshot::S3Saver).to receive(:new_with_configuration).with(saver_double, s3_configuration, s3_object_configuration).and_return(s3_saver_double) 90 | 91 | expect(Capybara::Screenshot.new_saver(args)).to eq(s3_saver_double) 92 | end 93 | end 94 | 95 | describe '#prune' do 96 | before do 97 | Capybara::Screenshot.reset_prune_history 98 | end 99 | 100 | it 'prunes once by default' do 101 | expect(Capybara::Screenshot::Pruner).to receive(:new).and_call_original.once 102 | 3.times { Capybara::Screenshot.prune } 103 | end 104 | 105 | it 'prunes every time if option force: true' do 106 | expect(Capybara::Screenshot::Pruner).to receive(:new).and_call_original.exactly(3).times 107 | 3.times { Capybara::Screenshot.prune(force: true) } 108 | end 109 | 110 | context 'prune strategy' do 111 | let(:prune_strategy) { { keep: 100 } } 112 | before do 113 | Capybara::Screenshot.prune_strategy = prune_strategy 114 | end 115 | 116 | it 'is passed to initializer' do 117 | expect(Capybara::Screenshot::Pruner).to receive(:new).with(prune_strategy).and_call_original 118 | Capybara::Screenshot.prune 119 | end 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /lib/capybara-screenshot/rspec.rb: -------------------------------------------------------------------------------- 1 | require "capybara-screenshot" 2 | 3 | require "capybara-screenshot/rspec/text_reporter" 4 | require "capybara-screenshot/rspec/html_link_reporter" 5 | require "capybara-screenshot/rspec/html_embed_reporter" 6 | require "capybara-screenshot/rspec/json_reporter" 7 | require "capybara-screenshot/rspec/textmate_link_reporter" 8 | 9 | module Capybara 10 | module Screenshot 11 | module RSpec 12 | 13 | # Reporters extend RSpec formatters to display information about screenshots for failed 14 | # examples. 15 | # 16 | # Technically, a reporter is a module that gets injected into a RSpec formatter class. 17 | # It uses method aliasing to extend some (usually just one) of the formatter's methods. 18 | # 19 | # Implementing a custom reporter is as simple as creating a module and setting up the 20 | # appropriate aliases. Use `BaseReporter.enhance_with_screenshot` if you don't want 21 | # to set up the aliases manually: 22 | # 23 | # module MyReporter 24 | # extend Capybara::Screenshot::RSpec::BaseReporter 25 | # 26 | # # Will replace the formatter's original `dump_failure_info` method with 27 | # # `dump_failure_info_with_screenshot` from this module: 28 | # enhance_with_screenshot :dump_failure_info 29 | # 30 | # def dump_failure_info_with_screenshot(example) 31 | # dump_failure_info_without_screenshot(example) # call original implementation 32 | # ... # your additions here 33 | # end 34 | # end 35 | # 36 | # Finally customize `Capybara::Screenshot::RSpec::FORMATTERS` to make sure your reporter 37 | # gets injected into the appropriate formatter. 38 | 39 | REPORTERS = { 40 | "RSpec::Core::Formatters::ProgressFormatter" => Capybara::Screenshot::RSpec::TextReporter, 41 | "RSpec::Core::Formatters::DocumentationFormatter" => Capybara::Screenshot::RSpec::TextReporter, 42 | "RSpec::Core::Formatters::HtmlFormatter" => Capybara::Screenshot::RSpec::HtmlLinkReporter, 43 | "RSpec::Core::Formatters::JsonFormatter" => Capybara::Screenshot::RSpec::JsonReporter, 44 | "RSpec::Core::Formatters::TextMateFormatter" => Capybara::Screenshot::RSpec::TextMateLinkReporter, # RSpec 2 45 | "RSpec::Mate::Formatters::TextMateFormatter" => Capybara::Screenshot::RSpec::TextMateLinkReporter, # RSpec 3 46 | "Fuubar" => Capybara::Screenshot::RSpec::TextReporter, 47 | "Spec::Runner::Formatter::TeamcityFormatter" => Capybara::Screenshot::RSpec::TextReporter 48 | } 49 | 50 | class << self 51 | attr_accessor :add_link_to_screenshot_for_failed_examples 52 | 53 | def after_failed_example(example) 54 | if example.example_group.include?(Capybara::DSL) # Capybara DSL method has been included for a feature we can snapshot 55 | Capybara.using_session(Capybara::Screenshot.final_session_name) do 56 | if Capybara::Screenshot.autosave_on_failure && example.exception && Capybara.page.current_url != '' 57 | filename_prefix = Capybara::Screenshot.filename_prefix_for(:rspec, example) 58 | 59 | saver = Capybara::Screenshot.new_saver(Capybara, Capybara.page, true, filename_prefix) 60 | saver.save 61 | 62 | example.metadata[:screenshot] = {} 63 | example.metadata[:screenshot][:html] = saver.html_path if saver.html_saved? 64 | example.metadata[:screenshot][:image] = saver.screenshot_path if saver.screenshot_saved? 65 | end 66 | end 67 | end 68 | end 69 | end 70 | 71 | self.add_link_to_screenshot_for_failed_examples = true 72 | end 73 | end 74 | end 75 | 76 | RSpec.configure do |config| 77 | config.before do 78 | Capybara::Screenshot.final_session_name = nil 79 | end 80 | 81 | config.after do |example_from_block_arg| 82 | # RSpec 3 no longer defines `example`, but passes the example as block argument instead 83 | example = config.respond_to?(:expose_current_running_example_as) ? example_from_block_arg : self.example 84 | 85 | Capybara::Screenshot::RSpec.after_failed_example(example) 86 | end 87 | 88 | config.before(:suite) do 89 | if Capybara::Screenshot::RSpec.add_link_to_screenshot_for_failed_examples 90 | RSpec.configuration.formatters.each do |formatter| 91 | next unless (reporter_module = Capybara::Screenshot::RSpec::REPORTERS[formatter.class.to_s]) 92 | next if formatter.singleton_class.included_modules.include?(reporter_module) 93 | formatter.singleton_class.send :include, reporter_module 94 | end 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/capybara-screenshot/saver.rb: -------------------------------------------------------------------------------- 1 | require 'capybara-screenshot/helpers' 2 | require 'capybara-screenshot/callbacks' 3 | 4 | module Capybara 5 | module Screenshot 6 | class Saver 7 | include Capybara::Screenshot::Callbacks 8 | 9 | define_callback :after_save_html 10 | define_callback :after_save_screenshot 11 | 12 | attr_reader :capybara, :page, :file_base_name 13 | 14 | def initialize(capybara, page, html_save=true, filename_prefix='screenshot') 15 | @capybara, @page, @html_save = capybara, page, html_save 16 | time_now = Time.now 17 | timestamp = "#{time_now.strftime('%Y-%m-%d-%H-%M-%S.')}#{'%03d' % (time_now.usec/1000).to_i}" 18 | 19 | filename = [filename_prefix] 20 | filename << timestamp if Capybara::Screenshot.append_timestamp 21 | filename << SecureRandom.hex if Capybara::Screenshot.append_random 22 | 23 | @file_base_name = filename.join('_') 24 | 25 | Capybara::Screenshot.prune 26 | end 27 | 28 | def save 29 | current_path do |path| 30 | if !path.empty? 31 | begin 32 | save_html if @html_save 33 | rescue StandardError => e 34 | warn "WARN: HTML source could not be saved. An exception is raised: #{e.inspect}." 35 | end 36 | 37 | begin 38 | save_screenshot 39 | rescue StandardError => e 40 | warn "WARN: Screenshot could not be saved. An exception is raised: #{e.inspect}." 41 | end 42 | else 43 | warn 'WARN: Screenshot could not be saved. `page.current_path` is empty.' 44 | end 45 | end 46 | end 47 | 48 | def save_html 49 | path = html_path 50 | clear_save_path do 51 | if Capybara::VERSION.match(/^\d+/)[0] == '1' 52 | capybara.save_page(page.body, "#{path}") 53 | else 54 | capybara.save_page("#{path}") 55 | end 56 | end 57 | @html_saved = true 58 | run_callbacks :after_save_html, html_path if html_saved? 59 | end 60 | 61 | def save_screenshot 62 | path = screenshot_path 63 | clear_save_path do 64 | result = Capybara::Screenshot.registered_drivers.fetch(capybara.current_driver) { |driver_name| 65 | warn "capybara-screenshot could not detect a screenshot driver for '#{capybara.current_driver}'. Saving with default with unknown results." 66 | Capybara::Screenshot.registered_drivers[:default] 67 | }.call(page.driver, path) 68 | @screenshot_saved = result != :not_supported 69 | end 70 | run_callbacks :after_save_screenshot, screenshot_path if screenshot_saved? 71 | end 72 | 73 | def html_path 74 | File.join(Capybara::Screenshot.capybara_root, "#{file_base_name}.html") 75 | end 76 | 77 | def screenshot_path 78 | File.join(Capybara::Screenshot.capybara_root, "#{file_base_name}.png") 79 | end 80 | 81 | def html_saved? 82 | @html_saved 83 | end 84 | 85 | def screenshot_saved? 86 | @screenshot_saved 87 | end 88 | 89 | # If Capybara::Screenshot.capybara_tmp_path is set then 90 | # the html_path or screenshot_path can be appended to this path in 91 | # some versions of Capybara instead of using it as an absolute path 92 | def clear_save_path 93 | old_path = Capybara::Screenshot.capybara_tmp_path 94 | Capybara::Screenshot.capybara_tmp_path = nil 95 | yield 96 | ensure 97 | Capybara::Screenshot.capybara_tmp_path = old_path 98 | end 99 | 100 | def output_screenshot_path 101 | output "HTML screenshot: #{html_path}" if html_saved? 102 | output "Image screenshot: #{screenshot_path}" if screenshot_saved? 103 | end 104 | 105 | # Print image to screen, if imgcat is available 106 | def display_image 107 | system("#{imgcat} #{screenshot_path}") unless imgcat.nil? 108 | end 109 | 110 | private 111 | 112 | def current_path 113 | # the current_path may raise error in selenium 114 | begin 115 | path = page.current_path.to_s 116 | rescue StandardError => e 117 | warn "WARN: Screenshot could not be saved. `page.current_path` raised exception: #{e.inspect}." 118 | end 119 | yield path if path 120 | end 121 | 122 | def output(message) 123 | puts " #{CapybaraScreenshot::Helpers.yellow(message)}" 124 | end 125 | 126 | def imgcat 127 | @imgcat ||= which('imgcat') 128 | end 129 | 130 | # Cross-platform way of finding an executable in the $PATH. 131 | # 132 | # which('ruby') #=> /usr/bin/ruby 133 | def which(cmd) 134 | exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : [''] 135 | ENV['PATH'].split(File::PATH_SEPARATOR).each do |path| 136 | exts.each { |ext| 137 | exe = File.join(path, "#{cmd}#{ext}") 138 | return exe if File.executable?(exe) && !File.directory?(exe) 139 | } 140 | end 141 | return nil 142 | end 143 | end 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /spec/rspec/rspec_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Capybara::Screenshot::RSpec, :type => :aruba do 4 | describe "used with RSpec" do 5 | include CommonSetup 6 | 7 | before do 8 | Capybara::Screenshot.capybara_tmp_path = expand_path('tmp') 9 | end 10 | 11 | def run_failing_case(code, error_message, format=nil) 12 | run_case code, format: format 13 | if error_message.kind_of?(Regexp) 14 | expect(last_command_started.output).to match(error_message) 15 | else 16 | expect(last_command_started.output).to include(error_message) 17 | end 18 | end 19 | 20 | def run_case(code, options = {}) 21 | write_file('spec/test_failure.rb', <<-RUBY) 22 | #{ensure_load_paths_valid} 23 | require 'rspec' 24 | require 'capybara' 25 | require 'capybara/rspec' 26 | require 'capybara-screenshot' 27 | require 'capybara-screenshot/rspec' 28 | 29 | #{setup_test_app} 30 | #{code} 31 | RUBY 32 | 33 | cmd = cmd_with_format(options[:format]) 34 | run_simple_with_retry cmd, false 35 | 36 | expect(last_command_started.output).to match('0 failures') if options[:assert_all_passed] 37 | end 38 | 39 | def cmd_with_format(format) 40 | "rspec #{"--format #{format} " if format}#{expand_path('spec/test_failure.rb')}" 41 | end 42 | 43 | it 'saves a screenshot on failure' do 44 | run_failing_case <<-RUBY, %q{Unable to find link or button "you'll never find me"} 45 | feature 'screenshot with failure' do 46 | scenario 'click on a missing link' do 47 | visit '/' 48 | expect(page.body).to include('This is the root page') 49 | click_on "you'll never find me" 50 | end 51 | end 52 | RUBY 53 | expect(expand_path('tmp/screenshot.html')).to_not have_file_content('This is the root page') 54 | end 55 | 56 | formatters = { 57 | progress: 'HTML screenshot:', 58 | documentation: 'HTML screenshot:', 59 | html: %r{]*>HTML page}, 60 | json: '"screenshot":{"' 61 | } 62 | 63 | # Textmate formatter is only included in RSpec 2 64 | if RSpec::Core::Version::STRING.to_i == 2 65 | formatters[:textmate] = %r{TextMate\.system\(.*open file://\./tmp/screenshot.html} 66 | end 67 | 68 | formatters.each do |formatter, error_message| 69 | it "uses the associated #{formatter} formatter" do 70 | run_failing_case <<-RUBY, error_message, formatter 71 | feature 'screenshot with failure' do 72 | scenario 'click on a missing link' do 73 | visit '/' 74 | click_on "you'll never find me" 75 | end 76 | end 77 | RUBY 78 | expect('tmp/screenshot.html').to have_file_content('This is the root page') 79 | end 80 | end 81 | 82 | it "does not save a screenshot for tests that don't use Capybara" do 83 | run_failing_case <<-RUBY, %q{expected: false} 84 | describe 'failing test' do 85 | it 'fails intentionally' do 86 | expect(true).to eql(false) 87 | end 88 | end 89 | RUBY 90 | expect('tmp/screenshot.html').to_not be_an_existing_file 91 | end 92 | 93 | it 'saves a screenshot for the correct session for failures using_session' do 94 | run_failing_case <<-RUBY, %q{Unable to find link or button "you'll never find me"} 95 | feature 'screenshot with failure' do 96 | scenario 'click on a missing link' do 97 | visit '/' 98 | expect(page.body).to include('This is the root page') 99 | using_session :different_session do 100 | visit '/different_page' 101 | expect(page.body).to include('This is a different page') 102 | click_on "you'll never find me" 103 | end 104 | end 105 | end 106 | RUBY 107 | expect('tmp/screenshot.html').to have_file_content(/is/) 108 | end 109 | 110 | context 'pruning' do 111 | before do 112 | create_screenshot_for_pruning 113 | configure_prune_strategy :last_run 114 | end 115 | 116 | it 'on failure it prunes previous screenshots when strategy is set' do 117 | run_failing_case <<-RUBY, 'HTML screenshot:', :progress 118 | feature 'screenshot with failure' do 119 | scenario 'click on a missing link' do 120 | visit '/' 121 | click_on "you'll never find me" 122 | end 123 | end 124 | RUBY 125 | assert_screenshot_pruned 126 | end 127 | 128 | it 'on success it never prunes' do 129 | run_case <<-CUCUMBER, assert_all_passed: true 130 | feature 'screenshot without failure' do 131 | scenario 'click on a link' do 132 | visit '/' 133 | end 134 | end 135 | CUCUMBER 136 | assert_screenshot_not_pruned 137 | end 138 | end 139 | 140 | context 'no pruning by default' do 141 | before do 142 | create_screenshot_for_pruning 143 | end 144 | 145 | it 'on failure it leaves existing screenshots' do 146 | run_failing_case <<-RUBY, 'HTML screenshot:', :progress 147 | feature 'screenshot with failure' do 148 | scenario 'click on a missing link' do 149 | visit '/' 150 | click_on "you'll never find me" 151 | end 152 | end 153 | RUBY 154 | assert_screenshot_not_pruned 155 | end 156 | end 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /lib/capybara-screenshot.rb: -------------------------------------------------------------------------------- 1 | module Capybara 2 | module Screenshot 3 | class << self 4 | attr_accessor :autosave_on_failure 5 | attr_accessor :registered_drivers 6 | attr_accessor :filename_prefix_formatters 7 | attr_accessor :append_timestamp 8 | attr_accessor :append_random 9 | attr_accessor :webkit_options 10 | attr_writer :final_session_name 11 | attr_accessor :prune_strategy 12 | attr_accessor :s3_configuration 13 | attr_accessor :s3_object_configuration 14 | end 15 | 16 | self.autosave_on_failure = true 17 | self.registered_drivers = {} 18 | self.filename_prefix_formatters = {} 19 | self.append_timestamp = true 20 | self.append_random = false 21 | self.webkit_options = {} 22 | self.prune_strategy = :keep_all 23 | self.s3_configuration = {} 24 | self.s3_object_configuration = {} 25 | 26 | def self.append_screenshot_path=(value) 27 | $stderr.puts "WARNING: Capybara::Screenshot.append_screenshot_path is deprecated. " + 28 | "Please use Capybara::Screenshot::RSpec.add_link_to_screenshot_for_failed_examples instead." 29 | RSpec.add_link_to_screenshot_for_failed_examples = value 30 | end 31 | 32 | def self.screenshot_and_save_page 33 | saver = new_saver(Capybara, Capybara.page) 34 | if saver.save 35 | {:html => saver.html_path, :image => saver.screenshot_path} 36 | end 37 | end 38 | 39 | def self.screenshot_and_open_image 40 | require "launchy" 41 | 42 | saver = new_saver(Capybara, Capybara.page, false) 43 | if saver.save 44 | Launchy.open saver.screenshot_path 45 | {:html => nil, :image => saver.screenshot_path} 46 | end 47 | end 48 | 49 | class << self 50 | alias screen_shot_and_save_page screenshot_and_save_page 51 | alias screen_shot_and_open_image screenshot_and_open_image 52 | end 53 | 54 | def self.filename_prefix_for(test_type, test) 55 | filename_prefix_formatters.fetch(test_type) { |key| 56 | filename_prefix_formatters[:default] 57 | }.call(test) 58 | end 59 | 60 | def self.capybara_root 61 | @capybara_root ||= if defined?(::Rails) && ::Rails.root.present? 62 | ::Rails.root.join capybara_tmp_path 63 | elsif defined?(Padrino) 64 | File.expand_path(capybara_tmp_path, Padrino.root) 65 | elsif defined?(Sinatra) 66 | File.join(Sinatra::Application.root, capybara_tmp_path) 67 | else 68 | capybara_tmp_path 69 | end.to_s 70 | end 71 | 72 | def self.register_driver(driver, &block) 73 | self.registered_drivers[driver] = block 74 | end 75 | 76 | def self.register_filename_prefix_formatter(test_type, &block) 77 | self.filename_prefix_formatters[test_type] = block 78 | end 79 | 80 | def self.final_session_name 81 | @final_session_name || Capybara.session_name || :default 82 | end 83 | 84 | # Prune screenshots based on prune_strategy 85 | # Will run only once unless force:true 86 | def self.prune(options = {}) 87 | reset_prune_history if options[:force] 88 | Capybara::Screenshot::Pruner.new(Capybara::Screenshot.prune_strategy).prune_old_screenshots unless @pruned_previous_screenshots 89 | @pruned_previous_screenshots = true 90 | end 91 | 92 | # Reset prune history allowing further prunining on next failure 93 | def self.reset_prune_history 94 | @pruned_previous_screenshots = nil 95 | end 96 | 97 | def self.new_saver(*args) 98 | saver = Saver.new(*args) 99 | 100 | unless s3_configuration.empty? 101 | require 'capybara-screenshot/s3_saver' 102 | saver = S3Saver.new_with_configuration(saver, s3_configuration, s3_object_configuration) 103 | end 104 | 105 | return saver 106 | end 107 | 108 | def self.after_save_html &block 109 | Saver.after_save_html &block 110 | end 111 | 112 | def self.after_save_screenshot &block 113 | Saver.after_save_screenshot &block 114 | end 115 | 116 | private 117 | 118 | # If the path isn't set, default to the current directory 119 | def self.capybara_tmp_path 120 | # `#save_and_open_page_path` is deprecated 121 | # https://github.com/jnicklas/capybara/blob/48ab1ede946dec2250a2d1d8cbb3313f25096456/History.md#L37 122 | if Capybara.respond_to?(:save_path) 123 | Capybara.save_path 124 | else 125 | Capybara.save_and_open_page_path 126 | end || '.' 127 | end 128 | 129 | # Configure the path unless '.' 130 | def self.capybara_tmp_path=(path) 131 | return if path == '.' 132 | 133 | # `#save_and_open_page_path` is deprecated 134 | # https://github.com/jnicklas/capybara/blob/48ab1ede946dec2250a2d1d8cbb3313f25096456/History.md#L37 135 | if Capybara.respond_to?(:save_path) 136 | Capybara.save_path = path 137 | else 138 | Capybara.save_and_open_page_path = path 139 | end 140 | end 141 | end 142 | end 143 | 144 | # Register driver renderers. 145 | # The block should return `:not_supported` if a screenshot could not be saved. 146 | Capybara::Screenshot.class_eval do 147 | register_driver(:default) do |driver, path| 148 | driver.render(path) 149 | end 150 | 151 | register_driver(:rack_test) do |driver, path| 152 | :not_supported 153 | end 154 | 155 | register_driver(:mechanize) do |driver, path| 156 | :not_supported 157 | end 158 | 159 | register_driver(:selenium) do |driver, path| 160 | driver.browser.save_screenshot(path) 161 | end 162 | 163 | register_driver(:poltergeist) do |driver, path| 164 | driver.render(path, :full => true) 165 | end 166 | 167 | register_driver(:poltergeist_billy) do |driver, path| 168 | driver.render(path, :full => true) 169 | end 170 | 171 | webkit_block = proc do |driver, path| 172 | if driver.respond_to?(:save_screenshot) 173 | driver.save_screenshot(path, webkit_options) 174 | else 175 | driver.render(path) 176 | end 177 | end 178 | 179 | register_driver :webkit, &webkit_block 180 | register_driver :webkit_debug, &webkit_block 181 | 182 | register_driver(:terminus) do |driver, path| 183 | if driver.respond_to?(:save_screenshot) 184 | driver.save_screenshot(path) 185 | else 186 | :not_supported 187 | end 188 | end 189 | end 190 | 191 | # Register filename prefix formatters 192 | Capybara::Screenshot.class_eval do 193 | register_filename_prefix_formatter(:default) do |test| 194 | 'screenshot' 195 | end 196 | end 197 | 198 | require 'capybara/dsl' 199 | require 'capybara/util/save_and_open_page' if Capybara::VERSION.match(/^\d+/)[0] == '1' # no longer needed in Capybara version 2 200 | 201 | require 'capybara-screenshot/saver' 202 | require 'capybara-screenshot/capybara' 203 | require 'capybara-screenshot/pruner' 204 | -------------------------------------------------------------------------------- /spec/unit/s3_saver_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'capybara-screenshot/s3_saver' 3 | 4 | describe Capybara::Screenshot::S3Saver do 5 | let(:saver) { double('saver') } 6 | let(:bucket_name) { double('bucket_name') } 7 | let(:s3_object_configuration) { {} } 8 | let(:s3_client) { double('s3_client') } 9 | let(:key_prefix){ "some/path/" } 10 | 11 | let(:s3_saver) { described_class.new(saver, s3_client, bucket_name, s3_object_configuration) } 12 | let(:s3_saver_with_key_prefix) { described_class.new(saver, s3_client, bucket_name, s3_object_configuration, key_prefix: key_prefix) } 13 | 14 | describe '.new_with_configuration' do 15 | let(:access_key_id) { double('access_key_id') } 16 | let(:secret_access_key) { double('secret_access_key') } 17 | let(:s3_client_credentials_using_defaults) { 18 | { 19 | access_key_id: access_key_id, 20 | secret_access_key: secret_access_key 21 | } 22 | } 23 | 24 | let(:region) { double('region') } 25 | let(:s3_client_credentials) { 26 | s3_client_credentials_using_defaults.merge(region: region) 27 | } 28 | 29 | before do 30 | allow(Aws::S3::Client).to receive(:new).and_return(s3_client) 31 | allow(described_class).to receive(:new) 32 | end 33 | 34 | it 'destructures the configuration into its components' do 35 | described_class.new_with_configuration(saver, { 36 | s3_client_credentials: s3_client_credentials, 37 | bucket_name: bucket_name 38 | }, s3_object_configuration) 39 | 40 | expect(Aws::S3::Client).to have_received(:new).with(s3_client_credentials) 41 | expect(described_class).to have_received(:new).with(saver, s3_client, bucket_name, s3_object_configuration, hash_including({})) 42 | end 43 | 44 | it 'passes key_prefix option if specified' do 45 | described_class.new_with_configuration(saver, { 46 | s3_client_credentials: s3_client_credentials, 47 | bucket_name: bucket_name, 48 | key_prefix: key_prefix, 49 | }, s3_object_configuration) 50 | 51 | expect(Aws::S3::Client).to have_received(:new).with(s3_client_credentials) 52 | expect(described_class).to have_received(:new).with(saver, s3_client, bucket_name, s3_object_configuration, hash_including(key_prefix: key_prefix)) 53 | end 54 | 55 | it 'defaults the region to us-east-1' do 56 | default_region = 'us-east-1' 57 | 58 | described_class.new_with_configuration(saver, { 59 | s3_client_credentials: s3_client_credentials_using_defaults, 60 | bucket_name: bucket_name 61 | }, s3_object_configuration) 62 | 63 | expect(Aws::S3::Client).to have_received(:new).with( 64 | s3_client_credentials.merge(region: default_region) 65 | ) 66 | 67 | expect(described_class).to have_received(:new).with(saver, s3_client, bucket_name, s3_object_configuration, hash_including({})) 68 | end 69 | 70 | it 'stores the object configuration when passed' do 71 | s3_object_configuration = { acl: 'public-read' } 72 | Capybara::Screenshot.s3_object_configuration = { acl: 'public-read' } 73 | 74 | described_class.new_with_configuration(saver, { 75 | s3_client_credentials: s3_client_credentials, 76 | bucket_name: bucket_name 77 | }, s3_object_configuration) 78 | 79 | expect(Aws::S3::Client).to have_received(:new).with(s3_client_credentials) 80 | expect(described_class).to have_received(:new).with(saver, s3_client, bucket_name, s3_object_configuration, hash_including({})) 81 | end 82 | 83 | it 'passes key_prefix option if specified' do 84 | described_class.new_with_configuration(saver, { 85 | s3_client_credentials: s3_client_credentials, 86 | bucket_name: bucket_name, 87 | key_prefix: key_prefix, 88 | }, s3_object_configuration) 89 | 90 | expect(Aws::S3::Client).to have_received(:new).with(s3_client_credentials) 91 | expect(described_class).to have_received(:new).with(saver, s3_client, bucket_name, s3_object_configuration, hash_including(key_prefix: key_prefix)) 92 | end 93 | end 94 | 95 | describe '#save' do 96 | before do 97 | allow(saver).to receive(:html_saved?).and_return(false) 98 | allow(saver).to receive(:screenshot_saved?).and_return(false) 99 | allow(saver).to receive(:save) 100 | end 101 | 102 | it 'calls save on the underlying saver' do 103 | expect(saver).to receive(:save) 104 | 105 | s3_saver.save 106 | end 107 | 108 | it 'uploads the html' do 109 | html_path = '/foo/bar.html' 110 | expect(saver).to receive(:html_path).and_return(html_path) 111 | expect(saver).to receive(:html_saved?).and_return(true) 112 | 113 | html_file = double('html_file') 114 | 115 | expect(File).to receive(:open).with(html_path).and_yield(html_file) 116 | 117 | expect(s3_client).to receive(:put_object).with( 118 | bucket: bucket_name, 119 | key: 'bar.html', 120 | body: html_file 121 | ) 122 | 123 | s3_saver.save 124 | end 125 | 126 | it 'uploads the screenshot' do 127 | screenshot_path = '/baz/bim.jpg' 128 | expect(saver).to receive(:screenshot_path).and_return(screenshot_path) 129 | expect(saver).to receive(:screenshot_saved?).and_return(true) 130 | 131 | screenshot_file = double('screenshot_file') 132 | 133 | expect(File).to receive(:open).with(screenshot_path).and_yield(screenshot_file) 134 | 135 | expect(s3_client).to receive(:put_object).with( 136 | bucket: bucket_name, 137 | key: 'bim.jpg', 138 | body: screenshot_file 139 | ) 140 | 141 | s3_saver.save 142 | end 143 | 144 | context 'with object configuration' do 145 | let(:s3_object_configuration) { { acl: 'public-read' } } 146 | let(:s3_saver) { described_class.new(saver, s3_client, bucket_name, s3_object_configuration) } 147 | 148 | it 'uploads the html' do 149 | html_path = '/foo/bar.html' 150 | expect(saver).to receive(:html_path).and_return(html_path) 151 | expect(saver).to receive(:html_saved?).and_return(true) 152 | 153 | html_file = double('html_file') 154 | 155 | expect(File).to receive(:open).with(html_path).and_yield(html_file) 156 | 157 | expect(s3_client).to receive(:put_object).with( 158 | bucket: bucket_name, 159 | key: 'bar.html', 160 | body: html_file, 161 | acl: 'public-read' 162 | ) 163 | 164 | s3_saver.save 165 | end 166 | 167 | it 'uploads the screenshot' do 168 | screenshot_path = '/baz/bim.jpg' 169 | expect(saver).to receive(:screenshot_path).and_return(screenshot_path) 170 | expect(saver).to receive(:screenshot_saved?).and_return(true) 171 | 172 | screenshot_file = double('screenshot_file') 173 | 174 | expect(File).to receive(:open).with(screenshot_path).and_yield(screenshot_file) 175 | 176 | expect(s3_client).to receive(:put_object).with( 177 | bucket: bucket_name, 178 | key: 'bim.jpg', 179 | body: screenshot_file, 180 | acl: 'public-read' 181 | ) 182 | 183 | s3_saver.save 184 | end 185 | end 186 | 187 | context 'with key_prefix specified' do 188 | it 'uploads the html with key prefix' do 189 | html_path = '/foo/bar.html' 190 | expect(saver).to receive(:html_path).and_return(html_path) 191 | expect(saver).to receive(:html_saved?).and_return(true) 192 | 193 | html_file = double('html_file') 194 | 195 | expect(File).to receive(:open).with(html_path).and_yield(html_file) 196 | 197 | expect(s3_client).to receive(:put_object).with( 198 | bucket: bucket_name, 199 | key: 'some/path/bar.html', 200 | body: html_file 201 | ) 202 | 203 | s3_saver_with_key_prefix.save 204 | end 205 | 206 | it 'uploads the screenshot with key prefix' do 207 | screenshot_path = '/baz/bim.jpg' 208 | expect(saver).to receive(:screenshot_path).and_return(screenshot_path) 209 | expect(saver).to receive(:screenshot_saved?).and_return(true) 210 | 211 | screenshot_file = double('screenshot_file') 212 | 213 | expect(File).to receive(:open).with(screenshot_path).and_yield(screenshot_file) 214 | 215 | expect(s3_client).to receive(:put_object).with( 216 | bucket: bucket_name, 217 | key: 'some/path/bim.jpg', 218 | body: screenshot_file 219 | ) 220 | 221 | s3_saver_with_key_prefix.save 222 | end 223 | 224 | context 'with object configuration' do 225 | let(:s3_object_configuration) { { acl: 'public-read' } } 226 | 227 | it 'uploads the html' do 228 | html_path = '/foo/bar.html' 229 | expect(saver).to receive(:html_path).and_return(html_path) 230 | expect(saver).to receive(:html_saved?).and_return(true) 231 | 232 | html_file = double('html_file') 233 | 234 | expect(File).to receive(:open).with(html_path).and_yield(html_file) 235 | 236 | expect(s3_client).to receive(:put_object).with( 237 | bucket: bucket_name, 238 | key: 'some/path/bar.html', 239 | body: html_file, 240 | acl: 'public-read' 241 | ) 242 | 243 | s3_saver_with_key_prefix.save 244 | end 245 | 246 | it 'uploads the screenshot' do 247 | screenshot_path = '/baz/bim.jpg' 248 | expect(saver).to receive(:screenshot_path).and_return(screenshot_path) 249 | expect(saver).to receive(:screenshot_saved?).and_return(true) 250 | 251 | screenshot_file = double('screenshot_file') 252 | 253 | expect(File).to receive(:open).with(screenshot_path).and_yield(screenshot_file) 254 | 255 | expect(s3_client).to receive(:put_object).with( 256 | bucket: bucket_name, 257 | key: 'some/path/bim.jpg', 258 | body: screenshot_file, 259 | acl: 'public-read' 260 | ) 261 | 262 | s3_saver_with_key_prefix.save 263 | end 264 | end 265 | end 266 | end 267 | 268 | # Needed because we cannot depend on Verifying Doubles 269 | # in older RSpec versions 270 | describe 'an actual saver' do 271 | it 'implements the methods needed by the s3 saver' do 272 | instance_methods = Capybara::Screenshot::Saver.instance_methods 273 | 274 | expect(instance_methods).to include(:save) 275 | expect(instance_methods).to include(:html_saved?) 276 | expect(instance_methods).to include(:html_path) 277 | expect(instance_methods).to include(:screenshot_saved?) 278 | expect(instance_methods).to include(:screenshot_path) 279 | end 280 | end 281 | 282 | describe 'any other method' do 283 | it 'transparently passes through to the saver' do 284 | allow(saver).to receive(:foo_bar) 285 | 286 | args = double('args') 287 | s3_saver.foo_bar(*args) 288 | 289 | expect(saver).to have_received(:foo_bar).with(*args) 290 | end 291 | end 292 | end 293 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 17 July 2017 - 1.0.16 -> 1.0.17 2 | 3 | * [Better handling of `page.current_path` exceptions for Spinach](https://github.com/mattheworiordan/capybara-screenshot/pull/208) 4 | 5 | 12 July 2017 - 1.0.15 -> 1.0.16 6 | 7 | * [Support s3 key name prefixes](https://github.com/mattheworiordan/capybara-screenshot/pull/202) 8 | 9 | 12 July 2017 - 1.0.14 -> 1.0.15 10 | 11 | * [SVG badges added](https://github.com/mattheworiordan/capybara-screenshot/pull/207) 12 | * [Ensure the reporter module is not loaded multiple times](https://github.com/mattheworiordan/capybara-screenshot/pull/205) 13 | * [S3 object config option](https://github.com/mattheworiordan/capybara-screenshot/pull/204) 14 | * [Saver can use injected page](https://github.com/mattheworiordan/capybara-screenshot/pull/181) 15 | * [After* callbacks](https://github.com/mattheworiordan/capybara-screenshot/pull/171) 16 | 17 | 15 Sep 2016 - 1.0.13 -> 1.0.14 18 | 19 | * CI stable again - dropped CI support for older versions of Ruby and JRuby. It is just too painful and there is no evidence that our users need this or that Capybara Screenshot is indeed broken. It is better to have a reliable build system so that PRs can get merged in reliably and easily. 20 | * Fixed an inconsistency in the use of `save_and_open_page_path` 21 | * Improved debugging of Aruba tests so that contributors can easily understand why their builds break 22 | * Path fixes thanks to [Ryan McGarvey](https://github.com/ryanmcgarvey) 23 | 24 | 23 May 2016 - 1.0.12 -> 1.0.13 25 | 26 | * Fixes [mkmf bug 162](https://github.com/mattheworiordan/capybara-screenshot/issues/162) and [mkmf bug 174](https://github.com/mattheworiordan/capybara-screenshot/issues/174) 27 | * Fix for `Capybara.save_path` method existence assumption 28 | 29 | 29 March 2016 - 1.0.11 -> 1.0.12 30 | 31 | * [Aruba upgrade - passing CI](https://github.com/mattheworiordan/capybara-screenshot/pull/156) 32 | * [imgcat support](https://github.com/mattheworiordan/capybara-screenshot/pull/153) 33 | * [Fix for capaybara root when Rails not defined](https://github.com/mattheworiordan/capybara-screenshot/pull/139) 34 | * [Fix using_session_with_screenshot](https://github.com/mattheworiordan/capybara-screenshot/pull/132) 35 | * [Skip screenshot on skipped test](https://github.com/mattheworiordan/capybara-screenshot/pull/131) 36 | * [Don't output screenshot paths if not saved](https://github.com/mattheworiordan/capybara-screenshot/pull/128) 37 | 38 | 22 July 2015 - 1.0.10 -> 1.0.11 39 | 40 | * [Support for Fuubar reporter](https://github.com/mattheworiordan/capybara-screenshot/pull/137) 41 | 42 | Thanks to [Kai Schlichting](https://github.com/lacco) 43 | 44 | 29 June 2015 - 1.0.9 -> 1.0.10 45 | 46 | * [Small fix to memoization](https://github.com/mattheworiordan/capybara-screenshot/pull/134) plus [mini refactor](https://github.com/mattheworiordan/capybara-screenshot/commit/1db950bc53c729b26b8881d058a8781d6e7611b8) 47 | 48 | Thanks to [Systho](https://github.com/Systho) 49 | 50 | 6 April 2015 - 1.0.8 -> 1.0.9 51 | ----------- 52 | 53 | * [Improved file links within screenshot output](https://github.com/mattheworiordan/capybara-screenshot/pull/123) 54 | 55 | Thanks to [Jan Lelis](https://github.com/janlelis) 56 | 57 | 6 April 2015 - 1.0.7 -> 1.0.8 58 | ----------- 59 | 60 | * Less aggressive pruning 61 | 62 | 9 March 2015 - 1.0.6 -> 1.0.7 63 | ----------- 64 | 65 | * Fix capybara-webkit bug, see https://github.com/mattheworiordan/capybara-screenshot/issues/119 66 | * Fix Travis CI builds in Ruby < 2.1 and added Ruby 2.2 support 67 | 68 | 8 March 2015 - 1.0.5 -> 1.0.6 69 | ----------- 70 | 71 | * Removed dependency on the colored gem 72 | 73 | Thanks to [François Bernier](https://github.com/fbernier) 74 | 75 | 10 Feburary 2015 - 1.04 -> 1.0.5 76 | ----------- 77 | 78 | * Added support for appending a random string to the filename 79 | 80 | Thanks to [Brad Wedell](https://github.com/streetlogics) 81 | 82 | 5 January 2015 - 1.0.3 -> 1.0.4 83 | ----------- 84 | 85 | * Added support for Poltergeist Billy 86 | * Don't initialize a new Capybara::Session in after hook 87 | 88 | Thanks to [Neodude](https://github.com/neodude) and [Dominik Masur](https://github.com/dmasur) 89 | 90 | 1 October 2014 - 1.0.2 -> 1.0.3 91 | ----------- 92 | 93 | * Added ability to prune screenshots automatically, see https://github.com/mattheworiordan/capybara-screenshot/pull/100 94 | 95 | Thanks to [Anton Kolomiychuk](https://github.com/akolomiychuk) for his contribution. 96 | 97 | 27 September 2014 - 1.0.1 -> 1.0.2 98 | ----------- 99 | 100 | * Improved documentation to cover RSpec 3's new approach to using `rails_helper` in place of `spec_helper` for Rails tests 101 | * Updated documentation to use Ruby formatting in language blocks 102 | * Removed need to manually `require 'capybara-screenshot'` for RSpec 103 | 104 | 18 September 2014 - 1.0.0 -> 1.0.1 105 | ----------- 106 | 107 | * Hot fix for RSpec version issue that assumed RSpec base library was always available, now uses `RSpec::Core::VERSION` 108 | * Improve Travis CI performance and stability 109 | 110 | 18 September 2014 - 0.3.22 -> 1.0.0 111 | ----------- 112 | 113 | Because of the broad test coverage now across RSpec, Cucumber, Spinach, Minitest and TestUnit using [Aruba](https://github.com/cucumber/aruba), I feel that this gem is ready for its first major release. New features and refactoring can now reliably be done without the fear of regressions. 114 | 115 | The major changes in this 1.0 release are: 116 | 117 | * Acceptance test coverage for RSpec, Cucumber, Spinach, Minitest and TestUnit 118 | * Travis CI test coverage across a matrix of old and new versions of the aforementioned testing frameworks, see https://github.com/mattheworiordan/capybara-screenshot/blob/master/.travis.yml 119 | * Support for RSpec 3 using the custom formatters 120 | * Support for sessions using `using_session`, see https://github.com/mattheworiordan/capybara-screenshot/pull/91 for more info 121 | * Support for RSpec DocumentationFormatter 122 | * Considerable refactoring of the test suite 123 | 124 | Special thanks goes to [Andrew Brown](https://github.com/dontfidget) who has contributed a huge amount of the code that has helped enable this Gem to have its stable major version release. 125 | 126 | 22 July 2014 - 0.3.21 -> 0.3.22 127 | ----------- 128 | 129 | Replaced [colorize](https://rubygems.org/gems/colorize) gem with [colored](https://rubygems.org/gems/colored) due to license issue, see https://github.com/mattheworiordan/capybara-screenshot/issues/93. 130 | 131 | 22 July 2014 - 0.3.20 -> 0.3.21 132 | ----------- 133 | 134 | As a result of recent merges and insufficient test coverage, it seems that for test suites other than RSpec the HTML or Image screenshot path was no longer being outputted in the test results. This has now been fixed, and screenshot output format for RSpec and all other test suites has been standardised. 135 | 136 | 11 July 2014 - 0.3.19 -> 0.3.20 137 | ----------- 138 | 139 | * Added reporters to improve screenshot info in RSpec output 140 | * Added support for Webkit options such as width and height 141 | 142 | Thanks to https://github.com/multiplegeorges and https://github.com/noniq 143 | 144 | 2 April 2014 - 0.3.18 -> 0.3.19 145 | ----------- 146 | 147 | * Added support Spinach, thanks to https://github.com/suchitpuri 148 | 149 | 2 March 2014 - 0.3.16 -> 0.3.17 150 | ----------- 151 | 152 | * Added support for RSpec 3 and cleaned up the logging so there is less noise within the test results when a driver does not support a particular format. 153 | * Updated Travis to test against Ruby 2.0 and Ruby 2.1 154 | 155 | Thanks to https://github.com/noniq 156 | 157 | 7 January 2014 158 | ----------- 159 | 160 | Bug fix for Minitest 5, thanks to https://github.com/cschramm 161 | 162 | 163 | 12 September 2013 164 | ----------- 165 | 166 | Added support for Test Unit, fixed RSpec deprecation warnings and fixed a dependency issue. 167 | 168 | Thanks to: 169 | 170 | * https://github.com/budnik 171 | * https://github.com/jkraemer 172 | * https://github.com/mariovisic 173 | 174 | 175 | 23 July 2013 176 | ----------- 177 | 178 | https://github.com/stevenwilkin contributed code to display a warning for [Mechanize](http://mechanize.rubyforge.org/) users. 179 | 180 | 3 June 2013 181 | ----------- 182 | 183 | Dropped Ruby 1.8 support for this Gem because of conflicts with Nokogiri requiring a later version of Ruby. Instead, there is a new branch https://github.com/mattheworiordan/capybara-screenshot/tree/ruby-1.8-support which can be used if requiring backwards compatabiltiy. 184 | 185 | 18 Apr 2013 186 | ----------- 187 | 188 | Improved documentation, Ruby 1.8.7 support by not allowing Capybara 2.1 to be used, improved Sinatra support. 189 | RSpec screenshot fix to only screenshot when applicable: https://github.com/mattheworiordan/capybara-screenshot/issues/44 190 | 191 | 07 Jan 2013 192 | ----------- 193 | 194 | Support for Terminus, thanks to https://github.com/jamesotron 195 | 196 | 27 Dec 2012 197 | ----------- 198 | 199 | Previos version bump broke Ruby 1.8.7 support, so Travis CI build added to this Gem and Ruby 1.8.7 support along with JRuby support added. 200 | 201 | 30 Oct 2012 - Significant version bump 0.3 202 | ----------- 203 | 204 | After some consideration, and continued problems with load order of capybara-screenshot in relation to other required gems, the commits from @adzap in the pull request https://github.com/mattheworiordan/capybara-screenshot/pull/29 have been incorporated. Moving forwards, for every testing framework you use, you will be required to add an explicit require. 205 | 206 | 207 | 15 Feb 2012 208 | ----------- 209 | 210 | Merged pull request https://github.com/mattheworiordan/capybara-screenshot/pull/14 to limit when capybara-screenshot is fired for RSpec 211 | 212 | 30 Jan 2012 213 | ----------- 214 | 215 | Merged pull request from https://github.com/hlascelles to support Padrino 216 | 217 | 15 Jan 2012 218 | ----------- 219 | 220 | Removed unnecessary and annoying warning that a screen shot cannot be taken. This message was being shown when RSpec tests were run that did not even invoke Capybara 221 | 222 | 13 Jan 2012 223 | ----------- 224 | 225 | Updated documentation to reflect support for more frameworks, https://github.com/mattheworiordan/capybara-screenshot/issues/9 226 | 227 | 3 Jan 2012 228 | ---------- 229 | 230 | Removed Cucumber dependency https://github.com/mattheworiordan/capybara-screenshot/issues/7 231 | Allowed PNG save path to be configured using capybara.save_and_open_page_path 232 | 233 | 3 December 2011 234 | --------------- 235 | 236 | More robust handling of Minitest for users who have it installed as a dependency 237 | https://github.com/mattheworiordan/capybara-screenshot/issues/5 238 | 239 | 240 | 2 December 2011 241 | --------------- 242 | 243 | Fixed bug related to teardown hook not being available in Minitest for some reason (possibly version issues). 244 | https://github.com/mattheworiordan/capybara-screenshot/issues/5 245 | 246 | 24 November 2011 247 | ---------------- 248 | 249 | Added support for: 250 | 251 | * More platforms (Poltergeist) 252 | * Removed Rails dependencies (bug) 253 | * Added screenshot capability for Selenium 254 | * Added support for embed for HTML reports 255 | 256 | Thanks to [https://github.com/rb2k](https://github.com/rb2k) for 2 [great commits](https://github.com/mattheworiordan/capybara-screenshot/pull/4) 257 | 258 | 16 November 2011 259 | ---------------- 260 | 261 | Added support for Minitest using teardown hooks 262 | 263 | 264 | 16 November 2011 265 | ---------------- 266 | 267 | Added support for RSpec by adding a RSpec configuration after hook and checking if Capybara is being used. 268 | 269 | 15 November 2011 270 | ---------------- 271 | 272 | Ensured that tests run other than Cucumber won't fail. Prior to this Cucumber was required. 273 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | capybara-screenshot gem 2 | ======================= 3 | 4 | [](https://travis-ci.org/mattheworiordan/capybara-screenshot) 5 | [](https://codeclimate.com/github/mattheworiordan/capybara-screenshot) 6 | [](http://badge.fury.io/rb/capybara-screenshot) 7 | 8 | #### Capture a screen shot for every test failure automatically! 9 | 10 | `capybara-screenshot` used with [Capybara](https://github.com/jnicklas/capybara) and [Cucumber](http://cukes.info/), [Rspec](https://www.relishapp.com/rspec) or [Minitest](https://github.com/seattlerb/minitest), will capture a screen shot for each failure in your test suite. The HTML for the failed page, and a screenshot image (when using [capybara-webkit](https://github.com/thoughtbot/capybara-webkit), [Selenium](http://seleniumhq.org/) or [poltergeist](https://github.com/jonleighton/poltergeist)) is saved into `$APPLICATION_ROOT/tmp/capybara`. 11 | 12 | Having screenshots readily available for each test failure is incredibly helpful when trying to quickly diagnose a problem in your failing steps. You can view the source code, and have a screen shot of the page (when applicable), at the time of each failure. 13 | 14 | _Please note that Ruby 1.9+ is required to use this Gem. For Ruby 1.8 support, please see the [capybara-screenshot Ruby 1.8 branch](https://github.com/mattheworiordan/capybara-screenshot/tree/ruby-1.8-support)_ 15 | 16 | Installation 17 | ----- 18 | 19 | ### Step 1: install the gem 20 | 21 | Using Bundler, add the following to your Gemfile 22 | 23 | ```ruby 24 | gem 'capybara-screenshot', :group => :test 25 | ``` 26 | 27 | or install manually using Ruby Gems: 28 | 29 | ``` 30 | gem install capybara-screenshot 31 | ``` 32 | 33 | ### Step 2: load capybara-screenshot into your tests 34 | 35 | #### Cucumber 36 | 37 | In env.rb or a support file, please add: 38 | 39 | ```ruby 40 | require 'capybara-screenshot/cucumber' 41 | ``` 42 | 43 | #### RSpec 44 | 45 | In rails_helper.rb, spec_helper.rb, or a support file, after the require for 'capybara/rspec', please add: 46 | 47 | ```ruby 48 | # remember: you must require 'capybara/rspec' first 49 | require 'capybara-screenshot/rspec' 50 | ``` 51 | 52 | *Note: As of RSpec Rails 3.0, it is recommended that all your Rails environment code is loaded into `rails_helper.rb` instead of `spec_helper.rb`, and as such, the capybara-screenshot require should be located in `rails_helper.rb`. See the [RSpec Rails 3.0 upgrade notes](https://www.relishapp.com/rspec/rspec-rails/v/3-0/docs/upgrade) for more info.* 53 | 54 | #### Minitest 55 | 56 | Typically in 'test/test_helper.rb', please add: 57 | 58 | ```ruby 59 | require 'capybara-screenshot/minitest' 60 | ``` 61 | 62 | Also, consider adding `include Capybara::Screenshot::MiniTestPlugin` to any test classes that fail. For example, to capture screenshots for all failing integration tests in minitest-rails, try something like: 63 | 64 | ```ruby 65 | class ActionDispatch::IntegrationTest 66 | include Capybara::Screenshot::MiniTestPlugin 67 | # ... 68 | end 69 | ``` 70 | 71 | #### Test::Unit 72 | 73 | Typically in 'test/test_helper.rb', please add: 74 | 75 | ```ruby 76 | require 'capybara-screenshot/testunit' 77 | ``` 78 | 79 | By default, screenshots will be captured for `Test::Unit` tests in the path 'test/integration'. You can add additional paths as: 80 | 81 | ```ruby 82 | Capybara::Screenshot.testunit_paths << 'test/feature' 83 | ``` 84 | 85 | 86 | Manual screenshots 87 | ---- 88 | 89 | If you require more control, you can generate the screenshot on demand rather than on failure. This is useful 90 | if the failure occurs at a point where the screen shot is not as useful for debugging a rendering problem. This 91 | can be more useful if you disable the auto-generate on failure feature with the following config 92 | 93 | ```ruby 94 | Capybara::Screenshot.autosave_on_failure = false 95 | ``` 96 | 97 | Anywhere the Capybara DSL methods (visit, click etc.) are available so too are the screenshot methods. 98 | 99 | ```ruby 100 | screenshot_and_save_page 101 | ``` 102 | 103 | Or for screenshot only, which will automatically open the image. 104 | 105 | ```ruby 106 | screenshot_and_open_image 107 | ``` 108 | 109 | These are just calls on the main library methods. 110 | 111 | ```ruby 112 | Capybara::Screenshot.screenshot_and_save_page 113 | Capybara::Screenshot.screenshot_and_open_image 114 | ``` 115 | 116 | Better looking HTML screenshots 117 | ------------------------------- 118 | 119 | By the default, HTML screenshots will not look very good when opened in a browser. This happens because the browser can't correctly resolve relative paths like ``, which stops CSS, images, etc... from beind loaded. To get a nicer looking page, configure Capybara with: 120 | 121 | ```ruby 122 | Capybara.asset_host = 'http://localhost:3000' 123 | ``` 124 | 125 | This will cause Capybara to add `