percy-selenium 1.1.2.pre.beta.0 → 1.1.3.pre.beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/README.md +133 -0
- data/lib/percy.rb +76 -2
- data/lib/version.rb +1 -1
- data/package.json +1 -1
- data/spec/lib/percy/percy_spec.rb +53 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 685b6a737cf5d8ebe9ce59dc70736248b7d5f30a6131bcc34d98ccdb7d1b6a03
|
|
4
|
+
data.tar.gz: b25796e9354d99d64ce0022263e4987cae49a57bb5e0e31443afc490240d7ec2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5e410bb024a3e3bac312041bd907e3a39a5f9f61f14450c0962dbb82af505ebec08319672e63d6e525da1679f14052c0c243bcfd818c65295bfb0c60baf53f4f
|
|
7
|
+
data.tar.gz: 46991f060f602406612f78986767bd5c5f64cfc0e21e4ef6834ee0609f113725dafd455d16d5ef3c0b3eb4b430475e0d9c4ce87a988e1328ad963914383397b5
|
data/.gitignore
CHANGED
data/README.md
CHANGED
|
@@ -62,3 +62,136 @@ $ percy exec -- [ruby test command]
|
|
|
62
62
|
- `driver` (**required**) - A selenium-webdriver driver instance
|
|
63
63
|
- `name` (**required**) - The snapshot name; must be unique to each snapshot
|
|
64
64
|
- `options` - [See per-snapshot configuration options](https://www.browserstack.com/docs/percy/take-percy-snapshots/overview#per-snapshot-configuration)
|
|
65
|
+
|
|
66
|
+
## Running Percy on Automate
|
|
67
|
+
`Percy.percy_screenshot(driver, name, options)` [ needs @percy/cli 1.27.0-beta.0+ ]
|
|
68
|
+
|
|
69
|
+
This is an example test using the `percy_screenshot` method.
|
|
70
|
+
|
|
71
|
+
``` ruby
|
|
72
|
+
require 'percy'
|
|
73
|
+
|
|
74
|
+
capabilities = {
|
|
75
|
+
'browserName' => 'chrome',
|
|
76
|
+
'bstack:options' => {
|
|
77
|
+
'userName' => '<your-username>',
|
|
78
|
+
'accessKey' => '<your-access-key>'
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
driver = Selenium::WebDriver.for(
|
|
83
|
+
:remote,
|
|
84
|
+
url: 'https://hub-cloud.browserstack.com/wd/hub',
|
|
85
|
+
capabilities: capabilities
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
driver.navigate.to "https://example.com"
|
|
89
|
+
|
|
90
|
+
# Take a Percy screenshot
|
|
91
|
+
Percy.percy_screenshot(driver, 'Screenshot 1')
|
|
92
|
+
|
|
93
|
+
driver.navigate.to "https://google.com"
|
|
94
|
+
Percy.percy_screenshot(driver, 'Screenshot 2')
|
|
95
|
+
|
|
96
|
+
driver.quit
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- `driver` (**required**) - A Selenium driver instance
|
|
100
|
+
- `name` (**required**) - The screenshot name; must be unique to each screenshot
|
|
101
|
+
- `options` (**optional**) - There are various options supported by `percy_screenshot` to serve further functionality.
|
|
102
|
+
- `sync` - Boolean value by default it falls back to `false`, Gives the processed result around screenshot [From CLI v1.28.0-beta.0+]
|
|
103
|
+
- `fullPage` - Boolean value by default it falls back to `false`, Takes full page screenshot [From CLI v1.27.6+]
|
|
104
|
+
- `freezeAnimatedImage` - Boolean value by default it falls back to `false`, you can pass `true` and Percy will freeze image based animations.
|
|
105
|
+
- `freezeImageBySelectors` - List of selectors. Images will be frozen which are passed using selectors. For this to work `freezeAnimatedImage` must be set to true.
|
|
106
|
+
- `freezeImageByXpaths` - List of xpaths. Images will be frozen which are passed using xpaths. For this to work `freezeAnimatedImage` must be set to true.
|
|
107
|
+
- `percyCSS` - Custom CSS to be added to DOM before the screenshot being taken. Note: This gets removed once the screenshot is taken.
|
|
108
|
+
- `ignoreRegionXpaths` - List of xpaths. Elements in the DOM can be ignored using xpath.
|
|
109
|
+
- `ignoreRegionSelectors` - List of selectors. Elements in the DOM can be ignored using selectors.
|
|
110
|
+
- `ignoreRegionSeleniumElements` - List of Selenium WebElement. Elements can be ignored using selenium elements.
|
|
111
|
+
- `customIgnoreRegions` - List of custom objects. Elements can be ignored using custom boundaries. Just passing a simple hash for it like below.
|
|
112
|
+
- example: `{top: 10, right: 10, bottom: 120, left: 10}`
|
|
113
|
+
- In the above example it will draw a rectangle of ignore region as per given coordinates.
|
|
114
|
+
- `top` (int): Top coordinate of the ignore region.
|
|
115
|
+
- `bottom` (int): Bottom coordinate of the ignore region.
|
|
116
|
+
- `left` (int): Left coordinate of the ignore region.
|
|
117
|
+
- `right` (int): Right coordinate of the ignore region.
|
|
118
|
+
- `considerRegionXpaths` - List of xpaths. Elements in the DOM can be considered for diffing and will be ignored by Intelli Ignore using xpaths.
|
|
119
|
+
- `considerRegionSelectors` - List of selectors. Elements in the DOM can be considered for diffing and will be ignored by Intelli Ignore using selectors.
|
|
120
|
+
- `considerRegionSeleniumElements` - List of Selenium WebElement. Elements can be considered for diffing and will be ignored by Intelli Ignore using selenium elements.
|
|
121
|
+
- `customConsiderRegions` - List of custom objects. Elements can be considered for diffing and will be ignored by Intelli Ignore using custom boundaries.
|
|
122
|
+
- example: `{top: 10, right: 10, bottom: 120, left: 10}`
|
|
123
|
+
- In the above example a rectangle of consider region will be drawn.
|
|
124
|
+
- Parameters:
|
|
125
|
+
- `top` (int): Top coordinate of the consider region.
|
|
126
|
+
- `bottom` (int): Bottom coordinate of the consider region.
|
|
127
|
+
- `left` (int): Left coordinate of the consider region.
|
|
128
|
+
- `right` (int): Right coordinate of the consider region.
|
|
129
|
+
- `regions` - Parameter that allows users to apply snapshot options to specific areas of the page. This parameter is an array where each object defines a custom region with configurations.
|
|
130
|
+
- Parameters:
|
|
131
|
+
- `elementSelector` (optional, only one of the following must be provided, if this is not provided then full page will be considered as region)
|
|
132
|
+
- `boundingBox` (hash): Defines the coordinates and size of the region.
|
|
133
|
+
- `x` (number): X-coordinate of the region.
|
|
134
|
+
- `y` (number): Y-coordinate of the region.
|
|
135
|
+
- `width` (number): Width of the region.
|
|
136
|
+
- `height` (number): Height of the region.
|
|
137
|
+
- `elementXpath` (string): The XPath selector for the element.
|
|
138
|
+
- `elementCSS` (string): The CSS selector for the element.
|
|
139
|
+
- `algorithm` (mandatory)
|
|
140
|
+
- Specifies the snapshot comparison algorithm.
|
|
141
|
+
- Allowed values: `standard`, `layout`, `ignore`, `intelliignore`.
|
|
142
|
+
- `configuration` (required for `standard` and `intelliignore` algorithms, ignored otherwise)
|
|
143
|
+
- `diffSensitivity` (number): Sensitivity level for detecting differences.
|
|
144
|
+
- `imageIgnoreThreshold` (number): Threshold for ignoring minor image differences.
|
|
145
|
+
- `carouselsEnabled` (boolean): Whether to enable carousel detection.
|
|
146
|
+
- `bannersEnabled` (boolean): Whether to enable banner detection.
|
|
147
|
+
- `adsEnabled` (boolean): Whether to enable ad detection.
|
|
148
|
+
- `assertion` (optional)
|
|
149
|
+
- Defines assertions to apply to the region.
|
|
150
|
+
- `diffIgnoreThreshold` (number): The threshold for ignoring minor differences.
|
|
151
|
+
|
|
152
|
+
### Example Usage for regions
|
|
153
|
+
|
|
154
|
+
``` ruby
|
|
155
|
+
region1 = {
|
|
156
|
+
elementSelector: {
|
|
157
|
+
elementCSS: '.ad-banner'
|
|
158
|
+
},
|
|
159
|
+
algorithm: 'intelliignore',
|
|
160
|
+
configuration: {
|
|
161
|
+
diffSensitivity: 2,
|
|
162
|
+
imageIgnoreThreshold: 0.2,
|
|
163
|
+
carouselsEnabled: true,
|
|
164
|
+
bannersEnabled: true,
|
|
165
|
+
adsEnabled: true
|
|
166
|
+
},
|
|
167
|
+
assertion: {
|
|
168
|
+
diffIgnoreThreshold: 0.4
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
# Using Percy.create_region helper
|
|
173
|
+
region2 = Percy.create_region(
|
|
174
|
+
algorithm: 'intelliignore',
|
|
175
|
+
diff_sensitivity: 3,
|
|
176
|
+
ads_enabled: true,
|
|
177
|
+
diff_ignore_threshold: 0.4
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
Percy.percy_screenshot(driver, 'Screenshot 1', regions: [region1, region2])
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Creating Percy on Automate build
|
|
184
|
+
Note: Automate Percy Token starts with `auto` keyword. The command can be triggered using `exec` keyword.
|
|
185
|
+
|
|
186
|
+
```sh-session
|
|
187
|
+
$ export PERCY_TOKEN=[your-project-token]
|
|
188
|
+
$ percy exec -- [ruby test command]
|
|
189
|
+
[percy] Percy has started!
|
|
190
|
+
[percy] [Ruby example] : Starting automate screenshot ...
|
|
191
|
+
[percy] Screenshot taken "Ruby example"
|
|
192
|
+
[percy] Stopping percy...
|
|
193
|
+
[percy] Finalized build #1: https://percy.io/[your-project]
|
|
194
|
+
[percy] Done!
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Refer to docs here: [Percy on Automate](https://www.browserstack.com/docs/percy/integrate/functional-and-visual)
|
data/lib/percy.rb
CHANGED
|
@@ -85,13 +85,16 @@ module Percy
|
|
|
85
85
|
get_serialized_dom(driver, options, percy_dom_script: percy_dom_script)
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
+
# Strip `readiness` before POSTing -- SDK-local config that the CLI
|
|
89
|
+
# already has via healthcheck.
|
|
90
|
+
post_options = options.reject { |k, _| k.to_s == 'readiness' }
|
|
88
91
|
response = fetch('percy/snapshot',
|
|
89
92
|
name: name,
|
|
90
93
|
url: driver.current_url,
|
|
91
94
|
dom_snapshot: dom_snapshot,
|
|
92
95
|
client_info: CLIENT_INFO,
|
|
93
96
|
environment_info: ENV_INFO,
|
|
94
|
-
**
|
|
97
|
+
**post_options,)
|
|
95
98
|
|
|
96
99
|
body = JSON.parse(response.body)
|
|
97
100
|
unless body['success']
|
|
@@ -113,8 +116,79 @@ module Percy
|
|
|
113
116
|
driver.manage
|
|
114
117
|
end
|
|
115
118
|
|
|
119
|
+
# Shallow-merge of global (@cli_config.snapshot.readiness) and per-snapshot
|
|
120
|
+
# (options[:readiness] / options['readiness']) readiness config. Per-snapshot
|
|
121
|
+
# keys win, unspecified global keys (notably preset: disabled) are inherited.
|
|
122
|
+
def self.resolve_readiness_config(options)
|
|
123
|
+
global = @cli_config&.dig('snapshot', 'readiness')
|
|
124
|
+
global = {} unless global.is_a?(Hash)
|
|
125
|
+
per_snapshot = options[:readiness] || options['readiness']
|
|
126
|
+
per_snapshot = {} unless per_snapshot.is_a?(Hash)
|
|
127
|
+
# Normalise symbol keys to strings so the merge collapses :preset and 'preset'.
|
|
128
|
+
[global, per_snapshot].each_with_object({}) do |hash, merged|
|
|
129
|
+
hash.each { |k, v| merged[k.to_s] = v }
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Readiness gate: runs PercyDOM.waitForReady via
|
|
134
|
+
# execute_async_script BEFORE serialize. Graceful on old CLIs that lack the
|
|
135
|
+
# method. Returns readiness diagnostics (or nil) for attachment to domSnapshot.
|
|
136
|
+
def self.wait_for_ready(driver, options)
|
|
137
|
+
readiness_config = resolve_readiness_config(options)
|
|
138
|
+
return nil if readiness_config['preset'] == 'disabled'
|
|
139
|
+
|
|
140
|
+
# Match the driver's async-script timeout to readiness.timeoutMs so a
|
|
141
|
+
# higher user-configured timeout isn't silently capped by Selenium's
|
|
142
|
+
# default (~30s) firing ScriptTimeoutException before the in-page
|
|
143
|
+
# Promise resolves.
|
|
144
|
+
timeout_ms = readiness_config['timeoutMs']
|
|
145
|
+
previous_timeout = nil
|
|
146
|
+
if timeout_ms.is_a?(Numeric) && timeout_ms > 0
|
|
147
|
+
begin
|
|
148
|
+
previous_timeout = driver.manage.timeouts.script_timeout
|
|
149
|
+
driver.manage.timeouts.script_timeout = (timeout_ms / 1000.0) + 2
|
|
150
|
+
rescue StandardError
|
|
151
|
+
previous_timeout = nil # best-effort; older Selenium / unsupported
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
begin
|
|
156
|
+
script = <<~JS
|
|
157
|
+
var cfg = #{readiness_config.to_json};
|
|
158
|
+
var done = arguments[arguments.length - 1];
|
|
159
|
+
try {
|
|
160
|
+
if (typeof PercyDOM !== 'undefined' && typeof PercyDOM.waitForReady === 'function') {
|
|
161
|
+
PercyDOM.waitForReady(cfg).then(function(r){ done(r); }).catch(function(){ done(); });
|
|
162
|
+
} else { done(); }
|
|
163
|
+
} catch (e) { done(); }
|
|
164
|
+
JS
|
|
165
|
+
driver.execute_async_script(script)
|
|
166
|
+
rescue StandardError => e
|
|
167
|
+
log("waitForReady failed, proceeding to serialize: #{e}", 'debug')
|
|
168
|
+
nil
|
|
169
|
+
ensure
|
|
170
|
+
if previous_timeout
|
|
171
|
+
begin
|
|
172
|
+
driver.manage.timeouts.script_timeout = previous_timeout
|
|
173
|
+
rescue StandardError
|
|
174
|
+
# best-effort
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
116
180
|
def self.get_serialized_dom(driver, options, percy_dom_script: nil)
|
|
117
|
-
|
|
181
|
+
# Readiness gate before serialize. Graceful on old CLI.
|
|
182
|
+
readiness_diagnostics = wait_for_ready(driver, options)
|
|
183
|
+
# Strip `readiness` from forwarded serialize args -- it's consumed by
|
|
184
|
+
# wait_for_ready upstream, not a PercyDOM.serialize argument.
|
|
185
|
+
serialize_options = options.reject { |k, _| k.to_s == 'readiness' }
|
|
186
|
+
dom_snapshot = driver.execute_script("return PercyDOM.serialize(#{serialize_options.to_json})")
|
|
187
|
+
# `!nil?` preserves legitimate falsy returns like {} ("gate ran, no
|
|
188
|
+
# notable diagnostics").
|
|
189
|
+
if !readiness_diagnostics.nil? && dom_snapshot.is_a?(Hash)
|
|
190
|
+
dom_snapshot['readiness_diagnostics'] = readiness_diagnostics
|
|
191
|
+
end
|
|
118
192
|
begin
|
|
119
193
|
page_origin = get_origin(driver.current_url)
|
|
120
194
|
iframes = percy_dom_script ? driver.find_elements(:tag_name, 'iframe') : []
|
data/lib/version.rb
CHANGED
data/package.json
CHANGED
|
@@ -748,6 +748,59 @@ RSpec.describe Percy do
|
|
|
748
748
|
expect(dom['corsIframes'].length).to eq(1)
|
|
749
749
|
expect(dom['corsIframes'][0]['frameUrl']).to eq('https://other.example.com/page')
|
|
750
750
|
end
|
|
751
|
+
|
|
752
|
+
# --- Readiness gate --------------------------------------
|
|
753
|
+
|
|
754
|
+
it 'runs waitForReady before serialize and attaches diagnostics' do
|
|
755
|
+
allow(driver).to receive(:execute_async_script).and_return(
|
|
756
|
+
'ok' => true, 'timed_out' => false,
|
|
757
|
+
)
|
|
758
|
+
allow(driver).to receive(:execute_script).and_return({'html' => '<html/>'})
|
|
759
|
+
allow(driver).to receive(:current_url).and_return('http://main.example.com/')
|
|
760
|
+
allow(driver).to receive(:find_elements).and_return([])
|
|
761
|
+
|
|
762
|
+
dom = Percy.get_serialized_dom(driver, {})
|
|
763
|
+
expect(driver).to have_received(:execute_async_script) do |script| # rubocop:disable RSpec/MessageSpies
|
|
764
|
+
expect(script).to include('waitForReady')
|
|
765
|
+
expect(script).to include("typeof PercyDOM.waitForReady === 'function'")
|
|
766
|
+
end
|
|
767
|
+
expect(dom['readiness_diagnostics']).to eq('ok' => true, 'timed_out' => false)
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
it 'embeds per-snapshot readiness config in the script' do
|
|
771
|
+
allow(driver).to receive(:execute_async_script).and_return(nil)
|
|
772
|
+
allow(driver).to receive(:execute_script).and_return({'html' => '<html/>'})
|
|
773
|
+
allow(driver).to receive(:current_url).and_return('http://main.example.com/')
|
|
774
|
+
allow(driver).to receive(:find_elements).and_return([])
|
|
775
|
+
|
|
776
|
+
Percy.get_serialized_dom(driver, readiness: {preset: 'strict', stabilityWindowMs: 500})
|
|
777
|
+
expect(driver).to have_received(:execute_async_script) do |script| # rubocop:disable RSpec/MessageSpies
|
|
778
|
+
expect(script).to include('"preset":"strict"')
|
|
779
|
+
expect(script).to include('"stabilityWindowMs":500')
|
|
780
|
+
end
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
it 'skips execute_async_script when preset is disabled' do
|
|
784
|
+
allow(driver).to receive(:execute_script).and_return({'html' => '<html/>'})
|
|
785
|
+
allow(driver).to receive(:current_url).and_return('http://main.example.com/')
|
|
786
|
+
allow(driver).to receive(:find_elements).and_return([])
|
|
787
|
+
expect(driver).to_not receive(:execute_async_script)
|
|
788
|
+
|
|
789
|
+
dom = Percy.get_serialized_dom(driver, readiness: {preset: 'disabled'})
|
|
790
|
+
expect(dom).to_not have_key('readiness_diagnostics')
|
|
791
|
+
expect(dom['html']).to eq('<html/>')
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
it 'still serializes when execute_async_script raises' do
|
|
795
|
+
allow(driver).to receive(:execute_async_script).and_raise(StandardError, 'readiness boom')
|
|
796
|
+
allow(driver).to receive(:execute_script).and_return({'html' => '<html/>'})
|
|
797
|
+
allow(driver).to receive(:current_url).and_return('http://main.example.com/')
|
|
798
|
+
allow(driver).to receive(:find_elements).and_return([])
|
|
799
|
+
|
|
800
|
+
dom = Percy.get_serialized_dom(driver, {})
|
|
801
|
+
expect(dom).to_not have_key('readiness_diagnostics')
|
|
802
|
+
expect(dom['html']).to eq('<html/>')
|
|
803
|
+
end
|
|
751
804
|
end
|
|
752
805
|
|
|
753
806
|
describe '.change_window_dimension_and_wait' do
|