@percy/webdriver-utils 1.27.5-alpha.0 → 1.27.5-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
@@ -1,6 +1,5 @@
1
1
  import utils from '@percy/sdk-utils';
2
2
  import Cache from './util/cache.js';
3
- import httpsAgent from './util/utils.js';
4
3
  const {
5
4
  request
6
5
  } = utils;
@@ -11,14 +10,20 @@ export default class Driver {
11
10
  this.executorUrl = executorUrl.includes('@') ? `https://${executorUrl.split('@')[1]}` : executorUrl;
12
11
  this.passedCapabilities = passedCapabilities;
13
12
  }
13
+ static requestPostOptions(command) {
14
+ return {
15
+ method: 'POST',
16
+ headers: {
17
+ 'Content-Type': 'application/json;charset=utf-8'
18
+ },
19
+ body: JSON.stringify(command)
20
+ };
21
+ }
14
22
  async getCapabilites() {
15
23
  return await Cache.withCache(Cache.caps, this.sessionId, async () => {
16
24
  try {
17
- const options = {
18
- agent: httpsAgent()
19
- };
20
25
  const baseUrl = `${this.executorUrl}/session/${this.sessionId}`;
21
- const caps = JSON.parse((await request(baseUrl, options)).body);
26
+ const caps = JSON.parse((await request(baseUrl)).body);
22
27
  return caps.value;
23
28
  } catch (err) {
24
29
  log.warn(`Falling back to legacy protocol, Error: ${err.message}`);
@@ -27,11 +32,8 @@ export default class Driver {
27
32
  });
28
33
  }
29
34
  async getWindowSize() {
30
- const options = {
31
- agent: httpsAgent()
32
- };
33
35
  const baseUrl = `${this.executorUrl}/session/${this.sessionId}/window/current/size`;
34
- const windowSize = JSON.parse((await request(baseUrl, options)).body);
36
+ const windowSize = JSON.parse((await request(baseUrl)).body);
35
37
  return windowSize;
36
38
  }
37
39
 
@@ -45,48 +47,53 @@ export default class Driver {
45
47
  if (!command.script.includes('browserstack_executor')) {
46
48
  command.script = `/* percy_automate_script */ \n ${command.script}`;
47
49
  }
48
- const options = {
49
- method: 'POST',
50
- headers: {
51
- 'Content-Type': 'application/json;charset=utf-8'
52
- },
53
- agent: httpsAgent(),
54
- body: JSON.stringify(command)
55
- };
50
+ const options = Driver.requestPostOptions(command);
56
51
  const baseUrl = `${this.executorUrl}/session/${this.sessionId}/execute/sync`;
57
52
  const response = JSON.parse((await request(baseUrl, options)).body);
58
53
  return response;
59
54
  }
60
55
  async takeScreenshot() {
61
- const options = {
62
- agent: httpsAgent()
63
- };
64
56
  const baseUrl = `${this.executorUrl}/session/${this.sessionId}/screenshot`;
65
- const screenShot = JSON.parse((await request(baseUrl, options)).body);
57
+ const screenShot = JSON.parse((await request(baseUrl)).body);
66
58
  return screenShot.value;
67
59
  }
68
60
  async rect(elementId) {
69
- const options = {
70
- agent: httpsAgent()
71
- };
72
61
  const baseUrl = `${this.executorUrl}/session/${this.sessionId}/element/${elementId}/rect`;
73
- const response = JSON.parse((await request(baseUrl, options)).body);
62
+ const response = JSON.parse((await request(baseUrl)).body);
74
63
  return response.value;
75
64
  }
76
65
  async findElement(using, value) {
77
- const options = {
78
- method: 'POST',
79
- headers: {
80
- 'Content-Type': 'application/json;charset=utf-8'
81
- },
82
- agent: httpsAgent(),
83
- body: JSON.stringify({
84
- using,
85
- value
86
- })
87
- };
66
+ const options = Driver.requestPostOptions({
67
+ using,
68
+ value
69
+ });
88
70
  const baseUrl = `${this.executorUrl}/session/${this.sessionId}/element`;
89
71
  const response = JSON.parse((await request(baseUrl, options)).body);
90
72
  return response.value;
91
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
+ }
92
99
  }
package/dist/index.js CHANGED
@@ -23,6 +23,7 @@ export default class WebdriverUtils {
23
23
  const comparisonData = await automate.screenshot(snapshotName, options);
24
24
  comparisonData.metadata.cliScreenshotStartTime = startTime;
25
25
  comparisonData.metadata.cliScreenshotEndTime = Date.now();
26
+ log.debug(`[${snapshotName}] : Comparison Data: ${JSON.stringify(comparisonData)}`);
26
27
  return comparisonData;
27
28
  } catch (e) {
28
29
  log.error(`[${snapshotName}] : Error - ${e.message}`);
@@ -49,7 +49,7 @@ export default class DesktopMetaData {
49
49
  async screenResolution() {
50
50
  return await Cache.withCache(Cache.resolution, this.driver.sessionId, async () => {
51
51
  const data = await this.driver.executeScript({
52
- script: 'return [(window.screen.width * window.devicePixelRatio).toString(), (window.screen.height * window.devicePixelRatio).toString()];',
52
+ script: 'return [parseInt(window.screen.width * window.devicePixelRatio).toString(), parseInt(window.screen.height * window.devicePixelRatio).toString()];',
53
53
  args: []
54
54
  });
55
55
  const screenInfo = data.value;
@@ -47,8 +47,7 @@ export default class AutomateProvider extends GenericProvider {
47
47
  error = e;
48
48
  throw e;
49
49
  } finally {
50
- var _response, _response$body;
51
- await this.percyScreenshotEnd(name, (_response = response) === null || _response === void 0 ? void 0 : (_response$body = _response.body) === null || _response$body === void 0 ? void 0 : _response$body.link, `${error}`);
50
+ await this.percyScreenshotEnd(name, error);
52
51
  }
53
52
  return response;
54
53
  }
@@ -61,23 +60,38 @@ export default class AutomateProvider extends GenericProvider {
61
60
  percyBuildUrl: this.buildInfo.url,
62
61
  state: 'begin'
63
62
  });
63
+ // Selenium Hub, set status error Code to 13 if an error is thrown
64
+ // Handling error with Selenium dialect is != W3C
65
+ if ((result === null || result === void 0 ? void 0 : result.status) === 13) throw new Error((result === null || result === void 0 ? void 0 : result.value) || 'Got invalid error response');
64
66
  this._markedPercy = result.success;
65
67
  return result;
66
68
  } catch (e) {
69
+ var _e$response, _JSON$parse, _e$response2;
67
70
  log.debug(`[${name}] : Could not mark Automate session as percy`);
68
71
  log.error(`[${name}] : error: ${e.toString()}`);
69
- return null;
72
+ /**
73
+ * - Handling Error when dialect is W3C
74
+ * ERROR response format from SeleniumHUB `{
75
+ * sessionId: ...,
76
+ * status: 13,
77
+ * value: { error: '', message: ''}
78
+ * }
79
+ */
80
+ const errResponse = (e === null || e === void 0 ? void 0 : (_e$response = e.response) === null || _e$response === void 0 ? void 0 : _e$response.body) && ((_JSON$parse = JSON.parse(e === null || e === void 0 ? void 0 : (_e$response2 = e.response) === null || _e$response2 === void 0 ? void 0 : _e$response2.body)) === null || _JSON$parse === void 0 ? void 0 : _JSON$parse.value) || {};
81
+ const errMessage = (errResponse === null || errResponse === void 0 ? void 0 : errResponse.message) || (errResponse === null || errResponse === void 0 ? void 0 : errResponse.error) || (e === null || e === void 0 ? void 0 : e.message) || (e === null || e === void 0 ? void 0 : e.error) || (e === null || e === void 0 ? void 0 : e.value) || e.toString();
82
+ throw new Error(errMessage);
70
83
  }
71
84
  });
72
85
  }
73
- async percyScreenshotEnd(name, percyScreenshotUrl, statusMessage = null) {
86
+ async percyScreenshotEnd(name, error) {
74
87
  return await TimeIt.run('percyScreenshotEnd', async () => {
75
88
  try {
89
+ var _this$buildInfo;
76
90
  await this.browserstackExecutor('percyScreenshot', {
77
91
  name,
78
- percyScreenshotUrl,
79
- status: percyScreenshotUrl ? 'success' : 'failure',
80
- statusMessage,
92
+ percyScreenshotUrl: (_this$buildInfo = this.buildInfo) === null || _this$buildInfo === void 0 ? void 0 : _this$buildInfo.url,
93
+ status: error ? 'failure' : 'success',
94
+ statusMessage: error ? `${error}` : '',
81
95
  state: 'end'
82
96
  });
83
97
  } catch (e) {
@@ -168,10 +182,6 @@ export default class AutomateProvider extends GenericProvider {
168
182
  } = await this.metaData.windowSize();
169
183
  const resolution = await this.metaData.screenResolution();
170
184
  const orientation = (_ref = this.metaData.orientation() || automateCaps.deviceOrientation) === null || _ref === void 0 ? void 0 : _ref.toLowerCase();
171
-
172
- // for android window size only constitutes of browser viewport, hence adding nav / status / url bar heights
173
- [this.header, this.footer] = await this.getHeaderFooter(deviceName, osVersion, browserName);
174
- height = this.metaData.device() && (osName === null || osName === void 0 ? void 0 : osName.toLowerCase()) === 'android' ? height + this.header + this.footer : height;
175
185
  return {
176
186
  name: deviceName,
177
187
  osName,
@@ -2,12 +2,6 @@ import utils from '@percy/sdk-utils';
2
2
  import MetaDataResolver from '../metadata/metaDataResolver.js';
3
3
  import Tile from '../util/tile.js';
4
4
  import Driver from '../driver.js';
5
- import Cache from '../util/cache.js';
6
- import httpsAgent from '../util/utils.js';
7
- const {
8
- request
9
- } = utils;
10
- const DEVICES_CONFIG_URL = 'https://storage.googleapis.com/percy-utils/devices.json';
11
5
  const log = utils.logger('webdriver-utils:genericProvider');
12
6
  export default class GenericProvider {
13
7
  clientInfo = new Set();
@@ -27,6 +21,14 @@ export default class GenericProvider {
27
21
  this.debugUrl = null;
28
22
  this.header = 0;
29
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.initialScrollFactor = {
30
+ value: [0, 0]
31
+ };
30
32
  }
31
33
  addDefaultOptions() {
32
34
  this.options.freezeAnimation = this.options.freezeAnimatedImage || this.options.freezeAnimation || false;
@@ -51,6 +53,22 @@ export default class GenericProvider {
51
53
  if (i) this.environmentInfo.add(i);
52
54
  }
53
55
  }
56
+ async getInitialPosition() {
57
+ if (this.currentTag.osName === 'iOS') {
58
+ this.initialScrollFactor = await this.driver.executeScript({
59
+ script: 'return [parseInt(window.scrollX), parseInt(window.scrollY)];',
60
+ args: []
61
+ });
62
+ }
63
+ }
64
+ async scrollToInitialPosition(x, y) {
65
+ if (this.currentTag.osName === 'iOS') {
66
+ await this.driver.executeScript({
67
+ script: `window.scrollTo(${x}, ${y})`,
68
+ args: []
69
+ });
70
+ }
71
+ }
54
72
  async screenshot(name, {
55
73
  ignoreRegionXpaths = [],
56
74
  ignoreRegionSelectors = [],
@@ -69,6 +87,8 @@ export default class GenericProvider {
69
87
  log.debug(`[${name}] : Tag ${JSON.stringify(tag)}`);
70
88
  const tiles = await this.getTiles(this.header, this.footer, fullscreen);
71
89
  log.debug(`[${name}] : Tiles ${JSON.stringify(tiles)}`);
90
+ this.currentTag = tag;
91
+ this.statusBarHeight = tiles.tiles[0].statusBarHeight;
72
92
  const ignoreRegions = await this.findRegions(ignoreRegionXpaths, ignoreRegionSelectors, ignoreRegionElements, customIgnoreRegions);
73
93
  const considerRegions = await this.findRegions(considerRegionXpaths, considerRegionSelectors, considerRegionElements, customConsiderRegions);
74
94
  await this.setDebugUrl();
@@ -133,9 +153,6 @@ export default class GenericProvider {
133
153
  } = await this.metaData.windowSize();
134
154
  const resolution = await this.metaData.screenResolution();
135
155
  const orientation = this.metaData.orientation();
136
- [this.header, this.footer] = await this.getHeaderFooter();
137
- // for android window size only constitutes of browser viewport, hence adding nav / status / url bar heights
138
- height = this.metaData.osName() === 'android' ? height + this.header + this.footer : height;
139
156
  return {
140
157
  name: this.metaData.deviceName(),
141
158
  osName: this.metaData.osName(),
@@ -153,29 +170,62 @@ export default class GenericProvider {
153
170
  async setDebugUrl() {
154
171
  this.debugUrl = 'https://localhost/v1';
155
172
  }
173
+ async doTransformations() {
174
+ const hideScrollbarStyle = `
175
+ /* Hide scrollbar for Chrome, Safari and Opera */
176
+ ::-webkit-scrollbar {
177
+ display: none !important;
178
+ }
179
+
180
+ /* Hide scrollbar for IE, Edge and Firefox */
181
+ body, html {
182
+ -ms-overflow-style: none !important; /* IE and Edge */
183
+ scrollbar-width: none !important; /* Firefox */
184
+ }`.replace(/\n/g, '');
185
+ const jsScript = `
186
+ const e = document.createElement('style');
187
+ e.setAttribute('class', 'poa-injected');
188
+ e.innerHTML = '${hideScrollbarStyle}'
189
+ document.head.appendChild(e);`;
190
+ await this.driver.executeScript({
191
+ script: jsScript,
192
+ args: []
193
+ });
194
+ }
195
+ async undoTransformations(data) {
196
+ const jsScript = `
197
+ const n = document.querySelectorAll('${data}');
198
+ n.forEach((e) => {e.remove()});`;
199
+ await this.driver.executeScript({
200
+ script: jsScript,
201
+ args: []
202
+ });
203
+ }
156
204
  async findRegions(xpaths, selectors, elements, customLocations) {
157
- const xpathRegions = await this.getSeleniumRegionsBy('xpath', xpaths);
158
- const selectorRegions = await this.getSeleniumRegionsBy('css selector', selectors);
159
- const elementRegions = await this.getSeleniumRegionsByElement(elements);
160
- const customRegions = await this.getSeleniumRegionsByLocation(customLocations);
161
- return [...xpathRegions, ...selectorRegions, ...elementRegions, ...customRegions];
205
+ let isRegionPassed = [xpaths, selectors, elements, customLocations].some(regions => regions.length > 0);
206
+ if (isRegionPassed) {
207
+ await this.doTransformations();
208
+ const xpathRegions = await this.getSeleniumRegionsBy('xpath', xpaths);
209
+ const selectorRegions = await this.getSeleniumRegionsBy('css selector', selectors);
210
+ const elementRegions = await this.getSeleniumRegionsByElement(elements);
211
+ const customRegions = await this.getSeleniumRegionsByLocation(customLocations);
212
+ await this.undoTransformations('.poa-injected');
213
+ return [...xpathRegions, ...selectorRegions, ...elementRegions, ...customRegions];
214
+ } else {
215
+ return [];
216
+ }
162
217
  }
163
- async getRegionObject(selector, elementId) {
164
- const scaleFactor = parseInt(await this.metaData.devicePixelRatio());
165
- const rect = await this.driver.rect(elementId);
166
- const location = {
167
- x: rect.x,
168
- y: rect.y
169
- };
170
- const size = {
171
- height: rect.height,
172
- width: rect.width
173
- };
218
+ async getRegionObjectFromBoundingBox(selector, element) {
219
+ const scaleFactor = await this.metaData.devicePixelRatio();
220
+ let headerAdjustment = 0;
221
+ if (this.currentTag.osName === 'iOS') {
222
+ headerAdjustment = this.statusBarHeight;
223
+ }
174
224
  const coOrdinates = {
175
- top: Math.floor(location.y * scaleFactor),
176
- bottom: Math.ceil((location.y + size.height) * scaleFactor),
177
- left: Math.floor(location.x * scaleFactor),
178
- right: Math.ceil((location.x + size.width) * scaleFactor)
225
+ top: Math.floor(element.y * scaleFactor) + Math.floor(headerAdjustment),
226
+ bottom: Math.ceil((element.y + element.height) * scaleFactor) + Math.ceil(headerAdjustment),
227
+ left: Math.floor(element.x * scaleFactor),
228
+ right: Math.ceil((element.x + element.width) * scaleFactor)
179
229
  };
180
230
  const jsonObject = {
181
231
  selector,
@@ -187,9 +237,9 @@ export default class GenericProvider {
187
237
  const regionsArray = [];
188
238
  for (const idx in elements) {
189
239
  try {
190
- const element = await this.driver.findElement(findBy, elements[idx]);
240
+ const boundingBoxRegion = await this.driver.findElementBoundingBox(findBy, elements[idx]);
191
241
  const selector = `${findBy}: ${elements[idx]}`;
192
- const region = await this.getRegionObject(selector, element[Object.keys(element)[0]]);
242
+ const region = await this.getRegionObjectFromBoundingBox(selector, boundingBoxRegion);
193
243
  regionsArray.push(region);
194
244
  } catch (e) {
195
245
  log.warn(`Selenium Element with ${findBy}: ${elements[idx]} not found. Ignoring this ${findBy}.`);
@@ -198,8 +248,57 @@ export default class GenericProvider {
198
248
  }
199
249
  return regionsArray;
200
250
  }
251
+ async updatePageShiftFactor(location, scaleFactor) {
252
+ const scrollFactors = await this.driver.executeScript({
253
+ script: 'return [parseInt(window.scrollX), parseInt(window.scrollY)];',
254
+ args: []
255
+ });
256
+ if (this.currentTag.osName === 'iOS' || this.currentTag.osName === 'OS X' && parseInt(this.currentTag.browserVersion) > 13 && this.currentTag.browserName.toLowerCase() === 'safari') {
257
+ this.pageYShiftFactor = this.statusBarHeight;
258
+ } else {
259
+ this.pageYShiftFactor = this.statusBarHeight - scrollFactors.value[1] * scaleFactor;
260
+ }
261
+ this.pageXShiftFactor = this.currentTag.osName === 'iOS' ? 0 : -(scrollFactors.value[0] * scaleFactor);
262
+ if (this.currentTag.osName === 'iOS') {
263
+ if (scrollFactors.value[0] !== this.initialScrollFactor.value[0] || scrollFactors.value[1] !== this.initialScrollFactor.value[1]) {
264
+ this.pageXShiftFactor = -1 * this.removeElementShiftFactor;
265
+ this.pageYShiftFactor = -1 * this.removeElementShiftFactor;
266
+ } else if (location.y === 0) {
267
+ this.pageYShiftFactor += -(scrollFactors.value[1] * scaleFactor);
268
+ }
269
+ }
270
+ }
271
+ async getRegionObject(selector, elementId) {
272
+ const scaleFactor = await this.metaData.devicePixelRatio();
273
+ const rect = await this.driver.rect(elementId);
274
+ const location = {
275
+ x: rect.x,
276
+ y: rect.y
277
+ };
278
+ const size = {
279
+ height: rect.height,
280
+ width: rect.width
281
+ };
282
+ // Update pageShiftFactor Element is not visible in viewport
283
+ // In case of iOS if the element is not visible in viewport it gives 0 for x-y coordinate.
284
+ // In case of iOS if the element is partially visible it gives negative x-y coordinate.
285
+ // Subtracting ScrollY/ScrollX ensures if the element is visible in viewport or not.
286
+ await this.updatePageShiftFactor(location, scaleFactor);
287
+ const coOrdinates = {
288
+ top: Math.floor(location.y * scaleFactor) + Math.floor(this.pageYShiftFactor),
289
+ bottom: Math.ceil((location.y + size.height) * scaleFactor) + Math.ceil(this.pageYShiftFactor),
290
+ left: Math.floor(location.x * scaleFactor) + Math.floor(this.pageXShiftFactor),
291
+ right: Math.ceil((location.x + size.width) * scaleFactor) + Math.ceil(this.pageXShiftFactor)
292
+ };
293
+ const jsonObject = {
294
+ selector,
295
+ coOrdinates
296
+ };
297
+ return jsonObject;
298
+ }
201
299
  async getSeleniumRegionsByElement(elements) {
202
300
  const regionsArray = [];
301
+ await this.getInitialPosition();
203
302
  for (let index = 0; index < elements.length; index++) {
204
303
  try {
205
304
  const selector = `element: ${index}`;
@@ -210,6 +309,7 @@ export default class GenericProvider {
210
309
  log.debug(e.toString());
211
310
  }
212
311
  }
312
+ await this.scrollToInitialPosition(this.initialScrollFactor.value[0], this.initialScrollFactor.value[1]);
213
313
  return regionsArray;
214
314
  }
215
315
  async getSeleniumRegionsByLocation(customLocations) {
@@ -239,12 +339,4 @@ export default class GenericProvider {
239
339
  }
240
340
  return elementsArray;
241
341
  }
242
- async getHeaderFooter(deviceName, osVersion, browserName) {
243
- // passing 0 as key, since across different pages and tests, this config will remain same
244
- const devicesConfig = await Cache.withCache(Cache.devicesConfig, 0, async () => {
245
- return (await request(DEVICES_CONFIG_URL, httpsAgent())).body;
246
- });
247
- let deviceKey = `${deviceName}-${osVersion}`;
248
- return devicesConfig[deviceKey] ? devicesConfig[deviceKey][browserName] ? [devicesConfig[deviceKey][browserName].header, devicesConfig[deviceKey][browserName].footer] : [0, 0] : [0, 0];
249
- }
250
342
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percy/webdriver-utils",
3
- "version": "1.27.5-alpha.0",
3
+ "version": "1.27.5-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.5-alpha.0",
33
- "@percy/sdk-utils": "1.27.5-alpha.0"
32
+ "@percy/config": "1.27.5-beta.1",
33
+ "@percy/sdk-utils": "1.27.5-beta.1"
34
34
  },
35
- "gitHead": "8ecc32db25f708a01192b8454d0fdf9c051f48a0"
35
+ "gitHead": "f5924b050d0c04f8ac63d610af291b32f51565f8"
36
36
  }
@@ -1,8 +0,0 @@
1
- import https from 'https';
2
- export function httpsAgent() {
3
- return new https.Agent({
4
- minVersion: 'TLSv1.2',
5
- maxVersion: 'TLSv1.2'
6
- });
7
- }
8
- export default httpsAgent;