@testim/testim-cli 3.245.0 → 3.248.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/chromiumInstaller.js +92 -0
- package/cli.js +89 -87
- package/cliAgentMode.js +39 -114
- package/commons/getSessionPlayerRequire.js +2 -1
- package/commons/prepareRunner.js +2 -0
- package/commons/testimDesiredCapabilitiesBuilder.js +1 -3
- package/commons/testimServicesApi.js +5 -0
- package/npm-shrinkwrap.json +413 -3723
- package/package.json +1 -5
- package/player/WebDriverHttpRequest.js +45 -37
- package/player/chromeLauncherTestPlayer.js +2 -2
- package/player/services/playbackTimeoutCalculator.js +5 -1
- package/player/stepActions/scripts/selectOption.js +6 -1
- package/player/stepActions/selectOptionStepAction.js +32 -26
- package/player/stepActions/sfdcStepAction.js +27 -0
- package/player/stepActions/stepActionRegistrar.js +12 -0
- package/player/webdriver.js +51 -52
- package/runOptions.js +3 -1
- package/runner.js +44 -41
- package/testRunHandler.js +4 -1
- package/utils.js +42 -2
- package/utils.test.js +26 -0
- package/workers/WorkerSelenium.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testim/testim-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.248.0",
|
|
4
4
|
"description": "Command line interface for running Testing on your CI",
|
|
5
5
|
"author": "Oren Rubin",
|
|
6
6
|
"contributors": [{
|
|
@@ -17,15 +17,11 @@
|
|
|
17
17
|
"@types/node": "10.17.24",
|
|
18
18
|
"@types/selenium-webdriver": "4.0.9",
|
|
19
19
|
"bundle-deps": "1.0.0",
|
|
20
|
-
"chai": "4.3.6",
|
|
21
|
-
"chai-as-promised": "7.1.1",
|
|
22
20
|
"gulp": "4.0.2",
|
|
23
21
|
"gulp-cli": "2.3.0",
|
|
24
22
|
"gulp-json-editor": "2.5.6",
|
|
25
23
|
"gulp-preprocess": "3.0.3",
|
|
26
24
|
"merge-stream": "2.0.0",
|
|
27
|
-
"mocha": "9.2.0",
|
|
28
|
-
"nyc": "15.1.0",
|
|
29
25
|
"proxyquire": "2.1.3",
|
|
30
26
|
"request": "2.88.2",
|
|
31
27
|
"sinon": "9.0.2",
|
|
@@ -4,8 +4,8 @@ const _ = require('lodash');
|
|
|
4
4
|
const httpRequest = require('../commons/httpRequest');
|
|
5
5
|
const utils = require('../utils');
|
|
6
6
|
const SeleniumProtocolError = require('./SeleniumProtocolError');
|
|
7
|
-
const {SELENIUM_STATUS_CODES} = require('./constants');
|
|
8
|
-
const logger = require('
|
|
7
|
+
const { SELENIUM_STATUS_CODES } = require('./constants');
|
|
8
|
+
const logger = require('../commons/logger').getLogger('WebDriverHttpRequest');
|
|
9
9
|
|
|
10
10
|
function isSuccessfulResponse(body, statusCode) {
|
|
11
11
|
/**
|
|
@@ -56,37 +56,37 @@ class WebDriverHttpRequest {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
handleFailedStatus(requestId, response, err = {}) {
|
|
59
|
-
const {body, statusCode, headers, text} = response;
|
|
59
|
+
const { body, statusCode, headers, text } = response;
|
|
60
60
|
const resBody = _.isEmpty(body) && text ? text : body;
|
|
61
61
|
/**
|
|
62
62
|
* in Appium you find sometimes more exact error messages in origValue
|
|
63
63
|
*/
|
|
64
64
|
if (resBody && resBody.value && typeof resBody.value.origValue === 'string' && typeof resBody.value.message === 'string') {
|
|
65
|
-
resBody.value.message +=
|
|
65
|
+
resBody.value.message += ` ${resBody.value.origValue}`;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
if (resBody && typeof resBody === 'string') {
|
|
69
|
-
throw new SeleniumProtocolError(resBody, {requestId});
|
|
69
|
+
throw new SeleniumProtocolError(resBody, { requestId });
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
if (resBody && resBody.value) {
|
|
73
73
|
const errorCode = SELENIUM_STATUS_CODES[resBody.status] || (resBody.value && SELENIUM_STATUS_CODES[resBody.value.error]) || SELENIUM_STATUS_CODES[-1];
|
|
74
|
-
|
|
74
|
+
const error = {
|
|
75
75
|
type: errorCode ? errorCode.id : 'unknown',
|
|
76
76
|
message: errorCode ? errorCode.message : 'unknown',
|
|
77
|
-
orgStatusMessage: resBody.value ? resBody.value.message : ''
|
|
77
|
+
orgStatusMessage: resBody.value ? resBody.value.message : '',
|
|
78
78
|
};
|
|
79
79
|
|
|
80
|
-
throw new SeleniumProtocolError(error, {requestId});
|
|
80
|
+
throw new SeleniumProtocolError(error, { requestId });
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
// IEServer webdriver bug where the error is put into the Allow header
|
|
84
84
|
// https://github.com/SeleniumHQ/selenium/issues/6828
|
|
85
85
|
if (statusCode === 405) {
|
|
86
86
|
const allowHeader = headers && headers.allow;
|
|
87
|
-
const
|
|
88
|
-
if (
|
|
89
|
-
throw
|
|
87
|
+
const _err = new SeleniumProtocolError(allowHeader, { requestId });
|
|
88
|
+
if (_err.message.match(/Command not found/)) {
|
|
89
|
+
throw _err;
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
@@ -94,12 +94,12 @@ class WebDriverHttpRequest {
|
|
|
94
94
|
status: -1,
|
|
95
95
|
type: err.code || 'ECONNREFUSED',
|
|
96
96
|
orgStatusMessage: err.rawResponse || err.message,
|
|
97
|
-
message: err.rawResponse || "Couldn't connect to selenium server"
|
|
98
|
-
}, {requestId});
|
|
97
|
+
message: err.rawResponse || "Couldn't connect to selenium server",
|
|
98
|
+
}, { requestId });
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
processHttpOnSuccess(response, requestId) {
|
|
102
|
-
const {body, statusCode} = response;
|
|
102
|
+
const { body, statusCode } = response;
|
|
103
103
|
if (isSuccessfulResponse(body, statusCode)) {
|
|
104
104
|
return body;
|
|
105
105
|
}
|
|
@@ -108,44 +108,49 @@ class WebDriverHttpRequest {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
processHttpOnError(err, requestId) {
|
|
111
|
-
const response = err && err.response || {};
|
|
111
|
+
const response = (err && err.response) || {};
|
|
112
112
|
return this.handleFailedStatus(requestId, response, err);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
httpPostRequest(path, body = {}) {
|
|
116
116
|
const requestId = utils.guid();
|
|
117
|
-
if (path ===
|
|
118
|
-
logger.info(
|
|
117
|
+
if (path === '/session') {
|
|
118
|
+
logger.info('POST REQUEST', { requestId, path, testResultId: this.testResultId });
|
|
119
119
|
} else {
|
|
120
|
-
logger.info(
|
|
120
|
+
logger.info('POST REQUEST', { requestId, path, body, testResultId: this.testResultId });
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
return httpRequest.postFullRes(`${this.gridUrl}${path}`, body, this.headers, this.connectionRetryTimeout)
|
|
124
|
-
.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
124
|
+
.then(response => {
|
|
125
|
+
logger.info('POST RESPONSE', {
|
|
126
|
+
requestId,
|
|
127
|
+
path,
|
|
128
|
+
res: JSON.stringify(response.body),
|
|
129
|
+
testResultId: this.testResultId,
|
|
130
|
+
});
|
|
131
|
+
return response;
|
|
132
|
+
})
|
|
130
133
|
.catch(err => this.processHttpOnError(err, requestId))
|
|
131
134
|
.then((response) => this.processHttpOnSuccess(response, requestId));
|
|
132
135
|
}
|
|
133
136
|
|
|
134
137
|
httpGetRequest(path) {
|
|
135
138
|
const requestId = utils.guid();
|
|
136
|
-
logger.info(
|
|
139
|
+
logger.info('GET REQUEST', { requestId, path, testResultId: this.testResultId });
|
|
137
140
|
const query = {};
|
|
138
141
|
return httpRequest.getFullRes(`${this.gridUrl}${path}`, query, this.headers, this.connectionRetryTimeout)
|
|
139
|
-
.
|
|
140
|
-
if (_.endsWith(path,
|
|
141
|
-
|
|
142
|
+
.then(response => {
|
|
143
|
+
if (_.endsWith(path, '/screenshot')) {
|
|
144
|
+
logger.info('GET RESPONSE', { requestId, path, testResultId: this.testResultId });
|
|
145
|
+
return response;
|
|
142
146
|
}
|
|
143
|
-
logger.info(
|
|
147
|
+
logger.info('GET RESPONSE', {
|
|
144
148
|
requestId,
|
|
145
149
|
path,
|
|
146
150
|
res: JSON.stringify(response.body),
|
|
147
|
-
testResultId: this.testResultId
|
|
151
|
+
testResultId: this.testResultId,
|
|
148
152
|
});
|
|
153
|
+
return response;
|
|
149
154
|
})
|
|
150
155
|
.catch(err => this.processHttpOnError(err, requestId))
|
|
151
156
|
.then((response) => this.processHttpOnSuccess(response, requestId));
|
|
@@ -153,14 +158,17 @@ class WebDriverHttpRequest {
|
|
|
153
158
|
|
|
154
159
|
httpDeleteRequest(path) {
|
|
155
160
|
const requestId = utils.guid();
|
|
156
|
-
logger.info(
|
|
161
|
+
logger.info('DELETE REQUEST', { requestId, path, testResultId: this.testResultId });
|
|
157
162
|
return httpRequest.deleteFullRes(`${this.gridUrl}${path}`, undefined, this.headers, this.connectionRetryTimeout)
|
|
158
|
-
.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
163
|
+
.then(response => {
|
|
164
|
+
logger.info('DELETE RESPONSE', {
|
|
165
|
+
requestId,
|
|
166
|
+
path,
|
|
167
|
+
res: JSON.stringify(response.body),
|
|
168
|
+
testResultId: this.testResultId,
|
|
169
|
+
});
|
|
170
|
+
return response;
|
|
171
|
+
})
|
|
164
172
|
.catch(err => this.processHttpOnError(err, requestId))
|
|
165
173
|
.then((response) => this.processHttpOnSuccess(response, requestId));
|
|
166
174
|
}
|
|
@@ -23,8 +23,8 @@ class LauncherDriver {
|
|
|
23
23
|
this.chrome.process.once('exit', () => { this._isAlive = false; });
|
|
24
24
|
this.chrome.process.once('close', () => { this._isAlive = false; });
|
|
25
25
|
this._isAlive = true;
|
|
26
|
-
const
|
|
27
|
-
await this.cdpTestRunner.initSession(
|
|
26
|
+
const webSocketDebuggerUrl = await utils.getCdpAddressForHost(`localhost:${this.chrome.port}`);
|
|
27
|
+
await this.cdpTestRunner.initSession(webSocketDebuggerUrl);
|
|
28
28
|
|
|
29
29
|
registerExitHook(() => this.chrome.kill());
|
|
30
30
|
}
|
|
@@ -4,6 +4,7 @@ const _ = require('lodash');
|
|
|
4
4
|
|
|
5
5
|
const COMMUNICATION_BUFFER_TIME = 1000;
|
|
6
6
|
const UI_VERIFICATION_STEPS = ['simple-ui-verification', 'wait-for-simple-ui-verification'];
|
|
7
|
+
const FULL_TIMEOUT_STEP_TYPES = [...UI_VERIFICATION_STEPS, 'custom-validation', 'sfdc-step-login'];
|
|
7
8
|
|
|
8
9
|
class PlaybackTimeoutCalculator {
|
|
9
10
|
constructor(isDebuggerConnected) {
|
|
@@ -41,7 +42,7 @@ class PlaybackTimeoutCalculator {
|
|
|
41
42
|
};
|
|
42
43
|
stepPlayback.setStartTimestamp();
|
|
43
44
|
const totalStepTime = this.getTotalStepRunTime(stepPlayback);
|
|
44
|
-
const currentRetryTimes =
|
|
45
|
+
const currentRetryTimes = FULL_TIMEOUT_STEP_TYPES.includes(stepPlayback.stepType) ? [totalStepTime] : getRetryTimeoutSuggestions(totalStepTime);
|
|
45
46
|
this.resetStepVariables(totalStepTime, currentRetryTimes);
|
|
46
47
|
stepPlayback.context.data.maxTotalStepTime = totalStepTime;
|
|
47
48
|
}
|
|
@@ -61,6 +62,9 @@ class PlaybackTimeoutCalculator {
|
|
|
61
62
|
if (UI_VERIFICATION_STEPS.includes(stepPlayback.stepType)) {
|
|
62
63
|
fallbackTimeout = stepPlayback.context.config.applitoolsStepTimeout || HALF_HOUR_IN_MS;
|
|
63
64
|
}
|
|
65
|
+
if (stepPlayback.step.type.startsWith('sfdc-')) {
|
|
66
|
+
fallbackTimeout = stepPlayback.step.defaultTimeout;
|
|
67
|
+
}
|
|
64
68
|
return (stepPlayback.step.useStepTimeout && stepPlayback.step.stepTimeout) ? stepPlayback.step.stepTimeout : fallbackTimeout;
|
|
65
69
|
}
|
|
66
70
|
getTotalStepTimeLeftToPlay(stepPlayback, totalStepTime = this.totalStepTime) {
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
+
/* eslint-disable no-restricted-syntax */ // This code only runs in Safari, not in IE.
|
|
1
2
|
module.exports = function (locatedElement, dispatchEventOnSelectElement) {
|
|
2
3
|
function closest(el, selectors) {
|
|
4
|
+
var originalMatches = Element.prototype.matches;
|
|
5
|
+
/* eslint-disable-next-line no-proto, no-undef */ // Some customers override the native Element prototype, so we need to create a new one if they messed it up.
|
|
6
|
+
var matches = originalMatches && isNativeFunction(originalMatches) ? originalMatches : document.createElement(el.tagName).__proto__.matches;
|
|
3
7
|
do {
|
|
4
|
-
if (
|
|
8
|
+
if (matches.call(el, selectors)) return el;
|
|
5
9
|
el = el.parentElement || el.parentNode;
|
|
6
10
|
} while (el !== null && el.nodeType === 1);
|
|
7
11
|
return null;
|
|
8
12
|
}
|
|
9
13
|
|
|
10
14
|
try {
|
|
15
|
+
/* eslint-disable-next-line no-undef */ // This code depends on pre-injecting this function as well.
|
|
11
16
|
var optionEl = getLocatedElement(locatedElement);
|
|
12
17
|
if (!optionEl) {
|
|
13
18
|
return { success: false, status: 'failed', result: 'option element not found' };
|
|
@@ -2,42 +2,48 @@
|
|
|
2
2
|
|
|
3
3
|
const StepAction = require('./stepAction');
|
|
4
4
|
const { extractElementId } = require('../../utils');
|
|
5
|
-
const { codeSnippets } = require('../../commons/getSessionPlayerRequire');
|
|
5
|
+
const { codeSnippets, utils } = require('../../commons/getSessionPlayerRequire');
|
|
6
6
|
const selectOption = require('./scripts/selectOption');
|
|
7
7
|
const featureFlags = require('../../commons/featureFlags');
|
|
8
8
|
|
|
9
9
|
class SelectOptionStepAction extends StepAction {
|
|
10
|
-
performAction() {
|
|
10
|
+
async performAction() {
|
|
11
11
|
const target = this.context.data[this.step.targetId || 'targetId'];
|
|
12
12
|
const { seleniumElement, locatedElement } = target;
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
.then(browserAndOS => {
|
|
16
|
-
const browserMajor = browserAndOS.browserMajor;
|
|
17
|
-
const isSafari = this.driver.isSafari();
|
|
18
|
-
const isShadowed = Boolean(this.step.element && this.step.element.isShadowed);
|
|
14
|
+
const browserAndOS = await this.driver.getBrowserAndOS();
|
|
19
15
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
const browserMajor = browserAndOS.browserMajor;
|
|
17
|
+
const isSafari = this.driver.isSafari();
|
|
18
|
+
const isShadowed = Boolean(this.step.element && this.step.element.isShadowed);
|
|
19
|
+
|
|
20
|
+
// TODO: Remove the special handling for safari < 12 after we upgrade our grid to safari 13.
|
|
21
|
+
// force use js code when element is shadow dom
|
|
22
|
+
if (!isSafari || (isSafari && browserMajor >= 13 && !isShadowed)) {
|
|
23
|
+
try {
|
|
24
|
+
const res = await this.driver.elementIdClick(extractElementId(seleniumElement));
|
|
25
|
+
return res;
|
|
26
|
+
} catch (err) {
|
|
27
|
+
// If customer overrides the native Element prototype, this click will fail for this reason. in such a case, fallback to use js code.
|
|
28
|
+
if (!err.message.includes('Cannot check the displayedness of a non-Element argument')) {
|
|
29
|
+
throw err;
|
|
24
30
|
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const safariSelectOptionDispatchEventOnSelectElement = featureFlags.flags.safariSelectOptionDispatchEventOnSelectElement.isEnabled();
|
|
35
|
+
const selectOptionCode = `
|
|
36
|
+
var getLocatedElement = ${codeSnippets.getLocatedElementCode};
|
|
37
|
+
var isNativeFunction = ${utils.isNativeFunction.toString()};
|
|
38
|
+
var selectOption = ${selectOption.toString()};
|
|
39
|
+
return selectOption.apply(null, arguments);
|
|
40
|
+
`;
|
|
25
41
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
`;
|
|
32
|
-
|
|
33
|
-
return this.driver.executeJSWithArray(selectOptionCode, [locatedElement, safariSelectOptionDispatchEventOnSelectElement])
|
|
34
|
-
.then(result => {
|
|
35
|
-
if (result.value && result.value.success) {
|
|
36
|
-
return { success: true };
|
|
37
|
-
}
|
|
38
|
-
return { success: false };
|
|
39
|
-
});
|
|
40
|
-
});
|
|
42
|
+
const result = await this.driver.executeJSWithArray(selectOptionCode, [locatedElement, safariSelectOptionDispatchEventOnSelectElement]);
|
|
43
|
+
if (result.value && result.value.success) {
|
|
44
|
+
return { success: true };
|
|
45
|
+
}
|
|
46
|
+
return { success: false };
|
|
41
47
|
}
|
|
42
48
|
}
|
|
43
49
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const StepAction = require('./stepAction');
|
|
2
|
+
const { sfdc } = require('../../commons/getSessionPlayerRequire');
|
|
3
|
+
|
|
4
|
+
class SfdcStepAction extends StepAction {
|
|
5
|
+
async performAction() {
|
|
6
|
+
const page = sfdc.sfdcNewSePage(this.driver);
|
|
7
|
+
try {
|
|
8
|
+
const actions = this.context.sfdcTestActions;
|
|
9
|
+
if (actions === undefined) {
|
|
10
|
+
throw new Error('No test actions were compiled');
|
|
11
|
+
}
|
|
12
|
+
await sfdc.sfdcExecute(page, actions);
|
|
13
|
+
return { success: true };
|
|
14
|
+
} catch (err) {
|
|
15
|
+
return {
|
|
16
|
+
success: false,
|
|
17
|
+
reason: err.reason || err.message,
|
|
18
|
+
exception: err,
|
|
19
|
+
shouldRetry: false, // TODO - check this. Our (bFormat) steps are probably not retryable?
|
|
20
|
+
};
|
|
21
|
+
} finally {
|
|
22
|
+
page.releaseObjects();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = SfdcStepAction;
|
|
@@ -27,6 +27,7 @@ const CliJsStepAction = require('./cliJsStepAction');
|
|
|
27
27
|
const CliConditionStepAction = require('./cliConditionStepAction');
|
|
28
28
|
const NodePackageStepAction = require('./nodePackageStepAction');
|
|
29
29
|
const ExtensionOnlyStepAction = require('./extensionOnlyStepAction');
|
|
30
|
+
const SfdcStepAction = require('./sfdcStepAction');
|
|
30
31
|
|
|
31
32
|
function register(stepActionByType, stepActionFactory) {
|
|
32
33
|
Object.keys(stepActionByType).forEach(type => {
|
|
@@ -82,6 +83,17 @@ module.exports = function (driver, stepActionFactory, runMode) {
|
|
|
82
83
|
'email-code-step': JsCodeStepAction,
|
|
83
84
|
'cli-email-code-step': CliJsStepAction,
|
|
84
85
|
'tdk-hybrid': TdkHybridStepAction,
|
|
86
|
+
|
|
87
|
+
'sfdc-step-login': SfdcStepAction,
|
|
88
|
+
'sfdc-step-logout': SfdcStepAction,
|
|
89
|
+
'sfdc-step-sobjectcreate': SfdcStepAction,
|
|
90
|
+
'sfdc-step-sobjectdelete': SfdcStepAction,
|
|
91
|
+
'sfdc-step-findrecord': SfdcStepAction,
|
|
92
|
+
'sfdc-step-quickaction': SfdcStepAction,
|
|
93
|
+
'sfdc-step-edit': SfdcStepAction,
|
|
94
|
+
'sfdc-step-sobjectvalidate': SfdcStepAction,
|
|
95
|
+
'sfdc-step-launchapp': SfdcStepAction,
|
|
96
|
+
'sfdc-step-closeconsoletabs': SfdcStepAction,
|
|
85
97
|
};
|
|
86
98
|
|
|
87
99
|
register(STEP_ACTION_MAPPING, stepActionFactory);
|