@wdio/browserstack-service 8.27.2 → 8.28.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/build/Percy/Percy-Handler.d.ts +32 -0
- package/build/Percy/Percy-Handler.d.ts.map +1 -0
- package/build/Percy/Percy-Handler.js +156 -0
- package/build/Percy/Percy.d.ts +15 -0
- package/build/Percy/Percy.d.ts.map +1 -0
- package/build/Percy/Percy.js +123 -0
- package/build/Percy/PercyBinary.d.ts +10 -0
- package/build/Percy/PercyBinary.d.ts.map +1 -0
- package/build/Percy/PercyBinary.js +149 -0
- package/build/Percy/PercyCaptureMap.d.ts +9 -0
- package/build/Percy/PercyCaptureMap.d.ts.map +1 -0
- package/build/Percy/PercyCaptureMap.js +35 -0
- package/build/Percy/PercyHelper.d.ts +8 -0
- package/build/Percy/PercyHelper.d.ts.map +1 -0
- package/build/Percy/PercyHelper.js +67 -0
- package/build/Percy/PercyLogger.d.ts +15 -0
- package/build/Percy/PercyLogger.d.ts.map +1 -0
- package/build/Percy/PercyLogger.js +67 -0
- package/build/Percy/PercySDK.d.ts +4 -0
- package/build/Percy/PercySDK.d.ts.map +1 -0
- package/build/Percy/PercySDK.js +39 -0
- package/build/bstackLogger.d.ts +1 -1
- package/build/bstackLogger.d.ts.map +1 -1
- package/build/constants.d.ts +3 -0
- package/build/constants.d.ts.map +1 -1
- package/build/constants.js +11 -0
- package/build/index.d.ts +2 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -0
- package/build/insights-handler.d.ts +0 -1
- package/build/insights-handler.d.ts.map +1 -1
- package/build/insights-handler.js +2 -16
- package/build/launcher.d.ts +7 -2
- package/build/launcher.d.ts.map +1 -1
- package/build/launcher.js +70 -1
- package/build/reporter.d.ts +2 -0
- package/build/reporter.d.ts.map +1 -1
- package/build/reporter.js +7 -1
- package/build/service.d.ts +2 -0
- package/build/service.d.ts.map +1 -1
- package/build/service.js +37 -8
- package/build/types.d.ts +16 -0
- package/build/types.d.ts.map +1 -1
- package/build/util.d.ts +3 -0
- package/build/util.d.ts.map +1 -1
- package/build/util.js +33 -1
- package/package.json +11 -8
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Capabilities } from '@wdio/types';
|
|
2
|
+
import type { BeforeCommandArgs, AfterCommandArgs } from '@wdio/reporter';
|
|
3
|
+
declare class _PercyHandler {
|
|
4
|
+
private _percyAutoCaptureMode;
|
|
5
|
+
private _browser;
|
|
6
|
+
private _capabilities;
|
|
7
|
+
private _isAppAutomate?;
|
|
8
|
+
private _framework?;
|
|
9
|
+
private _testMetadata;
|
|
10
|
+
private _sessionName?;
|
|
11
|
+
private _isPercyCleanupProcessingUnderway?;
|
|
12
|
+
private _percyScreenshotCounter;
|
|
13
|
+
private _percyDeferredScreenshots;
|
|
14
|
+
private _percyScreenshotInterval;
|
|
15
|
+
private _percyCaptureMap?;
|
|
16
|
+
constructor(_percyAutoCaptureMode: string | undefined, _browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser, _capabilities: Capabilities.RemoteCapability, _isAppAutomate?: boolean | undefined, _framework?: string | undefined);
|
|
17
|
+
_setSessionName(name: string): void;
|
|
18
|
+
teardown(): Promise<void>;
|
|
19
|
+
percyAutoCapture(eventName: string | null, sessionName: string | null): Promise<void>;
|
|
20
|
+
before(): Promise<void>;
|
|
21
|
+
deferCapture(sessionName: string, eventName: string | null): void;
|
|
22
|
+
isDOMChangingCommand(args: BeforeCommandArgs): boolean;
|
|
23
|
+
cleanupDeferredScreenshots(): Promise<void>;
|
|
24
|
+
browserBeforeCommand(args: BeforeCommandArgs): Promise<void>;
|
|
25
|
+
browserAfterCommand(args: BeforeCommandArgs & AfterCommandArgs): Promise<void>;
|
|
26
|
+
afterTest(): Promise<void>;
|
|
27
|
+
afterScenario(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
declare const PercyHandler: typeof _PercyHandler;
|
|
30
|
+
type PercyHandler = _PercyHandler;
|
|
31
|
+
export default PercyHandler;
|
|
32
|
+
//# sourceMappingURL=Percy-Handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Percy-Handler.d.ts","sourceRoot":"","sources":["../../src/Percy/Percy-Handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAazE,cAAM,aAAa;IAUX,OAAO,CAAC,qBAAqB;IAC7B,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,cAAc,CAAC;IACvB,OAAO,CAAC,UAAU,CAAC;IAbvB,OAAO,CAAC,aAAa,CAA6B;IAClD,OAAO,CAAC,YAAY,CAAC,CAAQ;IAC7B,OAAO,CAAC,iCAAiC,CAAC,CAAiB;IAC3D,OAAO,CAAC,uBAAuB,CAAS;IACxC,OAAO,CAAC,yBAAyB,CAAU;IAC3C,OAAO,CAAC,wBAAwB,CAAY;IAC5C,OAAO,CAAC,gBAAgB,CAAC,CAAiB;gBAG9B,qBAAqB,EAAE,MAAM,GAAG,SAAS,EACzC,QAAQ,EAAE,WAAW,CAAC,OAAO,GAAG,WAAW,CAAC,kBAAkB,EAC9D,aAAa,EAAE,YAAY,CAAC,gBAAgB,EAC5C,cAAc,CAAC,qBAAS,EACxB,UAAU,CAAC,oBAAQ;IAO/B,eAAe,CAAC,IAAI,EAAE,MAAM;IAItB,QAAQ;IAUR,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI;IAmBrE,MAAM;IAIZ,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAM1D,oBAAoB,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO;IA+BhD,0BAA0B;IAS1B,oBAAoB,CAAE,IAAI,EAAE,iBAAiB;IAoB7C,mBAAmB,CAAE,IAAI,EAAE,iBAAiB,GAAG,gBAAgB;IA0B/D,SAAS;IAMT,aAAa;CAKtB;AAGD,QAAA,MAAM,YAAY,EAAE,OAAO,aAAoD,CAAA;AAC/E,KAAK,YAAY,GAAG,aAAa,CAAA;AAEjC,eAAe,YAAY,CAAA"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { o11yClassErrorHandler, sleep } from '../util.js';
|
|
2
|
+
import PercyCaptureMap from './PercyCaptureMap.js';
|
|
3
|
+
import * as PercySDK from './PercySDK.js';
|
|
4
|
+
import { PercyLogger } from './PercyLogger.js';
|
|
5
|
+
import { PERCY_DOM_CHANGING_COMMANDS_ENDPOINTS, CAPTURE_MODES } from '../constants.js';
|
|
6
|
+
class _PercyHandler {
|
|
7
|
+
_percyAutoCaptureMode;
|
|
8
|
+
_browser;
|
|
9
|
+
_capabilities;
|
|
10
|
+
_isAppAutomate;
|
|
11
|
+
_framework;
|
|
12
|
+
_testMetadata = {};
|
|
13
|
+
_sessionName;
|
|
14
|
+
_isPercyCleanupProcessingUnderway = false;
|
|
15
|
+
_percyScreenshotCounter = 0;
|
|
16
|
+
_percyDeferredScreenshots = [];
|
|
17
|
+
_percyScreenshotInterval = null;
|
|
18
|
+
_percyCaptureMap;
|
|
19
|
+
constructor(_percyAutoCaptureMode, _browser, _capabilities, _isAppAutomate, _framework) {
|
|
20
|
+
this._percyAutoCaptureMode = _percyAutoCaptureMode;
|
|
21
|
+
this._browser = _browser;
|
|
22
|
+
this._capabilities = _capabilities;
|
|
23
|
+
this._isAppAutomate = _isAppAutomate;
|
|
24
|
+
this._framework = _framework;
|
|
25
|
+
if (!_percyAutoCaptureMode || !CAPTURE_MODES.includes(_percyAutoCaptureMode)) {
|
|
26
|
+
this._percyAutoCaptureMode = 'auto';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
_setSessionName(name) {
|
|
30
|
+
this._sessionName = name;
|
|
31
|
+
}
|
|
32
|
+
async teardown() {
|
|
33
|
+
await new Promise((resolve) => {
|
|
34
|
+
setInterval(() => {
|
|
35
|
+
if (this._percyScreenshotCounter === 0) {
|
|
36
|
+
resolve();
|
|
37
|
+
}
|
|
38
|
+
}, 1000);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
async percyAutoCapture(eventName, sessionName) {
|
|
42
|
+
try {
|
|
43
|
+
if (eventName) {
|
|
44
|
+
if (!sessionName) {
|
|
45
|
+
/* Service doesn't wait for handling of browser commands so the below counter is used in teardown method to delay service exit */
|
|
46
|
+
this._percyScreenshotCounter += 1;
|
|
47
|
+
}
|
|
48
|
+
this._percyCaptureMap?.increment(sessionName ? sessionName : this._sessionName, eventName);
|
|
49
|
+
await (this._isAppAutomate ? PercySDK.screenshotApp(this._percyCaptureMap?.getName(sessionName ? sessionName : this._sessionName, eventName)) : await PercySDK.screenshot(this._browser, this._percyCaptureMap?.getName(sessionName ? sessionName : this._sessionName, eventName)));
|
|
50
|
+
this._percyScreenshotCounter -= 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
this._percyScreenshotCounter -= 1;
|
|
55
|
+
this._percyCaptureMap?.decrement(sessionName ? sessionName : this._sessionName, eventName);
|
|
56
|
+
PercyLogger.error(`Error while trying to auto capture Percy screenshot ${err}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async before() {
|
|
60
|
+
this._percyCaptureMap = new PercyCaptureMap();
|
|
61
|
+
}
|
|
62
|
+
deferCapture(sessionName, eventName) {
|
|
63
|
+
/* Service doesn't wait for handling of browser commands so the below counter is used in teardown method to delay service exit */
|
|
64
|
+
this._percyScreenshotCounter += 1;
|
|
65
|
+
this._percyDeferredScreenshots.push({ sessionName, eventName });
|
|
66
|
+
}
|
|
67
|
+
isDOMChangingCommand(args) {
|
|
68
|
+
/*
|
|
69
|
+
Percy screenshots which are to be taken on events such as send keys, element click & screenshot are deferred until
|
|
70
|
+
another DOM changing command is seen such that any DOM processing post the previous command is completed
|
|
71
|
+
*/
|
|
72
|
+
return (typeof args.method === 'string' && typeof args.endpoint === 'string' &&
|
|
73
|
+
((args.method === 'POST' &&
|
|
74
|
+
(PERCY_DOM_CHANGING_COMMANDS_ENDPOINTS.includes(args.endpoint) ||
|
|
75
|
+
(
|
|
76
|
+
/* click / clear element */
|
|
77
|
+
args.endpoint.includes('/session/:sessionId/element') &&
|
|
78
|
+
(args.endpoint.includes('click') ||
|
|
79
|
+
args.endpoint.includes('clear'))) ||
|
|
80
|
+
/* execute script sync / async */
|
|
81
|
+
(args.endpoint.includes('/session/:sessionId/execute') && args.body?.script) ||
|
|
82
|
+
/* Touch action for Appium */
|
|
83
|
+
(args.endpoint.includes('/session/:sessionId/touch')))) ||
|
|
84
|
+
(args.method === 'DELETE' && args.endpoint === '/session/:sessionId')));
|
|
85
|
+
}
|
|
86
|
+
async cleanupDeferredScreenshots() {
|
|
87
|
+
this._isPercyCleanupProcessingUnderway = true;
|
|
88
|
+
for (const entry of this._percyDeferredScreenshots) {
|
|
89
|
+
await this.percyAutoCapture(entry.eventName, entry.sessionName);
|
|
90
|
+
}
|
|
91
|
+
this._percyDeferredScreenshots = [];
|
|
92
|
+
this._isPercyCleanupProcessingUnderway = false;
|
|
93
|
+
}
|
|
94
|
+
async browserBeforeCommand(args) {
|
|
95
|
+
try {
|
|
96
|
+
if (!this.isDOMChangingCommand(args)) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
do {
|
|
100
|
+
await sleep(1000);
|
|
101
|
+
} while (this._percyScreenshotInterval);
|
|
102
|
+
this._percyScreenshotInterval = setInterval(async () => {
|
|
103
|
+
if (!this._isPercyCleanupProcessingUnderway) {
|
|
104
|
+
clearInterval(this._percyScreenshotInterval);
|
|
105
|
+
await this.cleanupDeferredScreenshots();
|
|
106
|
+
this._percyScreenshotInterval = null;
|
|
107
|
+
}
|
|
108
|
+
}, 1000);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
PercyLogger.error(`Error while trying to cleanup deferred screenshots ${err}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async browserAfterCommand(args) {
|
|
115
|
+
try {
|
|
116
|
+
if (!args.endpoint || !this._percyAutoCaptureMode) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
let eventName = null;
|
|
120
|
+
const endpoint = args.endpoint;
|
|
121
|
+
if (endpoint.includes('click') && ['click', 'auto'].includes(this._percyAutoCaptureMode)) {
|
|
122
|
+
eventName = 'click';
|
|
123
|
+
}
|
|
124
|
+
else if (endpoint.includes('screenshot') && ['screenshot', 'auto'].includes(this._percyAutoCaptureMode)) {
|
|
125
|
+
eventName = 'screenshot';
|
|
126
|
+
}
|
|
127
|
+
else if (endpoint.includes('actions') && ['auto'].includes(this._percyAutoCaptureMode)) {
|
|
128
|
+
if (args.body && args.body.actions && Array.isArray(args.body.actions) && args.body.actions.length && args.body.actions[0].type === 'key') {
|
|
129
|
+
eventName = 'keys';
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else if (endpoint.includes('/session/:sessionId/element') && endpoint.includes('value') && ['auto'].includes(this._percyAutoCaptureMode)) {
|
|
133
|
+
eventName = 'keys';
|
|
134
|
+
}
|
|
135
|
+
if (eventName) {
|
|
136
|
+
this.deferCapture(this._sessionName, eventName);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
PercyLogger.error(`Error while trying to calculate auto capture parameters ${err}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async afterTest() {
|
|
144
|
+
if (this._percyAutoCaptureMode && this._percyAutoCaptureMode === 'testcase') {
|
|
145
|
+
await this.percyAutoCapture('testcase', null);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async afterScenario() {
|
|
149
|
+
if (this._percyAutoCaptureMode && this._percyAutoCaptureMode === 'testcase') {
|
|
150
|
+
await this.percyAutoCapture('testcase', null);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// https://github.com/microsoft/TypeScript/issues/6543
|
|
155
|
+
const PercyHandler = o11yClassErrorHandler(_PercyHandler);
|
|
156
|
+
export default PercyHandler;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { BrowserstackConfig, UserConfig } from '../types.js';
|
|
2
|
+
import type { Options } from '@wdio/types';
|
|
3
|
+
declare class Percy {
|
|
4
|
+
#private;
|
|
5
|
+
isProcessRunning: boolean;
|
|
6
|
+
constructor(options: BrowserstackConfig & Options.Testrunner, config: Options.Testrunner, bsConfig: UserConfig);
|
|
7
|
+
healthcheck(): Promise<boolean | undefined>;
|
|
8
|
+
start(): Promise<boolean>;
|
|
9
|
+
stop(): Promise<unknown>;
|
|
10
|
+
isRunning(): boolean;
|
|
11
|
+
fetchPercyToken(): Promise<any>;
|
|
12
|
+
createPercyConfig(): Promise<unknown>;
|
|
13
|
+
}
|
|
14
|
+
export default Percy;
|
|
15
|
+
//# sourceMappingURL=Percy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Percy.d.ts","sourceRoot":"","sources":["../../src/Percy/Percy.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACjE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAI1C,cAAM,KAAK;;IAWP,gBAAgB,UAAQ;gBAEZ,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,UAAU,EAAE,QAAQ,EAAE,UAAU;IAexG,WAAW;IAWX,KAAK;IA4CL,IAAI;IAWV,SAAS;IAIH,eAAe;IAsBf,iBAAiB;CA8B1B;AAED,eAAe,KAAK,CAAA"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { nodeRequest, getBrowserStackUser, getBrowserStackKey, sleep } from '../util.js';
|
|
6
|
+
import { PercyLogger } from './PercyLogger.js';
|
|
7
|
+
import PercyBinary from './PercyBinary.js';
|
|
8
|
+
const logDir = 'logs';
|
|
9
|
+
class Percy {
|
|
10
|
+
#logfile = path.join(logDir, 'percy.log');
|
|
11
|
+
#address = process.env.PERCY_SERVER_ADDRESS || 'http://127.0.0.1:5338';
|
|
12
|
+
#binaryPath = null;
|
|
13
|
+
#options;
|
|
14
|
+
#config;
|
|
15
|
+
#proc = null;
|
|
16
|
+
#isApp;
|
|
17
|
+
#projectName = undefined;
|
|
18
|
+
isProcessRunning = false;
|
|
19
|
+
constructor(options, config, bsConfig) {
|
|
20
|
+
this.#options = options;
|
|
21
|
+
this.#config = config;
|
|
22
|
+
this.#isApp = Boolean(options.app);
|
|
23
|
+
this.#projectName = bsConfig.projectName;
|
|
24
|
+
}
|
|
25
|
+
async #getBinaryPath() {
|
|
26
|
+
if (!this.#binaryPath) {
|
|
27
|
+
const pb = new PercyBinary();
|
|
28
|
+
this.#binaryPath = await pb.getBinaryPath(this.#config);
|
|
29
|
+
}
|
|
30
|
+
return this.#binaryPath;
|
|
31
|
+
}
|
|
32
|
+
async healthcheck() {
|
|
33
|
+
try {
|
|
34
|
+
const resp = await nodeRequest('GET', 'percy/healthcheck', null, this.#address);
|
|
35
|
+
if (resp) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async start() {
|
|
44
|
+
const binaryPath = await this.#getBinaryPath();
|
|
45
|
+
const logStream = fs.createWriteStream(this.#logfile, { flags: 'a' });
|
|
46
|
+
const token = await this.fetchPercyToken();
|
|
47
|
+
const configPath = await this.createPercyConfig();
|
|
48
|
+
if (!token) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const commandArgs = [`${this.#isApp ? 'app:exec' : 'exec'}:start`];
|
|
52
|
+
if (configPath) {
|
|
53
|
+
commandArgs.push('-c', configPath);
|
|
54
|
+
}
|
|
55
|
+
this.#proc = spawn(binaryPath, commandArgs, { env: { ...process.env, PERCY_TOKEN: token } });
|
|
56
|
+
this.#proc.stdout.pipe(logStream);
|
|
57
|
+
this.#proc.stderr.pipe(logStream);
|
|
58
|
+
this.isProcessRunning = true;
|
|
59
|
+
const that = this;
|
|
60
|
+
this.#proc.on('close', function () {
|
|
61
|
+
that.isProcessRunning = false;
|
|
62
|
+
});
|
|
63
|
+
do {
|
|
64
|
+
const healthcheck = await this.healthcheck();
|
|
65
|
+
if (healthcheck) {
|
|
66
|
+
PercyLogger.debug('Percy healthcheck successful');
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
await sleep(1000);
|
|
70
|
+
} while (this.isProcessRunning);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
async stop() {
|
|
74
|
+
const binaryPath = await this.#getBinaryPath();
|
|
75
|
+
return new Promise((resolve) => {
|
|
76
|
+
const proc = spawn(binaryPath, ['exec:stop']);
|
|
77
|
+
proc.on('close', (code) => {
|
|
78
|
+
this.isProcessRunning = false;
|
|
79
|
+
resolve(code);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
isRunning() {
|
|
84
|
+
return this.isProcessRunning;
|
|
85
|
+
}
|
|
86
|
+
async fetchPercyToken() {
|
|
87
|
+
const projectName = this.#projectName;
|
|
88
|
+
try {
|
|
89
|
+
const type = this.#isApp ? 'app' : 'automate';
|
|
90
|
+
const response = await nodeRequest('GET', `api/app_percy/get_project_token?name=${projectName}&type=${type}`, {
|
|
91
|
+
username: getBrowserStackUser(this.#config),
|
|
92
|
+
password: getBrowserStackKey(this.#config)
|
|
93
|
+
}, 'https://api.browserstack.com');
|
|
94
|
+
PercyLogger.debug('Percy fetch token success : ' + response.token);
|
|
95
|
+
return response.token;
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
PercyLogger.error(`Percy unable to fetch project token: ${err}`);
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async createPercyConfig() {
|
|
103
|
+
if (!this.#options.percyOptions) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const configPath = path.join(os.tmpdir(), 'percy.json');
|
|
107
|
+
const percyOptions = this.#options.percyOptions;
|
|
108
|
+
if (!percyOptions.version) {
|
|
109
|
+
percyOptions.version = '2';
|
|
110
|
+
}
|
|
111
|
+
return new Promise((resolve) => {
|
|
112
|
+
fs.writeFile(configPath, JSON.stringify(percyOptions), (err) => {
|
|
113
|
+
if (err) {
|
|
114
|
+
PercyLogger.error(`Error creating percy config: ${err}`);
|
|
115
|
+
resolve(null);
|
|
116
|
+
}
|
|
117
|
+
PercyLogger.debug('Percy config created at ' + configPath);
|
|
118
|
+
resolve(configPath);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export default Percy;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Options } from '@wdio/types';
|
|
2
|
+
declare class PercyBinary {
|
|
3
|
+
#private;
|
|
4
|
+
constructor();
|
|
5
|
+
getBinaryPath(conf: Options.Testrunner): Promise<string>;
|
|
6
|
+
validateBinary(binaryPath: string): Promise<unknown>;
|
|
7
|
+
download(conf: any, destParentDir: any): Promise<string>;
|
|
8
|
+
}
|
|
9
|
+
export default PercyBinary;
|
|
10
|
+
//# sourceMappingURL=PercyBinary.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PercyBinary.d.ts","sourceRoot":"","sources":["../../src/Percy/PercyBinary.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAE1C,cAAM,WAAW;;;IAmDP,aAAa,CAAC,IAAI,EAAE,OAAO,CAAC,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;IAgBxD,cAAc,CAAC,UAAU,EAAE,MAAM;IAiBjC,QAAQ,CAAC,IAAI,EAAE,GAAG,EAAE,aAAa,EAAE,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC;CAmEjE;AAED,eAAe,WAAW,CAAA"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import url from 'node:url';
|
|
2
|
+
import yauzl from 'yauzl';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import fsp from 'node:fs/promises';
|
|
5
|
+
import got from 'got';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import { spawn } from 'node:child_process';
|
|
9
|
+
import { PercyLogger } from './PercyLogger.js';
|
|
10
|
+
class PercyBinary {
|
|
11
|
+
#hostOS = process.platform;
|
|
12
|
+
#httpPath = null;
|
|
13
|
+
#binaryName = 'percy';
|
|
14
|
+
#orderedPaths = [
|
|
15
|
+
path.join(os.homedir(), '.browserstack'),
|
|
16
|
+
process.cwd(),
|
|
17
|
+
os.tmpdir()
|
|
18
|
+
];
|
|
19
|
+
constructor() {
|
|
20
|
+
const base = 'https://github.com/percy/cli/releases/latest/download';
|
|
21
|
+
if (this.#hostOS.match(/darwin|mac os/i)) {
|
|
22
|
+
this.#httpPath = base + '/percy-osx.zip';
|
|
23
|
+
}
|
|
24
|
+
else if (this.#hostOS.match(/mswin|msys|mingw|cygwin|bccwin|wince|emc|win32/i)) {
|
|
25
|
+
this.#httpPath = base + '/percy-win.zip';
|
|
26
|
+
this.#binaryName = 'percy.exe';
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
this.#httpPath = base + '/percy-linux.zip';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async #makePath(path) {
|
|
33
|
+
if (await this.#checkPath(path)) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
return fsp.mkdir(path).then(() => true).catch(() => false);
|
|
37
|
+
}
|
|
38
|
+
async #checkPath(path) {
|
|
39
|
+
try {
|
|
40
|
+
const hasDir = await fsp.access(path).then(() => true, () => false);
|
|
41
|
+
if (hasDir) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async #getAvailableDirs() {
|
|
50
|
+
for (let i = 0; i < this.#orderedPaths.length; i++) {
|
|
51
|
+
const path = this.#orderedPaths[i];
|
|
52
|
+
if (await this.#makePath(path)) {
|
|
53
|
+
return path;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
throw new Error('Error trying to download percy binary');
|
|
57
|
+
}
|
|
58
|
+
async getBinaryPath(conf) {
|
|
59
|
+
const destParentDir = await this.#getAvailableDirs();
|
|
60
|
+
const binaryPath = path.join(destParentDir, this.#binaryName);
|
|
61
|
+
if (await this.#checkPath(binaryPath)) {
|
|
62
|
+
return binaryPath;
|
|
63
|
+
}
|
|
64
|
+
const downloadedBinaryPath = await this.download(conf, destParentDir);
|
|
65
|
+
const isValid = await this.validateBinary(downloadedBinaryPath);
|
|
66
|
+
if (!isValid) {
|
|
67
|
+
// retry once
|
|
68
|
+
PercyLogger.error('Corrupt percy binary, retrying');
|
|
69
|
+
return await this.download(conf, destParentDir);
|
|
70
|
+
}
|
|
71
|
+
return downloadedBinaryPath;
|
|
72
|
+
}
|
|
73
|
+
async validateBinary(binaryPath) {
|
|
74
|
+
const versionRegex = /^.*@percy\/cli \d.\d+.\d+/;
|
|
75
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const proc = spawn(binaryPath, ['--version']);
|
|
78
|
+
proc.stdout.on('data', (data) => {
|
|
79
|
+
if (versionRegex.test(data)) {
|
|
80
|
+
resolve(true);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
proc.on('close', () => {
|
|
84
|
+
resolve(false);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async download(conf, destParentDir) {
|
|
89
|
+
if (!await this.#checkPath(destParentDir)) {
|
|
90
|
+
await fsp.mkdir(destParentDir);
|
|
91
|
+
}
|
|
92
|
+
const binaryName = this.#binaryName;
|
|
93
|
+
const zipFilePath = path.join(destParentDir, binaryName + '.zip');
|
|
94
|
+
const binaryPath = path.join(destParentDir, binaryName);
|
|
95
|
+
const downloadedFileStream = fs.createWriteStream(zipFilePath);
|
|
96
|
+
const options = url.parse(this.#httpPath);
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
const stream = got.extend({ followRedirect: true }).get(this.#httpPath, { isStream: true });
|
|
99
|
+
stream.on('error', (err) => {
|
|
100
|
+
PercyLogger.error('Got Error in percy binary download response: ' + err);
|
|
101
|
+
});
|
|
102
|
+
stream.pipe(downloadedFileStream)
|
|
103
|
+
.on('finish', () => {
|
|
104
|
+
yauzl.open(zipFilePath, { lazyEntries: true }, function (err, zipfile) {
|
|
105
|
+
if (err) {
|
|
106
|
+
return reject(err);
|
|
107
|
+
}
|
|
108
|
+
zipfile.readEntry();
|
|
109
|
+
zipfile.on('entry', (entry) => {
|
|
110
|
+
if (/\/$/.test(entry.fileName)) {
|
|
111
|
+
// Directory file names end with '/'.
|
|
112
|
+
zipfile.readEntry();
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
// file entry
|
|
116
|
+
const writeStream = fs.createWriteStream(path.join(destParentDir, entry.fileName));
|
|
117
|
+
zipfile.openReadStream(entry, function (zipErr, readStream) {
|
|
118
|
+
if (zipErr) {
|
|
119
|
+
reject(err);
|
|
120
|
+
}
|
|
121
|
+
readStream.on('end', function () {
|
|
122
|
+
writeStream.close();
|
|
123
|
+
zipfile.readEntry();
|
|
124
|
+
});
|
|
125
|
+
readStream.pipe(writeStream);
|
|
126
|
+
});
|
|
127
|
+
if (entry.fileName === binaryName) {
|
|
128
|
+
zipfile.close();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
zipfile.on('error', (zipErr) => {
|
|
133
|
+
reject(zipErr);
|
|
134
|
+
});
|
|
135
|
+
zipfile.once('end', () => {
|
|
136
|
+
fs.chmod(binaryPath, '0755', function (zipErr) {
|
|
137
|
+
if (zipErr) {
|
|
138
|
+
reject(zipErr);
|
|
139
|
+
}
|
|
140
|
+
resolve(binaryPath);
|
|
141
|
+
});
|
|
142
|
+
zipfile.close();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
export default PercyBinary;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
declare class PercyCaptureMap {
|
|
2
|
+
#private;
|
|
3
|
+
increment(sessionName: string, eventName: string): void;
|
|
4
|
+
decrement(sessionName: string, eventName: string): void;
|
|
5
|
+
getName(sessionName: string, eventName: string): string;
|
|
6
|
+
get(sessionName: string, eventName: string): number;
|
|
7
|
+
}
|
|
8
|
+
export default PercyCaptureMap;
|
|
9
|
+
//# sourceMappingURL=PercyCaptureMap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PercyCaptureMap.d.ts","sourceRoot":"","sources":["../../src/Percy/PercyCaptureMap.ts"],"names":[],"mappings":"AAKA,cAAM,eAAe;;IAGjB,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;IAYhD,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;IAQhD,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;IAI9C,GAAG,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;CAW7C;AAED,eAAe,eAAe,CAAA"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Maintains a counter for each driver to get consistent and
|
|
3
|
+
* unique screenshot names for percy
|
|
4
|
+
*/
|
|
5
|
+
class PercyCaptureMap {
|
|
6
|
+
#map = {};
|
|
7
|
+
increment(sessionName, eventName) {
|
|
8
|
+
if (!this.#map[sessionName]) {
|
|
9
|
+
this.#map[sessionName] = {};
|
|
10
|
+
}
|
|
11
|
+
if (!this.#map[sessionName][eventName]) {
|
|
12
|
+
this.#map[sessionName][eventName] = 0;
|
|
13
|
+
}
|
|
14
|
+
this.#map[sessionName][eventName]++;
|
|
15
|
+
}
|
|
16
|
+
decrement(sessionName, eventName) {
|
|
17
|
+
if (!this.#map[sessionName] || !this.#map[sessionName][eventName]) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
this.#map[sessionName][eventName]--;
|
|
21
|
+
}
|
|
22
|
+
getName(sessionName, eventName) {
|
|
23
|
+
return `${sessionName}-${eventName}-${this.get(sessionName, eventName)}`;
|
|
24
|
+
}
|
|
25
|
+
get(sessionName, eventName) {
|
|
26
|
+
if (!this.#map[sessionName]) {
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
if (!this.#map[sessionName][eventName]) {
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
return this.#map[sessionName][eventName] - 1;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export default PercyCaptureMap;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Capabilities } from '@wdio/types';
|
|
2
|
+
import type { BrowserstackConfig, UserConfig } from '../types.js';
|
|
3
|
+
import type { Options } from '@wdio/types';
|
|
4
|
+
import Percy from './Percy.js';
|
|
5
|
+
export declare const startPercy: (options: BrowserstackConfig & Options.Testrunner, config: Options.Testrunner, bsConfig: UserConfig) => Promise<Percy>;
|
|
6
|
+
export declare const stopPercy: (percy: Percy) => Promise<unknown>;
|
|
7
|
+
export declare const getBestPlatformForPercySnapshot: (capabilities?: Capabilities.RemoteCapabilities) => any;
|
|
8
|
+
//# sourceMappingURL=PercyHelper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PercyHelper.d.ts","sourceRoot":"","sources":["../../src/Percy/PercyHelper.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,KAAK,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAEjE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAG1C,OAAO,KAAK,MAAM,YAAY,CAAA;AAE9B,eAAO,MAAM,UAAU,YAAmB,kBAAkB,GAAG,QAAQ,UAAU,UAAU,QAAQ,UAAU,YAAY,UAAU,KAAG,QAAQ,KAAK,CAQlJ,CAAA;AAED,eAAO,MAAM,SAAS,UAAiB,KAAK,qBAG3C,CAAA;AAED,eAAO,MAAM,+BAA+B,kBAAmB,aAAa,kBAAkB,KAAI,GAgDjG,CAAA"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// ======= Percy helper methods start =======
|
|
2
|
+
import { PercyLogger } from './PercyLogger.js';
|
|
3
|
+
import Percy from './Percy.js';
|
|
4
|
+
export const startPercy = async (options, config, bsConfig) => {
|
|
5
|
+
PercyLogger.debug('Starting percy');
|
|
6
|
+
const percy = new Percy(options, config, bsConfig);
|
|
7
|
+
const response = await percy.start();
|
|
8
|
+
if (response) {
|
|
9
|
+
return percy;
|
|
10
|
+
}
|
|
11
|
+
return {};
|
|
12
|
+
};
|
|
13
|
+
export const stopPercy = async (percy) => {
|
|
14
|
+
PercyLogger.debug('Stopping percy');
|
|
15
|
+
return percy.stop();
|
|
16
|
+
};
|
|
17
|
+
export const getBestPlatformForPercySnapshot = (capabilities) => {
|
|
18
|
+
try {
|
|
19
|
+
const percyBrowserPreference = { 'chrome': 0, 'firefox': 1, 'edge': 2, 'safari': 3 };
|
|
20
|
+
let bestPlatformCaps = null;
|
|
21
|
+
let bestBrowser = null;
|
|
22
|
+
if (Array.isArray(capabilities)) {
|
|
23
|
+
capabilities
|
|
24
|
+
.flatMap((c) => {
|
|
25
|
+
if (Object.values(c).length > 0 && Object.values(c).every(c => typeof c === 'object' && c.capabilities)) {
|
|
26
|
+
return Object.values(c).map((o) => o.capabilities);
|
|
27
|
+
}
|
|
28
|
+
return c;
|
|
29
|
+
}).forEach((capability) => {
|
|
30
|
+
let currBrowserName = capability.browserName;
|
|
31
|
+
if (capability['bstack:options']) {
|
|
32
|
+
currBrowserName = capability['bstack:options'].browserName || currBrowserName;
|
|
33
|
+
}
|
|
34
|
+
if (!bestBrowser || !bestPlatformCaps || (bestPlatformCaps.deviceName || bestPlatformCaps['bstack:options']?.deviceName)) {
|
|
35
|
+
bestBrowser = currBrowserName;
|
|
36
|
+
bestPlatformCaps = capability;
|
|
37
|
+
}
|
|
38
|
+
else if (currBrowserName && percyBrowserPreference[currBrowserName.toLowerCase()] < percyBrowserPreference[bestBrowser.toLowerCase()]) {
|
|
39
|
+
bestBrowser = currBrowserName;
|
|
40
|
+
bestPlatformCaps = capability;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
return bestPlatformCaps;
|
|
44
|
+
}
|
|
45
|
+
else if (typeof capabilities === 'object') {
|
|
46
|
+
Object.entries(capabilities).forEach(([, caps]) => {
|
|
47
|
+
let currBrowserName = caps.capabilities.browserName;
|
|
48
|
+
if (caps.capabilities['bstack:options']) {
|
|
49
|
+
currBrowserName = caps.capabilities['bstack:options']?.browserName || currBrowserName;
|
|
50
|
+
}
|
|
51
|
+
if (!bestBrowser || !bestPlatformCaps || (bestPlatformCaps.deviceName || bestPlatformCaps['bstack:options']?.deviceName)) {
|
|
52
|
+
bestBrowser = currBrowserName;
|
|
53
|
+
bestPlatformCaps = caps.capabilities;
|
|
54
|
+
}
|
|
55
|
+
else if (currBrowserName && percyBrowserPreference[currBrowserName.toLowerCase()] < percyBrowserPreference[bestBrowser.toLowerCase()]) {
|
|
56
|
+
bestBrowser = currBrowserName;
|
|
57
|
+
bestPlatformCaps = caps.capabilities;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
return bestPlatformCaps;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
PercyLogger.error(`Error while trying to determine best platform for Percy snapshot ${err}`);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare class PercyLogger {
|
|
2
|
+
static logFilePath: string;
|
|
3
|
+
private static logFolderPath;
|
|
4
|
+
private static logFileStream;
|
|
5
|
+
static logToFile(logMessage: string, logLevel: string): void;
|
|
6
|
+
private static formatLog;
|
|
7
|
+
static info(message: string): void;
|
|
8
|
+
static error(message: string): void;
|
|
9
|
+
static debug(message: string, param?: any): void;
|
|
10
|
+
static warn(message: string): void;
|
|
11
|
+
static trace(message: string): void;
|
|
12
|
+
static clearLogger(): void;
|
|
13
|
+
static clearLogFile(): void;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=PercyLogger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PercyLogger.d.ts","sourceRoot":"","sources":["../../src/Percy/PercyLogger.ts"],"names":[],"mappings":"AAWA,qBAAa,WAAW;IACpB,OAAc,WAAW,SAA4C;IACrE,OAAO,CAAC,MAAM,CAAC,aAAa,CAAmC;IAC/D,OAAO,CAAC,MAAM,CAAC,aAAa,CAAuB;IAEnD,MAAM,CAAC,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;IAgBrD,OAAO,CAAC,MAAM,CAAC,SAAS;WAIV,IAAI,CAAC,OAAO,EAAE,MAAM;WAKpB,KAAK,CAAC,OAAO,EAAE,MAAM;WAKrB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,GAAG;WASlC,IAAI,CAAC,OAAO,EAAE,MAAM;WAKpB,KAAK,CAAC,OAAO,EAAE,MAAM;WAKrB,WAAW;WAOX,YAAY;CAK7B"}
|