├── .yardopts
├── Rakefile
├── CODEOWNERS
├── .rspec
├── Makefile
├── lib
└── percy
│ ├── version.rb
│ └── capybara.rb
├── spec
├── fixture
│ └── index.html
├── spec_helper.rb
└── lib
│ └── percy
│ └── percy_capybara_spec.rb
├── package.json
├── .gitignore
├── .rubocop.yml
├── .github
├── workflows
│ ├── changelog.yml
│ ├── lint.yml
│ ├── release.yml
│ ├── stale.yml
│ ├── Semgrep.yml
│ └── test.yml
├── dependabot.yml
├── release-drafter.yml
└── ISSUE_TEMPLATE
│ └── bug_report.md
├── Gemfile
├── Guardfile
├── LICENSE
├── percy-capybara.gemspec
├── Gemfile.lock
└── README.md
/.yardopts:
--------------------------------------------------------------------------------
1 | --protected
2 | --no-private
3 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'bundler/gem_tasks'
2 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @percy/percy-product-reviewers
2 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --require spec_helper
3 | --format d
4 |
5 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | release:
2 | rake build
3 | gem push pkg/percy-capybara-*
4 |
--------------------------------------------------------------------------------
/lib/percy/version.rb:
--------------------------------------------------------------------------------
1 | module PercyCapybara
2 | VERSION = '5.0.0'.freeze
3 | end
4 |
--------------------------------------------------------------------------------
/spec/fixture/index.html:
--------------------------------------------------------------------------------
1 |
I am a pageSnapshot me
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "test": "percy exec --testing -- bundle exec rspec"
5 | },
6 | "devDependencies": {
7 | "@percy/cli": "^1.16.0"
8 | }
9 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /_yardoc/
4 | /coverage/
5 | /doc/
6 | /pkg/
7 | /spec/reports/
8 | /tmp/
9 | *.bundle
10 | *.so
11 | *.o
12 | *.a
13 | *.rbc
14 | mkmf.log
15 | .DS_Store
16 | node_modules/
17 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | inherit_gem:
2 | percy-style:
3 | - default.yml
4 | RSpec/InstanceVariable:
5 | Exclude:
6 | -
7 | RSpec/ContextWording:
8 | Enabled: false
9 | AllCops:
10 | TargetRubyVersion: 2.4
11 | Include:
12 | - lib/**/*.rb
13 | - spec/lib/**/*.rb
14 |
--------------------------------------------------------------------------------
/.github/workflows/changelog.yml:
--------------------------------------------------------------------------------
1 | name: Changelog
2 | on:
3 | push:
4 | branches: [master]
5 | jobs:
6 | update_draft:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: release-drafter/release-drafter@v5
10 | env:
11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
12 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # Specify your gem's dependencies in percy-capybara.gemspec
4 | gemspec
5 |
6 | gem 'guard-rspec', require: false
7 |
8 | # (for development)
9 | # gem 'percy-client', path: '~/src/percy-client'
10 |
11 | group :test, :development do
12 | gem 'pry'
13 | gem 'puma', '5.6.6'
14 | gem 'webmock'
15 | gem 'simplecov', require: false
16 | end
17 |
--------------------------------------------------------------------------------
/Guardfile:
--------------------------------------------------------------------------------
1 | guard :rspec, cmd: 'bundle exec rspec' do
2 | require 'guard/rspec/dsl'
3 | dsl = Guard::RSpec::Dsl.new(self)
4 |
5 | # RSpec files
6 | rspec = dsl.rspec
7 | watch(rspec.spec_helper) { rspec.spec_dir }
8 | watch(rspec.spec_support) { rspec.spec_dir }
9 | watch(rspec.spec_files)
10 |
11 | # Ruby files
12 | ruby = dsl.ruby
13 | dsl.watch_spec_files_for(ruby.lib_files)
14 | end
15 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: bundler
4 | directory: /
5 | labels:
6 | - ⬆️⬇️ dependencies
7 | schedule:
8 | interval: weekly
9 | commit-message:
10 | prefix: ⬆️
11 | - package-ecosystem: github-actions
12 | directory: /
13 | schedule:
14 | interval: weekly
15 | commit-message:
16 | prefix: ⬆️👷
17 | labels:
18 | - ⬆️⬇️ dependencies
19 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 | on:
3 | push:
4 | branches: [master]
5 | pull_request:
6 | workflow_dispatch:
7 | jobs:
8 | lint:
9 | name: Lint
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v3
13 | - uses: ruby/setup-ruby@v1
14 | with:
15 | ruby-version: 2.6
16 | bundler-cache: true
17 | - uses: actions/cache@v3.0.11
18 | with:
19 | path: "./vendor/bundle"
20 | key: v1/${{ runner.os }}/ruby-2.6/${{ hashFiles('**/Gemfile.lock') }}
21 | restore-keys: v1/${{ runner.os }}/ruby-2.6/
22 | - run: bundle exec rubocop
23 |
--------------------------------------------------------------------------------
/.github/release-drafter.yml:
--------------------------------------------------------------------------------
1 | name-template: 'v$RESOLVED_VERSION'
2 | tag-template: 'v$RESOLVED_VERSION'
3 | categories:
4 | - title: '💥 Breaking Changes'
5 | labels:
6 | - 💥 breaking
7 | - title: '✨ Enhancements'
8 | labels:
9 | - ✨ enhancement
10 | - title: '🐛 Bug Fixes'
11 | labels:
12 | - 🐛 bug
13 | - title: '🏗 Maintenance'
14 | labels:
15 | - 🧹 maintenance
16 | - title: '⬆️⬇️ Dependency Updates'
17 | labels:
18 | - ⬆️⬇️ dependencies
19 | change-title-escapes: '\<*_@'
20 | version-resolver:
21 | major:
22 | labels:
23 | - 💥 breaking
24 | minor:
25 | labels:
26 | - ✨ enhancement
27 | default: patch
28 | template: '$CHANGES'
29 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | release:
4 | types: [published]
5 | jobs:
6 | publish:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v3
10 | - uses: ruby/setup-ruby@v1
11 | with:
12 | ruby-version: 2.6
13 | bundler-cache: true
14 | - uses: actions/cache@v3.0.11
15 | with:
16 | path: "./vendor/bundle"
17 | key: v1/${{ runner.os }}/ruby-2.6/${{ hashFiles('**/Gemfile.lock') }}
18 | restore-keys: v1/${{ runner.os }}/ruby-2.6/
19 | - uses: cadwallion/publish-rubygems-action@master
20 | env:
21 | RUBYGEMS_API_KEY: ${{secrets.RUBYGEMS_API_KEY}}
22 | RELEASE_COMMAND: make release
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2021 Perceptual Inc.
2 |
3 | The MIT License (MIT)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/.github/workflows/stale.yml:
--------------------------------------------------------------------------------
1 | name: 'Close stale issues and PRs'
2 | on:
3 | schedule:
4 | - cron: '0 19 * * 2'
5 |
6 | jobs:
7 | stale:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/stale@v6
11 | with:
12 | stale-issue-message: >-
13 | This issue is stale because it has been open for more than 14 days with no activity.
14 | Remove stale label or comment or this will be closed in 14 days.
15 | stale-pr-message: >-
16 | This PR is stale because it has been open for more than 14 days with no activity.
17 | Remove stale label or comment or this will be closed in 14 days.
18 | close-issue-message: >-
19 | This issue was closed because it has been stalled for 28 days with no activity.
20 | close-pr-message: >-
21 | This PR was closed because it has been stalled for 28 days with no activity.
22 | days-before-issue-stale: 14
23 | days-before-pr-stale: 14
24 | # close 14 days _after_ initial warning
25 | days-before-issue-close: 14
26 | days-before-pr-close: 14
27 | exempt-pr-labels: '❄️ on ice'
28 | exempt-issue-labels: '🐛 bug,❄️ on ice,✨ enhancement'
29 | exempt-all-assignees: true
30 | stale-pr-label: '🍞 stale'
31 | stale-issue-label: '🍞 stale'
32 |
--------------------------------------------------------------------------------
/percy-capybara.gemspec:
--------------------------------------------------------------------------------
1 | require_relative './lib/percy/version'
2 |
3 | Gem::Specification.new do |spec|
4 | spec.name = 'percy-capybara'
5 | spec.version = PercyCapybara::VERSION
6 | spec.authors = ['Perceptual Inc.']
7 | spec.email = ['team@percy.io']
8 | spec.summary = %q{Percy visual testing for Capybara}
9 | spec.description = %q{}
10 | spec.homepage = ''
11 | spec.license = 'MIT'
12 | spec.required_ruby_version = '>= 2.3.0'
13 |
14 | spec.metadata = {
15 | 'bug_tracker_uri' => 'https://github.com/percy/percy-capybara/issues',
16 | 'source_code_uri' => 'https://github.com/percy/percy-capybara',
17 | }
18 |
19 | spec.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md)
20 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
21 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
22 | spec.require_paths = ['lib']
23 |
24 | spec.add_runtime_dependency 'capybara', '>= 3'
25 |
26 | spec.add_development_dependency 'selenium-webdriver', '>= 4.0.0'
27 | spec.add_development_dependency 'geckodriver-bin', '~> 0.28.0'
28 | spec.add_development_dependency 'bundler', '>= 2.0'
29 | spec.add_development_dependency 'rake', '~> 13.0'
30 | spec.add_development_dependency 'rspec', '~> 3.5'
31 | spec.add_development_dependency 'capybara', '>= 3'
32 | spec.add_development_dependency 'percy-style', '~> 0.7.0'
33 | end
34 |
--------------------------------------------------------------------------------
/.github/workflows/Semgrep.yml:
--------------------------------------------------------------------------------
1 | # Name of this GitHub Actions workflow.
2 | name: Semgrep
3 |
4 | on:
5 | # Scan changed files in PRs (diff-aware scanning):
6 | # The branches below must be a subset of the branches above
7 | pull_request:
8 | branches: ["master", "main"]
9 | push:
10 | branches: ["master", "main"]
11 | schedule:
12 | - cron: '0 6 * * *'
13 |
14 |
15 | permissions:
16 | contents: read
17 |
18 | jobs:
19 | semgrep:
20 | # User definable name of this GitHub Actions job.
21 | permissions:
22 | contents: read # for actions/checkout to fetch code
23 | security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
24 | name: semgrep/ci
25 | # If you are self-hosting, change the following `runs-on` value:
26 | runs-on: ubuntu-latest
27 |
28 | container:
29 | # A Docker image with Semgrep installed. Do not change this.
30 | image: returntocorp/semgrep
31 |
32 | # Skip any PR created by dependabot to avoid permission issues:
33 | if: (github.actor != 'dependabot[bot]')
34 |
35 | steps:
36 | # Fetch project source with GitHub Actions Checkout.
37 | - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
38 | # Run the "semgrep ci" command on the command line of the docker image.
39 | - run: semgrep ci --sarif --output=semgrep.sarif
40 | env:
41 | # Add the rules that Semgrep uses by setting the SEMGREP_RULES environment variable.
42 | SEMGREP_RULES: p/default # more at semgrep.dev/explore
43 |
44 | - name: Upload SARIF file for GitHub Advanced Security Dashboard
45 | uses: github/codeql-action/upload-sarif@489225d82a57396c6f426a40e66d461b16b3461d # v2.20.4
46 | with:
47 | sarif_file: semgrep.sarif
48 | if: always()
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us fix the issue
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 | ---
8 |
9 |
22 |
23 | ## The problem
24 |
25 | Briefly describe the issue you are experiencing (or the feature you want to see
26 | added to Percy). Tell us what you were trying to do and what happened
27 | instead. Remember, this is _not_ a place to ask questions. For that, go to
28 | https://github.com/percy/cli/discussions/new
29 |
30 | ## Environment
31 |
32 | - Node version:
33 | - `@percy/cli` version:
34 | - Version of Percy SDK you’re using:
35 | - If needed, a build or snapshot ID:
36 | - OS version:
37 | - Type of shell command-line [interface]:
38 |
39 | ## Details
40 |
41 | If necessary, describe the problem you have been experiencing in more detail.
42 |
43 | ## Debug logs
44 |
45 | If you are reporting a bug, _always_ include logs! [Give the "Debugging SDKs"
46 | document a quick read for how to gather logs](https://www.browserstack.com/docs/percy/integrate/percy-sdk-workflow#debugging-sdks)
47 |
48 | Please do not trim or edit these logs, often times there are hints in the full
49 | logs that help debug what is going on.
50 |
51 | ## Code to reproduce issue
52 |
53 | Given the nature of testing/environment bugs, it’s best to try and isolate the
54 | issue in a reproducible repo. This will make it much easier for us to diagnose
55 | and fix.
56 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # This must be required & started before any app code (for proper coverage)
2 | require 'simplecov'
3 | SimpleCov.start
4 | SimpleCov.minimum_coverage 100
5 |
6 | require 'capybara/rspec'
7 | require 'webmock/rspec'
8 | require 'percy/capybara'
9 | require 'selenium-webdriver'
10 |
11 | RSpec.configure do |config|
12 | config.expect_with :rspec do |expectations|
13 | # This option will default to `true` in RSpec 4.
14 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true
15 | end
16 |
17 | config.mock_with :rspec do |mocks|
18 | mocks.verify_partial_doubles = true
19 | end
20 |
21 | config.disable_monkey_patching!
22 | # config.warnings = true
23 |
24 | # Run specs in random order to surface order dependencies. If you find an
25 | # order dependency and want to debug it, you can fix the order by providing
26 | # the seed, which is printed after each run.
27 | # --seed 1234
28 | config.order = :random
29 |
30 | # Seed global randomization in this process using the `--seed` CLI option.
31 | # Setting this allows you to use `--seed` to deterministically reproduce
32 | # test failures related to randomization by passing the same `--seed` value
33 | # as the one that triggered the failure.
34 | Kernel.srand config.seed
35 |
36 | # See https://github.com/teamcapybara/capybara#selecting-the-driver for other options
37 | Capybara.default_driver = :selenium_headless
38 | Capybara.javascript_driver = :selenium_headless
39 |
40 | # Setup for Capybara to test static files served by Rack
41 | Capybara.server_port = 3003
42 | Capybara.server = :puma, { Silent: true }
43 | Capybara.app = Rack::File.new(File.join(File.dirname(__FILE__), 'fixture'))
44 |
45 | # Ignore warning until Capybara releases an update solving their warning
46 | Selenium::WebDriver.logger.ignore(:browser_options)
47 | end
48 |
49 | ## Add cache clearing methods for tests
50 | Capybara::Session.class_eval {
51 | def __percy_clear_cache!
52 | @percy_dom = nil
53 | @percy_enabled = nil
54 | end
55 | }
56 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 | on:
3 | push:
4 | workflow_dispatch:
5 | inputs:
6 | branch:
7 | required: false
8 | type: string
9 | default: master
10 | jobs:
11 | test:
12 | name: Test
13 | strategy:
14 | matrix:
15 | os: [ubuntu-latest]
16 | ruby: ['2.6', '2.7']
17 | runs-on: ${{ matrix.os }}
18 | steps:
19 | - uses: actions-ecosystem/action-regex-match@v2
20 | id: regex-match
21 | if: ${{ github.event_name == 'workflow_dispatch' }}
22 | with:
23 | text: ${{ github.event.inputs.branch }}
24 | regex: '^[a-zA-Z0-9_/\-]+$'
25 | - name: Break on invalid branch name
26 | run: exit 1
27 | if: ${{ github.event_name == 'workflow_dispatch' && steps.regex-match.outputs && steps.regex-match.outputs.match == '' }}
28 | - uses: actions/checkout@v3
29 | - uses: ruby/setup-ruby@v1
30 | with:
31 | ruby-version: ${{matrix.ruby}}
32 | bundler-cache: true
33 | - uses: actions/cache@v3.0.11
34 | with:
35 | path: "./vendor/bundle"
36 | key: v1/${{ runner.os }}/ruby-${{ matrix.ruby }}/${{ hashFiles('**/Gemfile.lock') }}
37 | restore-keys: v1/${{ runner.os }}/ruby-${{ matrix.ruby }}/
38 | - uses: actions/setup-node@v3
39 | with:
40 | node-version: 16
41 | - name: Get yarn cache directory path
42 | id: yarn-cache-dir-path
43 | run: echo "::set-output name=dir::$(yarn cache dir)"
44 | - uses: actions/cache@v3
45 | with:
46 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
47 | key: v1/${{ runner.os }}/node-${{ matrix.node }}/${{ hashFiles('**/yarn.lock') }}
48 | restore-keys: v1/${{ runner.os }}/node-${{ matrix.node }}/
49 | - run: yarn
50 | - name: Set up @percy/cli from git
51 | if: ${{ github.event_name == 'workflow_dispatch' }}
52 | run: |
53 | cd /tmp
54 | git clone --branch ${{ github.event.inputs.branch }} --depth 1 https://github.com/percy/cli
55 | cd cli
56 | PERCY_PACKAGES=`find packages -mindepth 1 -maxdepth 1 -type d | sed -e 's/packages/@percy/g' | tr '\n' ' '`
57 | git log -1
58 | yarn
59 | yarn build
60 | yarn global:link
61 | cd ${{ github.workspace }}
62 | yarn remove @percy/cli && yarn link `echo $PERCY_PACKAGES`
63 | npx percy --version
64 | - run: npx percy exec --testing -- bundle exec rspec
65 |
--------------------------------------------------------------------------------
/lib/percy/capybara.rb:
--------------------------------------------------------------------------------
1 | require 'net/http'
2 | require 'uri'
3 | require 'capybara/dsl'
4 | require_relative './version'
5 |
6 | module PercyCapybara
7 | CLIENT_INFO = "percy-capybara/#{VERSION}".freeze
8 | ENV_INFO = "capybara/#{Capybara::VERSION} ruby/#{RUBY_VERSION}".freeze
9 |
10 | PERCY_DEBUG = ENV['PERCY_LOGLEVEL'] == 'debug'
11 | PERCY_SERVER_ADDRESS = ENV['PERCY_SERVER_ADDRESS'] || 'http://localhost:5338'
12 | PERCY_LABEL = "[\u001b[35m" + (PERCY_DEBUG ? 'percy:capybara' : 'percy') + "\u001b[39m]"
13 |
14 | private_constant :CLIENT_INFO
15 | private_constant :ENV_INFO
16 |
17 | # Take a DOM snapshot and post it to the snapshot endpoint
18 | def percy_snapshot(name, options = {})
19 | return unless percy_enabled?
20 |
21 | page = Capybara.current_session
22 |
23 | begin
24 | page.evaluate_script(fetch_percy_dom)
25 | dom_snapshot = page
26 | .evaluate_script("(function() { return PercyDOM.serialize(#{options.to_json}) })()")
27 |
28 | response = fetch('percy/snapshot',
29 | name: name,
30 | url: page.current_url,
31 | dom_snapshot: dom_snapshot,
32 | client_info: CLIENT_INFO,
33 | environment_info: ENV_INFO,
34 | **options,)
35 |
36 | unless response.body.to_json['success']
37 | raise StandardError, data['error']
38 | end
39 | rescue StandardError => e
40 | log("Could not take DOM snapshot '#{name}'")
41 |
42 | if PERCY_DEBUG then log(e) end
43 | end
44 | end
45 |
46 | # Determine if the Percy server is running, caching the result so it is only checked once
47 | private def percy_enabled?
48 | return @percy_enabled unless @percy_enabled.nil?
49 |
50 | begin
51 | response = fetch('percy/healthcheck')
52 | version = response['x-percy-core-version']
53 |
54 | if version.nil?
55 | log('You may be using @percy/agent ' \
56 | 'which is no longer supported by this SDK. ' \
57 | 'Please uninstall @percy/agent and install @percy/cli instead. ' \
58 | 'https://www.browserstack.com/docs/percy/migration/migrate-to-cli')
59 | @percy_enabled = false
60 | return false
61 | end
62 |
63 | if version.split('.')[0] != '1'
64 | log("Unsupported Percy CLI version, #{version}")
65 | @percy_enabled = false
66 | return false
67 | end
68 |
69 | @percy_enabled = true
70 | true
71 | rescue StandardError => e
72 | log('Percy is not running, disabling snapshots')
73 |
74 | if PERCY_DEBUG then log(e) end
75 | @percy_enabled = false
76 | false
77 | end
78 | end
79 |
80 | # Fetch the @percy/dom script, caching the result so it is only fetched once
81 | private def fetch_percy_dom
82 | return @percy_dom unless @percy_dom.nil?
83 |
84 | response = fetch('percy/dom.js')
85 | @percy_dom = response.body
86 | end
87 |
88 | private def log(msg)
89 | puts "#{PERCY_LABEL} #{msg}"
90 | end
91 |
92 | # Make an HTTP request (GET,POST) using Ruby's Net::HTTP. If `data` is present,
93 | # `fetch` will POST as JSON.
94 | private def fetch(url, data = nil)
95 | uri = URI("#{PERCY_SERVER_ADDRESS}/#{url}")
96 |
97 | response = if data
98 | Net::HTTP.post(uri, data.to_json)
99 | else
100 | Net::HTTP.get_response(uri)
101 | end
102 |
103 | unless response.is_a? Net::HTTPSuccess
104 | raise StandardError, "Failed with HTTP error code: #{response.code}"
105 | end
106 |
107 | response
108 | end
109 | end
110 |
111 | # Add the `percy_snapshot` method to the the Capybara session class
112 | # `page.percy_snapshot('name', { options })`
113 | Capybara::Session.class_eval { include PercyCapybara }
114 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | percy-capybara (5.0.0)
5 | capybara (>= 3)
6 |
7 | GEM
8 | remote: https://rubygems.org/
9 | specs:
10 | addressable (2.8.0)
11 | public_suffix (>= 2.0.2, < 5.0)
12 | archive-zip (0.12.0)
13 | io-like (~> 0.3.0)
14 | ast (2.4.2)
15 | capybara (3.36.0)
16 | addressable
17 | matrix
18 | mini_mime (>= 0.1.3)
19 | nokogiri (~> 1.8)
20 | rack (>= 1.6.0)
21 | rack-test (>= 0.6.3)
22 | regexp_parser (>= 1.5, < 3.0)
23 | xpath (~> 3.2)
24 | childprocess (4.1.0)
25 | coderay (1.1.3)
26 | crack (0.4.5)
27 | rexml
28 | diff-lcs (1.5.0)
29 | docile (1.4.0)
30 | ffi (1.15.5)
31 | formatador (1.1.0)
32 | geckodriver-bin (0.28.0)
33 | archive-zip (~> 0.7)
34 | guard (2.18.0)
35 | formatador (>= 0.2.4)
36 | listen (>= 2.7, < 4.0)
37 | lumberjack (>= 1.0.12, < 2.0)
38 | nenv (~> 0.1)
39 | notiffany (~> 0.0)
40 | pry (>= 0.13.0)
41 | shellany (~> 0.0)
42 | thor (>= 0.18.1)
43 | guard-compat (1.2.1)
44 | guard-rspec (4.7.3)
45 | guard (~> 2.1)
46 | guard-compat (~> 1.1)
47 | rspec (>= 2.99.0, < 4.0)
48 | hashdiff (1.0.1)
49 | io-like (0.3.1)
50 | jaro_winkler (1.5.4)
51 | listen (3.7.1)
52 | rb-fsevent (~> 0.10, >= 0.10.3)
53 | rb-inotify (~> 0.9, >= 0.9.10)
54 | lumberjack (1.2.8)
55 | matrix (0.4.2)
56 | method_source (1.0.0)
57 | mini_mime (1.1.2)
58 | mini_portile2 (2.8.0)
59 | nenv (0.3.0)
60 | nio4r (2.5.2)
61 | nokogiri (1.13.3)
62 | mini_portile2 (~> 2.8.0)
63 | racc (~> 1.4)
64 | notiffany (0.1.3)
65 | nenv (~> 0.1)
66 | shellany (~> 0.0)
67 | parallel (1.22.1)
68 | parser (3.1.1.0)
69 | ast (~> 2.4.1)
70 | percy-style (0.7.0)
71 | rubocop (~> 0.77.0)
72 | rubocop-rspec (~> 1.37.0)
73 | pry (0.14.1)
74 | coderay (~> 1.1)
75 | method_source (~> 1.0)
76 | public_suffix (4.0.6)
77 | puma (5.6.6)
78 | nio4r (~> 2.0)
79 | racc (1.6.0)
80 | rack (2.2.3)
81 | rack-test (1.1.0)
82 | rack (>= 1.0, < 3)
83 | rainbow (3.1.1)
84 | rake (13.0.6)
85 | rb-fsevent (0.11.1)
86 | rb-inotify (0.10.1)
87 | ffi (~> 1.0)
88 | regexp_parser (2.3.0)
89 | rexml (3.2.5)
90 | rspec (3.11.0)
91 | rspec-core (~> 3.11.0)
92 | rspec-expectations (~> 3.11.0)
93 | rspec-mocks (~> 3.11.0)
94 | rspec-core (3.11.0)
95 | rspec-support (~> 3.11.0)
96 | rspec-expectations (3.11.0)
97 | diff-lcs (>= 1.2.0, < 2.0)
98 | rspec-support (~> 3.11.0)
99 | rspec-mocks (3.11.1)
100 | diff-lcs (>= 1.2.0, < 2.0)
101 | rspec-support (~> 3.11.0)
102 | rspec-support (3.11.0)
103 | rubocop (0.77.0)
104 | jaro_winkler (~> 1.5.1)
105 | parallel (~> 1.10)
106 | parser (>= 2.6)
107 | rainbow (>= 2.2.2, < 4.0)
108 | ruby-progressbar (~> 1.7)
109 | unicode-display_width (>= 1.4.0, < 1.7)
110 | rubocop-rspec (1.37.1)
111 | rubocop (>= 0.68.1)
112 | ruby-progressbar (1.11.0)
113 | rubyzip (2.3.2)
114 | selenium-webdriver (4.1.0)
115 | childprocess (>= 0.5, < 5.0)
116 | rexml (~> 3.2, >= 3.2.5)
117 | rubyzip (>= 1.2.2)
118 | shellany (0.0.1)
119 | simplecov (0.21.2)
120 | docile (~> 1.1)
121 | simplecov-html (~> 0.11)
122 | simplecov_json_formatter (~> 0.1)
123 | simplecov-html (0.12.3)
124 | simplecov_json_formatter (0.1.4)
125 | thor (1.2.1)
126 | unicode-display_width (1.6.1)
127 | webmock (3.14.0)
128 | addressable (>= 2.8.0)
129 | crack (>= 0.3.2)
130 | hashdiff (>= 0.4.0, < 2.0.0)
131 | xpath (3.2.0)
132 | nokogiri (~> 1.8)
133 |
134 | PLATFORMS
135 | ruby
136 |
137 | DEPENDENCIES
138 | bundler (>= 2.0)
139 | capybara (>= 3)
140 | geckodriver-bin (~> 0.28.0)
141 | guard-rspec
142 | percy-capybara!
143 | percy-style (~> 0.7.0)
144 | pry
145 | puma (= 5.6.6)
146 | rake (~> 13.0)
147 | rspec (~> 3.5)
148 | selenium-webdriver (>= 4.0.0)
149 | simplecov
150 | webmock
151 |
152 | BUNDLED WITH
153 | 2.1.4
154 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # percy-capybara
2 | [](https://badge.fury.io/rb/percy-capybara)
3 | 
4 |
5 | [Percy](https://percy.io) visual testing for Ruby Selenium.
6 |
7 | ## Installation
8 |
9 | npm install `@percy/cli`:
10 |
11 | ```sh-session
12 | $ npm install --save-dev @percy/cli
13 | ```
14 |
15 | gem install `percy-capybara` package:
16 |
17 | ```ssh-session
18 | $ gem install percy-capybara
19 | ```
20 |
21 | ## Usage
22 |
23 | In your test setup file, require `percy/capybara`. For example if you're using
24 | rspec, you would add the following to your `spec_helper.rb` file:
25 |
26 | ``` ruby
27 | require 'percy/capybara'
28 | ```
29 |
30 | Now you can use `page.percy_snapshot` to capture snapshots.
31 |
32 | > Note: you may need to add `js: true` to your specs, depending on your driver setup
33 |
34 | ```ruby
35 | describe 'my feature, type: :feature do
36 | it 'renders the page' do
37 | visit 'https://example.com'
38 | page.percy_snapshot('Capybara snapshot')
39 | end
40 | end
41 | ```
42 |
43 | Running the test above normally will result in the following log:
44 |
45 | ```sh-session
46 | [percy] Percy is not running, disabling snapshots
47 | ```
48 |
49 | When running with [`percy
50 | exec`](https://github.com/percy/cli/tree/master/packages/cli-exec#percy-exec), and your project's
51 | `PERCY_TOKEN`, a new Percy build will be created and snapshots will be uploaded to your project.
52 |
53 | ```sh-session
54 | $ export PERCY_TOKEN=[your-project-token]
55 | $ percy exec -- [test command]
56 | [percy] Percy has started!
57 | [percy] Created build #1: https://percy.io/[your-project]
58 | [percy] Snapshot taken "Capybara example"
59 | [percy] Stopping percy...
60 | [percy] Finalized build #1: https://percy.io/[your-project]
61 | [percy] Done!
62 | ```
63 |
64 | ## Configuration
65 |
66 | `page.snapshot(name[, options])`
67 |
68 | - `name` (**required**) - The snapshot name; must be unique to each snapshot
69 | - `options` - [See per-snapshot configuration options](https://www.browserstack.com/docs/percy/take-percy-snapshots/overview#per-snapshot-configuration)
70 |
71 | ## Upgrading
72 |
73 | ### Automatically with `@percy/migrate`
74 |
75 | We built a tool to help automate migrating to the new CLI toolchain! Migrating
76 | can be done by running the following commands and following the prompts:
77 |
78 | ``` shell
79 | $ npx @percy/migrate
80 | ? Are you currently using percy-capybara? Yes
81 | ? Install @percy/cli (required to run percy)? Yes
82 | ? Migrate Percy config file? Yes
83 | ? Upgrade SDK to percy-capybara@^5.0.0? Yes
84 | ? The Capybara API has breaking changes, automatically convert to the new API? Yes
85 | ```
86 |
87 | This will automatically run the changes described below for you, with the
88 | exception of changing the `require`.
89 |
90 | ### Manually
91 |
92 | #### Require change
93 |
94 | The name of the require has changed from `require 'percy'` to `require
95 | 'percy/capybara'`. This is to avoid conflict with our [Ruby Selenium SDK's](https://github.com/percy/percy-selenium-ruby)
96 | require statement.
97 |
98 | #### API change
99 |
100 | The previous version of this SDK had the following function signature:
101 |
102 | ``` ruby
103 | Percy.snapshot(driver, name, options)
104 | ```
105 |
106 | v5.x of this SDK has a significant change to the API. There no longer is a stand
107 | alone module to call and you no longer need to pass the page/driver. It's
108 | available on the current Capybara session (`page`):
109 |
110 | ``` ruby
111 | page.percy_snapshot(name, options)
112 | ```
113 |
114 | If you were using this SDK outside of Capybara, you'll likely find the [Ruby
115 | Selenium SDK a better fit](https://github.com/percy/percy-selenium-ruby)
116 |
117 | #### Installing `@percy/cli` & removing `@percy/agent`
118 |
119 | If you're coming from a 4.x version of this package, make sure to install `@percy/cli` after
120 | upgrading to retain any existing scripts that reference the Percy CLI
121 | command. You will also want to uninstall `@percy/agent`, as it's been replaced
122 | by `@percy/cli`.
123 |
124 | ```sh-session
125 | $ npm uninstall @percy/agent
126 | $ npm install --save-dev @percy/cli
127 | ```
128 |
129 | #### Migrating config
130 |
131 | If you have a previous Percy configuration file, migrate it to the newest version with the
132 | [`config:migrate`](https://github.com/percy/cli/tree/master/packages/cli-config#percy-configmigrate-filepath-output) command:
133 |
134 | ```sh-session
135 | $ percy config:migrate
136 | ```
137 |
--------------------------------------------------------------------------------
/spec/lib/percy/percy_capybara_spec.rb:
--------------------------------------------------------------------------------
1 | LABEL = PercyCapybara::PERCY_LABEL
2 |
3 | # rubocop:disable RSpec/MultipleDescribes
4 | RSpec.describe PercyCapybara, type: :feature do
5 | before(:each) do
6 | WebMock.disable_net_connect!(allow: '127.0.0.1', disallow: 'localhost')
7 | page.__percy_clear_cache!
8 | end
9 |
10 | describe 'snapshot', type: :feature do
11 | it 'disables when healthcheck version is incorrect' do
12 | stub_request(:get, "#{PercyCapybara::PERCY_SERVER_ADDRESS}/percy/healthcheck")
13 | .to_return(status: 200, body: '', headers: {'x-percy-core-version': '0.1.0'})
14 |
15 | expect { page.percy_snapshot('Name') }
16 | .to output("#{LABEL} Unsupported Percy CLI version, 0.1.0\n").to_stdout
17 | end
18 |
19 | it 'disables when healthcheck version is missing' do
20 | stub_request(:get, "#{PercyCapybara::PERCY_SERVER_ADDRESS}/percy/healthcheck")
21 | .to_return(status: 200, body: '', headers: {})
22 |
23 | expect { page.percy_snapshot('Name') }
24 | .to output(
25 | "#{LABEL} You may be using @percy/agent which" \
26 | ' is no longer supported by this SDK. Please uninstall' \
27 | ' @percy/agent and install @percy/cli instead.' \
28 | " https://www.browserstack.com/docs/percy/migration/migrate-to-cli\n",
29 | ).to_stdout
30 | end
31 |
32 | it 'disables when healthcheck fails' do
33 | stub_request(:get, "#{PercyCapybara::PERCY_SERVER_ADDRESS}/percy/healthcheck")
34 | .to_return(status: 500, body: '', headers: {})
35 |
36 | expect { page.percy_snapshot('Name') }
37 | .to output("#{LABEL} Percy is not running, disabling snapshots\n").to_stdout
38 | end
39 |
40 | it 'disables when healthcheck fails to connect' do
41 | stub_request(:get, "#{PercyCapybara::PERCY_SERVER_ADDRESS}/percy/healthcheck")
42 | .to_raise(StandardError)
43 |
44 | expect { page.percy_snapshot('Name') }
45 | .to output("#{LABEL} Percy is not running, disabling snapshots\n").to_stdout
46 | end
47 |
48 | it 'throws an error when name is not provided' do
49 | stub_request(:get, "#{PercyCapybara::PERCY_SERVER_ADDRESS}/percy/healthcheck")
50 | .to_return(status: 500, body: '', headers: {})
51 |
52 | expect { page.percy_snapshot }.to raise_error(ArgumentError)
53 | end
54 |
55 | it 'logs an error when sending a snapshot fails' do
56 | stub_request(:get, "#{PercyCapybara::PERCY_SERVER_ADDRESS}/percy/healthcheck")
57 | .to_return(status: 200, body: '', headers: {'x-percy-core-version': '1.0.0'})
58 |
59 | stub_request(:get, "#{PercyCapybara::PERCY_SERVER_ADDRESS}/percy/dom.js")
60 | .to_return(
61 | status: 200,
62 | body: 'window.PercyDOM = { serialize: () => document.documentElement.outerHTML };',
63 | headers: {},
64 | )
65 |
66 | stub_request(:post, 'http://localhost:5338/percy/snapshot')
67 | .to_return(status: 200, body: '', headers: {})
68 |
69 | expect { page.percy_snapshot('Name') }
70 | .to output("#{LABEL} Could not take DOM snapshot 'Name'\n").to_stdout
71 | end
72 |
73 | it 'sends snapshots to the local server' do
74 | stub_request(:get, "#{PercyCapybara::PERCY_SERVER_ADDRESS}/percy/healthcheck")
75 | .to_return(status: 200, body: '', headers: {'x-percy-core-version': '1.0.0'})
76 |
77 | stub_request(:get, "#{PercyCapybara::PERCY_SERVER_ADDRESS}/percy/dom.js")
78 | .to_return(
79 | status: 200,
80 | body: 'window.PercyDOM = { serialize: () => document.documentElement.outerHTML };',
81 | headers: {},
82 | )
83 |
84 | stub_request(:post, 'http://localhost:5338/percy/snapshot')
85 | .to_return(status: 200, body: '{"success": "true" }', headers: {})
86 |
87 | visit 'index.html'
88 | page.percy_snapshot('Name', widths: [375])
89 |
90 | expect(WebMock)
91 | .to have_requested(:post, "#{PercyCapybara::PERCY_SERVER_ADDRESS}/percy/snapshot")
92 | .with(
93 | body: {
94 | name: 'Name',
95 | url: 'http://127.0.0.1:3003/index.html',
96 | dom_snapshot:
97 | "I am a pageSnapshot me\n",
98 | client_info: "percy-capybara/#{PercyCapybara::VERSION}",
99 | environment_info: "capybara/#{Capybara::VERSION} ruby/#{RUBY_VERSION}",
100 | widths: [375],
101 | }.to_json,
102 | ).once
103 | expect(page).to have_current_path('/index.html')
104 | end
105 | end
106 | end
107 |
108 | RSpec.describe PercyCapybara, type: :feature do
109 | before(:each) do
110 | WebMock.reset!
111 | WebMock.allow_net_connect!
112 | page.__percy_clear_cache!
113 | end
114 |
115 | describe 'integration', type: :feature do
116 | it 'sends snapshots to percy server' do
117 | visit 'index.html'
118 | page.percy_snapshot('Name', widths: [375])
119 | sleep 5 # wait for percy server to process
120 | resp = Net::HTTP.get_response(URI("#{PercyCapybara::PERCY_SERVER_ADDRESS}/test/requests"))
121 | requests = JSON.parse(resp.body)['requests']
122 | healthcheck = requests[0]
123 | expect(healthcheck['url']).to eq('/percy/healthcheck')
124 |
125 | snap = requests[2]['body']
126 | expect(snap['name']).to eq('Name')
127 | expect(snap['url']).to eq('http://127.0.0.1:3003/index.html')
128 | expect(snap['client_info']).to include('percy-capybara')
129 | expect(snap['environment_info']).to include('capybara')
130 | expect(snap['widths']).to eq([375])
131 | end
132 | end
133 | end
134 | # rubocop:enable RSpec/MultipleDescribes
135 |
--------------------------------------------------------------------------------