percy-capybara 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +11 -30
- data/.rspec +4 -0
- data/.travis.yml +7 -0
- data/.yardopts +2 -0
- data/Gemfile +6 -1
- data/Guardfile +14 -0
- data/LICENSE +2 -2
- data/README.md +34 -2
- data/Rakefile +1 -1
- data/lib/percy/capybara.rb +47 -0
- data/lib/percy/capybara/client.rb +20 -0
- data/lib/percy/capybara/client/builds.rb +24 -0
- data/lib/percy/capybara/client/snapshots.rb +147 -0
- data/lib/percy/capybara/version.rb +5 -0
- data/percy-capybara.gemspec +31 -0
- data/spec/lib/percy/capybara/client/builds_spec.rb +42 -0
- data/spec/lib/percy/capybara/client/snapshots_spec.rb +147 -0
- data/spec/lib/percy/capybara/client/testdata/css/base.css +1 -0
- data/spec/lib/percy/capybara/client/testdata/css/imports.css +1 -0
- data/spec/lib/percy/capybara/client/testdata/css/level0-imports.css +2 -0
- data/spec/lib/percy/capybara/client/testdata/css/level1-imports.css +2 -0
- data/spec/lib/percy/capybara/client/testdata/css/level2-imports.css +1 -0
- data/spec/lib/percy/capybara/client/testdata/css/simple-imports.css +1 -0
- data/spec/lib/percy/capybara/client/testdata/index.html +1 -0
- data/spec/lib/percy/capybara/client/testdata/test-css.html +18 -0
- data/spec/lib/percy/capybara_spec.rb +49 -0
- data/spec/spec_helper.rb +41 -0
- data/spec/support/test_helpers.rb +27 -0
- metadata +141 -9
- data/lib/perceptual.rb +0 -5
- data/lib/perceptual/version.rb +0 -3
- data/perceptual.gemspec +0 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 19a9bfb3b76169ea2b1ba301f69e6466d2da4cef
|
4
|
+
data.tar.gz: 1f1963b73fa8ce3df14d48c34003cc631b5aa4a0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cccbace3dee7650f246cd96736e620719428ba22eaa88cec41f7aafa3564901d6af0271e77247a3a2b4f8f2758efb2e570bf33b9ea6f409138bb048af8e967b9
|
7
|
+
data.tar.gz: 99c6891ee227853322830ebff8648df6dbf7bc5fc11488151ed43c9e6a9065948c16faee2e82c8e9ad47d7f87863801d5a594c547e12ac120fba96cd5b4d4924
|
data/.gitignore
CHANGED
@@ -1,35 +1,16 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
/.
|
1
|
+
/.bundle/
|
2
|
+
/Gemfile.lock
|
3
|
+
/.yardoc
|
4
|
+
/_yardoc/
|
4
5
|
/coverage/
|
5
|
-
/
|
6
|
+
/doc/
|
6
7
|
/pkg/
|
7
8
|
/spec/reports/
|
8
|
-
/test/tmp/
|
9
|
-
/test/version_tmp/
|
10
9
|
/tmp/
|
10
|
+
*.bundle
|
11
|
+
*.so
|
12
|
+
*.o
|
13
|
+
*.a
|
14
|
+
*.rbc
|
15
|
+
mkmf.log
|
11
16
|
.DS_Store
|
12
|
-
|
13
|
-
## Specific to RubyMotion:
|
14
|
-
.dat*
|
15
|
-
.repl_history
|
16
|
-
build/
|
17
|
-
|
18
|
-
## Documentation cache and generated files:
|
19
|
-
/.yardoc/
|
20
|
-
/_yardoc/
|
21
|
-
/doc/
|
22
|
-
/rdoc/
|
23
|
-
|
24
|
-
## Environment normalisation:
|
25
|
-
/.bundle/
|
26
|
-
/lib/bundler/man/
|
27
|
-
|
28
|
-
# for a library or gem, you might want to ignore these files since the code is
|
29
|
-
# intended to run in multiple environments; otherwise, check them in:
|
30
|
-
# Gemfile.lock
|
31
|
-
# .ruby-version
|
32
|
-
# .ruby-gemset
|
33
|
-
|
34
|
-
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
35
|
-
.rvmrc
|
data/.rspec
ADDED
data/.travis.yml
ADDED
data/.yardopts
ADDED
data/Gemfile
CHANGED
@@ -1,4 +1,9 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
2
|
|
3
|
-
# Specify your gem's dependencies in
|
3
|
+
# Specify your gem's dependencies in percy-capybara.gemspec
|
4
4
|
gemspec
|
5
|
+
|
6
|
+
gem 'guard-rspec', require: false
|
7
|
+
|
8
|
+
# (for development)
|
9
|
+
# gem 'percy-client', path: '~/src/percy-client'
|
data/Guardfile
ADDED
@@ -0,0 +1,14 @@
|
|
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
|
data/LICENSE
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
|
1
|
+
Copyright (c) 2015 Perceptual Inc.
|
2
2
|
|
3
|
-
|
3
|
+
The MIT License (MIT)
|
4
4
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
data/README.md
CHANGED
@@ -1,2 +1,34 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# Percy::Capybara
|
2
|
+
|
3
|
+
[](https://travis-ci.org/percy/percy-capybara)
|
4
|
+
[](http://badge.fury.io/rb/percy-capybara)
|
5
|
+
|
6
|
+
(in development, coming soon)
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Add this line to your application's Gemfile:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
gem 'percy-capybara'
|
14
|
+
```
|
15
|
+
|
16
|
+
And then execute:
|
17
|
+
|
18
|
+
$ bundle
|
19
|
+
|
20
|
+
Or install it yourself as:
|
21
|
+
|
22
|
+
$ gem install percy-capybara
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
...
|
27
|
+
|
28
|
+
## Contributing
|
29
|
+
|
30
|
+
1. Fork it ( https://github.com/percy/percy-capybara/fork )
|
31
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
32
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
33
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
34
|
+
5. Create a new Pull Request
|
data/Rakefile
CHANGED
@@ -1,2 +1,2 @@
|
|
1
|
-
require
|
1
|
+
require 'bundler/gem_tasks'
|
2
2
|
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'percy'
|
2
|
+
require 'percy/capybara/version'
|
3
|
+
require 'percy/capybara/client'
|
4
|
+
|
5
|
+
module Percy
|
6
|
+
module Capybara
|
7
|
+
# @see Percy::Capybara::Client
|
8
|
+
def self.capybara_client
|
9
|
+
@capybara_client ||= Percy::Capybara::Client.new
|
10
|
+
end
|
11
|
+
|
12
|
+
# {include:Percy::Capybara::Client::Snapshots#snapshot}
|
13
|
+
# @param (see Percy::Capybara::Client::Snapshots#snapshot)
|
14
|
+
# @option (see Percy::Capybara::Client::Snapshots#snapshot)
|
15
|
+
# @see Percy::Capybara::Client::Snapshots#snapshot
|
16
|
+
def self.snapshot(page, options = {})
|
17
|
+
capybara_client.snapshot(page, options)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Creates a new build.
|
21
|
+
#
|
22
|
+
# This usually does not need to be called explictly because the build is automatically created
|
23
|
+
# the first time a snapshot is created. However, this method might be useful in situations like
|
24
|
+
# multi-process tests where a single build must be created before forking.
|
25
|
+
#
|
26
|
+
# @see Percy::Capybara::Client::Builds#initialize_build
|
27
|
+
def self.initialize_build
|
28
|
+
capybara_client.initialize_build
|
29
|
+
end
|
30
|
+
|
31
|
+
# Finalize the current build.
|
32
|
+
#
|
33
|
+
# This must be called to indicate that the build is complete after all snapshots have been
|
34
|
+
# taken. It will silently return if no build or snapshots were created.
|
35
|
+
#
|
36
|
+
# @see Percy::Capybara::Client::Builds#finalize_current_build
|
37
|
+
def self.finalize_build
|
38
|
+
return if !capybara_client.build_initialized?
|
39
|
+
capybara_client.finalize_current_build
|
40
|
+
end
|
41
|
+
|
42
|
+
# Reset the global Percy::Capybara module state.
|
43
|
+
def self.reset
|
44
|
+
@capybara_client = nil
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'percy/capybara/client/builds'
|
2
|
+
require 'percy/capybara/client/snapshots'
|
3
|
+
|
4
|
+
module Percy
|
5
|
+
module Capybara
|
6
|
+
class Client
|
7
|
+
include Percy::Capybara::Client::Builds
|
8
|
+
include Percy::Capybara::Client::Snapshots
|
9
|
+
|
10
|
+
class Error < Exception; end
|
11
|
+
class BuildNotInitializedError < Error; end
|
12
|
+
|
13
|
+
attr_reader :client
|
14
|
+
|
15
|
+
def initialize(options = {})
|
16
|
+
@client = options[:client] || Percy.client
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Percy
|
2
|
+
module Capybara
|
3
|
+
class Client
|
4
|
+
module Builds
|
5
|
+
def current_build
|
6
|
+
@current_build ||= client.create_build(client.config.repo)
|
7
|
+
end
|
8
|
+
alias_method :initialize_build, :current_build
|
9
|
+
|
10
|
+
def build_initialized?
|
11
|
+
!!@current_build
|
12
|
+
end
|
13
|
+
|
14
|
+
def finalize_current_build
|
15
|
+
if !build_initialized?
|
16
|
+
raise Percy::Capybara::Client::BuildNotInitializedError.new(
|
17
|
+
'Failed to finalize build because no build has been initialized.')
|
18
|
+
end
|
19
|
+
client.finalize_build(current_build['data']['id'])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'httpclient'
|
3
|
+
require 'digest'
|
4
|
+
|
5
|
+
module Percy
|
6
|
+
module Capybara
|
7
|
+
class Client
|
8
|
+
module Snapshots
|
9
|
+
# @private
|
10
|
+
FETCH_SENTINEL_VALUE = '[[FETCH]]'
|
11
|
+
|
12
|
+
# Takes a snapshot of the given page HTML and its assets.
|
13
|
+
#
|
14
|
+
# @param [Capybara::Session] page The Capybara page to snapshot.
|
15
|
+
# @param [Hash] options
|
16
|
+
# @option options [String] :name A unique name for the current page that identifies it across
|
17
|
+
# builds. By default this is the URL of the page, but can be customized if the URL does not
|
18
|
+
# entirely identify the current state.
|
19
|
+
def snapshot(page, options = {})
|
20
|
+
name = options[:name]
|
21
|
+
current_build_id = current_build['data']['id']
|
22
|
+
resource_map = _find_resources(page)
|
23
|
+
snapshot = client.create_snapshot(current_build_id, resource_map.values, name: name)
|
24
|
+
|
25
|
+
# Upload the content for any missing resources.
|
26
|
+
snapshot['data']['links']['missing-resources']['linkage'].each do |missing_resource|
|
27
|
+
sha = missing_resource['id']
|
28
|
+
client.upload_resource(current_build_id, resource_map[sha].content)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# @private
|
33
|
+
def _find_resources(page)
|
34
|
+
resource_map = {}
|
35
|
+
resources = []
|
36
|
+
resources << _get_root_html_resource(page)
|
37
|
+
resources += _get_css_resources(page)
|
38
|
+
resources.each { |resource| resource_map[resource.sha] = resource }
|
39
|
+
resource_map
|
40
|
+
end
|
41
|
+
private :_find_resources
|
42
|
+
|
43
|
+
# @private
|
44
|
+
def _get_root_html_resource(page)
|
45
|
+
# Primary HTML.
|
46
|
+
script = <<-JS
|
47
|
+
var htmlElement = document.getElementsByTagName('html')[0];
|
48
|
+
return htmlElement.outerHTML;
|
49
|
+
JS
|
50
|
+
html = _evaluate_script(page, script)
|
51
|
+
sha = Digest::SHA256.hexdigest(html)
|
52
|
+
resource_url = page.current_url
|
53
|
+
Percy::Client::Resource.new(
|
54
|
+
sha, resource_url, is_root: true, mimetype: 'text/html', content: html)
|
55
|
+
end
|
56
|
+
private :_get_root_html_resource
|
57
|
+
|
58
|
+
# @private
|
59
|
+
def _get_css_resources(page)
|
60
|
+
resources = []
|
61
|
+
# Find all CSS resources.
|
62
|
+
# http://www.quirksmode.org/dom/w3c_css.html#access
|
63
|
+
script = <<-JS
|
64
|
+
function findStylesRecursively(stylesheet, result_data) {
|
65
|
+
result_data = result_data || {};
|
66
|
+
if (stylesheet.href) {
|
67
|
+
result_data[stylesheet.href] = result_data[stylesheet.href] || '';
|
68
|
+
|
69
|
+
// Remote stylesheet rules cannot be accessed because of the same-origin policy.
|
70
|
+
// Unfortunately, if you touch .cssRules in Selenium, it throws a JavascriptError
|
71
|
+
// with 'The operation is insecure'. To work around this, skip any remote stylesheets
|
72
|
+
// and mark them with a sentinel value so we can fetch them later.
|
73
|
+
var parser = document.createElement('a');
|
74
|
+
parser.href = stylesheet.href;
|
75
|
+
if (parser.host != window.location.host) {
|
76
|
+
result_data[stylesheet.href] = '#{FETCH_SENTINEL_VALUE}'; // Must be a string.
|
77
|
+
return;
|
78
|
+
}
|
79
|
+
}
|
80
|
+
|
81
|
+
for (var i = 0; i < stylesheet.cssRules.length; i++) {
|
82
|
+
var rule = stylesheet.cssRules[i];
|
83
|
+
// Skip stylesheet without hrefs (inline stylesheets).
|
84
|
+
// These will be present in the HTML snapshot.
|
85
|
+
if (stylesheet.href) {
|
86
|
+
// Append current rule text.
|
87
|
+
result_data[stylesheet.href] += rule.cssText + '\\n';
|
88
|
+
}
|
89
|
+
|
90
|
+
// Handle recursive @imports.
|
91
|
+
if (rule.styleSheet) {
|
92
|
+
findStylesRecursively(rule.styleSheet, result_data);
|
93
|
+
}
|
94
|
+
}
|
95
|
+
}
|
96
|
+
|
97
|
+
var percy_resources = {};
|
98
|
+
for (var i = 0; i < document.styleSheets.length; i++) {
|
99
|
+
findStylesRecursively(document.styleSheets[i], percy_resources);
|
100
|
+
}
|
101
|
+
return percy_resources;
|
102
|
+
JS
|
103
|
+
|
104
|
+
# Returned datastructure: {"<absolute URL>" => "<CSS text>", ...}
|
105
|
+
resource_data = _evaluate_script(page, script)
|
106
|
+
|
107
|
+
resource_data.each do |resource_url, css|
|
108
|
+
if css == FETCH_SENTINEL_VALUE
|
109
|
+
# Handle sentinel value that indicates a remote CSS resource that must be fetched.
|
110
|
+
response = _fetch_resource_url(resource_url)
|
111
|
+
next if !response
|
112
|
+
css = response.body
|
113
|
+
end
|
114
|
+
|
115
|
+
sha = Digest::SHA256.hexdigest(css)
|
116
|
+
resources << Percy::Client::Resource.new(
|
117
|
+
sha, resource_url, mimetype: 'text/css', content: css)
|
118
|
+
end
|
119
|
+
resources
|
120
|
+
end
|
121
|
+
private :_get_css_resources
|
122
|
+
|
123
|
+
# @private
|
124
|
+
def _fetch_resource_url(url)
|
125
|
+
response = Faraday.get(url)
|
126
|
+
content = response.body
|
127
|
+
if response.status != 200
|
128
|
+
STDERR.puts "[percy] Warning: failed to fetch page resource, this might be a bug: #{url}"
|
129
|
+
return nil
|
130
|
+
end
|
131
|
+
response
|
132
|
+
end
|
133
|
+
|
134
|
+
# @private
|
135
|
+
def _evaluate_script(page, script)
|
136
|
+
script = <<-JS
|
137
|
+
(function() {
|
138
|
+
#{script}
|
139
|
+
})();
|
140
|
+
JS
|
141
|
+
page.evaluate_script(script)
|
142
|
+
end
|
143
|
+
private :_evaluate_script
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'percy/capybara/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'percy-capybara'
|
8
|
+
spec.version = Percy::Capybara::VERSION
|
9
|
+
spec.authors = ['Perceptual Inc.']
|
10
|
+
spec.email = ['team@percy.io']
|
11
|
+
spec.summary = %q{Percy::Capybara}
|
12
|
+
spec.description = %q{}
|
13
|
+
spec.homepage = ''
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_dependency 'percy-client', '~> 0.1'
|
22
|
+
spec.add_dependency 'faraday'
|
23
|
+
|
24
|
+
spec.add_development_dependency 'bundler', '~> 1.7'
|
25
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
26
|
+
spec.add_development_dependency 'rspec', '~> 3.2'
|
27
|
+
spec.add_development_dependency 'capybara', '~> 2.4'
|
28
|
+
spec.add_development_dependency 'capybara-webkit', '~> 1.5'
|
29
|
+
spec.add_development_dependency 'selenium-webdriver'
|
30
|
+
spec.add_development_dependency 'webmock', '~> 1'
|
31
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
RSpec.describe Percy::Capybara::Client::Builds do
|
2
|
+
let(:capybara_client) { Percy::Capybara::Client.new }
|
3
|
+
|
4
|
+
describe '#current_build' do
|
5
|
+
it 'returns the current build or creates a new one' do
|
6
|
+
mock_double = double('build')
|
7
|
+
expect(capybara_client.client).to receive(:create_build)
|
8
|
+
.with(capybara_client.client.config.repo)
|
9
|
+
.and_return(mock_double)
|
10
|
+
.once
|
11
|
+
|
12
|
+
current_build = capybara_client.current_build
|
13
|
+
expect(current_build).to eq(mock_double)
|
14
|
+
# Verify that it memoizes the current build by calling it again:
|
15
|
+
expect(current_build).to eq(mock_double)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
describe '#build_initialized?' do
|
19
|
+
it 'is false before a build is initialized and true afterward' do
|
20
|
+
expect(capybara_client.client).to receive(:create_build).and_return(double('build'))
|
21
|
+
expect(capybara_client.build_initialized?).to be_falsey
|
22
|
+
|
23
|
+
capybara_client.initialize_build
|
24
|
+
expect(capybara_client.build_initialized?).to be_truthy
|
25
|
+
end
|
26
|
+
end
|
27
|
+
describe '#finalize_current_build' do
|
28
|
+
it 'finalizes the current build' do
|
29
|
+
build_data = {'data' => {'id' => 123}}
|
30
|
+
expect(capybara_client.client).to receive(:create_build).and_return(build_data)
|
31
|
+
capybara_client.initialize_build
|
32
|
+
|
33
|
+
expect(capybara_client.client).to receive(:finalize_build).with(123)
|
34
|
+
capybara_client.finalize_current_build
|
35
|
+
end
|
36
|
+
it 'raises an error if no current build exists' do
|
37
|
+
expect do
|
38
|
+
capybara_client.finalize_current_build
|
39
|
+
end.to raise_error(Percy::Capybara::Client::BuildNotInitializedError)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'digest'
|
3
|
+
|
4
|
+
RSpec.describe Percy::Capybara::Client::Snapshots do
|
5
|
+
let(:capybara_client) { Percy::Capybara::Client.new }
|
6
|
+
|
7
|
+
# Start a temp webserver that serves the testdata directory.
|
8
|
+
# You can test this server manually by running:
|
9
|
+
# ruby -run -e httpd spec/lib/percy/capybara/testdata -p 9090
|
10
|
+
before(:all) do
|
11
|
+
port = get_random_open_port
|
12
|
+
Capybara.app_host = "http://localhost:#{port}"
|
13
|
+
Capybara.run_server = false
|
14
|
+
|
15
|
+
# Note: using this form of popen to keep stdout and stderr silent and captured.
|
16
|
+
dir = File.expand_path('../testdata/', __FILE__)
|
17
|
+
@process = IO.popen([
|
18
|
+
'ruby', '-run', '-e', 'httpd', dir, '-p', port.to_s, err: [:child, :out]
|
19
|
+
].flatten)
|
20
|
+
|
21
|
+
# Block until the server is up.
|
22
|
+
verify_server_up(Capybara.app_host)
|
23
|
+
end
|
24
|
+
after(:all) { Process.kill('INT', @process.pid) }
|
25
|
+
|
26
|
+
describe '#_get_root_html_resource', type: :feature, js: true do
|
27
|
+
it 'includes the root DOM HTML' do
|
28
|
+
visit '/'
|
29
|
+
resource = capybara_client.send(:_get_root_html_resource, page)
|
30
|
+
|
31
|
+
expect(resource.is_root).to be_truthy
|
32
|
+
expect(resource.mimetype).to eq('text/html')
|
33
|
+
expect(resource.resource_url).to match(/http:\/\/localhost:\d+\//)
|
34
|
+
expect(resource.content).to include('Hello World!')
|
35
|
+
expect(resource.sha).to eq(Digest::SHA256.hexdigest(resource.content))
|
36
|
+
end
|
37
|
+
end
|
38
|
+
describe '#_get_css_resources', type: :feature, js: true do
|
39
|
+
it 'includes all linked and imported stylesheets' do
|
40
|
+
# For capybara-webkit.
|
41
|
+
page.driver.respond_to?(:allow_url) && page.driver.allow_url('maxcdn.bootstrapcdn.com')
|
42
|
+
|
43
|
+
visit '/test-css.html'
|
44
|
+
resources = capybara_client.send(:_get_css_resources, page)
|
45
|
+
|
46
|
+
expect(resources.length).to eq(7)
|
47
|
+
expect(resources.collect(&:mimetype).uniq).to eq(['text/css'])
|
48
|
+
|
49
|
+
resource = resources.select do |resource|
|
50
|
+
resource.resource_url.match(/http:\/\/localhost:\d+\/css\/base\.css/)
|
51
|
+
end.fetch(0)
|
52
|
+
expect(resource.is_root).to be_falsey
|
53
|
+
|
54
|
+
expect(resource.content).to include('.colored-by-base { color: red; }')
|
55
|
+
expect(resource.sha).to eq(Digest::SHA256.hexdigest(resource.content))
|
56
|
+
|
57
|
+
resource = resources.select do |resource|
|
58
|
+
resource.resource_url.match(/http:\/\/localhost:\d+\/css\/simple-imports\.css/)
|
59
|
+
end.fetch(0)
|
60
|
+
expect(resource.is_root).to be_falsey
|
61
|
+
expect(resource.content).to include('@import url("imports.css")')
|
62
|
+
expect(resource.sha).to eq(Digest::SHA256.hexdigest(resource.content))
|
63
|
+
|
64
|
+
resource = resources.select do |resource|
|
65
|
+
resource.resource_url.match(/http:\/\/localhost:\d+\/css\/imports\.css/)
|
66
|
+
end.fetch(0)
|
67
|
+
expect(resource.is_root).to be_falsey
|
68
|
+
expect(resource.content).to include('.colored-by-imports { color: red; }')
|
69
|
+
expect(resource.sha).to eq(Digest::SHA256.hexdigest(resource.content))
|
70
|
+
|
71
|
+
resource = resources.select do |resource|
|
72
|
+
resource.resource_url.match(/http:\/\/localhost:\d+\/css\/level0-imports\.css/)
|
73
|
+
end.fetch(0)
|
74
|
+
expect(resource.is_root).to be_falsey
|
75
|
+
expect(resource.content).to include('@import url("level1-imports.css")')
|
76
|
+
expect(resource.content).to include('.colored-by-level0-imports { color: red; }')
|
77
|
+
expect(resource.sha).to eq(Digest::SHA256.hexdigest(resource.content))
|
78
|
+
|
79
|
+
resource = resources.select do |resource|
|
80
|
+
resource.resource_url.match(/http:\/\/localhost:\d+\/css\/level1-imports\.css/)
|
81
|
+
end.fetch(0)
|
82
|
+
expect(resource.is_root).to be_falsey
|
83
|
+
expect(resource.content).to include('@import url("level2-imports.css")')
|
84
|
+
expect(resource.content).to include('.colored-by-level1-imports { color: red; }')
|
85
|
+
expect(resource.sha).to eq(Digest::SHA256.hexdigest(resource.content))
|
86
|
+
|
87
|
+
resource = resources.select do |resource|
|
88
|
+
resource.resource_url.match(/http:\/\/localhost:\d+\/css\/level2-imports\.css/)
|
89
|
+
end.fetch(0)
|
90
|
+
expect(resource.is_root).to be_falsey
|
91
|
+
expect(resource.content).to include(".colored-by-level2-imports { color: red; }")
|
92
|
+
expect(resource.sha).to eq(Digest::SHA256.hexdigest(resource.content))
|
93
|
+
|
94
|
+
resource = resources.select do |resource|
|
95
|
+
resource.resource_url == (
|
96
|
+
'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css')
|
97
|
+
end.fetch(0)
|
98
|
+
expect(resource.is_root).to be_falsey
|
99
|
+
expect(resource.content).to include('Bootstrap v3.3.4 (http://getbootstrap.com)')
|
100
|
+
expect(resource.sha).to eq(Digest::SHA256.hexdigest(resource.content))
|
101
|
+
end
|
102
|
+
end
|
103
|
+
describe '#snapshot', type: :feature, js: true do
|
104
|
+
context 'simple page with no resources' do
|
105
|
+
let(:content) { '<html><body>Hello World!</body><head></head></html>' }
|
106
|
+
|
107
|
+
it 'creates a snapshot and uploads missing resource' do
|
108
|
+
visit '/'
|
109
|
+
|
110
|
+
mock_response = {
|
111
|
+
'data' => {
|
112
|
+
'id' => '123',
|
113
|
+
'type' => 'builds',
|
114
|
+
},
|
115
|
+
}
|
116
|
+
stub_request(:post, 'https://percy.io/api/v1/repos/percy/percy-capybara/builds/')
|
117
|
+
.to_return(status: 201, body: mock_response.to_json)
|
118
|
+
|
119
|
+
resource = capybara_client.send(:_get_root_html_resource, page)
|
120
|
+
mock_response = {
|
121
|
+
'data' => {
|
122
|
+
'id' => '256',
|
123
|
+
'type' => 'snapshots',
|
124
|
+
'links' => {
|
125
|
+
'self' => "/api/v1/snapshots/123",
|
126
|
+
'missing-resources' => {
|
127
|
+
'linkage' => [
|
128
|
+
{
|
129
|
+
'type' => 'resources',
|
130
|
+
'id' => resource.sha,
|
131
|
+
},
|
132
|
+
],
|
133
|
+
},
|
134
|
+
},
|
135
|
+
},
|
136
|
+
}
|
137
|
+
stub_request(:post, 'https://percy.io/api/v1/builds/123/snapshots/')
|
138
|
+
.to_return(status: 201, body: mock_response.to_json)
|
139
|
+
|
140
|
+
stub_request(:post, "https://percy.io/api/v1/builds/123/resources/")
|
141
|
+
.with(body: /#{resource.sha}/).to_return(status: 201, body: {success: true}.to_json)
|
142
|
+
|
143
|
+
resource_map = capybara_client.snapshot(page)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
.colored-by-base { color: red; }
|
@@ -0,0 +1 @@
|
|
1
|
+
.colored-by-imports { color: red; }
|
@@ -0,0 +1 @@
|
|
1
|
+
.colored-by-level2-imports { color: red; }
|
@@ -0,0 +1 @@
|
|
1
|
+
@import url('imports.css');
|
@@ -0,0 +1 @@
|
|
1
|
+
<html><body>Hello World!</body><head></head></html>
|
@@ -0,0 +1,18 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<title>Test Percy::Capybara</title>
|
4
|
+
<link href="../css/base.css" rel="stylesheet" type="text/css">
|
5
|
+
<link href="/css/simple-imports.css" rel="stylesheet" type="text/css">
|
6
|
+
<link href="/css/level0-imports.css" rel="stylesheet" type="text/css">
|
7
|
+
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css" rel="stylesheet" type="text/css">
|
8
|
+
<style>
|
9
|
+
.colored-by-inline { color: red; }
|
10
|
+
</style>
|
11
|
+
<h1>Hello World!</h1>
|
12
|
+
<div class="colored-by-inline">This should be red</div>
|
13
|
+
<div class="colored-by-base">This should be red</div>
|
14
|
+
<div class="colored-by-imports">This should be red</div>
|
15
|
+
<div class="colored-by-level0-imports">This should be red</div>
|
16
|
+
<div class="colored-by-level1-imports">This should be red</div>
|
17
|
+
<div class="colored-by-level2-imports">This should be red</div>
|
18
|
+
</html>
|
@@ -0,0 +1,49 @@
|
|
1
|
+
RSpec.describe Percy::Capybara do
|
2
|
+
before(:each) { Percy::Capybara.reset }
|
3
|
+
|
4
|
+
describe '#capybara_client' do
|
5
|
+
it 'returns the current client or creates a new one' do
|
6
|
+
capybara_client = Percy::Capybara.capybara_client
|
7
|
+
expect(capybara_client).to be
|
8
|
+
# Verify that it memoizes the current object by calling it again:
|
9
|
+
expect(Percy::Capybara.capybara_client).to eq(capybara_client)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
describe '#snapshot' do
|
13
|
+
it 'passes all arguments through to the current capybara_client' do
|
14
|
+
mock_page = double('page')
|
15
|
+
capybara_client = Percy::Capybara.capybara_client
|
16
|
+
expect(capybara_client).to receive(:snapshot).with(mock_page, {}).once
|
17
|
+
Percy::Capybara.snapshot(mock_page)
|
18
|
+
expect(capybara_client).to receive(:snapshot).with(mock_page, name: '/foo.html (modal)').once
|
19
|
+
Percy::Capybara.snapshot(mock_page, name: '/foo.html (modal)')
|
20
|
+
end
|
21
|
+
end
|
22
|
+
describe '#snapshot' do
|
23
|
+
it 'delgates to Percy::Capybara::Client' do
|
24
|
+
capybara_client = Percy::Capybara.capybara_client
|
25
|
+
expect(capybara_client).to receive(:initialize_build).once
|
26
|
+
Percy::Capybara.initialize_build
|
27
|
+
end
|
28
|
+
end
|
29
|
+
describe '#finalize_build' do
|
30
|
+
it 'returns silently if no build is initialized' do
|
31
|
+
expect { Percy::Capybara.finalize_build }.to_not raise_error
|
32
|
+
end
|
33
|
+
it 'delgates to Percy::Capybara::Client' do
|
34
|
+
capybara_client = Percy::Capybara.capybara_client
|
35
|
+
build_data = {'data' => {'id' => 123}}
|
36
|
+
expect(capybara_client.client).to receive(:create_build).and_return(build_data).once
|
37
|
+
Percy::Capybara.initialize_build
|
38
|
+
expect(capybara_client).to receive(:finalize_current_build).once
|
39
|
+
Percy::Capybara.finalize_build
|
40
|
+
end
|
41
|
+
end
|
42
|
+
describe '#reset' do
|
43
|
+
it 'clears the current capybara_client' do
|
44
|
+
capybara_client = Percy::Capybara.capybara_client
|
45
|
+
Percy::Capybara.reset
|
46
|
+
expect(Percy::Capybara.capybara_client).to_not eq(capybara_client)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'capybara/rspec'
|
2
|
+
require 'capybara/webkit'
|
3
|
+
require 'webmock/rspec'
|
4
|
+
require 'support/test_helpers'
|
5
|
+
require 'percy'
|
6
|
+
require 'percy/capybara'
|
7
|
+
|
8
|
+
RSpec.configure do |config|
|
9
|
+
config.include TestHelpers
|
10
|
+
|
11
|
+
config.expect_with :rspec do |expectations|
|
12
|
+
# This option will default to `true` in RSpec 4.
|
13
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
14
|
+
end
|
15
|
+
|
16
|
+
config.mock_with :rspec do |mocks|
|
17
|
+
mocks.verify_partial_doubles = true
|
18
|
+
end
|
19
|
+
|
20
|
+
config.disable_monkey_patching!
|
21
|
+
# config.warnings = true
|
22
|
+
|
23
|
+
# Run specs in random order to surface order dependencies. If you find an
|
24
|
+
# order dependency and want to debug it, you can fix the order by providing
|
25
|
+
# the seed, which is printed after each run.
|
26
|
+
# --seed 1234
|
27
|
+
config.order = :random
|
28
|
+
|
29
|
+
# Seed global randomization in this process using the `--seed` CLI option.
|
30
|
+
# Setting this allows you to use `--seed` to deterministically reproduce
|
31
|
+
# test failures related to randomization by passing the same `--seed` value
|
32
|
+
# as the one that triggered the failure.
|
33
|
+
Kernel.srand config.seed
|
34
|
+
|
35
|
+
# Comment this out to test the default Selenium/Firefox flow:
|
36
|
+
Capybara.javascript_driver = :webkit
|
37
|
+
|
38
|
+
config.before(:all) do
|
39
|
+
WebMock.disable_net_connect!(allow_localhost: true, allow: [/maxcdn.bootstrapcdn.com/])
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'timeout'
|
3
|
+
|
4
|
+
module TestHelpers
|
5
|
+
class ServerDown < Exception; end
|
6
|
+
|
7
|
+
def get_random_open_port
|
8
|
+
# Using a port of "0" relies on the system to pick an open port.
|
9
|
+
server = TCPServer.new('127.0.0.1', 0)
|
10
|
+
port = server.addr[1]
|
11
|
+
server.close
|
12
|
+
port
|
13
|
+
end
|
14
|
+
|
15
|
+
def verify_server_up(host)
|
16
|
+
http = HTTPClient.new
|
17
|
+
4.times do
|
18
|
+
begin
|
19
|
+
http.get(host)
|
20
|
+
return true
|
21
|
+
rescue Errno::ECONNREFUSED
|
22
|
+
sleep 0.5
|
23
|
+
end
|
24
|
+
end
|
25
|
+
raise ServerDown, "Server failed to start: #{host}"
|
26
|
+
end
|
27
|
+
end
|
metadata
CHANGED
@@ -1,15 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: percy-capybara
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
-
|
7
|
+
- Perceptual Inc.
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-05-
|
11
|
+
date: 2015-05-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: percy-client
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: faraday
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
13
41
|
- !ruby/object:Gem::Dependency
|
14
42
|
name: bundler
|
15
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -38,21 +66,111 @@ dependencies:
|
|
38
66
|
- - "~>"
|
39
67
|
- !ruby/object:Gem::Version
|
40
68
|
version: '10.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.2'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.2'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: capybara
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '2.4'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '2.4'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: capybara-webkit
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '1.5'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '1.5'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: selenium-webdriver
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: webmock
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '1'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '1'
|
41
139
|
description: ''
|
42
140
|
email:
|
43
|
-
-
|
141
|
+
- team@percy.io
|
44
142
|
executables: []
|
45
143
|
extensions: []
|
46
144
|
extra_rdoc_files: []
|
47
145
|
files:
|
48
146
|
- ".gitignore"
|
147
|
+
- ".rspec"
|
148
|
+
- ".travis.yml"
|
149
|
+
- ".yardopts"
|
49
150
|
- Gemfile
|
151
|
+
- Guardfile
|
50
152
|
- LICENSE
|
51
153
|
- README.md
|
52
154
|
- Rakefile
|
53
|
-
- lib/
|
54
|
-
- lib/
|
55
|
-
-
|
155
|
+
- lib/percy/capybara.rb
|
156
|
+
- lib/percy/capybara/client.rb
|
157
|
+
- lib/percy/capybara/client/builds.rb
|
158
|
+
- lib/percy/capybara/client/snapshots.rb
|
159
|
+
- lib/percy/capybara/version.rb
|
160
|
+
- percy-capybara.gemspec
|
161
|
+
- spec/lib/percy/capybara/client/builds_spec.rb
|
162
|
+
- spec/lib/percy/capybara/client/snapshots_spec.rb
|
163
|
+
- spec/lib/percy/capybara/client/testdata/css/base.css
|
164
|
+
- spec/lib/percy/capybara/client/testdata/css/imports.css
|
165
|
+
- spec/lib/percy/capybara/client/testdata/css/level0-imports.css
|
166
|
+
- spec/lib/percy/capybara/client/testdata/css/level1-imports.css
|
167
|
+
- spec/lib/percy/capybara/client/testdata/css/level2-imports.css
|
168
|
+
- spec/lib/percy/capybara/client/testdata/css/simple-imports.css
|
169
|
+
- spec/lib/percy/capybara/client/testdata/index.html
|
170
|
+
- spec/lib/percy/capybara/client/testdata/test-css.html
|
171
|
+
- spec/lib/percy/capybara_spec.rb
|
172
|
+
- spec/spec_helper.rb
|
173
|
+
- spec/support/test_helpers.rb
|
56
174
|
homepage: ''
|
57
175
|
licenses:
|
58
176
|
- MIT
|
@@ -76,5 +194,19 @@ rubyforge_project:
|
|
76
194
|
rubygems_version: 2.2.2
|
77
195
|
signing_key:
|
78
196
|
specification_version: 4
|
79
|
-
summary:
|
80
|
-
test_files:
|
197
|
+
summary: Percy::Capybara
|
198
|
+
test_files:
|
199
|
+
- spec/lib/percy/capybara/client/builds_spec.rb
|
200
|
+
- spec/lib/percy/capybara/client/snapshots_spec.rb
|
201
|
+
- spec/lib/percy/capybara/client/testdata/css/base.css
|
202
|
+
- spec/lib/percy/capybara/client/testdata/css/imports.css
|
203
|
+
- spec/lib/percy/capybara/client/testdata/css/level0-imports.css
|
204
|
+
- spec/lib/percy/capybara/client/testdata/css/level1-imports.css
|
205
|
+
- spec/lib/percy/capybara/client/testdata/css/level2-imports.css
|
206
|
+
- spec/lib/percy/capybara/client/testdata/css/simple-imports.css
|
207
|
+
- spec/lib/percy/capybara/client/testdata/index.html
|
208
|
+
- spec/lib/percy/capybara/client/testdata/test-css.html
|
209
|
+
- spec/lib/percy/capybara_spec.rb
|
210
|
+
- spec/spec_helper.rb
|
211
|
+
- spec/support/test_helpers.rb
|
212
|
+
has_rdoc:
|
data/lib/perceptual.rb
DELETED
data/lib/perceptual/version.rb
DELETED
data/perceptual.gemspec
DELETED
@@ -1,23 +0,0 @@
|
|
1
|
-
# coding: utf-8
|
2
|
-
lib = File.expand_path('../lib', __FILE__)
|
3
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require 'perceptual/version'
|
5
|
-
|
6
|
-
Gem::Specification.new do |spec|
|
7
|
-
spec.name = "percy-capybara"
|
8
|
-
spec.version = Perceptual::VERSION
|
9
|
-
spec.authors = ["Mike Fotinakis"]
|
10
|
-
spec.email = ["mike@fotinakis.com"]
|
11
|
-
spec.summary = "Write me."
|
12
|
-
spec.description = ""
|
13
|
-
spec.homepage = ""
|
14
|
-
spec.license = "MIT"
|
15
|
-
|
16
|
-
spec.files = `git ls-files -z`.split("\x0")
|
17
|
-
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
-
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
-
spec.require_paths = ["lib"]
|
20
|
-
|
21
|
-
spec.add_development_dependency "bundler", "~> 1.7"
|
22
|
-
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
-
end
|