n8n-nodes-nvk-browser 1.0.6 → 1.0.8

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.
@@ -99,8 +99,10 @@ class MoveAndClick {
99
99
  const timeout = this.getNodeParameter('timeout', i) || 30000;
100
100
  const tabIndex = this.getNodeParameter('tabIndex', i) || 0;
101
101
  const autoStart = this.getNodeParameter('autoStart', i) || false;
102
+ const selectorTrimmed = selector.trim();
102
103
  // Track action type for return value (default to click)
103
104
  let actionType = 'click';
105
+ let executedActionsInfo = null;
104
106
  let instance = browserManager.getInstance(profileId);
105
107
  // Auto start profile nếu chưa chạy và option được bật
106
108
  if (!instance && autoStart) {
@@ -139,50 +141,62 @@ class MoveAndClick {
139
141
  const waitForClick = this.getNodeParameter('waitForClick', i) || 500;
140
142
  const button = this.getNodeParameter('button', i) || 'left';
141
143
  const clickCount = this.getNodeParameter('clickCount', i) || 1;
142
- // Parse selector - check if it's code with Locator.race() or simple selector
143
- const selectorTrimmed = selector.trim();
144
144
  // Check if selector contains Locator.race pattern or is a code block
145
145
  if (selectorTrimmed.includes('Locator.race') || selectorTrimmed.includes('puppeteer.Locator')) {
146
146
  // Execute as code - user provided full code
147
- // Extract the selector part from the code or execute the code directly
147
+ // Parse and execute ALL blocks automatically
148
148
  try {
149
- // Parse selectors from the code pattern
150
- const locators = [];
151
- // Find Locator.race blocks - check for both .click() and .fill() actions
152
- // Improved regex to handle multiline code from Chrome Recorder
153
- // Match pattern: puppeteer.Locator.race([...]).setTimeout(...).click(...) or .fill(...)
154
- const clickBlockPattern = /puppeteer\.Locator\.race\s*\(\s*\[([\s\S]*?)\]\s*\)[\s\S]*?\.click\s*\(/;
155
- const fillBlockPattern = /puppeteer\.Locator\.race\s*\(\s*\[([\s\S]*?)\]\s*\)[\s\S]*?\.fill\s*\(/;
156
- const clickBlockMatch = selector.match(clickBlockPattern);
157
- const fillBlockMatch = selector.match(fillBlockPattern);
158
- let raceBlockContent = null;
159
- let fillText = null;
160
- // Prioritize fill if both exist (fill usually comes after click)
161
- if (fillBlockMatch && fillBlockMatch[1]) {
162
- raceBlockContent = fillBlockMatch[1];
163
- actionType = 'fill';
164
- // Extract fill text value
165
- const fillTextMatch = selector.match(/\.fill\s*\(\s*['"]([^'"]*(?:\\.[^'"]*)*)['"]\s*\)/);
166
- if (fillTextMatch && fillTextMatch[1]) {
167
- fillText = fillTextMatch[1].replace(/\\(.)/g, '$1'); // Unescape
149
+ const actionBlocks = [];
150
+ // Find all click blocks
151
+ // Improved regex to handle closing braces - semicolon is optional (for last block)
152
+ const clickBlockPattern = /puppeteer\.Locator\.race\s*\(\s*\[([\s\S]*?)\]\s*\)[\s\S]*?\.click\s*\(([\s\S]*?)\)(?:\s*;|\s*$)/g;
153
+ let clickMatch;
154
+ let clickIndex = 0;
155
+ // Reset regex lastIndex to ensure we find all matches
156
+ clickBlockPattern.lastIndex = 0;
157
+ while ((clickMatch = clickBlockPattern.exec(selector)) !== null) {
158
+ const raceBlockContent = clickMatch[1];
159
+ const clickOptionsContent = clickMatch[2] || '';
160
+ // Extract locators from race block
161
+ const locators = [];
162
+ const locatorPattern = /(?:targetPage|page)\.locator\((['"])((?:(?!\1)[^\\]|\\.)*)\1\)/g;
163
+ let locatorMatch;
164
+ while ((locatorMatch = locatorPattern.exec(raceBlockContent)) !== null) {
165
+ const selectorValue = locatorMatch[2].replace(/\\(.)/g, '$1'); // Unescape
166
+ if (selectorValue && typeof page.locator === 'function') {
167
+ locators.push(page.locator(selectorValue));
168
+ }
168
169
  }
169
- }
170
- else if (clickBlockMatch && clickBlockMatch[1]) {
171
- raceBlockContent = clickBlockMatch[1];
172
- actionType = 'click';
173
- }
174
- else {
175
- // Fallback: find any Locator.race block (even without .click() or .fill())
176
- const anyRaceBlockMatch = selector.match(/puppeteer\.Locator\.race\s*\(\s*\[([\s\S]*?)\]\s*\)/);
177
- if (anyRaceBlockMatch && anyRaceBlockMatch[1]) {
178
- raceBlockContent = anyRaceBlockMatch[1];
179
- actionType = 'click'; // Default to click
170
+ if (locators.length > 0) {
171
+ // Parse offset from click options
172
+ let offset;
173
+ const offsetMatch = clickOptionsContent.match(/offset:\s*\{[\s\S]*?x:\s*(\d+)[\s\S]*?y:\s*(\d+)[\s\S]*?\}/);
174
+ if (offsetMatch && offsetMatch[1] && offsetMatch[2]) {
175
+ offset = {
176
+ x: parseInt(offsetMatch[1], 10),
177
+ y: parseInt(offsetMatch[2], 10),
178
+ };
179
+ }
180
+ actionBlocks.push({
181
+ type: 'click',
182
+ locators,
183
+ offset,
184
+ index: clickMatch.index || clickIndex++,
185
+ });
180
186
  }
181
187
  }
182
- if (raceBlockContent) {
183
- // Extract all locator calls from the race block
184
- // Handle both single and double quotes, and handle escaped quotes
185
- // Improved regex to handle complex selectors with parentheses and special characters
188
+ // Find all fill blocks
189
+ // Improved regex to handle closing braces - semicolon is optional (for last block)
190
+ const fillBlockPattern = /puppeteer\.Locator\.race\s*\(\s*\[([\s\S]*?)\]\s*\)[\s\S]*?\.fill\s*\(\s*['"]([^'"]*(?:\\.[^'"]*)*)['"]\s*\)(?:\s*;|\s*$)/g;
191
+ let fillMatch;
192
+ let fillIndex = 0;
193
+ // Reset regex lastIndex to ensure we find all matches
194
+ fillBlockPattern.lastIndex = 0;
195
+ while ((fillMatch = fillBlockPattern.exec(selector)) !== null) {
196
+ const raceBlockContent = fillMatch[1];
197
+ const fillTextValue = fillMatch[2].replace(/\\(.)/g, '$1'); // Unescape
198
+ // Extract locators from race block
199
+ const locators = [];
186
200
  const locatorPattern = /(?:targetPage|page)\.locator\((['"])((?:(?!\1)[^\\]|\\.)*)\1\)/g;
187
201
  let locatorMatch;
188
202
  while ((locatorMatch = locatorPattern.exec(raceBlockContent)) !== null) {
@@ -191,118 +205,134 @@ class MoveAndClick {
191
205
  locators.push(page.locator(selectorValue));
192
206
  }
193
207
  }
194
- }
195
- if (locators.length === 0) {
196
- // Fallback: Parse CSS selectors (lines with targetPage.locator('...') or page.locator('...'))
197
- const cssMatches = selector.match(/(?:targetPage|page)\.locator\(['"]([^'"]+)['"]\)/g);
198
- if (cssMatches) {
199
- cssMatches.forEach(match => {
200
- const cssSel = match.match(/['"]([^'"]+)['"]/)?.[1];
201
- if (cssSel && !cssSel.startsWith('::-p-xpath')) {
202
- // Use page.locator if available, otherwise fallback
203
- if (typeof page.locator === 'function') {
204
- locators.push(page.locator(cssSel));
205
- }
206
- }
207
- });
208
- }
209
- // Parse XPath selectors (::-p-xpath(...))
210
- const xpathMatches = selector.match(/::-p-xpath\(([^)]+)\)/g);
211
- if (xpathMatches) {
212
- xpathMatches.forEach(match => {
213
- const xpath = match.match(/::-p-xpath\(([^)]+)\)/)?.[1];
214
- if (xpath && typeof page.locator === 'function') {
215
- locators.push(page.locator(`::-p-xpath(${xpath})`));
216
- }
208
+ if (locators.length > 0) {
209
+ actionBlocks.push({
210
+ type: 'fill',
211
+ locators,
212
+ fillText: fillTextValue,
213
+ index: fillMatch.index || fillIndex++,
217
214
  });
218
215
  }
219
216
  }
220
- // Also check for >CSS> and >XPATH> prefixes
221
- if (selector.includes('>CSS>') || selector.includes('>XPATH>')) {
222
- const parts = selector.split(/\s+/);
223
- parts.forEach(part => {
224
- if (part.startsWith('>CSS>') && typeof page.locator === 'function') {
225
- const cssSel = part.replace('>CSS>', '');
226
- locators.push(page.locator(cssSel));
227
- }
228
- else if (part.startsWith('>XPATH>') && typeof page.locator === 'function') {
229
- const xpath = part.replace('>XPATH>', '');
230
- locators.push(page.locator(`::-p-xpath(${xpath})`));
231
- }
232
- });
217
+ // Sort blocks by their position in code
218
+ actionBlocks.sort((a, b) => a.index - b.index);
219
+ // Execute all blocks sequentially
220
+ executedActionsInfo = [];
221
+ if (actionBlocks.length === 0) {
222
+ throw new Error('No valid Locator.race() blocks found in code. Please ensure your code contains puppeteer.Locator.race([...]).click() or .fill() patterns.');
233
223
  }
234
- if (locators.length > 0 && typeof page.locator === 'function') {
235
- // Use Locator.race() if multiple locators and API is available
236
- let locator;
237
- if (locators.length > 1) {
238
- // Check if Locator.race is available
239
- const LocatorClass = puppeteer_core_1.default.Locator;
240
- if (LocatorClass && typeof LocatorClass.race === 'function') {
241
- locator = LocatorClass.race(locators);
242
- if (typeof locator.setTimeout === 'function') {
243
- locator = locator.setTimeout(timeout);
224
+ for (let blockIndex = 0; blockIndex < actionBlocks.length; blockIndex++) {
225
+ const block = actionBlocks[blockIndex];
226
+ try {
227
+ // Create locator from block's locators
228
+ let locator;
229
+ if (block.locators.length > 1) {
230
+ const LocatorClass = puppeteer_core_1.default.Locator;
231
+ if (LocatorClass && typeof LocatorClass.race === 'function') {
232
+ locator = LocatorClass.race(block.locators);
233
+ if (typeof locator.setTimeout === 'function') {
234
+ locator = locator.setTimeout(timeout);
235
+ }
236
+ }
237
+ else {
238
+ locator = block.locators[0];
239
+ if (typeof locator.setTimeout === 'function') {
240
+ locator = locator.setTimeout(timeout);
241
+ }
244
242
  }
245
243
  }
246
244
  else {
247
- // Fallback: try first locator
248
- locator = locators[0];
245
+ locator = block.locators[0];
249
246
  if (typeof locator.setTimeout === 'function') {
250
247
  locator = locator.setTimeout(timeout);
251
248
  }
252
249
  }
253
- }
254
- else {
255
- locator = locators[0];
256
- if (typeof locator.setTimeout === 'function') {
257
- locator = locator.setTimeout(timeout);
258
- }
259
- }
260
- // Wait before clicking if specified
261
- if (waitForClick > 0) {
262
- await page.waitForTimeout(waitForClick);
263
- }
264
- // Execute action based on detected type
265
- if (actionType === 'fill' && fillText) {
266
- // Fill action
267
- if (typeof locator.fill === 'function') {
268
- await locator.fill(fillText);
250
+ // Wait for element to be visible/actionable before executing
251
+ // Try to wait for the locator to be ready
252
+ try {
253
+ if (typeof locator.wait === 'function') {
254
+ await locator.wait({ timeout: timeout });
255
+ }
256
+ else if (typeof page.waitForSelector === 'function') {
257
+ // Fallback: try to wait using first locator's selector if available
258
+ // This is a best-effort approach
259
+ }
269
260
  }
270
- else {
271
- throw new Error('Locator.fill is not available');
261
+ catch (waitError) {
262
+ // Continue even if wait fails - element might already be ready
272
263
  }
273
- }
274
- else {
275
- // Click action (default)
276
- // Parse offset from code if present (handle multiline)
277
- let offset;
278
- // Match offset in click options, handling multiline and whitespace
279
- const offsetMatch = selector.match(/\.click\s*\(\s*\{[\s\S]*?offset:\s*\{[\s\S]*?x:\s*(\d+)[\s\S]*?y:\s*(\d+)[\s\S]*?\}[\s\S]*?\}/);
280
- if (offsetMatch && offsetMatch[1] && offsetMatch[2]) {
281
- offset = {
282
- x: parseInt(offsetMatch[1], 10),
283
- y: parseInt(offsetMatch[2], 10),
284
- };
264
+ // Wait before action if specified
265
+ if (waitForClick > 0) {
266
+ await page.waitForTimeout(waitForClick);
285
267
  }
286
- // Click with options
287
- const clickOptions = {
288
- button: button,
289
- clickCount: clickCount,
290
- };
291
- // Add offset if found in code
292
- if (offset) {
293
- clickOptions.offset = offset;
268
+ // Add small delay between actions to ensure page state is stable
269
+ if (blockIndex > 0) {
270
+ await page.waitForTimeout(100); // Small delay between actions
294
271
  }
295
- if (typeof locator.click === 'function') {
296
- await locator.click(clickOptions);
272
+ // Execute action
273
+ if (block.type === 'fill' && block.fillText) {
274
+ if (typeof locator.fill === 'function') {
275
+ await locator.fill(block.fillText);
276
+ executedActionsInfo.push({
277
+ type: 'fill',
278
+ success: true,
279
+ message: `Fill "${block.fillText}" performed successfully`,
280
+ });
281
+ actionType = 'fill';
282
+ }
283
+ else {
284
+ throw new Error('Locator.fill is not available');
285
+ }
297
286
  }
298
287
  else {
299
- throw new Error('Locator.click is not available');
288
+ // Click action
289
+ const clickOptions = {
290
+ button: button,
291
+ clickCount: clickCount,
292
+ };
293
+ if (block.offset) {
294
+ clickOptions.offset = block.offset;
295
+ }
296
+ if (typeof locator.click === 'function') {
297
+ // Ensure element is actionable before clicking
298
+ try {
299
+ // Try to scroll into view if needed
300
+ if (typeof locator.scrollIntoViewIfNeeded === 'function') {
301
+ await locator.scrollIntoViewIfNeeded();
302
+ }
303
+ }
304
+ catch (scrollError) {
305
+ // Continue even if scroll fails
306
+ }
307
+ await locator.click(clickOptions);
308
+ // Wait a bit after click to ensure action is processed
309
+ await page.waitForTimeout(50);
310
+ executedActionsInfo.push({
311
+ type: 'click',
312
+ success: true,
313
+ message: `Click performed successfully (block ${blockIndex + 1}/${actionBlocks.length})`,
314
+ });
315
+ actionType = 'click';
316
+ }
317
+ else {
318
+ throw new Error('Locator.click is not available');
319
+ }
300
320
  }
301
321
  }
322
+ catch (blockError) {
323
+ const errorMessage = blockError instanceof Error ? blockError.message : String(blockError);
324
+ executedActionsInfo.push({
325
+ type: block.type,
326
+ success: false,
327
+ message: `Block ${blockIndex + 1}/${actionBlocks.length} failed: ${errorMessage}`,
328
+ });
329
+ // Continue with next block even if one fails
330
+ }
302
331
  }
303
- else {
304
- // Fallback to simple selector parsing
305
- throw new Error('Could not parse Locator.race() from code. Please ensure your code contains a valid puppeteer.Locator.race([...]) pattern with targetPage.locator() or page.locator() calls.');
332
+ // Update actionType to reflect last executed action
333
+ if (executedActionsInfo.length > 0) {
334
+ const lastAction = executedActionsInfo[executedActionsInfo.length - 1];
335
+ actionType = lastAction.type;
306
336
  }
307
337
  }
308
338
  catch (error) {
@@ -370,14 +400,27 @@ class MoveAndClick {
370
400
  }
371
401
  }
372
402
  }
403
+ // Prepare return data
404
+ const returnJson = {
405
+ success: true,
406
+ selector,
407
+ method: clickMethod,
408
+ action: actionType || 'click',
409
+ };
410
+ // If we executed multiple blocks, include details
411
+ if (executedActionsInfo && executedActionsInfo.length > 0) {
412
+ const allSuccess = executedActionsInfo.every(a => a.success);
413
+ returnJson.success = allSuccess;
414
+ returnJson.message = `Executed ${executedActionsInfo.length} action(s)`;
415
+ returnJson.actions = executedActionsInfo;
416
+ returnJson.totalActions = executedActionsInfo.length;
417
+ returnJson.successfulActions = executedActionsInfo.filter(a => a.success).length;
418
+ }
419
+ else {
420
+ returnJson.message = actionType === 'fill' ? 'Fill performed successfully' : 'Click performed successfully';
421
+ }
373
422
  returnData.push({
374
- json: {
375
- success: true,
376
- selector,
377
- method: clickMethod,
378
- action: actionType || 'click',
379
- message: actionType === 'fill' ? 'Fill performed successfully' : 'Click performed successfully',
380
- },
423
+ json: returnJson,
381
424
  });
382
425
  }
383
426
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-nvk-browser",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "n8n nodes for managing Chrome browser profiles and page interactions",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",