@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 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
+ ;
@@ -0,0 +1,7 @@
1
+ function Undefined(obj) {
2
+ return obj === undefined;
3
+ }
4
+ export { Undefined };
5
+
6
+ // export the namespace by default
7
+ export * as default from './validations.js';
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
+ }