├── .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 content

Saved 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 | [![Build Status](https://travis-ci.org/mattheworiordan/capybara-screenshot.svg)](https://travis-ci.org/mattheworiordan/capybara-screenshot) 5 | [![Code Climate](https://d3s6mut3hikguw.cloudfront.net/github/mattheworiordan/capybara-screenshot.svg)](https://codeclimate.com/github/mattheworiordan/capybara-screenshot) 6 | [![Gem Version](https://badge.fury.io/rb/capybara-screenshot.svg)](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 `http://localhost:3000` to the HTML file, which gives the browser enough information to resolve relative paths. Next, start a rails server in development mode, on port 3000, to respond to requests for assets: 126 | 127 | ```bash 128 | rails s -p 3000 129 | ``` 130 | 131 | Now when you open the page, you should have something that looks much better. You can leave this setup in place and use the default HTML pages when you don't care about the presentation, or start the rails server when you need something better looking. 132 | 133 | Driver configuration 134 | -------------------- 135 | 136 | The gem supports the default rendering method for Capybara to generate the screenshot, which is: 137 | 138 | ```ruby 139 | page.driver.render(path) 140 | ``` 141 | 142 | There are also some specific driver configurations for Selenium, Webkit, and Poltergeist. See [the definitions here](https://github.com/mattheworiordan/capybara-screenshot/blob/master/lib/capybara-screenshot.rb). The Rack::Test driver, Rails' default, does not allow 143 | rendering, so it has a driver definition as a noop. 144 | 145 | Capybara-webkit defaults to a screenshot size of 1000px by 10px. To specify a custom size, use the following option: 146 | 147 | ```ruby 148 | Capybara::Screenshot.webkit_options = { width: 1024, height: 768 } 149 | ``` 150 | 151 | If a driver is not found the default rendering will be used. If this doesn't work with your driver, then you can 152 | add another driver configuration like so 153 | 154 | ```ruby 155 | # The driver name should match the Capybara driver config name. 156 | Capybara::Screenshot.register_driver(:exotic_browser_driver) do |driver, path| 157 | driver.super_dooper_render(path) 158 | end 159 | ``` 160 | 161 | If your driver is based on existing browser driver, like Firefox, instead of `.super_dooper_render` do `driver.browser.save_screenshot path`. 162 | 163 | 164 | Custom screenshot filename 165 | -------------------------- 166 | 167 | If you want to control the screenshot filename for a specific test library, to inject the test name into it for example, 168 | you can override how the basename is generated for the file like so 169 | 170 | ```ruby 171 | Capybara::Screenshot.register_filename_prefix_formatter(:rspec) do |example| 172 | "screenshot_#{example.description.gsub(' ', '-').gsub(/^.*\/spec\//,'')}" 173 | end 174 | ``` 175 | 176 | By default capybara-screenshot will append a timestamp to the basename. If you want to disable this behavior set the following option: 177 | 178 | ```ruby 179 | Capybara::Screenshot.append_timestamp = false 180 | ``` 181 | 182 | 183 | Custom screenshot directory 184 | -------------------------- 185 | By default, when running under Rails, Sinatra, and Padrino, screenshots are saved into `$APPLICATION_ROOT/tmp/capybara`. Otherwise, they're saved under `Dir.pwd`. 186 | If you want to customize the location, override the file path as: 187 | 188 | ```ruby 189 | Capybara.save_path = "/file/path" 190 | ``` 191 | 192 | 193 | Uploading screenshots to S3 194 | -------------------------- 195 | You can configure capybara-screenshot to automatically save your screenshots to an AWS S3 bucket. 196 | 197 | First, install the `aws-sdk` gem or add it to your Gemfile 198 | 199 | ```ruby 200 | gem 'capybara-screenshot', :group => :test 201 | gem 'aws-sdk', :group => :test 202 | ``` 203 | 204 | Next, configure capybara-screenshot with your S3 credentials, the bucket to save to, and an optional region (default: `us-east-1`). 205 | 206 | ```ruby 207 | Capybara::Screenshot.s3_configuration = { 208 | s3_client_credentials: { 209 | access_key_id: "my_access_key_id", 210 | secret_access_key: "my_secret_access_key", 211 | region: "eu-central-1" 212 | }, 213 | bucket_name: "my_screenshots" 214 | } 215 | ``` 216 | 217 | It is also possible to specify the object parameters such as acl. 218 | Configure the capybara-screenshot with these options in this way: 219 | 220 | ```ruby 221 | Capybara::Screenshot.s3_object_configuration = { 222 | acl: 'public-read' 223 | } 224 | ``` 225 | 226 | You may optionally specify a `:key_prefix` when generating the S3 keys, which can be used to create virtual [folders](http://docs.aws.amazon.com/AmazonS3/latest/UG/FolderOperations.html) in S3, e.g.: 227 | 228 | ```ruby 229 | Capybara::Screenshot.s3_configuration = { 230 | ... # other config here 231 | key_prefix: "some/folder/" 232 | } 233 | ``` 234 | 235 | Pruning old screenshots automatically 236 | -------------------------- 237 | By default screenshots are saved indefinitely, if you want them to be automatically pruned on a new failure, then you can specify one of the following prune strategies as follows: 238 | 239 | ```ruby 240 | # Keep only the screenshots generated from the last failing test suite 241 | Capybara::Screenshot.prune_strategy = :keep_last_run 242 | 243 | # Keep up to the number of screenshots specified in the hash 244 | Capybara::Screenshot.prune_strategy = { keep: 20 } 245 | ``` 246 | 247 | Callbacks 248 | --------- 249 | 250 | You can hook your own logic into callbacks after the html/screenshot has been saved. 251 | 252 | ```ruby 253 | # after Saver#save_html 254 | Capybara::Screenshot.after_save_html do |path| 255 | mail = Mail.new do 256 | delivery_method :sendmail 257 | from 'capybara-screenshot@example.com' 258 | to 'dev@example.com' 259 | subject 'Capybara Screenshot' 260 | add_file File.read path 261 | end 262 | mail.delivery_method :sendmail 263 | mail.deliver 264 | end 265 | 266 | # after Saver#save_screenshot 267 | Capybara::Screenshot.after_save_screenshot do |path| 268 | # ... 269 | end 270 | ``` 271 | 272 | Information about screenshots in RSpec output 273 | --------------------------------------------- 274 | 275 | By default, capybara-screenshot extend RSpec’s formatters to include a link to the screenshot and/or saved html page for each failed spec. If you want to disable this feature completely (eg. to avoid problems with CI tools), use: 276 | 277 | ```ruby 278 | Capybara::Screenshot::RSpec.add_link_to_screenshot_for_failed_examples = false 279 | ``` 280 | 281 | It’s also possible to directly embed the screenshot image in the output if you’re using RSpec’s HtmlFormatter: 282 | 283 | ```ruby 284 | Capybara::Screenshot::RSpec::REPORTERS["RSpec::Core::Formatters::HtmlFormatter"] = Capybara::Screenshot::RSpec::HtmlEmbedReporter 285 | ``` 286 | 287 | If you want to further customize the information added to RSpec’s output, just implement your own reporter class and customize `Capybara::Screenshot::RSpec::REPORTERS` accordingly. See [rspec.rb](lib/capybara-screenshot/rspec.rb) for more info. 288 | 289 | 290 | Common problems 291 | --------------- 292 | 293 | If you have recently upgraded from v0.2, or you find that screen shots are not automatically being generated, then it's most likely you have not included the necessary `require` statement for your testing framework described above. As of version 0.3, without the explicit require, Capybara-Screenshot will not automatically take screen shots. Please re-read the installation instructions above. 294 | 295 | Also make sure that you're not calling `Capybara.reset_sessions!` before the screenshot hook runs. For RSpec you want to make sure that you're using `append_after` instead of `after`, for instance: 296 | 297 | ```ruby 298 | config.append_after(:each) do 299 | Capybara.reset_sessions! 300 | end 301 | ``` 302 | 303 | [Raise an issue on the Capybara-Screenshot issue tracker](https://github.com/mattheworiordan/capybara-screenshot/issues) if you are still having problems. 304 | 305 | Repository & Contributing to this Gem 306 | ------------------------------------- 307 | 308 | #### Bugs 309 | 310 | Please raise an issue at [https://github.com/mattheworiordan/capybara-screenshot/issues](https://github.com/mattheworiordan/capybara-screenshot/issues) and ensure you provide sufficient detail to replicate the problem. 311 | 312 | #### Contributions 313 | 314 | Contributions are welcome. Please fork this gem, and submit a pull request. New features must include test coverage and must pass on all versions of the testing frameworks supported. Run `appraisal` to set up the your Gems. then `appraisal "rake travis:ci"` locally to test your changes against all versions of testing framework gems supported. 315 | 316 | #### Rubygems 317 | 318 | The gem details on RubyGems.org can be found at [https://rubygems.org/gems/capybara-screenshot](https://rubygems.org/gems/capybara-screenshot) 319 | 320 | About 321 | ----- 322 | 323 | This gem was written by **Matthew O'Riordan**, with contributions from [many kind people](https://github.com/mattheworiordan/capybara-screenshot/network/members). 324 | 325 | - [http://mattheworiordan.com](http://mattheworiordan.com) 326 | - [@mattheworiordan](http://twitter.com/#!/mattheworiordan) 327 | - [Linked In](http://www.linkedin.com/in/lemon) 328 | 329 | License 330 | ------- 331 | 332 | Copyright © 2016 Matthew O'Riordan, inc. It is free software, and may be redistributed under the terms specified in the LICENSE file. 333 | -------------------------------------------------------------------------------- /spec/unit/saver_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Capybara::Screenshot::Saver do 4 | before(:all) do 5 | @original_drivers = Capybara::Screenshot.registered_drivers 6 | Capybara::Screenshot.registered_drivers[:default] = lambda {|driver, path| driver.render(path) } 7 | end 8 | 9 | after(:all) do 10 | Capybara::Screenshot.registered_drivers = @original_drivers 11 | end 12 | 13 | before do 14 | allow(Capybara::Screenshot).to receive(:capybara_root).and_return(capybara_root) 15 | Timecop.freeze(Time.local(2012, 6, 7, 8, 9, 10, 0)) 16 | end 17 | 18 | let(:capybara_root) { '/tmp' } 19 | let(:timestamp) { '2012-06-07-08-09-10.000' } 20 | let(:file_basename) { "screenshot_#{timestamp}" } 21 | let(:screenshot_path) { "#{capybara_root}/#{file_basename}.png" } 22 | 23 | let(:driver_mock) { double('Capybara driver').as_null_object } 24 | let(:page_mock) { 25 | double('Capybara session page', :body => 'body', :driver => driver_mock).as_null_object.tap do |m| 26 | allow(m).to receive(:current_path).and_return('/') 27 | end 28 | } 29 | let(:capybara_mock) { 30 | double(Capybara).as_null_object.tap do |m| 31 | allow(m).to receive(:current_driver).and_return(:default) 32 | end 33 | } 34 | 35 | let(:saver) { Capybara::Screenshot::Saver.new(capybara_mock, page_mock) } 36 | 37 | context 'html filename with Capybara Version 1' do 38 | before do 39 | stub_const("Capybara::VERSION", '1') 40 | end 41 | 42 | it 'has a default format of "screenshot_Y-M-D-H-M-S.ms.html"' do 43 | expect(capybara_mock).to receive(:save_page).with('body', File.join(capybara_root, "#{file_basename}.html")) 44 | 45 | saver.save 46 | end 47 | 48 | it 'uses name argument as prefix' do 49 | saver = Capybara::Screenshot::Saver.new(capybara_mock, page_mock, true, 'custom-prefix') 50 | 51 | expect(capybara_mock).to receive(:save_page).with('body', File.join(capybara_root, "custom-prefix_#{timestamp}.html")) 52 | 53 | saver.save 54 | end 55 | end 56 | 57 | context 'html filename with Capybara Version 2' do 58 | before do 59 | stub_const("Capybara::VERSION", '2') 60 | end 61 | 62 | it 'has a default format of "screenshot_Y-M-D-H-M-S.ms.html"' do 63 | expect(capybara_mock).to receive(:save_page).with(File.join(capybara_root, "#{file_basename}.html")) 64 | 65 | saver.save 66 | end 67 | 68 | it 'uses name argument as prefix' do 69 | saver = Capybara::Screenshot::Saver.new(capybara_mock, page_mock, true, 'custom-prefix') 70 | 71 | expect(capybara_mock).to receive(:save_page).with(File.join(capybara_root, "custom-prefix_#{timestamp}.html")) 72 | 73 | saver.save 74 | end 75 | end 76 | 77 | context 'screenshot image path' do 78 | it 'is in capybara root output' do 79 | expect(driver_mock).to receive(:render).with(/^#{capybara_root}\//) 80 | 81 | saver.save 82 | end 83 | 84 | it 'has a default filename format of "screenshot_Y-M-D-H-M-S.ms.png"' do 85 | expect(driver_mock).to receive(:render).with(/#{file_basename}\.png$/) 86 | 87 | saver.save 88 | end 89 | 90 | it "does not append timestamp if append_timestamp is false " do 91 | default_config = Capybara::Screenshot.append_timestamp 92 | Capybara::Screenshot.append_timestamp = false 93 | expect(driver_mock).to receive(:render).with(/screenshot.png$/) 94 | 95 | saver.save 96 | Capybara::Screenshot.append_timestamp = default_config 97 | end 98 | 99 | it 'uses filename prefix argument as basename prefix' do 100 | saver = Capybara::Screenshot::Saver.new(capybara_mock, page_mock, true, 'custom-prefix') 101 | expect(driver_mock).to receive(:render).with(/#{capybara_root}\/custom-prefix_#{timestamp}\.png$/) 102 | 103 | saver.save 104 | end 105 | end 106 | 107 | it 'does not save html if false passed as html argument' do 108 | saver = Capybara::Screenshot::Saver.new(capybara_mock, page_mock, false) 109 | expect(capybara_mock).to_not receive(:save_page) 110 | 111 | saver.save 112 | expect(saver).to_not be_html_saved 113 | end 114 | 115 | context 'the current_path is empty' do 116 | before(:each) do 117 | allow(page_mock).to receive(:current_path).and_return(nil) 118 | end 119 | 120 | it 'does not save' do 121 | expect(capybara_mock).to_not receive(:save_page) 122 | expect(driver_mock).to_not receive(:render) 123 | 124 | saver.save 125 | expect(saver).to_not be_screenshot_saved 126 | expect(saver).to_not be_html_saved 127 | end 128 | 129 | it 'prints a warning' do 130 | expect(saver).to receive(:warn).with( 131 | 'WARN: Screenshot could not be saved. `page.current_path` is empty.', 132 | ) 133 | saver.save 134 | end 135 | end 136 | 137 | context 'when save_html raises' do 138 | before(:each) do 139 | allow(saver).to receive(:save_html).and_raise(NoMethodError.new('some error')) 140 | end 141 | 142 | it 'prints warning message' do 143 | expect(saver).to receive(:warn).with( 144 | 'WARN: HTML source could not be saved. An exception is raised: #.', 145 | ) 146 | saver.save 147 | end 148 | 149 | it 'tries to save screenshot' do 150 | expect(saver).to receive(:save_screenshot) 151 | saver.save 152 | end 153 | end 154 | 155 | context 'when save_screenshot raises' do 156 | before(:each) do 157 | allow(saver).to receive(:save_screenshot).and_raise(NoMethodError.new('some error')) 158 | end 159 | 160 | it 'prints warning message' do 161 | expect(saver).to receive(:warn).with( 162 | 'WARN: Screenshot could not be saved. An exception is raised: #.', 163 | ) 164 | saver.save 165 | end 166 | 167 | it 'tries to save screenshot' do 168 | expect(saver).to receive(:save_html) 169 | saver.save 170 | end 171 | end 172 | 173 | context 'when current_path raises' do 174 | before(:each) do 175 | allow(page_mock).to receive(:current_path).and_raise(NoMethodError.new('some error')) 176 | end 177 | 178 | it 'prints warning message' do 179 | expect(saver).to receive(:warn).with( 180 | 'WARN: Screenshot could not be saved. `page.current_path` raised exception: #.', 181 | ) 182 | saver.save 183 | end 184 | 185 | it 'does not print extra warning message' do 186 | expect(saver).not_to receive(:warn).with(/is empty/) 187 | saver.save 188 | end 189 | 190 | it 'still restores the original value of Capybara.save_and_open_page_path' do 191 | Capybara::Screenshot.capybara_tmp_path = 'tmp/bananas' 192 | 193 | allow(page_mock).to receive(:current_path).and_raise 194 | 195 | saver.save 196 | 197 | if Capybara.respond_to?(:save_path) 198 | expect(Capybara.save_path).to eq('tmp/bananas') 199 | else 200 | expect(Capybara.save_and_open_page_path).to eq('tmp/bananas') 201 | end 202 | end 203 | end 204 | 205 | describe '#output_screenshot_path' do 206 | let(:saver) { Capybara::Screenshot::Saver.new(capybara_mock, page_mock) } 207 | 208 | before do 209 | allow(saver).to receive(:html_path) { 'page.html' } 210 | allow(saver).to receive(:screenshot_path) { 'screenshot.png' } 211 | end 212 | 213 | it 'outputs the path for the HTML screenshot' do 214 | allow(saver).to receive(:html_saved?).and_return(true) 215 | expect(saver).to receive(:output).with("HTML screenshot: page.html") 216 | saver.output_screenshot_path 217 | end 218 | 219 | it 'outputs the path for the Image screenshot' do 220 | allow(saver).to receive(:screenshot_saved?).and_return(true) 221 | expect(saver).to receive(:output).with("Image screenshot: screenshot.png") 222 | saver.output_screenshot_path 223 | end 224 | end 225 | 226 | describe 'callbacks' do 227 | let(:saver) { Capybara::Screenshot::Saver.new(capybara_mock, page_mock) } 228 | 229 | before do 230 | allow(saver).to receive(:html_path) { 'page.html' } 231 | allow(saver).to receive(:screenshot_path) { 'screenshot.png' } 232 | end 233 | 234 | before :all do 235 | Capybara::Screenshot.after_save_html do |path| 236 | puts "after_save_html ran with #{path}" 237 | end 238 | Capybara::Screenshot.after_save_screenshot do |path| 239 | puts "after_save_screenshot ran with #{path}" 240 | end 241 | end 242 | 243 | after :all do 244 | Capybara::Screenshot::Saver.instance_eval { @callbacks = nil } 245 | end 246 | 247 | it 'runs after_save_html callbacks' do 248 | expect do 249 | saver.save 250 | end.to output(/after_save_html ran with page\.html/).to_stdout 251 | end 252 | 253 | it 'runs after_save_screenshot callbacks' do 254 | expect do 255 | saver.save 256 | end.to output(/after_save_screenshot ran with screenshot\.png/).to_stdout 257 | end 258 | end 259 | 260 | describe "with selenium driver" do 261 | before do 262 | allow(capybara_mock).to receive(:current_driver).and_return(:selenium) 263 | end 264 | 265 | it 'saves via browser' do 266 | browser_mock = double('browser') 267 | expect(driver_mock).to receive(:browser).and_return(browser_mock) 268 | expect(browser_mock).to receive(:save_screenshot).with(screenshot_path) 269 | 270 | saver.save 271 | expect(saver).to be_screenshot_saved 272 | end 273 | end 274 | 275 | describe "with poltergeist driver" do 276 | before do 277 | allow(capybara_mock).to receive(:current_driver).and_return(:poltergeist) 278 | end 279 | 280 | it 'saves driver render with :full => true' do 281 | expect(driver_mock).to receive(:render).with(screenshot_path, {:full => true}) 282 | 283 | saver.save 284 | expect(saver).to be_screenshot_saved 285 | end 286 | end 287 | 288 | describe "with poltergeist_billy driver" do 289 | before do 290 | allow(capybara_mock).to receive(:current_driver).and_return(:poltergeist_billy) 291 | end 292 | 293 | it 'saves driver render with :full => true' do 294 | expect(driver_mock).to receive(:render).with(screenshot_path, {:full => true}) 295 | 296 | saver.save 297 | expect(saver).to be_screenshot_saved 298 | end 299 | end 300 | 301 | describe "with webkit driver" do 302 | before do 303 | allow(capybara_mock).to receive(:current_driver).and_return(:webkit) 304 | end 305 | 306 | context 'has render method' do 307 | before do 308 | allow(driver_mock).to receive(:respond_to?).with(:'save_screenshot').and_return(false) 309 | end 310 | 311 | it 'saves driver render' do 312 | expect(driver_mock).to receive(:render).with(screenshot_path) 313 | 314 | saver.save 315 | expect(saver).to be_screenshot_saved 316 | end 317 | end 318 | 319 | context 'has save_screenshot method' do 320 | let(:webkit_options){ {width: 800, height: 600} } 321 | 322 | before do 323 | allow(driver_mock).to receive(:respond_to?).with(:'save_screenshot').and_return(true) 324 | end 325 | 326 | it 'saves driver render' do 327 | expect(driver_mock).to receive(:save_screenshot).with(screenshot_path, {}) 328 | 329 | saver.save 330 | expect(saver).to be_screenshot_saved 331 | end 332 | 333 | it 'passes webkit_options to driver' do 334 | allow(Capybara::Screenshot).to receive(:webkit_options).and_return( webkit_options ) 335 | expect(driver_mock).to receive(:save_screenshot).with(screenshot_path, webkit_options) 336 | 337 | saver.save 338 | expect(saver).to be_screenshot_saved 339 | end 340 | end 341 | end 342 | 343 | describe "with webkit debug driver" do 344 | before do 345 | allow(capybara_mock).to receive(:current_driver).and_return(:webkit_debug) 346 | end 347 | 348 | context 'has render method' do 349 | before do 350 | allow(driver_mock).to receive(:respond_to?).with(:'save_screenshot').and_return(false) 351 | end 352 | 353 | it 'saves driver render' do 354 | expect(driver_mock).to receive(:render).with(screenshot_path) 355 | 356 | saver.save 357 | expect(saver).to be_screenshot_saved 358 | end 359 | end 360 | 361 | context 'has save_screenshot method' do 362 | let(:webkit_options){ {width: 800, height: 600} } 363 | 364 | before do 365 | allow(driver_mock).to receive(:respond_to?).with(:'save_screenshot').and_return(true) 366 | end 367 | 368 | it 'saves driver render' do 369 | expect(driver_mock).to receive(:save_screenshot).with(screenshot_path, {}) 370 | 371 | saver.save 372 | expect(saver).to be_screenshot_saved 373 | end 374 | 375 | it 'passes webkit_options to driver' do 376 | allow(Capybara::Screenshot).to receive(:webkit_options).and_return( webkit_options ) 377 | expect(driver_mock).to receive(:save_screenshot).with(screenshot_path, webkit_options) 378 | 379 | saver.save 380 | expect(saver).to be_screenshot_saved 381 | end 382 | end 383 | end 384 | 385 | describe "with unknown driver" do 386 | before do 387 | allow(capybara_mock).to receive(:current_driver).and_return(:unknown) 388 | allow(saver).to receive(:warn).and_return(nil) 389 | end 390 | 391 | it 'saves driver render' do 392 | expect(driver_mock).to receive(:render).with(screenshot_path) 393 | 394 | saver.save 395 | expect(saver).to be_screenshot_saved 396 | end 397 | 398 | it 'outputs warning about unknown results' do 399 | # Not pure mock testing 400 | expect(saver).to receive(:warn).with(/screenshot driver for 'unknown'.*unknown results/).and_return(nil) 401 | 402 | saver.save 403 | expect(saver).to be_screenshot_saved 404 | end 405 | 406 | describe "with rack_test driver" do 407 | before do 408 | allow(capybara_mock).to receive(:current_driver).and_return(:rack_test) 409 | end 410 | 411 | it 'indicates that a screenshot could not be saved' do 412 | saver.save 413 | expect(saver).to_not be_screenshot_saved 414 | end 415 | end 416 | 417 | describe "with mechanize driver" do 418 | before do 419 | allow(capybara_mock).to receive(:current_driver).and_return(:mechanize) 420 | end 421 | 422 | it 'indicates that a screenshot could not be saved' do 423 | saver.save 424 | expect(saver).to_not be_screenshot_saved 425 | end 426 | end 427 | end 428 | end 429 | --------------------------------------------------------------------------------