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
- const bodyContentType = this.getNodeParameter('bodyContentType', i);
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 item = items[i];
805
- if (!item.binary || !item.binary[binaryPropertyName]) {
806
- throw new Error(`Binary data not found for property "${binaryPropertyName}"`);
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 = binaryBuffer.toString('base64');
812
- const mimeType = binaryData.mimeType || 'application/octet-stream';
813
- const fileName = binaryData.fileName || 'file';
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
- fileName,
830
+ contentLength,
820
831
  });
821
- useFormData = true;
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 item = items[i];
835
- if (!item.binary || !item.binary[binaryPropertyName]) {
836
- throw new Error(`Binary data not found for property "${binaryPropertyName}"`);
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 binaryData = item.binary[binaryPropertyName];
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: binaryData.mimeType || 'application/octet-stream',
847
- fileName: binaryData.fileName || 'file',
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
- // Don't set Content-Type header if using FormData (browser will set it automatically with boundary)
882
- if (contentType && !useFormData) {
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 (requestData.useFormData && typeof requestData.body === 'object') {
892
- const formData = new FormData();
893
- if (requestData.body.type === 'binary' && requestData.body.data) {
894
- // Single binary file
895
- const binaryData = requestData.body.data;
896
- const mimeType = requestData.body.mimeType || 'application/octet-stream';
897
- const fileName = requestData.body.fileName || 'file';
898
- // Convert base64 to blob
899
- const binaryString = atob(binaryData);
900
- const bytes = new Uint8Array(binaryString.length);
901
- for (let i = 0; i < binaryString.length; i++) {
902
- bytes[i] = binaryString.charCodeAt(i);
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
- const blob = new Blob([bytes], { type: mimeType });
905
- const file = new File([blob], fileName, { type: mimeType });
906
- formData.append('file', file);
907
- fetchOptions.body = formData;
908
- }
909
- else if (requestData.body.type === 'formData') {
910
- // FormData with mixed text and binary
911
- const items = requestData.body.items || [];
912
- for (const item of items) {
913
- if (item.type === 'binary' && item.binaryData && item.binaryData.data) {
914
- const binaryData = item.binaryData.data;
915
- const mimeType = item.binaryData.mimeType || 'application/octet-stream';
916
- const fileName = item.binaryData.fileName || 'file';
917
- // Convert base64 to blob
918
- const binaryString = atob(binaryData);
919
- const bytes = new Uint8Array(binaryString.length);
920
- for (let i = 0; i < binaryString.length; i++) {
921
- bytes[i] = binaryString.charCodeAt(i);
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
- else {
928
- formData.append(item.name, item.value || '');
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
- fetchOptions.body = formData;
964
+ else {
965
+ // Regular string body
966
+ fetchOptions.body = typeof requestData.body === 'string' ? requestData.body : String(requestData.body);
967
+ }
932
968
  }
933
- // Remove Content-Type header when using FormData (browser will set it with boundary)
934
- if (fetchOptions.headers && typeof fetchOptions.headers === 'object' && !Array.isArray(fetchOptions.headers)) {
935
- delete fetchOptions.headers['Content-Type'];
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
- const locatorPattern = /(?:targetPage|page)\.locator\((['"])((?:(?!\1)[^\\]|\\.)*)\1\)/g;
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
- while ((locatorMatch = locatorPattern.exec(raceBlockContent)) !== null) {
168
- const selectorValue = locatorMatch[2].replace(/\\(.)/g, '$1'); // Unescape
169
- if (selectorValue && typeof page.locator === 'function') {
170
- locators.push(page.locator(selectorValue));
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 locatorPattern = /(?:targetPage|page)\.locator\((['"])((?:(?!\1)[^\\]|\\.)*)\1\)/g;
255
+ const locatorPattern1 = /(?:targetPage|page)\.locator\((['"`])((?:(?!\1)[^\\]|\\.|[\r\n])*?)\1\)/g;
206
256
  let locatorMatch;
207
- while ((locatorMatch = locatorPattern.exec(raceBlockContent)) !== null) {
208
- const selectorValue = locatorMatch[2].replace(/\\(.)/g, '$1'); // Unescape
209
- if (selectorValue && typeof page.locator === 'function') {
210
- locators.push(page.locator(selectorValue));
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 locatorPattern = /(?:targetPage|page)\.locator\((['"])((?:(?!\1)[^\\]|\\.)*)\1\)/g;
323
+ const locatorPattern1 = /(?:targetPage|page)\.locator\((['"`])((?:(?!\1)[^\\]|\\.|[\r\n])*?)\1\)/g;
237
324
  let locatorMatch;
238
- while ((locatorMatch = locatorPattern.exec(raceBlockContent)) !== null) {
239
- const selectorValue = locatorMatch[2].replace(/\\(.)/g, '$1');
240
- if (selectorValue && typeof page.locator === 'function') {
241
- locators.push(page.locator(selectorValue));
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
- // Create locator from block's locators
279
- let locator;
280
- if (block.locators.length > 1) {
281
- const LocatorClass = puppeteer_core_1.default.Locator;
282
- if (LocatorClass && typeof LocatorClass.race === 'function') {
283
- locator = LocatorClass.race(block.locators);
284
- if (typeof locator.setTimeout === 'function') {
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
- else {
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 page.waitForSelector === 'function') {
308
- // Fallback: try to wait using first locator's selector if available
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
- catch (waitError) {
313
- // Continue even if wait fails - element might already be ready
314
- }
315
- // Wait before action if specified
316
- if (waitForClick > 0) {
317
- await page.waitForTimeout(waitForClick);
318
- }
319
- // Add small delay between actions to ensure page state is stable
320
- if (blockIndex > 0) {
321
- await page.waitForTimeout(100); // Small delay between actions
322
- }
323
- // Execute action
324
- if (block.type === 'fill' && block.fillText) {
325
- if (typeof locator.fill === 'function') {
326
- await locator.fill(block.fillText);
327
- executedActionsInfo.push({
328
- type: 'fill',
329
- success: true,
330
- message: `Fill "${block.fillText}" performed successfully`,
331
- });
332
- actionType = 'fill';
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
- else {
335
- throw new Error('Locator.fill is not available');
494
+ catch (locatorError) {
495
+ lastError = locatorError instanceof Error ? locatorError : new Error(String(locatorError));
496
+ // Will try fallback below
336
497
  }
337
498
  }
338
- else {
339
- // Click action
340
- const clickOptions = {
341
- button: button,
342
- clickCount: clickCount,
343
- };
344
- if (block.offset) {
345
- clickOptions.offset = block.offset;
346
- }
347
- if (typeof locator.click === 'function') {
348
- // Ensure element is actionable before clicking
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
- // Try to scroll into view if needed
351
- if (typeof locator.scrollIntoViewIfNeeded === 'function') {
352
- await locator.scrollIntoViewIfNeeded();
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 (scrollError) {
356
- // Continue even if scroll fails
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-nvk-browser",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "description": "n8n nodes for managing Chrome browser profiles and page interactions with Puppeteer automation",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",