monet 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -0
- data/README.md +24 -7
- data/Rakefile +15 -3
- data/config.yaml +11 -0
- data/lib/monet.rb +23 -2
- data/lib/monet/baseless_image.rb +11 -0
- data/lib/monet/baseline_control.rb +68 -0
- data/lib/monet/capture.rb +15 -17
- data/lib/monet/capture_map.rb +11 -15
- data/lib/monet/changeset.rb +23 -0
- data/lib/monet/compare.rb +18 -31
- data/lib/monet/config.rb +36 -4
- data/lib/monet/diff_strategy.rb +26 -1
- data/lib/monet/path_router.rb +64 -0
- data/lib/monet/url_helpers.rb +13 -0
- data/lib/monet/version.rb +1 -1
- data/spec/baseline_control_spec.rb +47 -0
- data/spec/capture_map_spec.rb +4 -4
- data/spec/capture_spec.rb +18 -14
- data/spec/compare_spec.rb +8 -2
- data/spec/config_spec.rb +1 -1
- data/spec/fixtures/diff.png +0 -0
- data/spec/path_router_spec.rb +50 -0
- data/spec/spec_helper.rb +2 -0
- metadata +12 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6fd195da9656b0fe86982f9ee484dcbf50a0aeb2
|
4
|
+
data.tar.gz: 88a19e81f68e678b6be8885f120848cb2ea63450
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f02f289bc355712dffdb37d0e4ca117d4bcb123559dce5c34ab260fbfbeb0736555f0d105055e6e2e757751477dcccf7e535cdc759f7f1ee25be2cbb8694ec67
|
7
|
+
data.tar.gz: fbb56feae30ce176c1a182a48c557ee986a4eff0d54ce65136d4dc445834c0a391586e6762b19dcd94d66f926b63dc2e360353a8414e20a3375e64b3cafba0da
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,4 @@
|
|
1
|
-
# Monet
|
2
|
-
[![Build Status](https://travis-ci.org/plukevdh/monet.png?branch=master)](https://travis-ci.org/plukevdh/monet)
|
3
|
-
[![Code Climate](https://codeclimate.com/github/plukevdh/monet.png)](https://codeclimate.com/github/plukevdh/monet)
|
1
|
+
# Monet [![Build Status](https://travis-ci.org/plukevdh/monet.png?branch=master)](https://travis-ci.org/plukevdh/monet) [![Code Climate](https://codeclimate.com/github/plukevdh/monet.png)](https://codeclimate.com/github/plukevdh/monet)
|
4
2
|
|
5
3
|
Monet is a libary built for making testing interfaces and design easy. We all have interfaces that we've added a new button, changed some CSS, or added new javascript interaction to and had the page layout explode unexpectedly. Monet is meant to make tracking those changes and ensuring consistent automatably easy.
|
6
4
|
|
@@ -20,7 +18,7 @@ Or install it yourself as:
|
|
20
18
|
|
21
19
|
## Usage
|
22
20
|
|
23
|
-
The basic gem requires a config file that is
|
21
|
+
The basic gem requires a config file that is called in an app initializer or via the built-in rake task. This config primarily exists to give the gem a list of paths it needs to collect and either baseline or compare to previous baselines. This config might look something like this:
|
24
22
|
|
25
23
|
```ruby
|
26
24
|
Monet.config do |config|
|
@@ -38,10 +36,29 @@ Monet.config do |config|
|
|
38
36
|
end
|
39
37
|
```
|
40
38
|
|
39
|
+
## Process
|
40
|
+
|
41
|
+
Captures are saved into the following structure:
|
42
|
+
|
43
|
+
```
|
44
|
+
/captures
|
45
|
+
/baselines
|
46
|
+
/captures
|
47
|
+
```
|
48
|
+
|
49
|
+
- /captures is where the current capture run images are stored, pre-comparison with baseline.
|
50
|
+
- /baselines is where all current baseline images are stored. persistent in-between capture runs.
|
51
|
+
|
52
|
+
During the capture process, any new captures that do not have a match found in baselines to compare with are considered new baselines.
|
53
|
+
Any images that match baseline are discarded.
|
54
|
+
Any images that flag differences, are flagged for review.
|
55
|
+
|
56
|
+
Review involves checking flagged images and marking as
|
57
|
+
1. discard
|
58
|
+
2. flag as issue
|
59
|
+
3. accept as new baseline
|
60
|
+
|
41
61
|
## Todo
|
42
|
-
- Site spidering
|
43
|
-
- Baseline caching
|
44
|
-
- Browser/driver config
|
45
62
|
- Parallelize PNG diffing
|
46
63
|
- Dashboard
|
47
64
|
- Rails integration
|
data/Rakefile
CHANGED
@@ -1,10 +1,22 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
2
|
require 'rspec/core/rake_task'
|
3
|
-
|
3
|
+
require 'monet'
|
4
|
+
|
5
|
+
task :clean do
|
6
|
+
config = Monet::Config.load
|
7
|
+
Monet.clean config
|
8
|
+
end
|
4
9
|
|
5
|
-
|
10
|
+
desc "Runs the site and grabs baselines"
|
11
|
+
task :baseline => [:clean, :run] do
|
12
|
+
end
|
6
13
|
|
14
|
+
desc "Run the baseline comparison"
|
15
|
+
task :run do
|
16
|
+
config = Monet::Config.load
|
17
|
+
Monet.capture config
|
18
|
+
Monet.compare config
|
7
19
|
end
|
8
20
|
|
9
21
|
RSpec::Core::RakeTask.new(:test)
|
10
|
-
task :default => :
|
22
|
+
task :default => :run
|
data/config.yaml
ADDED
data/lib/monet.rb
CHANGED
@@ -1,9 +1,30 @@
|
|
1
1
|
require "monet/version"
|
2
|
-
require
|
3
|
-
require "monet/capture_map"
|
2
|
+
require 'monet/config'
|
4
3
|
require "monet/capture"
|
5
4
|
require "monet/compare"
|
5
|
+
require "monet/baseline_control"
|
6
6
|
|
7
7
|
module Monet
|
8
|
+
class << self
|
9
|
+
def clean(opts)
|
10
|
+
config = load_config(opts)
|
11
|
+
Dir.glob(File.join(config.baseline_dir, "**", "*.png")).each do |img|
|
12
|
+
File.delete img
|
13
|
+
end
|
14
|
+
end
|
8
15
|
|
16
|
+
def capture(opts)
|
17
|
+
agent = Monet::Capture.new(load_config(opts))
|
18
|
+
agent.capture_all
|
19
|
+
end
|
20
|
+
|
21
|
+
def compare(opts)
|
22
|
+
control = Monet::BaselineControl.new(opts)
|
23
|
+
control.run
|
24
|
+
end
|
25
|
+
|
26
|
+
def load_config(options)
|
27
|
+
Monet::Config.build_config(options)
|
28
|
+
end
|
29
|
+
end
|
9
30
|
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'monet/path_router'
|
2
|
+
require 'monet/changeset'
|
3
|
+
require 'monet/baseless_image'
|
4
|
+
require 'monet/compare'
|
5
|
+
|
6
|
+
require 'fileutils'
|
7
|
+
|
8
|
+
module Monet
|
9
|
+
class BaselineControl
|
10
|
+
|
11
|
+
attr_reader :flags
|
12
|
+
|
13
|
+
def initialize(config)
|
14
|
+
@capture_dir = config.capture_dir
|
15
|
+
@baseline_dir = config.baseline_dir
|
16
|
+
|
17
|
+
strategy = Monet.const_get(config.compare_type)
|
18
|
+
|
19
|
+
@comparer = Monet::Compare.new(strategy)
|
20
|
+
@router = Monet::PathRouter.new(config)
|
21
|
+
@flags = []
|
22
|
+
end
|
23
|
+
|
24
|
+
def run
|
25
|
+
captures.each do |capture|
|
26
|
+
baseline_path = @router.capture_to_baseline(capture)
|
27
|
+
compare @comparer.compare(baseline_path, capture)
|
28
|
+
end
|
29
|
+
|
30
|
+
@flags
|
31
|
+
end
|
32
|
+
|
33
|
+
def compare(diff)
|
34
|
+
return baseline(diff) if diff.is_a? Monet::BaselessImage
|
35
|
+
return discard(diff.path) unless diff.modified?
|
36
|
+
|
37
|
+
puts "diff found #{diff.path}"
|
38
|
+
|
39
|
+
@flags << diff.path
|
40
|
+
end
|
41
|
+
|
42
|
+
def captures
|
43
|
+
Dir.glob File.join(site_dir(@capture_dir), "*.png")
|
44
|
+
end
|
45
|
+
|
46
|
+
def discard(path)
|
47
|
+
puts "discarding #{path}"
|
48
|
+
FileUtils.remove(path)
|
49
|
+
end
|
50
|
+
|
51
|
+
def baseline(diff)
|
52
|
+
path = diff.path
|
53
|
+
to = site_dir(@baseline_dir)
|
54
|
+
|
55
|
+
FileUtils.mkpath to unless Dir.exists? to
|
56
|
+
FileUtils.move(path, to)
|
57
|
+
|
58
|
+
puts "baselining #{path}"
|
59
|
+
|
60
|
+
@router.capture_to_baseline(path)
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
def site_dir(base)
|
65
|
+
File.join base, @router.root_dir
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/lib/monet/capture.rb
CHANGED
@@ -8,30 +8,28 @@ module Monet
|
|
8
8
|
class Capture
|
9
9
|
include Capybara::DSL
|
10
10
|
|
11
|
-
|
12
|
-
@config = (config.is_a? Monet::Config) ? config : Monet::Config.new(config)
|
11
|
+
MAX_HEIGHT = 10000
|
13
12
|
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
def initialize(config)
|
14
|
+
@config = Monet::Config.build_config(config)
|
15
|
+
@router = Monet::PathRouter.new(@config)
|
17
16
|
|
18
|
-
|
19
|
-
visit normalize_path(path)
|
20
|
-
page.driver.render(image_name_from_path(path), full: true)
|
17
|
+
Capybara.default_driver = @config.driver
|
21
18
|
end
|
22
19
|
|
23
|
-
|
24
|
-
|
25
|
-
|
20
|
+
def capture_all
|
21
|
+
@config.map.paths.each do |path|
|
22
|
+
capture(path)
|
23
|
+
end
|
26
24
|
end
|
27
25
|
|
28
|
-
def
|
29
|
-
|
30
|
-
end
|
26
|
+
def capture(path)
|
27
|
+
visit @router.build_url(path)
|
31
28
|
|
32
|
-
|
33
|
-
|
34
|
-
|
29
|
+
@config.dimensions.each do |width|
|
30
|
+
page.driver.resize(width, MAX_HEIGHT)
|
31
|
+
page.driver.render(@router.route_url_path(path, width), full: true)
|
32
|
+
end
|
35
33
|
end
|
36
34
|
end
|
37
35
|
end
|
data/lib/monet/capture_map.rb
CHANGED
@@ -1,14 +1,16 @@
|
|
1
1
|
require 'spidr'
|
2
|
+
require 'monet/url_helpers'
|
2
3
|
|
3
4
|
module Monet
|
4
5
|
class CaptureMap
|
5
6
|
extend Forwardable
|
6
7
|
|
7
8
|
class PathCollection
|
9
|
+
include URLHelpers
|
8
10
|
attr_reader :paths, :root_url
|
9
11
|
|
10
|
-
def initialize(
|
11
|
-
@root_url =
|
12
|
+
def initialize(root_uri)
|
13
|
+
@root_url = parse_uri(root_uri)
|
12
14
|
@paths = []
|
13
15
|
end
|
14
16
|
|
@@ -16,6 +18,10 @@ module Monet
|
|
16
18
|
@paths << normalized_path(path)
|
17
19
|
end
|
18
20
|
|
21
|
+
def paths=(paths)
|
22
|
+
@paths.concat paths.map {|p| normalized_path(p) }
|
23
|
+
end
|
24
|
+
|
19
25
|
def normalized_path(path)
|
20
26
|
path.chomp "/"
|
21
27
|
end
|
@@ -39,18 +45,16 @@ module Monet
|
|
39
45
|
end
|
40
46
|
end
|
41
47
|
|
42
|
-
InvalidURL = Class.new(StandardError)
|
43
|
-
|
44
48
|
attr_reader :type
|
45
49
|
|
46
|
-
def initialize(
|
50
|
+
def initialize(root_uri, type=:explicit, &block)
|
47
51
|
@type = type
|
48
|
-
@path_helper = type_mapper.new
|
52
|
+
@path_helper = type_mapper.new root_uri
|
49
53
|
|
50
54
|
yield(@path_helper) if block_given?
|
51
55
|
end
|
52
56
|
|
53
|
-
def_delegators :@path_helper, :paths, :add, :root_url
|
57
|
+
def_delegators :@path_helper, :paths, :paths=, :add, :root_url
|
54
58
|
|
55
59
|
def type_mapper
|
56
60
|
case @type
|
@@ -60,13 +64,5 @@ module Monet
|
|
60
64
|
PathSpider
|
61
65
|
end
|
62
66
|
end
|
63
|
-
|
64
|
-
private
|
65
|
-
def parse_uri(path)
|
66
|
-
uri = URI.parse path
|
67
|
-
raise InvalidURL, "#{path} is not a valid url" if uri.class == URI::Generic
|
68
|
-
|
69
|
-
uri.to_s
|
70
|
-
end
|
71
67
|
end
|
72
68
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Monet
|
2
|
+
class Changeset
|
3
|
+
attr_reader :path
|
4
|
+
|
5
|
+
def initialize(base_image, pixel_array, path)
|
6
|
+
@base_image = base_image
|
7
|
+
@changed_pixels = pixel_array
|
8
|
+
@path = path
|
9
|
+
end
|
10
|
+
|
11
|
+
def modified?
|
12
|
+
pixels_changed > 0
|
13
|
+
end
|
14
|
+
|
15
|
+
def pixels_changed
|
16
|
+
@changed_pixels.count
|
17
|
+
end
|
18
|
+
|
19
|
+
def percentage_changed
|
20
|
+
((pixels_changed.to_f / @base_image.area.to_f) * 100).round(2)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/monet/compare.rb
CHANGED
@@ -1,52 +1,39 @@
|
|
1
1
|
require 'oily_png'
|
2
2
|
require 'monet/diff_strategy'
|
3
|
+
require 'monet/changeset'
|
4
|
+
require 'monet/baseless_image'
|
3
5
|
|
4
6
|
module Monet
|
5
7
|
class Compare
|
6
8
|
extend Forwardable
|
7
9
|
|
8
|
-
class Changeset
|
9
|
-
def initialize(base_image, pixel_array)
|
10
|
-
@base_image = base_image
|
11
|
-
@changed_pixels = pixel_array
|
12
|
-
end
|
13
|
-
|
14
|
-
def modified?
|
15
|
-
pixels_changed > 0
|
16
|
-
end
|
17
|
-
|
18
|
-
def pixels_changed
|
19
|
-
@changed_pixels.count
|
20
|
-
end
|
21
|
-
|
22
|
-
def percentage_changed
|
23
|
-
((pixels_changed.to_f / @base_image.area.to_f) * 100).round(2)
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
10
|
def initialize(strategy=ColorBlend)
|
28
11
|
@strategy_class = strategy
|
29
12
|
end
|
30
13
|
|
31
14
|
def compare(base_image, new_image)
|
32
|
-
|
33
|
-
|
15
|
+
puts "comparing #{base_image} with #{new_image}"
|
16
|
+
begin
|
17
|
+
new_png = ChunkyPNG::Image.from_file(new_image)
|
18
|
+
base_png = ChunkyPNG::Image.from_file(base_image)
|
34
19
|
|
35
|
-
|
20
|
+
diff_stats = []
|
36
21
|
|
37
|
-
|
38
|
-
diff_strategy = @strategy_class.new(base_png, new_png)
|
22
|
+
diff_strategy = @strategy_class.new(base_png, new_png)
|
39
23
|
|
40
|
-
|
41
|
-
|
42
|
-
|
24
|
+
base_png.height.times do |y|
|
25
|
+
base_png.row(y).each_with_index do |pixel, x|
|
26
|
+
diff_strategy.calculate_for_pixel(pixel, x, y)
|
27
|
+
end
|
43
28
|
end
|
44
|
-
end
|
45
29
|
|
46
|
-
|
47
|
-
|
30
|
+
changeset = Changeset.new(base_png, diff_strategy.score, new_image)
|
31
|
+
diff_strategy.save(diff_filename(base_image)) if changeset.modified?
|
48
32
|
|
49
|
-
|
33
|
+
changeset
|
34
|
+
rescue Errno::ENOENT => e
|
35
|
+
return BaselessImage.new(new_image)
|
36
|
+
end
|
50
37
|
end
|
51
38
|
|
52
39
|
private
|
data/lib/monet/config.rb
CHANGED
@@ -1,15 +1,20 @@
|
|
1
1
|
require 'monet/capture_map'
|
2
|
+
require 'yaml'
|
2
3
|
|
3
4
|
module Monet
|
4
5
|
class Config
|
6
|
+
include URLHelpers
|
7
|
+
|
5
8
|
MissingBaseURL = Class.new(Exception)
|
6
9
|
|
7
10
|
DEFAULT_OPTIONS = {
|
8
11
|
driver: :poltergeist,
|
9
|
-
dimensions: [
|
10
|
-
map: nil,
|
12
|
+
dimensions: [1024],
|
11
13
|
base_url: nil,
|
12
|
-
|
14
|
+
map: nil,
|
15
|
+
compare_type: "ColorBlend",
|
16
|
+
capture_dir: "./captures",
|
17
|
+
baseline_dir: "./baselines"
|
13
18
|
}
|
14
19
|
|
15
20
|
attr_accessor *DEFAULT_OPTIONS.keys
|
@@ -26,20 +31,47 @@ module Monet
|
|
26
31
|
cfg
|
27
32
|
end
|
28
33
|
|
34
|
+
def self.load(path="./config.yaml")
|
35
|
+
config = YAML::load(File.open(path))
|
36
|
+
new(config)
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.build_config(opts)
|
40
|
+
(opts.is_a? Monet::Config) ? opts : new(opts)
|
41
|
+
end
|
42
|
+
|
43
|
+
def base_url=(url)
|
44
|
+
@base_url ||= parse_uri(url) unless url.nil?
|
45
|
+
end
|
46
|
+
|
29
47
|
def base_url
|
30
48
|
raise MissingBaseURL, "Please set the base_url in the config" unless @base_url
|
31
49
|
@base_url
|
32
50
|
end
|
33
51
|
|
34
52
|
def capture_dir=(path)
|
35
|
-
@capture_dir =
|
53
|
+
@capture_dir = expand_path(path)
|
54
|
+
end
|
55
|
+
|
56
|
+
def baseline_dir=(path)
|
57
|
+
@baseline_dir = expand_path(path)
|
58
|
+
end
|
59
|
+
|
60
|
+
def map=(paths)
|
61
|
+
map.paths = paths unless paths.nil?
|
36
62
|
end
|
37
63
|
|
38
64
|
def map(type=:explicit, &block)
|
39
65
|
@map ||= CaptureMap.new(base_url, type)
|
40
66
|
|
41
67
|
block.call(@map) if block_given? && type == :explicit
|
68
|
+
|
42
69
|
@map
|
43
70
|
end
|
71
|
+
|
72
|
+
private
|
73
|
+
def expand_path(path)
|
74
|
+
File.expand_path(path)
|
75
|
+
end
|
44
76
|
end
|
45
77
|
end
|
data/lib/monet/diff_strategy.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'monet/errors'
|
2
|
+
|
1
3
|
module Monet
|
2
4
|
class DiffStrategy
|
3
5
|
include ChunkyPNG::Color
|
@@ -55,7 +57,7 @@ module Monet
|
|
55
57
|
class ColorBlend < DiffStrategy
|
56
58
|
def initialize(base_image, diff_image)
|
57
59
|
super
|
58
|
-
@output = ChunkyPNG::Image.new(base_image.width,
|
60
|
+
@output = ChunkyPNG::Image.new(base_image.width, base_image.height, BLACK)
|
59
61
|
end
|
60
62
|
|
61
63
|
def calculate_for_pixel(pixel, x, y)
|
@@ -67,4 +69,27 @@ module Monet
|
|
67
69
|
super
|
68
70
|
end
|
69
71
|
end
|
72
|
+
|
73
|
+
class Highlight < DiffStrategy
|
74
|
+
ALPHA_COMPONENT = 30
|
75
|
+
|
76
|
+
def initialize(base_image, diff_image)
|
77
|
+
super
|
78
|
+
@output = ChunkyPNG::Image.new(base_image.width, base_image.height, WHITE)
|
79
|
+
end
|
80
|
+
|
81
|
+
def colors(pixel)
|
82
|
+
rgb_colors = %w(r g b).map {|color| for_color(color, pixel)}
|
83
|
+
end
|
84
|
+
|
85
|
+
def calculate_for_pixel(pixel, x, y)
|
86
|
+
if pixel == @diff_image[x,y]
|
87
|
+
@output[x,y] = rgba(*colors(pixel), ALPHA_COMPONENT)
|
88
|
+
else
|
89
|
+
@output[x,y] = html_color("blue")
|
90
|
+
end
|
91
|
+
|
92
|
+
super
|
93
|
+
end
|
94
|
+
end
|
70
95
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'monet/url_helpers'
|
2
|
+
|
3
|
+
module Monet
|
4
|
+
class PathRouter
|
5
|
+
include URLHelpers
|
6
|
+
|
7
|
+
def initialize(config)
|
8
|
+
@base_url = parse_uri(config.base_url)
|
9
|
+
@capture_path = config.capture_dir
|
10
|
+
@baseline_path = config.baseline_dir
|
11
|
+
end
|
12
|
+
|
13
|
+
def build_url(path)
|
14
|
+
"#{@base_url}#{path}"
|
15
|
+
end
|
16
|
+
|
17
|
+
# takes a url, gives the image path
|
18
|
+
def route_url(url, width="*")
|
19
|
+
uri = parse_uri(url)
|
20
|
+
route_url_path(uri.path, width)
|
21
|
+
end
|
22
|
+
|
23
|
+
# takes a url path, gives the image path
|
24
|
+
def route_url_path(path, width="*")
|
25
|
+
image_name(@capture_path, path, width)
|
26
|
+
end
|
27
|
+
|
28
|
+
def url_to_baseline(url, width)
|
29
|
+
uri = parse_uri(url)
|
30
|
+
url_path_to_baseline(uri.path, width)
|
31
|
+
end
|
32
|
+
|
33
|
+
def url_path_to_baseline(path, width)
|
34
|
+
image_name(@baseline_path, path, width)
|
35
|
+
end
|
36
|
+
|
37
|
+
def capture_to_baseline(path)
|
38
|
+
path.gsub(@capture_path, @baseline_path)
|
39
|
+
end
|
40
|
+
|
41
|
+
# takes a path, returns the URL used to generate the image
|
42
|
+
def route_path(path)
|
43
|
+
url = path.split("/").last
|
44
|
+
path = url.split('>')[1..-1].join("/").gsub(/-\d+\.png/, "")
|
45
|
+
|
46
|
+
"#{@base_url}/#{path}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def host
|
50
|
+
@base_url.host
|
51
|
+
end
|
52
|
+
alias :root_dir :host
|
53
|
+
|
54
|
+
private
|
55
|
+
def image_name(base_dir, path, width)
|
56
|
+
name = normalize_path(path).gsub(/\//, '>')
|
57
|
+
"#{base_dir}/#{host}/#{name}-#{width}.png"
|
58
|
+
end
|
59
|
+
|
60
|
+
def normalize_path(path)
|
61
|
+
"#{host}#{path}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Monet
|
2
|
+
module URLHelpers
|
3
|
+
::Monet::InvalidURL = Class.new(StandardError)
|
4
|
+
|
5
|
+
private
|
6
|
+
def parse_uri(path)
|
7
|
+
uri = path.is_a?(URI) ? path : URI.parse(path)
|
8
|
+
raise InvalidURL, "#{path} is not a valid url" if uri.class == URI::Generic
|
9
|
+
|
10
|
+
uri
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/lib/monet/version.rb
CHANGED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'monet/baseline_control'
|
3
|
+
|
4
|
+
describe Monet::BaselineControl do
|
5
|
+
Given(:control) do
|
6
|
+
config = flexmock("Config", compare_type: "ColorBlend", capture_dir: "./spec/fixtures", baseline_dir: "./baseline", base_url: "http://google.com")
|
7
|
+
flexmock Monet::BaselineControl.new(config)
|
8
|
+
end
|
9
|
+
|
10
|
+
context "makes a capture baseline if no baseline exists" do
|
11
|
+
Given(:diff) { Monet::BaselessImage.new("./fixtures/fake.png") }
|
12
|
+
Given { control.should_receive(:baseline).with(diff).and_return("./baseline/fake.png") }
|
13
|
+
|
14
|
+
When(:result) { control.compare(diff) }
|
15
|
+
|
16
|
+
Then { control.should have_received(:baseline).with(diff) }
|
17
|
+
And { result.should eq("./baseline/fake.png") }
|
18
|
+
end
|
19
|
+
|
20
|
+
context "discards captures that match baseline" do
|
21
|
+
Given(:diff) { flexmock(:on, Monet::Changeset, modified?: false, path: "./fixtures/fake.png") }
|
22
|
+
Given { control.should_receive(:discard).with("./fixtures/fake.png") }
|
23
|
+
|
24
|
+
When(:result) { control.compare(diff) }
|
25
|
+
|
26
|
+
Then { control.should have_received(:discard).with("./fixtures/fake.png") }
|
27
|
+
end
|
28
|
+
|
29
|
+
context "flags changed capture as different" do
|
30
|
+
Given(:diff) { flexmock(:on, Monet::Changeset, modified?: true, path: "./fixtures/fake.png") }
|
31
|
+
|
32
|
+
When(:diffs) { control.compare(diff) }
|
33
|
+
|
34
|
+
Then { diffs.length.should == 1 }
|
35
|
+
And { diffs.should == ["./fixtures/fake.png"] }
|
36
|
+
end
|
37
|
+
|
38
|
+
context "replaces baseline if requested" do
|
39
|
+
Given(:diff) { flexmock(:on, Monet::Changeset, modified?: true, path: "./fixtures/fake.png") }
|
40
|
+
Given { control.should_receive(:baseline).with(diff).and_return("./baseline/fake.png") }
|
41
|
+
|
42
|
+
When(:result) { control.baseline(diff) }
|
43
|
+
|
44
|
+
Then { control.should have_received(:baseline).with(diff) }
|
45
|
+
And { result.should eq("./baseline/fake.png") }
|
46
|
+
end
|
47
|
+
end
|
data/spec/capture_map_spec.rb
CHANGED
@@ -31,17 +31,17 @@ describe Monet::CaptureMap do
|
|
31
31
|
|
32
32
|
context "requires full url" do
|
33
33
|
When(:result) { Monet::CaptureMap.new("google.com") }
|
34
|
-
Then { result.should have_failed(Monet::
|
34
|
+
Then { result.should have_failed(Monet::InvalidURL, /google.com is not a valid url/) }
|
35
35
|
end
|
36
36
|
|
37
37
|
context "requires valid url" do
|
38
38
|
When(:result) { Monet::CaptureMap.new("google") }
|
39
|
-
Then { result.should have_failed(Monet::
|
39
|
+
Then { result.should have_failed(Monet::InvalidURL, /google is not a valid url/) }
|
40
40
|
end
|
41
41
|
|
42
42
|
context "with name" do
|
43
43
|
Given(:map) { Monet::CaptureMap.new("http://google.com") }
|
44
|
-
Then { map.root_url.should == "http://google.com" }
|
44
|
+
Then { map.root_url.to_s.should == "http://google.com" }
|
45
45
|
And { map.paths.should == [] }
|
46
46
|
end
|
47
47
|
|
@@ -54,7 +54,7 @@ describe Monet::CaptureMap do
|
|
54
54
|
}
|
55
55
|
|
56
56
|
context "add paths" do
|
57
|
-
Then { map.root_url.should == "http://google.com" }
|
57
|
+
Then { map.root_url.to_s.should == "http://google.com" }
|
58
58
|
And { map.paths.should == ['home', 'test/new'] }
|
59
59
|
end
|
60
60
|
end
|
data/spec/capture_spec.rb
CHANGED
@@ -3,30 +3,25 @@ require 'monet/capture'
|
|
3
3
|
|
4
4
|
describe Monet::Capture do
|
5
5
|
Given(:path) { File.expand_path './spec/tmp/output' }
|
6
|
-
Given(:
|
7
|
-
|
8
|
-
before do
|
9
|
-
Timecop.freeze
|
10
|
-
end
|
6
|
+
Given(:url) { "http://google.com" }
|
7
|
+
Given(:capture_agent) { Monet::Capture.new(capture_dir: path, base_url: url) }
|
11
8
|
|
12
9
|
after do
|
13
|
-
|
14
|
-
|
15
|
-
Dir.glob("#{path}/*.png").each do |file|
|
10
|
+
Dir.glob("#{path}/**/*.png").each do |file|
|
16
11
|
File.delete(file)
|
17
12
|
end
|
18
13
|
end
|
19
14
|
|
20
15
|
context "can pass config" do
|
21
16
|
context "as a hash" do
|
22
|
-
Given(:capture_agent) { Monet::Capture.new(capture_dir: path) }
|
17
|
+
Given(:capture_agent) { Monet::Capture.new(capture_dir: path, base_url: url) }
|
23
18
|
When(:config) { capture_agent.instance_variable_get :@config }
|
24
19
|
Then { config.should be_a(Monet::Config) }
|
25
20
|
Then { config.capture_dir.should == path }
|
26
21
|
end
|
27
22
|
|
28
23
|
context "as a Monet::Config" do
|
29
|
-
Given(:config) { Monet::Config.new(capture_dir: path) }
|
24
|
+
Given(:config) { Monet::Config.new(capture_dir: path, base_url: url) }
|
30
25
|
Given(:capture_agent) { Monet::Capture.new(config) }
|
31
26
|
When(:final) { capture_agent.instance_variable_get :@config }
|
32
27
|
Then { final.should be_a(Monet::Config) }
|
@@ -35,13 +30,22 @@ describe Monet::Capture do
|
|
35
30
|
end
|
36
31
|
|
37
32
|
context "converts name properly" do
|
38
|
-
|
39
|
-
|
33
|
+
Given(:capture_agent) { Monet::Capture.new(capture_dir: path, base_url: url, dimensions: [900]) }
|
34
|
+
When { capture_agent.capture("/") }
|
35
|
+
Then { File.exist?("#{path}/google.com/google.com>-900.png").should be_true }
|
36
|
+
end
|
37
|
+
|
38
|
+
context "captures all dimensions requested" do
|
39
|
+
Given(:capture_agent) { Monet::Capture.new(capture_dir: path, base_url: url, dimensions: [900, 1400]) }
|
40
|
+
When { capture_agent.capture("/") }
|
41
|
+
Then { File.exist?("#{path}/google.com/google.com>-900.png").should be_true }
|
42
|
+
Then { File.exist?("#{path}/google.com/google.com>-1400.png").should be_true }
|
40
43
|
end
|
41
44
|
|
42
45
|
context "prepends default protocol if missing" do
|
43
|
-
|
44
|
-
|
46
|
+
Given(:capture_agent) { Monet::Capture.new(capture_dir: path, base_url: "http://www.facebook.com") }
|
47
|
+
When { capture_agent.capture('/') }
|
48
|
+
Then { File.exist?("#{path}/www.facebook.com/www.facebook.com>-1024.png").should be_true }
|
45
49
|
end
|
46
50
|
|
47
51
|
end
|
data/spec/compare_spec.rb
CHANGED
@@ -12,6 +12,12 @@ describe Monet::Compare do
|
|
12
12
|
context "default compare" do
|
13
13
|
Given(:compare) { Monet::Compare.new }
|
14
14
|
|
15
|
+
context "missing baseline" do
|
16
|
+
When(:result) { compare.compare("./missing_image.png", image_same) }
|
17
|
+
Then { result.should be_a(Monet::BaselessImage) }
|
18
|
+
And { result.path.should == image_same }
|
19
|
+
end
|
20
|
+
|
15
21
|
context "identical image" do
|
16
22
|
When(:result) { compare.compare(image_base, image_same) }
|
17
23
|
Then { result.should_not be_modified }
|
@@ -45,7 +51,7 @@ describe Monet::Compare do
|
|
45
51
|
|
46
52
|
Then { result.should be_modified }
|
47
53
|
And { File.exist?(diff_name).should be_true }
|
48
|
-
And { result.pixels_changed.should eq(
|
49
|
-
And { result.percentage_changed.should eq(
|
54
|
+
And { result.pixels_changed.should eq(2387) }
|
55
|
+
And { result.percentage_changed.should eq(0.55) }
|
50
56
|
end
|
51
57
|
end
|
data/spec/config_spec.rb
CHANGED
@@ -12,7 +12,7 @@ describe Monet::Config do
|
|
12
12
|
When(:config) { Monet::Config.new(capture_dir: "./faker", base_url: "http://hoodie.io") }
|
13
13
|
Then { config.driver.should == :poltergeist }
|
14
14
|
And { config.capture_dir.should == File.expand_path("./faker") }
|
15
|
-
And { config.base_url.should == "http://hoodie.io" }
|
15
|
+
And { config.base_url.to_s.should == "http://hoodie.io" }
|
16
16
|
end
|
17
17
|
|
18
18
|
context "can set options" do
|
data/spec/fixtures/diff.png
CHANGED
Binary file
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'monet/path_router'
|
3
|
+
require 'monet/config'
|
4
|
+
|
5
|
+
describe Monet::PathRouter do
|
6
|
+
Given(:capture) { File.expand_path "./capture" }
|
7
|
+
Given(:baseline) { File.expand_path "./baseline" }
|
8
|
+
Given(:config) { Monet::Config.new(base_url: "http://google.com", capture_dir: "./capture", baseline_dir: "./baseline") }
|
9
|
+
Given(:router) { Monet::PathRouter.new(config) }
|
10
|
+
|
11
|
+
context "knows base dir" do
|
12
|
+
When(:path) { router.root_dir }
|
13
|
+
Then { path.should == "google.com" }
|
14
|
+
end
|
15
|
+
|
16
|
+
context "build url from path" do
|
17
|
+
When(:url) { router.build_url("/space/manager") }
|
18
|
+
Then { url.should == "http://google.com/space/manager" }
|
19
|
+
end
|
20
|
+
|
21
|
+
context "full url to path" do
|
22
|
+
When(:path) { router.route_url("http://google.com/space/manager", 900) }
|
23
|
+
Then { path.should == "#{capture}/google.com/google.com>space>manager-900.png" }
|
24
|
+
end
|
25
|
+
|
26
|
+
context "url path to path" do
|
27
|
+
When(:path) { router.route_url_path("/space/manager", 900) }
|
28
|
+
Then { path.should == "#{capture}/google.com/google.com>space>manager-900.png"}
|
29
|
+
end
|
30
|
+
|
31
|
+
context "path to url" do
|
32
|
+
When(:url) { router.route_path("#{capture}/google.com/google.com>space>manager-900.png") }
|
33
|
+
Then { url.should == "http://google.com/space/manager" }
|
34
|
+
end
|
35
|
+
|
36
|
+
context "path to baseline from url" do
|
37
|
+
When(:path) { router.url_to_baseline("http://google.com/space/manager", 900) }
|
38
|
+
Then { path.should == "#{baseline}/google.com/google.com>space>manager-900.png" }
|
39
|
+
end
|
40
|
+
|
41
|
+
context "path to baseline from path" do
|
42
|
+
When(:path) { router.url_path_to_baseline("/space/manager", 900) }
|
43
|
+
Then { path.should == "#{baseline}/google.com/google.com>space>manager-900.png" }
|
44
|
+
end
|
45
|
+
|
46
|
+
context "capture path to baseline path" do
|
47
|
+
When(:path) { router.capture_to_baseline("#{capture}/google.com/google.com>space>manager-900.png") }
|
48
|
+
Then { path.should == "#{baseline}/google.com/google.com>space>manager-900.png" }
|
49
|
+
end
|
50
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'vcr'
|
2
2
|
require 'rspec/given'
|
3
3
|
require 'timecop'
|
4
|
+
require 'pry'
|
4
5
|
|
5
6
|
VCR.configure do |c|
|
6
7
|
c.cassette_library_dir = 'spec/cassettes'
|
@@ -9,5 +10,6 @@ VCR.configure do |c|
|
|
9
10
|
end
|
10
11
|
|
11
12
|
RSpec.configure do |c|
|
13
|
+
c.mock_with :flexmock
|
12
14
|
c.treat_symbols_as_metadata_keys_with_true_values = true
|
13
15
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: monet
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Luke van der Hoeven
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-12-
|
11
|
+
date: 2013-12-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -137,15 +137,22 @@ files:
|
|
137
137
|
- LICENSE.txt
|
138
138
|
- README.md
|
139
139
|
- Rakefile
|
140
|
+
- config.yaml
|
140
141
|
- lib/monet.rb
|
142
|
+
- lib/monet/baseless_image.rb
|
143
|
+
- lib/monet/baseline_control.rb
|
141
144
|
- lib/monet/capture.rb
|
142
145
|
- lib/monet/capture_map.rb
|
146
|
+
- lib/monet/changeset.rb
|
143
147
|
- lib/monet/compare.rb
|
144
148
|
- lib/monet/config.rb
|
145
149
|
- lib/monet/diff_strategy.rb
|
146
150
|
- lib/monet/errors.rb
|
151
|
+
- lib/monet/path_router.rb
|
152
|
+
- lib/monet/url_helpers.rb
|
147
153
|
- lib/monet/version.rb
|
148
154
|
- monet.gemspec
|
155
|
+
- spec/baseline_control_spec.rb
|
149
156
|
- spec/capture_map_spec.rb
|
150
157
|
- spec/capture_spec.rb
|
151
158
|
- spec/cassettes/spider.yml
|
@@ -155,6 +162,7 @@ files:
|
|
155
162
|
- spec/fixtures/diff.png
|
156
163
|
- spec/fixtures/diff_size.png
|
157
164
|
- spec/fixtures/same.png
|
165
|
+
- spec/path_router_spec.rb
|
158
166
|
- spec/spec_helper.rb
|
159
167
|
homepage: http://plukevdh.github.com/monet
|
160
168
|
licenses: []
|
@@ -183,6 +191,7 @@ summary: Monet captures your web pages, sets up a baseline and then ensures that
|
|
183
191
|
if CSS changes blow your UI up. Simply capture your page, make changes, run tests
|
184
192
|
and compare the diff!
|
185
193
|
test_files:
|
194
|
+
- spec/baseline_control_spec.rb
|
186
195
|
- spec/capture_map_spec.rb
|
187
196
|
- spec/capture_spec.rb
|
188
197
|
- spec/cassettes/spider.yml
|
@@ -192,4 +201,5 @@ test_files:
|
|
192
201
|
- spec/fixtures/diff.png
|
193
202
|
- spec/fixtures/diff_size.png
|
194
203
|
- spec/fixtures/same.png
|
204
|
+
- spec/path_router_spec.rb
|
195
205
|
- spec/spec_helper.rb
|