n8n-nodes-nvk-browser 1.0.6 → 1.0.7

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,56 @@ 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
+ const clickBlockPattern = /puppeteer\.Locator\.race\s*\(\s*\[([\s\S]*?)\]\s*\)[\s\S]*?\.click\s*\(([\s\S]*?)\)/g;
152
+ let clickMatch;
153
+ let clickIndex = 0;
154
+ while ((clickMatch = clickBlockPattern.exec(selector)) !== null) {
155
+ const raceBlockContent = clickMatch[1];
156
+ const clickOptionsContent = clickMatch[2] || '';
157
+ // Extract locators from race block
158
+ const locators = [];
159
+ const locatorPattern = /(?:targetPage|page)\.locator\((['"])((?:(?!\1)[^\\]|\\.)*)\1\)/g;
160
+ let locatorMatch;
161
+ while ((locatorMatch = locatorPattern.exec(raceBlockContent)) !== null) {
162
+ const selectorValue = locatorMatch[2].replace(/\\(.)/g, '$1'); // Unescape
163
+ if (selectorValue && typeof page.locator === 'function') {
164
+ locators.push(page.locator(selectorValue));
165
+ }
168
166
  }
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
167
+ if (locators.length > 0) {
168
+ // Parse offset from click options
169
+ let offset;
170
+ const offsetMatch = clickOptionsContent.match(/offset:\s*\{[\s\S]*?x:\s*(\d+)[\s\S]*?y:\s*(\d+)[\s\S]*?\}/);
171
+ if (offsetMatch && offsetMatch[1] && offsetMatch[2]) {
172
+ offset = {
173
+ x: parseInt(offsetMatch[1], 10),
174
+ y: parseInt(offsetMatch[2], 10),
175
+ };
176
+ }
177
+ actionBlocks.push({
178
+ type: 'click',
179
+ locators,
180
+ offset,
181
+ index: clickMatch.index || clickIndex++,
182
+ });
180
183
  }
181
184
  }
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
185
+ // Find all fill blocks
186
+ const fillBlockPattern = /puppeteer\.Locator\.race\s*\(\s*\[([\s\S]*?)\]\s*\)[\s\S]*?\.fill\s*\(\s*['"]([^'"]*(?:\\.[^'"]*)*)['"]\s*\)/g;
187
+ let fillMatch;
188
+ let fillIndex = 0;
189
+ while ((fillMatch = fillBlockPattern.exec(selector)) !== null) {
190
+ const raceBlockContent = fillMatch[1];
191
+ const fillTextValue = fillMatch[2].replace(/\\(.)/g, '$1'); // Unescape
192
+ // Extract locators from race block
193
+ const locators = [];
186
194
  const locatorPattern = /(?:targetPage|page)\.locator\((['"])((?:(?!\1)[^\\]|\\.)*)\1\)/g;
187
195
  let locatorMatch;
188
196
  while ((locatorMatch = locatorPattern.exec(raceBlockContent)) !== null) {
@@ -191,118 +199,102 @@ class MoveAndClick {
191
199
  locators.push(page.locator(selectorValue));
192
200
  }
193
201
  }
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
- }
202
+ if (locators.length > 0) {
203
+ actionBlocks.push({
204
+ type: 'fill',
205
+ locators,
206
+ fillText: fillTextValue,
207
+ index: fillMatch.index || fillIndex++,
217
208
  });
218
209
  }
219
210
  }
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
- });
211
+ // Sort blocks by their position in code
212
+ actionBlocks.sort((a, b) => a.index - b.index);
213
+ // Execute all blocks sequentially
214
+ executedActionsInfo = [];
215
+ if (actionBlocks.length === 0) {
216
+ throw new Error('No valid Locator.race() blocks found in code. Please ensure your code contains puppeteer.Locator.race([...]).click() or .fill() patterns.');
233
217
  }
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);
218
+ for (const block of actionBlocks) {
219
+ try {
220
+ // Create locator from block's locators
221
+ let locator;
222
+ if (block.locators.length > 1) {
223
+ const LocatorClass = puppeteer_core_1.default.Locator;
224
+ if (LocatorClass && typeof LocatorClass.race === 'function') {
225
+ locator = LocatorClass.race(block.locators);
226
+ if (typeof locator.setTimeout === 'function') {
227
+ locator = locator.setTimeout(timeout);
228
+ }
229
+ }
230
+ else {
231
+ locator = block.locators[0];
232
+ if (typeof locator.setTimeout === 'function') {
233
+ locator = locator.setTimeout(timeout);
234
+ }
244
235
  }
245
236
  }
246
237
  else {
247
- // Fallback: try first locator
248
- locator = locators[0];
238
+ locator = block.locators[0];
249
239
  if (typeof locator.setTimeout === 'function') {
250
240
  locator = locator.setTimeout(timeout);
251
241
  }
252
242
  }
253
- }
254
- else {
255
- locator = locators[0];
256
- if (typeof locator.setTimeout === 'function') {
257
- locator = locator.setTimeout(timeout);
243
+ // Wait before action if specified
244
+ if (waitForClick > 0) {
245
+ await page.waitForTimeout(waitForClick);
258
246
  }
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);
247
+ // Execute action
248
+ if (block.type === 'fill' && block.fillText) {
249
+ if (typeof locator.fill === 'function') {
250
+ await locator.fill(block.fillText);
251
+ executedActionsInfo.push({
252
+ type: 'fill',
253
+ success: true,
254
+ message: `Fill "${block.fillText}" performed successfully`,
255
+ });
256
+ actionType = 'fill';
257
+ }
258
+ else {
259
+ throw new Error('Locator.fill is not available');
260
+ }
269
261
  }
270
262
  else {
271
- throw new Error('Locator.fill is not available');
272
- }
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),
263
+ // Click action
264
+ const clickOptions = {
265
+ button: button,
266
+ clickCount: clickCount,
284
267
  };
285
- }
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;
294
- }
295
- if (typeof locator.click === 'function') {
296
- await locator.click(clickOptions);
297
- }
298
- else {
299
- throw new Error('Locator.click is not available');
268
+ if (block.offset) {
269
+ clickOptions.offset = block.offset;
270
+ }
271
+ if (typeof locator.click === 'function') {
272
+ await locator.click(clickOptions);
273
+ executedActionsInfo.push({
274
+ type: 'click',
275
+ success: true,
276
+ message: 'Click performed successfully',
277
+ });
278
+ actionType = 'click';
279
+ }
280
+ else {
281
+ throw new Error('Locator.click is not available');
282
+ }
300
283
  }
301
284
  }
285
+ catch (blockError) {
286
+ executedActionsInfo.push({
287
+ type: block.type,
288
+ success: false,
289
+ message: blockError instanceof Error ? blockError.message : String(blockError),
290
+ });
291
+ // Continue with next block even if one fails
292
+ }
302
293
  }
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.');
294
+ // Update actionType to reflect last executed action
295
+ if (executedActionsInfo.length > 0) {
296
+ const lastAction = executedActionsInfo[executedActionsInfo.length - 1];
297
+ actionType = lastAction.type;
306
298
  }
307
299
  }
308
300
  catch (error) {
@@ -370,14 +362,27 @@ class MoveAndClick {
370
362
  }
371
363
  }
372
364
  }
365
+ // Prepare return data
366
+ const returnJson = {
367
+ success: true,
368
+ selector,
369
+ method: clickMethod,
370
+ action: actionType || 'click',
371
+ };
372
+ // If we executed multiple blocks, include details
373
+ if (executedActionsInfo && executedActionsInfo.length > 0) {
374
+ const allSuccess = executedActionsInfo.every(a => a.success);
375
+ returnJson.success = allSuccess;
376
+ returnJson.message = `Executed ${executedActionsInfo.length} action(s)`;
377
+ returnJson.actions = executedActionsInfo;
378
+ returnJson.totalActions = executedActionsInfo.length;
379
+ returnJson.successfulActions = executedActionsInfo.filter(a => a.success).length;
380
+ }
381
+ else {
382
+ returnJson.message = actionType === 'fill' ? 'Fill performed successfully' : 'Click performed successfully';
383
+ }
373
384
  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
- },
385
+ json: returnJson,
381
386
  });
382
387
  }
383
388
  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.7",
4
4
  "description": "n8n nodes for managing Chrome browser profiles and page interactions",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",