happo 1.0.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 +7 -0
- data/bin/happo +60 -0
- data/lib/happo/action.rb +33 -0
- data/lib/happo/diffs.html.erb +29 -0
- data/lib/happo/logger.rb +40 -0
- data/lib/happo/public/happo-runner.js +223 -0
- data/lib/happo/public/happo-styles.css +8 -0
- data/lib/happo/runner.rb +215 -0
- data/lib/happo/server.rb +68 -0
- data/lib/happo/snapshot_comparer.rb +91 -0
- data/lib/happo/snapshot_comparison_image/after.rb +21 -0
- data/lib/happo/snapshot_comparison_image/base.rb +108 -0
- data/lib/happo/snapshot_comparison_image/before.rb +21 -0
- data/lib/happo/snapshot_comparison_image/gutter.rb +34 -0
- data/lib/happo/snapshot_comparison_image/overlayed.rb +88 -0
- data/lib/happo/uploader.rb +70 -0
- data/lib/happo/utils.rb +81 -0
- data/lib/happo/version.rb +4 -0
- data/lib/happo/views/debug.erb +29 -0
- data/lib/happo/views/index.erb +27 -0
- data/lib/happo/views/review.erb +37 -0
- data/lib/happo.rb +6 -0
- metadata +191 -0
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'oily_png'
|
2
|
+
require 'diff-lcs'
|
3
|
+
require_relative 'snapshot_comparison_image/base'
|
4
|
+
require_relative 'snapshot_comparison_image/gutter'
|
5
|
+
require_relative 'snapshot_comparison_image/before'
|
6
|
+
require_relative 'snapshot_comparison_image/overlayed'
|
7
|
+
require_relative 'snapshot_comparison_image/after'
|
8
|
+
|
9
|
+
module Happo
|
10
|
+
# This class is responsible for comparing two Snapshots and generating a diff.
|
11
|
+
class SnapshotComparer
|
12
|
+
# @param png_before [ChunkyPNG::Image]
|
13
|
+
# @param png_after [ChunkyPNG::Image]
|
14
|
+
def initialize(png_before, png_after)
|
15
|
+
@png_after = png_after
|
16
|
+
@png_before = png_before
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [Hash]
|
20
|
+
def compare!
|
21
|
+
no_diff = {
|
22
|
+
diff_in_percent: 0,
|
23
|
+
diff_image: nil,
|
24
|
+
}
|
25
|
+
|
26
|
+
# If these images are totally identical, we don't need to do any more
|
27
|
+
# work.
|
28
|
+
return no_diff if @png_before == @png_after
|
29
|
+
|
30
|
+
array_before = to_array_of_arrays(@png_before)
|
31
|
+
array_after = to_array_of_arrays(@png_after)
|
32
|
+
|
33
|
+
# If the arrays of arrays of colors are identical, we don't need to do any
|
34
|
+
# more work. This might happen if some of the headers are different.
|
35
|
+
return no_diff if array_before == array_after
|
36
|
+
|
37
|
+
sdiff = Diff::LCS.sdiff(array_before, array_after)
|
38
|
+
number_of_different_rows = 0
|
39
|
+
|
40
|
+
sprite, all_comparisons = initialize_comparison_images(
|
41
|
+
[@png_after.width, @png_before.width].max, sdiff.size)
|
42
|
+
|
43
|
+
sdiff.each_with_index do |row, y|
|
44
|
+
# each row is a Diff::LCS::ContextChange instance
|
45
|
+
all_comparisons.each { |image| image.render_row(y, row) }
|
46
|
+
number_of_different_rows += 1 unless row.unchanged?
|
47
|
+
end
|
48
|
+
|
49
|
+
percent_changed = number_of_different_rows.to_f / sdiff.size * 100
|
50
|
+
{
|
51
|
+
diff_in_percent: percent_changed,
|
52
|
+
diff_image: (sprite if percent_changed > 0),
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
# @param [ChunkyPNG::Image]
|
59
|
+
# @return [Array<Array<Integer>>]
|
60
|
+
def to_array_of_arrays(chunky_png)
|
61
|
+
array_of_arrays = []
|
62
|
+
chunky_png.height.times do |y|
|
63
|
+
array_of_arrays << chunky_png.row(y)
|
64
|
+
end
|
65
|
+
array_of_arrays
|
66
|
+
end
|
67
|
+
|
68
|
+
# @param canvas [ChunkyPNG::Image] The output image to draw pixels on
|
69
|
+
# @return [Array<SnapshotComparisonImage>]
|
70
|
+
def initialize_comparison_images(width, height)
|
71
|
+
gutter_width = Happo::SnapshotComparisonImage::Gutter::WIDTH
|
72
|
+
total_width = (width * 3) + (gutter_width * 3)
|
73
|
+
|
74
|
+
sprite = ChunkyPNG::Image.new(total_width, height)
|
75
|
+
offset, comparison_images = 0, []
|
76
|
+
comparison_images << Happo::SnapshotComparisonImage::Gutter.new(offset, sprite)
|
77
|
+
offset += gutter_width
|
78
|
+
comparison_images << Happo::SnapshotComparisonImage::Before.new(offset, sprite)
|
79
|
+
offset += width
|
80
|
+
comparison_images << Happo::SnapshotComparisonImage::Gutter.new(offset, sprite)
|
81
|
+
offset += gutter_width
|
82
|
+
comparison_images << Happo::SnapshotComparisonImage::Overlayed.new(offset, sprite)
|
83
|
+
offset += width
|
84
|
+
comparison_images << Happo::SnapshotComparisonImage::Gutter.new(offset, sprite)
|
85
|
+
offset += gutter_width
|
86
|
+
comparison_images << Happo::SnapshotComparisonImage::After.new(offset, sprite)
|
87
|
+
|
88
|
+
[sprite, comparison_images]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Happo
|
2
|
+
module SnapshotComparisonImage
|
3
|
+
# This subclass of `SnapshotComparisonImage` knows how to draw the
|
4
|
+
# representation of the "after" image.
|
5
|
+
class After < SnapshotComparisonImage::Base
|
6
|
+
# @param y [Integer]
|
7
|
+
# @param row [Diff::LCS:ContextChange]
|
8
|
+
def render_changed_row(y, row)
|
9
|
+
render_added_row(y, row)
|
10
|
+
end
|
11
|
+
|
12
|
+
# @param y [Integer]
|
13
|
+
# @param row [Diff::LCS:ContextChange]
|
14
|
+
def render_added_row(y, row)
|
15
|
+
row.new_element.each_with_index do |pixel_after, x|
|
16
|
+
render_pixel(x, y, pixel_after)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'chunky_png'
|
2
|
+
|
3
|
+
module Happo
|
4
|
+
module SnapshotComparisonImage
|
5
|
+
# This model represents a "comparison image". Basically it's just a wrapper
|
6
|
+
# around a ChunkyPNG image with some nice methods to make life easier in the
|
7
|
+
# world of diffs.
|
8
|
+
#
|
9
|
+
# This model is never persisted.
|
10
|
+
class Base
|
11
|
+
include ChunkyPNG::Color
|
12
|
+
|
13
|
+
BASE_OPACITY = 0.1
|
14
|
+
BASE_ALPHA = (255 * BASE_OPACITY).round
|
15
|
+
BASE_DIFF_ALPHA = BASE_ALPHA * 2
|
16
|
+
|
17
|
+
MAGENTA = ChunkyPNG::Color.from_hex '#b33682'
|
18
|
+
RED = ChunkyPNG::Color.from_hex '#dc322f'
|
19
|
+
GREEN = ChunkyPNG::Color.from_hex '#859900'
|
20
|
+
|
21
|
+
# @param offset [Integer] the x-offset that this comparison image should
|
22
|
+
# use when rendering on the canvas image.
|
23
|
+
# @param canvas [ChunkyPNG::Image] The canvas image to render pixels on.
|
24
|
+
def initialize(offset, canvas)
|
25
|
+
@offset = offset
|
26
|
+
@canvas = canvas
|
27
|
+
end
|
28
|
+
|
29
|
+
# @param y [Integer]
|
30
|
+
# @param row [Diff::LCS:ContextChange]
|
31
|
+
def render_row(y, row)
|
32
|
+
if row.unchanged?
|
33
|
+
render_unchanged_row(y, row)
|
34
|
+
elsif row.deleting?
|
35
|
+
render_deleted_row(y, row)
|
36
|
+
elsif row.adding?
|
37
|
+
render_added_row(y, row)
|
38
|
+
else # changing?
|
39
|
+
render_changed_row(y, row)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# @param y [Integer]
|
44
|
+
# @param row [Diff::LCS:ContextChange]
|
45
|
+
def render_unchanged_row(y, row)
|
46
|
+
row.new_element.each_with_index do |pixel, x|
|
47
|
+
# Render the unchanged pixel as-is
|
48
|
+
render_pixel(x, y, pixel)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# @param y [Integer]
|
53
|
+
# @param row [Diff::LCS:ContextChange]
|
54
|
+
def render_changed_row(y, row)
|
55
|
+
# no default implementation
|
56
|
+
end
|
57
|
+
|
58
|
+
# @param y [Integer]
|
59
|
+
# @param row [Diff::LCS:ContextChange]
|
60
|
+
def render_added_row(y, row)
|
61
|
+
# no default implementation
|
62
|
+
end
|
63
|
+
|
64
|
+
# @param y [Integer]
|
65
|
+
# @param row [Diff::LCS:ContextChange]
|
66
|
+
def render_deleted_row(y, row)
|
67
|
+
# no default implementation
|
68
|
+
end
|
69
|
+
|
70
|
+
# Compute a score that represents the difference between 2 pixels
|
71
|
+
#
|
72
|
+
# This method simply takes the Euclidean distance between the RGBA channels
|
73
|
+
# of 2 colors over the maximum possible Euclidean distance. This gives us a
|
74
|
+
# percentage of how different the two colors are.
|
75
|
+
#
|
76
|
+
# Although it would be more perceptually accurate to calculate a proper
|
77
|
+
# Delta E in Lab colorspace, we probably don't need perceptual accuracy for
|
78
|
+
# this application, and it is nice to avoid the overhead of converting RGBA
|
79
|
+
# to Lab.
|
80
|
+
#
|
81
|
+
# @param pixel_after [Integer]
|
82
|
+
# @param pixel_before [Integer]
|
83
|
+
# @return [Float] number between 0 and 1 where 1 is completely different
|
84
|
+
# and 0 is no difference
|
85
|
+
def pixel_diff_score(pixel_after, pixel_before)
|
86
|
+
ChunkyPNG::Color::euclidean_distance_rgba(pixel_after, pixel_before) /
|
87
|
+
ChunkyPNG::Color::MAX_EUCLIDEAN_DISTANCE_RGBA
|
88
|
+
end
|
89
|
+
|
90
|
+
# @param diff_score [Float]
|
91
|
+
# @return [Integer] a number between 0 and 255 that represents the alpha
|
92
|
+
# channel of of the difference
|
93
|
+
def diff_alpha(diff_score)
|
94
|
+
(BASE_DIFF_ALPHA + ((255 - BASE_DIFF_ALPHA) * diff_score)).round
|
95
|
+
end
|
96
|
+
|
97
|
+
# Renders a pixel on the specified x and y position. Uses the offset that
|
98
|
+
# the comparison image has been configured with.
|
99
|
+
#
|
100
|
+
# @param x [Integer]
|
101
|
+
# @param y [Integer]
|
102
|
+
# @param pixel [Integer]
|
103
|
+
def render_pixel(x, y, pixel)
|
104
|
+
@canvas.set_pixel(x + @offset, y, pixel)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Happo
|
2
|
+
module SnapshotComparisonImage
|
3
|
+
# This subclass of `SnapshotComparisonImage` knows how to draw the
|
4
|
+
# representation of the "before" image.
|
5
|
+
class Before < SnapshotComparisonImage::Base
|
6
|
+
# @param y [Integer]
|
7
|
+
# @param row [Diff::LCS:ContextChange]
|
8
|
+
def render_changed_row(y, row)
|
9
|
+
render_deleted_row(y, row)
|
10
|
+
end
|
11
|
+
|
12
|
+
# @param y [Integer]
|
13
|
+
# @param row [Diff::LCS:ContextChange]
|
14
|
+
def render_deleted_row(y, row)
|
15
|
+
row.old_element.each_with_index do |pixel_before, x|
|
16
|
+
render_pixel(x, y, pixel_before)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Happo
|
2
|
+
module SnapshotComparisonImage
|
3
|
+
# This class renders a gutter-column with a color representing the type of
|
4
|
+
# change that has happened.
|
5
|
+
class Gutter < SnapshotComparisonImage::Base
|
6
|
+
WIDTH = 10
|
7
|
+
GRAY = ChunkyPNG::Color.from_hex '#cccccc'
|
8
|
+
|
9
|
+
def render_row(y, row)
|
10
|
+
WIDTH.times do |x|
|
11
|
+
render_pixel(x, y, gutter_color(row))
|
12
|
+
end
|
13
|
+
# render a two-pixel empty column
|
14
|
+
2.times do |x|
|
15
|
+
render_pixel(WIDTH - 1 - x, y, WHITE)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def gutter_color(row)
|
22
|
+
if row.unchanged?
|
23
|
+
WHITE
|
24
|
+
elsif row.deleting?
|
25
|
+
RED
|
26
|
+
elsif row.adding?
|
27
|
+
GREEN
|
28
|
+
else # changed?
|
29
|
+
GRAY
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Happo
|
2
|
+
module SnapshotComparisonImage
|
3
|
+
# This subclass of `SnapshotComparisonImage` knows how to overlay the
|
4
|
+
# after-image on top of the before-image, and render the difference in a
|
5
|
+
# scaled magenta color.
|
6
|
+
class Overlayed < SnapshotComparisonImage::Base
|
7
|
+
WHITE_OVERLAY = ChunkyPNG::Color.fade(WHITE, 1 - BASE_ALPHA)
|
8
|
+
|
9
|
+
# @param offset [Integer]
|
10
|
+
# @param canvas [ChunkyPNG::Image]
|
11
|
+
# @see SnapshotComparisonImage::Base
|
12
|
+
def initialize(offset, canvas)
|
13
|
+
@diff_pixels = {}
|
14
|
+
@faded_pixels = {}
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
# @param y [Integer]
|
19
|
+
# @param row [Diff::LCS:ContextChange]
|
20
|
+
def render_unchanged_row(y, row)
|
21
|
+
# Render translucent original pixels
|
22
|
+
row.new_element.each_with_index do |pixel, x|
|
23
|
+
render_faded_pixel(x, y, pixel)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# @param y [Integer]
|
28
|
+
# @param row [Diff::LCS:ContextChange]
|
29
|
+
def render_deleted_row(y, row)
|
30
|
+
row.old_element.each_with_index do |pixel_before, x|
|
31
|
+
render_faded_magenta_pixel(TRANSPARENT, pixel_before, x, y)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# @param y [Integer]
|
36
|
+
# @param row [Diff::LCS:ContextChange]
|
37
|
+
def render_added_row(y, row)
|
38
|
+
row.new_element.each_with_index do |pixel_after, x|
|
39
|
+
render_faded_magenta_pixel(pixel_after, TRANSPARENT, x, y)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# @param y [Integer]
|
44
|
+
# @param row [Diff::LCS:ContextChange]
|
45
|
+
def render_changed_row(y, row)
|
46
|
+
row.old_element.zip(row.new_element).each_with_index do |pixels, x|
|
47
|
+
pixel_before, pixel_after = pixels
|
48
|
+
render_faded_magenta_pixel(
|
49
|
+
pixel_after || TRANSPARENT,
|
50
|
+
pixel_before || TRANSPARENT,
|
51
|
+
x, y)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
# @param pixel_after [Integer]
|
58
|
+
# @param pixel_before [Integer]
|
59
|
+
# @param x [Integer]
|
60
|
+
# @param y [Integer]
|
61
|
+
def render_faded_magenta_pixel(pixel_after, pixel_before, x, y)
|
62
|
+
score = pixel_diff_score(pixel_after, pixel_before)
|
63
|
+
if score > 0
|
64
|
+
render_diff_pixel(x, y, score)
|
65
|
+
else
|
66
|
+
render_faded_pixel(x, y, pixel_after)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# @param x [Integer]
|
71
|
+
# @param y [Integer]
|
72
|
+
# @param score [Float]
|
73
|
+
def render_diff_pixel(x, y, score)
|
74
|
+
@diff_pixels[score] ||= compose_quick(fade(MAGENTA, diff_alpha(score)),
|
75
|
+
WHITE)
|
76
|
+
render_pixel(x, y, @diff_pixels[score])
|
77
|
+
end
|
78
|
+
|
79
|
+
# @param x [Integer]
|
80
|
+
# @param y [Integer]
|
81
|
+
# @param pixel [Integer]
|
82
|
+
def render_faded_pixel(x, y, pixel)
|
83
|
+
@faded_pixels[pixel] ||= compose_quick(WHITE_OVERLAY, pixel)
|
84
|
+
render_pixel(x, y, @faded_pixels[pixel])
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 's3'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
module Happo
|
5
|
+
class Uploader
|
6
|
+
def initialize
|
7
|
+
@s3_access_key_id = Happo::Utils.config['s3_access_key_id']
|
8
|
+
@s3_secret_access_key = Happo::Utils.config['s3_secret_access_key']
|
9
|
+
@s3_bucket_name = Happo::Utils.config['s3_bucket_name']
|
10
|
+
end
|
11
|
+
|
12
|
+
def upload_diffs
|
13
|
+
result_summary = YAML.load(File.read(File.join(
|
14
|
+
Happo::Utils.config['snapshots_folder'], 'result_summary.yaml')))
|
15
|
+
|
16
|
+
return [] if result_summary[:diff_examples].empty? &&
|
17
|
+
result_summary[:new_examples].empty?
|
18
|
+
|
19
|
+
bucket = find_or_build_bucket
|
20
|
+
dir = SecureRandom.uuid
|
21
|
+
|
22
|
+
diff_images = result_summary[:diff_examples].map do |diff|
|
23
|
+
image = bucket.objects.build(
|
24
|
+
"#{dir}/#{diff[:description]}_#{diff[:viewport]}.png")
|
25
|
+
image.content = open(Happo::Utils.path_to(diff[:description],
|
26
|
+
diff[:viewport],
|
27
|
+
'diff.png'))
|
28
|
+
image.content_type = 'image/png'
|
29
|
+
image.save
|
30
|
+
diff[:url] = image.url
|
31
|
+
diff
|
32
|
+
end
|
33
|
+
|
34
|
+
new_images = result_summary[:new_examples].map do |example|
|
35
|
+
image = bucket.objects.build(
|
36
|
+
"#{dir}/#{example[:description]}_#{example[:viewport]}.png")
|
37
|
+
image.content = open(Happo::Utils.path_to(example[:description],
|
38
|
+
example[:viewport],
|
39
|
+
'baseline.png'))
|
40
|
+
image.content_type = 'image/png'
|
41
|
+
image.save
|
42
|
+
example[:url] = image.url
|
43
|
+
example
|
44
|
+
end
|
45
|
+
|
46
|
+
html = bucket.objects.build("#{dir}/index.html")
|
47
|
+
path = File.expand_path(
|
48
|
+
File.join(File.dirname(__FILE__), 'diffs.html.erb'))
|
49
|
+
html.content = ERB.new(File.read(path)).result(binding)
|
50
|
+
html.content_type = 'text/html'
|
51
|
+
html.save
|
52
|
+
html.url
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def find_or_build_bucket
|
58
|
+
service = S3::Service.new(access_key_id: @s3_access_key_id,
|
59
|
+
secret_access_key: @s3_secret_access_key)
|
60
|
+
bucket = service.buckets.find(@s3_bucket_name)
|
61
|
+
|
62
|
+
if bucket.nil?
|
63
|
+
bucket = service.buckets.build(@s3_bucket_name)
|
64
|
+
bucket.save(location: :us)
|
65
|
+
end
|
66
|
+
|
67
|
+
bucket
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/happo/utils.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'erb'
|
3
|
+
require 'uri'
|
4
|
+
require 'base64'
|
5
|
+
|
6
|
+
module Happo
|
7
|
+
class Utils
|
8
|
+
def self.config
|
9
|
+
@@config ||= {
|
10
|
+
'snapshots_folder' => './snapshots',
|
11
|
+
'source_files' => [],
|
12
|
+
'stylesheets' => [],
|
13
|
+
'public_directories' => [],
|
14
|
+
'port' => 4567,
|
15
|
+
'driver' => :firefox,
|
16
|
+
'viewports' => {
|
17
|
+
'large' => {
|
18
|
+
'width' => 1024,
|
19
|
+
'height' => 768
|
20
|
+
},
|
21
|
+
'medium' => {
|
22
|
+
'width' => 640,
|
23
|
+
'height' => 888
|
24
|
+
},
|
25
|
+
'small' => {
|
26
|
+
'width' => 320,
|
27
|
+
'height' => 444
|
28
|
+
}
|
29
|
+
}
|
30
|
+
}.merge(config_from_file)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.config_from_file
|
34
|
+
config_file_name = ENV['HAPPO_CONFIG_FILE'] || '.happo.yaml'
|
35
|
+
YAML.load(ERB.new(File.read(config_file_name)).result)
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.normalize_description(description)
|
39
|
+
Base64.strict_encode64(description).strip
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.path_to(description, viewport_name, file_name)
|
43
|
+
File.join(
|
44
|
+
config['snapshots_folder'],
|
45
|
+
normalize_description(description),
|
46
|
+
"@#{viewport_name}",
|
47
|
+
file_name
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.construct_url(absolute_path, params = {})
|
52
|
+
query = URI.encode_www_form(params) unless params.empty?
|
53
|
+
|
54
|
+
URI::HTTP.build(host: 'localhost',
|
55
|
+
port: config['port'],
|
56
|
+
path: absolute_path,
|
57
|
+
query: query).to_s
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.current_snapshots
|
61
|
+
prepare_file = lambda do |file|
|
62
|
+
viewport_dir = File.expand_path('..', file)
|
63
|
+
description_dir = File.expand_path('..', viewport_dir)
|
64
|
+
{
|
65
|
+
description: Base64.decode64(File.basename(description_dir)),
|
66
|
+
viewport: File.basename(viewport_dir).sub('@', ''),
|
67
|
+
file: file
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
snapshots_folder = Happo::Utils.config['snapshots_folder']
|
72
|
+
diff_files = Dir.glob("#{snapshots_folder}/**/diff.png")
|
73
|
+
baselines = Dir.glob("#{snapshots_folder}/**/baseline.png")
|
74
|
+
|
75
|
+
{
|
76
|
+
diffs: diff_files.map(&prepare_file),
|
77
|
+
baselines: baselines.map(&prepare_file)
|
78
|
+
}
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Happo Debug Tool</title>
|
5
|
+
<link rel="stylesheet" href="/happo-styles.css"></link>
|
6
|
+
<script src="/happo-runner.js"></script>
|
7
|
+
<% @config['source_files'].each do |source_file| %>
|
8
|
+
<script src="/resource?file=<%= ERB::Util.url_encode(source_file) %>"></script>
|
9
|
+
<% end %>
|
10
|
+
<script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
|
11
|
+
</head>
|
12
|
+
<body>
|
13
|
+
<h1>Happo Debug Tool</h1>
|
14
|
+
<p>Click on an item to render that example in isolation.</p>
|
15
|
+
<script>
|
16
|
+
(function() {
|
17
|
+
var ul = $('<ul>');
|
18
|
+
$('body').append(ul);
|
19
|
+
$.each(happo.defined, function(_, example) {
|
20
|
+
ul.append($('<li>').append(
|
21
|
+
$('<a>', {
|
22
|
+
href: '/?description=' + encodeURIComponent(example.description)
|
23
|
+
}).text(example.description)
|
24
|
+
));
|
25
|
+
});
|
26
|
+
}());
|
27
|
+
</script>
|
28
|
+
</body>
|
29
|
+
</html>
|
@@ -0,0 +1,27 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Happo</title>
|
5
|
+
<style type="text/css">
|
6
|
+
* {
|
7
|
+
-webkit-transition: none !important;
|
8
|
+
-moz-transition: none !important;
|
9
|
+
transition: none !important;
|
10
|
+
-webkit-animation-duration: 0 !important;
|
11
|
+
-moz-animation-duration: 0s !important;
|
12
|
+
animation-duration: 0s !important;
|
13
|
+
}
|
14
|
+
</style>
|
15
|
+
|
16
|
+
<% @config['stylesheets'].each do |stylesheet| %>
|
17
|
+
<link rel="stylesheet" type="text/css"
|
18
|
+
href="/resource?file=<%= ERB::Util.url_encode(stylesheet) %>">
|
19
|
+
<% end %>
|
20
|
+
<script src="/happo-runner.js"></script>
|
21
|
+
<% @config['source_files'].each do |source_file| %>
|
22
|
+
<script src="/resource?file=<%= ERB::Util.url_encode(source_file) %>"></script>
|
23
|
+
<% end %>
|
24
|
+
</head>
|
25
|
+
<body style="background-color: #fff; margin: 0; pointer-events: none;">
|
26
|
+
</body>
|
27
|
+
</html>
|
@@ -0,0 +1,37 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Happo Review Tool</title>
|
5
|
+
<link rel="stylesheet" href="/happo-styles.css"></link>
|
6
|
+
</head>
|
7
|
+
<body>
|
8
|
+
<h1>Happo Review Tool</h1>
|
9
|
+
<h2>DIFFS</h2>
|
10
|
+
<% @snapshots[:diffs].each do |diff| %>
|
11
|
+
<h3>
|
12
|
+
<%= h diff[:description] %> @ <%= diff[:viewport] %>
|
13
|
+
</h3>
|
14
|
+
<p><img src="/resource?file=<%= ERB::Util.url_encode(diff[:file]) %>"></p>
|
15
|
+
<form style="display: inline-block"
|
16
|
+
action="/approve?description=<%= diff[:description] %>&viewport=<%= diff[:viewport] %>"
|
17
|
+
method="POST">
|
18
|
+
<button type="submit">Approve</button>
|
19
|
+
</form>
|
20
|
+
<form style="display: inline-block"
|
21
|
+
action="/reject?description=<%= diff[:description] %>&viewport=<%= diff[:viewport] %>"
|
22
|
+
method="POST">
|
23
|
+
<button type="submit">Reject</button>
|
24
|
+
</form>
|
25
|
+
<% end %>
|
26
|
+
|
27
|
+
<hr>
|
28
|
+
|
29
|
+
<h2>BASELINES</h2>
|
30
|
+
<% @snapshots[:baselines].each do |baseline| %>
|
31
|
+
<h3>
|
32
|
+
<%= h baseline[:description] %> @ <%= baseline[:viewport] %>
|
33
|
+
</h3>
|
34
|
+
<p><img src="/resource?file=<%= ERB::Util.url_encode(baseline[:file]) %>"></p>
|
35
|
+
<% end %>
|
36
|
+
</body>
|
37
|
+
</html>
|