n8n-nodes-nvk-browser 1.0.5 → 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.
@@ -22,7 +22,7 @@ exports.moveAndClickFields = [
22
22
  type: 'string',
23
23
  required: true,
24
24
  default: '',
25
- description: 'For Puppeteer: CSS selector, XPath (with >XPATH> prefix), or paste the full Puppeteer code from Chrome Recorder (export as "Puppeteer"). The node will automatically extract the first Locator.race().click() block. Examples: "textarea", ">XPATH>/html/body/div", or full code with puppeteer.Locator.race([...]).click(). For Javascript: CSS selector only.',
25
+ description: 'For Puppeteer: CSS selector, XPath (with >XPATH> prefix), or paste the full Puppeteer code from Chrome Recorder (export as "Puppeteer"). The node will automatically detect and execute .click() or .fill() actions from Locator.race() blocks. Examples: "textarea", ">XPATH>/html/body/div", or full code with puppeteer.Locator.race([...]).click()/.fill(). For Javascript: CSS selector only. Note: The node only executes click/fill actions, not goto() or other navigation actions.',
26
26
  typeOptions: {
27
27
  rows: 4,
28
28
  },
@@ -99,6 +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();
103
+ // Track action type for return value (default to click)
104
+ let actionType = 'click';
105
+ let executedActionsInfo = null;
102
106
  let instance = browserManager.getInstance(profileId);
103
107
  // Auto start profile nếu chưa chạy và option được bật
104
108
  if (!instance && autoStart) {
@@ -137,35 +141,21 @@ class MoveAndClick {
137
141
  const waitForClick = this.getNodeParameter('waitForClick', i) || 500;
138
142
  const button = this.getNodeParameter('button', i) || 'left';
139
143
  const clickCount = this.getNodeParameter('clickCount', i) || 1;
140
- // Parse selector - check if it's code with Locator.race() or simple selector
141
- const selectorTrimmed = selector.trim();
142
144
  // Check if selector contains Locator.race pattern or is a code block
143
145
  if (selectorTrimmed.includes('Locator.race') || selectorTrimmed.includes('puppeteer.Locator')) {
144
146
  // Execute as code - user provided full code
145
- // Extract the selector part from the code or execute the code directly
147
+ // Parse and execute ALL blocks automatically
146
148
  try {
147
- // Parse selectors from the code pattern
148
- const locators = [];
149
- // Find Locator.race blocks - prioritize blocks that end with .click()
150
- // Improved regex to handle multiline code from Chrome Recorder
151
- // Match pattern: puppeteer.Locator.race([...]).setTimeout(...).click(...)
152
- const clickBlockPattern = /puppeteer\.Locator\.race\s*\(\s*\[([\s\S]*?)\]\s*\)[\s\S]*?\.click\s*\(/;
153
- const clickBlockMatch = selector.match(clickBlockPattern);
154
- let raceBlockContent = null;
155
- if (clickBlockMatch && clickBlockMatch[1]) {
156
- raceBlockContent = clickBlockMatch[1];
157
- }
158
- else {
159
- // Fallback: find any Locator.race block (even without .click())
160
- const anyRaceBlockMatch = selector.match(/puppeteer\.Locator\.race\s*\(\s*\[([\s\S]*?)\]\s*\)/);
161
- if (anyRaceBlockMatch && anyRaceBlockMatch[1]) {
162
- raceBlockContent = anyRaceBlockMatch[1];
163
- }
164
- }
165
- if (raceBlockContent) {
166
- // Extract all locator calls from the race block
167
- // Handle both single and double quotes, and handle escaped quotes
168
- // Improved regex to handle complex selectors with parentheses and special characters
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 = [];
169
159
  const locatorPattern = /(?:targetPage|page)\.locator\((['"])((?:(?!\1)[^\\]|\\.)*)\1\)/g;
170
160
  let locatorMatch;
171
161
  while ((locatorMatch = locatorPattern.exec(raceBlockContent)) !== null) {
@@ -174,105 +164,137 @@ class MoveAndClick {
174
164
  locators.push(page.locator(selectorValue));
175
165
  }
176
166
  }
177
- }
178
- if (locators.length === 0) {
179
- // Fallback: Parse CSS selectors (lines with targetPage.locator('...') or page.locator('...'))
180
- const cssMatches = selector.match(/(?:targetPage|page)\.locator\(['"]([^'"]+)['"]\)/g);
181
- if (cssMatches) {
182
- cssMatches.forEach(match => {
183
- const cssSel = match.match(/['"]([^'"]+)['"]/)?.[1];
184
- if (cssSel && !cssSel.startsWith('::-p-xpath')) {
185
- // Use page.locator if available, otherwise fallback
186
- if (typeof page.locator === 'function') {
187
- locators.push(page.locator(cssSel));
188
- }
189
- }
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++,
190
182
  });
191
183
  }
192
- // Parse XPath selectors (::-p-xpath(...))
193
- const xpathMatches = selector.match(/::-p-xpath\(([^)]+)\)/g);
194
- if (xpathMatches) {
195
- xpathMatches.forEach(match => {
196
- const xpath = match.match(/::-p-xpath\(([^)]+)\)/)?.[1];
197
- if (xpath && typeof page.locator === 'function') {
198
- locators.push(page.locator(`::-p-xpath(${xpath})`));
199
- }
184
+ }
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 = [];
194
+ const locatorPattern = /(?:targetPage|page)\.locator\((['"])((?:(?!\1)[^\\]|\\.)*)\1\)/g;
195
+ let locatorMatch;
196
+ while ((locatorMatch = locatorPattern.exec(raceBlockContent)) !== null) {
197
+ const selectorValue = locatorMatch[2].replace(/\\(.)/g, '$1'); // Unescape
198
+ if (selectorValue && typeof page.locator === 'function') {
199
+ locators.push(page.locator(selectorValue));
200
+ }
201
+ }
202
+ if (locators.length > 0) {
203
+ actionBlocks.push({
204
+ type: 'fill',
205
+ locators,
206
+ fillText: fillTextValue,
207
+ index: fillMatch.index || fillIndex++,
200
208
  });
201
209
  }
202
210
  }
203
- // Also check for >CSS> and >XPATH> prefixes
204
- if (selector.includes('>CSS>') || selector.includes('>XPATH>')) {
205
- const parts = selector.split(/\s+/);
206
- parts.forEach(part => {
207
- if (part.startsWith('>CSS>') && typeof page.locator === 'function') {
208
- const cssSel = part.replace('>CSS>', '');
209
- locators.push(page.locator(cssSel));
210
- }
211
- else if (part.startsWith('>XPATH>') && typeof page.locator === 'function') {
212
- const xpath = part.replace('>XPATH>', '');
213
- locators.push(page.locator(`::-p-xpath(${xpath})`));
214
- }
215
- });
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.');
216
217
  }
217
- if (locators.length > 0 && typeof page.locator === 'function') {
218
- // Use Locator.race() if multiple locators and API is available
219
- let locator;
220
- if (locators.length > 1) {
221
- // Check if Locator.race is available
222
- const LocatorClass = puppeteer_core_1.default.Locator;
223
- if (LocatorClass && typeof LocatorClass.race === 'function') {
224
- locator = LocatorClass.race(locators);
225
- if (typeof locator.setTimeout === 'function') {
226
- 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
+ }
227
235
  }
228
236
  }
229
237
  else {
230
- // Fallback: try first locator
231
- locator = locators[0];
238
+ locator = block.locators[0];
232
239
  if (typeof locator.setTimeout === 'function') {
233
240
  locator = locator.setTimeout(timeout);
234
241
  }
235
242
  }
236
- }
237
- else {
238
- locator = locators[0];
239
- if (typeof locator.setTimeout === 'function') {
240
- locator = locator.setTimeout(timeout);
243
+ // Wait before action if specified
244
+ if (waitForClick > 0) {
245
+ await page.waitForTimeout(waitForClick);
246
+ }
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
+ }
261
+ }
262
+ else {
263
+ // Click action
264
+ const clickOptions = {
265
+ button: button,
266
+ clickCount: clickCount,
267
+ };
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
+ }
241
283
  }
242
284
  }
243
- // Wait before clicking if specified
244
- if (waitForClick > 0) {
245
- await page.waitForTimeout(waitForClick);
246
- }
247
- // Parse offset from code if present (handle multiline)
248
- let offset;
249
- // Match offset in click options, handling multiline and whitespace
250
- 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]*?\}/);
251
- if (offsetMatch && offsetMatch[1] && offsetMatch[2]) {
252
- offset = {
253
- x: parseInt(offsetMatch[1], 10),
254
- y: parseInt(offsetMatch[2], 10),
255
- };
256
- }
257
- // Click with options
258
- const clickOptions = {
259
- button: button,
260
- clickCount: clickCount,
261
- };
262
- // Add offset if found in code
263
- if (offset) {
264
- clickOptions.offset = offset;
265
- }
266
- if (typeof locator.click === 'function') {
267
- await locator.click(clickOptions);
268
- }
269
- else {
270
- throw new Error('Locator.click is not available');
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
271
292
  }
272
293
  }
273
- else {
274
- // Fallback to simple selector parsing
275
- 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;
276
298
  }
277
299
  }
278
300
  catch (error) {
@@ -340,13 +362,27 @@ class MoveAndClick {
340
362
  }
341
363
  }
342
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
+ }
343
384
  returnData.push({
344
- json: {
345
- success: true,
346
- selector,
347
- method: clickMethod,
348
- message: 'Click performed successfully',
349
- },
385
+ json: returnJson,
350
386
  });
351
387
  }
352
388
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-nvk-browser",
3
- "version": "1.0.5",
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",