ruby_raider 3.13.0 → 4.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 +4 -4
- data/.github/workflows/runtime.yml +38 -0
- data/.github/workflows/system_tests.yml +1 -23
- data/.reek.yml +1 -0
- data/.rspec +1 -0
- data/Gemfile +2 -0
- data/README.md +16 -49
- data/RELEASE.md +22 -0
- data/bin/raider +5 -0
- data/lib/commands/scaffolding_commands.rb +2 -25
- data/lib/commands/utility_commands.rb +23 -3
- data/lib/generators/automation/automation_generator.rb +0 -5
- data/lib/generators/automation/templates/component.tt +1 -0
- data/lib/generators/automation/templates/home.tt +2 -2
- data/lib/generators/automation/templates/login.tt +6 -26
- data/lib/generators/automation/templates/page.tt +23 -18
- data/lib/generators/automation/templates/partials/home_page_selector.tt +1 -2
- data/lib/generators/automation/templates/partials/initialize_selector.tt +0 -8
- data/lib/generators/automation/templates/partials/pdp_page_selector.tt +1 -2
- data/lib/generators/automation/templates/partials/url_methods.tt +8 -2
- data/lib/generators/automation/templates/partials/visit_method.tt +0 -5
- data/lib/generators/automation/templates/pdp.tt +1 -9
- data/lib/generators/common_generator.rb +0 -2
- data/lib/generators/generator.rb +12 -19
- data/lib/generators/helper_generator.rb +21 -19
- data/lib/generators/infrastructure/templates/azure_pipelines.tt +1 -1
- data/lib/generators/infrastructure/templates/github.tt +2 -3
- data/lib/generators/infrastructure/templates/github_appium.tt +0 -8
- data/lib/generators/invoke_generators.rb +4 -23
- data/lib/generators/menu_generator.rb +1 -9
- data/lib/generators/rspec/templates/accessibility_spec.tt +1 -9
- data/lib/generators/rspec/templates/spec.tt +8 -24
- data/lib/generators/template_renderer/partial_cache.rb +3 -1
- data/lib/generators/template_renderer.rb +1 -1
- data/lib/generators/templates/common/gemfile.tt +3 -10
- data/lib/generators/templates/common/git_ignore.tt +0 -5
- data/lib/generators/templates/common/partials/mobile_config.tt +3 -1
- data/lib/generators/templates/common/partials/web_config.tt +10 -1
- data/lib/generators/templates/common/rakefile.tt +0 -15
- data/lib/generators/templates/common/read_me.tt +357 -250
- data/lib/generators/templates/common/rubocop.tt +0 -2
- data/lib/generators/templates/helpers/driver_helper.tt +8 -0
- data/lib/generators/templates/helpers/gesture_helper.tt +78 -0
- data/lib/generators/templates/helpers/network_helper.tt +521 -0
- data/lib/generators/templates/helpers/partials/quit_driver.tt +0 -2
- data/lib/generators/templates/helpers/partials/screenshot.tt +0 -2
- data/lib/generators/templates/helpers/partials/selenium_driver.tt +5 -2
- data/lib/generators/templates/helpers/screenshot_helper.tt +490 -0
- data/lib/generators/templates/helpers/spec_helper.tt +39 -29
- data/lib/generators/templates/helpers/trace_helper.tt +240 -95
- data/lib/generators/templates/helpers/video_helper.tt +3 -8
- data/lib/ruby_raider/runtime/actionability.rb +129 -0
- data/lib/ruby_raider/runtime/actionable.rb +49 -0
- data/lib/ruby_raider/runtime/by_role.rb +55 -0
- data/lib/ruby_raider/runtime/config.rb +79 -0
- data/lib/ruby_raider/runtime/context.rb +38 -0
- data/lib/ruby_raider/runtime/errors.rb +69 -0
- data/lib/ruby_raider/runtime/finders.rb +73 -0
- data/lib/ruby_raider/runtime/js/actionability.js +134 -0
- data/lib/ruby_raider/runtime/js/role_engine.js +315 -0
- data/lib/ruby_raider/runtime/locator_converter.rb +72 -0
- data/lib/ruby_raider/runtime/matchers/base.rb +222 -0
- data/lib/ruby_raider/runtime/matchers/element.rb +183 -0
- data/lib/ruby_raider/runtime/matchers/page.rb +71 -0
- data/lib/ruby_raider/runtime/matchers/presence.rb +41 -0
- data/lib/ruby_raider/runtime/matchers/retrying_block.rb +104 -0
- data/lib/ruby_raider/runtime/observer.rb +170 -0
- data/lib/ruby_raider/runtime/role_finder.rb +83 -0
- data/lib/ruby_raider/runtime/rspec.rb +78 -0
- data/lib/ruby_raider/runtime/selectors.rb +60 -0
- data/lib/ruby_raider/runtime/strict.rb +142 -0
- data/lib/ruby_raider/runtime/wait.rb +84 -0
- data/lib/ruby_raider/runtime.rb +20 -0
- data/lib/scaffolding/name_normalizer.rb +1 -1
- data/lib/scaffolding/project_detector.rb +2 -28
- data/lib/scaffolding/scaffolding.rb +0 -20
- data/lib/scaffolding/templates/component.tt +0 -14
- data/lib/scaffolding/templates/page_object.tt +2 -14
- data/lib/scaffolding/templates/spec.tt +1 -9
- data/lib/utilities/utilities.rb +10 -8
- data/lib/version +1 -1
- data/ruby_raider.gemspec +2 -1
- data/sig/commands/scaffolding_commands.rbs +0 -2
- data/sig/commands/utility_commands.rbs +6 -0
- data/sig/generators/generator.rbs +0 -4
- data/sig/generators/helper_generator.rbs +0 -1
- data/sig/generators/menu_generator.rbs +0 -1
- data/sig/ruby_raider/runtime/actionability.rbs +25 -0
- data/sig/ruby_raider/runtime/actionable.rbs +13 -0
- data/sig/ruby_raider/runtime/by_role.rbs +15 -0
- data/sig/ruby_raider/runtime/config.rbs +26 -0
- data/sig/ruby_raider/runtime/context.rbs +12 -0
- data/sig/ruby_raider/runtime/errors.rbs +36 -0
- data/sig/ruby_raider/runtime/finders.rbs +26 -0
- data/sig/ruby_raider/runtime/locator_converter.rbs +21 -0
- data/sig/ruby_raider/runtime/matchers/base.rbs +64 -0
- data/sig/ruby_raider/runtime/matchers/element.rbs +78 -0
- data/sig/ruby_raider/runtime/matchers/page.rbs +38 -0
- data/sig/ruby_raider/runtime/matchers/presence.rbs +21 -0
- data/sig/ruby_raider/runtime/matchers/retrying_block.rbs +27 -0
- data/sig/ruby_raider/runtime/observer.rbs +46 -0
- data/sig/ruby_raider/runtime/role_finder.rbs +18 -0
- data/sig/ruby_raider/runtime/selectors.rbs +21 -0
- data/sig/ruby_raider/runtime/strict.rbs +35 -0
- data/sig/ruby_raider/runtime/wait.rbs +17 -0
- data/sig/ruby_raider/runtime.rbs +2 -0
- data/sig/scaffolding/project_detector.rbs +0 -4
- data/sig/scaffolding/scaffolding.rbs +0 -5
- data/sig/utilities/utilities.rbs +2 -1
- data/sig/vendor/selenium_webdriver.rbs +71 -0
- data/spec/commands/raider_commands_spec.rb +38 -3
- data/spec/commands/scaffolding_commands_spec.rb +27 -0
- data/spec/commands/utility_commands_spec.rb +53 -0
- data/spec/generators/gesture_helper_spec.rb +123 -0
- data/spec/generators/network_helper_spec.rb +634 -0
- data/spec/generators/screenshot_helper_spec.rb +776 -0
- data/spec/generators/template_renderer_spec.rb +0 -4
- data/spec/generators/trace_helper_spec.rb +489 -97
- data/spec/generators/video_helper_spec.rb +0 -9
- data/spec/integration/commands/browser_update_after_creation_spec.rb +0 -8
- data/spec/integration/commands/scaffolding_commands_spec.rb +12 -70
- data/spec/integration/commands/utility_commands_spec.rb +19 -41
- data/spec/integration/content/azure_content_spec.rb +2 -6
- data/spec/integration/content/ci_content_spec.rb +1 -55
- data/spec/integration/content/common_content_spec.rb +1 -118
- data/spec/integration/content/config_content_spec.rb +34 -31
- data/spec/integration/content/gemfile_content_spec.rb +7 -72
- data/spec/integration/content/helper_content_spec.rb +105 -99
- data/spec/integration/content/page_content_spec.rb +85 -87
- data/spec/integration/content/readme_content_spec.rb +134 -75
- data/spec/integration/content/rspec_reporter_content_spec.rb +2 -2
- data/spec/integration/content/rubocop_content_spec.rb +1 -1
- data/spec/integration/content/syntax_validation_spec.rb +2 -2
- data/spec/integration/content/test_content_spec.rb +23 -147
- data/spec/integration/end_to_end_features_spec.rb +1 -43
- data/spec/integration/end_to_end_spec.rb +319 -326
- data/spec/integration/generators/automation_generator_spec.rb +3 -37
- data/spec/integration/generators/axe_addon_spec.rb +2 -35
- data/spec/integration/generators/azure_generator_spec.rb +1 -1
- data/spec/integration/generators/common_generator_spec.rb +1 -40
- data/spec/integration/generators/config_features_spec.rb +1 -24
- data/spec/integration/generators/github_generator_spec.rb +0 -12
- data/spec/integration/generators/helpers_generator_spec.rb +74 -80
- data/spec/integration/generators/invoke_generators_spec.rb +54 -45
- data/spec/integration/generators/rspec_generator_spec.rb +0 -6
- data/spec/integration/scaffolding_e2e_spec.rb +17 -179
- data/spec/integration/spec_helper.rb +4 -6
- data/spec/integration/trace_format_checker.rb +11 -2
- data/spec/integration/trace_page_server.rb +72 -5
- data/spec/menus/menu_generator_spec.rb +14 -39
- data/spec/runtime/actionability_spec.rb +207 -0
- data/spec/runtime/actionable_spec.rb +125 -0
- data/spec/runtime/browser/actionability_script_browser_spec.rb +150 -0
- data/spec/runtime/browser/actionable_browser_spec.rb +98 -0
- data/spec/runtime/browser/finders_browser_spec.rb +148 -0
- data/spec/runtime/browser/matchers_browser_spec.rb +67 -0
- data/spec/runtime/browser/retrying_block_browser_spec.rb +37 -0
- data/spec/runtime/browser/role_engine_browser_spec.rb +184 -0
- data/spec/runtime/browser/strict_browser_spec.rb +38 -0
- data/spec/runtime/by_role_spec.rb +52 -0
- data/spec/runtime/config_spec.rb +79 -0
- data/spec/runtime/context_spec.rb +73 -0
- data/spec/runtime/errors_spec.rb +89 -0
- data/spec/runtime/finders_spec.rb +165 -0
- data/spec/runtime/fixtures/actionability.html +70 -0
- data/spec/runtime/fixtures/locators.html +46 -0
- data/spec/runtime/fixtures/matchers.html +33 -0
- data/spec/runtime/fixtures/roles.html +162 -0
- data/spec/runtime/matchers/base_spec.rb +165 -0
- data/spec/runtime/matchers/element_spec.rb +277 -0
- data/spec/runtime/matchers/page_spec.rb +102 -0
- data/spec/runtime/matchers/presence_spec.rb +59 -0
- data/spec/runtime/matchers/retrying_block_spec.rb +165 -0
- data/spec/runtime/observer_spec.rb +341 -0
- data/spec/runtime/role_finder_spec.rb +156 -0
- data/spec/runtime/role_query_spec.rb +75 -0
- data/spec/runtime/selectors_spec.rb +135 -0
- data/spec/runtime/strict_spec.rb +257 -0
- data/spec/runtime/support/browser.rb +48 -0
- data/spec/runtime/support/fake_bridge.rb +117 -0
- data/spec/runtime/wait_spec.rb +71 -0
- data/spec/scaffolding/name_normalizer_spec.rb +3 -2
- data/spec/scaffolding/scaffold_project_detector_spec.rb +4 -43
- data/spec/system/selenium_spec.rb +1 -5
- data/spec/system/support/system_test_helper.rb +2 -5
- data/spec/utilities/headless_config_spec.rb +0 -7
- data/spec/utilities/utilities_spec.rb +24 -0
- metadata +113 -48
- data/lib/generators/automation/templates/header.tt +0 -9
- data/lib/generators/cucumber/cucumber_generator.rb +0 -58
- data/lib/generators/cucumber/templates/accessibility_feature.tt +0 -5
- data/lib/generators/cucumber/templates/accessibility_steps.tt +0 -17
- data/lib/generators/cucumber/templates/cucumber.tt +0 -1
- data/lib/generators/cucumber/templates/env.tt +0 -7
- data/lib/generators/cucumber/templates/feature.tt +0 -21
- data/lib/generators/cucumber/templates/partials/appium_env.tt +0 -26
- data/lib/generators/cucumber/templates/partials/driver_world.tt +0 -18
- data/lib/generators/cucumber/templates/partials/mobile_steps.tt +0 -20
- data/lib/generators/cucumber/templates/partials/selenium_env.tt +0 -120
- data/lib/generators/cucumber/templates/partials/watir_env.tt +0 -48
- data/lib/generators/cucumber/templates/partials/watir_world.tt +0 -4
- data/lib/generators/cucumber/templates/partials/web_steps.tt +0 -28
- data/lib/generators/cucumber/templates/steps.tt +0 -5
- data/lib/generators/cucumber/templates/user_factory.tt +0 -15
- data/lib/generators/cucumber/templates/world.tt +0 -7
- data/lib/generators/templates/helpers/browser_helper.tt +0 -31
- data/lib/generators/templates/helpers/raider_element.tt +0 -159
- data/lib/generators/templates/helpers/wait_helper.tt +0 -611
- data/lib/scaffolding/templates/feature.tt +0 -6
- data/lib/scaffolding/templates/steps.tt +0 -19
- data/sig/generators/cucumber/cucumber_generator.rbs +0 -12
- data/spec/fixtures/watir_master/cucumber_watir/Readme.md.snapshot +0 -175
- data/spec/fixtures/watir_master/cucumber_watir/features/step_definitions/login_steps.rb.snapshot +0 -24
- data/spec/fixtures/watir_master/cucumber_watir/page_objects/abstract/page.rb.snapshot +0 -37
- data/spec/fixtures/watir_master/cucumber_watir/page_objects/pages/login.rb.snapshot +0 -33
- data/spec/fixtures/watir_master/rspec_watir/Readme.md.snapshot +0 -174
- data/spec/fixtures/watir_master/rspec_watir/page_objects/abstract/page.rb.snapshot +0 -37
- data/spec/fixtures/watir_master/rspec_watir/page_objects/pages/login.rb.snapshot +0 -33
- data/spec/fixtures/watir_master/rspec_watir/spec/login_page_spec.rb.snapshot +0 -49
- data/spec/integration/content/cucumber_reporter_content_spec.rb +0 -161
- data/spec/integration/generators/cucumber_generator_spec.rb +0 -100
- data/spec/integration/helpers/wait_helper_mobile_spec.rb +0 -281
- data/spec/integration/helpers/wait_helper_spec.rb +0 -679
- data/spec/integration/helpers/wait_helper_support.rb +0 -183
- data/spec/system/watir_spec.rb +0 -38
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'chunky_png'
|
|
4
|
+
require 'erb'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
require 'tmpdir'
|
|
7
|
+
require_relative '../../lib/generators/helper_generator'
|
|
8
|
+
require_relative '../../lib/ruby_raider/runtime'
|
|
9
|
+
|
|
10
|
+
# Renders screenshot_helper.tt for a selenium/rspec project and loads it.
|
|
11
|
+
module ScreenshotHelperRenderer
|
|
12
|
+
TEMPLATE = File.expand_path('../../lib/generators/templates/helpers/screenshot_helper.tt', __dir__)
|
|
13
|
+
|
|
14
|
+
def render_and_load_screenshot_helper
|
|
15
|
+
generator = HelpersGenerator.new(%w[selenium rspec proj])
|
|
16
|
+
rendered = ERB.new(File.read(TEMPLATE), trim_mode: '-').result(generator.instance_eval { binding })
|
|
17
|
+
Dir.mktmpdir('screenshot-helper') do |dir|
|
|
18
|
+
path = File.join(dir, 'screenshot_helper.rb')
|
|
19
|
+
File.write(path, rendered)
|
|
20
|
+
Object.send(:remove_const, :ScreenshotHelper) if Object.const_defined?(:ScreenshotHelper)
|
|
21
|
+
load path
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
RSpec.describe 'ScreenshotHelper' do
|
|
27
|
+
include ScreenshotHelperRenderer
|
|
28
|
+
|
|
29
|
+
before { render_and_load_screenshot_helper }
|
|
30
|
+
|
|
31
|
+
it 'raises Mismatch as a failed expectation' do
|
|
32
|
+
expect(ScreenshotHelper::Mismatch.ancestors).to include(RSpec::Expectations::ExpectationNotMetError)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
describe '.config' do
|
|
36
|
+
around { |example| Dir.mktmpdir { |dir| Dir.chdir(dir) { example.run } } }
|
|
37
|
+
|
|
38
|
+
it 'uses the defaults without a config file' do
|
|
39
|
+
expect(ScreenshotHelper.config).to eq(threshold: 0.2, max_diff_pixels: 0, max_diff_ratio: nil)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'uses the defaults when config.yml has no screenshots block' do
|
|
43
|
+
FileUtils.mkdir_p('config')
|
|
44
|
+
File.write('config/config.yml', "browser: chrome\n")
|
|
45
|
+
expect(ScreenshotHelper.config).to eq(threshold: 0.2, max_diff_pixels: 0, max_diff_ratio: nil)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def config_with(block)
|
|
49
|
+
FileUtils.mkdir_p('config')
|
|
50
|
+
File.write('config/config.yml', "screenshots:\n#{block}")
|
|
51
|
+
ScreenshotHelper.config
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'falls back to the default with one warning for an invalid value' do
|
|
55
|
+
ScreenshotHelper.instance_variable_get(:@warned).clear
|
|
56
|
+
bad = " threshold: 2\n max_diff_pixels: -1\n max_diff_ratio: lots\n"
|
|
57
|
+
expect { 2.times { expect(config_with(bad)).to eq(ScreenshotHelper::DEFAULTS) } }
|
|
58
|
+
.to output(/threshold: 2.*\n.*max_diff_pixels: -1.*\n.*max_diff_ratio: "lots"/).to_stderr
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'warns once per bad value, not on every call' do
|
|
62
|
+
ScreenshotHelper.instance_variable_get(:@warned).clear
|
|
63
|
+
expect { 3.times { config_with(" threshold: high\n") } }.to output(/\A[^\n]*threshold[^\n]*\n\z/).to_stderr
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'treats a blank threshold as invalid and a blank ratio as none' do
|
|
67
|
+
ScreenshotHelper.instance_variable_get(:@warned).clear
|
|
68
|
+
expect { expect(config_with(" threshold:\n max_diff_ratio:\n")).to eq(ScreenshotHelper::DEFAULTS) }
|
|
69
|
+
.to output(/threshold/).to_stderr
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'accepts a whole number threshold and a float max_diff_pixels is invalid' do
|
|
73
|
+
ScreenshotHelper.instance_variable_get(:@warned).clear
|
|
74
|
+
expect { expect(config_with(" threshold: 1\n max_diff_pixels: 2.5\n")).to include(threshold: 1, max_diff_pixels: 0) }
|
|
75
|
+
.to output(/max_diff_pixels/).to_stderr
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'reads the screenshots block over the defaults' do
|
|
79
|
+
FileUtils.mkdir_p('config')
|
|
80
|
+
File.write('config/config.yml', "screenshots:\n threshold: 0.1\n max_diff_ratio: 0.001\n")
|
|
81
|
+
expect(ScreenshotHelper.config).to eq(threshold: 0.1, max_diff_pixels: 0, max_diff_ratio: 0.001)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
describe 'ScreenshotHelper::Diff' do
|
|
86
|
+
def png(width, height, color = ChunkyPNG::Color::WHITE)
|
|
87
|
+
image = ChunkyPNG::Image.new(width, height, color)
|
|
88
|
+
yield image if block_given?
|
|
89
|
+
image.to_blob
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def decode(bytes)
|
|
93
|
+
ScreenshotHelper::Diff.decode(bytes)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def compare(expected, actual, threshold: 0.2)
|
|
97
|
+
ScreenshotHelper::Diff.count(decode(expected), decode(actual), threshold: threshold)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def diff_image(expected, actual, threshold: 0.2)
|
|
101
|
+
ChunkyPNG::Image.from_blob(
|
|
102
|
+
ScreenshotHelper::Diff.image(decode(expected), decode(actual), threshold: threshold)
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it 'finds no difference between identical images' do
|
|
107
|
+
result = compare(png(10, 10), png(10, 10))
|
|
108
|
+
expect([result.diff_pixels, result.diff_ratio, result.size_changed?]).to eq([0, 0.0, false])
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it 'counts one changed pixel and its share of the image' do
|
|
112
|
+
changed = png(10, 10) { |image| image[3, 4] = ChunkyPNG::Color::BLACK }
|
|
113
|
+
result = compare(png(10, 10), changed)
|
|
114
|
+
expect([result.diff_pixels, result.diff_ratio]).to eq([1, 0.01])
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it 'lets the threshold decide whether a small shade counts' do
|
|
118
|
+
grey = ChunkyPNG::Color.rgb(0x80, 0x80, 0x80)
|
|
119
|
+
slightly = ChunkyPNG::Color.rgb(0x82, 0x82, 0x82)
|
|
120
|
+
expected = png(4, 4, grey)
|
|
121
|
+
actual = png(4, 4, grey) { |image| image[0, 0] = slightly }
|
|
122
|
+
expect(compare(expected, actual, threshold: 0.2).diff_pixels).to eq(0)
|
|
123
|
+
expect(compare(expected, actual, threshold: 0).diff_pixels).to eq(1)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it 'reports a size change without counting pixels' do
|
|
127
|
+
result = compare(png(10, 10), png(10, 12))
|
|
128
|
+
expect([result.size_changed?, result.expected_size, result.actual_size, result.diff_pixels, result.diff_ratio])
|
|
129
|
+
.to eq([true, [10, 10], [10, 12], 0, 0.0])
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it 'draws both images side by side after a size change' do
|
|
133
|
+
diff = diff_image(png(10, 10), png(10, 12))
|
|
134
|
+
expect([diff.width, diff.height]).to eq([20, 12])
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it 'does not compare colours of equal pixels' do
|
|
138
|
+
allow(ScreenshotHelper::Diff).to receive(:delta).and_call_original
|
|
139
|
+
compare(png(10, 10), png(10, 10, ChunkyPNG::Color::WHITE))
|
|
140
|
+
expect(ScreenshotHelper::Diff).not_to have_received(:delta)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it 'blends transparent pixels over white before comparing' do
|
|
144
|
+
transparent = png(5, 5, ChunkyPNG::Color::TRANSPARENT)
|
|
145
|
+
expect(compare(transparent, png(5, 5), threshold: 0).diff_pixels).to eq(0)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it 'paints a changed pixel red and the rest grey in the diff image' do
|
|
149
|
+
changed = png(10, 10) { |image| image[3, 4] = ChunkyPNG::Color.rgb(0, 0, 0) }
|
|
150
|
+
diff = diff_image(png(10, 10), changed)
|
|
151
|
+
expect(diff[3, 4]).to eq(ChunkyPNG::Color.rgb(255, 0, 0))
|
|
152
|
+
expect(ChunkyPNG::Color.r(diff[0, 0])).to eq(ChunkyPNG::Color.g(diff[0, 0]))
|
|
153
|
+
expect(diff[0, 0]).not_to eq(diff[3, 4])
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it 'raises Unreadable for bytes that are not a PNG' do
|
|
157
|
+
expect { decode('garbage') }.to raise_error(ScreenshotHelper::Diff::Unreadable)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
describe 'ScreenshotHelper::Baseline' do
|
|
162
|
+
around { |example| Dir.mktmpdir { |dir| Dir.chdir(dir) { example.run } } }
|
|
163
|
+
|
|
164
|
+
it 'accepts plain and nested names' do
|
|
165
|
+
%w[home checkout/summary a-b_c].each do |name|
|
|
166
|
+
expect { ScreenshotHelper::Baseline.validate_name!(name) }.not_to raise_error
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
it 'rejects names that could leave the baseline folder or are malformed' do
|
|
171
|
+
['../x', '/x', 'a//b', 'a.png', '', 'a/', 'a' * 101, nil].each do |name|
|
|
172
|
+
expect { ScreenshotHelper::Baseline.validate_name!(name) }.to raise_error(ArgumentError)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
it 'accepts a name of exactly 100 characters' do
|
|
177
|
+
expect { ScreenshotHelper::Baseline.validate_name!('a' * 100) }.not_to raise_error
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
it 'refuses a bad name before anything is written' do
|
|
181
|
+
expect { ScreenshotHelper::Baseline.new('../x', browser: 'chrome') }.to raise_error(ArgumentError)
|
|
182
|
+
expect(Dir.children('.')).to be_empty
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
it 'builds a relative path under the browser folder' do
|
|
186
|
+
baseline = ScreenshotHelper::Baseline.new('checkout/summary', browser: 'edge')
|
|
187
|
+
expect(baseline.path).to eq('screenshots/baselines/edge/checkout/summary.png')
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
it 'writes atomically, creating folders and leaving no temp file' do
|
|
191
|
+
baseline = ScreenshotHelper::Baseline.new('a/b', browser: 'chrome')
|
|
192
|
+
expect(baseline.exist?).to be(false)
|
|
193
|
+
baseline.write('png bytes')
|
|
194
|
+
expect(baseline.exist?).to be(true)
|
|
195
|
+
expect(baseline.read).to eq('png bytes')
|
|
196
|
+
expect(Dir.children('screenshots/baselines/chrome/a')).to eq(['b.png'])
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
it 'resolves the path against root' do
|
|
200
|
+
Dir.mktmpdir do |root|
|
|
201
|
+
ScreenshotHelper::Baseline.new('x', browser: 'chrome', root: root).write('1')
|
|
202
|
+
expect(File.read(File.join(root, 'screenshots/baselines/chrome/x.png'))).to eq('1')
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
describe '.browser' do
|
|
208
|
+
around { |example| Dir.mktmpdir { |dir| Dir.chdir(dir) { example.run } } }
|
|
209
|
+
|
|
210
|
+
def browser_from(value)
|
|
211
|
+
FileUtils.mkdir_p('config')
|
|
212
|
+
File.write('config/config.yml', "browser: #{value}\n")
|
|
213
|
+
ScreenshotHelper.browser
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
it 'names the folder after the configured browser, without the microsoft prefix' do
|
|
217
|
+
expect(%w[chrome MicrosoftEdge edge Firefox].map { |value| browser_from(value) })
|
|
218
|
+
.to eq(%w[chrome edge edge firefox])
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
it 'falls back to chrome without a config' do
|
|
222
|
+
expect(ScreenshotHelper.browser).to eq('chrome')
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
it 'falls back to chrome for a value that could leave the baselines folder' do
|
|
226
|
+
['../../x', 'a/b', '"my browser"', '.'].each { |value| expect(browser_from(value)).to eq('chrome') }
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
describe '.update? and .ci?' do
|
|
231
|
+
around do |example|
|
|
232
|
+
saved = ENV.to_h.slice('SCREENSHOTS', 'CI')
|
|
233
|
+
example.run
|
|
234
|
+
ensure
|
|
235
|
+
%w[SCREENSHOTS CI].each { |key| ENV.delete(key) }
|
|
236
|
+
ENV.update(saved)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
it 'updates only when SCREENSHOTS=update, in any case' do
|
|
240
|
+
ENV.delete('SCREENSHOTS')
|
|
241
|
+
expect(ScreenshotHelper.update?).to be(false)
|
|
242
|
+
ENV['SCREENSHOTS'] = 'Update'
|
|
243
|
+
expect(ScreenshotHelper.update?).to be(true)
|
|
244
|
+
ENV['SCREENSHOTS'] = 'no'
|
|
245
|
+
expect(ScreenshotHelper.update?).to be(false)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
it 'detects CI through the reporter, or CI in the environment' do
|
|
249
|
+
hide_const('RaiderReporter')
|
|
250
|
+
ENV.delete('CI')
|
|
251
|
+
expect(ScreenshotHelper.ci?).to be(false)
|
|
252
|
+
ENV['CI'] = 'true'
|
|
253
|
+
expect(ScreenshotHelper.ci?).to be(true)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
it 'uses the reporter detector when it is there' do
|
|
257
|
+
ENV.delete('CI')
|
|
258
|
+
detector = instance_double('detector', ci: { provider: 'github' })
|
|
259
|
+
stub_const('RaiderReporter::CiDetector', class_double('RaiderReporter::CiDetector', new: detector))
|
|
260
|
+
expect(ScreenshotHelper.ci?).to be(true)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
it 'falls back to the environment when the detector fails' do
|
|
264
|
+
ENV['CI'] = '1'
|
|
265
|
+
stub_const('RaiderReporter::CiDetector', Class.new { define_method(:initialize) { raise 'boom' } })
|
|
266
|
+
expect(ScreenshotHelper.ci?).to be(true)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
describe '.decide' do
|
|
271
|
+
def decide(exists:, matched:, on_ci: false, update: false)
|
|
272
|
+
allow(ScreenshotHelper).to receive_messages(ci?: on_ci, update?: update)
|
|
273
|
+
ScreenshotHelper.decide(baseline_exists: exists, matched: matched)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
it 'creates a missing baseline locally' do
|
|
277
|
+
expect(decide(exists: false, matched: false)).to eq(:created)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
it 'fails a missing baseline on CI unless asked to update' do
|
|
281
|
+
expect(decide(exists: false, matched: false, on_ci: true)).to eq(:missing_on_ci)
|
|
282
|
+
expect(decide(exists: false, matched: false, on_ci: true, update: true)).to eq(:created)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
it 'updates a mismatch only when asked to' do
|
|
286
|
+
expect(decide(exists: true, matched: false, update: true)).to eq(:updated)
|
|
287
|
+
expect(decide(exists: true, matched: false)).to eq(:failed)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
it 'passes a match, update or not' do
|
|
291
|
+
expect(decide(exists: true, matched: true)).to eq(:passed)
|
|
292
|
+
expect(decide(exists: true, matched: true, update: true)).to eq(:passed)
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
describe '#match_screenshot' do
|
|
297
|
+
around { |example| Dir.mktmpdir { |dir| Dir.chdir(dir) { example.run } } }
|
|
298
|
+
|
|
299
|
+
before do
|
|
300
|
+
ScreenshotHelper.reset
|
|
301
|
+
allow(ScreenshotHelper).to receive_messages(ci?: false, update?: false)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Answers like a Selenium driver: queued PNGs (the last one repeats), scripts logged as events.
|
|
305
|
+
let(:driver_class) do
|
|
306
|
+
Class.new do
|
|
307
|
+
attr_reader :events, :captures
|
|
308
|
+
|
|
309
|
+
def initialize(*pngs, ratio: 2, failing: false)
|
|
310
|
+
@pngs = pngs
|
|
311
|
+
@ratio = ratio
|
|
312
|
+
@failing = failing
|
|
313
|
+
@events = []
|
|
314
|
+
@captures = 0
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def screenshot_as(_format)
|
|
318
|
+
@events << :capture
|
|
319
|
+
raise 'capture failed' if @failing
|
|
320
|
+
|
|
321
|
+
@captures += 1
|
|
322
|
+
@pngs[[@captures - 1, @pngs.length - 1].min]
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def execute_async_script(_script)
|
|
326
|
+
@events << :fonts
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def execute_script(script, *_args)
|
|
330
|
+
@events << :add_style if script.include?('appendChild')
|
|
331
|
+
@events << :remove_style if script.include?('.remove()')
|
|
332
|
+
@events << :scroll if script.include?('scrollIntoView')
|
|
333
|
+
raise 'no ratio' if script.include?('devicePixelRatio') && @ratio.nil?
|
|
334
|
+
|
|
335
|
+
@ratio if script.include?('devicePixelRatio')
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def png(width = 10, height = 10, color = ChunkyPNG::Color::WHITE)
|
|
341
|
+
image = ChunkyPNG::Image.new(width, height, color)
|
|
342
|
+
yield image if block_given?
|
|
343
|
+
image.to_blob
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def different
|
|
347
|
+
png { |image| image[1, 1] = ChunkyPNG::Color::BLACK }
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def host_for(driver)
|
|
351
|
+
Class.new do
|
|
352
|
+
include ScreenshotHelper
|
|
353
|
+
|
|
354
|
+
attr_reader :driver
|
|
355
|
+
|
|
356
|
+
def initialize(driver)
|
|
357
|
+
@driver = driver
|
|
358
|
+
end
|
|
359
|
+
end.new(driver)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def baseline_path(name = 'home', browser = 'chrome')
|
|
363
|
+
"screenshots/baselines/#{browser}/#{name}.png"
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def store_baseline(bytes, name = 'home')
|
|
367
|
+
FileUtils.mkdir_p(File.dirname(baseline_path(name)))
|
|
368
|
+
File.binwrite(baseline_path(name), bytes)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
it 'passes when the page matches the baseline' do
|
|
372
|
+
store_baseline(png)
|
|
373
|
+
driver = driver_class.new(png)
|
|
374
|
+
expect(host_for(driver).match_screenshot('home')).to be(true)
|
|
375
|
+
expect(driver.captures).to eq(1)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
it 'treats names that differ only in case as the same screenshot' do
|
|
379
|
+
host = host_for(driver_class.new(png))
|
|
380
|
+
host.match_screenshot('Home')
|
|
381
|
+
expect { host.match_screenshot('home') }.to raise_error(ArgumentError, /already used/)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
it 'lets an explicit max_diff_ratio: nil override a configured ratio' do
|
|
385
|
+
FileUtils.mkdir_p('config')
|
|
386
|
+
File.write('config/config.yml', "screenshots:\n max_diff_ratio: 0.05\n")
|
|
387
|
+
store_baseline(png)
|
|
388
|
+
expect(host_for(driver_class.new(different)).match_screenshot('home', timeout: 0)).to be(true)
|
|
389
|
+
ScreenshotHelper.reset
|
|
390
|
+
expect { host_for(driver_class.new(different)).match_screenshot('home', max_diff_ratio: nil, timeout: 0) }
|
|
391
|
+
.to raise_error(ScreenshotHelper::Mismatch)
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
it 'draws the diff image once, for a final failure only' do
|
|
395
|
+
store_baseline(png)
|
|
396
|
+
allow(ScreenshotHelper::Diff).to receive(:image).and_call_original
|
|
397
|
+
host_for(driver_class.new(different, different, png)).match_screenshot('home', timeout: 5)
|
|
398
|
+
expect(ScreenshotHelper::Diff).not_to have_received(:image)
|
|
399
|
+
ScreenshotHelper.reset
|
|
400
|
+
driver = driver_class.new(different)
|
|
401
|
+
expect { host_for(driver).match_screenshot('home', timeout: 0.6) }.to raise_error(ScreenshotHelper::Mismatch)
|
|
402
|
+
expect(driver.captures).to be >= 2
|
|
403
|
+
expect(ScreenshotHelper::Diff).to have_received(:image).once
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
it 'decodes the baseline once however many attempts it takes' do
|
|
407
|
+
store_baseline(png)
|
|
408
|
+
allow(ScreenshotHelper::Diff).to receive(:decode).and_call_original
|
|
409
|
+
host_for(driver_class.new(different, different, png)).match_screenshot('home', timeout: 5)
|
|
410
|
+
expect(ScreenshotHelper::Diff).to have_received(:decode).exactly(4).times
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
it 'retries a size change and passes when the element settles' do
|
|
414
|
+
store_baseline(png)
|
|
415
|
+
expect(host_for(driver_class.new(png(5, 5), png)).match_screenshot('home', timeout: 5)).to be(true)
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
it 'does not start another capture after the deadline' do
|
|
419
|
+
store_baseline(png)
|
|
420
|
+
driver = driver_class.new(different)
|
|
421
|
+
expect { host_for(driver).match_screenshot('home', timeout: 0) }.to raise_error(ScreenshotHelper::Mismatch)
|
|
422
|
+
expect(driver.captures).to eq(1)
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
it 'retries after a difference and passes once the page matches' do
|
|
426
|
+
store_baseline(png)
|
|
427
|
+
driver = driver_class.new(different, png)
|
|
428
|
+
expect(host_for(driver).match_screenshot('home', timeout: 5)).to be(true)
|
|
429
|
+
expect(driver.captures).to eq(2)
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
it 'raises Mismatch after the timeout, naming the baseline, the pixels and the limits' do
|
|
433
|
+
store_baseline(png)
|
|
434
|
+
driver = driver_class.new(different)
|
|
435
|
+
expect { host_for(driver).match_screenshot('home', timeout: 0.3) }
|
|
436
|
+
.to raise_error(ScreenshotHelper::Mismatch) { |error|
|
|
437
|
+
expect(error.message).to include(baseline_path, '1 pixel (1.00%) differ', 'threshold 0.2', '0 pixels')
|
|
438
|
+
}
|
|
439
|
+
expect(driver.captures).to be >= 2
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
it 'honours max_diff_pixels and max_diff_ratio' do
|
|
443
|
+
store_baseline(png)
|
|
444
|
+
expect(host_for(driver_class.new(different)).match_screenshot('home', max_diff_pixels: 1, timeout: 0)).to be(true)
|
|
445
|
+
ScreenshotHelper.reset
|
|
446
|
+
expect(host_for(driver_class.new(different)).match_screenshot('home', max_diff_ratio: 0.05, timeout: 0)).to be(true)
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
it 'reads the defaults from config.yml' do
|
|
450
|
+
FileUtils.mkdir_p('config')
|
|
451
|
+
File.write('config/config.yml', "screenshots:\n max_diff_pixels: 5\n")
|
|
452
|
+
store_baseline(png)
|
|
453
|
+
expect(host_for(driver_class.new(different)).match_screenshot('home', timeout: 0)).to be(true)
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
it 'fails a size change naming both sizes and the device pixel ratio, without comparing pixels' do
|
|
457
|
+
store_baseline(png(20, 10))
|
|
458
|
+
driver = driver_class.new(png(40, 20), ratio: 2)
|
|
459
|
+
expect { host_for(driver).match_screenshot('home', timeout: 0) }
|
|
460
|
+
.to raise_error(ScreenshotHelper::Mismatch, /size changed: baseline 20x10, now 40x20 \(device pixel ratio 2\)/)
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
it 'leaves the device pixel ratio out when it cannot be read' do
|
|
464
|
+
store_baseline(png(20, 10))
|
|
465
|
+
expect { host_for(driver_class.new(png(40, 20), ratio: nil)).match_screenshot('home', timeout: 0) }
|
|
466
|
+
.to raise_error(ScreenshotHelper::Mismatch) { |error|
|
|
467
|
+
expect(error.message).to include('size changed: baseline 20x10, now 40x20')
|
|
468
|
+
expect(error.message).not_to include('device pixel ratio')
|
|
469
|
+
}
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
it 'writes a missing baseline outside CI and passes' do
|
|
473
|
+
expect(host_for(driver_class.new(png)).match_screenshot('home')).to be(true)
|
|
474
|
+
expect(File.binread(baseline_path)).to eq(png)
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
it 'fails a missing baseline on CI and writes nothing' do
|
|
478
|
+
allow(ScreenshotHelper).to receive(:ci?).and_return(true)
|
|
479
|
+
driver = driver_class.new(png)
|
|
480
|
+
expect { host_for(driver).match_screenshot('home') }.to raise_error(
|
|
481
|
+
ScreenshotHelper::Mismatch,
|
|
482
|
+
"no baseline for 'home' on chrome; run it locally or with SCREENSHOTS=update and commit the file"
|
|
483
|
+
)
|
|
484
|
+
expect(File.exist?('screenshots')).to be(false)
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
it 'writes a missing baseline on CI when asked to update' do
|
|
488
|
+
allow(ScreenshotHelper).to receive_messages(ci?: true, update?: true)
|
|
489
|
+
expect(host_for(driver_class.new(png)).match_screenshot('home')).to be(true)
|
|
490
|
+
expect(File.exist?(baseline_path)).to be(true)
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
it 'rewrites a differing baseline when asked to update' do
|
|
494
|
+
allow(ScreenshotHelper).to receive(:update?).and_return(true)
|
|
495
|
+
store_baseline(png)
|
|
496
|
+
expect(host_for(driver_class.new(different)).match_screenshot('home', timeout: 0)).to be(true)
|
|
497
|
+
expect(File.binread(baseline_path)).to eq(different)
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
it 'fails an unreadable baseline and leaves the file alone' do
|
|
501
|
+
store_baseline('not a png')
|
|
502
|
+
expect { host_for(driver_class.new(png)).match_screenshot('home', timeout: 0) }
|
|
503
|
+
.to raise_error(ScreenshotHelper::Mismatch, /baseline .*home\.png is not a readable PNG/)
|
|
504
|
+
expect(File.binread(baseline_path)).to eq('not a png')
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
it 'replaces an unreadable baseline when asked to update' do
|
|
508
|
+
allow(ScreenshotHelper).to receive(:update?).and_return(true)
|
|
509
|
+
store_baseline('not a png')
|
|
510
|
+
expect(host_for(driver_class.new(png)).match_screenshot('home')).to be(true)
|
|
511
|
+
expect(File.binread(baseline_path)).to eq(png)
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
it 'refuses a bad name before any capture' do
|
|
515
|
+
driver = driver_class.new(png)
|
|
516
|
+
expect { host_for(driver).match_screenshot('../x') }.to raise_error(ArgumentError)
|
|
517
|
+
expect(driver.events).to be_empty
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
it 'refuses the same name twice in one test until reset' do
|
|
521
|
+
store_baseline(png)
|
|
522
|
+
host = host_for(driver_class.new(png))
|
|
523
|
+
host.match_screenshot('home')
|
|
524
|
+
expect { host.match_screenshot('home') }.to raise_error(ArgumentError, /already used/)
|
|
525
|
+
ScreenshotHelper.reset
|
|
526
|
+
expect(host.match_screenshot('home')).to be(true)
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
it 'compares an element, scrolled into view' do
|
|
530
|
+
store_baseline(png)
|
|
531
|
+
element = instance_double('element')
|
|
532
|
+
allow(element).to receive(:screenshot_as).with(:png).and_return(png)
|
|
533
|
+
driver = driver_class.new(different)
|
|
534
|
+
expect(host_for(driver).match_screenshot('home', element: element)).to be(true)
|
|
535
|
+
expect(driver.captures).to eq(0)
|
|
536
|
+
expect(driver.events).to include(:scroll)
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
it 'adds the settle stylesheet before the capture and removes it after' do
|
|
540
|
+
store_baseline(png)
|
|
541
|
+
driver = driver_class.new(png)
|
|
542
|
+
host_for(driver).match_screenshot('home')
|
|
543
|
+
expect(driver.events.reject { |event| event == :scroll })
|
|
544
|
+
.to eq(%i[fonts add_style capture remove_style])
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
it 'removes the settle stylesheet when the capture raises' do
|
|
548
|
+
store_baseline(png)
|
|
549
|
+
driver = driver_class.new(png, failing: true)
|
|
550
|
+
expect { host_for(driver).match_screenshot('home') }.to raise_error(RuntimeError, 'capture failed')
|
|
551
|
+
expect(driver.events.last).to eq(:remove_style)
|
|
552
|
+
end
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
describe 'trace step' do
|
|
556
|
+
around { |example| Dir.mktmpdir { |dir| Dir.chdir(dir) { example.run } } }
|
|
557
|
+
|
|
558
|
+
let(:seen) { [] }
|
|
559
|
+
let(:driver_class) do
|
|
560
|
+
Class.new do
|
|
561
|
+
def initialize(png)
|
|
562
|
+
@png = png
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def screenshot_as(_format)
|
|
566
|
+
@png
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def execute_async_script(_script); end
|
|
570
|
+
|
|
571
|
+
def execute_script(*); end
|
|
572
|
+
end
|
|
573
|
+
end
|
|
574
|
+
let(:host_class) do
|
|
575
|
+
Class.new do
|
|
576
|
+
include ScreenshotHelper
|
|
577
|
+
attr_reader :driver
|
|
578
|
+
|
|
579
|
+
def initialize(driver)
|
|
580
|
+
@driver = driver
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
end
|
|
584
|
+
let(:white) { ChunkyPNG::Image.new(10, 10, ChunkyPNG::Color::WHITE).to_blob }
|
|
585
|
+
let(:black) { ChunkyPNG::Image.new(10, 10, ChunkyPNG::Color::BLACK).to_blob }
|
|
586
|
+
|
|
587
|
+
before do
|
|
588
|
+
ScreenshotHelper.reset
|
|
589
|
+
allow(ScreenshotHelper).to receive_messages(ci?: false, update?: false)
|
|
590
|
+
seen = self.seen
|
|
591
|
+
observer = Object.new
|
|
592
|
+
observer.define_singleton_method(:screenshot_compared) { |**details| seen.replace([:screenshot_compared, details]) }
|
|
593
|
+
Raider.observer = observer
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
after do
|
|
597
|
+
Raider.observer = nil
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
def match(png, name: 'home', **options)
|
|
601
|
+
host_class.new(driver_class.new(png)).match_screenshot(name, timeout: 0, **options)
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
def store(bytes)
|
|
605
|
+
FileUtils.mkdir_p('screenshots/baselines/chrome')
|
|
606
|
+
File.binwrite('screenshots/baselines/chrome/home.png', bytes)
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
it 'reports a created comparison as passed with the actual PNG' do
|
|
610
|
+
match(white)
|
|
611
|
+
expect(seen).to eq([:screenshot_compared, { name: 'home', status: :passed, png: white, error: nil }])
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
it 'reports a passed comparison once' do
|
|
615
|
+
store(white)
|
|
616
|
+
match(white)
|
|
617
|
+
expect(seen).to eq([:screenshot_compared, { name: 'home', status: :passed, png: white, error: nil }])
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
it 'reports a failed comparison before the Mismatch is raised, with the last actual PNG' do
|
|
621
|
+
store(white)
|
|
622
|
+
expect { match(black) }.to raise_error(ScreenshotHelper::Mismatch) { |raised|
|
|
623
|
+
expect(seen.last).to include(name: 'home', status: :failed, png: black)
|
|
624
|
+
expect(seen.last[:error]).to be(raised)
|
|
625
|
+
}
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
it 'reports an updated comparison as passed' do
|
|
629
|
+
store(white)
|
|
630
|
+
allow(ScreenshotHelper).to receive(:update?).and_return(true)
|
|
631
|
+
match(black)
|
|
632
|
+
expect(seen).to eq([:screenshot_compared, { name: 'home', status: :passed, png: black, error: nil }])
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
it 'reports nothing when no screenshot was taken' do
|
|
636
|
+
allow(ScreenshotHelper).to receive(:ci?).and_return(true)
|
|
637
|
+
expect { match(white) }.to raise_error(ScreenshotHelper::Mismatch, /no baseline/)
|
|
638
|
+
expect(seen).to be_empty
|
|
639
|
+
end
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
describe 'Raider.report' do
|
|
643
|
+
after { Raider.observer = nil }
|
|
644
|
+
|
|
645
|
+
it 'passes the hook to an observer that has it and leaves one without it alone, silently' do
|
|
646
|
+
seen = []
|
|
647
|
+
Raider.observer = Class.new { define_method(:screenshot_compared) { |**details| seen << details } }.new
|
|
648
|
+
Raider.report(:screenshot_compared, name: 'a', status: :passed, png: 'x')
|
|
649
|
+
expect(seen).to eq([{ name: 'a', status: :passed, png: 'x' }])
|
|
650
|
+
|
|
651
|
+
other = Object.new
|
|
652
|
+
Raider.observer = other
|
|
653
|
+
expect { Raider.report(:screenshot_compared, name: 'a', status: :passed, png: 'x') }.not_to output.to_stderr
|
|
654
|
+
expect(Raider.observer).to be(other)
|
|
655
|
+
end
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
describe 'ScreenshotHelper::Report' do
|
|
659
|
+
let(:attached) { [] }
|
|
660
|
+
let(:recorded) { [] }
|
|
661
|
+
let(:reporter) do
|
|
662
|
+
files = attached
|
|
663
|
+
records = recorded
|
|
664
|
+
Module.new do
|
|
665
|
+
define_singleton_method(:attach) do |name, path, **options|
|
|
666
|
+
files << [name, path, options, File.exist?(path), File.binread(path)]
|
|
667
|
+
end
|
|
668
|
+
define_singleton_method(:comparison) { |**fields| records << fields }
|
|
669
|
+
end
|
|
670
|
+
end
|
|
671
|
+
let(:diff_result) do
|
|
672
|
+
ScreenshotHelper::Diff::Result.new(1, 0.01, [10, 10], [10, 10])
|
|
673
|
+
end
|
|
674
|
+
let(:options) { { threshold: 0.2, max_diff_pixels: 0, max_diff_ratio: nil } }
|
|
675
|
+
|
|
676
|
+
def report(status, result: diff_result, images: {})
|
|
677
|
+
ScreenshotHelper::Report.call(name: 'home', status: status, baseline: 'screenshots/baselines/chrome/home.png',
|
|
678
|
+
result: result, options: options, images: images)
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
it 'attaches the three images of a failed comparison, then records it' do
|
|
682
|
+
stub_const('RaiderReporter', reporter)
|
|
683
|
+
report(:failed, images: { expected: 'e', actual: 'a', diff: 'd' })
|
|
684
|
+
expect(attached.map { |file| file.first(3) }).to eq(
|
|
685
|
+
[['comparison:home:expected', attached[0][1], { type: :png }],
|
|
686
|
+
['comparison:home:actual', attached[1][1], { type: :png }],
|
|
687
|
+
['comparison:home:diff', attached[2][1], { type: :png }]]
|
|
688
|
+
)
|
|
689
|
+
expect(attached.map { |file| file[4] }).to eq(%w[e a d])
|
|
690
|
+
expect(recorded).to eq(
|
|
691
|
+
[{ name: 'home', status: 'failed', baseline: 'screenshots/baselines/chrome/home.png', diff_pixels: 1,
|
|
692
|
+
diff_ratio: 0.01, threshold: 0.2, max_diff_pixels: 0, max_diff_ratio: nil,
|
|
693
|
+
expected_size: [10, 10], actual_size: [10, 10] }]
|
|
694
|
+
)
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
it 'attaches only the actual image of a created comparison, with zero differences' do
|
|
698
|
+
stub_const('RaiderReporter', reporter)
|
|
699
|
+
report(:created, result: nil, images: { actual: ChunkyPNG::Image.new(7, 5).to_blob })
|
|
700
|
+
expect(attached.map(&:first)).to eq(['comparison:home:actual'])
|
|
701
|
+
expect(recorded.first).to include(status: 'created', diff_pixels: 0, diff_ratio: 0.0, expected_size: nil,
|
|
702
|
+
actual_size: [7, 5])
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
it 'gives an updated comparison without a diff result the size of the actual image' do
|
|
706
|
+
stub_const('RaiderReporter', reporter)
|
|
707
|
+
report(:updated, result: nil, images: { actual: ChunkyPNG::Image.new(3, 4).to_blob })
|
|
708
|
+
expect(recorded.first).to include(status: 'updated', expected_size: nil, actual_size: [3, 4])
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
it 'keeps the real sizes of an updated comparison that had a diff result' do
|
|
712
|
+
stub_const('RaiderReporter', reporter)
|
|
713
|
+
report(:updated, images: { actual: ChunkyPNG::Image.new(3, 4).to_blob })
|
|
714
|
+
expect(recorded.first).to include(expected_size: [10, 10], actual_size: [10, 10])
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
it 'attaches nothing for a passed comparison but still records it' do
|
|
718
|
+
stub_const('RaiderReporter', reporter)
|
|
719
|
+
report(:passed)
|
|
720
|
+
expect(attached).to be_empty
|
|
721
|
+
expect(recorded.map { |fields| fields[:status] }).to eq(['passed'])
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
it 'removes the temp files afterwards' do
|
|
725
|
+
stub_const('RaiderReporter', reporter)
|
|
726
|
+
report(:failed, images: { expected: 'e', actual: 'a', diff: 'd' })
|
|
727
|
+
expect(attached.map { |file| file[3] }).to all(be(true))
|
|
728
|
+
expect(attached.map { |file| File.exist?(file[1]) }).to all(be(false))
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
it 'attaches only and warns once when the reporter has no comparison method' do
|
|
732
|
+
old = Module.new
|
|
733
|
+
files = attached
|
|
734
|
+
old.define_singleton_method(:attach) { |name, path, **_| files << [name, path] }
|
|
735
|
+
stub_const('RaiderReporter', old)
|
|
736
|
+
expect { 2.times { report(:failed, images: { actual: 'a' }) } }
|
|
737
|
+
.to output(/raider_reporter.*comparison/m).to_stderr
|
|
738
|
+
expect(attached.length).to eq(2)
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
it 'does nothing without raider_reporter' do
|
|
742
|
+
hide_const('RaiderReporter')
|
|
743
|
+
expect { report(:failed, images: { actual: 'a' }) }.not_to raise_error
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
def host_with(png)
|
|
747
|
+
driver = Struct.new(:png) do
|
|
748
|
+
def screenshot_as(_format)
|
|
749
|
+
png
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
def execute_async_script(_script); end
|
|
753
|
+
|
|
754
|
+
def execute_script(*); end
|
|
755
|
+
end.new(png)
|
|
756
|
+
Class.new { include ScreenshotHelper }.new.tap { |host| host.define_singleton_method(:driver) { driver } }
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
it 'is called by match_screenshot before it raises' do
|
|
760
|
+
stub_const('RaiderReporter', reporter)
|
|
761
|
+
ScreenshotHelper.reset
|
|
762
|
+
allow(ScreenshotHelper).to receive_messages(ci?: false, update?: false)
|
|
763
|
+
Dir.mktmpdir do |dir|
|
|
764
|
+
Dir.chdir(dir) do
|
|
765
|
+
FileUtils.mkdir_p('screenshots/baselines/chrome')
|
|
766
|
+
File.binwrite('screenshots/baselines/chrome/home.png', ChunkyPNG::Image.new(4, 4, ChunkyPNG::Color::WHITE).to_blob)
|
|
767
|
+
black = ChunkyPNG::Image.new(4, 4, ChunkyPNG::Color::BLACK).to_blob
|
|
768
|
+
host = host_with(black)
|
|
769
|
+
expect { host.match_screenshot('home', timeout: 0) }.to raise_error(ScreenshotHelper::Mismatch)
|
|
770
|
+
end
|
|
771
|
+
end
|
|
772
|
+
expect(attached.map(&:first)).to eq(%w[comparison:home:expected comparison:home:actual comparison:home:diff])
|
|
773
|
+
expect(recorded.first).to include(status: 'failed', diff_pixels: 16)
|
|
774
|
+
end
|
|
775
|
+
end
|
|
776
|
+
end
|