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,490 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'chunky_png'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'set'
|
|
6
|
+
require 'rspec/expectations'
|
|
7
|
+
require 'ruby_raider/runtime'
|
|
8
|
+
require 'yaml'
|
|
9
|
+
|
|
10
|
+
# Visual regression for the browser: match_screenshot('home') compares the page (or an element)
|
|
11
|
+
# with a baseline PNG in screenshots/baselines/<browser>/ and fails the test on a difference.
|
|
12
|
+
# Tolerances live in config/config.yml under screenshots:. SCREENSHOTS=update rewrites baselines.
|
|
13
|
+
# Not wanted? Delete this file and the lines that load or include ScreenshotHelper.
|
|
14
|
+
module ScreenshotHelper
|
|
15
|
+
CONFIG_PATH = File.join('config', 'config.yml')
|
|
16
|
+
DEFAULTS = { threshold: 0.2, max_diff_pixels: 0, max_diff_ratio: nil }.freeze
|
|
17
|
+
# Told apart from nil: an explicit max_diff_ratio: nil switches the configured ratio off.
|
|
18
|
+
UNSET = Object.new.freeze
|
|
19
|
+
VALID = {
|
|
20
|
+
threshold: ->(value) { value.is_a?(Numeric) && value.between?(0, 1) },
|
|
21
|
+
max_diff_pixels: ->(value) { value.is_a?(Integer) && value >= 0 },
|
|
22
|
+
max_diff_ratio: ->(value) { value.nil? || (value.is_a?(Numeric) && value.between?(0, 1)) }
|
|
23
|
+
}.freeze
|
|
24
|
+
BROWSER_FOLDER = /\A[a-z0-9_-]+\z/.freeze
|
|
25
|
+
|
|
26
|
+
@names_used = Set.new
|
|
27
|
+
@warned = Set.new
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
# The names compared in this test, so two comparisons never share a baseline.
|
|
31
|
+
attr_reader :names_used
|
|
32
|
+
|
|
33
|
+
def reset
|
|
34
|
+
names_used.clear
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Case-insensitive: Home and home are the same file on a case-insensitive disk.
|
|
38
|
+
def claim(name)
|
|
39
|
+
raise ArgumentError, "screenshot name '#{name}' was already used in this test" unless names_used.add?(name.downcase)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# One warning per setting and value for the whole run.
|
|
43
|
+
# rubocop:disable Style/StderrPuts
|
|
44
|
+
def warn_once(key, value)
|
|
45
|
+
return unless @warned.add?([key, value])
|
|
46
|
+
|
|
47
|
+
$stderr.puts("[screenshots] ignoring #{key}: #{value.inspect} in config/config.yml " \
|
|
48
|
+
"(using #{DEFAULTS[key].inspect}); see the README for the allowed values")
|
|
49
|
+
end
|
|
50
|
+
# rubocop:enable Style/StderrPuts
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Raised when a screenshot differs from its baseline; counts as a failed expectation.
|
|
54
|
+
class Mismatch < RSpec::Expectations::ExpectationNotMetError; end
|
|
55
|
+
|
|
56
|
+
# The screenshots: block of config/config.yml over the defaults.
|
|
57
|
+
def self.config
|
|
58
|
+
settings = DEFAULTS.dup
|
|
59
|
+
file_settings.each do |key, value|
|
|
60
|
+
next unless settings.key?(key)
|
|
61
|
+
|
|
62
|
+
VALID.fetch(key).call(value) ? settings[key] = value : warn_once(key, value)
|
|
63
|
+
end
|
|
64
|
+
settings
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.file_settings
|
|
68
|
+
block = read_config['screenshots']
|
|
69
|
+
block.is_a?(Hash) ? block.transform_keys(&:to_sym) : {}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.read_config
|
|
73
|
+
return {} unless File.exist?(CONFIG_PATH)
|
|
74
|
+
|
|
75
|
+
loaded = YAML.safe_load(File.read(CONFIG_PATH), permitted_classes: [Symbol])
|
|
76
|
+
loaded.is_a?(Hash) ? loaded : {}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The baseline folder: the configured browser, lower case, without a microsoft prefix (MicrosoftEdge is edge).
|
|
80
|
+
def self.browser
|
|
81
|
+
name = read_config['browser'].to_s.downcase.delete_prefix('microsoft')
|
|
82
|
+
name.match?(BROWSER_FOLDER) ? name : 'chrome'
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.update?
|
|
86
|
+
ENV['SCREENSHOTS'].to_s.downcase == 'update'
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The reporter's CI detection when it is loaded, and CI in the environment.
|
|
90
|
+
def self.ci?
|
|
91
|
+
detected = defined?(RaiderReporter::CiDetector) ? RaiderReporter::CiDetector.new.ci : nil
|
|
92
|
+
!detected.nil? || !ENV['CI'].to_s.empty?
|
|
93
|
+
rescue StandardError
|
|
94
|
+
!ENV['CI'].to_s.empty?
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# What a comparison comes to: :passed, :failed, :created, :updated or :missing_on_ci.
|
|
98
|
+
def self.decide(baseline_exists:, matched:)
|
|
99
|
+
return missing_status unless baseline_exists
|
|
100
|
+
return :passed if matched
|
|
101
|
+
|
|
102
|
+
update? ? :updated : :failed
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def self.missing_status
|
|
106
|
+
ci? && !update? ? :missing_on_ci : :created
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Pixel comparison of two decoded PNGs and the picture of what differs. Internal.
|
|
110
|
+
module Diff
|
|
111
|
+
class Unreadable < StandardError; end
|
|
112
|
+
|
|
113
|
+
Result = Struct.new(:diff_pixels, :diff_ratio, :expected_size, :actual_size) do
|
|
114
|
+
def size_changed?
|
|
115
|
+
expected_size != actual_size
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
WHITE = ChunkyPNG::Color::WHITE
|
|
120
|
+
RED = ChunkyPNG::Color.rgb(255, 0, 0)
|
|
121
|
+
# pixelmatch: the largest possible YIQ delta; a pixel differs above this times threshold squared.
|
|
122
|
+
MAX_DELTA = 35_215.0
|
|
123
|
+
FADE = 0.1
|
|
124
|
+
|
|
125
|
+
def self.decode(png)
|
|
126
|
+
ChunkyPNG::Image.from_blob(png)
|
|
127
|
+
rescue StandardError => e
|
|
128
|
+
raise Unreadable, e.message
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def self.size_of(image)
|
|
132
|
+
[image.width, image.height]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# How many pixels differ. Equal pixels are skipped, so only the changed ones cost a colour comparison.
|
|
136
|
+
def self.count(expected, actual, threshold:)
|
|
137
|
+
sizes = [size_of(expected), size_of(actual)]
|
|
138
|
+
return Result.new(0, 0.0, *sizes) unless sizes.first == sizes.last
|
|
139
|
+
|
|
140
|
+
limit = MAX_DELTA * (threshold**2)
|
|
141
|
+
differing = 0
|
|
142
|
+
expected.pixels.each_with_index do |pixel, index|
|
|
143
|
+
other = actual.pixels[index]
|
|
144
|
+
differing += 1 if pixel != other && delta(pixel, other) > limit
|
|
145
|
+
end
|
|
146
|
+
Result.new(differing, differing.fdiv(expected.pixels.length), *sizes)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# The picture of a difference as PNG bytes: both images side by side after a size change,
|
|
150
|
+
# otherwise the differing pixels in red over a faded copy of the expected image.
|
|
151
|
+
def self.image(expected, actual, threshold:)
|
|
152
|
+
return side_by_side(expected, actual).to_blob unless size_of(expected) == size_of(actual)
|
|
153
|
+
|
|
154
|
+
limit = MAX_DELTA * (threshold**2)
|
|
155
|
+
canvas = ChunkyPNG::Image.new(expected.width, expected.height, WHITE)
|
|
156
|
+
expected.pixels.each_with_index do |pixel, index|
|
|
157
|
+
other = actual.pixels[index]
|
|
158
|
+
canvas.pixels[index] = pixel != other && delta(pixel, other) > limit ? RED : faded(pixel)
|
|
159
|
+
end
|
|
160
|
+
canvas.to_blob
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def self.side_by_side(expected, actual)
|
|
164
|
+
canvas = ChunkyPNG::Image.new(expected.width + actual.width, [expected.height, actual.height].max, WHITE)
|
|
165
|
+
canvas.compose!(expected, 0, 0)
|
|
166
|
+
canvas.compose!(actual, expected.width, 0)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def self.delta(first, second)
|
|
170
|
+
dy, di, dq = yiq(first).zip(yiq(second)).map { |one, two| one - two }
|
|
171
|
+
(0.5053 * dy * dy) + (0.299 * di * di) + (0.1957 * dq * dq)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def self.yiq(pixel)
|
|
175
|
+
red, green, blue = blended(pixel)
|
|
176
|
+
[(red * 0.29889531) + (green * 0.58662247) + (blue * 0.11448223),
|
|
177
|
+
(red * 0.59597799) - (green * 0.27417610) - (blue * 0.32180189),
|
|
178
|
+
(red * 0.21147017) - (green * 0.52261711) + (blue * 0.31114694)]
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Semi-transparent pixels count as they look over a white page.
|
|
182
|
+
def self.blended(pixel)
|
|
183
|
+
alpha = ChunkyPNG::Color.a(pixel)
|
|
184
|
+
[ChunkyPNG::Color.r(pixel), ChunkyPNG::Color.g(pixel), ChunkyPNG::Color.b(pixel)].map do |channel|
|
|
185
|
+
255 + ((channel - 255) * alpha / 255.0)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def self.faded(pixel)
|
|
190
|
+
luma = yiq(pixel).first
|
|
191
|
+
grey = (255 + ((luma - 255) * FADE)).round.clamp(0, 255)
|
|
192
|
+
ChunkyPNG::Color.rgb(grey, grey, grey)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Where a baseline lives and how it is written. Internal.
|
|
197
|
+
class Baseline
|
|
198
|
+
NAME_PATTERN = %r{\A[A-Za-z0-9_-]+(/[A-Za-z0-9_-]+)*\z}.freeze
|
|
199
|
+
MAX_NAME = 100
|
|
200
|
+
|
|
201
|
+
def self.validate_name!(name)
|
|
202
|
+
return if name.is_a?(String) && name.length <= MAX_NAME && name.match?(NAME_PATTERN)
|
|
203
|
+
|
|
204
|
+
raise ArgumentError, 'screenshot name must be letters, digits, - and _ with / for folders, ' \
|
|
205
|
+
"at most #{MAX_NAME} characters; got #{name.inspect}"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def initialize(name, browser:, root: Dir.pwd)
|
|
209
|
+
self.class.validate_name!(name)
|
|
210
|
+
@path = "screenshots/baselines/#{browser}/#{name}.png"
|
|
211
|
+
@root = root
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
attr_reader :path
|
|
215
|
+
|
|
216
|
+
def exist?
|
|
217
|
+
File.file?(full_path)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def read
|
|
221
|
+
File.binread(full_path)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Through a temp file and a rename, so two processes never leave half a PNG.
|
|
225
|
+
def write(png)
|
|
226
|
+
FileUtils.mkdir_p(File.dirname(full_path))
|
|
227
|
+
temp = "#{full_path}.#{Process.pid}.#{rand(36**6).to_s(36)}.tmp"
|
|
228
|
+
File.binwrite(temp, png)
|
|
229
|
+
File.rename(temp, full_path)
|
|
230
|
+
ensure
|
|
231
|
+
FileUtils.rm_f(temp) if temp
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
private
|
|
235
|
+
|
|
236
|
+
def full_path
|
|
237
|
+
File.join(@root, @path)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Takes one settled screenshot of the viewport or an element. Internal.
|
|
242
|
+
class Capture
|
|
243
|
+
STYLE_ID = 'raider-screenshot-settle'
|
|
244
|
+
FONTS_SCRIPT = <<~JS
|
|
245
|
+
var done = arguments[arguments.length - 1];
|
|
246
|
+
var timer = setTimeout(done, 2000);
|
|
247
|
+
(document.fonts ? document.fonts.ready : Promise.resolve()).then(function () { clearTimeout(timer); done(); });
|
|
248
|
+
JS
|
|
249
|
+
ADD_STYLE = <<~JS.tr("\n", ' ')
|
|
250
|
+
var style = document.createElement('style'); style.id = '#{STYLE_ID}';
|
|
251
|
+
style.textContent = '*,*::before,*::after{animation:none!important;transition:none!important;caret-color:transparent!important}';
|
|
252
|
+
document.head.appendChild(style);
|
|
253
|
+
JS
|
|
254
|
+
REMOVE_STYLE = "var style = document.getElementById('#{STYLE_ID}'); if (style) { style.remove(); }"
|
|
255
|
+
SCROLL = "arguments[0].scrollIntoView({ block: 'center', inline: 'center' });"
|
|
256
|
+
|
|
257
|
+
def initialize(driver, element)
|
|
258
|
+
@driver = driver
|
|
259
|
+
@element = element
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Fonts loaded, animations and the caret stopped; the stylesheet is gone again afterwards.
|
|
263
|
+
def png
|
|
264
|
+
@driver.execute_async_script(FONTS_SCRIPT)
|
|
265
|
+
@driver.execute_script(ADD_STYLE)
|
|
266
|
+
@element ? element_png : @driver.screenshot_as(:png)
|
|
267
|
+
ensure
|
|
268
|
+
remove_style
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
private
|
|
272
|
+
|
|
273
|
+
def element_png
|
|
274
|
+
@driver.execute_script(SCROLL, @element)
|
|
275
|
+
@element.screenshot_as(:png)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def remove_style
|
|
279
|
+
@driver.execute_script(REMOVE_STYLE)
|
|
280
|
+
rescue StandardError
|
|
281
|
+
nil
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Hands a comparison to raider_reporter while the test runs: its images, then the record. Internal.
|
|
286
|
+
module Report
|
|
287
|
+
IMAGES = %i[expected actual diff].freeze
|
|
288
|
+
|
|
289
|
+
# rubocop:disable Style/StderrPuts
|
|
290
|
+
def self.call(name:, status:, baseline:, result:, options:, images:)
|
|
291
|
+
return unless defined?(RaiderReporter)
|
|
292
|
+
|
|
293
|
+
attach_images(name, images)
|
|
294
|
+
record(name, status, baseline, result || sizes_of(images[:actual]), options)
|
|
295
|
+
rescue StandardError => e
|
|
296
|
+
$stderr.puts("[screenshots] could not report '#{name}': #{e.class}: #{e.message}")
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# One trace step per comparison, when the trace helper is watching. Never raises into the test.
|
|
300
|
+
def self.traced(name:, status:, png:, error: nil)
|
|
301
|
+
return unless png
|
|
302
|
+
|
|
303
|
+
Raider.report(:screenshot_compared, name: name, status: status == :failed ? :failed : :passed, png: png,
|
|
304
|
+
error: error)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def self.attach_images(name, images)
|
|
308
|
+
Dir.mktmpdir('raider-screenshot') do |folder|
|
|
309
|
+
IMAGES.each do |kind|
|
|
310
|
+
next unless images[kind]
|
|
311
|
+
|
|
312
|
+
path = File.join(folder, "#{kind}.png")
|
|
313
|
+
File.binwrite(path, images[kind])
|
|
314
|
+
RaiderReporter.attach("comparison:#{name}:#{kind}", path, type: :png)
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def self.record(name, status, baseline, result, options)
|
|
320
|
+
unless RaiderReporter.respond_to?(:comparison)
|
|
321
|
+
return if @warned
|
|
322
|
+
|
|
323
|
+
@warned = true
|
|
324
|
+
return $stderr.puts('[screenshots] this raider_reporter has no comparison method; update it to list comparisons in the report')
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
RaiderReporter.comparison(**fields(name, status, baseline, result), **options)
|
|
328
|
+
end
|
|
329
|
+
# rubocop:enable Style/StderrPuts
|
|
330
|
+
|
|
331
|
+
# With no diff result (created, or updated over an unreadable baseline) only the actual image has a size.
|
|
332
|
+
def self.sizes_of(png)
|
|
333
|
+
return unless png
|
|
334
|
+
|
|
335
|
+
Diff::Result.new(0, 0.0, nil, Diff.size_of(Diff.decode(png)))
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def self.fields(name, status, baseline, result)
|
|
339
|
+
{ name: name, status: status.to_s, baseline: baseline,
|
|
340
|
+
diff_pixels: result ? result.diff_pixels : 0, diff_ratio: result ? result.diff_ratio : 0.0,
|
|
341
|
+
expected_size: result&.expected_size, actual_size: result&.actual_size }
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# One match_screenshot call: capture until the page matches the baseline or time runs out. Internal.
|
|
346
|
+
class Comparison
|
|
347
|
+
PAUSE = 0.25
|
|
348
|
+
|
|
349
|
+
def initialize(driver, name, element:, options:, timeout:)
|
|
350
|
+
@driver = driver
|
|
351
|
+
@name = name
|
|
352
|
+
@element = element
|
|
353
|
+
@options = options
|
|
354
|
+
@timeout = timeout
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def call
|
|
358
|
+
Baseline.validate_name!(@name)
|
|
359
|
+
ScreenshotHelper.claim(@name)
|
|
360
|
+
@baseline = Baseline.new(@name, browser: ScreenshotHelper.browser)
|
|
361
|
+
@baseline.exist? ? compare : create
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
private
|
|
365
|
+
|
|
366
|
+
def create
|
|
367
|
+
status = ScreenshotHelper.decide(baseline_exists: false, matched: false)
|
|
368
|
+
raise Mismatch, missing_message if status == :missing_on_ci
|
|
369
|
+
|
|
370
|
+
actual = capture
|
|
371
|
+
@baseline.write(actual)
|
|
372
|
+
finish(status, nil, actual: actual)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def compare
|
|
376
|
+
expected = Diff.decode(@baseline.read)
|
|
377
|
+
actual_png, actual, result = capture_until_match(expected)
|
|
378
|
+
status = ScreenshotHelper.decide(baseline_exists: true, matched: within_limits?(result))
|
|
379
|
+
@baseline.write(actual_png) if status == :updated
|
|
380
|
+
finish(status, result, images(status, expected, actual, actual_png))
|
|
381
|
+
rescue Diff::Unreadable
|
|
382
|
+
unreadable
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def unreadable
|
|
386
|
+
raise Mismatch, "baseline #{@baseline.path} is not a readable PNG" unless ScreenshotHelper.update?
|
|
387
|
+
|
|
388
|
+
actual = capture
|
|
389
|
+
@baseline.write(actual)
|
|
390
|
+
finish(:updated, nil, actual: actual)
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# The expected, actual and diff pictures exist only for a failure; the diff is drawn once, here.
|
|
394
|
+
def images(status, expected, actual, actual_png)
|
|
395
|
+
return { actual: actual_png } if status == :updated
|
|
396
|
+
return {} unless status == :failed
|
|
397
|
+
|
|
398
|
+
{ expected: @baseline.read, actual: actual_png,
|
|
399
|
+
diff: Diff.image(expected, actual, threshold: @options[:threshold]) }
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def finish(status, result, images)
|
|
403
|
+
error = Mismatch.new(failure_message(result)) if status == :failed
|
|
404
|
+
Report.call(name: @name, status: status, baseline: @baseline.path, result: result,
|
|
405
|
+
options: @options, images: images)
|
|
406
|
+
Report.traced(name: @name, status: status, png: @actual, error: error)
|
|
407
|
+
raise error if error
|
|
408
|
+
|
|
409
|
+
true
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def capture
|
|
413
|
+
@actual = Capture.new(@driver, @element).png
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# Capture and count until the page matches or the deadline passes; the deadline is checked after every
|
|
417
|
+
# attempt, before the next capture. Cheap on every attempt: no diff picture.
|
|
418
|
+
def capture_until_match(expected)
|
|
419
|
+
deadline = now + (@timeout || Raider.timeout)
|
|
420
|
+
attempt = attempt_once(expected)
|
|
421
|
+
until within_limits?(attempt.last) || now >= deadline
|
|
422
|
+
sleep([[PAUSE, deadline - now].min, 0].max)
|
|
423
|
+
attempt = attempt_once(expected)
|
|
424
|
+
end
|
|
425
|
+
attempt
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def attempt_once(expected)
|
|
429
|
+
png = capture
|
|
430
|
+
actual = Diff.decode(png)
|
|
431
|
+
[png, actual, Diff.count(expected, actual, threshold: @options[:threshold])]
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def now
|
|
435
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# Within the allowed pixel count, or within the allowed share when one is set.
|
|
439
|
+
def within_limits?(result)
|
|
440
|
+
return false if result.size_changed?
|
|
441
|
+
|
|
442
|
+
ratio = @options[:max_diff_ratio]
|
|
443
|
+
result.diff_pixels <= @options[:max_diff_pixels] || (!ratio.nil? && result.diff_ratio <= ratio)
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def missing_message
|
|
447
|
+
"no baseline for '#{@name}' on #{ScreenshotHelper.browser}; " \
|
|
448
|
+
'run it locally or with SCREENSHOTS=update and commit the file'
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def failure_message(result)
|
|
452
|
+
"#{@baseline.path}: #{result.size_changed? ? size_detail(result) : pixel_detail(result)}"
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def size_detail(result)
|
|
456
|
+
ratio = device_pixel_ratio
|
|
457
|
+
"size changed: baseline #{result.expected_size.join('x')}, now #{result.actual_size.join('x')}" \
|
|
458
|
+
"#{ratio ? " (device pixel ratio #{ratio})" : ''}"
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def device_pixel_ratio
|
|
462
|
+
ratio = @driver.execute_script('return window.devicePixelRatio')
|
|
463
|
+
return unless ratio.is_a?(Numeric)
|
|
464
|
+
|
|
465
|
+
ratio == ratio.to_i ? ratio.to_i : ratio
|
|
466
|
+
rescue StandardError
|
|
467
|
+
nil
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def pixel_detail(result)
|
|
471
|
+
count = result.diff_pixels
|
|
472
|
+
"#{count} #{count == 1 ? 'pixel' : 'pixels'} (#{format('%.2f', result.diff_ratio * 100)}%) differ " \
|
|
473
|
+
"(allowed: #{allowed}, threshold #{@options[:threshold]})"
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def allowed
|
|
477
|
+
ratio = @options[:max_diff_ratio]
|
|
478
|
+
allowed = "#{@options[:max_diff_pixels]} pixels"
|
|
479
|
+
ratio ? "#{allowed} or #{format('%g', ratio * 100)}%" : allowed
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def match_screenshot(name, element: nil, threshold: nil, max_diff_pixels: nil, max_diff_ratio: ScreenshotHelper::UNSET,
|
|
484
|
+
timeout: nil)
|
|
485
|
+
given = { threshold: threshold, max_diff_pixels: max_diff_pixels }.compact
|
|
486
|
+
given[:max_diff_ratio] = max_diff_ratio unless max_diff_ratio.equal?(ScreenshotHelper::UNSET)
|
|
487
|
+
options = ScreenshotHelper.config.merge(given)
|
|
488
|
+
Comparison.new(driver, name, element: element, options: options, timeout: timeout).call
|
|
489
|
+
end
|
|
490
|
+
end
|
|
@@ -6,15 +6,18 @@ require 'axe-rspec'
|
|
|
6
6
|
require 'rspec'
|
|
7
7
|
require 'rspec/retry'
|
|
8
8
|
require 'raider_reporter/rspec'
|
|
9
|
+
require 'ruby_raider/runtime/rspec'
|
|
9
10
|
require 'tmpdir'
|
|
10
|
-
<%- if watir? -%>
|
|
11
|
-
require_relative 'browser_helper'
|
|
12
|
-
<%- else -%>
|
|
13
11
|
require_relative 'driver_helper'
|
|
14
|
-
<%- end -%>
|
|
15
12
|
require_relative 'video_helper'
|
|
16
|
-
<%- if
|
|
17
|
-
require_relative '
|
|
13
|
+
<%- if mobile? -%>
|
|
14
|
+
require_relative 'gesture_helper'
|
|
15
|
+
<%- end -%>
|
|
16
|
+
<%- if network_helper? -%>
|
|
17
|
+
require_relative 'network_helper'
|
|
18
|
+
<%- end -%>
|
|
19
|
+
<%- if screenshot_helper? -%>
|
|
20
|
+
require_relative 'screenshot_helper'
|
|
18
21
|
<%- end -%>
|
|
19
22
|
<%- if web? -%>
|
|
20
23
|
require_relative 'session_state'
|
|
@@ -32,13 +35,17 @@ module SpecHelper
|
|
|
32
35
|
config.display_try_failure_messages = true
|
|
33
36
|
config.default_retry_count = ENV.fetch('RETRY_COUNT', 0).to_i
|
|
34
37
|
config.add_formatter(RaiderReporter::RSpec::Formatter)
|
|
35
|
-
<%- if watir? -%>
|
|
36
|
-
config.include(BrowserHelper)
|
|
37
|
-
<%- else -%>
|
|
38
38
|
config.include(DriverHelper)
|
|
39
|
+
config.include(Raider::Matchers)
|
|
40
|
+
config.include(Raider::Helpers)
|
|
41
|
+
<%- if mobile? -%>
|
|
42
|
+
config.include(GestureHelper)
|
|
39
43
|
<%- end -%>
|
|
40
|
-
<%- if
|
|
41
|
-
config.include(
|
|
44
|
+
<%- if network_helper? -%>
|
|
45
|
+
config.include(NetworkHelper)
|
|
46
|
+
<%- end -%>
|
|
47
|
+
<%- if screenshot_helper? -%>
|
|
48
|
+
config.include(ScreenshotHelper)
|
|
42
49
|
<%- end -%>
|
|
43
50
|
<%- if mobile? -%>
|
|
44
51
|
config.before(:each) do
|
|
@@ -51,17 +58,12 @@ module SpecHelper
|
|
|
51
58
|
else
|
|
52
59
|
driver.manage.window.maximize
|
|
53
60
|
end
|
|
54
|
-
<%- elsif watir? -%>
|
|
55
|
-
config.before(:each) do
|
|
56
|
-
viewport = YAML.load_file('config/config.yml')['viewport']
|
|
57
|
-
if viewport
|
|
58
|
-
browser.window.resize_to(viewport['width'], viewport['height'])
|
|
59
|
-
else
|
|
60
|
-
browser.window.maximize
|
|
61
|
-
end
|
|
62
61
|
<%- end -%>
|
|
63
|
-
@video_recorder = VideoHelper.recorder_for(
|
|
62
|
+
@video_recorder = VideoHelper.recorder_for(driver)
|
|
64
63
|
@video_recorder.start
|
|
64
|
+
<%- if screenshot_helper? -%>
|
|
65
|
+
ScreenshotHelper.reset
|
|
66
|
+
<%- end -%>
|
|
65
67
|
<%- if trace_helper? -%>
|
|
66
68
|
@trace_recorder = TraceHelper.recorder_for(driver) if defined?(TraceHelper)
|
|
67
69
|
@trace_recorder&.start
|
|
@@ -71,32 +73,37 @@ module SpecHelper
|
|
|
71
73
|
|
|
72
74
|
config.before(:each, :logged_in) do
|
|
73
75
|
user = ModelFactory.for('users')['registered user']
|
|
74
|
-
account = Account.new(
|
|
75
|
-
SessionState.new(
|
|
76
|
-
valid: -> { account.tap(&:visit).
|
|
76
|
+
account = Account.new(driver)
|
|
77
|
+
SessionState.new(driver).reuse(
|
|
78
|
+
valid: -> { account.tap(&:visit).welcome_message.text == "Welcome back #{user['name']}" }
|
|
77
79
|
) do
|
|
78
|
-
login = Login.new(
|
|
80
|
+
login = Login.new(driver)
|
|
79
81
|
login.visit
|
|
80
82
|
login.login(user['username'], user['password'])
|
|
81
83
|
end
|
|
82
84
|
end
|
|
83
85
|
<%- end -%>
|
|
84
86
|
|
|
87
|
+
<%- failure_ref = network_helper? ? 'failure' : 'example.exception' -%>
|
|
85
88
|
config.after(:each) do |example|
|
|
86
89
|
example_name = example.description
|
|
90
|
+
<%- if network_helper? -%>
|
|
91
|
+
mock_error = NetworkHelper.finish(driver) if defined?(NetworkHelper)
|
|
92
|
+
failure = example.exception || mock_error
|
|
93
|
+
<%- end -%>
|
|
87
94
|
<%- if trace_helper? -%>
|
|
88
95
|
if defined?(TraceHelper)
|
|
89
96
|
attempt = example.metadata[:retry_attempts].to_i + 1
|
|
90
|
-
keep_trace = TraceHelper.keep_all? || (
|
|
97
|
+
keep_trace = TraceHelper.keep_all? || (<%= failure_ref %> && (defined?(VideoHelper) ? VideoHelper.final_attempt?(example) : true))
|
|
91
98
|
trace_test = { identifier: example.id.delete_prefix('./'), full_name: example.full_description,
|
|
92
|
-
status:
|
|
99
|
+
status: <%= failure_ref %> ? 'failed' : 'passed', attempt: attempt, error: <%= failure_ref %> }
|
|
93
100
|
trace_path = @trace_recorder&.stop(keep: keep_trace, test: trace_test)
|
|
94
101
|
end
|
|
95
102
|
<%- end -%>
|
|
96
|
-
keep_video =
|
|
103
|
+
keep_video = <%= failure_ref %> ? VideoHelper.final_attempt?(example) : false
|
|
97
104
|
video_path = @video_recorder&.stop(keep: keep_video)
|
|
98
|
-
if
|
|
99
|
-
@_exception =
|
|
105
|
+
if <%= failure_ref %>
|
|
106
|
+
@_exception = <%= failure_ref %>
|
|
100
107
|
Dir.mktmpdir do |temp_folder|
|
|
101
108
|
screenshot_path = "#{temp_folder}/#{example_name}.png"
|
|
102
109
|
<%= partial('screenshot', strip: true) %>
|
|
@@ -113,6 +120,9 @@ module SpecHelper
|
|
|
113
120
|
<%- end -%>
|
|
114
121
|
@video_recorder&.cleanup
|
|
115
122
|
<%= partial('quit_driver', strip: true) %>
|
|
123
|
+
<%- if network_helper? -%>
|
|
124
|
+
raise mock_error if mock_error
|
|
125
|
+
<%- end -%>
|
|
116
126
|
end
|
|
117
127
|
end
|
|
118
128
|
end
|