@percy/webdriver-utils 1.27.6-alpha.0 → 1.27.6-beta.1

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.
package/dist/driver.js CHANGED
@@ -71,4 +71,29 @@ export default class Driver {
71
71
  const response = JSON.parse((await request(baseUrl, options)).body);
72
72
  return response.value;
73
73
  }
74
+ async findElementBoundingBox(using, value) {
75
+ if (using === 'xpath') {
76
+ return await this.findElementXpath(value);
77
+ } else if (using === 'css selector') {
78
+ return await this.findElementSelector(value);
79
+ }
80
+ }
81
+ async findElementXpath(xpath) {
82
+ xpath = xpath.replace(/'/g, '"');
83
+ const command = {
84
+ script: `return document.evaluate('${xpath}', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.getBoundingClientRect();`,
85
+ args: []
86
+ };
87
+ const response = await this.executeScript(command);
88
+ return response.value;
89
+ }
90
+ async findElementSelector(selector) {
91
+ selector = selector.replace('\\', '\\\\');
92
+ const command = {
93
+ script: `return document.querySelector('${selector}').getBoundingClientRect();`,
94
+ args: []
95
+ };
96
+ const response = await this.executeScript(command);
97
+ return response.value;
98
+ }
74
99
  }
@@ -100,16 +100,17 @@ export default class AutomateProvider extends GenericProvider {
100
100
  }
101
101
  });
102
102
  }
103
- async getTiles(headerHeight, footerHeight, fullscreen) {
104
- var _responseValue$metada;
103
+ async getTiles(fullscreen) {
104
+ var _this$options;
105
105
  if (!this.driver) throw new Error('Driver is null, please initialize driver with createDriver().');
106
106
  log.debug('Starting actual screenshotting phase');
107
107
  const dpr = await this.metaData.devicePixelRatio();
108
+ const screenshotType = (_this$options = this.options) !== null && _this$options !== void 0 && _this$options.fullPage ? 'fullpage' : 'singlepage';
108
109
  const response = await TimeIt.run('percyScreenshot:screenshot', async () => {
109
110
  return await this.browserstackExecutor('percyScreenshot', {
110
111
  state: 'screenshot',
111
112
  percyBuildId: this.buildInfo.id,
112
- screenshotType: 'singlepage',
113
+ screenshotType: screenshotType,
113
114
  scaleFactor: dpr,
114
115
  options: this.options
115
116
  });
@@ -121,20 +122,19 @@ export default class AutomateProvider extends GenericProvider {
121
122
  const tiles = [];
122
123
  const tileResponse = JSON.parse(responseValue.result);
123
124
  log.debug('Tiles captured successfully');
124
- const windowHeight = (responseValue === null || responseValue === void 0 ? void 0 : (_responseValue$metada = responseValue.metadata) === null || _responseValue$metada === void 0 ? void 0 : _responseValue$metada.window_height) || 0;
125
- for (let tileData of tileResponse.sha) {
125
+ for (let tileData of tileResponse.tiles) {
126
126
  tiles.push(new Tile({
127
- statusBarHeight: tileResponse.header_height || 0,
128
- navBarHeight: tileResponse.footer_height || 0,
129
- headerHeight,
130
- footerHeight,
127
+ statusBarHeight: tileData.status_bar || 0,
128
+ navBarHeight: tileData.nav_bar || 0,
129
+ headerHeight: tileData.header_height || 0,
130
+ footerHeight: tileData.footer_height || 0,
131
131
  fullscreen,
132
- sha: tileData.split('-')[0] // drop build id
132
+ sha: tileData.sha.split('-')[0] // drop build id
133
133
  }));
134
134
  }
135
135
 
136
136
  const metadata = {
137
- windowHeight: Math.ceil(windowHeight * dpr)
137
+ screenshotType: screenshotType
138
138
  };
139
139
  return {
140
140
  tiles: tiles,
@@ -21,6 +21,12 @@ export default class GenericProvider {
21
21
  this.debugUrl = null;
22
22
  this.header = 0;
23
23
  this.footer = 0;
24
+ this.statusBarHeight = 0;
25
+ this.pageXShiftFactor = 0;
26
+ this.pageYShiftFactor = 0;
27
+ this.currentTag = null;
28
+ this.removeElementShiftFactor = 50000;
29
+ this.initialScrollLocation = null;
24
30
  }
25
31
  addDefaultOptions() {
26
32
  this.options.freezeAnimation = this.options.freezeAnimatedImage || this.options.freezeAnimation || false;
@@ -45,6 +51,29 @@ export default class GenericProvider {
45
51
  if (i) this.environmentInfo.add(i);
46
52
  }
47
53
  }
54
+ isIOS() {
55
+ var _this$currentTag;
56
+ return ((_this$currentTag = this.currentTag) === null || _this$currentTag === void 0 ? void 0 : _this$currentTag.osName) === 'iOS';
57
+ }
58
+ async getScrollDetails() {
59
+ return await this.driver.executeScript({
60
+ script: 'return [parseInt(window.scrollX), parseInt(window.scrollY)];',
61
+ args: []
62
+ });
63
+ }
64
+ async getInitialScrollLocation() {
65
+ if (this.initialScrollLocation) {
66
+ return this.initialScrollLocation;
67
+ }
68
+ this.initialScrollLocation = await this.getScrollDetails();
69
+ return this.initialScrollLocation;
70
+ }
71
+ async scrollToPosition(x, y) {
72
+ await this.driver.executeScript({
73
+ script: `window.scrollTo(${x}, ${y})`,
74
+ args: []
75
+ });
76
+ }
48
77
  async screenshot(name, {
49
78
  ignoreRegionXpaths = [],
50
79
  ignoreRegionSelectors = [],
@@ -61,8 +90,10 @@ export default class GenericProvider {
61
90
  log.debug('Fetching comparisong tag ...');
62
91
  const tag = await this.getTag();
63
92
  log.debug(`[${name}] : Tag ${JSON.stringify(tag)}`);
64
- const tiles = await this.getTiles(this.header, this.footer, fullscreen);
93
+ const tiles = await this.getTiles(fullscreen);
65
94
  log.debug(`[${name}] : Tiles ${JSON.stringify(tiles)}`);
95
+ this.currentTag = tag;
96
+ this.statusBarHeight = tiles.tiles[0].statusBarHeight;
66
97
  const ignoreRegions = await this.findRegions(ignoreRegionXpaths, ignoreRegionSelectors, ignoreRegionElements, customIgnoreRegions);
67
98
  const considerRegions = await this.findRegions(considerRegionXpaths, considerRegionSelectors, considerRegionElements, customConsiderRegions);
68
99
  await this.setDebugUrl();
@@ -99,7 +130,7 @@ export default class GenericProvider {
99
130
  });
100
131
  ;
101
132
  }
102
- async getTiles(headerHeight, footerHeight, fullscreen) {
133
+ async getTiles(fullscreen) {
103
134
  if (!this.driver) throw new Error('Driver is null, please initialize driver with createDriver().');
104
135
  const base64content = await this.driver.takeScreenshot();
105
136
  log.debug('Tiles captured successfully');
@@ -108,8 +139,8 @@ export default class GenericProvider {
108
139
  content: base64content,
109
140
  statusBarHeight: 0,
110
141
  navBarHeight: 0,
111
- headerHeight,
112
- footerHeight,
142
+ headerHeight: this.header,
143
+ footerHeight: this.footer,
113
144
  fullscreen
114
145
  })],
115
146
  // TODO: Add Generic support sha for contextual diff for non-automate
@@ -144,29 +175,73 @@ export default class GenericProvider {
144
175
  async setDebugUrl() {
145
176
  this.debugUrl = 'https://localhost/v1';
146
177
  }
178
+ async doTransformations() {
179
+ var _this$options;
180
+ const hideScrollbarStyle = `
181
+ /* Hide scrollbar for Chrome, Safari and Opera */
182
+ ::-webkit-scrollbar {
183
+ display: none !important;
184
+ }
185
+
186
+ /* Hide scrollbar for IE, Edge and Firefox */
187
+ body, html {
188
+ -ms-overflow-style: none !important; /* IE and Edge */
189
+ scrollbar-width: none !important; /* Firefox */
190
+ }`.replace(/\n/g, '');
191
+ const jsScript = `
192
+ const e = document.createElement('style');
193
+ e.setAttribute('class', 'poa-injected');
194
+ e.innerHTML = '${hideScrollbarStyle}'
195
+ document.head.appendChild(e);`;
196
+ await this.driver.executeScript({
197
+ script: jsScript,
198
+ args: []
199
+ });
200
+ if ((_this$options = this.options) !== null && _this$options !== void 0 && _this$options.fullPage || this.isIOS()) {
201
+ await this.getInitialScrollLocation();
202
+ }
203
+ }
204
+ async undoTransformations(data) {
205
+ const jsScript = `
206
+ const n = document.querySelectorAll('${data}');
207
+ n.forEach((e) => {e.remove()});`;
208
+ await this.driver.executeScript({
209
+ script: jsScript,
210
+ args: []
211
+ });
212
+ }
147
213
  async findRegions(xpaths, selectors, elements, customLocations) {
148
- const xpathRegions = await this.getSeleniumRegionsBy('xpath', xpaths);
149
- const selectorRegions = await this.getSeleniumRegionsBy('css selector', selectors);
150
- const elementRegions = await this.getSeleniumRegionsByElement(elements);
151
- const customRegions = await this.getSeleniumRegionsByLocation(customLocations);
152
- return [...xpathRegions, ...selectorRegions, ...elementRegions, ...customRegions];
214
+ let isRegionPassed = [xpaths, selectors, elements, customLocations].some(regions => regions.length > 0);
215
+ if (isRegionPassed) {
216
+ await this.doTransformations();
217
+ const xpathRegions = await this.getSeleniumRegionsBy('xpath', xpaths);
218
+ const selectorRegions = await this.getSeleniumRegionsBy('css selector', selectors);
219
+ const elementRegions = await this.getSeleniumRegionsByElement(elements);
220
+ const customRegions = await this.getSeleniumRegionsByLocation(customLocations);
221
+ await this.undoTransformations('.poa-injected');
222
+ return [...xpathRegions, ...selectorRegions, ...elementRegions, ...customRegions];
223
+ } else {
224
+ return [];
225
+ }
153
226
  }
154
- async getRegionObject(selector, elementId) {
155
- const scaleFactor = parseInt(await this.metaData.devicePixelRatio());
156
- const rect = await this.driver.rect(elementId);
157
- const location = {
158
- x: rect.x,
159
- y: rect.y
160
- };
161
- const size = {
162
- height: rect.height,
163
- width: rect.width
164
- };
227
+ async getRegionObjectFromBoundingBox(selector, element) {
228
+ var _this$options2;
229
+ const scaleFactor = await this.metaData.devicePixelRatio();
230
+ let scrollX = 0,
231
+ scrollY = 0;
232
+ if ((_this$options2 = this.options) !== null && _this$options2 !== void 0 && _this$options2.fullPage) {
233
+ scrollX = this.initialScrollLocation.value[0];
234
+ scrollY = this.initialScrollLocation.value[1];
235
+ }
236
+ let headerAdjustment = 0;
237
+ if (this.isIOS()) {
238
+ headerAdjustment = this.statusBarHeight;
239
+ }
165
240
  const coOrdinates = {
166
- top: Math.floor(location.y * scaleFactor),
167
- bottom: Math.ceil((location.y + size.height) * scaleFactor),
168
- left: Math.floor(location.x * scaleFactor),
169
- right: Math.ceil((location.x + size.width) * scaleFactor)
241
+ top: Math.floor((element.y + scrollY) * scaleFactor) + Math.floor(headerAdjustment),
242
+ bottom: Math.ceil((element.y + element.height + scrollY) * scaleFactor) + Math.ceil(headerAdjustment),
243
+ left: Math.floor((element.x + scrollX) * scaleFactor),
244
+ right: Math.ceil((element.x + element.width + scrollX) * scaleFactor)
170
245
  };
171
246
  const jsonObject = {
172
247
  selector,
@@ -178,9 +253,9 @@ export default class GenericProvider {
178
253
  const regionsArray = [];
179
254
  for (const idx in elements) {
180
255
  try {
181
- const element = await this.driver.findElement(findBy, elements[idx]);
256
+ const boundingBoxRegion = await this.driver.findElementBoundingBox(findBy, elements[idx]);
182
257
  const selector = `${findBy}: ${elements[idx]}`;
183
- const region = await this.getRegionObject(selector, element[Object.keys(element)[0]]);
258
+ const region = await this.getRegionObjectFromBoundingBox(selector, boundingBoxRegion);
184
259
  regionsArray.push(region);
185
260
  } catch (e) {
186
261
  log.warn(`Selenium Element with ${findBy}: ${elements[idx]} not found. Ignoring this ${findBy}.`);
@@ -189,6 +264,60 @@ export default class GenericProvider {
189
264
  }
190
265
  return regionsArray;
191
266
  }
267
+ async updatePageShiftFactor(location, scaleFactor, scrollFactors) {
268
+ var _this$options3;
269
+ if (this.isIOS() || this.currentTag.osName === 'OS X' && parseInt(this.currentTag.browserVersion) > 13 && this.currentTag.browserName.toLowerCase() === 'safari') {
270
+ this.pageYShiftFactor = this.statusBarHeight;
271
+ } else {
272
+ this.pageYShiftFactor = this.statusBarHeight - scrollFactors.value[1] * scaleFactor;
273
+ }
274
+ this.pageXShiftFactor = this.isIOS() ? 0 : -(scrollFactors.value[0] * scaleFactor);
275
+ if (this.isIOS() && !((_this$options3 = this.options) !== null && _this$options3 !== void 0 && _this$options3.fullPage)) {
276
+ if (scrollFactors.value[0] !== this.initialScrollLocation.value[0] || scrollFactors.value[1] !== this.initialScrollLocation.value[1]) {
277
+ this.pageXShiftFactor = -1 * this.removeElementShiftFactor;
278
+ this.pageYShiftFactor = -1 * this.removeElementShiftFactor;
279
+ } else if (location.y === 0) {
280
+ this.pageYShiftFactor += -(scrollFactors.value[1] * scaleFactor);
281
+ }
282
+ }
283
+ }
284
+ async getRegionObject(selector, elementId) {
285
+ var _this$options4;
286
+ const scaleFactor = await this.metaData.devicePixelRatio();
287
+ const rect = await this.driver.rect(elementId);
288
+ const location = {
289
+ x: rect.x,
290
+ y: rect.y
291
+ };
292
+ const size = {
293
+ height: rect.height,
294
+ width: rect.width
295
+ };
296
+ let scrollX = 0,
297
+ scrollY = 0;
298
+ const scrollFactors = await this.getScrollDetails();
299
+ if ((_this$options4 = this.options) !== null && _this$options4 !== void 0 && _this$options4.fullPage) {
300
+ scrollX = scrollFactors.value[0];
301
+ scrollY = scrollFactors.value[1];
302
+ }
303
+
304
+ // Update pageShiftFactor Element is not visible in viewport
305
+ // In case of iOS if the element is not visible in viewport it gives 0 for x-y coordinate.
306
+ // In case of iOS if the element is partially visible it gives negative x-y coordinate.
307
+ // Subtracting ScrollY/ScrollX ensures if the element is visible in viewport or not.
308
+ await this.updatePageShiftFactor(location, scaleFactor, scrollFactors);
309
+ const coOrdinates = {
310
+ top: Math.floor((location.y + scrollY) * scaleFactor) + Math.floor(this.pageYShiftFactor),
311
+ bottom: Math.ceil((location.y + size.height + scrollY) * scaleFactor) + Math.ceil(this.pageYShiftFactor),
312
+ left: Math.floor((location.x + scrollX) * scaleFactor) + Math.floor(this.pageXShiftFactor),
313
+ right: Math.ceil((location.x + size.width + scrollX) * scaleFactor) + Math.ceil(this.pageXShiftFactor)
314
+ };
315
+ const jsonObject = {
316
+ selector,
317
+ coOrdinates
318
+ };
319
+ return jsonObject;
320
+ }
192
321
  async getSeleniumRegionsByElement(elements) {
193
322
  const regionsArray = [];
194
323
  for (let index = 0; index < elements.length; index++) {
@@ -201,6 +330,9 @@ export default class GenericProvider {
201
330
  log.debug(e.toString());
202
331
  }
203
332
  }
333
+ if (this.isIOS()) {
334
+ await this.scrollToPosition(this.initialScrollLocation.value[0], this.initialScrollLocation.value[1]);
335
+ }
204
336
  return regionsArray;
205
337
  }
206
338
  async getSeleniumRegionsByLocation(customLocations) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percy/webdriver-utils",
3
- "version": "1.27.6-alpha.0",
3
+ "version": "1.27.6-beta.1",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "publishConfig": {
11
11
  "access": "public",
12
- "tag": "alpha"
12
+ "tag": "beta"
13
13
  },
14
14
  "engines": {
15
15
  "node": ">=14"
@@ -29,8 +29,8 @@
29
29
  "test:coverage": "yarn test --coverage"
30
30
  },
31
31
  "dependencies": {
32
- "@percy/config": "1.27.6-alpha.0",
33
- "@percy/sdk-utils": "1.27.6-alpha.0"
32
+ "@percy/config": "1.27.6-beta.1",
33
+ "@percy/sdk-utils": "1.27.6-beta.1"
34
34
  },
35
- "gitHead": "415a083dc13e9453990b042a41d6676f6618df0c"
35
+ "gitHead": "1a3bf9e775769215ef590faa40502ce0ee0a6f78"
36
36
  }
@@ -1,5 +0,0 @@
1
- export default class CapabilitiesManager {
2
- constructor(capabilities) {
3
- this.capabilities = capabilities;
4
- }
5
- }
@@ -1,72 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { colors } from '@percy/logger/utils';
4
- import semver from 'semver';
5
- export default class CapabilitiesValidator {
6
- constructor(automateCapabilities, sessionCapabilities) {
7
- this.automateCapabilities = automateCapabilities;
8
- this.sessionCapabilities = sessionCapabilities;
9
- }
10
- WARN_LOG_LEVEL = 'WARN';
11
- ERROR_LOG_LEVEL = 'ERROR';
12
- log(message, errorLevel = this.ERROR_LOG_LEVEL) {
13
- if (errorLevel === this.WARN_LOG_LEVEL) {
14
- console.warn(colors.yellow(message));
15
- } else if (errorLevel === this.ERROR_LOG_LEVEL) {
16
- console.error(colors.red(message));
17
- }
18
- }
19
- existsInArray(array, value) {
20
- if (!array || array.length === 0) return false;
21
- return array.some(element => {
22
- return element === value;
23
- });
24
- }
25
- validateBrowserOSVersions() {
26
- var _this$sessionCapabili, _this$sessionCapabili2;
27
- const cwd = process.cwd();
28
- const excludeBrowserData = JSON.parse(fs.readFileSync(path.join(cwd, 'packages/webdriver-utils/src/util/exclude_browsers.json')));
29
- let {
30
- os,
31
- osVersion,
32
- browserName,
33
- browserVersion,
34
- deviceName
35
- } = this.automateCapabilities;
36
- const platform = (_this$sessionCapabili = this.sessionCapabilities) === null || _this$sessionCapabili === void 0 ? void 0 : (_this$sessionCapabili2 = _this$sessionCapabili.platformName) === null || _this$sessionCapabili2 === void 0 ? void 0 : _this$sessionCapabili2.toLowerCase();
37
- if (!os || !osVersion || !browserName || !browserVersion) {
38
- this.log('OS/Browser Combination is not supported on Percy');
39
- throw new Error('OS/Browser Combination is not supported on Percy');
40
- }
41
- const isMobile = ['ios', 'android'].includes(platform);
42
- if (isMobile && !deviceName) {
43
- this.log('Device capabilities are incorrect or not supported on Percy');
44
- throw new Error('Device capabilities are incorrect or not supported on Percy');
45
- }
46
- if (excludeBrowserData !== null && excludeBrowserData !== void 0 && excludeBrowserData.os[os]) {
47
- const osData = excludeBrowserData === null || excludeBrowserData === void 0 ? void 0 : excludeBrowserData.os[os];
48
- if (osData !== null && osData !== void 0 && osData.os_versions) {
49
- if (this.existsInArray(osData.os_versions, osVersion)) {
50
- this.log(`${os} ${osVersion} is not supported on Percy`);
51
- throw new Error(`${os} ${osVersion} is not supported on Percy`);
52
- }
53
- }
54
- if (osData !== null && osData !== void 0 && osData.browsers) {
55
- const browserData = osData === null || osData === void 0 ? void 0 : osData.browsers[browserName.toLowerCase()];
56
- if (browserData && browserData.min_version === 'all') {
57
- this.log(`${browserName} is not supported on Percy`);
58
- throw new Error(`${browserName} is not supported on Percy`);
59
- } else if (browserData && parseInt(browserVersion, 10) < parseInt(browserData.min_version, 10)) {
60
- this.log(`${browserName}: ${browserVersion} is not supported on Percy`);
61
- throw new Error(`${browserName}: ${browserVersion} is not supported on Percy`);
62
- }
63
- }
64
- if (osData !== null && osData !== void 0 && osData.device_names) {
65
- if (this.existsInArray(osData === null || osData === void 0 ? void 0 : osData.device_names, deviceName)) {
66
- this.log(`${deviceName} is not supported on Percy`);
67
- throw new Error(`${deviceName} is not supported in Percy`);
68
- }
69
- }
70
- }
71
- }
72
- }
@@ -1,7 +0,0 @@
1
- export const SELENIUM_TO_API_OS_MAPPING = {
2
- MAC: 'OS X',
3
- WIN8: 'Windows',
4
- XP: 'Windows',
5
- WINDOWS: 'Windows',
6
- ANY: 'OS X'
7
- };