n8n-nodes-nvk-browser 1.0.21 → 1.0.23
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.
|
@@ -731,7 +731,6 @@ async function executeGetNetworkResponse(i, browserManager, returnData) {
|
|
|
731
731
|
}
|
|
732
732
|
}
|
|
733
733
|
async function executeBrowserHttpRequest(i, browserManager, returnData) {
|
|
734
|
-
const items = this.getInputData();
|
|
735
734
|
const profileId = this.getNodeParameter('profileId', i);
|
|
736
735
|
const method = this.getNodeParameter('method', i) || 'GET';
|
|
737
736
|
const url = this.getNodeParameter('url', i);
|
|
@@ -784,10 +783,11 @@ async function executeBrowserHttpRequest(i, browserManager, returnData) {
|
|
|
784
783
|
let body;
|
|
785
784
|
let contentType;
|
|
786
785
|
let useFormData = false;
|
|
786
|
+
let bodyContentType;
|
|
787
787
|
if (method === 'POST') {
|
|
788
788
|
const sendBody = this.getNodeParameter('sendBody', i);
|
|
789
789
|
if (sendBody) {
|
|
790
|
-
|
|
790
|
+
bodyContentType = this.getNodeParameter('bodyContentType', i);
|
|
791
791
|
if (bodyContentType === 'json') {
|
|
792
792
|
const jsonBody = this.getNodeParameter('jsonBody', i);
|
|
793
793
|
body = jsonBody;
|
|
@@ -799,26 +799,37 @@ async function executeBrowserHttpRequest(i, browserManager, returnData) {
|
|
|
799
799
|
contentType = 'text/plain';
|
|
800
800
|
}
|
|
801
801
|
else if (bodyContentType === 'binary') {
|
|
802
|
-
// Handle n8n Binary File
|
|
802
|
+
// Handle n8n Binary File - similar to n8n HTTP Request
|
|
803
803
|
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i);
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
|
|
804
|
+
const itemBinaryData = this.helpers.assertBinaryData(i, binaryPropertyName);
|
|
805
|
+
let uploadData;
|
|
806
|
+
let contentLength;
|
|
807
|
+
if (itemBinaryData.id) {
|
|
808
|
+
// Binary data is stored as stream/file
|
|
809
|
+
const binaryStream = await this.helpers.getBinaryStream(itemBinaryData.id);
|
|
810
|
+
const chunks = [];
|
|
811
|
+
for await (const chunk of binaryStream) {
|
|
812
|
+
chunks.push(chunk);
|
|
813
|
+
}
|
|
814
|
+
uploadData = Buffer.concat(chunks);
|
|
815
|
+
contentLength = uploadData.length;
|
|
816
|
+
}
|
|
817
|
+
else {
|
|
818
|
+
// Binary data is in memory
|
|
819
|
+
uploadData = Buffer.from(itemBinaryData.data, 'base64');
|
|
820
|
+
contentLength = uploadData.length;
|
|
807
821
|
}
|
|
808
|
-
const binaryData = item.binary[binaryPropertyName];
|
|
809
|
-
const binaryBuffer = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName);
|
|
810
822
|
// Convert binary to base64 for transfer to browser context
|
|
811
|
-
const base64Data =
|
|
812
|
-
const mimeType =
|
|
813
|
-
|
|
814
|
-
// We'll send this to browser context to create FormData
|
|
823
|
+
const base64Data = uploadData.toString('base64');
|
|
824
|
+
const mimeType = itemBinaryData.mimeType || 'application/octet-stream';
|
|
825
|
+
// Send binary data directly (not as FormData)
|
|
815
826
|
body = JSON.stringify({
|
|
816
827
|
type: 'binary',
|
|
817
828
|
data: base64Data,
|
|
818
829
|
mimeType,
|
|
819
|
-
|
|
830
|
+
contentLength,
|
|
820
831
|
});
|
|
821
|
-
|
|
832
|
+
contentType = mimeType;
|
|
822
833
|
}
|
|
823
834
|
else if (bodyContentType === 'formData') {
|
|
824
835
|
const formDataParam = this.getNodeParameter('formData', i);
|
|
@@ -830,21 +841,31 @@ async function executeBrowserHttpRequest(i, browserManager, returnData) {
|
|
|
830
841
|
for (const param of formDataParam.parameter) {
|
|
831
842
|
if (param.name) {
|
|
832
843
|
if (param.parameterType === 'binary') {
|
|
844
|
+
// Handle binary file in form data - similar to n8n HTTP Request
|
|
833
845
|
const binaryPropertyName = param.binaryPropertyName || 'data';
|
|
834
|
-
const
|
|
835
|
-
|
|
836
|
-
|
|
846
|
+
const itemBinaryData = this.helpers.assertBinaryData(i, binaryPropertyName);
|
|
847
|
+
let uploadData;
|
|
848
|
+
if (itemBinaryData.id) {
|
|
849
|
+
// Binary data is stored as stream/file
|
|
850
|
+
const binaryStream = await this.helpers.getBinaryStream(itemBinaryData.id);
|
|
851
|
+
const chunks = [];
|
|
852
|
+
for await (const chunk of binaryStream) {
|
|
853
|
+
chunks.push(chunk);
|
|
854
|
+
}
|
|
855
|
+
uploadData = Buffer.concat(chunks);
|
|
856
|
+
}
|
|
857
|
+
else {
|
|
858
|
+
// Binary data is in memory
|
|
859
|
+
uploadData = Buffer.from(itemBinaryData.data, 'base64');
|
|
837
860
|
}
|
|
838
|
-
const
|
|
839
|
-
const binaryBuffer = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName);
|
|
840
|
-
const base64Data = binaryBuffer.toString('base64');
|
|
861
|
+
const base64Data = uploadData.toString('base64');
|
|
841
862
|
formDataItems.push({
|
|
842
863
|
name: param.name,
|
|
843
864
|
type: 'binary',
|
|
844
865
|
binaryData: {
|
|
845
866
|
data: base64Data,
|
|
846
|
-
mimeType:
|
|
847
|
-
fileName:
|
|
867
|
+
mimeType: itemBinaryData.mimeType || 'application/octet-stream',
|
|
868
|
+
fileName: itemBinaryData.fileName || 'file',
|
|
848
869
|
},
|
|
849
870
|
});
|
|
850
871
|
}
|
|
@@ -878,8 +899,16 @@ async function executeBrowserHttpRequest(i, browserManager, returnData) {
|
|
|
878
899
|
}
|
|
879
900
|
}
|
|
880
901
|
}
|
|
881
|
-
//
|
|
882
|
-
|
|
902
|
+
// Set Content-Type header
|
|
903
|
+
// For binary body, set content-type and content-length
|
|
904
|
+
if (bodyContentType === 'binary' && typeof body === 'string') {
|
|
905
|
+
const bodyData = JSON.parse(body);
|
|
906
|
+
if (bodyData.type === 'binary') {
|
|
907
|
+
browserHeaders['Content-Type'] = bodyData.mimeType || 'application/octet-stream';
|
|
908
|
+
browserHeaders['Content-Length'] = String(bodyData.contentLength || 0);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
else if (contentType && !useFormData) {
|
|
883
912
|
browserHeaders['Content-Type'] = contentType;
|
|
884
913
|
}
|
|
885
914
|
const response = await page.evaluate(async (requestData) => {
|
|
@@ -888,55 +917,62 @@ async function executeBrowserHttpRequest(i, browserManager, returnData) {
|
|
|
888
917
|
headers: requestData.headers,
|
|
889
918
|
};
|
|
890
919
|
if (requestData.body) {
|
|
891
|
-
if (
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
920
|
+
if (typeof requestData.body === 'string') {
|
|
921
|
+
try {
|
|
922
|
+
const bodyObj = JSON.parse(requestData.body);
|
|
923
|
+
if (bodyObj.type === 'binary' && bodyObj.data) {
|
|
924
|
+
// Binary body - send directly as ArrayBuffer
|
|
925
|
+
const binaryData = bodyObj.data;
|
|
926
|
+
// Convert base64 to ArrayBuffer
|
|
927
|
+
const binaryString = atob(binaryData);
|
|
928
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
929
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
930
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
931
|
+
}
|
|
932
|
+
fetchOptions.body = bytes.buffer;
|
|
933
|
+
// Content-Type and Content-Length are already set in headers
|
|
903
934
|
}
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
935
|
+
else if (bodyObj.type === 'formData' && requestData.useFormData) {
|
|
936
|
+
// FormData with mixed text and binary
|
|
937
|
+
const formData = new FormData();
|
|
938
|
+
const items = bodyObj.items || [];
|
|
939
|
+
for (const item of items) {
|
|
940
|
+
if (item.type === 'binary' && item.binaryData && item.binaryData.data) {
|
|
941
|
+
const binaryData = item.binaryData.data;
|
|
942
|
+
const mimeType = item.binaryData.mimeType || 'application/octet-stream';
|
|
943
|
+
const fileName = item.binaryData.fileName || 'file';
|
|
944
|
+
// Convert base64 to blob
|
|
945
|
+
const binaryString = atob(binaryData);
|
|
946
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
947
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
948
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
949
|
+
}
|
|
950
|
+
const blob = new Blob([bytes], { type: mimeType });
|
|
951
|
+
const file = new File([blob], fileName, { type: mimeType });
|
|
952
|
+
formData.append(item.name, file);
|
|
953
|
+
}
|
|
954
|
+
else {
|
|
955
|
+
formData.append(item.name, item.value || '');
|
|
922
956
|
}
|
|
923
|
-
const blob = new Blob([bytes], { type: mimeType });
|
|
924
|
-
const file = new File([blob], fileName, { type: mimeType });
|
|
925
|
-
formData.append(item.name, file);
|
|
926
957
|
}
|
|
927
|
-
|
|
928
|
-
|
|
958
|
+
fetchOptions.body = formData;
|
|
959
|
+
// Remove Content-Type header when using FormData (browser will set it with boundary)
|
|
960
|
+
if (fetchOptions.headers && typeof fetchOptions.headers === 'object' && !Array.isArray(fetchOptions.headers)) {
|
|
961
|
+
delete fetchOptions.headers['Content-Type'];
|
|
929
962
|
}
|
|
930
963
|
}
|
|
931
|
-
|
|
964
|
+
else {
|
|
965
|
+
// Regular string body
|
|
966
|
+
fetchOptions.body = typeof requestData.body === 'string' ? requestData.body : String(requestData.body);
|
|
967
|
+
}
|
|
932
968
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
969
|
+
catch {
|
|
970
|
+
// Not JSON, treat as regular string
|
|
971
|
+
fetchOptions.body = typeof requestData.body === 'string' ? requestData.body : String(requestData.body);
|
|
936
972
|
}
|
|
937
973
|
}
|
|
938
974
|
else {
|
|
939
|
-
fetchOptions.body = requestData.body;
|
|
975
|
+
fetchOptions.body = typeof requestData.body === 'string' ? requestData.body : String(requestData.body);
|
|
940
976
|
}
|
|
941
977
|
}
|
|
942
978
|
const response = await fetch(requestData.url, fetchOptions);
|
|
@@ -161,13 +161,63 @@ class MoveAndClick {
|
|
|
161
161
|
const raceBlockContent = clickMatch[1];
|
|
162
162
|
const clickOptionsContent = clickMatch[2] || '';
|
|
163
163
|
// Extract locators from race block
|
|
164
|
+
// Improved regex to handle:
|
|
165
|
+
// - Single quotes, double quotes, or template literals
|
|
166
|
+
// - Multi-line strings
|
|
167
|
+
// - Escaped quotes
|
|
168
|
+
// - Complex selectors with nested quotes
|
|
164
169
|
const locators = [];
|
|
165
|
-
|
|
170
|
+
// Try multiple patterns to extract locators
|
|
171
|
+
// Pattern 1: Standard locator with quotes
|
|
172
|
+
const locatorPattern1 = /(?:targetPage|page)\.locator\((['"`])((?:(?!\1)[^\\]|\\.|[\r\n])*?)\1\)/g;
|
|
166
173
|
let locatorMatch;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
174
|
+
// Reset regex
|
|
175
|
+
locatorPattern1.lastIndex = 0;
|
|
176
|
+
while ((locatorMatch = locatorPattern1.exec(raceBlockContent)) !== null) {
|
|
177
|
+
const selectorValue = locatorMatch[2]
|
|
178
|
+
.replace(/\\(.)/g, '$1') // Unescape
|
|
179
|
+
.replace(/[\r\n]/g, ' ') // Replace newlines with spaces
|
|
180
|
+
.trim();
|
|
181
|
+
if (selectorValue) {
|
|
182
|
+
try {
|
|
183
|
+
if (typeof page.locator === 'function') {
|
|
184
|
+
locators.push(page.locator(selectorValue));
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
// Fallback: create a simple locator-like object
|
|
188
|
+
// This will be handled by fallback logic later
|
|
189
|
+
locators.push({ selector: selectorValue, isFallback: true });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
// If locator creation fails, still add selector for fallback
|
|
194
|
+
locators.push({ selector: selectorValue, isFallback: true, error: err });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// If no locators found with pattern 1, try pattern 2 (handles more complex cases)
|
|
199
|
+
if (locators.length === 0) {
|
|
200
|
+
// Pattern 2: More flexible, handles nested structures
|
|
201
|
+
const locatorPattern2 = /\.locator\((['"`])((?:(?!\1)[^\\]|\\.|[\r\n])*?)\1\)/g;
|
|
202
|
+
locatorPattern2.lastIndex = 0;
|
|
203
|
+
while ((locatorMatch = locatorPattern2.exec(raceBlockContent)) !== null) {
|
|
204
|
+
const selectorValue = locatorMatch[2]
|
|
205
|
+
.replace(/\\(.)/g, '$1')
|
|
206
|
+
.replace(/[\r\n]/g, ' ')
|
|
207
|
+
.trim();
|
|
208
|
+
if (selectorValue) {
|
|
209
|
+
try {
|
|
210
|
+
if (typeof page.locator === 'function') {
|
|
211
|
+
locators.push(page.locator(selectorValue));
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
locators.push({ selector: selectorValue, isFallback: true });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
locators.push({ selector: selectorValue, isFallback: true, error: err });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
171
221
|
}
|
|
172
222
|
}
|
|
173
223
|
if (locators.length > 0) {
|
|
@@ -200,14 +250,51 @@ class MoveAndClick {
|
|
|
200
250
|
while ((fillMatch = fillBlockPattern.exec(selector)) !== null) {
|
|
201
251
|
const raceBlockContent = fillMatch[1];
|
|
202
252
|
const fillTextValue = fillMatch[2].replace(/\\(.)/g, '$1'); // Unescape
|
|
203
|
-
// Extract locators from race block
|
|
253
|
+
// Extract locators from race block - same improved logic as click blocks
|
|
204
254
|
const locators = [];
|
|
205
|
-
const
|
|
255
|
+
const locatorPattern1 = /(?:targetPage|page)\.locator\((['"`])((?:(?!\1)[^\\]|\\.|[\r\n])*?)\1\)/g;
|
|
206
256
|
let locatorMatch;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
257
|
+
locatorPattern1.lastIndex = 0;
|
|
258
|
+
while ((locatorMatch = locatorPattern1.exec(raceBlockContent)) !== null) {
|
|
259
|
+
const selectorValue = locatorMatch[2]
|
|
260
|
+
.replace(/\\(.)/g, '$1')
|
|
261
|
+
.replace(/[\r\n]/g, ' ')
|
|
262
|
+
.trim();
|
|
263
|
+
if (selectorValue) {
|
|
264
|
+
try {
|
|
265
|
+
if (typeof page.locator === 'function') {
|
|
266
|
+
locators.push(page.locator(selectorValue));
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
locators.push({ selector: selectorValue, isFallback: true });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
locators.push({ selector: selectorValue, isFallback: true, error: err });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (locators.length === 0) {
|
|
278
|
+
const locatorPattern2 = /\.locator\((['"`])((?:(?!\1)[^\\]|\\.|[\r\n])*?)\1\)/g;
|
|
279
|
+
locatorPattern2.lastIndex = 0;
|
|
280
|
+
while ((locatorMatch = locatorPattern2.exec(raceBlockContent)) !== null) {
|
|
281
|
+
const selectorValue = locatorMatch[2]
|
|
282
|
+
.replace(/\\(.)/g, '$1')
|
|
283
|
+
.replace(/[\r\n]/g, ' ')
|
|
284
|
+
.trim();
|
|
285
|
+
if (selectorValue) {
|
|
286
|
+
try {
|
|
287
|
+
if (typeof page.locator === 'function') {
|
|
288
|
+
locators.push(page.locator(selectorValue));
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
locators.push({ selector: selectorValue, isFallback: true });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
catch (err) {
|
|
295
|
+
locators.push({ selector: selectorValue, isFallback: true, error: err });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
211
298
|
}
|
|
212
299
|
}
|
|
213
300
|
if (locators.length > 0) {
|
|
@@ -231,14 +318,51 @@ class MoveAndClick {
|
|
|
231
318
|
const raceBlockContent = singleBlockMatch[1];
|
|
232
319
|
const clickOptionsContent = singleBlockMatch[2] || '';
|
|
233
320
|
const fillTextValue = singleBlockMatch[3] ? singleBlockMatch[3].replace(/\\(.)/g, '$1') : null;
|
|
234
|
-
// Extract locators
|
|
321
|
+
// Extract locators - use improved regex
|
|
235
322
|
const locators = [];
|
|
236
|
-
const
|
|
323
|
+
const locatorPattern1 = /(?:targetPage|page)\.locator\((['"`])((?:(?!\1)[^\\]|\\.|[\r\n])*?)\1\)/g;
|
|
237
324
|
let locatorMatch;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
325
|
+
locatorPattern1.lastIndex = 0;
|
|
326
|
+
while ((locatorMatch = locatorPattern1.exec(raceBlockContent)) !== null) {
|
|
327
|
+
const selectorValue = locatorMatch[2]
|
|
328
|
+
.replace(/\\(.)/g, '$1')
|
|
329
|
+
.replace(/[\r\n]/g, ' ')
|
|
330
|
+
.trim();
|
|
331
|
+
if (selectorValue) {
|
|
332
|
+
try {
|
|
333
|
+
if (typeof page.locator === 'function') {
|
|
334
|
+
locators.push(page.locator(selectorValue));
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
locators.push({ selector: selectorValue, isFallback: true });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
catch (err) {
|
|
341
|
+
locators.push({ selector: selectorValue, isFallback: true, error: err });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (locators.length === 0) {
|
|
346
|
+
const locatorPattern2 = /\.locator\((['"`])((?:(?!\1)[^\\]|\\.|[\r\n])*?)\1\)/g;
|
|
347
|
+
locatorPattern2.lastIndex = 0;
|
|
348
|
+
while ((locatorMatch = locatorPattern2.exec(raceBlockContent)) !== null) {
|
|
349
|
+
const selectorValue = locatorMatch[2]
|
|
350
|
+
.replace(/\\(.)/g, '$1')
|
|
351
|
+
.replace(/[\r\n]/g, ' ')
|
|
352
|
+
.trim();
|
|
353
|
+
if (selectorValue) {
|
|
354
|
+
try {
|
|
355
|
+
if (typeof page.locator === 'function') {
|
|
356
|
+
locators.push(page.locator(selectorValue));
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
locators.push({ selector: selectorValue, isFallback: true });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
catch (err) {
|
|
363
|
+
locators.push({ selector: selectorValue, isFallback: true, error: err });
|
|
364
|
+
}
|
|
365
|
+
}
|
|
242
366
|
}
|
|
243
367
|
}
|
|
244
368
|
if (locators.length > 0) {
|
|
@@ -274,101 +398,275 @@ class MoveAndClick {
|
|
|
274
398
|
}
|
|
275
399
|
for (let blockIndex = 0; blockIndex < actionBlocks.length; blockIndex++) {
|
|
276
400
|
const block = actionBlocks[blockIndex];
|
|
401
|
+
let actionSuccess = false;
|
|
402
|
+
let lastError = null;
|
|
277
403
|
try {
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if (
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
locator = locator.setTimeout(timeout);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
else {
|
|
289
|
-
locator = block.locators[0];
|
|
290
|
-
if (typeof locator.setTimeout === 'function') {
|
|
291
|
-
locator = locator.setTimeout(timeout);
|
|
404
|
+
// Separate real locators from fallback selectors
|
|
405
|
+
const realLocators = [];
|
|
406
|
+
const fallbackSelectors = [];
|
|
407
|
+
for (const loc of block.locators) {
|
|
408
|
+
if (loc && typeof loc === 'object' && loc.isFallback) {
|
|
409
|
+
if (loc.selector) {
|
|
410
|
+
fallbackSelectors.push(loc.selector);
|
|
292
411
|
}
|
|
293
412
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
locator = block.locators[0];
|
|
297
|
-
if (typeof locator.setTimeout === 'function') {
|
|
298
|
-
locator = locator.setTimeout(timeout);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
// Wait for element to be visible/actionable before executing
|
|
302
|
-
// Try to wait for the locator to be ready
|
|
303
|
-
try {
|
|
304
|
-
if (typeof locator.wait === 'function') {
|
|
305
|
-
await locator.wait({ timeout: timeout });
|
|
413
|
+
else if (loc && (typeof loc.click === 'function' || typeof loc.fill === 'function' || typeof loc.wait === 'function')) {
|
|
414
|
+
realLocators.push(loc);
|
|
306
415
|
}
|
|
307
|
-
else if (typeof
|
|
308
|
-
|
|
309
|
-
// This is a best-effort approach
|
|
416
|
+
else if (loc && typeof loc === 'object' && loc.selector) {
|
|
417
|
+
fallbackSelectors.push(loc.selector);
|
|
310
418
|
}
|
|
311
419
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
420
|
+
// Try Locator API first if we have real locators
|
|
421
|
+
if (realLocators.length > 0) {
|
|
422
|
+
try {
|
|
423
|
+
let locator;
|
|
424
|
+
if (realLocators.length > 1) {
|
|
425
|
+
const LocatorClass = puppeteer_core_1.default.Locator;
|
|
426
|
+
if (LocatorClass && typeof LocatorClass.race === 'function') {
|
|
427
|
+
locator = LocatorClass.race(realLocators);
|
|
428
|
+
if (typeof locator.setTimeout === 'function') {
|
|
429
|
+
locator = locator.setTimeout(timeout);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
locator = realLocators[0];
|
|
434
|
+
if (typeof locator.setTimeout === 'function') {
|
|
435
|
+
locator = locator.setTimeout(timeout);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
locator = realLocators[0];
|
|
441
|
+
if (typeof locator.setTimeout === 'function') {
|
|
442
|
+
locator = locator.setTimeout(timeout);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
// Wait for element to be visible/actionable
|
|
446
|
+
try {
|
|
447
|
+
if (typeof locator.wait === 'function') {
|
|
448
|
+
await locator.wait({ timeout: timeout });
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
catch (waitError) {
|
|
452
|
+
// Continue - element might already be ready
|
|
453
|
+
}
|
|
454
|
+
// Wait before action if specified
|
|
455
|
+
if (waitForClick > 0) {
|
|
456
|
+
await page.waitForTimeout(waitForClick);
|
|
457
|
+
}
|
|
458
|
+
// Add small delay between actions
|
|
459
|
+
if (blockIndex > 0) {
|
|
460
|
+
await page.waitForTimeout(100);
|
|
461
|
+
}
|
|
462
|
+
// Execute action with Locator API
|
|
463
|
+
if (block.type === 'fill' && block.fillText) {
|
|
464
|
+
if (typeof locator.fill === 'function') {
|
|
465
|
+
await locator.fill(block.fillText);
|
|
466
|
+
actionSuccess = true;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
// Click action
|
|
471
|
+
const clickOptions = {
|
|
472
|
+
button: button,
|
|
473
|
+
clickCount: clickCount,
|
|
474
|
+
};
|
|
475
|
+
if (block.offset) {
|
|
476
|
+
clickOptions.offset = block.offset;
|
|
477
|
+
}
|
|
478
|
+
if (typeof locator.click === 'function') {
|
|
479
|
+
// Scroll into view if needed
|
|
480
|
+
try {
|
|
481
|
+
if (typeof locator.scrollIntoViewIfNeeded === 'function') {
|
|
482
|
+
await locator.scrollIntoViewIfNeeded();
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
catch (scrollError) {
|
|
486
|
+
// Continue even if scroll fails
|
|
487
|
+
}
|
|
488
|
+
await locator.click(clickOptions);
|
|
489
|
+
await page.waitForTimeout(50);
|
|
490
|
+
actionSuccess = true;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
333
493
|
}
|
|
334
|
-
|
|
335
|
-
|
|
494
|
+
catch (locatorError) {
|
|
495
|
+
lastError = locatorError instanceof Error ? locatorError : new Error(String(locatorError));
|
|
496
|
+
// Will try fallback below
|
|
336
497
|
}
|
|
337
498
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
499
|
+
// If Locator API failed or we only have fallback selectors, try traditional API
|
|
500
|
+
if (!actionSuccess && (fallbackSelectors.length > 0 || realLocators.length === 0)) {
|
|
501
|
+
// Try each selector in order until one works
|
|
502
|
+
const selectorsToTry = fallbackSelectors.length > 0 ? fallbackSelectors :
|
|
503
|
+
realLocators.map((loc) => {
|
|
504
|
+
// Try to extract selector from locator if possible
|
|
505
|
+
if (loc && typeof loc === 'object') {
|
|
506
|
+
return loc.selector || loc.toString();
|
|
507
|
+
}
|
|
508
|
+
return null;
|
|
509
|
+
}).filter((s) => s !== null);
|
|
510
|
+
for (const selectorStr of selectorsToTry) {
|
|
511
|
+
if (!selectorStr)
|
|
512
|
+
continue;
|
|
349
513
|
try {
|
|
350
|
-
//
|
|
351
|
-
|
|
352
|
-
|
|
514
|
+
// Normalize selector - handle XPath and CSS
|
|
515
|
+
let normalizedSelector = selectorStr.trim();
|
|
516
|
+
let isXPath = false;
|
|
517
|
+
if (normalizedSelector.startsWith('::-p-xpath(')) {
|
|
518
|
+
// Extract XPath from ::-p-xpath(...)
|
|
519
|
+
const xpathMatch = normalizedSelector.match(/::-p-xpath\((.+)\)/);
|
|
520
|
+
if (xpathMatch) {
|
|
521
|
+
normalizedSelector = xpathMatch[1];
|
|
522
|
+
isXPath = true;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
else if (normalizedSelector.startsWith('::-p-aria(') || normalizedSelector.startsWith('::-p-text(')) {
|
|
526
|
+
// For aria and text selectors, try to find element using evaluate
|
|
527
|
+
try {
|
|
528
|
+
await page.waitForFunction((sel) => {
|
|
529
|
+
// Try to find element by text or aria label
|
|
530
|
+
if (sel.includes('::-p-text(')) {
|
|
531
|
+
const textMatch = sel.match(/::-p-text\((.+)\)/);
|
|
532
|
+
if (textMatch) {
|
|
533
|
+
const text = textMatch[1].replace(/^['"]|['"]$/g, '');
|
|
534
|
+
const xpath = `//*[contains(text(), "${text}")]`;
|
|
535
|
+
const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
536
|
+
return result.singleNodeValue;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return null;
|
|
540
|
+
}, { timeout: timeout }, selectorStr);
|
|
541
|
+
// Click using evaluate
|
|
542
|
+
await page.evaluate((sel) => {
|
|
543
|
+
if (sel.includes('::-p-text(')) {
|
|
544
|
+
const textMatch = sel.match(/::-p-text\((.+)\)/);
|
|
545
|
+
if (textMatch) {
|
|
546
|
+
const text = textMatch[1].replace(/^['"]|['"]$/g, '');
|
|
547
|
+
const xpath = `//*[contains(text(), "${text}")]`;
|
|
548
|
+
const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
549
|
+
const element = result.singleNodeValue;
|
|
550
|
+
if (element) {
|
|
551
|
+
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
552
|
+
element.click();
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return false;
|
|
558
|
+
}, selectorStr);
|
|
559
|
+
actionSuccess = true;
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
catch (e) {
|
|
563
|
+
// Continue to next selector
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
// Wait for selector
|
|
568
|
+
if (isXPath) {
|
|
569
|
+
await page.waitForFunction((xpath) => {
|
|
570
|
+
const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
571
|
+
return result.singleNodeValue !== null;
|
|
572
|
+
}, { timeout: timeout }, normalizedSelector);
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
await page.waitForSelector(normalizedSelector, { timeout: timeout, visible: true });
|
|
353
576
|
}
|
|
577
|
+
// Wait before action
|
|
578
|
+
if (waitForClick > 0) {
|
|
579
|
+
await page.waitForTimeout(waitForClick);
|
|
580
|
+
}
|
|
581
|
+
// Scroll into view
|
|
582
|
+
await page.evaluate((sel, isXPath) => {
|
|
583
|
+
let element = null;
|
|
584
|
+
if (isXPath) {
|
|
585
|
+
const result = document.evaluate(sel, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
586
|
+
element = result.singleNodeValue;
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
element = document.querySelector(sel);
|
|
590
|
+
}
|
|
591
|
+
if (element) {
|
|
592
|
+
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
593
|
+
}
|
|
594
|
+
}, normalizedSelector, isXPath);
|
|
595
|
+
// Execute action
|
|
596
|
+
if (block.type === 'fill' && block.fillText) {
|
|
597
|
+
if (isXPath) {
|
|
598
|
+
await page.evaluate((xpath, text) => {
|
|
599
|
+
const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
600
|
+
const element = result.singleNodeValue;
|
|
601
|
+
if (element) {
|
|
602
|
+
element.value = text;
|
|
603
|
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
604
|
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
605
|
+
}
|
|
606
|
+
}, normalizedSelector, block.fillText);
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
await page.type(normalizedSelector, block.fillText, { delay: 10 });
|
|
610
|
+
}
|
|
611
|
+
actionSuccess = true;
|
|
612
|
+
}
|
|
613
|
+
else {
|
|
614
|
+
// Click action
|
|
615
|
+
const clickOptions = {
|
|
616
|
+
button: button,
|
|
617
|
+
clickCount: clickCount,
|
|
618
|
+
};
|
|
619
|
+
if (block.offset) {
|
|
620
|
+
clickOptions.offset = block.offset;
|
|
621
|
+
}
|
|
622
|
+
if (isXPath) {
|
|
623
|
+
await page.evaluate((xpath, offset) => {
|
|
624
|
+
const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
625
|
+
const element = result.singleNodeValue;
|
|
626
|
+
if (element) {
|
|
627
|
+
const rect = element.getBoundingClientRect();
|
|
628
|
+
const x = offset ? rect.left + offset.x : rect.left + rect.width / 2;
|
|
629
|
+
const y = offset ? rect.top + offset.y : rect.top + rect.height / 2;
|
|
630
|
+
const event = new MouseEvent('click', {
|
|
631
|
+
view: window,
|
|
632
|
+
bubbles: true,
|
|
633
|
+
cancelable: true,
|
|
634
|
+
clientX: x,
|
|
635
|
+
clientY: y,
|
|
636
|
+
button: 0,
|
|
637
|
+
});
|
|
638
|
+
element.dispatchEvent(event);
|
|
639
|
+
}
|
|
640
|
+
}, normalizedSelector, block.offset);
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
await page.click(normalizedSelector, clickOptions);
|
|
644
|
+
}
|
|
645
|
+
await page.waitForTimeout(50);
|
|
646
|
+
actionSuccess = true;
|
|
647
|
+
}
|
|
648
|
+
// Success - break out of selector loop
|
|
649
|
+
break;
|
|
354
650
|
}
|
|
355
|
-
catch (
|
|
356
|
-
|
|
651
|
+
catch (selectorError) {
|
|
652
|
+
lastError = selectorError instanceof Error ? selectorError : new Error(String(selectorError));
|
|
653
|
+
// Try next selector
|
|
654
|
+
continue;
|
|
357
655
|
}
|
|
358
|
-
await locator.click(clickOptions);
|
|
359
|
-
// Wait a bit after click to ensure action is processed
|
|
360
|
-
await page.waitForTimeout(50);
|
|
361
|
-
executedActionsInfo.push({
|
|
362
|
-
type: 'click',
|
|
363
|
-
success: true,
|
|
364
|
-
message: `Click performed successfully (block ${blockIndex + 1}/${actionBlocks.length})`,
|
|
365
|
-
});
|
|
366
|
-
actionType = 'click';
|
|
367
|
-
}
|
|
368
|
-
else {
|
|
369
|
-
throw new Error('Locator.click is not available');
|
|
370
656
|
}
|
|
371
657
|
}
|
|
658
|
+
// Report result
|
|
659
|
+
if (actionSuccess) {
|
|
660
|
+
executedActionsInfo.push({
|
|
661
|
+
type: block.type,
|
|
662
|
+
success: true,
|
|
663
|
+
message: `${block.type === 'fill' ? 'Fill' : 'Click'} performed successfully (block ${blockIndex + 1}/${actionBlocks.length})`,
|
|
664
|
+
});
|
|
665
|
+
actionType = block.type;
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
throw lastError || new Error(`Failed to execute ${block.type} action: All selectors failed`);
|
|
669
|
+
}
|
|
372
670
|
}
|
|
373
671
|
catch (blockError) {
|
|
374
672
|
const errorMessage = blockError instanceof Error ? blockError.message : String(blockError);
|
package/package.json
CHANGED