@percy/webdriver-utils 1.27.0-alpha.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.
- package/dist/driver.js +65 -0
- package/dist/index.js +36 -0
- package/dist/metadata/desktopMetaData.js +56 -0
- package/dist/metadata/metaDataResolver.js +13 -0
- package/dist/metadata/mobileMetaData.js +50 -0
- package/dist/providers/automateProvider.js +133 -0
- package/dist/providers/genericProvider.js +223 -0
- package/dist/providers/providerResolver.js +9 -0
- package/dist/util/cache.js +62 -0
- package/dist/util/tile.js +19 -0
- package/dist/util/timing.js +47 -0
- package/dist/util/validations.js +7 -0
- package/package.json +36 -0
package/dist/driver.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import utils from '@percy/sdk-utils';
|
|
2
|
+
import Cache from './util/cache.js';
|
|
3
|
+
const {
|
|
4
|
+
request
|
|
5
|
+
} = utils;
|
|
6
|
+
export default class Driver {
|
|
7
|
+
constructor(sessionId, executorUrl) {
|
|
8
|
+
this.sessionId = sessionId;
|
|
9
|
+
this.executorUrl = executorUrl.includes('@') ? `https://${executorUrl.split('@')[1]}` : executorUrl;
|
|
10
|
+
}
|
|
11
|
+
async getCapabilites() {
|
|
12
|
+
return await Cache.withCache(Cache.caps, this.sessionId, async () => {
|
|
13
|
+
const baseUrl = `${this.executorUrl}/session/${this.sessionId}`;
|
|
14
|
+
const caps = JSON.parse((await request(baseUrl)).body);
|
|
15
|
+
return caps.value;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
async getWindowSize() {
|
|
19
|
+
const baseUrl = `${this.executorUrl}/session/${this.sessionId}/window/current/size`;
|
|
20
|
+
const windowSize = JSON.parse((await request(baseUrl)).body);
|
|
21
|
+
return windowSize;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// command => {script: "", args: []}
|
|
25
|
+
async executeScript(command) {
|
|
26
|
+
if (!command.constructor === Object || !(Object.keys(command).length === 2 && Object.keys(command).includes('script') && Object.keys(command).includes('args'))) {
|
|
27
|
+
throw new Error('Please pass command as {script: "", args: []}');
|
|
28
|
+
}
|
|
29
|
+
const options = {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: {
|
|
32
|
+
'Content-Type': 'application/json;charset=utf-8'
|
|
33
|
+
},
|
|
34
|
+
body: JSON.stringify(command)
|
|
35
|
+
};
|
|
36
|
+
const baseUrl = `${this.executorUrl}/session/${this.sessionId}/execute/sync`;
|
|
37
|
+
const response = JSON.parse((await request(baseUrl, options)).body);
|
|
38
|
+
return response;
|
|
39
|
+
}
|
|
40
|
+
async takeScreenshot() {
|
|
41
|
+
const baseUrl = `${this.executorUrl}/session/${this.sessionId}/screenshot`;
|
|
42
|
+
const screenShot = JSON.parse((await request(baseUrl)).body);
|
|
43
|
+
return screenShot.value;
|
|
44
|
+
}
|
|
45
|
+
async rect(elementId) {
|
|
46
|
+
const baseUrl = `${this.executorUrl}/session/${this.sessionId}/element/${elementId}/rect`;
|
|
47
|
+
const response = JSON.parse((await request(baseUrl)).body);
|
|
48
|
+
return response.value;
|
|
49
|
+
}
|
|
50
|
+
async findElement(using, value) {
|
|
51
|
+
const options = {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: {
|
|
54
|
+
'Content-Type': 'application/json;charset=utf-8'
|
|
55
|
+
},
|
|
56
|
+
body: JSON.stringify({
|
|
57
|
+
using,
|
|
58
|
+
value
|
|
59
|
+
})
|
|
60
|
+
};
|
|
61
|
+
const baseUrl = `${this.executorUrl}/session/${this.sessionId}/element`;
|
|
62
|
+
const response = JSON.parse((await request(baseUrl, options)).body);
|
|
63
|
+
return response.value;
|
|
64
|
+
}
|
|
65
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import ProviderResolver from './providers/providerResolver.js';
|
|
2
|
+
import utils from '@percy/sdk-utils';
|
|
3
|
+
import { camelcase } from '@percy/config/utils';
|
|
4
|
+
export default class WebdriverUtils {
|
|
5
|
+
log = utils.logger('webdriver-utils:main');
|
|
6
|
+
constructor({
|
|
7
|
+
sessionId,
|
|
8
|
+
commandExecutorUrl,
|
|
9
|
+
capabilities,
|
|
10
|
+
sessionCapabilites,
|
|
11
|
+
snapshotName,
|
|
12
|
+
clientInfo,
|
|
13
|
+
environmentInfo,
|
|
14
|
+
options = {}
|
|
15
|
+
}) {
|
|
16
|
+
this.sessionId = sessionId;
|
|
17
|
+
this.commandExecutorUrl = commandExecutorUrl;
|
|
18
|
+
this.capabilities = capabilities;
|
|
19
|
+
this.sessionCapabilites = sessionCapabilites;
|
|
20
|
+
this.snapshotName = snapshotName;
|
|
21
|
+
const camelCasedOptions = {};
|
|
22
|
+
Object.keys(options).forEach(key => {
|
|
23
|
+
let newKey = camelcase(key);
|
|
24
|
+
camelCasedOptions[newKey] = options[key];
|
|
25
|
+
});
|
|
26
|
+
this.options = camelCasedOptions;
|
|
27
|
+
this.clientInfo = clientInfo;
|
|
28
|
+
this.environmentInfo = environmentInfo;
|
|
29
|
+
}
|
|
30
|
+
async automateScreenshot() {
|
|
31
|
+
this.log.info('Starting automate screenshot');
|
|
32
|
+
const automate = ProviderResolver.resolve(this.sessionId, this.commandExecutorUrl, this.capabilities, this.sessionCapabilites, this.clientInfo, this.environmentInfo, this.options);
|
|
33
|
+
await automate.createDriver();
|
|
34
|
+
return await automate.screenshot(this.snapshotName, this.options);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export default class DesktopMetaData {
|
|
2
|
+
constructor(driver, opts) {
|
|
3
|
+
this.driver = driver;
|
|
4
|
+
this.capabilities = opts;
|
|
5
|
+
}
|
|
6
|
+
browserName() {
|
|
7
|
+
return this.capabilities.browserName.toLowerCase();
|
|
8
|
+
}
|
|
9
|
+
browserVersion() {
|
|
10
|
+
return this.capabilities.browserVersion.split('.')[0];
|
|
11
|
+
}
|
|
12
|
+
osName() {
|
|
13
|
+
let osName = this.capabilities.os;
|
|
14
|
+
if (osName) return osName.toLowerCase();
|
|
15
|
+
osName = this.capabilities.platform;
|
|
16
|
+
return osName;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// showing major version
|
|
20
|
+
osVersion() {
|
|
21
|
+
return this.capabilities.osVersion.toLowerCase();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// combination of browserName + browserVersion + osVersion + osName
|
|
25
|
+
deviceName() {
|
|
26
|
+
return this.browserName() + '_' + this.browserVersion() + '_' + this.osVersion() + '_' + this.osName();
|
|
27
|
+
}
|
|
28
|
+
orientation() {
|
|
29
|
+
return 'landscape';
|
|
30
|
+
}
|
|
31
|
+
async windowSize() {
|
|
32
|
+
const dpr = await this.devicePixelRatio();
|
|
33
|
+
const data = await this.driver.getWindowSize();
|
|
34
|
+
const width = parseInt(data.value.width * dpr),
|
|
35
|
+
height = parseInt(data.value.height * dpr);
|
|
36
|
+
return {
|
|
37
|
+
width,
|
|
38
|
+
height
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async screenResolution() {
|
|
42
|
+
const data = await this.driver.executeScript({
|
|
43
|
+
script: 'return [window.screen.width.toString(), window.screen.height.toString()];',
|
|
44
|
+
args: []
|
|
45
|
+
});
|
|
46
|
+
const screenInfo = data.value;
|
|
47
|
+
return `${screenInfo[0]} x ${screenInfo[1]}`;
|
|
48
|
+
}
|
|
49
|
+
async devicePixelRatio() {
|
|
50
|
+
const devicePixelRatio = await this.driver.executeScript({
|
|
51
|
+
script: 'return window.devicePixelRatio;',
|
|
52
|
+
args: []
|
|
53
|
+
});
|
|
54
|
+
return devicePixelRatio.value;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import DesktopMetaData from './desktopMetaData.js';
|
|
2
|
+
import MobileMetaData from './mobileMetaData.js';
|
|
3
|
+
export default class MetaDataResolver {
|
|
4
|
+
static resolve(driver, capabilities, opts) {
|
|
5
|
+
if (!driver) throw new Error('Please pass a Driver object');
|
|
6
|
+
const platform = opts.platformName || opts.platform;
|
|
7
|
+
if (['ios', 'android'].includes(platform.toLowerCase())) {
|
|
8
|
+
return new MobileMetaData(driver, capabilities);
|
|
9
|
+
} else {
|
|
10
|
+
return new DesktopMetaData(driver, capabilities);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export default class MobileMetaData {
|
|
2
|
+
constructor(driver, opts) {
|
|
3
|
+
this.driver = driver;
|
|
4
|
+
this.capabilities = opts;
|
|
5
|
+
}
|
|
6
|
+
browserName() {
|
|
7
|
+
return this.capabilities.browserName.toLowerCase();
|
|
8
|
+
}
|
|
9
|
+
browserVersion() {
|
|
10
|
+
var _this$capabilities$br;
|
|
11
|
+
const bsVersion = (_this$capabilities$br = this.capabilities.browserVersion) === null || _this$capabilities$br === void 0 ? void 0 : _this$capabilities$br.split('.');
|
|
12
|
+
if ((bsVersion === null || bsVersion === void 0 ? void 0 : bsVersion.length) > 0) {
|
|
13
|
+
return bsVersion[0];
|
|
14
|
+
}
|
|
15
|
+
return this.capabilities.version.split('.')[0];
|
|
16
|
+
}
|
|
17
|
+
osName() {
|
|
18
|
+
let osName = this.capabilities.os.toLowerCase();
|
|
19
|
+
if (osName === 'mac' && this.browserName() === 'iphone') {
|
|
20
|
+
osName = 'ios';
|
|
21
|
+
}
|
|
22
|
+
return osName;
|
|
23
|
+
}
|
|
24
|
+
osVersion() {
|
|
25
|
+
return this.capabilities.osVersion.split('.')[0];
|
|
26
|
+
}
|
|
27
|
+
deviceName() {
|
|
28
|
+
return this.capabilities.deviceName.split('-')[0];
|
|
29
|
+
}
|
|
30
|
+
orientation() {
|
|
31
|
+
return this.capabilities.orientation;
|
|
32
|
+
}
|
|
33
|
+
async windowSize() {
|
|
34
|
+
const dpr = await this.devicePixelRatio();
|
|
35
|
+
const data = await this.driver.getWindowSize();
|
|
36
|
+
const width = parseInt(data.value.width * dpr),
|
|
37
|
+
height = parseInt(data.value.height * dpr);
|
|
38
|
+
return {
|
|
39
|
+
width,
|
|
40
|
+
height
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
async devicePixelRatio() {
|
|
44
|
+
const devicePixelRatio = await this.driver.executeScript({
|
|
45
|
+
script: 'return window.devicePixelRatio;',
|
|
46
|
+
args: []
|
|
47
|
+
});
|
|
48
|
+
return devicePixelRatio.value;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import utils from '@percy/sdk-utils';
|
|
2
|
+
import GenericProvider from './genericProvider.js';
|
|
3
|
+
import Cache from '../util/cache.js';
|
|
4
|
+
import Tile from '../util/tile.js';
|
|
5
|
+
const log = utils.logger('webdriver-utils:automateProvider');
|
|
6
|
+
const {
|
|
7
|
+
TimeIt
|
|
8
|
+
} = utils;
|
|
9
|
+
export default class AutomateProvider extends GenericProvider {
|
|
10
|
+
constructor(sessionId, commandExecutorUrl, capabilities, sessionCapabilites, clientInfo, environmentInfo, options) {
|
|
11
|
+
super(sessionId, commandExecutorUrl, capabilities, sessionCapabilites, clientInfo, environmentInfo, options);
|
|
12
|
+
this._markedPercy = false;
|
|
13
|
+
}
|
|
14
|
+
static supports(commandExecutorUrl) {
|
|
15
|
+
return commandExecutorUrl.includes(process.env.AA_DOMAIN || 'browserstack');
|
|
16
|
+
}
|
|
17
|
+
async screenshot(name, {
|
|
18
|
+
ignoreRegionXpaths = [],
|
|
19
|
+
ignoreRegionSelectors = [],
|
|
20
|
+
ignoreRegionElements = [],
|
|
21
|
+
customIgnoreRegions = []
|
|
22
|
+
}) {
|
|
23
|
+
let response = null;
|
|
24
|
+
let error;
|
|
25
|
+
try {
|
|
26
|
+
let result = await this.percyScreenshotBegin(name);
|
|
27
|
+
this.setDebugUrl(result);
|
|
28
|
+
response = await super.screenshot(name, {
|
|
29
|
+
ignoreRegionXpaths,
|
|
30
|
+
ignoreRegionSelectors,
|
|
31
|
+
ignoreRegionElements,
|
|
32
|
+
customIgnoreRegions
|
|
33
|
+
});
|
|
34
|
+
} catch (e) {
|
|
35
|
+
error = e;
|
|
36
|
+
throw e;
|
|
37
|
+
} finally {
|
|
38
|
+
var _response, _response$body;
|
|
39
|
+
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}`);
|
|
40
|
+
}
|
|
41
|
+
return response;
|
|
42
|
+
}
|
|
43
|
+
async percyScreenshotBegin(name) {
|
|
44
|
+
return await TimeIt.run('percyScreenshotBegin', async () => {
|
|
45
|
+
try {
|
|
46
|
+
let result = await this.browserstackExecutor('percyScreenshot', {
|
|
47
|
+
name,
|
|
48
|
+
percyBuildId: process.env.PERCY_BUILD_ID,
|
|
49
|
+
percyBuildUrl: process.env.PERCY_BUILD_URL,
|
|
50
|
+
state: 'begin'
|
|
51
|
+
});
|
|
52
|
+
this._markedPercy = result.success;
|
|
53
|
+
return result;
|
|
54
|
+
} catch (e) {
|
|
55
|
+
log.debug(`[${name}] Could not mark Automate session as percy`);
|
|
56
|
+
log.error(`[${name}] error: ${e.toString()}`);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
async percyScreenshotEnd(name, percyScreenshotUrl, statusMessage = null) {
|
|
62
|
+
return await TimeIt.run('percyScreenshotEnd', async () => {
|
|
63
|
+
try {
|
|
64
|
+
await this.browserstackExecutor('percyScreenshot', {
|
|
65
|
+
name,
|
|
66
|
+
percyScreenshotUrl,
|
|
67
|
+
status: percyScreenshotUrl ? 'success' : 'failure',
|
|
68
|
+
statusMessage,
|
|
69
|
+
state: 'end'
|
|
70
|
+
});
|
|
71
|
+
} catch (e) {
|
|
72
|
+
log.debug(`[${name}] Could not mark Automate session as percy`);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
async getTiles(fullscreen) {
|
|
77
|
+
if (!this.driver) throw new Error('Driver is null, please initialize driver with createDriver().');
|
|
78
|
+
const response = await TimeIt.run('percyScreenshot:screenshot', async () => {
|
|
79
|
+
return await this.browserstackExecutor('percyScreenshot', {
|
|
80
|
+
state: 'screenshot',
|
|
81
|
+
percyBuildId: process.env.PERCY_BUILD_ID,
|
|
82
|
+
screenshotType: 'singlepage',
|
|
83
|
+
scaleFactor: await this.driver.executeScript({
|
|
84
|
+
script: 'return window.devicePixelRatio;',
|
|
85
|
+
args: []
|
|
86
|
+
}),
|
|
87
|
+
options: this.options
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
const responseValue = JSON.parse(response.value);
|
|
91
|
+
if (!responseValue.success) {
|
|
92
|
+
throw new Error('Failed to get screenshots from Automate.' + ' Check dashboard for error.');
|
|
93
|
+
}
|
|
94
|
+
const tiles = [];
|
|
95
|
+
const tileResponse = JSON.parse(responseValue.result);
|
|
96
|
+
for (let tileData of tileResponse.sha) {
|
|
97
|
+
tiles.push(new Tile({
|
|
98
|
+
statusBarHeight: 0,
|
|
99
|
+
navBarHeight: 0,
|
|
100
|
+
headerHeight: 0,
|
|
101
|
+
footerHeight: 0,
|
|
102
|
+
fullscreen,
|
|
103
|
+
sha: tileData.split('-')[0] // drop build id
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
tiles: tiles,
|
|
109
|
+
domInfoSha: tileResponse.dom_sha
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async browserstackExecutor(action, args) {
|
|
113
|
+
if (!this.driver) throw new Error('Driver is null, please initialize driver with createDriver().');
|
|
114
|
+
let options = args ? {
|
|
115
|
+
action,
|
|
116
|
+
arguments: args
|
|
117
|
+
} : {
|
|
118
|
+
action
|
|
119
|
+
};
|
|
120
|
+
let res = await this.driver.executeScript({
|
|
121
|
+
script: `browserstack_executor: ${JSON.stringify(options)}`,
|
|
122
|
+
args: []
|
|
123
|
+
});
|
|
124
|
+
return res;
|
|
125
|
+
}
|
|
126
|
+
async setDebugUrl() {
|
|
127
|
+
if (!this.driver) throw new Error('Driver is null, please initialize driver with createDriver().');
|
|
128
|
+
this.debugUrl = await Cache.withCache(Cache.bstackSessionDetails, this.driver.sessionId, async () => {
|
|
129
|
+
const sessionDetails = await this.browserstackExecutor('getSessionDetails');
|
|
130
|
+
return JSON.parse(sessionDetails.value).browser_url;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import utils from '@percy/sdk-utils';
|
|
2
|
+
import MetaDataResolver from '../metadata/metaDataResolver.js';
|
|
3
|
+
import Tile from '../util/tile.js';
|
|
4
|
+
import Driver from '../driver.js';
|
|
5
|
+
const log = utils.logger('webdriver-utils:genericProvider');
|
|
6
|
+
export default class GenericProvider {
|
|
7
|
+
clientInfo = new Set();
|
|
8
|
+
environmentInfo = new Set();
|
|
9
|
+
options = {};
|
|
10
|
+
constructor(sessionId, commandExecutorUrl, capabilities, sessionCapabilites, clientInfo, environmentInfo, options) {
|
|
11
|
+
this.sessionId = sessionId;
|
|
12
|
+
this.commandExecutorUrl = commandExecutorUrl;
|
|
13
|
+
this.capabilities = capabilities;
|
|
14
|
+
this.sessionCapabilites = sessionCapabilites;
|
|
15
|
+
this.addClientInfo(clientInfo);
|
|
16
|
+
this.addEnvironmentInfo(environmentInfo);
|
|
17
|
+
this.options = options;
|
|
18
|
+
this.driver = null;
|
|
19
|
+
this.metaData = null;
|
|
20
|
+
this.debugUrl = null;
|
|
21
|
+
}
|
|
22
|
+
async createDriver() {
|
|
23
|
+
this.driver = new Driver(this.sessionId, this.commandExecutorUrl);
|
|
24
|
+
const caps = await this.driver.getCapabilites();
|
|
25
|
+
this.metaData = await MetaDataResolver.resolve(this.driver, caps, this.capabilities);
|
|
26
|
+
}
|
|
27
|
+
static supports(_commandExecutorUrl) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
addClientInfo(info) {
|
|
31
|
+
for (let i of [].concat(info)) {
|
|
32
|
+
if (i) this.clientInfo.add(i);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
addEnvironmentInfo(info) {
|
|
36
|
+
for (let i of [].concat(info)) {
|
|
37
|
+
if (i) this.environmentInfo.add(i);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async addPercyCSS(userCSS) {
|
|
41
|
+
const createStyleElement = `const e = document.createElement('style');
|
|
42
|
+
e.setAttribute('data-percy-specific-css', true);
|
|
43
|
+
e.innerHTML = '${userCSS}';
|
|
44
|
+
document.body.appendChild(e);`;
|
|
45
|
+
await this.driver.executeScript({
|
|
46
|
+
script: createStyleElement,
|
|
47
|
+
args: []
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async removePercyCSS() {
|
|
51
|
+
const removeStyleElement = `const n = document.querySelector('[data-percy-specific-css]');
|
|
52
|
+
n.remove();`;
|
|
53
|
+
await this.driver.executeScript({
|
|
54
|
+
script: removeStyleElement,
|
|
55
|
+
args: []
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
async screenshot(name, {
|
|
59
|
+
ignoreRegionXpaths = [],
|
|
60
|
+
ignoreRegionSelectors = [],
|
|
61
|
+
ignoreRegionElements = [],
|
|
62
|
+
customIgnoreRegions = []
|
|
63
|
+
}) {
|
|
64
|
+
let fullscreen = false;
|
|
65
|
+
const percyCSS = this.options.percyCSS || '';
|
|
66
|
+
await this.addPercyCSS(percyCSS);
|
|
67
|
+
const tag = await this.getTag();
|
|
68
|
+
const tiles = await this.getTiles(fullscreen);
|
|
69
|
+
const ignoreRegions = await this.findIgnoredRegions(ignoreRegionXpaths, ignoreRegionSelectors, ignoreRegionElements, customIgnoreRegions);
|
|
70
|
+
await this.setDebugUrl();
|
|
71
|
+
await this.removePercyCSS();
|
|
72
|
+
log.debug(`${name} : Tag ${JSON.stringify(tag)}`);
|
|
73
|
+
log.debug(`${name} : Tiles ${JSON.stringify(tiles)}`);
|
|
74
|
+
log.debug(`${name} : Debug url ${this.debugUrl}`);
|
|
75
|
+
return {
|
|
76
|
+
name,
|
|
77
|
+
tag,
|
|
78
|
+
tiles: tiles.tiles,
|
|
79
|
+
// TODO: Fetch this one for bs automate, check appium sdk
|
|
80
|
+
externalDebugUrl: this.debugUrl,
|
|
81
|
+
ignoredElementsData: ignoreRegions,
|
|
82
|
+
environmentInfo: [...this.environmentInfo].join('; '),
|
|
83
|
+
clientInfo: [...this.clientInfo].join(' '),
|
|
84
|
+
domInfoSha: tiles.domInfoSha
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// TODO: get dom sha for non-automate
|
|
89
|
+
async getDomContent() {
|
|
90
|
+
// execute script and return dom content
|
|
91
|
+
return 'dummyValue';
|
|
92
|
+
}
|
|
93
|
+
async getTiles(fullscreen) {
|
|
94
|
+
if (!this.driver) throw new Error('Driver is null, please initialize driver with createDriver().');
|
|
95
|
+
const base64content = await this.driver.takeScreenshot();
|
|
96
|
+
return {
|
|
97
|
+
tiles: [new Tile({
|
|
98
|
+
content: base64content,
|
|
99
|
+
// TODO: Need to add method to fetch these attr
|
|
100
|
+
statusBarHeight: 0,
|
|
101
|
+
navBarHeight: 0,
|
|
102
|
+
headerHeight: 0,
|
|
103
|
+
footerHeight: 0,
|
|
104
|
+
fullscreen
|
|
105
|
+
})],
|
|
106
|
+
// TODO: Add Generic support sha for contextual diff
|
|
107
|
+
domInfoSha: await this.getDomContent()
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
async getTag() {
|
|
111
|
+
if (!this.driver) throw new Error('Driver is null, please initialize driver with createDriver().');
|
|
112
|
+
const {
|
|
113
|
+
width,
|
|
114
|
+
height
|
|
115
|
+
} = await this.metaData.windowSize();
|
|
116
|
+
const resolution = await this.metaData.screenResolution();
|
|
117
|
+
const orientation = this.metaData.orientation();
|
|
118
|
+
return {
|
|
119
|
+
name: this.metaData.deviceName(),
|
|
120
|
+
osName: this.metaData.osName(),
|
|
121
|
+
osVersion: this.metaData.osVersion(),
|
|
122
|
+
width,
|
|
123
|
+
height,
|
|
124
|
+
orientation: orientation,
|
|
125
|
+
browserName: this.metaData.browserName(),
|
|
126
|
+
browserVersion: this.metaData.browserVersion(),
|
|
127
|
+
resolution: resolution
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// TODO: Add Debugging Url
|
|
132
|
+
async setDebugUrl() {
|
|
133
|
+
this.debugUrl = 'https://localhost/v1';
|
|
134
|
+
}
|
|
135
|
+
async findIgnoredRegions(ignoreRegionXpaths, ignoreRegionSelectors, ignoreRegionElements, customIgnoreRegions) {
|
|
136
|
+
const ignoreElementXpaths = await this.getIgnoreRegionsBy('xpath', ignoreRegionXpaths);
|
|
137
|
+
const ignoreElementSelectors = await this.getIgnoreRegionsBy('css selector', ignoreRegionSelectors);
|
|
138
|
+
const ignoreElements = await this.getIgnoreRegionsByElement(ignoreRegionElements);
|
|
139
|
+
const ignoreElementCustom = await this.getCustomIgnoreRegions(customIgnoreRegions);
|
|
140
|
+
return {
|
|
141
|
+
ignoreElementsData: [...ignoreElementXpaths, ...ignoreElementSelectors, ...ignoreElements, ...ignoreElementCustom]
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
async ignoreElementObject(selector, elementId) {
|
|
145
|
+
const scaleFactor = parseInt(await this.metaData.devicePixelRatio());
|
|
146
|
+
const rect = await this.driver.rect(elementId);
|
|
147
|
+
const location = {
|
|
148
|
+
x: rect.x,
|
|
149
|
+
y: rect.y
|
|
150
|
+
};
|
|
151
|
+
const size = {
|
|
152
|
+
height: rect.height,
|
|
153
|
+
width: rect.width
|
|
154
|
+
};
|
|
155
|
+
const coOrdinates = {
|
|
156
|
+
top: Math.floor(location.y * scaleFactor),
|
|
157
|
+
bottom: Math.ceil((location.y + size.height) * scaleFactor),
|
|
158
|
+
left: Math.floor(location.x * scaleFactor),
|
|
159
|
+
right: Math.ceil((location.x + size.width) * scaleFactor)
|
|
160
|
+
};
|
|
161
|
+
const jsonObject = {
|
|
162
|
+
selector,
|
|
163
|
+
coOrdinates
|
|
164
|
+
};
|
|
165
|
+
return jsonObject;
|
|
166
|
+
}
|
|
167
|
+
async getIgnoreRegionsBy(findBy, elements) {
|
|
168
|
+
const ignoredElementsArray = [];
|
|
169
|
+
for (const idx in elements) {
|
|
170
|
+
try {
|
|
171
|
+
const element = await this.driver.findElement(findBy, elements[idx]);
|
|
172
|
+
const selector = `${findBy}: ${elements[idx]}`;
|
|
173
|
+
const ignoredRegion = await this.ignoreElementObject(selector, element[Object.keys(element)[0]]);
|
|
174
|
+
ignoredElementsArray.push(ignoredRegion);
|
|
175
|
+
} catch (e) {
|
|
176
|
+
log.warn(`Selenium Element with ${findBy}: ${elements[idx]} not found. Ignoring this ${findBy}.`);
|
|
177
|
+
log.error(e.toString());
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return ignoredElementsArray;
|
|
181
|
+
}
|
|
182
|
+
async getIgnoreRegionsByElement(elements) {
|
|
183
|
+
const ignoredElementsArray = [];
|
|
184
|
+
for (let index = 0; index < elements.length; index++) {
|
|
185
|
+
try {
|
|
186
|
+
const selector = `element: ${index}`;
|
|
187
|
+
const ignoredRegion = await this.ignoreElementObject(selector, elements[index]);
|
|
188
|
+
ignoredElementsArray.push(ignoredRegion);
|
|
189
|
+
} catch (e) {
|
|
190
|
+
log.warn(`Correct Web Element not passed at index ${index}.`);
|
|
191
|
+
log.debug(e.toString());
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return ignoredElementsArray;
|
|
195
|
+
}
|
|
196
|
+
async getCustomIgnoreRegions(customLocations) {
|
|
197
|
+
const ignoredElementsArray = [];
|
|
198
|
+
const {
|
|
199
|
+
width,
|
|
200
|
+
height
|
|
201
|
+
} = await this.metaData.windowSize();
|
|
202
|
+
for (let index = 0; index < customLocations.length; index++) {
|
|
203
|
+
const customLocation = customLocations[index];
|
|
204
|
+
const invalid = customLocation.top >= height || customLocation.bottom > height || customLocation.left >= width || customLocation.right > width;
|
|
205
|
+
if (!invalid) {
|
|
206
|
+
const selector = `custom ignore region ${index}`;
|
|
207
|
+
const ignoredRegion = {
|
|
208
|
+
selector,
|
|
209
|
+
coOrdinates: {
|
|
210
|
+
top: customLocation.top,
|
|
211
|
+
bottom: customLocation.bottom,
|
|
212
|
+
left: customLocation.left,
|
|
213
|
+
right: customLocation.right
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
ignoredElementsArray.push(ignoredRegion);
|
|
217
|
+
} else {
|
|
218
|
+
log.warn(`Values passed in custom ignored region at index: ${index} is not valid`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return ignoredElementsArray;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import GenericProvider from './genericProvider.js';
|
|
2
|
+
import AutomateProvider from './automateProvider.js';
|
|
3
|
+
export default class ProviderResolver {
|
|
4
|
+
static resolve(sessionId, commandExecutorUrl, capabilities, sessionCapabilities, clientInfo, environmentInfo, options) {
|
|
5
|
+
// We can safely do [0] because GenericProvider is catch all
|
|
6
|
+
const Klass = [AutomateProvider, GenericProvider].filter(x => x.supports(commandExecutorUrl))[0];
|
|
7
|
+
return new Klass(sessionId, commandExecutorUrl, capabilities, sessionCapabilities, clientInfo, environmentInfo, options);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import utils from '@percy/sdk-utils';
|
|
2
|
+
const {
|
|
3
|
+
Undefined
|
|
4
|
+
} = utils;
|
|
5
|
+
export default class Cache {
|
|
6
|
+
static cache = {};
|
|
7
|
+
|
|
8
|
+
// Common stores, const, dont modify outside
|
|
9
|
+
static caps = 'caps';
|
|
10
|
+
static bstackSessionDetails = 'bstackSessionDetails';
|
|
11
|
+
static systemBars = 'systemBars';
|
|
12
|
+
|
|
13
|
+
// maintainance
|
|
14
|
+
static lastTime = Date.now();
|
|
15
|
+
static timeout = 5 * 60 * 1000;
|
|
16
|
+
static async withCache(store, key, func, cacheExceptions = false) {
|
|
17
|
+
this.maintain();
|
|
18
|
+
if (Undefined(this.cache[store])) this.cache[store] = {};
|
|
19
|
+
store = this.cache[store];
|
|
20
|
+
if (store[key]) {
|
|
21
|
+
if (store[key].success) {
|
|
22
|
+
return store[key].val;
|
|
23
|
+
} else {
|
|
24
|
+
throw store[key].val;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const obj = {
|
|
28
|
+
success: false,
|
|
29
|
+
val: null,
|
|
30
|
+
time: Date.now()
|
|
31
|
+
};
|
|
32
|
+
try {
|
|
33
|
+
obj.val = await func();
|
|
34
|
+
obj.success = true;
|
|
35
|
+
} catch (e) {
|
|
36
|
+
obj.val = e;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// We seem to have correct coverage for both flows but nyc is marking it as missing
|
|
40
|
+
// branch coverage anyway
|
|
41
|
+
/* istanbul ignore next */
|
|
42
|
+
if (obj.success || cacheExceptions) {
|
|
43
|
+
store[key] = obj;
|
|
44
|
+
}
|
|
45
|
+
if (!obj.success) throw obj.val;
|
|
46
|
+
return obj.val;
|
|
47
|
+
}
|
|
48
|
+
static maintain() {
|
|
49
|
+
if (this.lastTime + this.timeout > Date.now()) return;
|
|
50
|
+
for (const [, store] of Object.entries(this.cache)) {
|
|
51
|
+
for (const [key, item] of Object.entries(store)) {
|
|
52
|
+
if (item.time + this.timeout < Date.now()) {
|
|
53
|
+
delete store[key];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
this.lastTime = Date.now();
|
|
58
|
+
}
|
|
59
|
+
static reset() {
|
|
60
|
+
this.cache = {};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export default class Tile {
|
|
2
|
+
constructor({
|
|
3
|
+
content,
|
|
4
|
+
statusBarHeight,
|
|
5
|
+
navBarHeight,
|
|
6
|
+
headerHeight,
|
|
7
|
+
footerHeight,
|
|
8
|
+
fullscreen,
|
|
9
|
+
sha
|
|
10
|
+
}) {
|
|
11
|
+
this.content = content;
|
|
12
|
+
this.statusBarHeight = statusBarHeight;
|
|
13
|
+
this.navBarHeight = navBarHeight;
|
|
14
|
+
this.headerHeight = headerHeight;
|
|
15
|
+
this.footerHeight = footerHeight;
|
|
16
|
+
this.fullscreen = fullscreen;
|
|
17
|
+
this.sha = sha;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import validations from './validations.js';
|
|
2
|
+
const {
|
|
3
|
+
Undefined
|
|
4
|
+
} = validations;
|
|
5
|
+
export default class TimeIt {
|
|
6
|
+
static data = {};
|
|
7
|
+
static enabled = process.env.PERCY_METRICS === 'true';
|
|
8
|
+
static async run(store, func) {
|
|
9
|
+
if (!this.enabled) return await func();
|
|
10
|
+
const t1 = Date.now();
|
|
11
|
+
try {
|
|
12
|
+
return await func();
|
|
13
|
+
} finally {
|
|
14
|
+
if (Undefined(this.data[store])) this.data[store] = [];
|
|
15
|
+
this.data[store].push(Date.now() - t1);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
static min(store) {
|
|
19
|
+
return Math.min(...this.data[store]);
|
|
20
|
+
}
|
|
21
|
+
static max(store) {
|
|
22
|
+
return Math.max(...this.data[store]);
|
|
23
|
+
}
|
|
24
|
+
static avg(store) {
|
|
25
|
+
const vals = this.data[store];
|
|
26
|
+
return vals.reduce((a, b) => a + b, 0) / vals.length;
|
|
27
|
+
}
|
|
28
|
+
static summary({
|
|
29
|
+
includeVals
|
|
30
|
+
} = {}) {
|
|
31
|
+
const agg = {};
|
|
32
|
+
for (const key of Object.keys(this.data)) {
|
|
33
|
+
agg[key] = {
|
|
34
|
+
min: this.min(key),
|
|
35
|
+
max: this.max(key),
|
|
36
|
+
avg: this.avg(key),
|
|
37
|
+
count: this.data[key].length
|
|
38
|
+
};
|
|
39
|
+
if (includeVals) agg[key].vals = this.data[key];
|
|
40
|
+
}
|
|
41
|
+
return agg;
|
|
42
|
+
}
|
|
43
|
+
static reset() {
|
|
44
|
+
this.data = {};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
;
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@percy/webdriver-utils",
|
|
3
|
+
"version": "1.27.0-alpha.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/percy/cli",
|
|
8
|
+
"directory": "packages/webdriver-utils"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public",
|
|
12
|
+
"tag": "alpha"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=14"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"main": "./dist/index.js",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "node ../../scripts/build",
|
|
27
|
+
"lint": "eslint --ignore-path ../../.gitignore .",
|
|
28
|
+
"test": "node ../../scripts/test",
|
|
29
|
+
"test:coverage": "yarn test --coverage"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@percy/config": "1.27.0-alpha.0",
|
|
33
|
+
"@percy/sdk-utils": "1.27.0-alpha.0"
|
|
34
|
+
},
|
|
35
|
+
"gitHead": "16f2c87641d844c6af6c3e198f6aff1c08ee0ec1"
|
|
36
|
+
}
|