@applitools/nml-client 1.11.12 → 1.11.14
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/CHANGELOG.md +31 -0
- package/dist/server/req-broker.js +117 -26
- package/dist/server/requests.js +28 -25
- package/package.json +4 -4
- package/types/server/req-broker.d.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.11.14](https://github.com/Applitools-Dev/sdk/compare/js/nml-client@1.11.13...js/nml-client@1.11.14) (2026-01-11)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* nml broker retry mechanism | FLD-3968 FLD-3963 FLD-3950 ([#3430](https://github.com/Applitools-Dev/sdk/issues/3430)) ([42617e0](https://github.com/Applitools-Dev/sdk/commit/42617e021f43a89f8a8f2cb914f489ac8d215714))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Dependencies
|
|
12
|
+
|
|
13
|
+
* @applitools/driver bumped to 1.24.4
|
|
14
|
+
#### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* scrolling element fallback logic | FLD-3959 ([#3442](https://github.com/Applitools-Dev/sdk/issues/3442)) ([36348b4](https://github.com/Applitools-Dev/sdk/commit/36348b46e6a127c99d4ccfa58bf386a8e414fb40))
|
|
17
|
+
* @applitools/spec-driver-webdriver bumped to 1.5.4
|
|
18
|
+
|
|
19
|
+
* @applitools/core-base bumped to 1.31.1
|
|
20
|
+
#### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* per-API key heartbeat management | FLD-3889 ([#3406](https://github.com/Applitools-Dev/sdk/issues/3406)) ([5d7f380](https://github.com/Applitools-Dev/sdk/commit/5d7f38037f17006dcc923c4a3dc925e8dded25d8))
|
|
23
|
+
|
|
24
|
+
## [1.11.13](https://github.com/Applitools-Dev/sdk/compare/js/nml-client@1.11.12...js/nml-client@1.11.13) (2025-12-14)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Dependencies
|
|
28
|
+
|
|
29
|
+
* @applitools/core-base bumped to 1.31.0
|
|
30
|
+
#### Features
|
|
31
|
+
|
|
32
|
+
* Baseline branch fallback list | FLD-3837 ([#3373](https://github.com/Applitools-Dev/sdk/issues/3373)) ([e94bb10](https://github.com/Applitools-Dev/sdk/commit/e94bb10ad6b49322a56e4ce6dfde560b237e9ac0))
|
|
33
|
+
|
|
3
34
|
## [1.11.12](https://github.com/Applitools-Dev/sdk/compare/js/nml-client@1.11.11...js/nml-client@1.11.12) (2025-12-01)
|
|
4
35
|
|
|
5
36
|
|
|
@@ -26,6 +26,24 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
26
26
|
exports.makeReqBroker = void 0;
|
|
27
27
|
const req_1 = __importStar(require("@applitools/req"));
|
|
28
28
|
const utils = __importStar(require("@applitools/utils"));
|
|
29
|
+
function isInvalidBrokerMessage(result) {
|
|
30
|
+
try {
|
|
31
|
+
const parsedResult = JSON.parse(result);
|
|
32
|
+
// Bare empty arrays indicate result is not ready yet
|
|
33
|
+
if (Array.isArray(parsedResult) && parsedResult.length === 0)
|
|
34
|
+
return true;
|
|
35
|
+
// Check objects with empty payload arrays
|
|
36
|
+
if (parsedResult && typeof parsedResult === 'object' && !Array.isArray(parsedResult)) {
|
|
37
|
+
const { payload } = parsedResult;
|
|
38
|
+
if (Array.isArray(payload) && payload.length === 0)
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return true; // unparsable response is equivalent to empty one
|
|
45
|
+
}
|
|
46
|
+
}
|
|
29
47
|
function makeReqBroker({ settings, logger }) {
|
|
30
48
|
return (0, req_1.makeReq)({
|
|
31
49
|
method: 'POST',
|
|
@@ -63,34 +81,106 @@ function handleLogs({ logger: defaultLogger } = {}) {
|
|
|
63
81
|
},
|
|
64
82
|
};
|
|
65
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Creates a custom fetch function that buffers polling responses to work around node-fetch clone bug.
|
|
86
|
+
*
|
|
87
|
+
* node-fetch 3.x has a bug where cloning a response after reading its body results in
|
|
88
|
+
* empty clones. Since polling requests use retry validation that reads the body AND
|
|
89
|
+
* have afterResponse hooks that also read the body, we need to buffer BEFORE retry
|
|
90
|
+
* validation runs. We do this by wrapping the fetch function itself.
|
|
91
|
+
*
|
|
92
|
+
* See: js/packages/nml-client/docs/NODE_FETCH_CLONE_BUG.md for full investigation details
|
|
93
|
+
*/
|
|
94
|
+
function makeBufferingFetch() {
|
|
95
|
+
return async (input, init) => {
|
|
96
|
+
// Use globalReq to fetch, forwarding all options (proxy, headers, etc.)
|
|
97
|
+
// Pass init directly to preserve proxy settings and other options
|
|
98
|
+
const response = await (0, req_1.default)(input, init);
|
|
99
|
+
// Buffer the body once
|
|
100
|
+
const bufferedBody = await response.text();
|
|
101
|
+
// Create fresh Response with buffered string (not stream)
|
|
102
|
+
const bufferedResponse = new req_1.Response(bufferedBody, {
|
|
103
|
+
status: response.status,
|
|
104
|
+
statusText: response.statusText,
|
|
105
|
+
headers: response.headers,
|
|
106
|
+
});
|
|
107
|
+
// Monkey-patch .text() and .json() to return from buffered string
|
|
108
|
+
// WITHOUT consuming the Response body. This is critical because:
|
|
109
|
+
// 1. Retry validation calls .text() to check for empty payloads
|
|
110
|
+
// 2. Then hooks/caller call .json() to parse the result
|
|
111
|
+
// 3. Calling .text() normally consumes the body, breaking .json()
|
|
112
|
+
bufferedResponse.text = async () => bufferedBody;
|
|
113
|
+
bufferedResponse.json = async () => JSON.parse(bufferedBody);
|
|
114
|
+
// Also patch .clone() to return responses with the same monkey-patched methods
|
|
115
|
+
const originalClone = bufferedResponse.clone.bind(bufferedResponse);
|
|
116
|
+
bufferedResponse.clone = () => {
|
|
117
|
+
const cloned = originalClone();
|
|
118
|
+
cloned.text = async () => bufferedBody;
|
|
119
|
+
cloned.json = async () => JSON.parse(bufferedBody);
|
|
120
|
+
return cloned;
|
|
121
|
+
};
|
|
122
|
+
return bufferedResponse;
|
|
123
|
+
};
|
|
124
|
+
}
|
|
66
125
|
function handleLongRequests({ req, logger: defaultLogger }) {
|
|
67
126
|
return {
|
|
68
127
|
async afterResponse({ request, response, options }) {
|
|
128
|
+
var _a;
|
|
69
129
|
if (response.status === 200) {
|
|
130
|
+
let attemptCount = 0;
|
|
131
|
+
const MAX_ATTEMPTS = 1500;
|
|
132
|
+
const logger = (_a = options === null || options === void 0 ? void 0 : options.logger) !== null && _a !== void 0 ? _a : defaultLogger;
|
|
133
|
+
// Use custom fetch that buffers responses to work around node-fetch clone bug
|
|
134
|
+
const bufferingFetch = makeBufferingFetch();
|
|
70
135
|
return req(request.url + '-response', {
|
|
71
136
|
proxy: options === null || options === void 0 ? void 0 : options.proxy,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
137
|
+
fetch: bufferingFetch,
|
|
138
|
+
retry: [
|
|
139
|
+
{
|
|
140
|
+
// 1500 attempts x 200 ms = 5 minutes
|
|
141
|
+
limit: MAX_ATTEMPTS,
|
|
142
|
+
timeout: 200,
|
|
143
|
+
statuses: [404],
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
limit: MAX_ATTEMPTS,
|
|
147
|
+
timeout: 200,
|
|
148
|
+
validate: async ({ response }) => {
|
|
149
|
+
if (!response || response.status !== 200)
|
|
150
|
+
return false;
|
|
151
|
+
try {
|
|
152
|
+
const body = await response.clone().text();
|
|
153
|
+
const isInvalid = isInvalidBrokerMessage(body);
|
|
154
|
+
if (isInvalid) {
|
|
155
|
+
logger === null || logger === void 0 ? void 0 : logger.log(`Broker polling request "${options === null || options === void 0 ? void 0 : options.name}" received empty response, retrying (attempt ${attemptCount + 1}/${MAX_ATTEMPTS})`);
|
|
156
|
+
}
|
|
157
|
+
return isInvalid;
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
],
|
|
78
165
|
hooks: [
|
|
79
166
|
{
|
|
80
167
|
beforeRequest({ request, options: beforeOptions }) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
logger === null || logger === void 0 ? void 0 : logger.log(`Broker polling request "${options === null || options === void 0 ? void 0 : options.name}" will be sent to the address "[${request.method}]${request.url}" with body`, beforeOptions === null || beforeOptions === void 0 ? void 0 : beforeOptions.body);
|
|
168
|
+
attemptCount++;
|
|
169
|
+
logger === null || logger === void 0 ? void 0 : logger.log(`Broker polling request "${options === null || options === void 0 ? void 0 : options.name}" attempt #${attemptCount}/${MAX_ATTEMPTS} will be sent to the address "[${request.method}]${request.url}" with body`, beforeOptions === null || beforeOptions === void 0 ? void 0 : beforeOptions.body);
|
|
84
170
|
},
|
|
85
171
|
async afterResponse({ request, response }) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
172
|
+
try {
|
|
173
|
+
const body = await response.clone().text();
|
|
174
|
+
logger === null || logger === void 0 ? void 0 : logger.log(`Broker polling request "${options === null || options === void 0 ? void 0 : options.name}" attempt #${attemptCount}/${MAX_ATTEMPTS} that was sent to the address "[${request.method}]${request.url}" respond with ${response.statusText}(${response.status}) with body ${JSON.stringify(body)}`);
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
logger === null || logger === void 0 ? void 0 : logger.log(`Broker polling request "${options === null || options === void 0 ? void 0 : options.name}" attempt #${attemptCount}/${MAX_ATTEMPTS} that was sent to the address "[${request.method}]${request.url}" respond with ${response.statusText}(${response.status})`);
|
|
178
|
+
}
|
|
89
179
|
},
|
|
90
180
|
afterError({ request, error }) {
|
|
91
181
|
var _a;
|
|
92
182
|
const logger = (_a = options === null || options === void 0 ? void 0 : options.logger) !== null && _a !== void 0 ? _a : defaultLogger;
|
|
93
|
-
logger === null || logger === void 0 ? void 0 : logger.error(`Broker polling request "${options === null || options === void 0 ? void 0 : options.name}" that was sent to the address "[${request.method}]${request.url}" failed with error`, error);
|
|
183
|
+
logger === null || logger === void 0 ? void 0 : logger.error(`Broker polling request "${options === null || options === void 0 ? void 0 : options.name}" attempt #${attemptCount}/${MAX_ATTEMPTS} that was sent to the address "[${request.method}]${request.url}" failed with error`, error);
|
|
94
184
|
},
|
|
95
185
|
},
|
|
96
186
|
],
|
|
@@ -105,21 +195,22 @@ function handleUnexpectedResponse() {
|
|
|
105
195
|
if (response.status !== 200) {
|
|
106
196
|
throw new Error(`Something went wrong when communicating with the mobile application, please try running your test again (error code: ${response.status})`);
|
|
107
197
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
198
|
+
const body = await response.clone().text();
|
|
199
|
+
if (!body) {
|
|
200
|
+
return response;
|
|
201
|
+
}
|
|
202
|
+
const result = JSON.parse(body);
|
|
203
|
+
if (result === null || result === void 0 ? void 0 : result.payload) {
|
|
204
|
+
const error = utils.types.isArray(result.payload)
|
|
205
|
+
? result.payload.find(payload => payload.error)
|
|
206
|
+
: result.payload.error;
|
|
207
|
+
if (error) {
|
|
208
|
+
const nmlError = new Error(`There was a problem when interacting with the mobile application. The provided error message was "${error.message}" and had a stack trace of "${error.stack}"`);
|
|
209
|
+
nmlError.nextPath = result.nextPath;
|
|
210
|
+
throw nmlError;
|
|
120
211
|
}
|
|
121
|
-
return { response, body };
|
|
122
212
|
}
|
|
213
|
+
return response;
|
|
123
214
|
},
|
|
124
215
|
};
|
|
125
216
|
}
|
package/dist/server/requests.js
CHANGED
|
@@ -81,7 +81,7 @@ function makeNMLRequests({ settings, logger: mainLogger, }) {
|
|
|
81
81
|
brokerUrl = result.nextPath;
|
|
82
82
|
}
|
|
83
83
|
async function takeScreenshots({ settings, logger = mainLogger, }) {
|
|
84
|
-
var _a
|
|
84
|
+
var _a;
|
|
85
85
|
logger = logger.extend(mainLogger, { tags: [`nml-request-${utils.general.shortid()}`] });
|
|
86
86
|
logger.log('Request "takeScreenshots" called with settings', settings);
|
|
87
87
|
const { localEnvironment, renderEnvironments, environmentSettings } = await (0, get_environments_info_1.getNMLEnvironmentsInfo)({
|
|
@@ -114,32 +114,35 @@ function makeNMLRequests({ settings, logger: mainLogger, }) {
|
|
|
114
114
|
let screenshots;
|
|
115
115
|
if (Number(result.protocolVersion) >= 2) {
|
|
116
116
|
logger.log(`Request "takeScreenshots" was performed on applitools lib v${result.nmlVersion} through protocol v${result.protocolVersion} on device`, result.payload.debugInfo);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
errMessage += ` local environment ${JSON.stringify(localEnvironment)}:\n`;
|
|
122
|
-
errMessage += `\t ${JSON.stringify(errors)}`;
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
errMessage += ' environments:';
|
|
126
|
-
errMessage += ` \n\t ${errors
|
|
127
|
-
.map((err, index) => `Environment ${JSON.stringify(renderEnvironments[index])}: ${JSON.stringify(err)}`)
|
|
128
|
-
.join('\n\t')}`;
|
|
117
|
+
// Note: Payload-level errors (payload.error) are already handled by req-broker.ts afterResponse hook
|
|
118
|
+
if (localEnvironment) {
|
|
119
|
+
if (result.payload.result[0].error) {
|
|
120
|
+
throw new Error(`There was a problem in taking screenshot for local environment ${JSON.stringify(localEnvironment)}. The provided error message was "${result.payload.result[0].error.message}" and had a stack trace of "${result.payload.result[0].error.stack}"`);
|
|
129
121
|
}
|
|
130
|
-
|
|
122
|
+
screenshots = [
|
|
123
|
+
{
|
|
124
|
+
image: result.payload.result[0].result.screenshotUrl,
|
|
125
|
+
calculateRegions: result.payload.result[0].result.selectorRegions,
|
|
126
|
+
dom: result.payload.result[0].result.dom,
|
|
127
|
+
environment: localEnvironment,
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
screenshots = renderEnvironments.map((environment, index) => {
|
|
133
|
+
if (result.payload.result[index].error) {
|
|
134
|
+
throw new Error(`There was a problem in taking screenshot for environment ${JSON.stringify(environment)}. The provided error message was "${result.payload.result[index].error.message}" and had a stack trace of "${result.payload.result[index].error.stack}"`);
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
image: result.payload.result[index].result.screenshotUrl,
|
|
138
|
+
calculateRegions: result.payload.result[index].result.selectorRegions,
|
|
139
|
+
dom: result.payload.result[index].result.dom,
|
|
140
|
+
environment,
|
|
141
|
+
};
|
|
142
|
+
});
|
|
131
143
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
image: result.payload.result[index].result.screenshotUrl,
|
|
135
|
-
calculateRegions: result.payload.result[index].result.selectorRegions,
|
|
136
|
-
dom: result.payload.result[index].result.dom,
|
|
137
|
-
environment: localEnvironment || environment,
|
|
138
|
-
};
|
|
139
|
-
});
|
|
140
|
-
if (localEnvironment && renderEnvironments.length > 1) {
|
|
141
|
-
logger.warn(`Local environment detected with ${renderEnvironments.length} rendered environment(s). Using local environment ${JSON.stringify(localEnvironment)} and ignoring rendered environments: ${JSON.stringify(renderEnvironments)}. Note - this warning shouldn't appear in normal usage.`);
|
|
142
|
-
screenshots = [screenshots[0]];
|
|
144
|
+
if (localEnvironment && renderEnvironments.length > 0) {
|
|
145
|
+
logger.warn(`Local environment detected with ${renderEnvironments.length} rendered environment(s). Using local environment and ignoring rendered environments: ${JSON.stringify(renderEnvironments)}. Note - this warning shouldn't appear in normal usage.`);
|
|
143
146
|
}
|
|
144
147
|
}
|
|
145
148
|
else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applitools/nml-client",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.14",
|
|
4
4
|
"description": "Client to integrate the SDKs to the Native Mobile Library (NML)",
|
|
5
5
|
"homepage": "https://applitools.com",
|
|
6
6
|
"bugs": {
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"@applitools/utils": "1.14.1"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@applitools/core-base": "1.
|
|
45
|
-
"@applitools/spec-driver-webdriver": "^1.5.
|
|
44
|
+
"@applitools/core-base": "1.31.1",
|
|
45
|
+
"@applitools/spec-driver-webdriver": "^1.5.4",
|
|
46
46
|
"@applitools/test-server": "^1.3.5",
|
|
47
47
|
"@applitools/test-utils": "^1.5.17",
|
|
48
48
|
"@types/node": "^12.20.55",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"webdriver": "^7.31.1"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
|
-
"@applitools/core-base": "1.
|
|
53
|
+
"@applitools/core-base": "1.31.1"
|
|
54
54
|
},
|
|
55
55
|
"engines": {
|
|
56
56
|
"node": ">=12.13.0"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BrokerServerSettings } from '../types';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import type { Logger } from '@applitools/logger';
|
|
3
|
+
import type { Req, Options } from '@applitools/req';
|
|
4
4
|
export type ReqBrokerOptions = Options & {
|
|
5
5
|
name: string;
|
|
6
6
|
body: {
|