@percy/webdriver-utils 1.27.0-alpha.0 → 1.27.0-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 +16 -4
- package/dist/index.js +14 -5
- package/dist/metadata/desktopMetaData.js +28 -16
- package/dist/metadata/metaDataResolver.js +2 -1
- package/dist/metadata/mobileMetaData.js +34 -12
- package/dist/metadata/normalizeData.js +34 -0
- package/dist/providers/automateProvider.js +70 -21
- package/dist/providers/genericProvider.js +102 -42
- package/dist/providers/providerResolver.js +2 -2
- package/dist/util/cache.js +3 -0
- package/package.json +5 -5
- package/dist/util/timing.js +0 -47
- package/dist/util/validations.js +0 -7
package/dist/driver.js
CHANGED
|
@@ -3,16 +3,23 @@ import Cache from './util/cache.js';
|
|
|
3
3
|
const {
|
|
4
4
|
request
|
|
5
5
|
} = utils;
|
|
6
|
+
const log = utils.logger('webdriver-utils:driver');
|
|
6
7
|
export default class Driver {
|
|
7
|
-
constructor(sessionId, executorUrl) {
|
|
8
|
+
constructor(sessionId, executorUrl, passedCapabilities) {
|
|
8
9
|
this.sessionId = sessionId;
|
|
9
10
|
this.executorUrl = executorUrl.includes('@') ? `https://${executorUrl.split('@')[1]}` : executorUrl;
|
|
11
|
+
this.passedCapabilities = passedCapabilities;
|
|
10
12
|
}
|
|
11
13
|
async getCapabilites() {
|
|
12
14
|
return await Cache.withCache(Cache.caps, this.sessionId, async () => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
try {
|
|
16
|
+
const baseUrl = `${this.executorUrl}/session/${this.sessionId}`;
|
|
17
|
+
const caps = JSON.parse((await request(baseUrl)).body);
|
|
18
|
+
return caps.value;
|
|
19
|
+
} catch (err) {
|
|
20
|
+
log.warn(`Falling back to legacy protocol, Error: ${err.message}`);
|
|
21
|
+
return this.passedCapabilities;
|
|
22
|
+
}
|
|
16
23
|
});
|
|
17
24
|
}
|
|
18
25
|
async getWindowSize() {
|
|
@@ -26,6 +33,11 @@ export default class Driver {
|
|
|
26
33
|
if (!command.constructor === Object || !(Object.keys(command).length === 2 && Object.keys(command).includes('script') && Object.keys(command).includes('args'))) {
|
|
27
34
|
throw new Error('Please pass command as {script: "", args: []}');
|
|
28
35
|
}
|
|
36
|
+
// browser_executor is custom BS executor script, if there is anything extra it breaks
|
|
37
|
+
// percy_automate_script is an anchor comment to identify percy automate scripts
|
|
38
|
+
if (!command.script.includes('browserstack_executor')) {
|
|
39
|
+
command.script = `/* percy_automate_script */ \n ${command.script}`;
|
|
40
|
+
}
|
|
29
41
|
const options = {
|
|
30
42
|
method: 'POST',
|
|
31
43
|
headers: {
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,8 @@ export default class WebdriverUtils {
|
|
|
11
11
|
snapshotName,
|
|
12
12
|
clientInfo,
|
|
13
13
|
environmentInfo,
|
|
14
|
-
options = {}
|
|
14
|
+
options = {},
|
|
15
|
+
buildInfo = {}
|
|
15
16
|
}) {
|
|
16
17
|
this.sessionId = sessionId;
|
|
17
18
|
this.commandExecutorUrl = commandExecutorUrl;
|
|
@@ -26,11 +27,19 @@ export default class WebdriverUtils {
|
|
|
26
27
|
this.options = camelCasedOptions;
|
|
27
28
|
this.clientInfo = clientInfo;
|
|
28
29
|
this.environmentInfo = environmentInfo;
|
|
30
|
+
this.buildInfo = buildInfo;
|
|
29
31
|
}
|
|
30
32
|
async automateScreenshot() {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
try {
|
|
34
|
+
this.log.info(`[${this.snapshotName}] : Starting automate screenshot ...`);
|
|
35
|
+
const automate = ProviderResolver.resolve(this.sessionId, this.commandExecutorUrl, this.capabilities, this.sessionCapabilites, this.clientInfo, this.environmentInfo, this.options, this.buildInfo);
|
|
36
|
+
this.log.debug(`[${this.snapshotName}] : Resolved provider ...`);
|
|
37
|
+
await automate.createDriver();
|
|
38
|
+
this.log.debug(`[${this.snapshotName}] : Created driver ...`);
|
|
39
|
+
return await automate.screenshot(this.snapshotName, this.options);
|
|
40
|
+
} catch (e) {
|
|
41
|
+
this.log.error(`[${this.snapshotName}] : Error - ${e.message}`);
|
|
42
|
+
this.log.error(`[${this.snapshotName}] : Error Log - ${e.toString()}`);
|
|
43
|
+
}
|
|
35
44
|
}
|
|
36
45
|
}
|
|
@@ -1,29 +1,37 @@
|
|
|
1
|
+
import Cache from '../util/cache.js';
|
|
1
2
|
export default class DesktopMetaData {
|
|
2
3
|
constructor(driver, opts) {
|
|
3
4
|
this.driver = driver;
|
|
4
5
|
this.capabilities = opts;
|
|
5
6
|
}
|
|
7
|
+
device() {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
6
10
|
browserName() {
|
|
7
|
-
|
|
11
|
+
var _this$capabilities, _this$capabilities$br;
|
|
12
|
+
return (_this$capabilities = this.capabilities) === null || _this$capabilities === void 0 ? void 0 : (_this$capabilities$br = _this$capabilities.browserName) === null || _this$capabilities$br === void 0 ? void 0 : _this$capabilities$br.toLowerCase();
|
|
8
13
|
}
|
|
9
14
|
browserVersion() {
|
|
10
|
-
|
|
15
|
+
var _this$capabilities2, _this$capabilities2$b;
|
|
16
|
+
return (_this$capabilities2 = this.capabilities) === null || _this$capabilities2 === void 0 ? void 0 : (_this$capabilities2$b = _this$capabilities2.browserVersion) === null || _this$capabilities2$b === void 0 ? void 0 : _this$capabilities2$b.split('.')[0];
|
|
11
17
|
}
|
|
12
18
|
osName() {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
osName =
|
|
19
|
+
var _this$capabilities3, _osName, _this$capabilities4;
|
|
20
|
+
let osName = (_this$capabilities3 = this.capabilities) === null || _this$capabilities3 === void 0 ? void 0 : _this$capabilities3.os;
|
|
21
|
+
if (osName) return (_osName = osName) === null || _osName === void 0 ? void 0 : _osName.toLowerCase();
|
|
22
|
+
osName = (_this$capabilities4 = this.capabilities) === null || _this$capabilities4 === void 0 ? void 0 : _this$capabilities4.platform;
|
|
16
23
|
return osName;
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
// showing major version
|
|
20
27
|
osVersion() {
|
|
21
|
-
|
|
28
|
+
var _this$capabilities5, _this$capabilities5$o;
|
|
29
|
+
return (_this$capabilities5 = this.capabilities) === null || _this$capabilities5 === void 0 ? void 0 : (_this$capabilities5$o = _this$capabilities5.osVersion) === null || _this$capabilities5$o === void 0 ? void 0 : _this$capabilities5$o.toLowerCase();
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
// combination of browserName + browserVersion + osVersion + osName
|
|
25
33
|
deviceName() {
|
|
26
|
-
return this.
|
|
34
|
+
return this.osName() + '_' + this.osVersion() + '_' + this.browserName() + '_' + this.browserVersion();
|
|
27
35
|
}
|
|
28
36
|
orientation() {
|
|
29
37
|
return 'landscape';
|
|
@@ -39,18 +47,22 @@ export default class DesktopMetaData {
|
|
|
39
47
|
};
|
|
40
48
|
}
|
|
41
49
|
async screenResolution() {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
return await Cache.withCache(Cache.resolution, this.driver.sessionId, async () => {
|
|
51
|
+
const data = await this.driver.executeScript({
|
|
52
|
+
script: 'return [(window.screen.width * window.devicePixelRatio).toString(), (window.screen.height * window.devicePixelRatio).toString()];',
|
|
53
|
+
args: []
|
|
54
|
+
});
|
|
55
|
+
const screenInfo = data.value;
|
|
56
|
+
return `${screenInfo[0]} x ${screenInfo[1]}`;
|
|
45
57
|
});
|
|
46
|
-
const screenInfo = data.value;
|
|
47
|
-
return `${screenInfo[0]} x ${screenInfo[1]}`;
|
|
48
58
|
}
|
|
49
59
|
async devicePixelRatio() {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
60
|
+
return await Cache.withCache(Cache.dpr, this.driver.sessionId, async () => {
|
|
61
|
+
const devicePixelRatio = await this.driver.executeScript({
|
|
62
|
+
script: 'return window.devicePixelRatio;',
|
|
63
|
+
args: []
|
|
64
|
+
});
|
|
65
|
+
return devicePixelRatio.value;
|
|
53
66
|
});
|
|
54
|
-
return devicePixelRatio.value;
|
|
55
67
|
}
|
|
56
68
|
}
|
|
@@ -2,9 +2,10 @@ import DesktopMetaData from './desktopMetaData.js';
|
|
|
2
2
|
import MobileMetaData from './mobileMetaData.js';
|
|
3
3
|
export default class MetaDataResolver {
|
|
4
4
|
static resolve(driver, capabilities, opts) {
|
|
5
|
+
var _capabilities$platfor, _capabilities$device, _capabilities$device$;
|
|
5
6
|
if (!driver) throw new Error('Please pass a Driver object');
|
|
6
7
|
const platform = opts.platformName || opts.platform;
|
|
7
|
-
if (['ios', 'android'].includes(platform.toLowerCase())) {
|
|
8
|
+
if (['ios', 'android'].includes(platform.toLowerCase()) || ['ios', 'android'].includes(capabilities === null || capabilities === void 0 ? void 0 : (_capabilities$platfor = capabilities.platformName) === null || _capabilities$platfor === void 0 ? void 0 : _capabilities$platfor.toLowerCase()) || ['ipad', 'iphone'].includes(capabilities === null || capabilities === void 0 ? void 0 : (_capabilities$device = capabilities.device) === null || _capabilities$device === void 0 ? void 0 : (_capabilities$device$ = _capabilities$device.toString()) === null || _capabilities$device$ === void 0 ? void 0 : _capabilities$device$.toLowerCase())) {
|
|
8
9
|
return new MobileMetaData(driver, capabilities);
|
|
9
10
|
} else {
|
|
10
11
|
return new DesktopMetaData(driver, capabilities);
|
|
@@ -1,34 +1,44 @@
|
|
|
1
|
+
import Cache from '../util/cache.js';
|
|
2
|
+
// Todo: Implement a base metadata for the common functions.
|
|
1
3
|
export default class MobileMetaData {
|
|
2
4
|
constructor(driver, opts) {
|
|
3
5
|
this.driver = driver;
|
|
4
6
|
this.capabilities = opts;
|
|
5
7
|
}
|
|
8
|
+
device() {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
6
11
|
browserName() {
|
|
7
|
-
|
|
12
|
+
var _this$capabilities, _this$capabilities$br;
|
|
13
|
+
return (_this$capabilities = this.capabilities) === null || _this$capabilities === void 0 ? void 0 : (_this$capabilities$br = _this$capabilities.browserName) === null || _this$capabilities$br === void 0 ? void 0 : _this$capabilities$br.toLowerCase();
|
|
8
14
|
}
|
|
9
15
|
browserVersion() {
|
|
10
|
-
var _this$capabilities$
|
|
11
|
-
const bsVersion = (_this$capabilities$
|
|
16
|
+
var _this$capabilities$br2, _this$capabilities2, _this$capabilities2$v;
|
|
17
|
+
const bsVersion = (_this$capabilities$br2 = this.capabilities.browserVersion) === null || _this$capabilities$br2 === void 0 ? void 0 : _this$capabilities$br2.split('.');
|
|
12
18
|
if ((bsVersion === null || bsVersion === void 0 ? void 0 : bsVersion.length) > 0) {
|
|
13
19
|
return bsVersion[0];
|
|
14
20
|
}
|
|
15
|
-
return this.capabilities.version.split('.')[0];
|
|
21
|
+
return (_this$capabilities2 = this.capabilities) === null || _this$capabilities2 === void 0 ? void 0 : (_this$capabilities2$v = _this$capabilities2.version) === null || _this$capabilities2$v === void 0 ? void 0 : _this$capabilities2$v.split('.')[0];
|
|
16
22
|
}
|
|
17
23
|
osName() {
|
|
18
|
-
|
|
24
|
+
var _this$capabilities3, _this$capabilities3$o;
|
|
25
|
+
let osName = (_this$capabilities3 = this.capabilities) === null || _this$capabilities3 === void 0 ? void 0 : (_this$capabilities3$o = _this$capabilities3.os) === null || _this$capabilities3$o === void 0 ? void 0 : _this$capabilities3$o.toLowerCase();
|
|
19
26
|
if (osName === 'mac' && this.browserName() === 'iphone') {
|
|
20
27
|
osName = 'ios';
|
|
21
28
|
}
|
|
22
29
|
return osName;
|
|
23
30
|
}
|
|
24
31
|
osVersion() {
|
|
25
|
-
|
|
32
|
+
var _this$capabilities4, _this$capabilities4$o;
|
|
33
|
+
return (_this$capabilities4 = this.capabilities) === null || _this$capabilities4 === void 0 ? void 0 : (_this$capabilities4$o = _this$capabilities4.osVersion) === null || _this$capabilities4$o === void 0 ? void 0 : _this$capabilities4$o.split('.')[0];
|
|
26
34
|
}
|
|
27
35
|
deviceName() {
|
|
28
|
-
|
|
36
|
+
var _this$capabilities5, _this$capabilities5$d;
|
|
37
|
+
return (_this$capabilities5 = this.capabilities) === null || _this$capabilities5 === void 0 ? void 0 : (_this$capabilities5$d = _this$capabilities5.deviceName) === null || _this$capabilities5$d === void 0 ? void 0 : _this$capabilities5$d.split('-')[0];
|
|
29
38
|
}
|
|
30
39
|
orientation() {
|
|
31
|
-
|
|
40
|
+
var _this$capabilities6;
|
|
41
|
+
return (_this$capabilities6 = this.capabilities) === null || _this$capabilities6 === void 0 ? void 0 : _this$capabilities6.orientation;
|
|
32
42
|
}
|
|
33
43
|
async windowSize() {
|
|
34
44
|
const dpr = await this.devicePixelRatio();
|
|
@@ -40,11 +50,23 @@ export default class MobileMetaData {
|
|
|
40
50
|
height
|
|
41
51
|
};
|
|
42
52
|
}
|
|
53
|
+
async screenResolution() {
|
|
54
|
+
return await Cache.withCache(Cache.resolution, this.driver.sessionId, async () => {
|
|
55
|
+
const data = await this.driver.executeScript({
|
|
56
|
+
script: 'return [parseInt(window.screen.width * window.devicePixelRatio).toString(), parseInt(window.screen.height * window.devicePixelRatio).toString()];',
|
|
57
|
+
args: []
|
|
58
|
+
});
|
|
59
|
+
const screenInfo = data.value;
|
|
60
|
+
return `${screenInfo[0]} x ${screenInfo[1]}`;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
43
63
|
async devicePixelRatio() {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
64
|
+
return await Cache.withCache(Cache.dpr, this.driver.sessionId, async () => {
|
|
65
|
+
const devicePixelRatio = await this.driver.executeScript({
|
|
66
|
+
script: 'return window.devicePixelRatio;',
|
|
67
|
+
args: []
|
|
68
|
+
});
|
|
69
|
+
return devicePixelRatio.value;
|
|
47
70
|
});
|
|
48
|
-
return devicePixelRatio.value;
|
|
49
71
|
}
|
|
50
72
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export default class NormalizeData {
|
|
2
|
+
osRollUp(os) {
|
|
3
|
+
if (os.toLowerCase().startsWith('win')) {
|
|
4
|
+
return 'Windows';
|
|
5
|
+
} else if (os.toLowerCase().startsWith('mac')) {
|
|
6
|
+
return 'OS X';
|
|
7
|
+
} else if (os.toLowerCase().includes('iphone') || os.toLowerCase().startsWith('ios')) {
|
|
8
|
+
return 'iOS';
|
|
9
|
+
} else if (os.toLowerCase().startsWith('android')) {
|
|
10
|
+
return 'Android';
|
|
11
|
+
}
|
|
12
|
+
return os;
|
|
13
|
+
}
|
|
14
|
+
browserRollUp(browserName, device) {
|
|
15
|
+
if (device) {
|
|
16
|
+
if (browserName !== null && browserName !== void 0 && browserName.toLowerCase().includes('chrome')) {
|
|
17
|
+
return 'chrome';
|
|
18
|
+
} else if (browserName !== null && browserName !== void 0 && browserName.toLowerCase().includes('iphone') || browserName !== null && browserName !== void 0 && browserName.toLowerCase().includes('ipad')) {
|
|
19
|
+
return 'safari';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return browserName;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Responses for browser version differ for devices and desktops from capabilities
|
|
26
|
+
// Differenences in selenium and appium responses causes inconsistency
|
|
27
|
+
// So to tackle for devices on UI we will show device names else browser versions
|
|
28
|
+
browserVersionOrDeviceNameRollup(browserVersion, deviceName, device) {
|
|
29
|
+
if (device) {
|
|
30
|
+
return deviceName;
|
|
31
|
+
}
|
|
32
|
+
return browserVersion === null || browserVersion === void 0 ? void 0 : browserVersion.split('.')[0];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -2,14 +2,16 @@ import utils from '@percy/sdk-utils';
|
|
|
2
2
|
import GenericProvider from './genericProvider.js';
|
|
3
3
|
import Cache from '../util/cache.js';
|
|
4
4
|
import Tile from '../util/tile.js';
|
|
5
|
+
import NormalizeData from '../metadata/normalizeData.js';
|
|
5
6
|
const log = utils.logger('webdriver-utils:automateProvider');
|
|
6
7
|
const {
|
|
7
8
|
TimeIt
|
|
8
9
|
} = utils;
|
|
9
10
|
export default class AutomateProvider extends GenericProvider {
|
|
10
|
-
constructor(sessionId, commandExecutorUrl, capabilities, sessionCapabilites, clientInfo, environmentInfo, options) {
|
|
11
|
-
super(sessionId, commandExecutorUrl, capabilities, sessionCapabilites, clientInfo, environmentInfo, options);
|
|
11
|
+
constructor(sessionId, commandExecutorUrl, capabilities, sessionCapabilites, clientInfo, environmentInfo, options, buildInfo) {
|
|
12
|
+
super(sessionId, commandExecutorUrl, capabilities, sessionCapabilites, clientInfo, environmentInfo, options, buildInfo);
|
|
12
13
|
this._markedPercy = false;
|
|
14
|
+
this.automateResults = null;
|
|
13
15
|
}
|
|
14
16
|
static supports(commandExecutorUrl) {
|
|
15
17
|
return commandExecutorUrl.includes(process.env.AA_DOMAIN || 'browserstack');
|
|
@@ -18,18 +20,30 @@ export default class AutomateProvider extends GenericProvider {
|
|
|
18
20
|
ignoreRegionXpaths = [],
|
|
19
21
|
ignoreRegionSelectors = [],
|
|
20
22
|
ignoreRegionElements = [],
|
|
21
|
-
customIgnoreRegions = []
|
|
23
|
+
customIgnoreRegions = [],
|
|
24
|
+
considerRegionXpaths = [],
|
|
25
|
+
considerRegionSelectors = [],
|
|
26
|
+
considerRegionElements = [],
|
|
27
|
+
customConsiderRegions = []
|
|
22
28
|
}) {
|
|
23
29
|
let response = null;
|
|
24
30
|
let error;
|
|
31
|
+
log.debug(`[${name}] : Preparing to capture screenshots on automate ...`);
|
|
25
32
|
try {
|
|
26
|
-
|
|
27
|
-
this.
|
|
33
|
+
log.debug(`[${name}] : Marking automate session as percy ...`);
|
|
34
|
+
const result = await this.percyScreenshotBegin(name);
|
|
35
|
+
this.automateResults = JSON.parse(result.value);
|
|
36
|
+
log.debug(`[${name}] : Fetching the debug url ...`);
|
|
37
|
+
this.setDebugUrl();
|
|
28
38
|
response = await super.screenshot(name, {
|
|
29
39
|
ignoreRegionXpaths,
|
|
30
40
|
ignoreRegionSelectors,
|
|
31
41
|
ignoreRegionElements,
|
|
32
|
-
customIgnoreRegions
|
|
42
|
+
customIgnoreRegions,
|
|
43
|
+
considerRegionXpaths,
|
|
44
|
+
considerRegionSelectors,
|
|
45
|
+
considerRegionElements,
|
|
46
|
+
customConsiderRegions
|
|
33
47
|
});
|
|
34
48
|
} catch (e) {
|
|
35
49
|
error = e;
|
|
@@ -45,15 +59,15 @@ export default class AutomateProvider extends GenericProvider {
|
|
|
45
59
|
try {
|
|
46
60
|
let result = await this.browserstackExecutor('percyScreenshot', {
|
|
47
61
|
name,
|
|
48
|
-
percyBuildId:
|
|
49
|
-
percyBuildUrl:
|
|
62
|
+
percyBuildId: this.buildInfo.id,
|
|
63
|
+
percyBuildUrl: this.buildInfo.url,
|
|
50
64
|
state: 'begin'
|
|
51
65
|
});
|
|
52
66
|
this._markedPercy = result.success;
|
|
53
67
|
return result;
|
|
54
68
|
} catch (e) {
|
|
55
|
-
log.debug(`[${name}] Could not mark Automate session as percy`);
|
|
56
|
-
log.error(`[${name}] error: ${e.toString()}`);
|
|
69
|
+
log.debug(`[${name}] : Could not mark Automate session as percy`);
|
|
70
|
+
log.error(`[${name}] : error: ${e.toString()}`);
|
|
57
71
|
return null;
|
|
58
72
|
}
|
|
59
73
|
});
|
|
@@ -69,21 +83,20 @@ export default class AutomateProvider extends GenericProvider {
|
|
|
69
83
|
state: 'end'
|
|
70
84
|
});
|
|
71
85
|
} catch (e) {
|
|
72
|
-
log.debug(`[${name}] Could not
|
|
86
|
+
log.debug(`[${name}] : Could not execute percyScreenshot command for Automate`);
|
|
87
|
+
log.error(e);
|
|
73
88
|
}
|
|
74
89
|
});
|
|
75
90
|
}
|
|
76
|
-
async getTiles(fullscreen) {
|
|
91
|
+
async getTiles(headerHeight, footerHeight, fullscreen) {
|
|
77
92
|
if (!this.driver) throw new Error('Driver is null, please initialize driver with createDriver().');
|
|
93
|
+
log.debug('Starting actual screenshotting phase');
|
|
78
94
|
const response = await TimeIt.run('percyScreenshot:screenshot', async () => {
|
|
79
95
|
return await this.browserstackExecutor('percyScreenshot', {
|
|
80
96
|
state: 'screenshot',
|
|
81
|
-
percyBuildId:
|
|
97
|
+
percyBuildId: this.buildInfo.id,
|
|
82
98
|
screenshotType: 'singlepage',
|
|
83
|
-
scaleFactor: await this.
|
|
84
|
-
script: 'return window.devicePixelRatio;',
|
|
85
|
-
args: []
|
|
86
|
-
}),
|
|
99
|
+
scaleFactor: await this.metaData.devicePixelRatio(),
|
|
87
100
|
options: this.options
|
|
88
101
|
});
|
|
89
102
|
});
|
|
@@ -93,12 +106,13 @@ export default class AutomateProvider extends GenericProvider {
|
|
|
93
106
|
}
|
|
94
107
|
const tiles = [];
|
|
95
108
|
const tileResponse = JSON.parse(responseValue.result);
|
|
109
|
+
log.debug('Tiles captured successfully');
|
|
96
110
|
for (let tileData of tileResponse.sha) {
|
|
97
111
|
tiles.push(new Tile({
|
|
98
112
|
statusBarHeight: 0,
|
|
99
113
|
navBarHeight: 0,
|
|
100
|
-
headerHeight
|
|
101
|
-
footerHeight
|
|
114
|
+
headerHeight,
|
|
115
|
+
footerHeight,
|
|
102
116
|
fullscreen,
|
|
103
117
|
sha: tileData.split('-')[0] // drop build id
|
|
104
118
|
}));
|
|
@@ -126,8 +140,43 @@ export default class AutomateProvider extends GenericProvider {
|
|
|
126
140
|
async setDebugUrl() {
|
|
127
141
|
if (!this.driver) throw new Error('Driver is null, please initialize driver with createDriver().');
|
|
128
142
|
this.debugUrl = await Cache.withCache(Cache.bstackSessionDetails, this.driver.sessionId, async () => {
|
|
129
|
-
|
|
130
|
-
return JSON.parse(sessionDetails.value).browser_url;
|
|
143
|
+
return `https://automate.browserstack.com/builds/${this.automateResults.buildHash}/sessions/${this.automateResults.sessionHash}`;
|
|
131
144
|
});
|
|
132
145
|
}
|
|
146
|
+
async getTag() {
|
|
147
|
+
var _automateCaps$os_vers, _ref;
|
|
148
|
+
if (!this.driver) throw new Error('Driver is null, please initialize driver with createDriver().');
|
|
149
|
+
if (!this.automateResults) throw new Error('Comparison tag details not available');
|
|
150
|
+
const automateCaps = this.automateResults.capabilities;
|
|
151
|
+
const normalizeTags = new NormalizeData();
|
|
152
|
+
let deviceName = this.automateResults.deviceName;
|
|
153
|
+
const osName = normalizeTags.osRollUp(automateCaps.os);
|
|
154
|
+
const osVersion = (_automateCaps$os_vers = automateCaps.os_version) === null || _automateCaps$os_vers === void 0 ? void 0 : _automateCaps$os_vers.split('.')[0];
|
|
155
|
+
const browserName = normalizeTags.browserRollUp(automateCaps.browserName, this.metaData.device());
|
|
156
|
+
const browserVersion = normalizeTags.browserVersionOrDeviceNameRollup(automateCaps.browserVersion, deviceName, this.metaData.device());
|
|
157
|
+
if (!this.metaData.device()) {
|
|
158
|
+
deviceName = `${osName}_${osVersion}_${browserName}_${browserVersion}`;
|
|
159
|
+
}
|
|
160
|
+
let {
|
|
161
|
+
width,
|
|
162
|
+
height
|
|
163
|
+
} = await this.metaData.windowSize();
|
|
164
|
+
const resolution = await this.metaData.screenResolution();
|
|
165
|
+
const orientation = (_ref = this.metaData.orientation() || automateCaps.deviceOrientation) === null || _ref === void 0 ? void 0 : _ref.toLowerCase();
|
|
166
|
+
|
|
167
|
+
// for android window size only constitutes of browser viewport, hence adding nav / status / url bar heights
|
|
168
|
+
[this.header, this.footer] = await this.getHeaderFooter(deviceName, osVersion, browserName);
|
|
169
|
+
height = this.metaData.device() && (osName === null || osName === void 0 ? void 0 : osName.toLowerCase()) === 'android' ? height + this.header + this.footer : height;
|
|
170
|
+
return {
|
|
171
|
+
name: deviceName,
|
|
172
|
+
osName,
|
|
173
|
+
osVersion,
|
|
174
|
+
width,
|
|
175
|
+
height,
|
|
176
|
+
orientation,
|
|
177
|
+
browserName,
|
|
178
|
+
browserVersion,
|
|
179
|
+
resolution
|
|
180
|
+
};
|
|
181
|
+
}
|
|
133
182
|
}
|
|
@@ -2,12 +2,17 @@ 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
|
+
const {
|
|
7
|
+
request
|
|
8
|
+
} = utils;
|
|
9
|
+
const DEVICES_CONFIG_URL = 'https://storage.googleapis.com/percy-utils/devices.json';
|
|
5
10
|
const log = utils.logger('webdriver-utils:genericProvider');
|
|
6
11
|
export default class GenericProvider {
|
|
7
12
|
clientInfo = new Set();
|
|
8
13
|
environmentInfo = new Set();
|
|
9
14
|
options = {};
|
|
10
|
-
constructor(sessionId, commandExecutorUrl, capabilities, sessionCapabilites, clientInfo, environmentInfo, options) {
|
|
15
|
+
constructor(sessionId, commandExecutorUrl, capabilities, sessionCapabilites, clientInfo, environmentInfo, options, buildInfo) {
|
|
11
16
|
this.sessionId = sessionId;
|
|
12
17
|
this.commandExecutorUrl = commandExecutorUrl;
|
|
13
18
|
this.capabilities = capabilities;
|
|
@@ -15,13 +20,46 @@ export default class GenericProvider {
|
|
|
15
20
|
this.addClientInfo(clientInfo);
|
|
16
21
|
this.addEnvironmentInfo(environmentInfo);
|
|
17
22
|
this.options = options;
|
|
23
|
+
this.buildInfo = buildInfo;
|
|
18
24
|
this.driver = null;
|
|
19
25
|
this.metaData = null;
|
|
20
26
|
this.debugUrl = null;
|
|
27
|
+
this.header = 0;
|
|
28
|
+
this.footer = 0;
|
|
29
|
+
}
|
|
30
|
+
addDefaultOptions() {
|
|
31
|
+
this.options.freezeAnimation = this.options.freezeAnimation || false;
|
|
32
|
+
}
|
|
33
|
+
defaultPercyCSS() {
|
|
34
|
+
return `*, *::before, *::after {
|
|
35
|
+
-moz-transition: none !important;
|
|
36
|
+
transition: none !important;
|
|
37
|
+
-moz-animation: none !important;
|
|
38
|
+
animation: none !important;
|
|
39
|
+
animation-duration: 0 !important;
|
|
40
|
+
caret-color: transparent !important;
|
|
41
|
+
content-visibility: visible !important;
|
|
42
|
+
}
|
|
43
|
+
html{
|
|
44
|
+
scrollbar-width: auto !important;
|
|
45
|
+
}
|
|
46
|
+
svg {
|
|
47
|
+
shape-rendering: geometricPrecision !important;
|
|
48
|
+
}
|
|
49
|
+
scrollbar, scrollcorner, scrollbar thumb, scrollbar scrollbarbutton {
|
|
50
|
+
pointer-events: none !important;
|
|
51
|
+
-moz-appearance: none !important;
|
|
52
|
+
display: none !important;
|
|
53
|
+
}
|
|
54
|
+
video::-webkit-media-controls {
|
|
55
|
+
display: none !important;
|
|
56
|
+
}`;
|
|
21
57
|
}
|
|
22
58
|
async createDriver() {
|
|
23
|
-
this.driver = new Driver(this.sessionId, this.commandExecutorUrl);
|
|
59
|
+
this.driver = new Driver(this.sessionId, this.commandExecutorUrl, this.capabilities);
|
|
60
|
+
log.debug(`Passed capabilities -> ${JSON.stringify(this.capabilities)}`);
|
|
24
61
|
const caps = await this.driver.getCapabilites();
|
|
62
|
+
log.debug(`Fetched capabilities -> ${JSON.stringify(caps)}`);
|
|
25
63
|
this.metaData = await MetaDataResolver.resolve(this.driver, caps, this.capabilities);
|
|
26
64
|
}
|
|
27
65
|
static supports(_commandExecutorUrl) {
|
|
@@ -59,26 +97,39 @@ export default class GenericProvider {
|
|
|
59
97
|
ignoreRegionXpaths = [],
|
|
60
98
|
ignoreRegionSelectors = [],
|
|
61
99
|
ignoreRegionElements = [],
|
|
62
|
-
customIgnoreRegions = []
|
|
100
|
+
customIgnoreRegions = [],
|
|
101
|
+
considerRegionXpaths = [],
|
|
102
|
+
considerRegionSelectors = [],
|
|
103
|
+
considerRegionElements = [],
|
|
104
|
+
customConsiderRegions = []
|
|
63
105
|
}) {
|
|
64
106
|
let fullscreen = false;
|
|
65
|
-
|
|
107
|
+
this.addDefaultOptions();
|
|
108
|
+
const percyCSS = (this.defaultPercyCSS() + (this.options.percyCSS || '')).split('\n').join('');
|
|
109
|
+
log.debug(`[${name}] : Applying the percyCSS - ${this.options.percyCSS}`);
|
|
66
110
|
await this.addPercyCSS(percyCSS);
|
|
111
|
+
log.debug('Fetching comparisong tag ...');
|
|
67
112
|
const tag = await this.getTag();
|
|
68
|
-
|
|
69
|
-
const
|
|
113
|
+
log.debug(`[${name}] : Tag ${JSON.stringify(tag)}`);
|
|
114
|
+
const tiles = await this.getTiles(this.header, this.footer, fullscreen);
|
|
115
|
+
log.debug(`[${name}] : Tiles ${JSON.stringify(tiles)}`);
|
|
116
|
+
const ignoreRegions = await this.findRegions(ignoreRegionXpaths, ignoreRegionSelectors, ignoreRegionElements, customIgnoreRegions);
|
|
117
|
+
const considerRegions = await this.findRegions(considerRegionXpaths, considerRegionSelectors, considerRegionElements, customConsiderRegions);
|
|
70
118
|
await this.setDebugUrl();
|
|
119
|
+
log.debug(`[${name}] : Debug url ${this.debugUrl}`);
|
|
71
120
|
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
121
|
return {
|
|
76
122
|
name,
|
|
77
123
|
tag,
|
|
78
124
|
tiles: tiles.tiles,
|
|
79
125
|
// TODO: Fetch this one for bs automate, check appium sdk
|
|
80
126
|
externalDebugUrl: this.debugUrl,
|
|
81
|
-
ignoredElementsData:
|
|
127
|
+
ignoredElementsData: {
|
|
128
|
+
ignoreElementsData: ignoreRegions
|
|
129
|
+
},
|
|
130
|
+
consideredElementsData: {
|
|
131
|
+
considerElementsData: considerRegions
|
|
132
|
+
},
|
|
82
133
|
environmentInfo: [...this.environmentInfo].join('; '),
|
|
83
134
|
clientInfo: [...this.clientInfo].join(' '),
|
|
84
135
|
domInfoSha: tiles.domInfoSha
|
|
@@ -90,31 +141,34 @@ export default class GenericProvider {
|
|
|
90
141
|
// execute script and return dom content
|
|
91
142
|
return 'dummyValue';
|
|
92
143
|
}
|
|
93
|
-
async getTiles(fullscreen) {
|
|
144
|
+
async getTiles(headerHeight, footerHeight, fullscreen) {
|
|
94
145
|
if (!this.driver) throw new Error('Driver is null, please initialize driver with createDriver().');
|
|
95
146
|
const base64content = await this.driver.takeScreenshot();
|
|
147
|
+
log.debug('Tiles captured successfully');
|
|
96
148
|
return {
|
|
97
149
|
tiles: [new Tile({
|
|
98
150
|
content: base64content,
|
|
99
|
-
// TODO: Need to add method to fetch these attr
|
|
100
151
|
statusBarHeight: 0,
|
|
101
152
|
navBarHeight: 0,
|
|
102
|
-
headerHeight
|
|
103
|
-
footerHeight
|
|
153
|
+
headerHeight,
|
|
154
|
+
footerHeight,
|
|
104
155
|
fullscreen
|
|
105
156
|
})],
|
|
106
|
-
// TODO: Add Generic support sha for contextual diff
|
|
157
|
+
// TODO: Add Generic support sha for contextual diff for non-automate
|
|
107
158
|
domInfoSha: await this.getDomContent()
|
|
108
159
|
};
|
|
109
160
|
}
|
|
110
161
|
async getTag() {
|
|
111
162
|
if (!this.driver) throw new Error('Driver is null, please initialize driver with createDriver().');
|
|
112
|
-
|
|
163
|
+
let {
|
|
113
164
|
width,
|
|
114
165
|
height
|
|
115
166
|
} = await this.metaData.windowSize();
|
|
116
167
|
const resolution = await this.metaData.screenResolution();
|
|
117
168
|
const orientation = this.metaData.orientation();
|
|
169
|
+
[this.header, this.footer] = await this.getHeaderFooter();
|
|
170
|
+
// for android window size only constitutes of browser viewport, hence adding nav / status / url bar heights
|
|
171
|
+
height = this.metaData.osName() === 'android' ? height + this.header + this.footer : height;
|
|
118
172
|
return {
|
|
119
173
|
name: this.metaData.deviceName(),
|
|
120
174
|
osName: this.metaData.osName(),
|
|
@@ -128,20 +182,18 @@ export default class GenericProvider {
|
|
|
128
182
|
};
|
|
129
183
|
}
|
|
130
184
|
|
|
131
|
-
// TODO: Add Debugging Url
|
|
185
|
+
// TODO: Add Debugging Url for non-automate
|
|
132
186
|
async setDebugUrl() {
|
|
133
187
|
this.debugUrl = 'https://localhost/v1';
|
|
134
188
|
}
|
|
135
|
-
async
|
|
136
|
-
const
|
|
137
|
-
const
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
return
|
|
141
|
-
ignoreElementsData: [...ignoreElementXpaths, ...ignoreElementSelectors, ...ignoreElements, ...ignoreElementCustom]
|
|
142
|
-
};
|
|
189
|
+
async findRegions(xpaths, selectors, elements, customLocations) {
|
|
190
|
+
const xpathRegions = await this.getSeleniumRegionsBy('xpath', xpaths);
|
|
191
|
+
const selectorRegions = await this.getSeleniumRegionsBy('css selector', selectors);
|
|
192
|
+
const elementRegions = await this.getSeleniumRegionsByElement(elements);
|
|
193
|
+
const customRegions = await this.getSeleniumRegionsByLocation(customLocations);
|
|
194
|
+
return [...xpathRegions, ...selectorRegions, ...elementRegions, ...customRegions];
|
|
143
195
|
}
|
|
144
|
-
async
|
|
196
|
+
async getRegionObject(selector, elementId) {
|
|
145
197
|
const scaleFactor = parseInt(await this.metaData.devicePixelRatio());
|
|
146
198
|
const rect = await this.driver.rect(elementId);
|
|
147
199
|
const location = {
|
|
@@ -164,37 +216,37 @@ export default class GenericProvider {
|
|
|
164
216
|
};
|
|
165
217
|
return jsonObject;
|
|
166
218
|
}
|
|
167
|
-
async
|
|
168
|
-
const
|
|
219
|
+
async getSeleniumRegionsBy(findBy, elements) {
|
|
220
|
+
const regionsArray = [];
|
|
169
221
|
for (const idx in elements) {
|
|
170
222
|
try {
|
|
171
223
|
const element = await this.driver.findElement(findBy, elements[idx]);
|
|
172
224
|
const selector = `${findBy}: ${elements[idx]}`;
|
|
173
|
-
const
|
|
174
|
-
|
|
225
|
+
const region = await this.getRegionObject(selector, element[Object.keys(element)[0]]);
|
|
226
|
+
regionsArray.push(region);
|
|
175
227
|
} catch (e) {
|
|
176
228
|
log.warn(`Selenium Element with ${findBy}: ${elements[idx]} not found. Ignoring this ${findBy}.`);
|
|
177
229
|
log.error(e.toString());
|
|
178
230
|
}
|
|
179
231
|
}
|
|
180
|
-
return
|
|
232
|
+
return regionsArray;
|
|
181
233
|
}
|
|
182
|
-
async
|
|
183
|
-
const
|
|
234
|
+
async getSeleniumRegionsByElement(elements) {
|
|
235
|
+
const regionsArray = [];
|
|
184
236
|
for (let index = 0; index < elements.length; index++) {
|
|
185
237
|
try {
|
|
186
238
|
const selector = `element: ${index}`;
|
|
187
|
-
const
|
|
188
|
-
|
|
239
|
+
const region = await this.getRegionObject(selector, elements[index]);
|
|
240
|
+
regionsArray.push(region);
|
|
189
241
|
} catch (e) {
|
|
190
242
|
log.warn(`Correct Web Element not passed at index ${index}.`);
|
|
191
243
|
log.debug(e.toString());
|
|
192
244
|
}
|
|
193
245
|
}
|
|
194
|
-
return
|
|
246
|
+
return regionsArray;
|
|
195
247
|
}
|
|
196
|
-
async
|
|
197
|
-
const
|
|
248
|
+
async getSeleniumRegionsByLocation(customLocations) {
|
|
249
|
+
const elementsArray = [];
|
|
198
250
|
const {
|
|
199
251
|
width,
|
|
200
252
|
height
|
|
@@ -203,8 +255,8 @@ export default class GenericProvider {
|
|
|
203
255
|
const customLocation = customLocations[index];
|
|
204
256
|
const invalid = customLocation.top >= height || customLocation.bottom > height || customLocation.left >= width || customLocation.right > width;
|
|
205
257
|
if (!invalid) {
|
|
206
|
-
const selector = `custom
|
|
207
|
-
const
|
|
258
|
+
const selector = `custom region ${index}`;
|
|
259
|
+
const region = {
|
|
208
260
|
selector,
|
|
209
261
|
coOrdinates: {
|
|
210
262
|
top: customLocation.top,
|
|
@@ -213,11 +265,19 @@ export default class GenericProvider {
|
|
|
213
265
|
right: customLocation.right
|
|
214
266
|
}
|
|
215
267
|
};
|
|
216
|
-
|
|
268
|
+
elementsArray.push(region);
|
|
217
269
|
} else {
|
|
218
270
|
log.warn(`Values passed in custom ignored region at index: ${index} is not valid`);
|
|
219
271
|
}
|
|
220
272
|
}
|
|
221
|
-
return
|
|
273
|
+
return elementsArray;
|
|
274
|
+
}
|
|
275
|
+
async getHeaderFooter(deviceName, osVersion, browserName) {
|
|
276
|
+
// passing 0 as key, since across different pages and tests, this config will remain same
|
|
277
|
+
const devicesConfig = await Cache.withCache(Cache.devicesConfig, 0, async () => {
|
|
278
|
+
return (await request(DEVICES_CONFIG_URL)).body;
|
|
279
|
+
});
|
|
280
|
+
let deviceKey = `${deviceName}-${osVersion}`;
|
|
281
|
+
return devicesConfig[deviceKey] ? devicesConfig[deviceKey][browserName] ? [devicesConfig[deviceKey][browserName].header, devicesConfig[deviceKey][browserName].footer] : [0, 0] : [0, 0];
|
|
222
282
|
}
|
|
223
283
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import GenericProvider from './genericProvider.js';
|
|
2
2
|
import AutomateProvider from './automateProvider.js';
|
|
3
3
|
export default class ProviderResolver {
|
|
4
|
-
static resolve(sessionId, commandExecutorUrl, capabilities, sessionCapabilities, clientInfo, environmentInfo, options) {
|
|
4
|
+
static resolve(sessionId, commandExecutorUrl, capabilities, sessionCapabilities, clientInfo, environmentInfo, options, buildInfo) {
|
|
5
5
|
// We can safely do [0] because GenericProvider is catch all
|
|
6
6
|
const Klass = [AutomateProvider, GenericProvider].filter(x => x.supports(commandExecutorUrl))[0];
|
|
7
|
-
return new Klass(sessionId, commandExecutorUrl, capabilities, sessionCapabilities, clientInfo, environmentInfo, options);
|
|
7
|
+
return new Klass(sessionId, commandExecutorUrl, capabilities, sessionCapabilities, clientInfo, environmentInfo, options, buildInfo);
|
|
8
8
|
}
|
|
9
9
|
}
|
package/dist/util/cache.js
CHANGED
|
@@ -9,6 +9,9 @@ export default class Cache {
|
|
|
9
9
|
static caps = 'caps';
|
|
10
10
|
static bstackSessionDetails = 'bstackSessionDetails';
|
|
11
11
|
static systemBars = 'systemBars';
|
|
12
|
+
static devicesConfig = 'devicesConfig';
|
|
13
|
+
static dpr = 'dpr';
|
|
14
|
+
static resolution = 'resolution';
|
|
12
15
|
|
|
13
16
|
// maintainance
|
|
14
17
|
static lastTime = Date.now();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@percy/webdriver-utils",
|
|
3
|
-
"version": "1.27.0-
|
|
3
|
+
"version": "1.27.0-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": "
|
|
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.0-
|
|
33
|
-
"@percy/sdk-utils": "1.27.0-
|
|
32
|
+
"@percy/config": "1.27.0-beta.1",
|
|
33
|
+
"@percy/sdk-utils": "1.27.0-beta.1"
|
|
34
34
|
},
|
|
35
|
-
"gitHead": "
|
|
35
|
+
"gitHead": "40cdf9c38613ccaf5e3707cd2cd2d2778ffbd5dd"
|
|
36
36
|
}
|
package/dist/util/timing.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
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
|
-
;
|